{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "settings-page",
  "title": "Settings Page",
  "type": "registry:page",
  "description": "Linear-style settings shell: a nav rail of icon sections (collapses to a horizontal tab strip on narrow screens) switching between fully composed panels — profile form with sticky save bar, notification switches, theme picker, workspace identity, billing summary, and a type-to-confirm danger zone. Swap the panel internals for your own forms.",
  "categories": [
    "layout",
    "dashboard"
  ],
  "files": [
    {
      "path": "packages/elements-lit/src/blocks/settings-page/settings-page.ts",
      "content": "import { LitElement, css, html, nothing } from 'lit'\nimport { Bell, Building2, CreditCard, Palette, TriangleAlert, User, type IconNode } from 'lucide'\nimport { cn } from '../../lib/utils'\nimport { tailwind } from '../../lib/styles'\nimport { ThemeController } from '../../lib/theme'\nimport { icon } from '../../lib/icon'\n// Side-effect imports: register the composed elements for standalone use\n// (consumers who install only this block still get working primitives).\nimport '../../components/button/button'\nimport '../../components/dialog/dialog'\nimport '../../components/input/input'\nimport type { UipInput } from '../../components/input/input'\nimport '../../components/label/label'\nimport '../../components/section-card/section-card'\nimport '../../components/select/select'\nimport '../../components/separator/separator'\nimport '../../components/switch/switch'\nimport type { UipSwitch } from '../../components/switch/switch'\nimport '../../components/textarea/textarea'\n\nexport type SettingsSectionId = 'profile' | 'notifications' | 'appearance' | 'workspace' | 'billing' | 'danger'\n\nexport interface SettingsSection {\n  id: SettingsSectionId\n  label: string\n  /** Lucide icon node (React's `icon` component becomes data — see `icon()` usage below). */\n  icon: IconNode\n}\n\nconst defaultSections: SettingsSection[] = [\n  { id: 'profile', label: 'Profile', icon: User },\n  { id: 'notifications', label: 'Notifications', icon: Bell },\n  { id: 'appearance', label: 'Appearance', icon: Palette },\n  { id: 'workspace', label: 'Workspace', icon: Building2 },\n  { id: 'billing', label: 'Billing', icon: CreditCard },\n  { id: 'danger', label: 'Danger zone', icon: TriangleAlert },\n]\n\n// Kebab names for icon()'s lucide-<name> class (React parity). Custom\n// sections should reuse one of these nodes; unknown nodes fall back to\n// rendering without the lucide-<name> suffix.\nconst iconNames = new Map<IconNode, string>([\n  [User, 'user'],\n  [Bell, 'bell'],\n  [Palette, 'palette'],\n  [Building2, 'building-2'],\n  [CreditCard, 'credit-card'],\n  [TriangleAlert, 'triangle-alert'],\n])\n\n/**\n * <uip-settings-page> — the registry SettingsPage block as a web component.\n *\n * Class strings are React's SettingsPage verbatim. React state (useState)\n * becomes { state: true } fields. `sections` is a JS property (same ids as\n * React; icons are lucide IconNodes — React's component references become\n * data). `initialSection` is React's `initialSection` prop\n * (`initial-section` attribute). There are no onX callbacks in React, so no\n * CustomEvents.\n */\nexport class UipSettingsPage extends LitElement {\n  static shadowRootOptions = { ...LitElement.shadowRootOptions, delegatesFocus: true }\n  // :host display can't be a utility class (the host has no template of its own).\n  static styles = [tailwind, css`:host { display: block; }`]\n\n  static properties = {\n    sections: { attribute: false },\n    initialSection: { attribute: 'initial-section' },\n    active: { state: true },\n    notifyEmail: { state: true },\n    notifyPush: { state: true },\n    notifyDigest: { state: true },\n    themeChoice: { state: true },\n    deleteOpen: { state: true },\n    deleteConfirm: { state: true },\n  }\n\n  sections: SettingsSection[] = defaultSections\n  initialSection: SettingsSectionId = 'profile'\n  private active: SettingsSectionId = 'profile'\n  private notifyEmail = true\n  private notifyPush = false\n  private notifyDigest = true\n  private themeChoice: 'light' | 'dark' | 'system' = 'system'\n  private deleteOpen = false\n  private deleteConfirm = ''\n\n  constructor() {\n    super()\n    new ThemeController(this)\n  }\n\n  connectedCallback() {\n    super.connectedCallback()\n    this.setAttribute('data-uipkge', '')\n    this.setAttribute('data-slot', 'settings-page')\n    this.active = this.initialSection\n  }\n\n  private toggleRow(heading: string, description: string, checked: boolean, onChange: (v: boolean) => void) {\n    return html`\n      <li data-slot=\"settings-page\" class=\"flex items-center justify-between gap-4 py-4\">\n        <div>\n          <p class=\"text-sm font-medium\">${heading}</p>\n          <p class=\"text-muted-foreground text-xs\">${description}</p>\n        </div>\n        <uip-switch\n          .checked=${checked}\n          aria-label=${`Toggle ${heading.toLowerCase()}`}\n          @change=${(e: Event) => onChange((e.target as UipSwitch).checked)}\n        ></uip-switch>\n      </li>\n    `\n  }\n\n  render() {\n    return html`\n      <div class=\"bg-background border-border flex min-h-svh flex-col rounded-xl border lg:flex-row\">\n        <nav class=\"border-border shrink-0 border-b p-3 lg:w-56 lg:border-r lg:border-b-0\" aria-label=\"Settings sections\">\n          <div class=\"flex [scrollbar-width:none] gap-1 overflow-x-auto lg:flex-col lg:overflow-visible [&::-webkit-scrollbar]:hidden\">\n            ${this.sections.map(\n              (section) => {\n                const isActive = this.active === section.id\n                return html`\n                  <button\n                    type=\"button\"\n                    aria-current=${isActive ? 'page' : nothing}\n                    @click=${() => {\n                      this.active = section.id\n                    }}\n                    class=${cn(\n                      'focus-visible:ring-ring flex shrink-0 items-center gap-2 rounded-md px-3 py-2 text-sm font-medium transition-colors focus-visible:ring-2 focus-visible:outline-none',\n                      isActive\n                        ? section.id === 'danger'\n                          ? 'text-destructive bg-destructive/10'\n                          : 'bg-accent text-accent-foreground'\n                        : 'text-muted-foreground hover:bg-accent/50 hover:text-foreground',\n                    )}\n                  >\n                    <span class=\"inline-flex [&>svg]:size-4\">${icon(section.icon, iconNames.get(section.icon) ?? 'shapes')}</span>\n                    ${section.label}\n                  </button>\n                `\n              },\n            )}\n          </div>\n        </nav>\n\n        <div class=\"flex min-w-0 flex-1 flex-col\">\n          ${this.active === 'profile'\n            ? html`\n                <section class=\"flex flex-1 flex-col\">\n                  <div class=\"flex-1 space-y-6 p-6\">\n                    <uip-section-card\n                      heading=\"Public profile\"\n                      description=\"How teammates see you across the workspace.\"\n                    >\n                      <div class=\"space-y-4\">\n                        <div class=\"space-y-2\">\n                          <uip-label for=\"sp-name\">Display name</uip-label>\n                          <uip-input id=\"sp-name\" value=\"Amara Osei\"></uip-input>\n                        </div>\n                        <div class=\"space-y-2\">\n                          <uip-label for=\"sp-email\">Email</uip-label>\n                          <uip-input id=\"sp-email\" value=\"amara@acme.com\" type=\"email\"></uip-input>\n                        </div>\n                        <div class=\"space-y-2\">\n                          <uip-label for=\"sp-bio\">Bio</uip-label>\n                          <uip-textarea id=\"sp-bio\" value=\"Design engineer. Builds systems, breaks assumptions.\" rows=\"3\"></uip-textarea>\n                        </div>\n                        <div class=\"space-y-2\">\n                          <uip-label for=\"sp-role\">Role</uip-label>\n                          <uip-select id=\"sp-role\" value=\"editor\" class=\"w-full sm:w-56\" aria-label=\"Role\">\n                            <option value=\"admin\">Admin</option>\n                            <option value=\"editor\">Editor</option>\n                            <option value=\"viewer\">Viewer</option>\n                          </uip-select>\n                        </div>\n                      </div>\n                    </uip-section-card>\n                  </div>\n                  <div class=\"border-border flex items-center justify-end gap-2 border-t p-4\">\n                    <uip-button variant=\"ghost\" size=\"sm\">Discard</uip-button>\n                    <uip-button size=\"sm\">Save changes</uip-button>\n                  </div>\n                </section>\n              `\n            : nothing}\n          ${this.active === 'notifications'\n            ? html`\n                <section class=\"flex-1 p-6\">\n                  <uip-section-card heading=\"Notifications\" description=\"Choose what reaches you and where.\">\n                    <ul class=\"-my-4 divide-y\">\n                      ${this.toggleRow('Email notifications', 'Mentions, assignments and approvals.', this.notifyEmail, (v) => {\n                        this.notifyEmail = v\n                      })}\n                      ${this.toggleRow('Push notifications', 'Real-time browser alerts while you work.', this.notifyPush, (v) => {\n                        this.notifyPush = v\n                      })}\n                      ${this.toggleRow('Weekly digest', 'A summary of everything, every Monday.', this.notifyDigest, (v) => {\n                        this.notifyDigest = v\n                      })}\n                    </ul>\n                  </uip-section-card>\n                </section>\n              `\n            : nothing}\n          ${this.active === 'appearance'\n            ? html`\n                <section class=\"flex-1 p-6\">\n                  <uip-section-card heading=\"Appearance\" description=\"Theme follows your system by default.\">\n                    <div class=\"space-y-5\">\n                      <div class=\"grid grid-cols-3 gap-3\">\n                        ${(['light', 'dark', 'system'] as const).map(\n                          (theme) => html`\n                            <button\n                              type=\"button\"\n                              @click=${() => {\n                                this.themeChoice = theme\n                              }}\n                              class=${cn(\n                                'focus-visible:ring-ring rounded-lg border p-3 text-left transition-colors focus-visible:ring-2 focus-visible:outline-none',\n                                this.themeChoice === theme ? 'border-primary ring-primary/20 ring-2' : 'hover:bg-accent/50',\n                              )}\n                            >\n                              <span class=\"block text-sm font-medium capitalize\">${theme}</span>\n                              <span class=\"text-muted-foreground block text-xs\">\n                                ${theme === 'light' ? 'Bright surfaces' : theme === 'dark' ? 'Low-light friendly' : 'Match device'}\n                              </span>\n                            </button>\n                          `,\n                        )}\n                      </div>\n                      <uip-separator></uip-separator>\n                      <div class=\"flex items-center justify-between\">\n                        <div>\n                          <p class=\"text-sm font-medium\">Reduced motion</p>\n                          <p class=\"text-muted-foreground text-xs\">Collapse animations to instant transitions.</p>\n                        </div>\n                        <uip-switch aria-label=\"Toggle reduced motion\"></uip-switch>\n                      </div>\n                    </div>\n                  </uip-section-card>\n                </section>\n              `\n            : nothing}\n          ${this.active === 'workspace'\n            ? html`\n                <section class=\"flex-1 p-6\">\n                  <uip-section-card heading=\"Workspace\" description=\"Identity and defaults for everyone here.\">\n                    <div class=\"space-y-4\">\n                      <div class=\"space-y-2\">\n                        <uip-label for=\"sp-ws-name\">Workspace name</uip-label>\n                        <uip-input id=\"sp-ws-name\" value=\"Acme Inc\"></uip-input>\n                      </div>\n                      <div class=\"space-y-2\">\n                        <uip-label for=\"sp-ws-url\">URL slug</uip-label>\n                        <div class=\"flex\">\n                          <span class=\"border-border bg-muted text-muted-foreground flex items-center rounded-l-md border px-3 font-mono text-xs\">\n                            acme.uipkge.app\n                          </span>\n                          <uip-input id=\"sp-ws-url\" value=\"acme\" class=\"font-mono text-xs [&::part(control)]:rounded-l-none\"></uip-input>\n                        </div>\n                      </div>\n                      <p class=\"text-muted-foreground text-xs\">16 members · 4 pending invites</p>\n                    </div>\n                  </uip-section-card>\n                </section>\n              `\n            : nothing}\n          ${this.active === 'billing'\n            ? html`\n                <section class=\"flex-1 p-6\">\n                  <uip-section-card heading=\"Billing\" description=\"Plan, seats and invoices.\">\n                    <div class=\"flex items-center justify-between gap-4\">\n                      <div>\n                        <p class=\"text-sm font-medium\">Team plan · $12 / seat / month</p>\n                        <p class=\"text-muted-foreground mt-1 text-xs\">Next invoice Sep 1, 2026 · 18 seats</p>\n                      </div>\n                      <uip-button variant=\"outline\" size=\"sm\">Manage billing</uip-button>\n                    </div>\n                  </uip-section-card>\n                </section>\n              `\n            : nothing}\n          ${this.active === 'danger'\n            ? html`\n                <section class=\"flex-1 p-6\">\n                  <div class=\"border-destructive/40 rounded-xl border p-6\">\n                    <div class=\"flex items-center justify-between gap-4\">\n                      <div>\n                        <p class=\"text-destructive text-sm font-medium\">Delete this workspace</p>\n                        <p class=\"text-muted-foreground mt-1 text-xs\">\n                          All projects, members and data will be permanently removed.\n                        </p>\n                      </div>\n                      <uip-button\n                        variant=\"destructive\"\n                        size=\"sm\"\n                        @click=${() => {\n                          this.deleteOpen = true\n                        }}\n                        >Delete…</uip-button\n                      >\n                    </div>\n                  </div>\n                </section>\n              `\n            : nothing}\n        </div>\n\n        <uip-dialog\n          heading=\"Delete workspace?\"\n          description=\"This removes Acme Inc and all of its data for every member. This cannot be undone.\"\n          class=\"[&::part(content)]:max-w-sm\"\n          ?open=${this.deleteOpen}\n          @open-change=${(e: CustomEvent<{ open: boolean }>) => {\n            this.deleteOpen = e.detail.open\n          }}\n        >\n          <uip-input\n            .value=${this.deleteConfirm}\n            placeholder='Type \"DELETE\" to confirm'\n            @input=${(e: Event) => {\n              this.deleteConfirm = (e.target as UipInput).value\n            }}\n          ></uip-input>\n          <uip-button\n            slot=\"footer\"\n            variant=\"outline\"\n            size=\"sm\"\n            @click=${() => {\n              this.deleteOpen = false\n            }}\n            >Cancel</uip-button\n          >\n          <uip-button\n            slot=\"footer\"\n            variant=\"destructive\"\n            size=\"sm\"\n            ?disabled=${this.deleteConfirm !== 'DELETE'}\n            @click=${() => {\n              this.deleteOpen = false\n            }}\n            >Delete forever</uip-button\n          >\n        </uip-dialog>\n      </div>\n    `\n  }\n\n}\n\ncustomElements.get('uip-settings-page') || customElements.define('uip-settings-page', UipSettingsPage)\n\ndeclare global {\n  interface HTMLElementTagNameMap {\n    'uip-settings-page': UipSettingsPage\n  }\n}\n",
      "type": "registry:file",
      "target": "src/uip/blocks/settings-page/settings-page.ts"
    }
  ],
  "dependencies": [
    "lit",
    "lucide"
  ],
  "registryDependencies": [
    "https://uipkge.dev/r/lit/button.json",
    "https://uipkge.dev/r/lit/dialog.json",
    "https://uipkge.dev/r/lit/input.json",
    "https://uipkge.dev/r/lit/label.json",
    "https://uipkge.dev/r/lit/lit-core.json",
    "https://uipkge.dev/r/lit/section-card.json",
    "https://uipkge.dev/r/lit/select.json",
    "https://uipkge.dev/r/lit/separator.json",
    "https://uipkge.dev/r/lit/switch.json",
    "https://uipkge.dev/r/lit/textarea.json"
  ]
}
