@eifi1/ui-kit 0.29.0 → 0.30.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -4
- package/dist/account/account-parts.d.ts +21 -0
- package/dist/account/account-parts.js +40 -0
- package/dist/account/account-parts.js.map +1 -0
- package/dist/account/data-export-setting.d.ts +73 -0
- package/dist/account/data-export-setting.js +113 -0
- package/dist/account/data-export-setting.js.map +1 -0
- package/dist/account/delete-account-setting.d.ts +91 -0
- package/dist/account/delete-account-setting.js +199 -0
- package/dist/account/delete-account-setting.js.map +1 -0
- package/dist/account/email-change-setting.d.ts +136 -0
- package/dist/account/email-change-setting.js +277 -0
- package/dist/account/email-change-setting.js.map +1 -0
- package/dist/account/sessions-setting.d.ts +110 -0
- package/dist/account/sessions-setting.js +146 -0
- package/dist/account/sessions-setting.js.map +1 -0
- package/dist/admin/admin-action-confirm.d.ts +91 -0
- package/dist/admin/admin-action-confirm.js +276 -0
- package/dist/admin/admin-action-confirm.js.map +1 -0
- package/dist/admin/admin-action-log.d.ts +105 -0
- package/dist/admin/admin-action-log.js +153 -0
- package/dist/admin/admin-action-log.js.map +1 -0
- package/dist/admin/admin-parts.d.ts +40 -0
- package/dist/admin/admin-parts.js +45 -0
- package/dist/admin/admin-parts.js.map +1 -0
- package/dist/admin/invitations-panel.d.ts +91 -0
- package/dist/admin/invitations-panel.js +289 -0
- package/dist/admin/invitations-panel.js.map +1 -0
- package/dist/admin/reviewer-scope-editor.d.ts +91 -0
- package/dist/admin/reviewer-scope-editor.js +134 -0
- package/dist/admin/reviewer-scope-editor.js.map +1 -0
- package/dist/admin/role-select.d.ts +91 -0
- package/dist/admin/role-select.js +128 -0
- package/dist/admin/role-select.js.map +1 -0
- package/dist/admin/transfer-ownership-dialog.d.ts +91 -0
- package/dist/admin/transfer-ownership-dialog.js +83 -0
- package/dist/admin/transfer-ownership-dialog.js.map +1 -0
- package/dist/admin/user-roster.d.ts +91 -0
- package/dist/admin/user-roster.js +285 -0
- package/dist/admin/user-roster.js.map +1 -0
- package/dist/auth/auth-errors.d.ts +85 -6
- package/dist/auth/auth-errors.js +62 -2
- package/dist/auth/auth-errors.js.map +1 -1
- package/dist/auth/forgot-password-form.d.ts +10 -3
- package/dist/auth/forgot-password-form.js +6 -3
- package/dist/auth/forgot-password-form.js.map +1 -1
- package/dist/auth/register-form.d.ts +17 -12
- package/dist/auth/register-form.js +6 -4
- package/dist/auth/register-form.js.map +1 -1
- package/dist/auth/reset-password-form.d.ts +10 -3
- package/dist/auth/reset-password-form.js +6 -3
- package/dist/auth/reset-password-form.js.map +1 -1
- package/dist/auth/sign-in-form.d.ts +61 -9
- package/dist/auth/sign-in-form.js +94 -13
- package/dist/auth/sign-in-form.js.map +1 -1
- package/dist/auth/verify-email.d.ts +7 -2
- package/dist/auth/verify-email.js +44 -8
- package/dist/auth/verify-email.js.map +1 -1
- package/dist/chart.d.ts +16 -11
- package/dist/components/account-chips.d.ts +47 -7
- package/dist/components/account-chips.js +19 -3
- package/dist/components/account-chips.js.map +1 -1
- package/dist/components/account-settings-labels.d.ts +12 -1
- package/dist/components/account-settings-labels.js +4 -1
- package/dist/components/account-settings-labels.js.map +1 -1
- package/dist/components/account-settings.d.ts +61 -6
- package/dist/components/account-settings.js +115 -15
- package/dist/components/account-settings.js.map +1 -1
- package/dist/components/amount-input.d.ts +17 -12
- package/dist/components/button-group.d.ts +17 -12
- package/dist/components/calculator.d.ts +17 -12
- package/dist/components/confirm-dialog.d.ts +17 -12
- package/dist/components/copy-button.d.ts +14 -9
- package/dist/components/danger-confirm.d.ts +17 -12
- package/dist/components/facing-pair.d.ts +17 -12
- package/dist/components/file-button.d.ts +17 -12
- package/dist/components/file-dropzone.d.ts +17 -12
- package/dist/components/form-actions.d.ts +17 -12
- package/dist/components/iban-input.d.ts +17 -12
- package/dist/components/language-select.d.ts +17 -12
- package/dist/components/legal-page.d.ts +17 -12
- package/dist/components/legal.d.ts +17 -12
- package/dist/components/money-field.d.ts +17 -12
- package/dist/components/number-field.d.ts +17 -12
- package/dist/components/number-input.d.ts +17 -12
- package/dist/components/numpad-sheet.d.ts +17 -12
- package/dist/components/phone-input.d.ts +17 -12
- package/dist/components/series-chart.d.ts +17 -12
- package/dist/components/settings-fields.d.ts +16 -11
- package/dist/components/share-card.d.ts +16 -11
- package/dist/components/text-link.d.ts +17 -12
- package/dist/components/time-input.d.ts +17 -12
- package/dist/components/ui.d.ts +13 -8
- package/dist/{register-form-V1OeNlMr.d.ts → delete-account-setting-CGVMckMw.d.ts} +6616 -5645
- package/dist/feedback/feedback-attachment.d.ts +17 -12
- package/dist/feedback/feedback-context.d.ts +17 -12
- package/dist/feedback/feedback-crash.d.ts +17 -12
- package/dist/feedback/feedback-dialog.d.ts +17 -12
- package/dist/feedback/feedback-inbox.d.ts +17 -12
- package/dist/feedback/feedback-labels.d.ts +17 -12
- package/dist/feedback/feedback-menu.d.ts +17 -12
- package/dist/feedback/feedback-record.d.ts +17 -12
- package/dist/feedback/feedback-row-detail.d.ts +17 -12
- package/dist/feedback/feedback-status-undo.d.ts +17 -12
- package/dist/feedback/feedback-submit.d.ts +17 -12
- package/dist/feedback/feedback-swipe.d.ts +17 -12
- package/dist/feedback/feedback-table.d.ts +14 -9
- package/dist/feedback/feedback-thread.d.ts +17 -12
- package/dist/feedback.d.ts +17 -12
- package/dist/hooks/use-anchored-panel.d.ts +6 -2
- package/dist/hooks/use-anchored-panel.js +2 -1
- package/dist/hooks/use-anchored-panel.js.map +1 -1
- package/dist/hooks/use-file-drop.d.ts +17 -12
- package/dist/i18n/defaults.d.ts +17 -12
- package/dist/i18n/defaults.js +22 -0
- package/dist/i18n/defaults.js.map +1 -1
- package/dist/i18n/german.d.ts +17 -12
- package/dist/i18n/german.js +215 -11
- package/dist/i18n/german.js.map +1 -1
- package/dist/i18n/kit-labels.d.ts +10 -5
- package/dist/i18n/kit-labels.js.map +1 -1
- package/dist/i18n/languages.d.ts +11 -6
- package/dist/i18n/locales/de-CH.d.ts +17 -12
- package/dist/i18n/locales/en.d.ts +17 -12
- package/dist/i18n/locales/es.d.ts +17 -12
- package/dist/i18n/locales/es.js +207 -9
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.d.ts +17 -12
- package/dist/i18n/locales/fr.js +209 -6
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.d.ts +17 -12
- package/dist/i18n/locales/hu.js +207 -11
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.d.ts +17 -12
- package/dist/i18n/locales/it.js +209 -6
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.d.ts +17 -12
- package/dist/i18n/locales/zh.js +200 -6
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/i18n/review.d.ts +17 -12
- package/dist/i18n/review.js +37 -1
- package/dist/i18n/review.js.map +1 -1
- package/dist/index.d.ts +8 -3
- package/dist/index.js +53 -0
- package/dist/index.js.map +1 -1
- package/dist/rhf/fields.d.ts +17 -12
- package/dist/rhf/form.d.ts +17 -12
- package/dist/rhf.d.ts +17 -12
- package/dist/shell/app-shell.d.ts +17 -12
- package/dist/shell/top-bar-brand.d.ts +17 -12
- package/dist/shell.d.ts +11 -6
- package/dist/wizard/stepper-nav.d.ts +17 -12
- package/dist/wizard.d.ts +17 -12
- package/package.json +1 -1
- package/src/account/account-parts.ts +67 -0
- package/src/account/data-export-setting.tsx +218 -0
- package/src/account/delete-account-setting.tsx +362 -0
- package/src/account/email-change-setting.tsx +457 -0
- package/src/account/sessions-setting.tsx +283 -0
- package/src/admin/admin-action-confirm.tsx +495 -0
- package/src/admin/admin-action-log.tsx +297 -0
- package/src/admin/admin-parts.ts +82 -0
- package/src/admin/invitations-panel.tsx +516 -0
- package/src/admin/reviewer-scope-editor.tsx +234 -0
- package/src/admin/role-select.tsx +286 -0
- package/src/admin/transfer-ownership-dialog.tsx +172 -0
- package/src/admin/user-roster.tsx +573 -0
- package/src/auth/auth-errors.ts +165 -8
- package/src/auth/forgot-password-form.tsx +21 -5
- package/src/auth/register-form.tsx +24 -6
- package/src/auth/reset-password-form.tsx +19 -5
- package/src/auth/sign-in-form.tsx +207 -35
- package/src/auth/verify-email.tsx +59 -8
- package/src/components/account-chips.tsx +64 -7
- package/src/components/account-settings-labels.ts +15 -1
- package/src/components/account-settings.tsx +199 -23
- package/src/hooks/use-anchored-panel.ts +14 -3
- package/src/i18n/defaults.ts +22 -0
- package/src/i18n/german.ts +241 -5
- package/src/i18n/kit-labels.tsx +33 -0
- package/src/i18n/locales/es.ts +230 -3
- package/src/i18n/locales/fr.ts +232 -0
- package/src/i18n/locales/hu.ts +224 -5
- package/src/i18n/locales/it.ts +230 -0
- package/src/i18n/locales/zh.ts +205 -0
- package/src/i18n/review.ts +36 -0
- package/src/index.ts +93 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/auth/verify-email.tsx"],"sourcesContent":["import { useEffect, useState } from \"react\";\nimport type { ComponentPropsWithoutRef, ReactNode } from \"react\";\nimport { CheckCircle2, MailWarning, TimerOff, XCircle } from \"lucide-react\";\nimport { cn } from \"../lib/cn\";\nimport { useKitLabels } from \"../i18n/kit-labels\";\nimport { useAnnounce } from \"../hooks/use-announce\";\nimport { AlertBanner } from \"../components/alert-banner\";\nimport { LoadingState } from \"../components/loading-state\";\nimport { Button, EmptyState } from \"../components/ui\";\nimport { OUTCOME_CLASS, STATUS_HEADING_CLASS, isThenable, useOncePerKey } from \"./status-parts\";\nimport type { AuthHeadingLevel } from \"./status-parts\";\n\n/* ── Labels ──────────────────────────────────────────────────────────────── */\n\n/** Every string the verification page and banner render — the `verifyEmail` namespace.\n * keksdose's words (`verify_email.*`, and its server's \"has expired\"). */\nexport interface VerifyEmailLabels {\n /** The page's heading. */\n title: string;\n /** While the link is redeemed. */\n verifying: string;\n verified: string;\n /** The way on from the page — into the app, or to sign-in. */\n continue: string;\n invalid: string;\n expired: string;\n /** The page was opened without a `token`. */\n noToken: string;\n /** Under `invalid`: why a link stops working. */\n invalidHint: string;\n /** Under a dead link, signed out: where a new one comes from. */\n requestInApp: string;\n /** Under a dead link, signed in, beside \"Send again\". */\n requestHere: string;\n /** The resend button, on the page and in the banner. */\n resend: string;\n /** The resend button while a just-sent mail cools down, given the seconds left. */\n resendIn: (seconds: number) => string;\n /** Said (and shown) after a resend went out. */\n resent: string;\n /** Said (and shown) after a resend failed. */\n resendError: string;\n /** The banner's message. */\n banner: string;\n /** The banner's ×: it defers, it does not close anything. */\n dismiss: string;\n}\n\nexport const DEFAULT_VERIFY_EMAIL_LABELS: VerifyEmailLabels = {\n title: \"Confirm your email address\",\n verifying: \"Confirming…\",\n verified: \"Your email address is confirmed.\",\n continue: \"Continue\",\n invalid: \"This confirmation link is not valid\",\n expired: \"This confirmation link has expired\",\n noToken: \"This link is missing its confirmation token.\",\n invalidHint: \"The link may have expired or already been used.\",\n requestInApp: \"You can request a new one from inside the app.\",\n requestHere: \"You can request a new one here.\",\n resend: \"Send again\",\n resendIn: (seconds) => `Send again in ${seconds} s`,\n resent: \"Confirmation email sent\",\n resendError: \"Could not send the confirmation email\",\n banner: \"Please confirm your email address.\",\n dismiss: \"Not now\",\n};\n\n/** A minute: long enough that an impatient second click does not mail twice, short\n * enough that a mail lost to a spam filter can be re-sent without leaving the page. The\n * server's own limit is the real one (kastlan: 3 an hour per address). */\nconst DEFAULT_COOLDOWN = 60;\n\n/* ── Resend ──────────────────────────────────────────────────────────────── */\n\ntype ResendPhase = \"idle\" | \"sending\" | \"sent\" | \"failed\";\n\n/**\n * \"Send again\" with its own memory: busy while the request runs, then — after a mail\n * went out — held for `cooldown` seconds with the countdown on the button, so a second\n * click cannot mail twice and a throttled server is not asked to refuse. A failure frees\n * it at once. The outcome is a polite live region that is always rendered (a region that\n * mounts with its message is often not read), and visible.\n */\nfunction useResend(onResend: (() => Promise<unknown> | void) | undefined, cooldown: number) {\n const [phase, setPhase] = useState<ResendPhase>(\"idle\");\n const [remaining, setRemaining] = useState(0);\n const counting = remaining > 0;\n useEffect(() => {\n if (!counting) return;\n const id = setInterval(() => setRemaining((r) => Math.max(0, r - 1)), 1000);\n return () => clearInterval(id);\n }, [counting]);\n\n const sent = () => {\n setPhase(\"sent\");\n setRemaining(Math.max(0, Math.round(cooldown)));\n };\n const send = () => {\n if (!onResend || phase === \"sending\" || counting) return;\n let result: unknown;\n try {\n result = onResend();\n } catch {\n setPhase(\"failed\");\n return;\n }\n if (!isThenable(result)) {\n sent();\n return;\n }\n setPhase(\"sending\");\n result.then(sent, () => setPhase(\"failed\"));\n };\n return { phase, remaining, send };\n}\n\nfunction ResendButton({\n state,\n labels,\n variant,\n}: {\n state: ReturnType<typeof useResend>;\n labels: VerifyEmailLabels;\n variant: \"secondary\" | \"brand\";\n}) {\n return (\n <Button\n type=\"button\"\n variant={variant}\n size={variant === \"secondary\" ? \"sm\" : undefined}\n onClick={state.send}\n pending={state.phase === \"sending\"}\n // `disabledReason`, not `disabled`: the button just pressed has the focus, and a\n // native `disabled` would drop it to <body>. The reason is what just happened.\n disabledReason={state.remaining > 0 ? labels.resent : undefined}\n >\n {state.remaining > 0 ? labels.resendIn(state.remaining) : labels.resend}\n </Button>\n );\n}\n\nfunction ResendStatus({ phase, labels, className }: { phase: ResendPhase; labels: VerifyEmailLabels; className?: string }) {\n return (\n <span\n role=\"status\"\n className={cn(phase === \"failed\" ? \"text-[var(--danger)]\" : \"text-[var(--success)]\", className)}\n >\n {phase === \"sent\" ? labels.resent : phase === \"failed\" ? labels.resendError : \"\"}\n </span>\n );\n}\n\n/* ── The page ────────────────────────────────────────────────────────────── */\n\n/** What became of a confirmation link that did not verify. */\nexport type VerifyEmailFailure = \"invalid\" | \"expired\";\n\nexport interface VerifyEmailStatusProps extends Omit<ComponentPropsWithoutRef<\"div\">, \"children\" | \"title\"> {\n /** The link's token — the app's `?token=`. Empty: the link was cut short, and the\n * page says so without asking anyone. */\n token: string;\n /**\n * Redeem it — `POST /auth/verify-email {token}`, once per token, on mount. A POST and\n * not the link's own GET (§10.10): a mail scanner that opens every link must not\n * confirm an address nobody read. Resolve when it verified (an address that already\n * was counts: keksdose's redeem is idempotent); reject with the client's error.\n */\n onVerify: (token: string) => Promise<unknown>;\n /** Which dead end a rejection is. Default: `invalid` for everything — §5.2's servers\n * answer one `token_invalid`; an app whose server tells expiry apart maps it here. */\n classifyError?: (error: unknown) => VerifyEmailFailure;\n /** Someone is signed in on this device: \"Continue\" goes into the app, and a dead link\n * offers \"Send again\" (with `onResend`). */\n signedIn?: boolean;\n /** Send a new confirmation mail (`POST /auth/verify-email/resend`) — offered on a dead\n * link while signed in. */\n onResend?: () => Promise<unknown> | void;\n /** Seconds \"Send again\" waits after a mail went out. Default 60; `0` never waits. */\n cooldown?: number;\n /** Where \"Continue\" goes. Default `/` signed in, `/login` signed out. */\n continueHref?: string;\n /** App content under \"confirmed\" — keksdose's \"Others can now share a budget with you\n * directly.\" */\n verifiedContent?: ReactNode;\n /** The heading. Default `labels.title`; `null` draws none, for a page that puts it in\n * `AuthLayout`'s `title` instead. */\n title?: ReactNode;\n /** The heading's level. Default `h2`, under the page's `h1`. */\n headingAs?: AuthHeadingLevel;\n labels?: Partial<VerifyEmailLabels>;\n}\n\ntype VerifyOutcome = \"verified\" | VerifyEmailFailure;\n\n/**\n * Redeem an email-confirmation link (`/verify-email?token=…`, docs/auth-harmonization.md\n * §7) — keksdose's `VerifyEmailPage` (features/auth/verify-email-page.tsx), with\n * kastlan's outcome blocks and its separate \"expired\".\n *\n * Opened from an inbox, often in a browser with no session, so the token is the only\n * credential and the page renders signed-out. It redeems once per token on mount\n * (StrictMode's double effect included — the token is single-use) and shows one of:\n * confirmed; not valid; expired; or the link had no token at all. Verification gates\n * nothing (§2.8), so every outcome ends on \"Continue\". Signed in, a dead link also\n * offers \"Send again\", which waits `cooldown` seconds after a mail went out.\n *\n * The outcome replaces the spinner, which says nothing to a screen reader on its own,\n * so it is also announced.\n */\nexport function VerifyEmailStatus({\n token,\n onVerify,\n classifyError,\n signedIn = false,\n onResend,\n cooldown = DEFAULT_COOLDOWN,\n continueHref,\n verifiedContent,\n title,\n headingAs: Heading = \"h2\",\n labels: labelsProp,\n className,\n ...rest\n}: VerifyEmailStatusProps) {\n const labels = useKitLabels(\"verifyEmail\", DEFAULT_VERIFY_EMAIL_LABELS, labelsProp);\n const [settled, setSettled] = useState<{ token: string; outcome: VerifyOutcome } | null>(null);\n const outcome: VerifyOutcome | \"verifying\" =\n token === \"\" ? \"invalid\" : settled?.token === token ? settled.outcome : \"verifying\";\n const { announce, regionProps } = useAnnounce();\n const resend = useResend(onResend, cooldown);\n\n useOncePerKey(token === \"\" ? null : token, onVerify, (key, result) => {\n const next: VerifyOutcome = result.ok ? \"verified\" : (classifyError?.(result.error) ?? \"invalid\");\n setSettled({ token: key, outcome: next });\n announce(labels[next]);\n });\n\n const href = continueHref ?? (signedIn ? \"/\" : \"/login\");\n // `replace`: the token is spent, so Back must not land on this page again (keksdose F1).\n const continueButton = (\n <Button href={href} replace variant={outcome === \"verified\" ? \"brand\" : \"secondary\"}>\n {labels.continue}\n </Button>\n );\n const canResend = signedIn && onResend !== undefined;\n\n let body: ReactNode;\n if (outcome === \"verifying\") {\n body = <LoadingState size=\"sm\" label={labels.verifying} />;\n } else if (outcome === \"verified\") {\n body = (\n <EmptyState\n tone=\"success\"\n icon={<CheckCircle2 />}\n title={labels.verified}\n hint={verifiedContent}\n action={continueButton}\n className={OUTCOME_CLASS}\n />\n );\n } else {\n const hint = (\n <>\n {outcome === \"invalid\" && token !== \"\" ? `${labels.invalidHint} ` : \"\"}\n {canResend ? labels.requestHere : labels.requestInApp}\n </>\n );\n body = (\n <EmptyState\n tone=\"danger\"\n icon={outcome === \"expired\" ? <TimerOff /> : <XCircle />}\n title={token === \"\" ? labels.noToken : labels[outcome]}\n hint={hint}\n action={\n <>\n {canResend && <ResendButton state={resend} labels={labels} variant=\"brand\" />}\n {continueButton}\n </>\n }\n className={OUTCOME_CLASS}\n />\n );\n }\n\n return (\n <div\n {...rest}\n data-state={outcome}\n aria-busy={outcome === \"verifying\" || undefined}\n className={cn(\"space-y-3\", className)}\n >\n {title === null ? null : <Heading className={STATUS_HEADING_CLASS}>{title ?? labels.title}</Heading>}\n {body}\n {canResend && <ResendStatus phase={resend.phase} labels={labels} className=\"block text-center text-xs\" />}\n {/* Rendered from the start, so the outcome is read when it arrives. */}\n <div {...regionProps} />\n </div>\n );\n}\n\n/* ── The banner ──────────────────────────────────────────────────────────── */\n\nexport interface EmailVerificationBannerProps {\n /** Send a new confirmation mail (`POST /auth/verify-email/resend`). Resolve when it\n * went out; reject and the banner says it could not. */\n onResend: () => Promise<unknown> | void;\n /** Seconds \"Send again\" waits after a mail went out. Default 60; `0` never waits. */\n cooldown?: number;\n /** The ×, \"Not now\". The banner does not hide itself: the app stops rendering it —\n * keksdose for the rest of the session. Left out, no ×. */\n onDismiss?: () => void;\n /** `strip` (default): keksdose's band across the top of the shell. `box`: kastlan's\n * callout inside the page. */\n variant?: \"strip\" | \"box\";\n /** The app's sentence after the kit's — keksdose's \"Until you do, nobody can share a\n * budget with you directly.\" */\n children?: ReactNode;\n className?: string;\n labels?: Partial<VerifyEmailLabels>;\n}\n\n/**\n * The nudge for a signed-in account whose address is not confirmed — keksdose's\n * `VerifyEmailBanner` (features/auth/verify-email-banner.tsx), kastlan's copy of it.\n *\n * A prompt, not a wall: verification gates nothing (§2.8). The app renders it while\n * `/auth/me` says `email_verified: false`, and stops when the user dismisses it or the\n * page above confirms the address. \"Send again\" waits `cooldown` seconds after a mail\n * went out (the button says how long), and its outcome is written into the banner and\n * read out — no toast needed.\n */\nexport function EmailVerificationBanner({\n onResend,\n cooldown = DEFAULT_COOLDOWN,\n onDismiss,\n variant = \"strip\",\n children,\n className,\n labels: labelsProp,\n}: EmailVerificationBannerProps) {\n const labels = useKitLabels(\"verifyEmail\", DEFAULT_VERIFY_EMAIL_LABELS, labelsProp);\n const resend = useResend(onResend, cooldown);\n return (\n <AlertBanner\n tone=\"warning\"\n variant={variant}\n icon={<MailWarning />}\n action={<ResendButton state={resend} labels={labels} variant=\"secondary\" />}\n onDismiss={onDismiss}\n dismissLabel={onDismiss ? labels.dismiss : undefined}\n className={className}\n >\n {labels.banner}\n {children != null && children !== false && <> {children}</>}{\" \"}\n <ResendStatus phase={resend.phase} labels={labels} className=\"font-medium\" />\n </AlertBanner>\n );\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/auth/verify-email.tsx"],"sourcesContent":["import { useEffect, useState } from \"react\";\nimport type { ComponentPropsWithoutRef, ReactNode } from \"react\";\nimport { CheckCircle2, MailWarning, TimerOff, XCircle } from \"lucide-react\";\nimport { cn } from \"../lib/cn\";\nimport { useKitLabels } from \"../i18n/kit-labels\";\nimport { useAnnounce } from \"../hooks/use-announce\";\nimport { AlertBanner } from \"../components/alert-banner\";\nimport { LoadingState } from \"../components/loading-state\";\nimport { Button, EmptyState } from \"../components/ui\";\nimport { OUTCOME_CLASS, STATUS_HEADING_CLASS, isThenable, useOncePerKey } from \"./status-parts\";\nimport { authErrorCode } from \"./auth-errors\";\nimport type { AuthHeadingLevel } from \"./status-parts\";\n\n/* ── Labels ──────────────────────────────────────────────────────────────── */\n\n/** Every string the verification page and banner render — the `verifyEmail` namespace.\n * keksdose's words (`verify_email.*`, and its server's \"has expired\"). */\nexport interface VerifyEmailLabels {\n /** The page's heading. */\n title: string;\n /** While the link is redeemed. */\n verifying: string;\n verified: string;\n /** The way on from the page — into the app, or to sign-in. */\n continue: string;\n invalid: string;\n expired: string;\n /** The page was opened without a `token`. */\n noToken: string;\n /** Under `invalid`: why a link stops working. */\n invalidHint: string;\n /** Under a dead link, signed out: where a new one comes from. */\n requestInApp: string;\n /** Under a dead link, signed in, beside \"Send again\". */\n requestHere: string;\n /** The resend button, on the page and in the banner. */\n resend: string;\n /** The resend button while a just-sent mail cools down, given the seconds left. */\n resendIn: (seconds: number) => string;\n /** Said (and shown) after a resend went out. */\n resent: string;\n /** Said (and shown) after a resend failed. */\n resendError: string;\n /** The banner's message. */\n banner: string;\n /** The banner's ×: it defers, it does not close anything. */\n dismiss: string;\n}\n\nexport const DEFAULT_VERIFY_EMAIL_LABELS: VerifyEmailLabels = {\n title: \"Confirm your email address\",\n verifying: \"Confirming…\",\n verified: \"Your email address is confirmed.\",\n continue: \"Continue\",\n invalid: \"This confirmation link is not valid\",\n expired: \"This confirmation link has expired\",\n noToken: \"This link is missing its confirmation token.\",\n invalidHint: \"The link may have expired or already been used.\",\n requestInApp: \"You can request a new one from inside the app.\",\n requestHere: \"You can request a new one here.\",\n resend: \"Send again\",\n resendIn: (seconds) => `Send again in ${seconds} s`,\n resent: \"Confirmation email sent\",\n resendError: \"Could not send the confirmation email\",\n banner: \"Please confirm your email address.\",\n dismiss: \"Not now\",\n};\n\n/** A minute: long enough that an impatient second click does not mail twice, short\n * enough that a mail lost to a spam filter can be re-sent without leaving the page. The\n * server's own limit is the real one (kastlan: 3 an hour per address). */\nconst DEFAULT_COOLDOWN = 60;\n\n/* ── Resend ──────────────────────────────────────────────────────────────── */\n\ntype ResendPhase = \"idle\" | \"sending\" | \"sent\" | \"failed\";\n\n/**\n * \"Send again\" with its own memory: busy while the request runs, then — after a mail\n * went out — held for `cooldown` seconds with the countdown on the button, so a second\n * click cannot mail twice and a throttled server is not asked to refuse. A failure frees\n * it at once. The outcome is a polite live region that is always rendered (a region that\n * mounts with its message is often not read), and visible.\n */\nfunction useResend(onResend: (() => Promise<unknown> | void) | undefined, cooldown: number) {\n const [phase, setPhase] = useState<ResendPhase>(\"idle\");\n // The refusal itself, for `describeError` (0.29.1): the server's own sentence — keksdose's\n // per-address throttle, \"try again in N minutes\" — beats a fixed \"could not send\".\n const [error, setError] = useState<unknown>(undefined);\n const [remaining, setRemaining] = useState(0);\n const counting = remaining > 0;\n useEffect(() => {\n if (!counting) return;\n const id = setInterval(() => setRemaining((r) => Math.max(0, r - 1)), 1000);\n return () => clearInterval(id);\n }, [counting]);\n\n const sent = () => {\n setPhase(\"sent\");\n setRemaining(Math.max(0, Math.round(cooldown)));\n };\n const send = () => {\n if (!onResend || phase === \"sending\" || counting) return;\n let result: unknown;\n try {\n result = onResend();\n } catch (e) {\n setError(e);\n setPhase(\"failed\");\n return;\n }\n if (!isThenable(result)) {\n sent();\n return;\n }\n setPhase(\"sending\");\n result.then(sent, (e: unknown) => {\n setError(e);\n setPhase(\"failed\");\n });\n };\n return { phase, remaining, send, error };\n}\n\nfunction ResendButton({\n state,\n labels,\n variant,\n}: {\n state: ReturnType<typeof useResend>;\n labels: VerifyEmailLabels;\n variant: \"secondary\" | \"brand\";\n}) {\n return (\n <Button\n type=\"button\"\n variant={variant}\n size={variant === \"secondary\" ? \"sm\" : undefined}\n onClick={state.send}\n pending={state.phase === \"sending\"}\n // `disabledReason`, not `disabled`: the button just pressed has the focus, and a\n // native `disabled` would drop it to <body>. The reason is what just happened.\n disabledReason={state.remaining > 0 ? labels.resent : undefined}\n >\n {state.remaining > 0 ? labels.resendIn(state.remaining) : labels.resend}\n </Button>\n );\n}\n\nfunction ResendStatus({\n phase,\n error,\n describeError,\n labels,\n className,\n}: {\n phase: ResendPhase;\n error?: unknown;\n describeError?: (error: unknown) => string | undefined;\n labels: VerifyEmailLabels;\n className?: string;\n}) {\n const failed = phase === \"failed\" ? (describeError?.(error) ?? labels.resendError) : \"\";\n return (\n <span\n role=\"status\"\n className={cn(phase === \"failed\" ? \"text-[var(--danger)]\" : \"text-[var(--success)]\", className)}\n >\n {phase === \"sent\" ? labels.resent : failed}\n </span>\n );\n}\n\n/* ── The page ────────────────────────────────────────────────────────────── */\n\n/** What became of a confirmation link that did not verify. */\nexport type VerifyEmailFailure = \"invalid\" | \"expired\";\n\n/** The default when the app gives no `classifyError`: the coded refusal decides. */\nfunction defaultVerifyFailure(error: unknown): VerifyEmailFailure {\n return authErrorCode(error) === \"token_expired\" ? \"expired\" : \"invalid\";\n}\n\nexport interface VerifyEmailStatusProps extends Omit<ComponentPropsWithoutRef<\"div\">, \"children\" | \"title\"> {\n /** The link's token — the app's `?token=`. Empty: the link was cut short, and the\n * page says so without asking anyone. */\n token: string;\n /**\n * Redeem it — `POST /auth/verify-email {token}`, once per token, on mount. A POST and\n * not the link's own GET (§10.10): a mail scanner that opens every link must not\n * confirm an address nobody read. Resolve when it verified (an address that already\n * was counts: keksdose's redeem is idempotent); reject with the client's error.\n */\n onVerify: (token: string) => Promise<unknown>;\n /** Which dead end a rejection is. Default: `invalid` for everything — §5.2's servers\n * answer one `token_invalid`; an app whose server tells expiry apart maps it here. */\n classifyError?: (error: unknown) => VerifyEmailFailure;\n /** 0.29.1: the words for a refused resend — the server's own sentence (a throttle's\n * \"try again in N minutes\"), else the kit's `resendError`. */\n describeError?: (error: unknown) => string | undefined;\n /** Someone is signed in on this device: \"Continue\" goes into the app, and a dead link\n * offers \"Send again\" (with `onResend`). */\n signedIn?: boolean;\n /** Send a new confirmation mail (`POST /auth/verify-email/resend`) — offered on a dead\n * link while signed in. */\n onResend?: () => Promise<unknown> | void;\n /** Seconds \"Send again\" waits after a mail went out. Default 60; `0` never waits. */\n cooldown?: number;\n /** Where \"Continue\" goes. Default `/` signed in, `/login` signed out. */\n continueHref?: string;\n /** App content under \"confirmed\" — keksdose's \"Others can now share a budget with you\n * directly.\" */\n verifiedContent?: ReactNode;\n /** The heading. Default `labels.title`; `null` draws none, for a page that puts it in\n * `AuthLayout`'s `title` instead. */\n title?: ReactNode;\n /** The heading's level. Default `h2`, under the page's `h1`. */\n headingAs?: AuthHeadingLevel;\n labels?: Partial<VerifyEmailLabels>;\n}\n\ntype VerifyOutcome = \"verified\" | VerifyEmailFailure;\n\n/**\n * Redeem an email-confirmation link (`/verify-email?token=…`, docs/auth-harmonization.md\n * §7) — keksdose's `VerifyEmailPage` (features/auth/verify-email-page.tsx), with\n * kastlan's outcome blocks and its separate \"expired\".\n *\n * Opened from an inbox, often in a browser with no session, so the token is the only\n * credential and the page renders signed-out. It redeems once per token on mount\n * (StrictMode's double effect included — the token is single-use) and shows one of:\n * confirmed; not valid; expired; or the link had no token at all. Verification gates\n * nothing (§2.8), so every outcome ends on \"Continue\". Signed in, a dead link also\n * offers \"Send again\", which waits `cooldown` seconds after a mail went out.\n *\n * The outcome replaces the spinner, which says nothing to a screen reader on its own,\n * so it is also announced.\n */\nexport function VerifyEmailStatus({\n token,\n onVerify,\n classifyError,\n describeError,\n signedIn = false,\n onResend,\n cooldown = DEFAULT_COOLDOWN,\n continueHref,\n verifiedContent,\n title,\n headingAs: Heading = \"h2\",\n labels: labelsProp,\n className,\n ...rest\n}: VerifyEmailStatusProps) {\n const labels = useKitLabels(\"verifyEmail\", DEFAULT_VERIFY_EMAIL_LABELS, labelsProp);\n const [settled, setSettled] = useState<{ token: string; outcome: VerifyOutcome } | null>(null);\n const outcome: VerifyOutcome | \"verifying\" =\n token === \"\" ? \"invalid\" : settled?.token === token ? settled.outcome : \"verifying\";\n const { announce, regionProps } = useAnnounce();\n const resend = useResend(onResend, cooldown);\n\n useOncePerKey(token === \"\" ? null : token, onVerify, (key, result) => {\n // Without a classifier, the coded refusal decides (0.29.1): `token_expired` is\n // \"expired\", anything else \"invalid\".\n const next: VerifyOutcome = result.ok\n ? \"verified\"\n : (classifyError ?? defaultVerifyFailure)(result.error);\n setSettled({ token: key, outcome: next });\n announce(labels[next]);\n });\n\n const href = continueHref ?? (signedIn ? \"/\" : \"/login\");\n // `replace`: the token is spent, so Back must not land on this page again (keksdose F1).\n const continueButton = (\n <Button href={href} replace variant={outcome === \"verified\" ? \"brand\" : \"secondary\"}>\n {labels.continue}\n </Button>\n );\n const canResend = signedIn && onResend !== undefined;\n\n let body: ReactNode;\n if (outcome === \"verifying\") {\n body = <LoadingState size=\"sm\" label={labels.verifying} />;\n } else if (outcome === \"verified\") {\n body = (\n <EmptyState\n tone=\"success\"\n icon={<CheckCircle2 />}\n title={labels.verified}\n hint={verifiedContent}\n action={continueButton}\n className={OUTCOME_CLASS}\n />\n );\n } else {\n const hint = (\n <>\n {outcome === \"invalid\" && token !== \"\" ? `${labels.invalidHint} ` : \"\"}\n {canResend ? labels.requestHere : labels.requestInApp}\n </>\n );\n body = (\n <EmptyState\n tone=\"danger\"\n icon={outcome === \"expired\" ? <TimerOff /> : <XCircle />}\n title={token === \"\" ? labels.noToken : labels[outcome]}\n hint={hint}\n action={\n <>\n {canResend && <ResendButton state={resend} labels={labels} variant=\"brand\" />}\n {continueButton}\n </>\n }\n className={OUTCOME_CLASS}\n />\n );\n }\n\n return (\n <div\n {...rest}\n data-state={outcome}\n aria-busy={outcome === \"verifying\" || undefined}\n className={cn(\"space-y-3\", className)}\n >\n {title === null ? null : <Heading className={STATUS_HEADING_CLASS}>{title ?? labels.title}</Heading>}\n {body}\n {canResend && (\n <ResendStatus\n phase={resend.phase}\n error={resend.error}\n describeError={describeError}\n labels={labels}\n className=\"block text-center text-xs\"\n />\n )}\n {/* Rendered from the start, so the outcome is read when it arrives. */}\n <div {...regionProps} />\n </div>\n );\n}\n\n/* ── The banner ──────────────────────────────────────────────────────────── */\n\nexport interface EmailVerificationBannerProps {\n /** Send a new confirmation mail (`POST /auth/verify-email/resend`). Resolve when it\n * went out; reject and the banner says it could not. */\n onResend: () => Promise<unknown> | void;\n /** 0.29.1: the words for a refused resend — the server's own sentence, else the kit's. */\n describeError?: (error: unknown) => string | undefined;\n /** Seconds \"Send again\" waits after a mail went out. Default 60; `0` never waits. */\n cooldown?: number;\n /** The ×, \"Not now\". The banner does not hide itself: the app stops rendering it —\n * keksdose for the rest of the session. Left out, no ×. */\n onDismiss?: () => void;\n /** `strip` (default): keksdose's band across the top of the shell. `box`: kastlan's\n * callout inside the page. */\n variant?: \"strip\" | \"box\";\n /** The app's sentence after the kit's — keksdose's \"Until you do, nobody can share a\n * budget with you directly.\" */\n children?: ReactNode;\n className?: string;\n labels?: Partial<VerifyEmailLabels>;\n}\n\n/**\n * The nudge for a signed-in account whose address is not confirmed — keksdose's\n * `VerifyEmailBanner` (features/auth/verify-email-banner.tsx), kastlan's copy of it.\n *\n * A prompt, not a wall: verification gates nothing (§2.8). The app renders it while\n * `/auth/me` says `email_verified: false`, and stops when the user dismisses it or the\n * page above confirms the address. \"Send again\" waits `cooldown` seconds after a mail\n * went out (the button says how long), and its outcome is written into the banner and\n * read out — no toast needed.\n */\nexport function EmailVerificationBanner({\n onResend,\n describeError,\n cooldown = DEFAULT_COOLDOWN,\n onDismiss,\n variant = \"strip\",\n children,\n className,\n labels: labelsProp,\n}: EmailVerificationBannerProps) {\n const labels = useKitLabels(\"verifyEmail\", DEFAULT_VERIFY_EMAIL_LABELS, labelsProp);\n const resend = useResend(onResend, cooldown);\n return (\n <AlertBanner\n tone=\"warning\"\n variant={variant}\n icon={<MailWarning />}\n action={<ResendButton state={resend} labels={labels} variant=\"secondary\" />}\n onDismiss={onDismiss}\n dismissLabel={onDismiss ? labels.dismiss : undefined}\n className={className}\n >\n {labels.banner}\n {children != null && children !== false && <> {children}</>}{\" \"}\n <ResendStatus\n phase={resend.phase}\n error={resend.error}\n describeError={describeError}\n labels={labels}\n className=\"font-medium\"\n />\n </AlertBanner>\n );\n}\n"],"mappings":"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","names":[]}
|
package/dist/chart.d.ts
CHANGED
|
@@ -13,7 +13,10 @@ export { TimeTickUnit } from './components/series-chart-ticks.js';
|
|
|
13
13
|
import 'recharts';
|
|
14
14
|
import 'react';
|
|
15
15
|
import './lib/direction.js';
|
|
16
|
-
import './
|
|
16
|
+
import './delete-account-setting-CGVMckMw.js';
|
|
17
|
+
import './components/tooltip.js';
|
|
18
|
+
import './hooks/use-anchored-rect.js';
|
|
19
|
+
import './lib/clipping.js';
|
|
17
20
|
import './data-table-labels-DUELeXpa.js';
|
|
18
21
|
import './components/data-table-sort.js';
|
|
19
22
|
import './components/swipeable-row.js';
|
|
@@ -23,10 +26,6 @@ import './components/popover.js';
|
|
|
23
26
|
import './components/chip.js';
|
|
24
27
|
import 'lucide-react';
|
|
25
28
|
import './components/field-sync.js';
|
|
26
|
-
import './components/tooltip.js';
|
|
27
|
-
import './hooks/use-anchored-rect.js';
|
|
28
|
-
import './lib/clipping.js';
|
|
29
|
-
import './components/field-parts.js';
|
|
30
29
|
import './wizard/types.js';
|
|
31
30
|
import './tour/tour.js';
|
|
32
31
|
import './search/command-palette.js';
|
|
@@ -67,6 +66,11 @@ import './components/image-grid.js';
|
|
|
67
66
|
import './components/lightbox.js';
|
|
68
67
|
import './components/write-lock.js';
|
|
69
68
|
import './auth/sign-in-form.js';
|
|
69
|
+
import './shell/topbar-controls.js';
|
|
70
|
+
import './theme/palette-presets.js';
|
|
71
|
+
import './theme/theme-store.js';
|
|
72
|
+
import 'zustand/middleware';
|
|
73
|
+
import 'zustand';
|
|
70
74
|
import './auth/complete-name-dialog.js';
|
|
71
75
|
import './auth/forgot-password-form.js';
|
|
72
76
|
import './auth/status-parts.js';
|
|
@@ -75,8 +79,13 @@ import './auth/verify-email.js';
|
|
|
75
79
|
import './auth/not-found-page.js';
|
|
76
80
|
import './auth/accept-invitation.js';
|
|
77
81
|
import './shell/company-switcher.js';
|
|
78
|
-
import './
|
|
82
|
+
import './account/email-change-setting.js';
|
|
83
|
+
import './account/sessions-setting.js';
|
|
79
84
|
import './lib/format.js';
|
|
85
|
+
import './account/data-export-setting.js';
|
|
86
|
+
import './components/account-chips.js';
|
|
87
|
+
import './admin/admin-parts.js';
|
|
88
|
+
import './admin/admin-action-log.js';
|
|
80
89
|
import './components/reauth-dialog.js';
|
|
81
90
|
import './components/server-wake.js';
|
|
82
91
|
import './lib/server-wake.js';
|
|
@@ -90,8 +99,4 @@ import './components/sign-chip.js';
|
|
|
90
99
|
import './components/column-mapper.js';
|
|
91
100
|
import './lib/column-mapping.js';
|
|
92
101
|
import './lib/table-text.js';
|
|
93
|
-
import './
|
|
94
|
-
import './theme/palette-presets.js';
|
|
95
|
-
import './theme/theme-store.js';
|
|
96
|
-
import 'zustand/middleware';
|
|
97
|
-
import 'zustand';
|
|
102
|
+
import './components/field-parts.js';
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as react from 'react';
|
|
2
|
-
import {
|
|
2
|
+
import { ReactElement, ReactNode } from 'react';
|
|
3
3
|
import { LucideIcon } from 'lucide-react';
|
|
4
4
|
import { DateInput, FormatDateStyle, FormatRelativeTimeOptions } from '../lib/format.js';
|
|
5
5
|
import { ChipTone, ChipSize, ChipVariant, ChipShape } from './chip.js';
|
|
@@ -78,18 +78,58 @@ declare function RoleChip<R extends string = string>({ value: role, roles, size,
|
|
|
78
78
|
* - `registered` — an allowlist row whose address has an account now (Kurvenschmiede).
|
|
79
79
|
* - `unverified` — the email address is not confirmed (keksdose).
|
|
80
80
|
* - `passwordChange` — must set a new password at the next sign-in (keksdose).
|
|
81
|
+
* - `deletion` — 0.30.0: the user asked for the account to be deleted
|
|
82
|
+
* (docs/user-admin-harmonization.md §2.1, §6.4). It is deactivated at once and erased
|
|
83
|
+
* on a date (`after_days`: keksdose, Kurvenschmiede) or by an operator (kastlan). The
|
|
84
|
+
* chip says the date when there is one ({@link AccountStateChipProps.date}), and
|
|
85
|
+
* "Deletion requested" when there is none. It stands IN PLACE of `inactive`: an
|
|
86
|
+
* account marked for deletion is always deactivated, and two chips would say it twice.
|
|
81
87
|
*
|
|
82
88
|
* An account can be several at once (inactive AND unverified): render one chip per
|
|
83
89
|
* state, as keksdose's state column does.
|
|
84
90
|
*/
|
|
85
|
-
type AccountState = "active" | "inactive" | "invited" | "registered" | "unverified" | "passwordChange";
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
*
|
|
91
|
+
type AccountState = "active" | "inactive" | "invited" | "registered" | "unverified" | "passwordChange" | "deletion";
|
|
92
|
+
/**
|
|
93
|
+
* The `accountState` namespace. One word per state, plus the two forms of `deletion`.
|
|
94
|
+
*
|
|
95
|
+
* The 0.30 keys are OPTIONAL, like every key added to a namespace an app annotates
|
|
96
|
+
* (`const L: AccountStateLabels = {…}`): a new required key would be a compile error in
|
|
97
|
+
* each app on its next update. Left out, they fall back to the provider's, then English.
|
|
98
|
+
*/
|
|
99
|
+
interface AccountStateLabels extends Record<Exclude<AccountState, "deletion">, string> {
|
|
100
|
+
/** 0.30.0: an account marked for deletion with no date — kastlan's `operator` mode,
|
|
101
|
+
* where an operator erases it (§6.4). "Deletion requested". */
|
|
102
|
+
deletion?: string;
|
|
103
|
+
/** 0.30.0: an account that will be erased on `date`, already formatted in the
|
|
104
|
+
* reader's locale — the `after_days` mode. "Deletion on 6 Nov 2026". */
|
|
105
|
+
deletionOn?: (date: string) => string;
|
|
106
|
+
}
|
|
107
|
+
declare const DEFAULT_ACCOUNT_STATE_LABELS: {
|
|
108
|
+
active: string;
|
|
109
|
+
inactive: string;
|
|
110
|
+
invited: string;
|
|
111
|
+
registered: string;
|
|
112
|
+
unverified: string;
|
|
113
|
+
passwordChange: string;
|
|
114
|
+
deletion: string;
|
|
115
|
+
deletionOn: (date: string) => string;
|
|
116
|
+
};
|
|
117
|
+
/** The default tone of each state: green for the normal case, red for switched off
|
|
118
|
+
* (and for going, `deletion`), amber for something the user still has to do, blue for
|
|
119
|
+
* an open invitation. */
|
|
90
120
|
declare const ACCOUNT_STATE_TONES: Readonly<Record<AccountState, ChipTone>>;
|
|
91
121
|
interface AccountStateChipProps extends AccountChipLook {
|
|
92
122
|
state: AccountState;
|
|
123
|
+
/**
|
|
124
|
+
* 0.30.0, `state="deletion"` only: the day the account will be erased — the row's
|
|
125
|
+
* `deletion_scheduled_at` (§3.2, §6.4). Formatted as a date in the reader's locale
|
|
126
|
+
* ("Deletion on 6 Nov 2026"), through the provider's `formatDate` when the app set
|
|
127
|
+
* one, as {@link DateMark} does. Missing or `null` — an operator erases it, so there
|
|
128
|
+
* is no date — the chip says "Deletion requested". Ignored for every other state.
|
|
129
|
+
*/
|
|
130
|
+
date?: DateInput;
|
|
131
|
+
/** Over the provider's locale, for the `deletion` date. */
|
|
132
|
+
locale?: string;
|
|
93
133
|
/** Over {@link ACCOUNT_STATE_TONES} — Kurvenschmiede draws `inactive` neutral. */
|
|
94
134
|
tone?: ChipTone;
|
|
95
135
|
/** Over the label, for an app whose word for the state is its own ("Deactivated"). */
|
|
@@ -101,7 +141,7 @@ interface AccountStateChipProps extends AccountChipLook {
|
|
|
101
141
|
* An account's state as a chip, in the kit's words and tones. Carries `data-state`
|
|
102
142
|
* with the key.
|
|
103
143
|
*/
|
|
104
|
-
declare function AccountStateChip({ state, tone, children, labels: labelsProp, size, variant, shape, caps, className, ...rest }: AccountStateChipProps): react.JSX.Element;
|
|
144
|
+
declare function AccountStateChip({ state, date, locale: localeProp, tone, children, labels: labelsProp, size, variant, shape, caps, className, ...rest }: AccountStateChipProps): react.JSX.Element;
|
|
105
145
|
interface DateMarkProps {
|
|
106
146
|
value: DateInput;
|
|
107
147
|
/**
|
|
@@ -40,7 +40,9 @@ const DEFAULT_ACCOUNT_STATE_LABELS = {
|
|
|
40
40
|
invited: "Invited",
|
|
41
41
|
registered: "Registered",
|
|
42
42
|
unverified: "Unverified",
|
|
43
|
-
passwordChange: "Must change password"
|
|
43
|
+
passwordChange: "Must change password",
|
|
44
|
+
deletion: "Deletion requested",
|
|
45
|
+
deletionOn: (date) => `Deletion on ${date}`
|
|
44
46
|
};
|
|
45
47
|
const ACCOUNT_STATE_TONES = {
|
|
46
48
|
active: "success",
|
|
@@ -48,10 +50,14 @@ const ACCOUNT_STATE_TONES = {
|
|
|
48
50
|
invited: "info",
|
|
49
51
|
registered: "neutral",
|
|
50
52
|
unverified: "warning",
|
|
51
|
-
passwordChange: "warning"
|
|
53
|
+
passwordChange: "warning",
|
|
54
|
+
// Red like `inactive`, which it replaces: the account cannot sign in, and it is going.
|
|
55
|
+
deletion: "danger"
|
|
52
56
|
};
|
|
53
57
|
function AccountStateChip({
|
|
54
58
|
state,
|
|
59
|
+
date,
|
|
60
|
+
locale: localeProp,
|
|
55
61
|
tone,
|
|
56
62
|
children,
|
|
57
63
|
labels: labelsProp,
|
|
@@ -63,18 +69,28 @@ function AccountStateChip({
|
|
|
63
69
|
...rest
|
|
64
70
|
}) {
|
|
65
71
|
const labels = useKitLabels("accountState", DEFAULT_ACCOUNT_STATE_LABELS, labelsProp);
|
|
72
|
+
const locale = useKitLocale(localeProp);
|
|
73
|
+
const fromProvider = useKitDateFormatter();
|
|
74
|
+
const when = state === "deletion" ? toDate(date ?? null) : null;
|
|
75
|
+
let text;
|
|
76
|
+
if (state !== "deletion") text = labels[state];
|
|
77
|
+
else if (when) {
|
|
78
|
+
const day = fromProvider && fromProvider(toLocalIso(when), { unit: "day", source: "dateMark", locale, weekday: false }) || formatDate(when, "medium", { locale });
|
|
79
|
+
text = (labels.deletionOn ?? DEFAULT_ACCOUNT_STATE_LABELS.deletionOn)(day);
|
|
80
|
+
} else text = labels.deletion ?? DEFAULT_ACCOUNT_STATE_LABELS.deletion;
|
|
66
81
|
return /* @__PURE__ */ jsx(
|
|
67
82
|
Chip,
|
|
68
83
|
{
|
|
69
84
|
...rest,
|
|
70
85
|
"data-state": state,
|
|
86
|
+
"data-date": when ? toLocalIso(when) : void 0,
|
|
71
87
|
tone: tone ?? ACCOUNT_STATE_TONES[state],
|
|
72
88
|
size,
|
|
73
89
|
variant,
|
|
74
90
|
shape,
|
|
75
91
|
caps,
|
|
76
92
|
className,
|
|
77
|
-
children: children ??
|
|
93
|
+
children: children ?? text
|
|
78
94
|
}
|
|
79
95
|
);
|
|
80
96
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/account-chips.tsx"],"sourcesContent":["import type { ReactElement, ReactNode } from \"react\";\nimport type { LucideIcon } from \"lucide-react\";\nimport { cn } from \"../lib/cn\";\nimport { formatDate, formatRelativeTime, toDate, EMPTY_FORMATTED_VALUE } from \"../lib/format\";\nimport type { DateInput, FormatDateStyle, FormatRelativeTimeOptions } from \"../lib/format\";\nimport { useKitDateFormatter, useKitLabels, useKitLocale } from \"../i18n/kit-labels\";\nimport { toLocalIso } from \"../lib/dates\";\nimport { Chip } from \"./chip\";\nimport type { ChipShape, ChipSize, ChipTone, ChipVariant } from \"./chip\";\nimport { Tooltip } from \"./tooltip\";\nimport type { DataTableColumn } from \"./data-table\";\n\n/**\n * The parts an admin roster repeats (0.18.0) — and only those.\n *\n * Kurvenschmiede (features/admin/users-panel.tsx, allowlist-panel.tsx) and keksdose\n * (features/admin/users-panel.tsx, the allowlist card in admin-page.tsx) each built the\n * same table: name over email, a role, whether the account is active, created, last\n * login, a row of actions, and an allowlist with a note. What they share is not the\n * table — the columns around it (keksdose's plan and privacy mode, Kurvenschmiede's\n * owned-work counts and its role SELECT), the actions and every rule about who may do\n * what are each app's own. What they share is the vocabulary: both spelled the same\n * square, uppercase status chip by hand (keksdose's `Badge` with its own tone map,\n * Kurvenschmiede's inline `<Chip size=\"xs\" shape=\"square\" caps>`), and both formatted\n * their two date columns by hand (`toLocaleDateString` in one, a \"3 days ago\" helper\n * in the other). So: {@link RoleChip}, {@link AccountStateChip} and\n * {@link dateColumn}, and the roster stays the app's `DataTable`.\n */\n\n/** The chip look both rosters use: a square, uppercase status tag. Each can be changed. */\ninterface AccountChipLook {\n /** Default `sm` — keksdose's roster; Kurvenschmiede's denser lists pass `xs`. */\n size?: ChipSize;\n /** Default `soft`. `dot` for a column of them that should not outshout the names. */\n variant?: ChipVariant;\n /** Default `square`: a chip in a table cell, beside square controls. */\n shape?: ChipShape;\n /** Default true — the status-badge type. */\n caps?: boolean;\n className?: string;\n [key: `data-${string}`]: string | number | boolean | undefined;\n}\n\n/* ── RoleChip ──────────────────────────────────────────────────────────────── */\n\n/** How one role is drawn. `label` is the app's, translated. */\nexport interface RoleDefinition {\n label: string;\n tone?: ChipTone;\n /** A lucide component or an element — Kurvenschmiede marks its admins with a shield. */\n icon?: LucideIcon | ReactElement;\n}\n\n/**\n * Every role an app has, keyed by the value its API sends. Annotate the app's table\n * with it (`const ROLES: RoleVocabulary<UserRole> = {…}`) and TypeScript asks for a\n * label when a role is added.\n */\nexport type RoleVocabulary<R extends string = string> = Readonly<Record<R, RoleDefinition>>;\n\nexport interface RoleChipProps<R extends string = string> extends AccountChipLook {\n /**\n * The role key as the API sends it. `null` / `undefined` renders nothing. Named\n * `value`, not `role`, because `role` on a JSX element is the ARIA role — the lint\n * rule and every reader would take it for one.\n */\n value: R | null | undefined;\n /** What each key is called and how it is toned. A key missing here is shown as the\n * key itself, in `neutral`: an unknown role is still a fact the admin should see. */\n roles: Readonly<Partial<Record<R, RoleDefinition>>>;\n}\n\n/**\n * A role as a chip: the API's key looked up in the app's {@link RoleVocabulary}.\n *\n * The vocabulary is a prop and not kit English, because the roles are the app's —\n * Kurvenschmiede has admin, member and customer, keksdose admin and user, kastlan its\n * own — and so are their names. Carries `data-role` with the key, for a test or a\n * style hook.\n */\nexport function RoleChip<R extends string = string>({\n value: role,\n roles,\n size = \"sm\",\n variant,\n shape = \"square\",\n caps = true,\n className,\n ...rest\n}: RoleChipProps<R>) {\n if (role === null || role === undefined || role === \"\") return null;\n const def = roles[role];\n return (\n <Chip\n {...rest}\n data-role={role}\n tone={def?.tone ?? \"neutral\"}\n icon={def?.icon}\n size={size}\n variant={variant}\n shape={shape}\n caps={caps}\n className={className}\n >\n {def?.label ?? role}\n </Chip>\n );\n}\n\n/* ── AccountStateChip ──────────────────────────────────────────────────────── */\n\n/**\n * The states both rosters show, each as the app's own flag maps onto it:\n *\n * - `active` / `inactive` — the account may sign in, or an admin switched it off\n * (both apps' `is_active`).\n * - `invited` — on the allowlist and mailed, not registered yet (an allowlist row).\n * - `registered` — an allowlist row whose address has an account now (Kurvenschmiede).\n * - `unverified` — the email address is not confirmed (keksdose).\n * - `passwordChange` — must set a new password at the next sign-in (keksdose).\n *\n * An account can be several at once (inactive AND unverified): render one chip per\n * state, as keksdose's state column does.\n */\nexport type AccountState = \"active\" | \"inactive\" | \"invited\" | \"registered\" | \"unverified\" | \"passwordChange\";\n\nexport type AccountStateLabels = Record<AccountState, string>;\n\nexport const DEFAULT_ACCOUNT_STATE_LABELS: AccountStateLabels = {\n active: \"Active\",\n inactive: \"Inactive\",\n invited: \"Invited\",\n registered: \"Registered\",\n unverified: \"Unverified\",\n passwordChange: \"Must change password\",\n};\n\n/** The default tone of each state: green for the normal case, red for switched off,\n * amber for something the user still has to do, blue for an open invitation. */\nexport const ACCOUNT_STATE_TONES: Readonly<Record<AccountState, ChipTone>> = {\n active: \"success\",\n inactive: \"danger\",\n invited: \"info\",\n registered: \"neutral\",\n unverified: \"warning\",\n passwordChange: \"warning\",\n};\n\nexport interface AccountStateChipProps extends AccountChipLook {\n state: AccountState;\n /** Over {@link ACCOUNT_STATE_TONES} — Kurvenschmiede draws `inactive` neutral. */\n tone?: ChipTone;\n /** Over the label, for an app whose word for the state is its own (\"Deactivated\"). */\n children?: ReactNode;\n /** Prop > `<UiKitProvider labels={{ accountState }}>` > English. */\n labels?: Partial<AccountStateLabels>;\n}\n\n/**\n * An account's state as a chip, in the kit's words and tones. Carries `data-state`\n * with the key.\n */\nexport function AccountStateChip({\n state,\n tone,\n children,\n labels: labelsProp,\n size = \"sm\",\n variant,\n shape = \"square\",\n caps = true,\n className,\n ...rest\n}: AccountStateChipProps) {\n // An `undefined` in the prop is skipped (the provider's merge does that), so an\n // optional value cannot blank a default.\n const labels = useKitLabels(\"accountState\", DEFAULT_ACCOUNT_STATE_LABELS, labelsProp);\n return (\n <Chip\n {...rest}\n data-state={state}\n tone={tone ?? ACCOUNT_STATE_TONES[state]}\n size={size}\n variant={variant}\n shape={shape}\n caps={caps}\n className={className}\n >\n {children ?? labels[state]}\n </Chip>\n );\n}\n\n/* ── Date columns ──────────────────────────────────────────────────────────── */\n\nexport interface DateMarkProps {\n value: DateInput;\n /**\n * `\"date\"` (default): the date in `dateStyle` — \"Created\". `\"relative\"`: how long\n * ago — \"Last login\", where the question is \"recently?\" — with the full date and\n * time in a tooltip (keksdose's last-seen column).\n */\n display?: \"date\" | \"relative\";\n /** Default `medium`. Under `\"relative\"`, the style of the tooltip's date (default\n * `dateTime`) and of the date `relative.absoluteAfterDays` switches to. Given, it wins\n * over the provider's `formatDate`. */\n dateStyle?: FormatDateStyle | Intl.DateTimeFormatOptions;\n /** `formatRelativeTime` options: `numeric`, `style`, `absoluteAfterDays`, `now`. */\n relative?: Omit<FormatRelativeTimeOptions, \"locale\" | \"empty\" | \"absoluteStyle\">;\n /** What a missing value reads as. Default \"—\"; a last-login column passes its\n * translated \"Never\". */\n empty?: ReactNode;\n /** Over the provider's locale. */\n locale?: string;\n className?: string;\n}\n\n/**\n * A date in a table cell: a `<time>` with a machine-readable `dateTime`, nowrap and\n * tabular figures, in the provider's locale — or `empty` when there is none.\n *\n * `display=\"date\"` without a `dateStyle` is written by `<UiKitProvider formatDate>` when\n * the app set one (keksdose K12: its `DateCell` exists to put every column's date in the\n * app's date-format preference, weekday and all — dev#546), called with the LOCAL\n * calendar day of the value as `\"YYYY-MM-DD\"` and `weekday: true`. Without one, or for\n * an empty answer, it is `Intl`'s `medium` date as before. The relative display is not\n * a calendar day and keeps `formatRelativeTime`.\n */\nexport function DateMark({\n value,\n display = \"date\",\n dateStyle,\n relative,\n empty = EMPTY_FORMATTED_VALUE,\n locale: localeProp,\n className,\n}: DateMarkProps) {\n const locale = useKitLocale(localeProp);\n const fromProvider = useKitDateFormatter();\n const date = toDate(value);\n const cls = cn(\"whitespace-nowrap tabular-nums\", className);\n if (!date) return <span className={cn(cls, \"text-[var(--text-muted)]\")}>{empty}</span>;\n if (display === \"date\") {\n const own =\n dateStyle === undefined && fromProvider\n ? fromProvider(toLocalIso(date), { unit: \"day\", source: \"dateMark\", locale, weekday: true })\n : \"\";\n return (\n <time dateTime={date.toISOString()} className={cls}>\n {own || formatDate(date, dateStyle ?? \"medium\", { locale })}\n </time>\n );\n }\n const text = formatRelativeTime(date, { ...relative, locale, absoluteStyle: dateStyle ?? \"medium\" });\n return (\n <Tooltip label={formatDate(date, dateStyle ?? \"dateTime\", { locale })} lazy>\n {/* Not a tab stop: a roster of fifty rows would be fifty more of them, and the\n relative words already answer the column's question; the exact time is extra. */}\n <time dateTime={date.toISOString()} className={cls}>\n {text}\n </time>\n </Tooltip>\n );\n}\n\nexport interface DateColumnOptions<T> extends Omit<DateMarkProps, \"value\" | \"className\"> {\n key: string;\n header: ReactNode;\n /** The value of this row: an ISO string, epoch ms, a Date, or null for none. */\n value: (row: T) => DateInput;\n /** Sortable by the instant; rows with no date sort last either way. Default true. */\n sortable?: boolean;\n /** Default `desc` — a date column is sorted to find the newest. */\n firstSort?: \"asc\" | \"desc\";\n /** Add the table's from–to date filter over the column. Default false. */\n filter?: boolean;\n /** Further column settings (`mobileHidden`, `headClassName`, …), merged over. */\n column?: Partial<DataTableColumn<T>>;\n}\n\n/**\n * A {@link DataTableColumn} for a date: {@link DateMark} cells, sorted by the instant\n * (newest first on the first click), optionally the date filter. The roster's\n * \"Created\" is `dateColumn({ key, header, value })`; its \"Last login\" adds\n * `display: \"relative\", empty: t(\"never\")`.\n */\nexport function dateColumn<T>({\n key,\n header,\n value,\n sortable = true,\n firstSort = \"desc\",\n filter = false,\n column,\n ...mark\n}: DateColumnOptions<T>): DataTableColumn<T> {\n return {\n key,\n header,\n cell: (row) => <DateMark value={value(row)} {...mark} />,\n sortBy: sortable ? (row) => toDate(value(row))?.getTime() ?? null : undefined,\n firstSort: sortable ? firstSort : undefined,\n filter: filter\n ? {\n type: \"date\",\n // The filter compares the first ten characters, so hand it an ISO string.\n getValue: (row) => {\n const raw = value(row);\n if (typeof raw === \"string\") return raw;\n return toDate(raw)?.toISOString() ?? null;\n },\n }\n : undefined,\n ...column,\n };\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/components/account-chips.tsx"],"sourcesContent":["import type { ReactElement, ReactNode } from \"react\";\nimport type { LucideIcon } from \"lucide-react\";\nimport { cn } from \"../lib/cn\";\nimport { formatDate, formatRelativeTime, toDate, EMPTY_FORMATTED_VALUE } from \"../lib/format\";\nimport type { DateInput, FormatDateStyle, FormatRelativeTimeOptions } from \"../lib/format\";\nimport { useKitDateFormatter, useKitLabels, useKitLocale } from \"../i18n/kit-labels\";\nimport { toLocalIso } from \"../lib/dates\";\nimport { Chip } from \"./chip\";\nimport type { ChipShape, ChipSize, ChipTone, ChipVariant } from \"./chip\";\nimport { Tooltip } from \"./tooltip\";\nimport type { DataTableColumn } from \"./data-table\";\n\n/**\n * The parts an admin roster repeats (0.18.0) — and only those.\n *\n * Kurvenschmiede (features/admin/users-panel.tsx, allowlist-panel.tsx) and keksdose\n * (features/admin/users-panel.tsx, the allowlist card in admin-page.tsx) each built the\n * same table: name over email, a role, whether the account is active, created, last\n * login, a row of actions, and an allowlist with a note. What they share is not the\n * table — the columns around it (keksdose's plan and privacy mode, Kurvenschmiede's\n * owned-work counts and its role SELECT), the actions and every rule about who may do\n * what are each app's own. What they share is the vocabulary: both spelled the same\n * square, uppercase status chip by hand (keksdose's `Badge` with its own tone map,\n * Kurvenschmiede's inline `<Chip size=\"xs\" shape=\"square\" caps>`), and both formatted\n * their two date columns by hand (`toLocaleDateString` in one, a \"3 days ago\" helper\n * in the other). So: {@link RoleChip}, {@link AccountStateChip} and\n * {@link dateColumn}, and the roster stays the app's `DataTable`.\n */\n\n/** The chip look both rosters use: a square, uppercase status tag. Each can be changed. */\ninterface AccountChipLook {\n /** Default `sm` — keksdose's roster; Kurvenschmiede's denser lists pass `xs`. */\n size?: ChipSize;\n /** Default `soft`. `dot` for a column of them that should not outshout the names. */\n variant?: ChipVariant;\n /** Default `square`: a chip in a table cell, beside square controls. */\n shape?: ChipShape;\n /** Default true — the status-badge type. */\n caps?: boolean;\n className?: string;\n [key: `data-${string}`]: string | number | boolean | undefined;\n}\n\n/* ── RoleChip ──────────────────────────────────────────────────────────────── */\n\n/** How one role is drawn. `label` is the app's, translated. */\nexport interface RoleDefinition {\n label: string;\n tone?: ChipTone;\n /** A lucide component or an element — Kurvenschmiede marks its admins with a shield. */\n icon?: LucideIcon | ReactElement;\n}\n\n/**\n * Every role an app has, keyed by the value its API sends. Annotate the app's table\n * with it (`const ROLES: RoleVocabulary<UserRole> = {…}`) and TypeScript asks for a\n * label when a role is added.\n */\nexport type RoleVocabulary<R extends string = string> = Readonly<Record<R, RoleDefinition>>;\n\nexport interface RoleChipProps<R extends string = string> extends AccountChipLook {\n /**\n * The role key as the API sends it. `null` / `undefined` renders nothing. Named\n * `value`, not `role`, because `role` on a JSX element is the ARIA role — the lint\n * rule and every reader would take it for one.\n */\n value: R | null | undefined;\n /** What each key is called and how it is toned. A key missing here is shown as the\n * key itself, in `neutral`: an unknown role is still a fact the admin should see. */\n roles: Readonly<Partial<Record<R, RoleDefinition>>>;\n}\n\n/**\n * A role as a chip: the API's key looked up in the app's {@link RoleVocabulary}.\n *\n * The vocabulary is a prop and not kit English, because the roles are the app's —\n * Kurvenschmiede has admin, member and customer, keksdose admin and user, kastlan its\n * own — and so are their names. Carries `data-role` with the key, for a test or a\n * style hook.\n */\nexport function RoleChip<R extends string = string>({\n value: role,\n roles,\n size = \"sm\",\n variant,\n shape = \"square\",\n caps = true,\n className,\n ...rest\n}: RoleChipProps<R>) {\n if (role === null || role === undefined || role === \"\") return null;\n const def = roles[role];\n return (\n <Chip\n {...rest}\n data-role={role}\n tone={def?.tone ?? \"neutral\"}\n icon={def?.icon}\n size={size}\n variant={variant}\n shape={shape}\n caps={caps}\n className={className}\n >\n {def?.label ?? role}\n </Chip>\n );\n}\n\n/* ── AccountStateChip ──────────────────────────────────────────────────────── */\n\n/**\n * The states both rosters show, each as the app's own flag maps onto it:\n *\n * - `active` / `inactive` — the account may sign in, or an admin switched it off\n * (both apps' `is_active`).\n * - `invited` — on the allowlist and mailed, not registered yet (an allowlist row).\n * - `registered` — an allowlist row whose address has an account now (Kurvenschmiede).\n * - `unverified` — the email address is not confirmed (keksdose).\n * - `passwordChange` — must set a new password at the next sign-in (keksdose).\n * - `deletion` — 0.30.0: the user asked for the account to be deleted\n * (docs/user-admin-harmonization.md §2.1, §6.4). It is deactivated at once and erased\n * on a date (`after_days`: keksdose, Kurvenschmiede) or by an operator (kastlan). The\n * chip says the date when there is one ({@link AccountStateChipProps.date}), and\n * \"Deletion requested\" when there is none. It stands IN PLACE of `inactive`: an\n * account marked for deletion is always deactivated, and two chips would say it twice.\n *\n * An account can be several at once (inactive AND unverified): render one chip per\n * state, as keksdose's state column does.\n */\nexport type AccountState =\n | \"active\"\n | \"inactive\"\n | \"invited\"\n | \"registered\"\n | \"unverified\"\n | \"passwordChange\"\n | \"deletion\";\n\n/**\n * The `accountState` namespace. One word per state, plus the two forms of `deletion`.\n *\n * The 0.30 keys are OPTIONAL, like every key added to a namespace an app annotates\n * (`const L: AccountStateLabels = {…}`): a new required key would be a compile error in\n * each app on its next update. Left out, they fall back to the provider's, then English.\n */\nexport interface AccountStateLabels extends Record<Exclude<AccountState, \"deletion\">, string> {\n /** 0.30.0: an account marked for deletion with no date — kastlan's `operator` mode,\n * where an operator erases it (§6.4). \"Deletion requested\". */\n deletion?: string;\n /** 0.30.0: an account that will be erased on `date`, already formatted in the\n * reader's locale — the `after_days` mode. \"Deletion on 6 Nov 2026\". */\n deletionOn?: (date: string) => string;\n}\n\nexport const DEFAULT_ACCOUNT_STATE_LABELS = {\n active: \"Active\",\n inactive: \"Inactive\",\n invited: \"Invited\",\n registered: \"Registered\",\n unverified: \"Unverified\",\n passwordChange: \"Must change password\",\n deletion: \"Deletion requested\",\n deletionOn: (date: string) => `Deletion on ${date}`,\n} satisfies Required<AccountStateLabels>;\n\n/** The default tone of each state: green for the normal case, red for switched off\n * (and for going, `deletion`), amber for something the user still has to do, blue for\n * an open invitation. */\nexport const ACCOUNT_STATE_TONES: Readonly<Record<AccountState, ChipTone>> = {\n active: \"success\",\n inactive: \"danger\",\n invited: \"info\",\n registered: \"neutral\",\n unverified: \"warning\",\n passwordChange: \"warning\",\n // Red like `inactive`, which it replaces: the account cannot sign in, and it is going.\n deletion: \"danger\",\n};\n\nexport interface AccountStateChipProps extends AccountChipLook {\n state: AccountState;\n /**\n * 0.30.0, `state=\"deletion\"` only: the day the account will be erased — the row's\n * `deletion_scheduled_at` (§3.2, §6.4). Formatted as a date in the reader's locale\n * (\"Deletion on 6 Nov 2026\"), through the provider's `formatDate` when the app set\n * one, as {@link DateMark} does. Missing or `null` — an operator erases it, so there\n * is no date — the chip says \"Deletion requested\". Ignored for every other state.\n */\n date?: DateInput;\n /** Over the provider's locale, for the `deletion` date. */\n locale?: string;\n /** Over {@link ACCOUNT_STATE_TONES} — Kurvenschmiede draws `inactive` neutral. */\n tone?: ChipTone;\n /** Over the label, for an app whose word for the state is its own (\"Deactivated\"). */\n children?: ReactNode;\n /** Prop > `<UiKitProvider labels={{ accountState }}>` > English. */\n labels?: Partial<AccountStateLabels>;\n}\n\n/**\n * An account's state as a chip, in the kit's words and tones. Carries `data-state`\n * with the key.\n */\nexport function AccountStateChip({\n state,\n date,\n locale: localeProp,\n tone,\n children,\n labels: labelsProp,\n size = \"sm\",\n variant,\n shape = \"square\",\n caps = true,\n className,\n ...rest\n}: AccountStateChipProps) {\n // An `undefined` in the prop is skipped (the provider's merge does that), so an\n // optional value cannot blank a default.\n const labels = useKitLabels(\"accountState\", DEFAULT_ACCOUNT_STATE_LABELS, labelsProp);\n const locale = useKitLocale(localeProp);\n const fromProvider = useKitDateFormatter();\n const when = state === \"deletion\" ? toDate(date ?? null) : null;\n let text: ReactNode;\n if (state !== \"deletion\") text = labels[state];\n else if (when) {\n // A date in a SENTENCE, so no weekday (keksdose's rule, KitDateFormatContext).\n const day =\n (fromProvider && fromProvider(toLocalIso(when), { unit: \"day\", source: \"dateMark\", locale, weekday: false })) ||\n formatDate(when, \"medium\", { locale });\n text = (labels.deletionOn ?? DEFAULT_ACCOUNT_STATE_LABELS.deletionOn)(day);\n } else text = labels.deletion ?? DEFAULT_ACCOUNT_STATE_LABELS.deletion;\n return (\n <Chip\n {...rest}\n data-state={state}\n data-date={when ? toLocalIso(when) : undefined}\n tone={tone ?? ACCOUNT_STATE_TONES[state]}\n size={size}\n variant={variant}\n shape={shape}\n caps={caps}\n className={className}\n >\n {children ?? text}\n </Chip>\n );\n}\n\n/* ── Date columns ──────────────────────────────────────────────────────────── */\n\nexport interface DateMarkProps {\n value: DateInput;\n /**\n * `\"date\"` (default): the date in `dateStyle` — \"Created\". `\"relative\"`: how long\n * ago — \"Last login\", where the question is \"recently?\" — with the full date and\n * time in a tooltip (keksdose's last-seen column).\n */\n display?: \"date\" | \"relative\";\n /** Default `medium`. Under `\"relative\"`, the style of the tooltip's date (default\n * `dateTime`) and of the date `relative.absoluteAfterDays` switches to. Given, it wins\n * over the provider's `formatDate`. */\n dateStyle?: FormatDateStyle | Intl.DateTimeFormatOptions;\n /** `formatRelativeTime` options: `numeric`, `style`, `absoluteAfterDays`, `now`. */\n relative?: Omit<FormatRelativeTimeOptions, \"locale\" | \"empty\" | \"absoluteStyle\">;\n /** What a missing value reads as. Default \"—\"; a last-login column passes its\n * translated \"Never\". */\n empty?: ReactNode;\n /** Over the provider's locale. */\n locale?: string;\n className?: string;\n}\n\n/**\n * A date in a table cell: a `<time>` with a machine-readable `dateTime`, nowrap and\n * tabular figures, in the provider's locale — or `empty` when there is none.\n *\n * `display=\"date\"` without a `dateStyle` is written by `<UiKitProvider formatDate>` when\n * the app set one (keksdose K12: its `DateCell` exists to put every column's date in the\n * app's date-format preference, weekday and all — dev#546), called with the LOCAL\n * calendar day of the value as `\"YYYY-MM-DD\"` and `weekday: true`. Without one, or for\n * an empty answer, it is `Intl`'s `medium` date as before. The relative display is not\n * a calendar day and keeps `formatRelativeTime`.\n */\nexport function DateMark({\n value,\n display = \"date\",\n dateStyle,\n relative,\n empty = EMPTY_FORMATTED_VALUE,\n locale: localeProp,\n className,\n}: DateMarkProps) {\n const locale = useKitLocale(localeProp);\n const fromProvider = useKitDateFormatter();\n const date = toDate(value);\n const cls = cn(\"whitespace-nowrap tabular-nums\", className);\n if (!date) return <span className={cn(cls, \"text-[var(--text-muted)]\")}>{empty}</span>;\n if (display === \"date\") {\n const own =\n dateStyle === undefined && fromProvider\n ? fromProvider(toLocalIso(date), { unit: \"day\", source: \"dateMark\", locale, weekday: true })\n : \"\";\n return (\n <time dateTime={date.toISOString()} className={cls}>\n {own || formatDate(date, dateStyle ?? \"medium\", { locale })}\n </time>\n );\n }\n const text = formatRelativeTime(date, { ...relative, locale, absoluteStyle: dateStyle ?? \"medium\" });\n return (\n <Tooltip label={formatDate(date, dateStyle ?? \"dateTime\", { locale })} lazy>\n {/* Not a tab stop: a roster of fifty rows would be fifty more of them, and the\n relative words already answer the column's question; the exact time is extra. */}\n <time dateTime={date.toISOString()} className={cls}>\n {text}\n </time>\n </Tooltip>\n );\n}\n\nexport interface DateColumnOptions<T> extends Omit<DateMarkProps, \"value\" | \"className\"> {\n key: string;\n header: ReactNode;\n /** The value of this row: an ISO string, epoch ms, a Date, or null for none. */\n value: (row: T) => DateInput;\n /** Sortable by the instant; rows with no date sort last either way. Default true. */\n sortable?: boolean;\n /** Default `desc` — a date column is sorted to find the newest. */\n firstSort?: \"asc\" | \"desc\";\n /** Add the table's from–to date filter over the column. Default false. */\n filter?: boolean;\n /** Further column settings (`mobileHidden`, `headClassName`, …), merged over. */\n column?: Partial<DataTableColumn<T>>;\n}\n\n/**\n * A {@link DataTableColumn} for a date: {@link DateMark} cells, sorted by the instant\n * (newest first on the first click), optionally the date filter. The roster's\n * \"Created\" is `dateColumn({ key, header, value })`; its \"Last login\" adds\n * `display: \"relative\", empty: t(\"never\")`.\n */\nexport function dateColumn<T>({\n key,\n header,\n value,\n sortable = true,\n firstSort = \"desc\",\n filter = false,\n column,\n ...mark\n}: DateColumnOptions<T>): DataTableColumn<T> {\n return {\n key,\n header,\n cell: (row) => <DateMark value={value(row)} {...mark} />,\n sortBy: sortable ? (row) => toDate(value(row))?.getTime() ?? null : undefined,\n firstSort: sortable ? firstSort : undefined,\n filter: filter\n ? {\n type: \"date\",\n // The filter compares the first ten characters, so hand it an ISO string.\n getValue: (row) => {\n const raw = value(row);\n if (typeof raw === \"string\") return raw;\n return toDate(raw)?.toISOString() ?? null;\n },\n }\n : undefined,\n ...column,\n };\n}\n"],"mappings":"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","names":[]}
|
|
@@ -16,8 +16,19 @@ interface ProfileSettingLabels {
|
|
|
16
16
|
email: string;
|
|
17
17
|
role: string;
|
|
18
18
|
memberSince: string;
|
|
19
|
+
/** The single name field — the card without `firstName` / `lastName`. */
|
|
19
20
|
displayName: string;
|
|
20
21
|
save: string;
|
|
22
|
+
/**
|
|
23
|
+
* 0.30.0 (docs/user-admin-harmonization.md §6.1): the two fields that replace
|
|
24
|
+
* `displayName` once the card is given `firstName` / `lastName`. OPTIONAL, like
|
|
25
|
+
* `TwoFactorSettingLabels.qrAlt` and for its reason — this interface is annotated at
|
|
26
|
+
* call sites — and filled from the provider's `accountSettings.profile`, then English.
|
|
27
|
+
*/
|
|
28
|
+
firstName?: string;
|
|
29
|
+
lastName?: string;
|
|
30
|
+
/** Why Save is held while a name is blank: both are required (§3.1). */
|
|
31
|
+
nameRequired?: string;
|
|
21
32
|
}
|
|
22
33
|
interface PasswordSettingLabels {
|
|
23
34
|
title: string;
|
|
@@ -96,7 +107,7 @@ interface PasskeysSettingLabels {
|
|
|
96
107
|
neverUsed: string;
|
|
97
108
|
}
|
|
98
109
|
interface AccountSettingsLabels {
|
|
99
|
-
profile: ProfileSettingLabels
|
|
110
|
+
profile: Required<ProfileSettingLabels>;
|
|
100
111
|
password: PasswordSettingLabels;
|
|
101
112
|
twoFactor: Required<TwoFactorSettingLabels>;
|
|
102
113
|
passkeys: Required<PasskeysSettingLabels>;
|
|
@@ -8,7 +8,10 @@ const DEFAULT_ACCOUNT_SETTINGS_LABELS = {
|
|
|
8
8
|
role: "Role",
|
|
9
9
|
memberSince: "Member since",
|
|
10
10
|
displayName: "Display name",
|
|
11
|
-
save: "Save"
|
|
11
|
+
save: "Save",
|
|
12
|
+
firstName: "First name",
|
|
13
|
+
lastName: "Last name",
|
|
14
|
+
nameRequired: "Enter both a first and a last name."
|
|
12
15
|
},
|
|
13
16
|
password: {
|
|
14
17
|
title: "Change password",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/account-settings-labels.ts"],"sourcesContent":["import { useMemo } from \"react\";\nimport { useKitLabelOverrides } from \"../i18n/kit-labels\";\n\n/**\n * The `accountSettings` namespace of `<UiKitProvider labels>`: one record per section —\n * `profile`, `password`, `twoFactor`, `passkeys` — each merged key by key, so an app\n * can translate `accountSettings.twoFactor.scanHint` alone.\n *\n * Before 0.12.0 every section took a REQUIRED `labels` prop and had no English of its\n * own, so each app spelled out 30-odd keys at the call site and still hard-coded some\n * (kastlan's profile page passed `email: \"Email\"` in English to a German page). The\n * prop stays, now partial, and wins over the provider as everywhere else in the kit.\n *\n * Its own module, beside the components rather than in them, so `PasskeysSetting`\n * (its own file) and the older three read one tree without importing each other.\n */\n\nexport interface ProfileSettingLabels {\n title: string;\n email: string;\n role: string;\n memberSince: string;\n displayName: string;\n save: string;\n}\n\nexport interface PasswordSettingLabels {\n title: string;\n current: string;\n next: string;\n confirm: string;\n submit: string;\n tooShort: string;\n mismatch: string;\n}\n\nexport interface TwoFactorSettingLabels {\n status: string;\n enabledText: string;\n disabledText: string;\n enable: string;\n scanHint: string;\n codeLabel: string;\n verify: string;\n disableSection: string;\n password: string;\n disable: string;\n /**\n * Alt text for the setup QR code, which shipped as a hardcoded `alt=\"QR\"`.\n *\n * OPTIONAL, like the two keys below, and not because it matters less: this interface\n * is annotated at consumer call sites (`const LABELS: TwoFactorSettingLabels = {…}`),\n * so a new REQUIRED key is a compile error in every app on the next `npm update` —\n * the additive-API rule in the README. Each falls back to the provider's\n * `accountSettings.twoFactor`, then English.\n */\n qrAlt?: string;\n /** The line over the manual-entry key, for a phone that cannot scan its own screen. */\n secretHint?: string;\n /** The copy button beside that key. */\n copySecret?: string;\n}\n\nexport interface PasskeysSettingLabels {\n title: string;\n /** The line under the title saying what a passkey is for — `mode=\"instead\"` (the\n * default): the passkey stands in for the password. */\n description: string;\n /**\n * The same line for `mode=\"alongside\"` (0.18.0): the password keeps working and the\n * passkey is a second way in. OPTIONAL for the reason `TwoFactorSettingLabels.qrAlt`\n * is — this interface is annotated at call sites — and filled from the provider's\n * `accountSettings.passkeys`, then English.\n */\n descriptionAlongside?: string;\n empty: string;\n loading: string;\n /** Accessible name of the list. */\n list: string;\n nameLabel: string;\n namePlaceholder: string;\n add: string;\n /** The add button while the app's WebAuthn ceremony runs — the OS prompt is open. */\n adding: string;\n rename: string;\n /** The row's rename button, named for its passkey — it starts with `rename`, the\n * visible text, so voice control finds it by what it says. */\n renameItem: (name: string) => string;\n /** Accessible name of the inline rename field. */\n renameField: (name: string) => string;\n save: string;\n cancel: string;\n delete: string;\n /** The row's delete button, named for its passkey (see `renameItem`). */\n deleteItem: (name: string) => string;\n /** The inline question a delete asks first. */\n deleteConfirm: (name: string) => string;\n /** The destructive button that answers it. */\n confirmDelete: string;\n /** `date` arrives formatted in the provider's locale. */\n created: (date: string) => string;\n lastUsed: (date: string) => string;\n neverUsed: string;\n}\n\nexport interface AccountSettingsLabels {\n profile: ProfileSettingLabels;\n password: PasswordSettingLabels;\n // `Required` here, optional on the section interfaces: the namespace is what a full\n // catalogue (`@eifi1/ui-kit/i18n/<code>`, `missingKitLabels`) is written against, so\n // it must name the late keys; the section interfaces are annotated at call sites.\n twoFactor: Required<TwoFactorSettingLabels>;\n passkeys: Required<PasskeysSettingLabels>;\n}\n\nexport const DEFAULT_ACCOUNT_SETTINGS_LABELS: AccountSettingsLabels = {\n profile: {\n title: \"Profile\",\n email: \"Email\",\n role: \"Role\",\n memberSince: \"Member since\",\n displayName: \"Display name\",\n save: \"Save\",\n },\n password: {\n title: \"Change password\",\n current: \"Current password\",\n next: \"New password\",\n confirm: \"Confirm new password\",\n submit: \"Change password\",\n tooShort: \"The new password is too short.\",\n mismatch: \"The passwords don’t match.\",\n },\n twoFactor: {\n status: \"Two-factor authentication\",\n enabledText: \"On\",\n disabledText: \"Off\",\n enable: \"Set up two-factor authentication\",\n scanHint: \"Scan this code with your authenticator app, then enter the code it shows.\",\n codeLabel: \"Verification code\",\n verify: \"Verify and turn on\",\n disableSection: \"Turn off two-factor authentication\",\n password: \"Current password\",\n disable: \"Turn off\",\n // Unchanged from the fallback it had before the namespace (i18n-labels.test pins it).\n qrAlt: \"QR code\",\n secretHint: \"Can’t scan it? Enter this key in the app instead:\",\n copySecret: \"Copy key\",\n },\n passkeys: {\n title: \"Passkeys\",\n description: \"Sign in with your fingerprint, face or device PIN instead of a password.\",\n // Kurvenschmiede overrode `description` because \"instead of\" is false where the\n // password stays; this is the sentence for those apps, picked by `mode`.\n descriptionAlongside: \"Sign in with your fingerprint, face or device PIN. Your password keeps working too.\",\n empty: \"No passkeys yet\",\n loading: \"Loading passkeys…\",\n list: \"Your passkeys\",\n nameLabel: \"Name (optional)\",\n namePlaceholder: \"e.g. Laptop, Phone\",\n add: \"Add passkey\",\n adding: \"Waiting for your device…\",\n rename: \"Rename\",\n renameItem: (name) => `Rename ${name}`,\n renameField: (name) => `New name for ${name}`,\n save: \"Save\",\n cancel: \"Cancel\",\n delete: \"Delete\",\n deleteItem: (name) => `Delete ${name}`,\n deleteConfirm: (name) => `Delete “${name}”? It can no longer be used to sign in.`,\n confirmDelete: \"Delete passkey\",\n created: (date) => `Added ${date}`,\n lastUsed: (date) => `Last used ${date}`,\n neverUsed: \"Never used\",\n },\n};\n\n/**\n * One section of the namespace, resolved: English, then the provider's\n * `accountSettings.<section>`, then the component's own `labels` prop. An `undefined`\n * in either source is skipped, so a prop object built from optional values cannot\n * blank out a default.\n */\nexport function useAccountSettingsLabels<K extends keyof AccountSettingsLabels>(\n section: K,\n prop?: Partial<AccountSettingsLabels[K]>,\n): AccountSettingsLabels[K] {\n const fromProvider = useKitLabelOverrides(\"accountSettings\")?.[section] as\n | Partial<AccountSettingsLabels[K]>\n | undefined;\n return useMemo(\n () => ({\n ...DEFAULT_ACCOUNT_SETTINGS_LABELS[section],\n ...definedOnly(fromProvider),\n ...definedOnly(prop),\n }),\n [section, fromProvider, prop],\n );\n}\n\nfunction definedOnly<T extends object>(value: T | undefined): Partial<T> {\n if (!value) return {};\n return Object.fromEntries(Object.entries(value).filter(([, v]) => v !== undefined)) as Partial<T>;\n}\n"],"mappings":";AAAA,SAAS,eAAe;AACxB,SAAS,4BAA4B;AAkH9B,MAAM,kCAAyD;AAAA,EACpE,SAAS;AAAA,IACP,OAAO;AAAA,IACP,OAAO;AAAA,IACP,MAAM;AAAA,IACN,aAAa;AAAA,IACb,aAAa;AAAA,IACb,MAAM;AAAA,EACR;AAAA,EACA,UAAU;AAAA,IACR,OAAO;AAAA,IACP,SAAS;AAAA,IACT,MAAM;AAAA,IACN,SAAS;AAAA,IACT,QAAQ;AAAA,IACR,UAAU;AAAA,IACV,UAAU;AAAA,EACZ;AAAA,EACA,WAAW;AAAA,IACT,QAAQ;AAAA,IACR,aAAa;AAAA,IACb,cAAc;AAAA,IACd,QAAQ;AAAA,IACR,UAAU;AAAA,IACV,WAAW;AAAA,IACX,QAAQ;AAAA,IACR,gBAAgB;AAAA,IAChB,UAAU;AAAA,IACV,SAAS;AAAA;AAAA,IAET,OAAO;AAAA,IACP,YAAY;AAAA,IACZ,YAAY;AAAA,EACd;AAAA,EACA,UAAU;AAAA,IACR,OAAO;AAAA,IACP,aAAa;AAAA;AAAA;AAAA,IAGb,sBAAsB;AAAA,IACtB,OAAO;AAAA,IACP,SAAS;AAAA,IACT,MAAM;AAAA,IACN,WAAW;AAAA,IACX,iBAAiB;AAAA,IACjB,KAAK;AAAA,IACL,QAAQ;AAAA,IACR,QAAQ;AAAA,IACR,YAAY,CAAC,SAAS,UAAU,IAAI;AAAA,IACpC,aAAa,CAAC,SAAS,gBAAgB,IAAI;AAAA,IAC3C,MAAM;AAAA,IACN,QAAQ;AAAA,IACR,QAAQ;AAAA,IACR,YAAY,CAAC,SAAS,UAAU,IAAI;AAAA,IACpC,eAAe,CAAC,SAAS,gBAAW,IAAI;AAAA,IACxC,eAAe;AAAA,IACf,SAAS,CAAC,SAAS,SAAS,IAAI;AAAA,IAChC,UAAU,CAAC,SAAS,aAAa,IAAI;AAAA,IACrC,WAAW;AAAA,EACb;AACF;AAQO,SAAS,yBACd,SACA,MAC0B;AAC1B,QAAM,eAAe,qBAAqB,iBAAiB,IAAI,OAAO;AAGtE,SAAO;AAAA,IACL,OAAO;AAAA,MACL,GAAG,gCAAgC,OAAO;AAAA,MAC1C,GAAG,YAAY,YAAY;AAAA,MAC3B,GAAG,YAAY,IAAI;AAAA,IACrB;AAAA,IACA,CAAC,SAAS,cAAc,IAAI;AAAA,EAC9B;AACF;AAEA,SAAS,YAA8B,OAAkC;AACvE,MAAI,CAAC,MAAO,QAAO,CAAC;AACpB,SAAO,OAAO,YAAY,OAAO,QAAQ,KAAK,EAAE,OAAO,CAAC,CAAC,EAAE,CAAC,MAAM,MAAM,MAAS,CAAC;AACpF;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/components/account-settings-labels.ts"],"sourcesContent":["import { useMemo } from \"react\";\nimport { useKitLabelOverrides } from \"../i18n/kit-labels\";\n\n/**\n * The `accountSettings` namespace of `<UiKitProvider labels>`: one record per section —\n * `profile`, `password`, `twoFactor`, `passkeys` — each merged key by key, so an app\n * can translate `accountSettings.twoFactor.scanHint` alone.\n *\n * Before 0.12.0 every section took a REQUIRED `labels` prop and had no English of its\n * own, so each app spelled out 30-odd keys at the call site and still hard-coded some\n * (kastlan's profile page passed `email: \"Email\"` in English to a German page). The\n * prop stays, now partial, and wins over the provider as everywhere else in the kit.\n *\n * Its own module, beside the components rather than in them, so `PasskeysSetting`\n * (its own file) and the older three read one tree without importing each other.\n */\n\nexport interface ProfileSettingLabels {\n title: string;\n email: string;\n role: string;\n memberSince: string;\n /** The single name field — the card without `firstName` / `lastName`. */\n displayName: string;\n save: string;\n /**\n * 0.30.0 (docs/user-admin-harmonization.md §6.1): the two fields that replace\n * `displayName` once the card is given `firstName` / `lastName`. OPTIONAL, like\n * `TwoFactorSettingLabels.qrAlt` and for its reason — this interface is annotated at\n * call sites — and filled from the provider's `accountSettings.profile`, then English.\n */\n firstName?: string;\n lastName?: string;\n /** Why Save is held while a name is blank: both are required (§3.1). */\n nameRequired?: string;\n}\n\nexport interface PasswordSettingLabels {\n title: string;\n current: string;\n next: string;\n confirm: string;\n submit: string;\n tooShort: string;\n mismatch: string;\n}\n\nexport interface TwoFactorSettingLabels {\n status: string;\n enabledText: string;\n disabledText: string;\n enable: string;\n scanHint: string;\n codeLabel: string;\n verify: string;\n disableSection: string;\n password: string;\n disable: string;\n /**\n * Alt text for the setup QR code, which shipped as a hardcoded `alt=\"QR\"`.\n *\n * OPTIONAL, like the two keys below, and not because it matters less: this interface\n * is annotated at consumer call sites (`const LABELS: TwoFactorSettingLabels = {…}`),\n * so a new REQUIRED key is a compile error in every app on the next `npm update` —\n * the additive-API rule in the README. Each falls back to the provider's\n * `accountSettings.twoFactor`, then English.\n */\n qrAlt?: string;\n /** The line over the manual-entry key, for a phone that cannot scan its own screen. */\n secretHint?: string;\n /** The copy button beside that key. */\n copySecret?: string;\n}\n\nexport interface PasskeysSettingLabels {\n title: string;\n /** The line under the title saying what a passkey is for — `mode=\"instead\"` (the\n * default): the passkey stands in for the password. */\n description: string;\n /**\n * The same line for `mode=\"alongside\"` (0.18.0): the password keeps working and the\n * passkey is a second way in. OPTIONAL for the reason `TwoFactorSettingLabels.qrAlt`\n * is — this interface is annotated at call sites — and filled from the provider's\n * `accountSettings.passkeys`, then English.\n */\n descriptionAlongside?: string;\n empty: string;\n loading: string;\n /** Accessible name of the list. */\n list: string;\n nameLabel: string;\n namePlaceholder: string;\n add: string;\n /** The add button while the app's WebAuthn ceremony runs — the OS prompt is open. */\n adding: string;\n rename: string;\n /** The row's rename button, named for its passkey — it starts with `rename`, the\n * visible text, so voice control finds it by what it says. */\n renameItem: (name: string) => string;\n /** Accessible name of the inline rename field. */\n renameField: (name: string) => string;\n save: string;\n cancel: string;\n delete: string;\n /** The row's delete button, named for its passkey (see `renameItem`). */\n deleteItem: (name: string) => string;\n /** The inline question a delete asks first. */\n deleteConfirm: (name: string) => string;\n /** The destructive button that answers it. */\n confirmDelete: string;\n /** `date` arrives formatted in the provider's locale. */\n created: (date: string) => string;\n lastUsed: (date: string) => string;\n neverUsed: string;\n}\n\nexport interface AccountSettingsLabels {\n profile: Required<ProfileSettingLabels>;\n password: PasswordSettingLabels;\n // `Required` here, optional on the section interfaces: the namespace is what a full\n // catalogue (`@eifi1/ui-kit/i18n/<code>`, `missingKitLabels`) is written against, so\n // it must name the late keys; the section interfaces are annotated at call sites.\n twoFactor: Required<TwoFactorSettingLabels>;\n passkeys: Required<PasskeysSettingLabels>;\n}\n\nexport const DEFAULT_ACCOUNT_SETTINGS_LABELS: AccountSettingsLabels = {\n profile: {\n title: \"Profile\",\n email: \"Email\",\n role: \"Role\",\n memberSince: \"Member since\",\n displayName: \"Display name\",\n save: \"Save\",\n firstName: \"First name\",\n lastName: \"Last name\",\n nameRequired: \"Enter both a first and a last name.\",\n },\n password: {\n title: \"Change password\",\n current: \"Current password\",\n next: \"New password\",\n confirm: \"Confirm new password\",\n submit: \"Change password\",\n tooShort: \"The new password is too short.\",\n mismatch: \"The passwords don’t match.\",\n },\n twoFactor: {\n status: \"Two-factor authentication\",\n enabledText: \"On\",\n disabledText: \"Off\",\n enable: \"Set up two-factor authentication\",\n scanHint: \"Scan this code with your authenticator app, then enter the code it shows.\",\n codeLabel: \"Verification code\",\n verify: \"Verify and turn on\",\n disableSection: \"Turn off two-factor authentication\",\n password: \"Current password\",\n disable: \"Turn off\",\n // Unchanged from the fallback it had before the namespace (i18n-labels.test pins it).\n qrAlt: \"QR code\",\n secretHint: \"Can’t scan it? Enter this key in the app instead:\",\n copySecret: \"Copy key\",\n },\n passkeys: {\n title: \"Passkeys\",\n description: \"Sign in with your fingerprint, face or device PIN instead of a password.\",\n // Kurvenschmiede overrode `description` because \"instead of\" is false where the\n // password stays; this is the sentence for those apps, picked by `mode`.\n descriptionAlongside: \"Sign in with your fingerprint, face or device PIN. Your password keeps working too.\",\n empty: \"No passkeys yet\",\n loading: \"Loading passkeys…\",\n list: \"Your passkeys\",\n nameLabel: \"Name (optional)\",\n namePlaceholder: \"e.g. Laptop, Phone\",\n add: \"Add passkey\",\n adding: \"Waiting for your device…\",\n rename: \"Rename\",\n renameItem: (name) => `Rename ${name}`,\n renameField: (name) => `New name for ${name}`,\n save: \"Save\",\n cancel: \"Cancel\",\n delete: \"Delete\",\n deleteItem: (name) => `Delete ${name}`,\n deleteConfirm: (name) => `Delete “${name}”? It can no longer be used to sign in.`,\n confirmDelete: \"Delete passkey\",\n created: (date) => `Added ${date}`,\n lastUsed: (date) => `Last used ${date}`,\n neverUsed: \"Never used\",\n },\n};\n\n/**\n * One section of the namespace, resolved: English, then the provider's\n * `accountSettings.<section>`, then the component's own `labels` prop. An `undefined`\n * in either source is skipped, so a prop object built from optional values cannot\n * blank out a default.\n */\nexport function useAccountSettingsLabels<K extends keyof AccountSettingsLabels>(\n section: K,\n prop?: Partial<AccountSettingsLabels[K]>,\n): AccountSettingsLabels[K] {\n const fromProvider = useKitLabelOverrides(\"accountSettings\")?.[section] as\n | Partial<AccountSettingsLabels[K]>\n | undefined;\n return useMemo(\n () => ({\n ...DEFAULT_ACCOUNT_SETTINGS_LABELS[section],\n ...definedOnly(fromProvider),\n ...definedOnly(prop),\n }),\n [section, fromProvider, prop],\n );\n}\n\nfunction definedOnly<T extends object>(value: T | undefined): Partial<T> {\n if (!value) return {};\n return Object.fromEntries(Object.entries(value).filter(([, v]) => v !== undefined)) as Partial<T>;\n}\n"],"mappings":";AAAA,SAAS,eAAe;AACxB,SAAS,4BAA4B;AA6H9B,MAAM,kCAAyD;AAAA,EACpE,SAAS;AAAA,IACP,OAAO;AAAA,IACP,OAAO;AAAA,IACP,MAAM;AAAA,IACN,aAAa;AAAA,IACb,aAAa;AAAA,IACb,MAAM;AAAA,IACN,WAAW;AAAA,IACX,UAAU;AAAA,IACV,cAAc;AAAA,EAChB;AAAA,EACA,UAAU;AAAA,IACR,OAAO;AAAA,IACP,SAAS;AAAA,IACT,MAAM;AAAA,IACN,SAAS;AAAA,IACT,QAAQ;AAAA,IACR,UAAU;AAAA,IACV,UAAU;AAAA,EACZ;AAAA,EACA,WAAW;AAAA,IACT,QAAQ;AAAA,IACR,aAAa;AAAA,IACb,cAAc;AAAA,IACd,QAAQ;AAAA,IACR,UAAU;AAAA,IACV,WAAW;AAAA,IACX,QAAQ;AAAA,IACR,gBAAgB;AAAA,IAChB,UAAU;AAAA,IACV,SAAS;AAAA;AAAA,IAET,OAAO;AAAA,IACP,YAAY;AAAA,IACZ,YAAY;AAAA,EACd;AAAA,EACA,UAAU;AAAA,IACR,OAAO;AAAA,IACP,aAAa;AAAA;AAAA;AAAA,IAGb,sBAAsB;AAAA,IACtB,OAAO;AAAA,IACP,SAAS;AAAA,IACT,MAAM;AAAA,IACN,WAAW;AAAA,IACX,iBAAiB;AAAA,IACjB,KAAK;AAAA,IACL,QAAQ;AAAA,IACR,QAAQ;AAAA,IACR,YAAY,CAAC,SAAS,UAAU,IAAI;AAAA,IACpC,aAAa,CAAC,SAAS,gBAAgB,IAAI;AAAA,IAC3C,MAAM;AAAA,IACN,QAAQ;AAAA,IACR,QAAQ;AAAA,IACR,YAAY,CAAC,SAAS,UAAU,IAAI;AAAA,IACpC,eAAe,CAAC,SAAS,gBAAW,IAAI;AAAA,IACxC,eAAe;AAAA,IACf,SAAS,CAAC,SAAS,SAAS,IAAI;AAAA,IAChC,UAAU,CAAC,SAAS,aAAa,IAAI;AAAA,IACrC,WAAW;AAAA,EACb;AACF;AAQO,SAAS,yBACd,SACA,MAC0B;AAC1B,QAAM,eAAe,qBAAqB,iBAAiB,IAAI,OAAO;AAGtE,SAAO;AAAA,IACL,OAAO;AAAA,MACL,GAAG,gCAAgC,OAAO;AAAA,MAC1C,GAAG,YAAY,YAAY;AAAA,MAC3B,GAAG,YAAY,IAAI;AAAA,IACrB;AAAA,IACA,CAAC,SAAS,cAAc,IAAI;AAAA,EAC9B;AACF;AAEA,SAAS,YAA8B,OAAkC;AACvE,MAAI,CAAC,MAAO,QAAO,CAAC;AACpB,SAAO,OAAO,YAAY,OAAO,QAAQ,KAAK,EAAE,OAAO,CAAC,CAAC,EAAE,CAAC,MAAM,MAAM,MAAS,CAAC;AACpF;","names":[]}
|
|
@@ -13,18 +13,73 @@ export { AccountSettingsLabels, DEFAULT_ACCOUNT_SETTINGS_LABELS, PasskeysSetting
|
|
|
13
13
|
* Every `labels` prop is optional since 0.12.0: the strings come from the provider's
|
|
14
14
|
* `accountSettings` namespace, then English (see account-settings-labels.ts).
|
|
15
15
|
*/
|
|
16
|
-
|
|
16
|
+
/** What {@link ProfileSetting} hands `onSave` in its two-field form (0.30.0): both
|
|
17
|
+
* trimmed, 1–120 characters each — the `PATCH /auth/me` body's `first_name` /
|
|
18
|
+
* `last_name` (docs/user-admin-harmonization.md §6.1). */
|
|
19
|
+
interface ProfileNameValues {
|
|
20
|
+
firstName: string;
|
|
21
|
+
lastName: string;
|
|
22
|
+
}
|
|
23
|
+
/** What both forms of {@link ProfileSetting} take. */
|
|
24
|
+
interface ProfileSettingCommonProps {
|
|
17
25
|
name?: string | null;
|
|
18
26
|
email?: string | null;
|
|
19
27
|
role?: ReactNode;
|
|
20
28
|
memberSince?: ReactNode;
|
|
21
|
-
value: string;
|
|
22
|
-
onChange: (value: string) => void;
|
|
23
|
-
onSave: () => void;
|
|
24
29
|
saving?: boolean;
|
|
25
30
|
/** Prop > `<UiKitProvider labels={{ accountSettings: { profile } }}>` > English. */
|
|
26
31
|
labels?: Partial<ProfileSettingLabels>;
|
|
27
|
-
}
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* The single display name, as before 0.30.0 — controlled through `value` / `onChange`,
|
|
35
|
+
* saved by `onSave()`. Kept unchanged for an app that has not moved to first and last
|
|
36
|
+
* name yet.
|
|
37
|
+
*/
|
|
38
|
+
interface ProfileSettingDisplayNameProps extends ProfileSettingCommonProps {
|
|
39
|
+
value: string;
|
|
40
|
+
onChange: (value: string) => void;
|
|
41
|
+
onSave: () => void;
|
|
42
|
+
firstName?: undefined;
|
|
43
|
+
lastName?: undefined;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* First and last name (0.30.0, docs/user-admin-harmonization.md §6.1): the saved
|
|
47
|
+
* values in, the edited ones out through `onSave`. The card keeps the drafts itself.
|
|
48
|
+
*/
|
|
49
|
+
interface ProfileSettingNamesProps extends ProfileSettingCommonProps {
|
|
50
|
+
/** The saved given name — `first_name`. `null` reads as empty. */
|
|
51
|
+
firstName: string | null;
|
|
52
|
+
/** The saved family name — `last_name`. `null` (a migrated account, §3.3) reads as
|
|
53
|
+
* empty, and Save then waits for one. */
|
|
54
|
+
lastName: string | null;
|
|
55
|
+
/**
|
|
56
|
+
* Save the two names (`PATCH /auth/me`, which also clears `name_incomplete`). Return
|
|
57
|
+
* a promise and Save is busy until it settles. Refresh `firstName` / `lastName` from
|
|
58
|
+
* the answer: the card compares its drafts with them, so Save turns off once the new
|
|
59
|
+
* names come back. A rejection keeps what was typed — the app says why (a toast), as
|
|
60
|
+
* for `PasswordSetting`.
|
|
61
|
+
*/
|
|
62
|
+
onSave: (values: ProfileNameValues) => void | Promise<unknown>;
|
|
63
|
+
value?: undefined;
|
|
64
|
+
onChange?: undefined;
|
|
65
|
+
}
|
|
66
|
+
type ProfileSettingProps = ProfileSettingDisplayNameProps | ProfileSettingNamesProps;
|
|
67
|
+
/**
|
|
68
|
+
* The profile section: avatar, address, role, member since, and the name.
|
|
69
|
+
*
|
|
70
|
+
* **Two forms.** Given `firstName` / `lastName` (0.30.0, §6.1) it edits first and last
|
|
71
|
+
* name in two fields, side by side where the card is wide enough — the order and the
|
|
72
|
+
* 1–120-character rule of `RegisterForm` (auth §3.1), so the name a person signed up
|
|
73
|
+
* with is edited the way it was typed. Without them it is the single `displayName`
|
|
74
|
+
* field it always was, `value` / `onChange` / `onSave()` unchanged, for an app that has
|
|
75
|
+
* not moved yet (kastlan split the one name at its first space on save; Kurvenschmiede
|
|
76
|
+
* built its own card instead).
|
|
77
|
+
*
|
|
78
|
+
* The two-field form owns its drafts, starting from the saved names and following them
|
|
79
|
+
* when they change; Save waits for a change, holds — saying why — while a name is
|
|
80
|
+
* blank, and is a `commit` (a {@link WriteLockProvider} locks it, keksdose's demo).
|
|
81
|
+
*/
|
|
82
|
+
declare function ProfileSetting(props: ProfileSettingProps): react.JSX.Element;
|
|
28
83
|
/**
|
|
29
84
|
* What {@link PasswordSetting}'s `strength` takes besides `true`: the meter's own options,
|
|
30
85
|
* minus the ones the card owns — `value` (the new password), `minLength` (the card's, so
|
|
@@ -94,4 +149,4 @@ declare function TwoFactorSetting({ enabled, setup, onStartSetup, onEnable, onDi
|
|
|
94
149
|
labels?: Partial<TwoFactorSettingLabels>;
|
|
95
150
|
}): react.JSX.Element;
|
|
96
151
|
|
|
97
|
-
export { PasswordSetting, PasswordSettingLabels, type PasswordSettingStrength, ProfileSetting, ProfileSettingLabels, TwoFactorSetting, TwoFactorSettingLabels, type TwoFactorSetupData };
|
|
152
|
+
export { PasswordSetting, PasswordSettingLabels, type PasswordSettingStrength, type ProfileNameValues, ProfileSetting, type ProfileSettingDisplayNameProps, ProfileSettingLabels, type ProfileSettingNamesProps, type ProfileSettingProps, TwoFactorSetting, TwoFactorSettingLabels, type TwoFactorSetupData };
|