Something went wrong. Try again.
Monorepo for Tangled forked from tangled.org/core
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156<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-2xs 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-2xs" } });
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 { PROFILE_LIMITS } from "#lib/api/profile.ts"; 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, websiteError }: { draft: ProfileDraft; websiteError?: string } = $props();
const slot = avatarPicker(); const acceptedTypes: Record<string, true> = { "image/png": true, "image/jpeg": true }; const uid = $props.id(); const pictureErrorId = `${uid}-picture-error`; const websiteErrorId = `${uid}-website-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-xl"> <StepHeader title="Welcome!" subtitle="Let's set up your profile. All fields are optional." />
<div class="flex flex-col gap-xl 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 data-testid="welcomeAvatarInput" 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-xs text-center typography-paragraph-small {pictureError ? 'text-foreground-danger' : 'text-foreground-subtle'}" > {pictureError ?? "PNG or JPEG, up to 1 MB."} </p> </div>
<div class="flex min-w-0 flex-1 flex-col gap-md"> <Field label="Bio" labelCase="normal"> {#snippet children({ id })} <Input {id} placeholder="Tell us about yourself" maxlength={PROFILE_LIMITS.description} bind:value={draft.description} /> {/snippet} </Field>
<Field label="Pronouns" labelCase="normal"> {#snippet children({ id })} <Input {id} placeholder="they/them" maxlength={PROFILE_LIMITS.pronouns} bind:value={draft.pronouns} /> {/snippet} </Field>
<Field label="Website" labelCase="normal"> {#snippet children({ id })} <Input {id} placeholder="example.com" error={!!websiteError} aria-describedby={websiteError ? websiteErrorId : undefined} bind:value={draft.website} /> {#if websiteError} <p id={websiteErrorId} class="typography-paragraph-small text-foreground-danger" > {websiteError} </p> {/if} {/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>