Something went wrong. Try again.
Monorepo for Tangled tangled.org
Something went wrong. Try again.
Svelte
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131<script module lang="ts"> import { tv, type VariantProps } from "tailwind-variants";
export const avatarPicker = tv({ slots: { label: "flex size-40 cursor-pointer flex-col items-center justify-center gap-1 overflow-hidden rounded-full border border-border-default text-foreground-subtle transition-colors focus-within:outline-2 focus-within:outline-offset-2 focus-within:outline-border-focus hover:bg-background-subtle", image: "size-full rounded-full object-cover", placeholder: "flex flex-col items-center justify-center gap-1" } });
export type AvatarPickerVariants = VariantProps<typeof avatarPicker>;
export interface ProfileDraft { description: string; pronouns: string; website: string; avatarFile: File | null; avatarPreview?: string; }</script>
<script lang="ts"> import { prefersReducedMotion } from "svelte/motion"; import Camera from "$icon/camera"; import Field from "$lib/components/ui/Field.svelte"; import Input from "$lib/components/ui/Input.svelte"; import StepHeader from "$lib/components/welcome/StepHeader.svelte";
let { draft }: { draft: ProfileDraft } = $props();
const slot = avatarPicker(); const acceptedTypes: Record<string, true> = { "image/png": true, "image/jpeg": true }; const uid = $props.id(); const pictureErrorId = `${uid}-picture-error`; let pictureError = $state<string>();
const pick = (event: Event) => { const file = (event.currentTarget as HTMLInputElement).files?.[0]; if (!file) return; if (!acceptedTypes[file.type]) { pictureError = "Choose a PNG or JPEG image."; return; } if (file.size > 1_000_000) { pictureError = "Profile pictures must be 1 MB or smaller."; return; } if (draft.avatarPreview) URL.revokeObjectURL(draft.avatarPreview); draft.avatarFile = file; draft.avatarPreview = URL.createObjectURL(file); pictureError = undefined; };</script>
<div class="flex flex-col gap-6"> <StepHeader title="Welcome!" subtitle="Let's set up your profile. All fields are optional." />
<div class="flex flex-col gap-6 md:flex-row"> <div class="flex shrink-0 justify-center md:block"> <label class={slot.label()}> {#if draft.avatarPreview} <img src={draft.avatarPreview} alt="Your new avatar" class={slot.image()} class:avatar-preview-enter={!prefersReducedMotion.current} /> {:else} <span class={slot.placeholder()}> <Camera class="size-6" aria-hidden="true" /> <span class="typography-paragraph-small">Upload picture</span> </span> {/if} <input type="file" accept="image/png,image/jpeg" class="sr-only" aria-describedby={pictureErrorId} aria-invalid={pictureError ? "true" : undefined} onchange={pick} /> </label> <p id={pictureErrorId} role="status" class="mt-2 typography-paragraph-small text-foreground-danger" > {pictureError ?? ""} </p> </div>
<div class="flex min-w-0 flex-1 flex-col gap-4"> <Field label="Bio" labelCase="normal"> {#snippet children({ id })} <Input {id} placeholder="Tell us about yourself" bind:value={draft.description} /> {/snippet} </Field>
<Field label="Pronouns" labelCase="normal"> {#snippet children({ id })} <Input {id} placeholder="they/them" bind:value={draft.pronouns} /> {/snippet} </Field>
<Field label="Website" labelCase="normal"> {#snippet children({ id })} <Input {id} placeholder="example.com" bind:value={draft.website} /> {/snippet} </Field> </div> </div></div>
<style> .avatar-preview-enter { transition: opacity 220ms cubic-bezier(0.23, 1, 0.32, 1), scale 220ms cubic-bezier(0.23, 1, 0.32, 1);
@starting-style { opacity: 0; scale: 0.94; } }</style>