[READ-ONLY] Mirror of https://github.com/flo-bit/ui-kit. 🦊 fox ui, svelte 5 and tailwind 4 flo-bit.dev/ui-kit/
svelte tailwindcss ui-components
0

Configure Feed

Select the types of activity you want to include in your feed.

improve atproto login handle, remove advanced text editor from docs

Florian (Mar 25, 2026, 4:59 PM +0100) be3fc8d9 44f7acce

+468 -385
+5
.changeset/afraid-rules-hear.md
··· 1 + --- 2 + '@foxui/social': patch 3 + --- 4 + 5 + improve atproto login modal
+5
apps/docs/src/lib/later/advanced-text-area/Card.svelte
··· 1 + <script> 2 + import { AdvancedTextArea } from '@foxui/all'; 3 + </script> 4 + 5 + <AdvancedTextArea></AdvancedTextArea>
+19
apps/docs/src/lib/later/advanced-text-area/Documentation.md
··· 1 + ## Usage 2 + 3 + ```svelte 4 + <script lang="ts"> 5 + import { AdvancedTextArea } from '@foxui/text'; 6 + 7 + let content = $state(''); 8 + 9 + const handleSubmit = (e: Event) => { 10 + e.preventDefault(); 11 + console.log(content); 12 + }; 13 + </script> 14 + 15 + <form onsubmit={handleSubmit}> 16 + <AdvancedTextArea bind:value={content} placeholder="Write something..." /> 17 + </form> 18 + 19 + ```
+67
apps/docs/src/lib/later/advanced-text-area/Example.svelte
··· 1 + <script lang="ts"> 2 + import { Box, Button, toast } from '@foxui/all'; 3 + import { AdvancedTextArea } from '@foxui/all'; 4 + 5 + let content = $state(''); 6 + 7 + function handleSubmit(e: Event) { 8 + if (!content) { 9 + toast.error('Please enter a post'); 10 + return; 11 + } 12 + content = ''; 13 + 14 + e.preventDefault(); 15 + toast.success('Post submitted'); 16 + } 17 + </script> 18 + 19 + <form class="not-prose" onsubmit={handleSubmit}> 20 + <AdvancedTextArea bind:value={content}> 21 + {#snippet actionButtons()} 22 + <Button 23 + variant="ghost" 24 + size="icon" 25 + onclick={() => { 26 + toast('Sorry, I was too lazy to actually add this feature to this demo'); 27 + }} 28 + > 29 + <svg 30 + xmlns="http://www.w3.org/2000/svg" 31 + fill="none" 32 + viewBox="0 0 24 24" 33 + stroke-width="1.5" 34 + stroke="currentColor" 35 + > 36 + <path 37 + stroke-linecap="round" 38 + stroke-linejoin="round" 39 + d="m2.25 15.75 5.159-5.159a2.25 2.25 0 0 1 3.182 0l5.159 5.159m-1.5-1.5 1.409-1.409a2.25 2.25 0 0 1 3.182 0l2.909 2.909m-18 3.75h16.5a1.5 1.5 0 0 0 1.5-1.5V6a1.5 1.5 0 0 0-1.5-1.5H3.75A1.5 1.5 0 0 0 2.25 6v12a1.5 1.5 0 0 0 1.5 1.5Zm10.5-11.25h.008v.008h-.008V8.25Zm.375 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Z" 40 + /> 41 + </svg> 42 + </Button> 43 + <Button 44 + variant="ghost" 45 + size="icon" 46 + onclick={() => { 47 + toast('Sorry, I was too lazy to actually add this feature to this demo'); 48 + }} 49 + > 50 + <svg 51 + xmlns="http://www.w3.org/2000/svg" 52 + fill="none" 53 + viewBox="0 0 24 24" 54 + stroke-width="1.5" 55 + stroke="currentColor" 56 + class="size-6" 57 + > 58 + <path 59 + stroke-linecap="round" 60 + stroke-linejoin="round" 61 + d="M15.182 15.182a4.5 4.5 0 0 1-6.364 0M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0ZM9.75 9.75c0 .414-.168.75-.375.75S9 10.164 9 9.75 9.168 9 9.375 9s.375.336.375.75Zm-.375 0h.008v.015h-.008V9.75Zm5.625 0c0 .414-.168.75-.375.75s-.375-.336-.375-.75.168-.75.375-.75.375.336.375.75Zm-.375 0h.008v.015h-.008V9.75Z" 62 + /> 63 + </svg> 64 + </Button> 65 + {/snippet} 66 + </AdvancedTextArea> 67 + </form>
+38
apps/docs/src/lib/later/advanced-text-area/api.ts
··· 1 + import type { APISchema } from '$lib/types/schema'; 2 + 3 + export default [ 4 + { 5 + title: 'AdvancedTextArea', 6 + description: 'A textarea with optional additional content area and action buttons, suitable for composing posts or messages.', 7 + props: { 8 + value: { 9 + type: 'string', 10 + description: 'The current text value.', 11 + bindable: true 12 + }, 13 + placeholder: { 14 + type: 'string', 15 + description: 'Placeholder text.', 16 + default: "'Write something here...'" 17 + }, 18 + rows: { 19 + type: 'number', 20 + description: 'The number of visible text rows.', 21 + default: '3' 22 + }, 23 + additionalContent: { 24 + type: 'Snippet', 25 + description: 'Additional content to display below the textarea (e.g. media previews).' 26 + }, 27 + actionButtons: { 28 + type: 'Snippet', 29 + description: 'Action buttons displayed in the bottom-left area (e.g. attach, emoji).' 30 + }, 31 + submitButton: { 32 + type: { type: 'union', definition: "Snippet | string | null" }, 33 + description: 'The submit button. Can be a string label, a custom snippet, or null to hide.', 34 + default: "'Post'" 35 + } 36 + } 37 + } 38 + ] satisfies APISchema[];
+13
apps/docs/src/lib/later/advanced-text-area/index.ts
··· 1 + import Docs from './Documentation.md'; 2 + import Example from './Example.svelte'; 3 + import Card from './Card.svelte'; 4 + import api from './api'; 5 + 6 + export default { 7 + slug: 'advanced-text-area', 8 + title: 'Advanced Text Area', 9 + docs: Docs, 10 + example: Example, 11 + card: Card, 12 + api, 13 + };
+217
packages/social/src/lib/components/atproto-login/AtprotoLogin.svelte
··· 1 + <script lang="ts"> 2 + import { Button, Subheading, Avatar, cn } from '@foxui/core'; 3 + import { AtprotoHandlePopup } from '../atproto-handle-popup'; 4 + import type { Profile } from '../atproto-handle-popup/helper'; 5 + import { type ATProtoLoginProps } from '.'; 6 + import { onMount, tick } from 'svelte'; 7 + import type { HTMLFormAttributes } from 'svelte/elements'; 8 + 9 + let value = $state(''); 10 + let error: string | null = $state(null); 11 + let loading = $state(false); 12 + 13 + let { 14 + login, 15 + signup, 16 + formAction, 17 + formMethod = 'get', 18 + loginOnSelect = true, 19 + class: className, 20 + ...rest 21 + }: ATProtoLoginProps & { 22 + loginOnSelect?: boolean; 23 + class?: string; 24 + } & Omit<HTMLFormAttributes, 'onsubmit' | 'action' | 'method'> = $props(); 25 + 26 + let selectedActor: Profile | undefined = $state(); 27 + let recentLogins: Record<string, Profile> = $state({}); 28 + let recentLoginsView = $state(true); 29 + let input: HTMLInputElement | null = $state(null); 30 + let submitButton: HTMLButtonElement | null = $state(null); 31 + 32 + let showRecentLogins = $derived( 33 + Object.keys(recentLogins).length > 0 && !loading && !selectedActor && recentLoginsView 34 + ); 35 + 36 + onMount(() => { 37 + try { 38 + recentLogins = JSON.parse(localStorage.getItem('recent-logins') || '{}'); 39 + } catch { 40 + // ignore parse errors 41 + } 42 + }); 43 + 44 + function focusInput() { 45 + tick().then(() => input?.focus()); 46 + } 47 + 48 + function focusSubmit() { 49 + tick().then(() => submitButton?.focus()); 50 + } 51 + 52 + function removeRecentLogin(did: string) { 53 + // eslint-disable-next-line @typescript-eslint/no-unused-vars 54 + const { [did]: _, ...rest } = recentLogins; 55 + recentLogins = rest; 56 + try { 57 + localStorage.setItem('recent-logins', JSON.stringify(recentLogins)); 58 + } catch { 59 + // ignore storage errors 60 + } 61 + } 62 + 63 + async function onLogin(handle: string) { 64 + if (loading || !login) return; 65 + loading = true; 66 + error = null; 67 + try { 68 + await login(handle); 69 + } catch (err) { 70 + error = err instanceof Error ? err.message : String(err); 71 + } finally { 72 + loading = false; 73 + } 74 + } 75 + 76 + async function onSubmit(evt?: Event) { 77 + if (formAction || !login) return; 78 + evt?.preventDefault(); 79 + await onLogin(value); 80 + } 81 + 82 + function handleActorSelected(actor: Profile) { 83 + selectedActor = actor; 84 + value = actor.handle; 85 + 86 + if (loginOnSelect) onSubmit(); 87 + else focusSubmit(); 88 + } 89 + </script> 90 + 91 + <form 92 + onsubmit={(e) => onSubmit(e)} 93 + action={formAction} 94 + method={formMethod} 95 + class={cn('flex flex-col gap-2', className)} 96 + {...rest} 97 + > 98 + <Subheading class="inline-flex items-center gap-2 font-bold"> 99 + Login with your internet handle 100 + </Subheading> 101 + <div class="mt-1 text-xs font-light text-neutral-800 dark:text-neutral-200"> 102 + like your bluesky account 103 + </div> 104 + 105 + {#if showRecentLogins} 106 + <div class="mt-2 mb-2 text-sm font-medium">Recent logins</div> 107 + 108 + <div class="flex flex-col gap-2"> 109 + {#each Object.values(recentLogins) 110 + .filter((l) => l.handle && l.handle !== 'handle.invalid') 111 + .slice(0, 4) as recentLogin (recentLogin.did)} 112 + <div 113 + class="bg-base-100 hover:bg-base-200 dark:bg-base-700 dark:hover:bg-base-600 border-base-300 dark:border-base-600 group relative flex h-10 w-full items-center justify-between gap-2 rounded-full border px-3 font-semibold transition-colors duration-100" 114 + > 115 + <div class="flex items-center gap-2"> 116 + <Avatar src={recentLogin.avatar} class="size-6" /> 117 + {recentLogin.handle} 118 + </div> 119 + <button 120 + type="button" 121 + class="absolute inset-0 z-0 cursor-pointer" 122 + onclick={() => handleActorSelected(recentLogin)} 123 + > 124 + <span class="sr-only">login as {recentLogin.handle}</span> 125 + </button> 126 + <button 127 + type="button" 128 + class="relative z-10 cursor-pointer rounded-full p-0.5" 129 + onclick={(e) => { 130 + e.stopPropagation(); 131 + removeRecentLogin(recentLogin.did); 132 + }} 133 + > 134 + <svg 135 + xmlns="http://www.w3.org/2000/svg" 136 + fill="none" 137 + viewBox="0 0 24 24" 138 + stroke-width="1.5" 139 + stroke="currentColor" 140 + class="size-3" 141 + > 142 + <path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12" /> 143 + </svg> 144 + <span class="sr-only">remove</span> 145 + </button> 146 + </div> 147 + {/each} 148 + </div> 149 + {:else if !selectedActor} 150 + <div class="mt-4 w-full"> 151 + <AtprotoHandlePopup bind:value onselected={handleActorSelected} bind:ref={input} /> 152 + </div> 153 + {:else} 154 + <div 155 + class="bg-base-100 dark:bg-base-700 border-base-300 dark:border-base-600 mt-4 flex h-10 w-full items-center justify-between gap-2 rounded-full border px-3 font-semibold" 156 + > 157 + <div class="flex items-center gap-2"> 158 + <Avatar src={selectedActor.avatar} class="size-6" /> 159 + {selectedActor.handle} 160 + </div> 161 + <button 162 + type="button" 163 + class="cursor-pointer rounded-full p-0.5" 164 + onclick={() => { 165 + selectedActor = undefined; 166 + value = ''; 167 + focusInput(); 168 + }} 169 + > 170 + <svg 171 + xmlns="http://www.w3.org/2000/svg" 172 + fill="none" 173 + viewBox="0 0 24 24" 174 + stroke-width="1.5" 175 + stroke="currentColor" 176 + class="size-3" 177 + > 178 + <path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12" /> 179 + </svg> 180 + <span class="sr-only">clear selection</span> 181 + </button> 182 + </div> 183 + {/if} 184 + 185 + {#if error} 186 + <p class="text-accent-500 mt-2 text-sm font-medium">{error}</p> 187 + {/if} 188 + 189 + <div class="mt-2"> 190 + {#if showRecentLogins} 191 + <div class="mt-2 mb-4 text-sm font-medium">Or login with new handle</div> 192 + 193 + <Button 194 + onclick={() => { 195 + recentLoginsView = false; 196 + focusInput(); 197 + }} 198 + class="w-full" 199 + > 200 + Login with new handle 201 + </Button> 202 + {:else} 203 + <Button bind:ref={submitButton} type="submit" class="w-full" disabled={loading}> 204 + {loading ? 'Loading...' : 'Login'} 205 + </Button> 206 + {/if} 207 + </div> 208 + 209 + {#if signup} 210 + <div 211 + class="border-base-200 dark:border-base-800 text-base-800 dark:text-base-200 mt-2 flex flex-col gap-3 border-t pt-2 text-sm leading-7" 212 + > 213 + <span class="text-sm font-medium">Don't have an account?</span> 214 + <Button onclick={() => signup?.()} variant="secondary">Sign Up</Button> 215 + </div> 216 + {/if} 217 + </form>
+21 -228
packages/social/src/lib/components/atproto-login/AtprotoLoginModal.svelte
··· 1 - <script lang="ts" module> 2 - export const atProtoLoginModalState = $state({ 3 - open: false, 4 - show: () => (atProtoLoginModalState.open = true), 5 - hide: () => (atProtoLoginModalState.open = false) 6 - }); 7 - </script> 8 - 9 1 <script lang="ts"> 10 - import { Button, Modal, Subheading, Avatar } from '@foxui/core'; 11 - import { AtprotoHandlePopup } from '../atproto-handle-popup'; 12 - import type { Profile } from '../atproto-handle-popup/helper'; 13 - import { saveRecentLogin, type ATProtoLoginProps } from '.'; 14 - import { onMount, tick } from 'svelte'; 15 - 16 - let value = $state(''); 17 - let error: string | null = $state(null); 18 - let loading = $state(false); 2 + import { Modal } from '@foxui/core'; 3 + import AtprotoLogin from './AtprotoLogin.svelte'; 4 + import type { ATProtoLoginProps } from '.'; 19 5 20 6 let { 7 + open = $bindable(false), 21 8 login, 22 9 signup, 23 10 formAction, 24 11 formMethod = 'get', 25 12 loginOnSelect = true 26 - }: ATProtoLoginProps & { loginOnSelect?: boolean } = $props(); 13 + }: ATProtoLoginProps & { open?: boolean; loginOnSelect?: boolean } = $props(); 27 14 28 - let selectedActor: Profile | undefined = $state(); 29 - let recentLogins: Record<string, Profile> = $state({}); 30 - let recentLoginsView = $state(true); 31 - let input: HTMLInputElement | null = $state(null); 32 - let submitButton: HTMLButtonElement | null = $state(null); 33 - 34 - let showRecentLogins = $derived( 35 - Object.keys(recentLogins).length > 0 && !loading && !selectedActor && recentLoginsView 36 - ); 37 - 38 - onMount(() => { 39 - try { 40 - recentLogins = JSON.parse(localStorage.getItem('recent-logins') || '{}'); 41 - } catch { 42 - // ignore parse errors 15 + async function wrappedLogin(handle: string) { 16 + if (!login) return; 17 + const result = await login(handle); 18 + if (result) { 19 + open = false; 43 20 } 44 - }); 45 - 46 - $effect(() => { 47 - if (!atProtoLoginModalState.open) { 48 - error = null; 49 - value = ''; 50 - loading = false; 51 - selectedActor = undefined; 52 - recentLoginsView = true; 53 - } else { 54 - focusInput(); 55 - } 56 - }); 57 - 58 - function focusInput() { 59 - tick().then(() => input?.focus()); 60 - } 61 - 62 - function focusSubmit() { 63 - tick().then(() => submitButton?.focus()); 64 - } 65 - 66 - function removeRecentLogin(did: string) { 67 - const { [did]: _, ...rest } = recentLogins; 68 - recentLogins = rest; 69 - try { 70 - localStorage.setItem('recent-logins', JSON.stringify(recentLogins)); 71 - } catch { 72 - // ignore storage errors 73 - } 74 - } 75 - 76 - async function onLogin(handle: string) { 77 - if (loading || !login) return; 78 - loading = true; 79 - error = null; 80 - try { 81 - const hide = await login(handle); 82 - if (hide) { 83 - atProtoLoginModalState.hide(); 84 - value = ''; 85 - } 86 - } catch (err) { 87 - error = err instanceof Error ? err.message : String(err); 88 - } finally { 89 - loading = false; 90 - } 91 - } 92 - 93 - async function onSubmit(evt?: Event) { 94 - if (formAction || !login) return; 95 - evt?.preventDefault(); 96 - await onLogin(value); 97 - } 98 - 99 - function handleActorSelected(actor: Profile) { 100 - selectedActor = actor; 101 - value = actor.handle; 102 - 103 - if (loginOnSelect) onSubmit(); 104 - else focusSubmit(); 21 + return result; 105 22 } 106 23 </script> 107 24 108 - <Modal 109 - bind:open={atProtoLoginModalState.open} 110 - class="max-w-sm gap-2 p-4 sm:p-6" 111 - onOpenAutoFocus={(e: Event) => { 112 - e.preventDefault(); 113 - input?.focus(); 114 - }} 115 - > 116 - <form 117 - onsubmit={(e) => onSubmit(e)} 118 - action={formAction} 119 - method={formMethod} 120 - class="flex flex-col gap-2" 121 - > 122 - <Subheading class="inline-flex items-center gap-2 font-bold"> 123 - Login with your internet handle 124 - </Subheading> 125 - <div class="mt-1 text-xs font-light text-neutral-800 dark:text-neutral-200"> 126 - like your bluesky account 127 - </div> 128 - 129 - {#if showRecentLogins} 130 - <div class="mt-2 mb-2 text-sm font-medium">Recent logins</div> 131 - 132 - <div class="flex flex-col gap-2"> 133 - {#each Object.values(recentLogins) 134 - .filter((l) => l.handle && l.handle !== 'handle.invalid') 135 - .slice(0, 4) as recentLogin (recentLogin.did)} 136 - <div 137 - class="bg-base-100 hover:bg-base-200 dark:bg-base-700 dark:hover:bg-base-600 border-base-300 dark:border-base-600 group relative flex h-10 w-full items-center justify-between gap-2 rounded-full border px-3 font-semibold transition-colors duration-100" 138 - > 139 - <div class="flex items-center gap-2"> 140 - <Avatar src={recentLogin.avatar} class="size-6" /> 141 - {recentLogin.handle} 142 - </div> 143 - <button 144 - type="button" 145 - class="absolute inset-0 z-0 cursor-pointer" 146 - onclick={() => handleActorSelected(recentLogin)} 147 - > 148 - <span class="sr-only">login as {recentLogin.handle}</span> 149 - </button> 150 - <button 151 - type="button" 152 - class="relative z-10 cursor-pointer rounded-full p-0.5" 153 - onclick={(e) => { 154 - e.stopPropagation(); 155 - removeRecentLogin(recentLogin.did); 156 - }} 157 - > 158 - <svg 159 - xmlns="http://www.w3.org/2000/svg" 160 - fill="none" 161 - viewBox="0 0 24 24" 162 - stroke-width="1.5" 163 - stroke="currentColor" 164 - class="size-3" 165 - > 166 - <path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12" /> 167 - </svg> 168 - <span class="sr-only">remove</span> 169 - </button> 170 - </div> 171 - {/each} 172 - </div> 173 - {:else if !selectedActor} 174 - <div class="mt-4 w-full"> 175 - <AtprotoHandlePopup bind:value onselected={handleActorSelected} bind:ref={input} /> 176 - </div> 177 - {:else} 178 - <div 179 - class="bg-base-100 dark:bg-base-700 border-base-300 dark:border-base-600 mt-4 flex h-10 w-full items-center justify-between gap-2 rounded-full border px-3 font-semibold" 180 - > 181 - <div class="flex items-center gap-2"> 182 - <Avatar src={selectedActor.avatar} class="size-6" /> 183 - {selectedActor.handle} 184 - </div> 185 - <button 186 - type="button" 187 - class="cursor-pointer rounded-full p-0.5" 188 - onclick={() => { 189 - selectedActor = undefined; 190 - value = ''; 191 - focusInput(); 192 - }} 193 - > 194 - <svg 195 - xmlns="http://www.w3.org/2000/svg" 196 - fill="none" 197 - viewBox="0 0 24 24" 198 - stroke-width="1.5" 199 - stroke="currentColor" 200 - class="size-3" 201 - > 202 - <path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12" /> 203 - </svg> 204 - <span class="sr-only">clear selection</span> 205 - </button> 206 - </div> 207 - {/if} 208 - 209 - {#if error} 210 - <p class="text-accent-500 mt-2 text-sm font-medium">{error}</p> 211 - {/if} 212 - 213 - <div class="mt-2"> 214 - {#if showRecentLogins} 215 - <div class="mt-2 mb-4 text-sm font-medium">Or login with new handle</div> 216 - 217 - <Button 218 - onclick={() => { 219 - recentLoginsView = false; 220 - focusInput(); 221 - }} 222 - class="w-full" 223 - > 224 - Login with new handle 225 - </Button> 226 - {:else} 227 - <Button bind:ref={submitButton} type="submit" class="w-full" disabled={loading}> 228 - {loading ? 'Loading...' : 'Login'} 229 - </Button> 230 - {/if} 231 - </div> 232 - 233 - {#if signup} 234 - <div 235 - class="border-base-200 dark:border-base-800 text-base-800 dark:text-base-200 mt-2 flex flex-col gap-3 border-t text-sm leading-7 pt-2" 236 - > 237 - <span class="text-sm font-medium">Don't have an account?</span> 238 - <Button onclick={() => signup?.()} variant="secondary">Sign Up</Button> 239 - </div> 240 - {/if} 241 - </form> 25 + <Modal bind:open class="max-w-sm gap-2 p-4 sm:p-6"> 26 + {#key open} 27 + <AtprotoLogin 28 + login={wrappedLogin} 29 + {signup} 30 + {formAction} 31 + {formMethod} 32 + {loginOnSelect} 33 + /> 34 + {/key} 242 35 </Modal>
+1 -1
packages/social/src/lib/components/atproto-login/index.ts
··· 1 + export { default as AtprotoLogin } from './AtprotoLogin.svelte'; 1 2 export { default as AtprotoLoginModal } from './AtprotoLoginModal.svelte'; 2 - export { atProtoLoginModalState } from './AtprotoLoginModal.svelte'; 3 3 4 4 export type ATProtoLoginProps = { 5 5 login?: (handle: string) => Promise<boolean | undefined>;
+19 -2
apps/docs/src/lib/docs/social/atproto-login/Documentation.md
··· 3 3 ### Add modal to your `+layout.svelte` 4 4 5 5 ```svelte 6 + <script> 7 + let open = $state(false); 8 + </script> 9 + 6 10 <AtprotoLoginModal 11 + bind:open 7 12 login={async (handle) => { 8 13 // do login and return true if login 9 14 // is successful and should hide the modal ··· 18 23 ### Open the modal 19 24 20 25 ```svelte 21 - <Button onclick={() => atProtoLoginModalState.show()}>Login</Button> 22 - ``` 26 + <Button onclick={() => (open = true)}>Login</Button> 27 + ``` 28 + 29 + ### Inline (non-modal) version 30 + 31 + Use `AtprotoLogin` to embed the login form directly in a page: 32 + 33 + ```svelte 34 + <AtprotoLogin 35 + login={async (handle) => { 36 + // do login 37 + }} 38 + /> 39 + ```
+24 -11
apps/docs/src/lib/docs/social/atproto-login/Example.svelte
··· 1 1 <script lang="ts"> 2 - import { AtprotoLoginModal, atProtoLoginModalState, Button, toast } from '@foxui/all'; 3 - </script> 2 + import { AtprotoLogin, AtprotoLoginModal, Button, toast, Box } from '@foxui/all'; 4 3 4 + let open = $state(false); 5 5 6 - 7 - <Button onclick={() => atProtoLoginModalState.show()}> 8 - Login 9 - </Button> 10 - 11 - <AtprotoLoginModal login={async (handle: string) => { 6 + async function login(handle: string) { 12 7 if (!handle) { 13 8 toast.error('Please enter a handle'); 14 9 return false; ··· 19 14 } 20 15 toast.success(`Login successful for ${handle}`); 21 16 return true; 22 - }} signup={async () => { 17 + } 18 + 19 + async function signup() { 23 20 toast.success('Signup successful'); 24 - atProtoLoginModalState.hide(); 21 + open = false; 25 22 return true; 26 - }} /> 23 + } 24 + </script> 25 + 26 + <div class="flex flex-col"> 27 + <div> 28 + <h3>Modal version</h3> 29 + <Button onclick={() => (open = true)}>Open login modal</Button> 30 + </div> 31 + <div class="pt-4"> 32 + <h3>Inline version</h3> 33 + <Box class="not-prose max-w-sm"> 34 + <AtprotoLogin {login} {signup} /> 35 + </Box> 36 + </div> 37 + </div> 38 + 39 + <AtprotoLoginModal bind:open {login} {signup} />
+39 -1
apps/docs/src/lib/docs/social/atproto-login/api.ts
··· 3 3 export default [ 4 4 { 5 5 title: 'AtprotoLoginModal', 6 - description: 'A modal dialog for AT Protocol (Bluesky) login with handle autocomplete. Export atProtoLoginModalState to control the modal.', 6 + description: 'A modal dialog for AT Protocol (Bluesky) login with handle autocomplete.', 7 + props: { 8 + open: { 9 + type: 'boolean', 10 + description: 'Whether the modal is open.', 11 + default: 'false', 12 + bindable: true 13 + }, 14 + login: { 15 + type: { type: 'function', definition: '(handle: string) => Promise<boolean | undefined>' }, 16 + description: 'Callback to handle the login action. Should return true on success.' 17 + }, 18 + signup: { 19 + type: { type: 'function', definition: '() => Promise<boolean | undefined>' }, 20 + description: 'Callback to handle the signup action.' 21 + }, 22 + formAction: { 23 + type: 'string', 24 + description: 'The form action URL for server-side form submission.' 25 + }, 26 + formMethod: { 27 + type: { type: 'enum', definition: "'dialog' | 'get' | 'post'" }, 28 + description: 'The form submission method.', 29 + default: "'get'" 30 + }, 31 + loginOnSelect: { 32 + type: 'boolean', 33 + description: 'Whether to automatically trigger login when a handle is selected from autocomplete.', 34 + default: 'true' 35 + } 36 + } 37 + }, 38 + { 39 + title: 'AtprotoLogin', 40 + description: 'An inline AT Protocol (Bluesky) login form with handle autocomplete. Use this to embed login directly in a page.', 7 41 props: { 8 42 login: { 9 43 type: { type: 'function', definition: '(handle: string) => Promise<boolean | undefined>' }, ··· 26 60 type: 'boolean', 27 61 description: 'Whether to automatically trigger login when a handle is selected from autocomplete.', 28 62 default: 'true' 63 + }, 64 + class: { 65 + type: 'string', 66 + description: 'Additional CSS classes for the form element.' 29 67 } 30 68 } 31 69 }
-5
apps/docs/src/lib/docs/text/advanced-text-area/Card.svelte
··· 1 - <script> 2 - import { AdvancedTextArea } from '@foxui/all'; 3 - </script> 4 - 5 - <AdvancedTextArea></AdvancedTextArea>
-19
apps/docs/src/lib/docs/text/advanced-text-area/Documentation.md
··· 1 - ## Usage 2 - 3 - ```svelte 4 - <script lang="ts"> 5 - import { AdvancedTextArea } from '@foxui/text'; 6 - 7 - let content = $state(''); 8 - 9 - const handleSubmit = (e: Event) => { 10 - e.preventDefault(); 11 - console.log(content); 12 - }; 13 - </script> 14 - 15 - <form onsubmit={handleSubmit}> 16 - <AdvancedTextArea bind:value={content} placeholder="Write something..." /> 17 - </form> 18 - 19 - ```
-67
apps/docs/src/lib/docs/text/advanced-text-area/Example.svelte
··· 1 - <script lang="ts"> 2 - import { Box, Button, toast } from '@foxui/all'; 3 - import { AdvancedTextArea } from '@foxui/all'; 4 - 5 - let content = $state(''); 6 - 7 - function handleSubmit(e: Event) { 8 - if (!content) { 9 - toast.error('Please enter a post'); 10 - return; 11 - } 12 - content = ''; 13 - 14 - e.preventDefault(); 15 - toast.success('Post submitted'); 16 - } 17 - </script> 18 - 19 - <form class="not-prose" onsubmit={handleSubmit}> 20 - <AdvancedTextArea bind:value={content}> 21 - {#snippet actionButtons()} 22 - <Button 23 - variant="ghost" 24 - size="icon" 25 - onclick={() => { 26 - toast('Sorry, I was too lazy to actually add this feature to this demo'); 27 - }} 28 - > 29 - <svg 30 - xmlns="http://www.w3.org/2000/svg" 31 - fill="none" 32 - viewBox="0 0 24 24" 33 - stroke-width="1.5" 34 - stroke="currentColor" 35 - > 36 - <path 37 - stroke-linecap="round" 38 - stroke-linejoin="round" 39 - d="m2.25 15.75 5.159-5.159a2.25 2.25 0 0 1 3.182 0l5.159 5.159m-1.5-1.5 1.409-1.409a2.25 2.25 0 0 1 3.182 0l2.909 2.909m-18 3.75h16.5a1.5 1.5 0 0 0 1.5-1.5V6a1.5 1.5 0 0 0-1.5-1.5H3.75A1.5 1.5 0 0 0 2.25 6v12a1.5 1.5 0 0 0 1.5 1.5Zm10.5-11.25h.008v.008h-.008V8.25Zm.375 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Z" 40 - /> 41 - </svg> 42 - </Button> 43 - <Button 44 - variant="ghost" 45 - size="icon" 46 - onclick={() => { 47 - toast('Sorry, I was too lazy to actually add this feature to this demo'); 48 - }} 49 - > 50 - <svg 51 - xmlns="http://www.w3.org/2000/svg" 52 - fill="none" 53 - viewBox="0 0 24 24" 54 - stroke-width="1.5" 55 - stroke="currentColor" 56 - class="size-6" 57 - > 58 - <path 59 - stroke-linecap="round" 60 - stroke-linejoin="round" 61 - d="M15.182 15.182a4.5 4.5 0 0 1-6.364 0M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0ZM9.75 9.75c0 .414-.168.75-.375.75S9 10.164 9 9.75 9.168 9 9.375 9s.375.336.375.75Zm-.375 0h.008v.015h-.008V9.75Zm5.625 0c0 .414-.168.75-.375.75s-.375-.336-.375-.75.168-.75.375-.75.375.336.375.75Zm-.375 0h.008v.015h-.008V9.75Z" 62 - /> 63 - </svg> 64 - </Button> 65 - {/snippet} 66 - </AdvancedTextArea> 67 - </form>
-38
apps/docs/src/lib/docs/text/advanced-text-area/api.ts
··· 1 - import type { APISchema } from '$lib/types/schema'; 2 - 3 - export default [ 4 - { 5 - title: 'AdvancedTextArea', 6 - description: 'A textarea with optional additional content area and action buttons, suitable for composing posts or messages.', 7 - props: { 8 - value: { 9 - type: 'string', 10 - description: 'The current text value.', 11 - bindable: true 12 - }, 13 - placeholder: { 14 - type: 'string', 15 - description: 'Placeholder text.', 16 - default: "'Write something here...'" 17 - }, 18 - rows: { 19 - type: 'number', 20 - description: 'The number of visible text rows.', 21 - default: '3' 22 - }, 23 - additionalContent: { 24 - type: 'Snippet', 25 - description: 'Additional content to display below the textarea (e.g. media previews).' 26 - }, 27 - actionButtons: { 28 - type: 'Snippet', 29 - description: 'Action buttons displayed in the bottom-left area (e.g. attach, emoji).' 30 - }, 31 - submitButton: { 32 - type: { type: 'union', definition: "Snippet | string | null" }, 33 - description: 'The submit button. Can be a string label, a custom snippet, or null to hide.', 34 - default: "'Post'" 35 - } 36 - } 37 - } 38 - ] satisfies APISchema[];
-13
apps/docs/src/lib/docs/text/advanced-text-area/index.ts
··· 1 - import Docs from './Documentation.md'; 2 - import Example from './Example.svelte'; 3 - import Card from './Card.svelte'; 4 - import api from './api'; 5 - 6 - export default { 7 - slug: 'advanced-text-area', 8 - title: 'Advanced Text Area', 9 - docs: Docs, 10 - example: Example, 11 - card: Card, 12 - api, 13 - };