{
  "contract": "tessli.public-library-item.v1",
  "canonicalPath": "/library/settings-account",
  "representations": {
    "json": "/library/settings-account/item.json",
    "markdown": "/library/settings-account/item.md"
  },
  "preview": "/preview/settings-account",
  "item": {
    "slug": "settings-account",
    "title": "Settings: account and danger zone",
    "kind": "page",
    "category": "settings",
    "summary": "An account settings page whose profile form validates per field and reports saves in place, with a separate danger zone that offers export before a typed, confirmed delete.",
    "status": "community",
    "install": "npx shadcn@latest add online-scope-lab/tessli/settings-account",
    "registry": {
      "type": "registry:block",
      "registryDependencies": [
        "button"
      ],
      "dependencies": []
    },
    "notes": {
      "id": "settings-account",
      "kind": "page",
      "category": "settings",
      "title": "Settings: account and danger zone",
      "summary": "An account settings page whose profile form validates per field and reports saves in place, with a separate danger zone that offers export before a typed, confirmed delete.",
      "use_when": [
        "People edit a small profile (name, sign-in email) and can also close their account from the same page.",
        "Deleting the account removes data people may want to keep, so an export has to come first."
      ],
      "avoid_when": [
        "The settings span many areas (billing, notifications, security); give each its own page and keep only the danger zone here.",
        "The action is reversible, such as archiving or leaving a workspace; offer undo instead of typed confirmation.",
        "Deletion needs an admin or a support request; explain that route instead of showing a button that cannot finish the job."
      ],
      "rules": [
        "ST-05",
        "ST-04",
        "ST-03",
        "A-04",
        "A-05",
        "A-06",
        "H-01",
        "W-02"
      ],
      "ai_default": "One card with Name and Email inputs, placeholder-only labels and a \"Submit\" button that gives no sign of whether anything saved, followed by a red \"Delete account\" button in the same card that deletes at once or opens an \"Are you sure?\" dialog with OK and Cancel and no word about what is lost or how to keep a copy.",
      "why_this_is_better": [
        "Every field has a visible label and a hint; errors are written under the field, linked with aria-describedby, and the first invalid field takes focus, so people know which field to fix and how.",
        "Saving reports its result in place (\"Saved\", or \"Not saved\" with what is still safe), in space reserved under the button, so the form never jumps and a failed save does not lose edits.",
        "The danger zone is its own section at the end of the page with its own heading, and its trigger is outlined, so Save stays the only primary action and a stray click cannot delete anything.",
        "The danger zone offers the export first and lists, with real counts, what deletion removes and what stays with other people, before anyone opens the dialog.",
        "The dialog repeats the consequences, gives Cancel the focus, and asks people to type their email address, so confirming is a decision rather than a reflex.",
        "Errors say what happened to the data (\"Nothing was removed\") and offer one way forward."
      ],
      "failures": [
        "Leaving the counts out of the deletes list, or writing \"all your data\", which hides what is actually lost.",
        "Making the danger-zone trigger a filled red button beside Save, so the page has two competing primary actions.",
        "Typing a fixed word such as DELETE to confirm, which people learn to type without reading; use something specific to the account.",
        "Disabling the confirm button until the text matches, which gives keyboard and screen-reader users no reason why it does nothing.",
        "Removing the export link because the export is slow; say how long it takes instead.",
        "Showing \"Saved\" before the server confirms, or clearing the form when a save fails."
      ],
      "states": [
        "default",
        "field-errors",
        "saved",
        "save-error",
        "confirm-delete",
        "delete-error",
        "deletion-started"
      ],
      "responsive": {
        "desktop": "Each section is a two-column row from 1024px, its heading and explanation on the left and the form or danger zone on the right; Save and its status message sit side by side, and the dialog's Cancel and Delete buttons sit right-aligned in one row.",
        "mobile": "Sections stack heading first, inputs and buttons run full width with the status message under Save, and the dialog stacks Cancel above Delete so the safe action is reached first."
      },
      "a11y": [
        "The page title, sections and danger-zone blocks are headings in order (headingLevel sets the top level); the confirmation is a native modal dialog labelled by its own heading and described by the consequences.",
        "Every input has a visible label and a hint; errors are linked with aria-describedby, inputs are marked aria-invalid, and the first invalid field takes focus on submit.",
        "Save results, deletion errors and the start of deletion are announced through polite status regions; each message starts with its outcome and carries an icon as well as colour.",
        "Escape or Cancel closes the dialog and returns focus to the button that opened it; when deletion starts, focus moves to the message that replaces that button.",
        "Buttons and the export link are at least 44px tall with a visible focus ring; nothing depends on hover."
      ],
      "status": "community",
      "reviewed_by": [],
      "reviewed_at": null,
      "contributors": [],
      "provenance": "original",
      "license": "MIT"
    },
    "rules": [
      {
        "id": "ST-05",
        "statement": "Before a destructive action, name what is lost and if it can be undone; confirm it.",
        "path": "/rules/ST-05"
      },
      {
        "id": "ST-04",
        "statement": "Say what failed, what is still safe, and one way forward (retry, edit or contact).",
        "path": "/rules/ST-04"
      },
      {
        "id": "ST-03",
        "statement": "Reserve space so loading, toggles and validation don't shift surrounding content.",
        "path": "/rules/ST-03"
      },
      {
        "id": "A-04",
        "statement": "Give every control a visible label, groups a legend, and errors a link to their field.",
        "path": "/rules/A-04"
      },
      {
        "id": "A-05",
        "statement": "Announce results, saves and errors that appear in place through a polite live region.",
        "path": "/rules/A-05"
      },
      {
        "id": "A-06",
        "statement": "Make everything work by keyboard, close overlays on Escape, and hide nothing behind hover.",
        "path": "/rules/A-06"
      },
      {
        "id": "H-01",
        "statement": "Give each section, card or dialog at most one primary-styled action.",
        "path": "/rules/H-01"
      },
      {
        "id": "W-02",
        "statement": "Label actions with what happens next, not \"Get started\", \"Submit\" or \"Learn more\".",
        "path": "/rules/W-02"
      }
    ],
    "explanation": "Account settings usually get one form and one red button. The form says nothing after Submit, and the button deletes everything at once, or asks \"Are you sure?\" without saying what will be lost.\n\nThis page treats them as two different jobs. The **profile form** validates each field and shows the error under that field. It reports the save result next to the button, in space that is already there, so the page never shifts. A failed save keeps the edits and says that nothing changed.\n\nThe **danger zone** is set apart: it comes last, has its own heading, and its trigger is an outlined button, so it never competes with Save. It offers the export first, then lists what deleting removes, with the person's own counts, and what stays with other people. The native `<dialog>` repeats the list, puts focus on Cancel and asks for the account's email address. Escape closes it and focus goes back to the trigger.\n\nWhen adapting it, pass real counts to `deletes`, a real download URL to `exportHref`, and an `onSaveProfile` and `onDeleteAccount` that throw on failure, so the error states show. Keep the typed confirmation specific to the account.",
    "source": {
      "path": "web/registry/items/settings-account/settings-account.tsx",
      "code": "\"use client\";\n\nimport * as React from \"react\";\n\nimport { cn } from \"@/registry/shadcn/lib/utils\";\nimport { Button, buttonVariants } from \"@/registry/shadcn/ui/button\";\n\nexport type ProfileValues = {\n  /** The name other members see. */\n  name: string;\n  /** The sign-in address. Also the text typed to confirm deletion. */\n  email: string;\n};\n\ntype ProfileField = keyof ProfileValues;\ntype ProfileErrors = Partial<Record<ProfileField, string>>;\ntype SaveStatus = \"idle\" | \"saving\" | \"saved\" | \"error\";\ntype DeleteStatus = \"idle\" | \"deleting\" | \"error\" | \"started\";\n\nexport type SettingsAccountProps = Omit<\n  React.ComponentProps<\"div\">,\n  \"title\"\n> & {\n  /**\n   * 1 when this is the page's title (the default). Use 2 inside a layout that\n   * already has an h1; section headings follow one level below.\n   */\n  headingLevel?: 1 | 2;\n  title?: React.ReactNode;\n  /** One sentence on what this page changes. */\n  description?: React.ReactNode;\n  /** The product's name, used in the confirmation dialog's question. */\n  productName: string;\n  /** The saved profile. */\n  profile: ProfileValues;\n  nameHint?: React.ReactNode;\n  emailHint?: React.ReactNode;\n  /**\n   * Called with valid, trimmed values. Throw or reject to show the save error.\n   * Without it, saving only updates this component's state (as in previews).\n   */\n  onSaveProfile?: (values: ProfileValues) => void | Promise<void>;\n  /** Said when saving fails: what failed, that nothing changed, what to do. */\n  saveErrorMessage?: React.ReactNode;\n  /** A URL that downloads everything the account owns. */\n  exportHref: string;\n  exportLabel?: string;\n  /** What the export contains and in which format. */\n  exportDescription?: React.ReactNode;\n  /** What deletion removes, one line each, with the user's real counts. */\n  deletes: React.ReactNode[];\n  /** What survives deletion (shared work, records kept by law), if anything. */\n  keeps?: React.ReactNode[];\n  /** Throw or reject to show the delete error. Without it, deletion is only shown as started. */\n  onDeleteAccount?: () => void | Promise<void>;\n  /** Said when deletion fails: that nothing was deleted, and what to do. */\n  deleteErrorMessage?: React.ReactNode;\n  /** Replaces the delete action once deletion has started. */\n  deletionStartedMessage?: React.ReactNode;\n  /** The UI state to start in. For previews and tests. */\n  initialState?: {\n    values?: ProfileValues;\n    showErrors?: boolean;\n    save?: \"saved\" | \"error\";\n    confirmOpen?: boolean;\n    confirmText?: string;\n    deleteError?: boolean;\n    deletionStarted?: boolean;\n  };\n};\n\nconst EMAIL_PATTERN = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/u;\nconst NAME_MAX = 60;\n\nfunction validate(values: ProfileValues): ProfileErrors {\n  const errors: ProfileErrors = {};\n  const name = values.name.trim();\n  if (!name) errors.name = \"Enter your name, as other members should see it.\";\n  else if (name.length > NAME_MAX)\n    errors.name = `Use ${NAME_MAX} characters or fewer.`;\n  const email = values.email.trim();\n  if (!email) errors.email = \"Enter the email address you sign in with.\";\n  else if (!EMAIL_PATTERN.test(email))\n    errors.email = \"Enter an email address in the format name@example.com.\";\n  return errors;\n}\n\nconst inputClass =\n  \"h-11 w-full min-w-0 rounded-md border border-input bg-background px-3 text-base shadow-xs transition-[color,box-shadow] outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 aria-invalid:border-destructive motion-reduce:transition-none dark:bg-input/30\";\n\n// The final, irreversible action. Text uses the background token so it reads\n// on the destructive fill in light and dark themes alike.\nconst destructiveSolid =\n  \"h-11 w-full bg-destructive px-5 text-background hover:bg-destructive/90 sm:w-auto\";\n// Opens the confirmation; outlined so it never competes with Save.\nconst destructiveOutline =\n  \"h-11 w-full border-destructive px-5 text-destructive hover:bg-destructive/10 hover:text-destructive sm:w-auto dark:border-destructive dark:hover:bg-destructive/10\";\n\nfunction StatusIcon({ kind }: { kind: \"ok\" | \"error\" }) {\n  return (\n    <svg\n      aria-hidden=\"true\"\n      viewBox=\"0 0 16 16\"\n      className=\"mt-0.5 size-4 shrink-0\"\n      fill=\"none\"\n      stroke=\"currentColor\"\n      strokeWidth=\"1.5\"\n      strokeLinecap=\"round\"\n      strokeLinejoin=\"round\"\n    >\n      {kind === \"ok\" ? (\n        <path d=\"M3.5 8.5l3 3 6-7\" />\n      ) : (\n        <>\n          <circle cx=\"8\" cy=\"8\" r=\"6.25\" />\n          <path d=\"M8 4.75v4M8 11.25v.01\" />\n        </>\n      )}\n    </svg>\n  );\n}\n\nexport function SettingsAccount({\n  headingLevel = 1,\n  title = \"Account\",\n  description,\n  productName,\n  profile,\n  nameHint = \"Shown to people you share with.\",\n  emailHint = \"You sign in with this address. A new address is used once you confirm it.\",\n  onSaveProfile,\n  saveErrorMessage = \"Not saved. Nothing on your account changed and your edits are still here. Try again.\",\n  exportHref,\n  exportLabel = \"Download my data\",\n  exportDescription,\n  deletes,\n  keeps,\n  onDeleteAccount,\n  deleteErrorMessage = \"Your account wasn’t deleted. Nothing was removed. Try again, or contact support if it keeps failing.\",\n  deletionStartedMessage = \"Deletion has started. You’ll be signed out shortly.\",\n  initialState = {},\n  className,\n  ...props\n}: SettingsAccountProps) {\n  const id = React.useId();\n  const Title = `h${headingLevel}` as const;\n  const Section = `h${headingLevel + 1}` as \"h2\" | \"h3\";\n  const Sub = `h${headingLevel + 2}` as \"h3\" | \"h4\";\n\n  // Profile form\n  const [saved, setSaved] = React.useState<ProfileValues>(profile);\n  const [values, setValues] = React.useState<ProfileValues>(\n    initialState.values ?? profile,\n  );\n  const [showErrors, setShowErrors] = React.useState(\n    initialState.showErrors ?? false,\n  );\n  const [saveStatus, setSaveStatus] = React.useState<SaveStatus>(\n    initialState.save ?? \"idle\",\n  );\n  const [pendingEmail, setPendingEmail] = React.useState<string | null>(null);\n  const errors = showErrors ? validate(values) : {};\n  const errorCount = Object.keys(errors).length;\n  const fieldRefs = {\n    name: React.useRef<HTMLInputElement>(null),\n    email: React.useRef<HTMLInputElement>(null),\n  };\n\n  // Deletion\n  const dialogRef = React.useRef<HTMLDialogElement>(null);\n  const triggerRef = React.useRef<HTMLButtonElement>(null);\n  const cancelRef = React.useRef<HTMLButtonElement>(null);\n  const confirmRef = React.useRef<HTMLInputElement>(null);\n  const startedRef = React.useRef<HTMLDivElement>(null);\n  const [confirmText, setConfirmText] = React.useState(\n    initialState.confirmText ?? \"\",\n  );\n  const [confirmError, setConfirmError] = React.useState<string | null>(null);\n  const [deleteStatus, setDeleteStatus] = React.useState<DeleteStatus>(\n    initialState.deletionStarted\n      ? \"started\"\n      : initialState.deleteError\n        ? \"error\"\n        : \"idle\",\n  );\n  const deletionStarted = React.useRef(initialState.deletionStarted ?? false);\n\n  function updateField(field: ProfileField, value: string) {\n    setValues((current) => ({ ...current, [field]: value }));\n    if (saveStatus === \"saved\" || saveStatus === \"error\") setSaveStatus(\"idle\");\n  }\n\n  async function handleSave(event: React.FormEvent<HTMLFormElement>) {\n    event.preventDefault();\n    if (saveStatus === \"saving\") return;\n    const found = validate(values);\n    setShowErrors(true);\n    const firstInvalid = ([\"name\", \"email\"] as const).find((f) => found[f]);\n    if (firstInvalid) {\n      setSaveStatus(\"idle\");\n      fieldRefs[firstInvalid].current?.focus();\n      return;\n    }\n    const next = { name: values.name.trim(), email: values.email.trim() };\n    setSaveStatus(\"saving\");\n    try {\n      await onSaveProfile?.(next);\n      setPendingEmail(\n        next.email.toLowerCase() !== saved.email.toLowerCase()\n          ? next.email\n          : null,\n      );\n      setSaved(next);\n      setValues(next);\n      setSaveStatus(\"saved\");\n    } catch {\n      setSaveStatus(\"error\");\n    }\n  }\n\n  const openDialog = React.useCallback(() => {\n    const dialog = dialogRef.current;\n    if (!dialog) return;\n    if (!dialog.open) dialog.showModal();\n    // Cancel takes focus, so Enter or Space never confirms by accident.\n    cancelRef.current?.focus();\n  }, []);\n\n  // initialState describes where the UI starts, so it is read once.\n  const openOnMount = React.useRef(initialState.confirmOpen ?? false);\n  React.useEffect(() => {\n    if (openOnMount.current) openDialog();\n  }, [openDialog]);\n\n  function handleDialogClose() {\n    if (deletionStarted.current) {\n      startedRef.current?.focus();\n      return;\n    }\n    setConfirmText(\"\");\n    setConfirmError(null);\n    setDeleteStatus(\"idle\");\n    triggerRef.current?.focus();\n  }\n\n  // The account email is typed to confirm; case and outer spaces don't count.\n  const confirmPhrase = profile.email;\n  const confirmMatches =\n    confirmText.trim().toLowerCase() === confirmPhrase.toLowerCase();\n\n  async function handleDelete(event: React.FormEvent<HTMLFormElement>) {\n    event.preventDefault();\n    if (deleteStatus === \"deleting\") return;\n    if (!confirmMatches) {\n      setConfirmError(\n        confirmText.trim()\n          ? `That doesn’t match. Type ${confirmPhrase} exactly.`\n          : `Type ${confirmPhrase} to confirm.`,\n      );\n      confirmRef.current?.focus();\n      return;\n    }\n    setConfirmError(null);\n    setDeleteStatus(\"deleting\");\n    try {\n      await onDeleteAccount?.();\n      deletionStarted.current = true;\n      setDeleteStatus(\"started\");\n      const dialog = dialogRef.current;\n      // Closing fires onClose, which moves focus. If the dialog was already\n      // closed while the request ran, no close event follows, so move focus\n      // here: the delete button it would have returned to is gone.\n      if (dialog?.open) dialog.close();\n      else startedRef.current?.focus();\n    } catch {\n      setDeleteStatus(\"error\");\n    }\n  }\n\n  const ids = {\n    title: `${id}-title`,\n    profile: `${id}-profile`,\n    danger: `${id}-danger`,\n    exportHeading: `${id}-export`,\n    deleteHeading: `${id}-delete`,\n    nameHint: `${id}-name-hint`,\n    nameError: `${id}-name-error`,\n    emailHint: `${id}-email-hint`,\n    emailError: `${id}-email-error`,\n    dialogTitle: `${id}-dialog-title`,\n    dialogDescription: `${id}-dialog-description`,\n    confirmHint: `${id}-confirm-hint`,\n    confirmError: `${id}-confirm-error`,\n  };\n\n  const fields: {\n    key: ProfileField;\n    label: string;\n    type: \"text\" | \"email\";\n    autoComplete: string;\n    hint: React.ReactNode;\n  }[] = [\n    {\n      key: \"name\",\n      label: \"Name\",\n      type: \"text\",\n      autoComplete: \"name\",\n      hint: nameHint,\n    },\n    {\n      key: \"email\",\n      label: \"Email address\",\n      type: \"email\",\n      autoComplete: \"email\",\n      hint: emailHint,\n    },\n  ];\n\n  const sectionGrid =\n    \"grid gap-6 border-t border-border py-10 lg:grid-cols-[minmax(0,1fr)_minmax(0,2fr)] lg:gap-12\";\n  const sectionHeading = \"text-lg leading-snug font-semibold tracking-tight\";\n  const sectionText =\n    \"mt-2 max-w-prose text-sm leading-relaxed text-pretty text-muted-foreground\";\n\n  let saveMessage: React.ReactNode = null;\n  if (saveStatus === \"saving\") saveMessage = \"Saving…\";\n  else if (saveStatus === \"saved\")\n    saveMessage = pendingEmail ? (\n      <>\n        Saved. We sent a confirmation link to {pendingEmail}. Until you open it,\n        you sign in with your previous address.\n      </>\n    ) : (\n      \"Saved. Your profile is up to date.\"\n    );\n  else if (saveStatus === \"error\") saveMessage = saveErrorMessage;\n  else if (errorCount)\n    saveMessage = `Not saved. Fix the ${\n      errorCount === 1 ? \"field\" : `${errorCount} fields`\n    } marked above.`;\n  const saveIsProblem = saveStatus === \"error\" || errorCount > 0;\n\n  return (\n    <div\n      data-slot=\"settings-account\"\n      className={cn(\"bg-background text-foreground\", className)}\n      {...props}\n    >\n      <div className=\"mx-auto w-full max-w-5xl px-4 py-10 sm:px-6 sm:py-12 lg:px-8 lg:py-16\">\n        <header className=\"pb-8\">\n          <Title\n            id={ids.title}\n            className=\"text-3xl leading-tight font-semibold tracking-tight text-balance\"\n          >\n            {title}\n          </Title>\n          {description ? (\n            <p className=\"mt-3 max-w-prose text-base leading-relaxed text-pretty text-muted-foreground\">\n              {description}\n            </p>\n          ) : null}\n        </header>\n\n        <section aria-labelledby={ids.profile} className={sectionGrid}>\n          <div>\n            <Section id={ids.profile} className={sectionHeading}>\n              Profile\n            </Section>\n            <p className={sectionText}>\n              How you appear to other members, and where we reach you.\n            </p>\n          </div>\n\n          <form noValidate onSubmit={handleSave} className=\"max-w-md\">\n            <div className=\"flex flex-col gap-4\">\n              {fields.map((field) => {\n                const error = errors[field.key];\n                const hintId = ids[`${field.key}Hint` as const];\n                const errorId = ids[`${field.key}Error` as const];\n                return (\n                  <div key={field.key}>\n                    <label\n                      htmlFor={`${id}-${field.key}`}\n                      className=\"text-sm font-medium\"\n                    >\n                      {field.label}\n                    </label>\n                    <p\n                      id={hintId}\n                      className=\"mt-1 text-sm leading-snug text-muted-foreground\"\n                    >\n                      {field.hint}\n                    </p>\n                    <input\n                      ref={fieldRefs[field.key]}\n                      id={`${id}-${field.key}`}\n                      name={field.key}\n                      type={field.type}\n                      autoComplete={field.autoComplete}\n                      spellCheck={false}\n                      required\n                      value={values[field.key]}\n                      onChange={(event) =>\n                        updateField(field.key, event.target.value)\n                      }\n                      aria-invalid={error ? true : undefined}\n                      aria-describedby={error ? `${errorId} ${hintId}` : hintId}\n                      className={cn(inputClass, \"mt-2\")}\n                    />\n                    {/* Reserved so an error never pushes the form down. */}\n                    <p\n                      id={errorId}\n                      className=\"mt-1.5 flex min-h-5 gap-1.5 text-sm leading-5 font-medium text-destructive\"\n                    >\n                      {error ? (\n                        <>\n                          <StatusIcon kind=\"error\" />\n                          <span>{error}</span>\n                        </>\n                      ) : null}\n                    </p>\n                  </div>\n                );\n              })}\n            </div>\n\n            <div className=\"mt-4 flex flex-col gap-3 sm:flex-row sm:items-start sm:gap-4\">\n              <Button\n                type=\"submit\"\n                size=\"lg\"\n                aria-disabled={saveStatus === \"saving\" ? true : undefined}\n                className=\"h-11 w-full px-5 sm:w-auto\"\n              >\n                {saveStatus === \"saving\" ? \"Saving…\" : \"Save changes\"}\n              </Button>\n              <p\n                role=\"status\"\n                className={cn(\n                  \"flex min-h-11 gap-1.5 text-sm leading-5 sm:pt-3\",\n                  saveIsProblem ? \"font-medium text-destructive\" : \"\",\n                )}\n              >\n                {saveMessage ? (\n                  <>\n                    {saveStatus === \"saved\" || saveIsProblem ? (\n                      <StatusIcon kind={saveIsProblem ? \"error\" : \"ok\"} />\n                    ) : null}\n                    <span>{saveMessage}</span>\n                  </>\n                ) : null}\n              </p>\n            </div>\n          </form>\n        </section>\n\n        <section aria-labelledby={ids.danger} className={sectionGrid}>\n          <div>\n            <Section id={ids.danger} className={sectionHeading}>\n              Delete account\n            </Section>\n            <p className={sectionText}>\n              Deleting your account is permanent. Download a copy of your data\n              first.\n            </p>\n          </div>\n\n          <div className=\"rounded-lg border border-destructive/50 bg-card text-card-foreground\">\n            <div className=\"p-5 sm:p-6\">\n              <Sub\n                id={ids.exportHeading}\n                className=\"text-base leading-snug font-semibold\"\n              >\n                Keep a copy first\n              </Sub>\n              {exportDescription ? (\n                <p className={sectionText}>{exportDescription}</p>\n              ) : null}\n              <a\n                href={exportHref}\n                className={cn(\n                  buttonVariants({ variant: \"outline\", size: \"lg\" }),\n                  \"mt-4 h-11 w-full px-5 sm:w-auto\",\n                )}\n              >\n                {exportLabel}\n              </a>\n            </div>\n\n            <div className=\"border-t border-border p-5 sm:p-6\">\n              <Sub\n                id={ids.deleteHeading}\n                className=\"text-base leading-snug font-semibold\"\n              >\n                What deleting removes\n              </Sub>\n              <ul className=\"mt-3 max-w-prose list-disc space-y-1.5 pl-5 text-sm leading-relaxed\">\n                {deletes.map((line, index) => (\n                  <li key={index}>{line}</li>\n                ))}\n              </ul>\n              {keeps?.length ? (\n                <>\n                  <p className=\"mt-4 text-sm font-medium\">What stays</p>\n                  <ul className=\"mt-2 max-w-prose list-disc space-y-1.5 pl-5 text-sm leading-relaxed text-muted-foreground\">\n                    {keeps.map((line, index) => (\n                      <li key={index}>{line}</li>\n                    ))}\n                  </ul>\n                </>\n              ) : null}\n              <p className=\"mt-4 text-sm font-semibold\">\n                This can’t be undone.\n              </p>\n\n              <div\n                ref={startedRef}\n                tabIndex={-1}\n                role=\"status\"\n                className=\"mt-4 rounded-sm text-sm leading-relaxed outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50\"\n              >\n                {deleteStatus === \"started\" ? (\n                  <p className=\"flex gap-1.5 font-medium\">\n                    <StatusIcon kind=\"ok\" />\n                    <span>{deletionStartedMessage}</span>\n                  </p>\n                ) : null}\n              </div>\n              {deleteStatus === \"started\" ? null : (\n                <Button\n                  ref={triggerRef}\n                  type=\"button\"\n                  variant=\"outline\"\n                  size=\"lg\"\n                  aria-haspopup=\"dialog\"\n                  onClick={openDialog}\n                  className={destructiveOutline}\n                >\n                  Delete account…\n                </Button>\n              )}\n            </div>\n          </div>\n        </section>\n      </div>\n\n      <dialog\n        ref={dialogRef}\n        aria-labelledby={ids.dialogTitle}\n        aria-describedby={ids.dialogDescription}\n        onClose={handleDialogClose}\n        onCancel={(event) => {\n          // Keep the dialog open on Escape while the request is running.\n          if (deleteStatus === \"deleting\") event.preventDefault();\n        }}\n        className=\"m-auto max-h-[calc(100dvh-2rem)] w-[calc(100%-2rem)] max-w-lg overflow-y-auto rounded-lg border border-border bg-card p-0 text-card-foreground shadow-lg backdrop:bg-background/80\"\n      >\n        <form noValidate onSubmit={handleDelete} className=\"p-5 sm:p-6\">\n          <h2\n            id={ids.dialogTitle}\n            className=\"text-lg leading-snug font-semibold tracking-tight text-balance\"\n          >\n            Delete your {productName} account?\n          </h2>\n          <div\n            id={ids.dialogDescription}\n            className=\"mt-3 text-sm leading-relaxed\"\n          >\n            <p>This permanently removes:</p>\n            <ul className=\"mt-2 list-disc space-y-1 pl-5\">\n              {deletes.map((line, index) => (\n                <li key={index}>{line}</li>\n              ))}\n            </ul>\n            <p className=\"mt-3 font-semibold\">This can’t be undone.</p>\n          </div>\n          <p className=\"mt-3 text-sm leading-relaxed text-muted-foreground\">\n            No copy yet?{\" \"}\n            <a\n              href={exportHref}\n              className=\"rounded-sm font-medium text-foreground underline underline-offset-4 outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50\"\n            >\n              {exportLabel}\n            </a>{\" \"}\n            before you continue.\n          </p>\n\n          <div className=\"mt-5\">\n            <label\n              htmlFor={`${id}-confirm`}\n              className=\"text-sm font-medium break-words\"\n            >\n              Type <span className=\"font-semibold\">{confirmPhrase}</span> to\n              confirm\n            </label>\n            <p\n              id={ids.confirmHint}\n              className=\"mt-1 text-sm leading-snug text-muted-foreground\"\n            >\n              Your account’s email address.\n            </p>\n            <input\n              ref={confirmRef}\n              id={`${id}-confirm`}\n              name=\"confirm\"\n              type=\"text\"\n              autoComplete=\"off\"\n              autoCapitalize=\"none\"\n              spellCheck={false}\n              value={confirmText}\n              onChange={(event) => {\n                setConfirmText(event.target.value);\n                if (confirmError) setConfirmError(null);\n              }}\n              aria-invalid={confirmError ? true : undefined}\n              aria-describedby={\n                confirmError\n                  ? `${ids.confirmError} ${ids.confirmHint}`\n                  : ids.confirmHint\n              }\n              className={cn(inputClass, \"mt-2\")}\n            />\n            <p\n              id={ids.confirmError}\n              className=\"mt-1.5 flex min-h-5 gap-1.5 text-sm leading-5 font-medium break-words text-destructive\"\n            >\n              {confirmError ? (\n                <>\n                  <StatusIcon kind=\"error\" />\n                  <span className=\"min-w-0\">{confirmError}</span>\n                </>\n              ) : null}\n            </p>\n          </div>\n\n          <p\n            role=\"status\"\n            className=\"mt-2 flex min-h-10 gap-1.5 text-sm leading-5 font-medium text-destructive\"\n          >\n            {deleteStatus === \"error\" ? (\n              <>\n                <StatusIcon kind=\"error\" />\n                <span>{deleteErrorMessage}</span>\n              </>\n            ) : deleteStatus === \"deleting\" ? (\n              <span className=\"text-foreground\">Deleting your account…</span>\n            ) : null}\n          </p>\n\n          <div className=\"mt-3 flex flex-col gap-3 sm:flex-row sm:justify-end\">\n            <Button\n              ref={cancelRef}\n              type=\"button\"\n              variant=\"outline\"\n              size=\"lg\"\n              onClick={() => dialogRef.current?.close()}\n              className=\"h-11 w-full px-5 sm:w-auto\"\n            >\n              Cancel\n            </Button>\n            <Button\n              type=\"submit\"\n              size=\"lg\"\n              aria-disabled={deleteStatus === \"deleting\" ? true : undefined}\n              className={destructiveSolid}\n            >\n              {deleteStatus === \"deleting\"\n                ? \"Deleting…\"\n                : deleteStatus === \"error\"\n                  ? \"Try deleting again\"\n                  : \"Delete my account\"}\n            </Button>\n          </div>\n        </form>\n      </dialog>\n    </div>\n  );\n}\n"
    }
  },
  "boundaries": [
    "Community items are merged and pass automated checks (schema, build, accessibility, overflow and console errors in three themes at five widths); only items with status reviewed have been checked by a maintainer.",
    "Each rule id links to its page at /rules/<id>; every rule is a draft until a maintainer signs it.",
    "Tessli does not claim that using an item improves AI-generated UI; that claim waits for a published evaluation."
  ]
}
