@loworbitstudio/visor 1.16.1 → 1.18.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/dist/CHANGELOG.json +13 -1
- package/dist/index.js +433 -12
- package/dist/registry.json +109 -3
- package/dist/visor-manifest.json +247 -1
- package/package.json +1 -1
package/dist/registry.json
CHANGED
|
@@ -2795,6 +2795,57 @@
|
|
|
2795
2795
|
}
|
|
2796
2796
|
]
|
|
2797
2797
|
},
|
|
2798
|
+
{
|
|
2799
|
+
"name": "doc-nav",
|
|
2800
|
+
"type": "registry:ui",
|
|
2801
|
+
"description": "A manifest-driven, grouped/collapsible, multi-product-aware documentation navigation. Renders peer collapsible groups from a manifest slice — a pinned Shared set, accordion product groups (one open at a time), and an Appendix bucket for ad-hoc docs — resolving the active pill from currentPath. The group row wraps and is never an overflow-x scroll strip. Replaces the vanilla-JS doc nav.",
|
|
2802
|
+
"category": "navigation",
|
|
2803
|
+
"dependencies": [
|
|
2804
|
+
"@phosphor-icons/react",
|
|
2805
|
+
"@loworbitstudio/visor-core"
|
|
2806
|
+
],
|
|
2807
|
+
"registryDependencies": [
|
|
2808
|
+
"utils"
|
|
2809
|
+
],
|
|
2810
|
+
"files": [
|
|
2811
|
+
{
|
|
2812
|
+
"path": "components/ui/doc-nav/doc-nav.tsx",
|
|
2813
|
+
"type": "registry:ui",
|
|
2814
|
+
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport {\n CaretDownIcon,\n CaretRightIcon,\n ArrowSquareOutIcon,\n} from \"@phosphor-icons/react\"\nimport { cn } from \"../../../lib/utils\"\nimport styles from \"./doc-nav.module.css\"\n\n/**\n * Order threshold at or above which a doc with no group and no scope is\n * treated as an ad-hoc \"random\" doc and tucked into the Appendix bucket.\n * Mirrors the PL-2185 rule: no scope + no group + order >= 10 -> Appendix.\n */\nconst APPENDIX_ORDER_THRESHOLD = 10\n\nconst APPENDIX_ID = \"appendix\"\nconst APPENDIX_LABEL = \"Appendix\"\nconst SHARED_ID = \"shared\"\nconst SHARED_LABEL = \"Shared\"\n\n/**\n * A single documentation entry — the manifest slice DocFrame passes down.\n * Every field but `order`/`label`/`href` is optional; absence of `scope`,\n * `group`, and `tier` reproduces the flat, single-row behaviour.\n */\nexport interface DocEntry {\n /** Sort order within the resolved group. `0` marks the hub/overview entry. */\n order: number\n /** Visible pill label. */\n label: string\n /** Destination URL (route or static `/docs/*.html`). */\n href: string\n /** Manifest kind (`route`, `local-html`, `external`, …). Used to detect external links. */\n kind?: string\n /** PL-2177 product scope — which product group(s) the doc belongs to. */\n scope?: string[]\n /** PL-2185 nav section-label hint. Defaults from `scope`. */\n group?: string\n /** PL-2170 load depth — referenced here, owned there. */\n tier?: number\n /** Force external treatment (new tab + badge). Defaults from `kind`/`href`. */\n external?: boolean\n}\n\nexport interface DocNavProps\n extends Omit<React.ComponentProps<\"nav\">, \"children\"> {\n /** The manifest slice for the active view (Shared + product-scoped), pre-filtered by DocFrame. */\n docs: DocEntry[]\n /** Which product group is open (accordion). Absent → single-product mode (no accordion). */\n activeProduct?: string\n /**\n * Accordion callback — expand a product group (the parent collapses the\n * sibling by swapping `activeProduct`). Absent → groups render as plain,\n * independently expandable links (static-doc mode).\n */\n onProductToggle?: (id: string) => void\n /** Groups that stay open regardless of the accordion. Default `[\"shared\"]`. */\n pinnedGroups?: string[]\n /** Resolves the active pill and auto-expands its group. */\n currentPath: string\n /** Whether non-active, non-pinned groups start collapsed. Default `true` — the anti-wall rule. */\n defaultCollapsed?: boolean\n}\n\ntype GroupRole = \"pinned\" | \"collapsible\" | \"appendix\"\n\ninterface ResolvedGroup {\n id: string\n label: string\n role: GroupRole\n entries: DocEntry[]\n}\n\n// ─── helpers ─────────────────────────────────────────────────────────────────\n\nfunction slug(value: string): string {\n return value.trim().toLowerCase().replace(/\\s+/g, \"-\")\n}\n\nfunction titleCase(value: string): string {\n if (value.length === 0) return value\n return value.charAt(0).toUpperCase() + value.slice(1)\n}\n\n/** Strip query + hash so a static `href` compares cleanly against `currentPath`. */\nfunction normalizePath(value: string): string {\n return value.replace(/[?#].*$/, \"\")\n}\n\nfunction hrefMatchesPath(href: string, currentPath: string): boolean {\n if (!href) return false\n return normalizePath(href) === normalizePath(currentPath)\n}\n\nfunction isExternalEntry(entry: DocEntry): boolean {\n if (typeof entry.external === \"boolean\") return entry.external\n if (entry.kind === \"external\") return true\n return /^https?:\\/\\//.test(entry.href)\n}\n\n/** Resolve which group an entry belongs to, following the PL-2185 rules. */\nfunction groupKeyFor(entry: DocEntry): { id: string; label: string } {\n const hasGroup = typeof entry.group === \"string\" && entry.group.length > 0\n const hasScope = Array.isArray(entry.scope) && entry.scope.length > 0\n\n if (!hasGroup && !hasScope && entry.order >= APPENDIX_ORDER_THRESHOLD) {\n return { id: APPENDIX_ID, label: APPENDIX_LABEL }\n }\n if (hasGroup) {\n return { id: slug(entry.group as string), label: entry.group as string }\n }\n if (hasScope) {\n const first = (entry.scope as string[])[0]\n return { id: slug(first), label: titleCase(first) }\n }\n return { id: SHARED_ID, label: SHARED_LABEL }\n}\n\n/** Bucket the docs into ordered, non-empty groups (pinned → collapsible → appendix). */\nfunction resolveGroups(\n docs: DocEntry[],\n pinnedGroups: string[]\n): ResolvedGroup[] {\n const byId = new Map<string, ResolvedGroup>()\n const appearance: string[] = []\n\n for (const entry of docs) {\n const key = groupKeyFor(entry)\n let group = byId.get(key.id)\n if (!group) {\n const role: GroupRole =\n key.id === APPENDIX_ID\n ? \"appendix\"\n : pinnedGroups.includes(key.id)\n ? \"pinned\"\n : \"collapsible\"\n group = { id: key.id, label: key.label, role, entries: [] }\n byId.set(key.id, group)\n appearance.push(key.id)\n }\n group.entries.push(entry)\n }\n\n for (const group of byId.values()) {\n group.entries.sort((a, b) => a.order - b.order)\n }\n\n const rank = (group: ResolvedGroup): number => {\n if (group.role === \"pinned\") {\n const idx = pinnedGroups.indexOf(group.id)\n return idx === -1 ? 0 : idx\n }\n if (group.role === \"appendix\") return Number.MAX_SAFE_INTEGER\n return 1000 + appearance.indexOf(group.id)\n }\n\n return appearance\n .map((id) => byId.get(id) as ResolvedGroup)\n .sort((a, b) => rank(a) - rank(b))\n}\n\n// ─── DocNav ──────────────────────────────────────────────────────────────────\n\nconst DocNav = React.forwardRef<HTMLElement, DocNavProps>(\n (\n {\n docs,\n activeProduct,\n onProductToggle,\n pinnedGroups = [SHARED_ID],\n currentPath,\n defaultCollapsed = true,\n className,\n \"aria-label\": ariaLabel,\n ...props\n },\n ref\n ) => {\n const groups = React.useMemo(\n () => resolveGroups(docs, pinnedGroups),\n [docs, pinnedGroups]\n )\n\n // The group holding the active doc — always expanded on load.\n const activeGroupId = React.useMemo(() => {\n for (const group of groups) {\n if (group.entries.some((e) => hrefMatchesPath(e.href, currentPath))) {\n return group.id\n }\n }\n return null\n }, [groups, currentPath])\n\n const isControlled = onProductToggle != null\n\n // Uncontrolled expand state (static-doc mode + appendix, which is never a product).\n const [openSet, setOpenSet] = React.useState<Set<string>>(() => {\n const initial = new Set<string>()\n if (activeGroupId) initial.add(activeGroupId)\n if (activeProduct) initial.add(activeProduct)\n for (const group of groups) {\n if (group.role === \"pinned\") continue\n if (group.role === \"appendix\") {\n // A lone ad-hoc doc renders inline; two or more collapse.\n if (group.entries.length <= 1) initial.add(group.id)\n continue\n }\n if (!defaultCollapsed) initial.add(group.id)\n }\n return initial\n })\n\n const isOpen = (group: ResolvedGroup): boolean => {\n if (group.role === \"pinned\") return true\n if (group.role === \"appendix\") return openSet.has(group.id)\n // Collapsible product group.\n if (isControlled) {\n return group.id === activeProduct || group.id === activeGroupId\n }\n return openSet.has(group.id) || group.id === activeGroupId\n }\n\n const toggle = (group: ResolvedGroup): void => {\n if (group.role === \"pinned\") return\n if (group.role === \"collapsible\" && isControlled) {\n onProductToggle(group.id)\n return\n }\n setOpenSet((prev) => {\n const next = new Set(prev)\n if (next.has(group.id)) next.delete(group.id)\n else next.add(group.id)\n return next\n })\n }\n\n return (\n <nav\n ref={ref}\n aria-label={ariaLabel ?? \"Documentation\"}\n data-slot=\"doc-nav\"\n className={cn(styles.root, className)}\n {...props}\n >\n {groups.map((group) => (\n <DocNavGroup\n key={group.id}\n group={group}\n open={isOpen(group)}\n currentPath={currentPath}\n onToggle={() => toggle(group)}\n />\n ))}\n </nav>\n )\n }\n)\nDocNav.displayName = \"DocNav\"\n\n// ─── DocNavGroup (internal) ──────────────────────────────────────────────────\n\ninterface DocNavGroupProps {\n group: ResolvedGroup\n open: boolean\n currentPath: string\n onToggle: () => void\n}\n\nfunction DocNavGroup({ group, open, currentPath, onToggle }: DocNavGroupProps) {\n const panelId = React.useId()\n const isPinned = group.role === \"pinned\"\n const Caret = open ? CaretDownIcon : CaretRightIcon\n\n const head = (\n <>\n <Caret className={styles.caret} weight=\"fill\" aria-hidden=\"true\" />\n <span className={styles.scopeDot} aria-hidden=\"true\" />\n <span className={styles.groupLabel}>{group.label}</span>\n {!open && <span className={styles.count}>{group.entries.length}</span>}\n </>\n )\n\n return (\n <div\n data-slot=\"doc-nav-group\"\n data-group={group.id}\n data-role={group.role}\n data-state={open ? \"open\" : \"closed\"}\n className={cn(\n styles.group,\n isPinned && styles.groupPinned,\n !open && styles.groupCollapsed\n )}\n >\n {isPinned ? (\n <div className={styles.head}>{head}</div>\n ) : (\n <button\n type=\"button\"\n data-slot=\"doc-nav-group-trigger\"\n className={cn(styles.head, styles.headButton)}\n aria-expanded={open}\n aria-controls={panelId}\n onClick={onToggle}\n >\n {head}\n </button>\n )}\n\n {open && (\n <div id={panelId} className={styles.pills}>\n {group.entries.map((entry, index) => (\n <DocNavPill\n key={entry.href}\n entry={entry}\n active={hrefMatchesPath(entry.href, currentPath)}\n // Hub = the group's overview entry: an explicit order-0 doc, or the\n // lead pill of the pinned Shared set (which often starts at order 1).\n isHub={entry.order === 0 || (group.role === \"pinned\" && index === 0)}\n />\n ))}\n </div>\n )}\n </div>\n )\n}\n\n// ─── DocNavPill (internal) ───────────────────────────────────────────────────\n\ninterface DocNavPillProps {\n entry: DocEntry\n active: boolean\n /** Marks the group's hub/overview entry — a leading accent dot. */\n isHub: boolean\n}\n\nfunction DocNavPill({ entry, active, isHub }: DocNavPillProps) {\n const external = isExternalEntry(entry)\n\n return (\n <a\n href={entry.href}\n data-slot=\"doc-nav-pill\"\n data-active={active || undefined}\n data-hub={isHub || undefined}\n aria-current={active ? \"page\" : undefined}\n className={cn(styles.pill, active && styles.pillActive, isHub && styles.pillHub)}\n target={external ? \"_blank\" : undefined}\n rel={external ? \"noopener noreferrer\" : undefined}\n >\n <span className={styles.pillNum}>{entry.order}</span>\n <span className={styles.pillLabel}>{entry.label}</span>\n {external && (\n <>\n <ArrowSquareOutIcon className={styles.pillExternal} aria-hidden=\"true\" />\n <span className={styles.srOnly}>(opens in a new tab)</span>\n </>\n )}\n </a>\n )\n}\n\nexport { DocNav }\n"
|
|
2815
|
+
},
|
|
2816
|
+
{
|
|
2817
|
+
"path": "components/ui/doc-nav/doc-nav.module.css",
|
|
2818
|
+
"type": "registry:ui",
|
|
2819
|
+
"content": "/* DocNav — manifest-driven, grouped/collapsible doc navigation.\n *\n * Renders peer, collapsible groups from a manifest slice: a pinned Shared set,\n * accordion product groups (one open at a time), and an Appendix bucket for\n * ad-hoc docs. The group row wraps (flex-wrap) and is NEVER an overflow-x\n * scroll strip — the run-off-the-edge defect this component exists to kill.\n *\n * Theme-agnostic: every value references a CSS custom property token.\n */\n\n/* Root — the wrapping row of peer groups. */\n.root {\n /* Accent-pop indirection (B2): the active pill / caret / hub / pinned frame\n * all route through this single token so a consumer (DocFrame) can retint the\n * whole nav in one place. Defaults to the theme's vivid `--accent` — not\n * `--primary`, which reads dull on muted-primary themes such as Strata. */\n --doc-nav-accent: var(--accent, #2563eb);\n /* Group cluster radius — a controlled, fixed radius the doc-nav owns, NOT the\n * theme `--radius-*` scale, which decorative themes inflate far past what a\n * dense nav cluster wants (for example Strata inflates `--radius-xl` to 32px).\n * The approved design pins ~12px per theme for exactly this reason; any single\n * theme radius token renders ~6px on some themes and ~32px on others.\n * Overridable by a consumer/theme via this variable. */\n --doc-nav-group-radius: 0.75rem;\n /* Resting pill fill — a recessed well, darker than the group's `--surface-card`\n * in BOTH modes. The surface ramp inverts light↔dark (page is darkest in dark\n * but lightest in light), so a single surface token can't stay \"below\" the card;\n * mixing the card toward neutral-950 darkens it directionally in either mode.\n * Overridable by a consumer/theme. */\n --doc-nav-pill-bg: color-mix(\n in srgb,\n var(--surface-card, #111827),\n var(--color-neutral-950, #030712) 20%\n );\n display: flex;\n flex-wrap: wrap;\n align-items: flex-start;\n gap: var(--spacing-2, 0.5rem);\n /* Deliberately no overflow-x: groups wrap down, never scroll sideways. */\n}\n\n/* Group — a bordered, surface-backed cluster: header + pill row. */\n.group {\n display: inline-flex;\n flex-direction: column;\n gap: var(--spacing-2, 0.5rem);\n padding: var(--spacing-2, 0.5rem);\n border: var(--stroke-width-thin, 1px) solid\n var(--hairline, var(--border-default, #e5e7eb));\n border-radius: var(--doc-nav-group-radius, var(--radius-md, 0.375rem));\n background: var(--surface-card, #ffffff);\n}\n\n/* Pinned — the Shared group stays open; a subtle frame keyed to the group hue.\n * Routed through overridable tokens (P5) so borderless themes (Animal / ENTR)\n * can null the frame — set `--doc-nav-pin-border: transparent` and\n * `--doc-nav-pin-bg: var(--surface-card)`. Default derives from the group's\n * shared hue, not `--primary` (which read as an unwanted border on those\n * borderless themes). */\n.groupPinned {\n border-color: var(\n --doc-nav-pin-border,\n color-mix(in srgb, var(--doc-nav-group-accent, var(--info, #0ea5e9)) 32%, transparent)\n );\n background: var(\n --doc-nav-pin-bg,\n color-mix(in srgb, var(--doc-nav-group-accent, var(--info, #0ea5e9)) 6%, var(--surface-card, #ffffff))\n );\n}\n\n/* Collapsed — a group reduces to a single pill-shaped chip. */\n.groupCollapsed {\n padding: 0;\n border-color: transparent;\n background: transparent;\n}\n\n/* Header — the group label row. As a chip when collapsed, plain when open. */\n.head {\n display: inline-flex;\n align-items: center;\n gap: var(--spacing-2, 0.5rem);\n padding: var(--spacing-1, 0.25rem) var(--spacing-2, 0.5rem);\n font-family: var(--font-mono, ui-monospace, \"SFMono-Regular\", Menlo, monospace);\n font-size: var(--font-size-2xs, 0.6875rem);\n font-weight: var(--font-weight-bold, 700);\n letter-spacing: 0.14em;\n text-transform: uppercase;\n /* Collapse the label's line box to the glyph height so the all-caps text\n * optically centers against the dot/caret (matching `.pill`). Without this the\n * caps ride high — the empty descender space pushes the line-box center below\n * the visual center of the caps. */\n line-height: 1;\n color: var(--text-secondary, #4b5563);\n}\n\n/* Button variant — collapsible group heads are clickable. */\n.headButton {\n cursor: pointer;\n border: 0;\n background: transparent;\n font-family: inherit;\n transition: color var(--motion-duration-fast, 150ms)\n var(--motion-easing-standard, ease);\n}\n\n.headButton:hover {\n color: var(--text-primary, #111827);\n}\n\n.headButton:focus-visible {\n outline: var(--focus-ring-width, 2px) solid\n var(--border-focus, currentColor);\n outline-offset: var(--focus-ring-offset, 2px);\n border-radius: var(--radius-sm, 0.25rem);\n}\n\n/* Collapsed group head — reads as a resting chip. */\n.groupCollapsed .head {\n border: var(--stroke-width-thin, 1px) solid\n var(--hairline, var(--border-default, #e5e7eb));\n border-radius: var(--radius-full, 9999px);\n /* A collapsed chip stands alone in the nav row, so it carries more vertical\n * padding than the tight open-group label (the design gives it ~7px 12px). */\n padding: var(--spacing-2, 0.5rem) var(--spacing-3, 0.75rem);\n /* The chip reads as the group's card surface (matching the design's chip-bg),\n * not a lighter subtle surface — `--surface-subtle` resolves as a mid-gray in\n * dark themes. */\n background: var(--surface-card, #111827);\n}\n\n/* Caret — accent-popped fill glyph (P6). */\n.caret {\n flex-shrink: 0;\n color: var(--doc-nav-accent, var(--accent, #2563eb));\n}\n\n/* Scope dot — a per-group grouping cue, color-coded with a soft glow (P1).\n * `color` carries the group hue so `currentColor` drives both the fill and the\n * glow; a consumer overrides the whole group via `--doc-nav-group-accent`.\n * Default (product groups) is the vivid accent; pinned/appendix retint below. */\n.scopeDot {\n width: var(--spacing-2, 0.5rem);\n height: var(--spacing-2, 0.5rem);\n flex-shrink: 0;\n border-radius: var(--radius-full, 9999px);\n color: var(--doc-nav-group-accent, var(--accent, #2563eb));\n background: currentColor;\n box-shadow: 0 0 var(--spacing-2, 0.5rem)\n color-mix(in srgb, currentColor 60%, transparent);\n}\n\n/* Shared (pinned) group reads in the info hue. */\n.groupPinned .scopeDot {\n color: var(--doc-nav-group-accent, var(--info, #0ea5e9));\n}\n\n/* Appendix reads muted and un-glowed — an ad-hoc bucket, not a product. */\n.group[data-role=\"appendix\"] .scopeDot {\n color: var(--doc-nav-group-accent, var(--text-tertiary, #9ca3af));\n box-shadow: none;\n}\n\n/* Group label text. */\n.groupLabel {\n min-width: 0;\n}\n\n/* Trailing count badge — a hairline mono pill on a collapsed group (N2). */\n.count {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n min-width: var(--spacing-4, 1rem);\n padding: 0 calc(var(--spacing-1, 0.25rem) * 1.5);\n border: var(--stroke-width-thin, 1px) solid\n var(--hairline, var(--border-default, #e5e7eb));\n border-radius: var(--radius-full, 9999px);\n font-family: var(--font-mono, ui-monospace, \"SFMono-Regular\", Menlo, monospace);\n /* ~9px — a quarter down from the xs step, matching the design's tiny badge. */\n font-size: calc(var(--font-size-xs, 0.75rem) * 0.75);\n font-variant-numeric: tabular-nums;\n color: var(--text-tertiary, #9ca3af);\n}\n\n/* Pill row — wraps within the group. */\n.pills {\n display: flex;\n flex-wrap: wrap;\n gap: var(--spacing-1, 0.25rem);\n}\n\n/* Pill — a single doc link. Mono · UPPERCASE · letter-spaced (B1). */\n.pill {\n display: inline-flex;\n align-items: center;\n gap: var(--spacing-2, 0.5rem);\n padding: calc(var(--spacing-1, 0.25rem) * 1.5) var(--spacing-3, 0.75rem);\n border: var(--stroke-width-thin, 1px) solid transparent;\n border-radius: var(--radius-full, 9999px);\n background: var(--doc-nav-pill-bg, var(--surface-card, #111827));\n font-family: var(--font-mono, ui-monospace, \"SFMono-Regular\", Menlo, monospace);\n font-size: var(--font-size-2xs, 0.6875rem);\n font-weight: var(--font-weight-medium, 500);\n letter-spacing: 0.05em;\n text-transform: uppercase;\n line-height: 1;\n white-space: nowrap;\n color: var(--text-secondary, #4b5563);\n text-decoration: none;\n cursor: pointer;\n transition: color var(--motion-duration-fast, 150ms)\n var(--motion-easing-standard, ease),\n border-color var(--motion-duration-fast, 150ms)\n var(--motion-easing-standard, ease),\n background-color var(--motion-duration-fast, 150ms)\n var(--motion-easing-standard, ease);\n}\n\n.pill:hover {\n color: var(--text-primary, #111827);\n border-color: var(--hairline, var(--border-default, #e5e7eb));\n}\n\n.pill:focus-visible {\n outline: var(--focus-ring-width, 2px) solid\n var(--border-focus, currentColor);\n outline-offset: var(--focus-ring-offset, 2px);\n}\n\n/* Shared-group pills carry the group's tint at rest (P4) — hover/active win. */\n.groupPinned .pill:not(.pillActive) {\n color: var(--doc-nav-group-accent, var(--info, #0ea5e9));\n}\n\n.groupPinned .pill:not(.pillActive):hover {\n color: var(--text-primary, #111827);\n}\n\n/* Active — the current doc, resolved from currentPath. Contrast-safe (VI-612):\n * the label is `--text-primary` (always legible), NOT raw `--doc-nav-accent`,\n * which is unreadable when a theme's accent is white/near-white (Blacklight, say)\n * on a light card. The active pill reads as active via a distinct opaque surface\n * plus an accent ring blended with a hairline so it never fully vanishes. */\n.pillActive {\n color: var(--text-primary, #111827);\n border-color: color-mix(\n in srgb,\n var(--doc-nav-accent, var(--accent, #2563eb)) 55%,\n var(--hairline, var(--border-default, #e5e7eb))\n );\n background: var(\n --surface-selected,\n color-mix(\n in srgb,\n var(--surface-card, #111827),\n var(--doc-nav-accent, var(--accent, #2563eb)) 14%\n )\n );\n}\n\n/* Hub — the group's overview entry gets a leading accent dot with a glow (N1).\n * `color` carries the accent so `currentColor` drives fill + glow together. */\n.pillHub::before {\n content: \"\";\n width: calc(var(--spacing-1, 0.25rem) * 1.25);\n height: calc(var(--spacing-1, 0.25rem) * 1.25);\n flex-shrink: 0;\n border-radius: var(--radius-full, 9999px);\n color: var(--doc-nav-accent, var(--accent, #2563eb));\n background: currentColor;\n box-shadow: 0 0 var(--spacing-2, 0.5rem)\n color-mix(in srgb, currentColor 60%, transparent);\n}\n\n/* Leading order number — mono, a step under the pill (B1). */\n.pillNum {\n font-family: var(--font-mono, ui-monospace, \"SFMono-Regular\", Menlo, monospace);\n font-size: calc(var(--font-size-xs, 0.75rem) * 0.8);\n font-variant-numeric: tabular-nums;\n color: var(--text-tertiary, #9ca3af);\n}\n\n.pillActive .pillNum {\n color: var(--doc-nav-accent, var(--accent, #2563eb));\n}\n\n/* Label text. */\n.pillLabel {\n min-width: 0;\n}\n\n/* External-link badge. */\n.pillExternal {\n flex-shrink: 0;\n color: var(--text-tertiary, #9ca3af);\n}\n\n/* Visually-hidden text for assistive technology. */\n.srOnly {\n position: absolute;\n width: var(--stroke-width-thin, 1px);\n height: var(--stroke-width-thin, 1px);\n padding: 0;\n margin: calc(-1 * var(--stroke-width-thin, 1px));\n overflow: hidden;\n clip: rect(0, 0, 0, 0);\n white-space: nowrap;\n border: 0;\n}\n"
|
|
2820
|
+
}
|
|
2821
|
+
]
|
|
2822
|
+
},
|
|
2823
|
+
{
|
|
2824
|
+
"name": "doc-frame",
|
|
2825
|
+
"type": "registry:ui",
|
|
2826
|
+
"description": "The themed doc-page shell that wraps DocNav — a sticky header with a flexible brand/logo slot, the DocNav slot, and a content wrapper for the doc. Reads a single docs manifest, derives the active product, and passes the slice + active-state down to DocNav. Themed entirely by Visor tokens. Replaces the vanilla-JS doc shell (nav.js + docs.css) on the React/route track.",
|
|
2827
|
+
"category": "navigation",
|
|
2828
|
+
"dependencies": [
|
|
2829
|
+
"@phosphor-icons/react",
|
|
2830
|
+
"@loworbitstudio/visor-core"
|
|
2831
|
+
],
|
|
2832
|
+
"registryDependencies": [
|
|
2833
|
+
"utils",
|
|
2834
|
+
"doc-nav"
|
|
2835
|
+
],
|
|
2836
|
+
"files": [
|
|
2837
|
+
{
|
|
2838
|
+
"path": "components/ui/doc-frame/doc-frame.tsx",
|
|
2839
|
+
"type": "registry:ui",
|
|
2840
|
+
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport { CompassIcon } from \"@phosphor-icons/react\"\nimport { cn } from \"../../../lib/utils\"\nimport { DocNav, type DocEntry } from \"../doc-nav/doc-nav\"\nimport styles from \"./doc-frame.module.css\"\n\n/**\n * PL-2185 · VI-609 — the themed doc-page shell that wraps <DocNav>.\n *\n * DocFrame owns the page: theme tokens, a sticky header (a flexible brand/logo\n * slot, an OVERVIEW home pill, and a right-aligned meta slot), the <DocNav>\n * slot, and the content wrapper (the doc as children). It reads the single\n * `manifest`, derives the active product, and passes the slice + active-state\n * down to DocNav. Everything is themed by Visor tokens, so the shell adopts the\n * active project theme without modification.\n *\n * Replaces the vanilla-JS doc shell (nav.js + docs.css). `children` accepts any\n * content — a Next route's MDX, or a static doc's HTML body injected server-side\n * by a downstream catch-all route — so one React frame serves both tracks.\n */\n\n// ─── manifest types ───────────────────────────────────────────────────────────\n\n/**\n * A product entry in the PL-2177 roster. `id` matches the values in each\n * `DocEntry.scope[]` and the DocNav group id; `label` is the visible name\n * (defaults to a title-cased id).\n */\nexport interface DocProductEntry {\n /** Product id — matches `DocEntry.scope[]` values and the DocNav group id. */\n id: string\n /** Visible product name. Defaults to a title-cased `id`. */\n label?: string\n}\n\n/**\n * The parsed docs manifest — the single input DocFrame derives everything from.\n * Additive over PL-2177: `docs` is required; `products`, `brand`, and\n * `dispositions` are optional. Absence of `products` (or a single product) =\n * single-product mode: DocNav degrades to one grouped row, no accordion.\n */\nexport interface DocsManifest {\n /** Every documentation entry across the shared set and all products. */\n docs: DocEntry[]\n /**\n * PL-2177 product roster. Absent or length ≤ 1 → single-product mode.\n * When omitted, products are inferred from the distinct `scope` values in\n * `docs`.\n */\n products?: DocProductEntry[]\n /**\n * Text wordmark shown when no `logo` prop is passed and the active theme\n * ships no `brand.logo` SVG (the final fallback in the logo resolution order).\n */\n brand?: string\n /** PL-2170 disposition map — referenced, owned there. Opaque pass-through. */\n dispositions?: Record<string, unknown>\n}\n\n/** The OVERVIEW / home pill in the header — a link back to the docs hub. */\nexport interface DocFrameHome {\n /** Destination for the home pill. */\n href: string\n /** Visible label — rendered mono, UPPERCASE (e.g. `\"Overview\"`). */\n label: string\n}\n\n// ─── props ─────────────────────────────────────────────────────────────────────\n\nexport interface DocFrameProps\n extends Omit<React.ComponentProps<\"div\">, \"children\"> {\n /** The parsed manifest — the single input everything derives from. */\n manifest: DocsManifest\n /**\n * The brand slot. Any node — an `<img>` of an SVG, an inline `<svg>`, or a\n * full component (an animated mark, a Visor `<Brand>`). Resolution order:\n * explicit `logo` → the active theme's `brand.logo` SVG (mode-aware via\n * `--brand-logo`, upgraded only once it loads) → the manifest `brand` text.\n */\n logo?: React.ReactNode\n /**\n * The OVERVIEW / home pill rendered after the brand — a bordered mono chip\n * with a leading compass glyph. Omit to hide it.\n */\n home?: DocFrameHome\n /**\n * A right-aligned breadcrumb / status slot in the header (mono, UPPERCASE,\n * `--text-tertiary`) — e.g. `ARTIST · BUILD-READY`. Any node.\n */\n meta?: React.ReactNode\n /**\n * Which product group is expanded (the accordion). Defaults to the route's\n * product, else the first product in the roster. Absent roster →\n * single-product mode.\n */\n activeProduct?: string\n /**\n * The active route, for active-state resolution. Next consumers pass\n * `usePathname()`; a static page passes `location.pathname`. Defaults to\n * `window.location.pathname` in the browser (framework-agnostic — no hard\n * next/navigation dependency), or `\"/\"` during SSR.\n */\n currentPath?: string\n /**\n * A Visor theme class name applied to the shell root, scoping all doc-shell\n * CSS variables. Defaults to the app's ambient theme (inherited from an\n * ancestor). e.g. `theme=\"strata-theme\"`.\n */\n theme?: string\n /**\n * Per-group nav accent overrides, keyed by DocNav group id or role, driving\n * its `--doc-nav-group-accent` hook. Merged over the sensible default palette\n * (`pro → --warning`, the amber Pro dot); DocNav already defaults\n * shared → --info, other products → --accent, appendix → --text-tertiary.\n * Values are CSS colors or `var(--token)` references.\n */\n groupAccents?: Record<string, string>\n /**\n * Force the borderless treatment — null the pinned Shared group's frame\n * (`--doc-nav-pin-border: transparent`) and settle its fill onto the card\n * surface, for themes that carry structure from surface contrast, not borders.\n * A theme that is borderless by design nulls the token in its own CSS; this\n * prop is the per-instance escape hatch.\n */\n borderless?: boolean\n /** The doc content, rendered in the content wrapper below the nav. */\n children: React.ReactNode\n}\n\n// ─── constants ─────────────────────────────────────────────────────────────────\n\n/**\n * Default per-group nav accents. DocNav already resolves shared → --info,\n * products → --accent, and appendix → --text-tertiary via its own fallbacks;\n * the only design-intent override the frame supplies is the amber Pro dot.\n */\nconst DEFAULT_GROUP_ACCENTS: Record<string, string> = {\n pro: \"var(--warning, #d97706)\",\n}\n\n// ─── helpers ───────────────────────────────────────────────────────────────────\n\nfunction titleCase(value: string): string {\n if (value.length === 0) return value\n return value.charAt(0).toUpperCase() + value.slice(1)\n}\n\n/** Strip query + hash so an href compares cleanly against the current path. */\nfunction normalizePath(value: string): string {\n return value.replace(/[?#].*$/, \"\")\n}\n\n/** Extract the URL from a computed `url(\"…\")` custom-property value, if any. */\nfunction cssUrl(value: string): string | null {\n const match = value.match(/url\\(\\s*[\"']?([^\"')]+)[\"']?\\s*\\)/)\n return match ? match[1] : null\n}\n\n/**\n * Resolve the product roster: the explicit `manifest.products`, else inferred\n * from the distinct `scope` values across `docs` (first-appearance order).\n */\nfunction resolveProducts(manifest: DocsManifest): DocProductEntry[] {\n if (manifest.products && manifest.products.length > 0) {\n return manifest.products\n }\n const seen = new Map<string, DocProductEntry>()\n for (const doc of manifest.docs) {\n if (!Array.isArray(doc.scope)) continue\n for (const id of doc.scope) {\n if (!seen.has(id)) seen.set(id, { id, label: titleCase(id) })\n }\n }\n return [...seen.values()]\n}\n\n/** The product owning the doc at `path` (its first `scope`), if any. */\nfunction productForPath(docs: DocEntry[], path: string): string | undefined {\n const target = normalizePath(path)\n for (const doc of docs) {\n if (normalizePath(doc.href) !== target) continue\n if (Array.isArray(doc.scope) && doc.scope.length > 0) return doc.scope[0]\n return undefined\n }\n return undefined\n}\n\n// ─── brand slot ────────────────────────────────────────────────────────────────\n\n/**\n * The default brand slot: the active theme's `brand.logo` SVG when the theme\n * ships one (via the `--brand-logo` custom property, mode-aware light/dark),\n * else the manifest `brand` text wordmark (a leading glyph chip + the name).\n *\n * SSR-safe + hardened by construction: the resting state renders the text\n * wordmark (always visible), and a client effect upgrades to the theme logo\n * only once the image actually loads — a 404 keeps the wordmark, never an empty\n * slot, and there is never a flash of invisibility on reload.\n */\nfunction DocFrameBrand({ brand }: { brand?: string }) {\n const ref = React.useRef<HTMLSpanElement>(null)\n const [hasThemeLogo, setHasThemeLogo] = React.useState(false)\n\n React.useEffect(() => {\n const el = ref.current\n if (!el || typeof window === \"undefined\") return\n const value = window\n .getComputedStyle(el)\n .getPropertyValue(\"--brand-logo\")\n .trim()\n const url = value && value !== \"none\" ? cssUrl(value) : null\n if (!url) {\n setHasThemeLogo(false)\n return\n }\n // Hardening: probe-load the logo and only upgrade once it resolves, so a\n // failed load (a 404 theme-logo URL) leaves the visible wordmark in place.\n let cancelled = false\n const probe = new window.Image()\n probe.onload = () => {\n if (!cancelled) setHasThemeLogo(true)\n }\n probe.onerror = () => {\n if (!cancelled) setHasThemeLogo(false)\n }\n probe.src = url\n return () => {\n cancelled = true\n }\n }, [])\n\n const glyphChar = brand?.trim().charAt(0).toUpperCase()\n\n return (\n <span\n ref={ref}\n className={styles.brand}\n data-slot=\"doc-frame-brand\"\n data-theme-logo={hasThemeLogo || undefined}\n >\n <span\n className={styles.themeLogo}\n role=\"img\"\n aria-label={brand ?? \"Documentation\"}\n />\n {brand ? (\n <span className={styles.wordmark}>\n {glyphChar ? (\n <span className={styles.glyph} aria-hidden=\"true\">\n {glyphChar}\n </span>\n ) : null}\n {brand}\n </span>\n ) : null}\n </span>\n )\n}\n\n// ─── DocFrame ────────────────────────────────────────────────────────────────\n\nconst DocFrame = React.forwardRef<HTMLDivElement, DocFrameProps>(\n (\n {\n manifest,\n logo,\n home,\n meta,\n activeProduct,\n currentPath,\n theme,\n groupAccents,\n borderless,\n children,\n className,\n style,\n ...props\n },\n ref\n ) => {\n const resolvedPath =\n currentPath ??\n (typeof window !== \"undefined\" ? window.location.pathname : \"/\")\n\n const products = React.useMemo(() => resolveProducts(manifest), [manifest])\n const isMultiProduct = products.length > 1\n\n // Seed the open accordion product: explicit prop → the route's product →\n // the first product in the roster.\n const seededProduct =\n activeProduct ??\n productForPath(manifest.docs, resolvedPath) ??\n products[0]?.id\n\n const [openProduct, setOpenProduct] = React.useState<string | undefined>(\n seededProduct\n )\n\n // Keep the open product in sync when a controlling `activeProduct` changes.\n React.useEffect(() => {\n if (activeProduct) setOpenProduct(activeProduct)\n }, [activeProduct])\n\n const resolvedLogo = logo ?? <DocFrameBrand brand={manifest.brand} />\n\n // Borderless drops the pinned-group frame via `--doc-nav-pin-border`. Set it\n // explicitly with the `borderless` prop; a theme that is borderless by design\n // nulls the token in its own CSS rather than the component hardcoding (and\n // leaking) private theme names into the public bundle.\n const isBorderless = borderless ?? false\n\n // Per-group nav accents → DocNav's `--doc-nav-group-accent` hook. Rendered\n // as a frame-scoped <style> keyed to this instance so it never leaks to a\n // sibling DocFrame. DocNav only *reads* the variable (via a fallback), so\n // any value set here wins with no specificity battle.\n const frameId = React.useId()\n const resolvedAccents: Record<string, string> = {\n ...DEFAULT_GROUP_ACCENTS,\n ...groupAccents,\n }\n const accentCss = Object.entries(resolvedAccents)\n .map(\n ([key, value]) =>\n `[data-doc-frame=\"${frameId}\"] [data-group=\"${key}\"],` +\n `[data-doc-frame=\"${frameId}\"] [data-role=\"${key}\"]` +\n `{--doc-nav-group-accent:${value}}`\n )\n .join(\"\")\n\n const frameStyle = {\n ...(isBorderless\n ? {\n \"--doc-nav-pin-border\": \"transparent\",\n \"--doc-nav-pin-bg\": \"var(--surface-card)\",\n }\n : {}),\n ...style,\n } as React.CSSProperties\n\n return (\n <div\n ref={ref}\n data-slot=\"doc-frame\"\n data-doc-frame={frameId}\n data-active-product={isMultiProduct ? openProduct : undefined}\n data-borderless={isBorderless || undefined}\n className={cn(styles.frame, theme, className)}\n style={frameStyle}\n {...props}\n >\n {accentCss ? <style>{accentCss}</style> : null}\n <header data-slot=\"doc-frame-header\" className={styles.chrome}>\n <div className={styles.headerRow}>\n <div className={styles.brandSlot} data-slot=\"doc-frame-brand-slot\">\n {resolvedLogo}\n </div>\n {home ? (\n <a\n href={home.href}\n className={styles.home}\n data-slot=\"doc-frame-home\"\n >\n <CompassIcon\n className={styles.homeIcon}\n weight=\"regular\"\n aria-hidden=\"true\"\n />\n <span>{home.label}</span>\n </a>\n ) : null}\n <div className={styles.spacer} aria-hidden=\"true\" />\n {meta ? (\n <div className={styles.meta} data-slot=\"doc-frame-meta\">\n {meta}\n </div>\n ) : null}\n </div>\n <div className={styles.navRow} data-slot=\"doc-frame-nav\">\n <DocNav\n docs={manifest.docs}\n currentPath={resolvedPath}\n activeProduct={isMultiProduct ? openProduct : undefined}\n onProductToggle={isMultiProduct ? setOpenProduct : undefined}\n />\n </div>\n </header>\n\n <div className={styles.content} data-slot=\"doc-frame-content\">\n {children}\n </div>\n </div>\n )\n }\n)\nDocFrame.displayName = \"DocFrame\"\n\nexport { DocFrame }\n"
|
|
2841
|
+
},
|
|
2842
|
+
{
|
|
2843
|
+
"path": "components/ui/doc-frame/doc-frame.module.css",
|
|
2844
|
+
"type": "registry:ui",
|
|
2845
|
+
"content": "/* DocFrame — the themed doc-page shell that wraps DocNav.\n *\n * A sticky header (a flexible brand/logo slot + an OVERVIEW home pill + a\n * right-aligned meta slot, then the DocNav slot) above a content wrapper (the\n * doc as children). The header sticks to the page scroll so the brand + nav\n * never leave view. Theme-agnostic: every value references a CSS custom property\n * token, so the shell adopts the active project theme.\n *\n * No ancestor sets overflow: hidden/auto — that would create a scroll container\n * and break the viewport-anchored sticky header.\n */\n\n/* Shell root — a centered, max-width column. */\n.frame {\n /* A tight, controlled glyph radius the frame owns — NOT the theme `--radius-*`\n * scale, which decorative themes inflate far past what a 24px brand chip wants\n * (Strata inflates `--radius-md` to 10px, `--radius-xl` to 32px). Overridable\n * by a consumer/theme via this variable. */\n --doc-frame-glyph-radius: 0.375rem;\n display: flex;\n flex-direction: column;\n min-height: 100%;\n max-width: var(--doc-frame-max-width, 75rem);\n margin-inline: auto;\n background: var(--surface-page, #ffffff);\n color: var(--text-primary, #111827);\n}\n\n/* Sticky chrome — the header row + nav row. The page surface (opaque, so\n * scrolled content never bleeds through) sits below the card-surfaced nav\n * groups + chips, which read lighter on top. Sticks to the nearest scroll\n * ancestor (the page/viewport). */\n.chrome {\n position: sticky;\n top: 0;\n z-index: 10;\n display: flex;\n flex-direction: column;\n background: var(--surface-page, #ffffff);\n border-bottom: var(--stroke-width-thin, 1px) solid\n var(--hairline, var(--border-default, #e5e7eb));\n}\n\n/* Header row — brand/logo slot, then the OVERVIEW home pill, a flexible spacer,\n * and the right-aligned meta slot. A hairline rule separates it from the nav. */\n.headerRow {\n display: flex;\n align-items: center;\n gap: var(--spacing-3, 0.75rem) var(--spacing-4, 1rem);\n flex-wrap: wrap;\n padding: var(--spacing-3, 0.75rem) var(--spacing-5, 1.25rem);\n border-bottom: var(--stroke-width-thin, 1px) solid\n var(--hairline, var(--border-default, #e5e7eb));\n}\n\n.brandSlot {\n display: inline-flex;\n align-items: center;\n min-width: 0;\n}\n\n.spacer {\n flex: 1 1 auto;\n}\n\n/* Nav row — holds the DocNav slot. */\n.navRow {\n display: flex;\n min-width: 0;\n padding: var(--spacing-3, 0.75rem) var(--spacing-5, 1.25rem);\n}\n\n/* Content wrapper — the doc, as children. */\n.content {\n flex: 1 1 auto;\n padding: var(--spacing-6, 1.5rem) var(--spacing-5, 1.25rem)\n var(--spacing-8, 2rem);\n}\n\n/* ─── brand slot ─────────────────────────────────────────────────────────── */\n\n/* Wraps the theme-logo layer + the text wordmark. */\n.brand {\n position: relative;\n display: inline-flex;\n align-items: center;\n gap: var(--spacing-2, 0.5rem);\n min-width: 0;\n}\n\n/* Text wordmark — the resting/fallback brand (a leading glyph chip + the name).\n * Mode-adaptive via --text-primary. */\n.wordmark {\n display: inline-flex;\n align-items: center;\n gap: var(--spacing-2, 0.5rem);\n font-size: var(--font-size-lg, 1.125rem);\n font-weight: var(--font-weight-bold, 700);\n letter-spacing: -0.01em;\n line-height: 1.1;\n color: var(--text-primary, #111827);\n}\n\n/* Brand glyph — a single-letter mark in the theme's primary brand color, its\n * guaranteed-contrasting on-primary text. A controlled small radius (see\n * `--doc-frame-glyph-radius`), not the ballooning theme radius scale. */\n.glyph {\n display: grid;\n place-items: center;\n flex-shrink: 0;\n width: var(--spacing-6, 1.5rem);\n height: var(--spacing-6, 1.5rem);\n border-radius: var(--doc-frame-glyph-radius, 0.375rem);\n background: var(--primary, #111827);\n color: var(--primary-text, #ffffff);\n font-size: var(--font-size-xs, 0.75rem);\n font-weight: var(--font-weight-bold, 700);\n line-height: 1;\n}\n\n/* Theme-logo layer — the active theme's brand logo SVG (mode-aware via the\n * --brand-logo custom property). Hidden until the client effect confirms the\n * theme ships one AND it loads, so the resting state is always the visible\n * wordmark and a failed load never leaves an empty slot. */\n.themeLogo {\n display: none;\n height: var(--doc-frame-logo-height, var(--spacing-5, 1.25rem));\n /* An explicit min-width keeps a masked/tinted mark from collapsing to a\n * zero-width box on themes that ship no --brand-logo-aspect-ratio. */\n min-width: var(--spacing-8, 2rem);\n aspect-ratio: var(--brand-logo-aspect-ratio, 3 / 1);\n background-image: var(--brand-logo);\n background-repeat: no-repeat;\n background-position: left center;\n background-size: contain;\n}\n\n/* When the theme provides --brand-logo, show the image and drop the wordmark. */\n.brand[data-theme-logo] .themeLogo {\n display: inline-block;\n}\n\n.brand[data-theme-logo] .wordmark {\n display: none;\n}\n\n/* ─── header chrome — home pill + meta ───────────────────────────────────── */\n\n/* OVERVIEW / home pill — a bordered mono chip with a leading compass glyph. */\n.home {\n display: inline-flex;\n align-items: center;\n gap: var(--spacing-2, 0.5rem);\n flex-shrink: 0;\n /* ~6px 12px — a compact chip that clears the mono caps. */\n padding: calc(var(--spacing-1, 0.25rem) * 1.5) var(--spacing-3, 0.75rem);\n border: var(--stroke-width-thin, 1px) solid\n var(--hairline, var(--border-default, #e5e7eb));\n border-radius: var(--radius-full, 9999px);\n background: var(--surface-card, #ffffff);\n font-family: var(--font-mono, ui-monospace, \"SFMono-Regular\", Menlo, monospace);\n font-size: var(--font-size-2xs, 0.6875rem);\n font-weight: var(--font-weight-medium, 500);\n letter-spacing: 0.06em;\n /* Collapse the line box to the glyph height so the all-caps label optically\n * centers against the compass icon. */\n line-height: 1;\n text-transform: uppercase;\n color: var(--text-tertiary, #9ca3af);\n text-decoration: none;\n white-space: nowrap;\n transition: color var(--motion-duration-fast, 150ms)\n var(--motion-easing-standard, ease),\n border-color var(--motion-duration-fast, 150ms)\n var(--motion-easing-standard, ease);\n}\n\n.home:hover {\n color: var(--text-primary, #111827);\n border-color: var(--hairline-strong, var(--border-default, #d1d5db));\n}\n\n.home:focus-visible {\n outline: var(--focus-ring-width, 2px) solid var(--border-focus, currentColor);\n outline-offset: var(--focus-ring-offset, 2px);\n}\n\n/* Compass glyph — a hair larger than the caps (the design's 13px vs 11px);\n * inherits the pill color via currentColor. */\n.homeIcon {\n flex-shrink: 0;\n width: var(--font-size-sm, 0.875rem);\n height: var(--font-size-sm, 0.875rem);\n}\n\n/* Meta slot — a right-aligned breadcrumb/status line, mono UPPERCASE, tertiary. */\n.meta {\n flex-shrink: 0;\n font-family: var(--font-mono, ui-monospace, \"SFMono-Regular\", Menlo, monospace);\n font-size: var(--font-size-2xs, 0.6875rem);\n font-weight: var(--font-weight-medium, 500);\n letter-spacing: 0.12em;\n line-height: 1;\n text-transform: uppercase;\n color: var(--text-tertiary, #9ca3af);\n white-space: nowrap;\n}\n\n/* ─── mobile ─────────────────────────────────────────────────────────────── */\n\n@media (max-width: 640px) {\n .headerRow {\n padding: var(--spacing-3, 0.75rem) var(--spacing-4, 1rem);\n }\n\n .navRow {\n padding: var(--spacing-3, 0.75rem) var(--spacing-4, 1rem);\n }\n\n .content {\n padding: var(--spacing-5, 1.25rem) var(--spacing-4, 1rem)\n var(--spacing-6, 1.5rem);\n }\n}\n"
|
|
2846
|
+
}
|
|
2847
|
+
]
|
|
2848
|
+
},
|
|
2798
2849
|
{
|
|
2799
2850
|
"name": "key-value-list",
|
|
2800
2851
|
"type": "registry:ui",
|
|
@@ -2936,7 +2987,7 @@
|
|
|
2936
2987
|
{
|
|
2937
2988
|
"path": "components/ui/status-badge/status-badge.tsx",
|
|
2938
2989
|
"type": "registry:ui",
|
|
2939
|
-
"content": "import * as React from \"react\"\nimport { cn } from \"../../../lib/utils\"\nimport { Badge } from \"../badge/badge\"\nimport type { BadgeProps } from \"../badge/badge\"\nimport styles from \"./status-badge.module.css\"\n\nexport type StatusBadgeStatus =\n | \"healthy\"\n | \"degraded\"\n | \"down\"\n | \"failed\"\n | \"running\"\n | \"pending\"\n | \"queued\"\n | \"idle\"\n | \"complete\"\n | \"live\"\n | \"warn\"\n | \"scheduled\"\n | \"sold\"\n | \"draft\"\n | \"prospect\"\n | \"pitched\"\n | \"contracted\"\n | \"active\"\n | \"paused\"\n | \"completed\"\n | \"archived\"\n\nexport type StatusBadgeTone = \"subtle\" | \"filled\"\n\nexport interface StatusBadgeProps\n extends Omit<React.HTMLAttributes<HTMLSpanElement>, \"children\"> {\n /** Semantic admin status. Drives both color and default label. */\n status: StatusBadgeStatus\n /** Visible text. Defaults to the capitalized status key. */\n label?: React.ReactNode\n /** Which Badge variant family to use. Defaults to \"subtle\". */\n tone?: StatusBadgeTone\n /** Render the leading indicator dot. Defaults to true. */\n indicator?: boolean\n /** Animate the indicator dot with a soft pulse. Defaults to false. */\n pulse?: boolean\n}\n\n/**\n * Default human-readable labels for each status. Exported so consumers can\n * localize or override without reimplementing the map.\n */\nexport const statusBadgeLabels: Record<StatusBadgeStatus, string> = {\n healthy: \"Healthy\",\n degraded: \"Degraded\",\n down: \"Down\",\n failed: \"Failed\",\n running: \"Running\",\n pending: \"Pending\",\n queued: \"Queued\",\n idle: \"Idle\",\n complete: \"Complete\",\n live: \"Live\",\n warn: \"Warn\",\n scheduled: \"Scheduled\",\n sold: \"Sold\",\n draft: \"Draft\",\n prospect: \"Prospect\",\n pitched: \"Pitched\",\n contracted: \"Contracted\",\n active: \"Active\",\n paused: \"Paused\",\n completed: \"Completed\",\n archived: \"Archived\",\n}\n\ntype BadgeVariant = NonNullable<BadgeProps[\"variant\"]>\n\n/**\n * Color group keyed on the semantic status. Used to pick the indicator dot\n * class and the underlying Badge variant.\n */\ntype StatusColorGroup =\n | \"success\"\n | \"warning\"\n | \"destructive\"\n | \"info\"\n | \"neutral\"\n\nconst STATUS_COLOR_GROUP: Record<StatusBadgeStatus, StatusColorGroup> = {\n healthy: \"success\",\n complete: \"success\",\n degraded: \"warning\",\n pending: \"warning\",\n down: \"destructive\",\n failed: \"destructive\",\n running: \"info\",\n queued: \"neutral\",\n idle: \"neutral\",\n // Admin-ui event tones — map to existing semantic groups.\n // live: active/in-progress positive event → success accent\n // warn: needs attention but not failing → warning\n // scheduled: upcoming/
|
|
2990
|
+
"content": "import * as React from \"react\"\nimport { cn } from \"../../../lib/utils\"\nimport { Badge } from \"../badge/badge\"\nimport type { BadgeProps } from \"../badge/badge\"\nimport styles from \"./status-badge.module.css\"\n\nexport type StatusBadgeStatus =\n | \"healthy\"\n | \"degraded\"\n | \"down\"\n | \"failed\"\n | \"running\"\n | \"pending\"\n | \"queued\"\n | \"idle\"\n | \"complete\"\n | \"live\"\n | \"warn\"\n | \"scheduled\"\n | \"sold\"\n | \"draft\"\n | \"prospect\"\n | \"pitched\"\n | \"contracted\"\n | \"active\"\n | \"paused\"\n | \"completed\"\n | \"archived\"\n\nexport type StatusBadgeTone = \"subtle\" | \"filled\"\n\nexport interface StatusBadgeProps\n extends Omit<React.HTMLAttributes<HTMLSpanElement>, \"children\"> {\n /** Semantic admin status. Drives both color and default label. */\n status: StatusBadgeStatus\n /** Visible text. Defaults to the capitalized status key. */\n label?: React.ReactNode\n /** Which Badge variant family to use. Defaults to \"subtle\". */\n tone?: StatusBadgeTone\n /** Render the leading indicator dot. Defaults to true. */\n indicator?: boolean\n /** Animate the indicator dot with a soft pulse. Defaults to false. */\n pulse?: boolean\n}\n\n/**\n * Default human-readable labels for each status. Exported so consumers can\n * localize or override without reimplementing the map.\n */\nexport const statusBadgeLabels: Record<StatusBadgeStatus, string> = {\n healthy: \"Healthy\",\n degraded: \"Degraded\",\n down: \"Down\",\n failed: \"Failed\",\n running: \"Running\",\n pending: \"Pending\",\n queued: \"Queued\",\n idle: \"Idle\",\n complete: \"Complete\",\n live: \"Live\",\n warn: \"Warn\",\n scheduled: \"Scheduled\",\n sold: \"Sold\",\n draft: \"Draft\",\n prospect: \"Prospect\",\n pitched: \"Pitched\",\n contracted: \"Contracted\",\n active: \"Active\",\n paused: \"Paused\",\n completed: \"Completed\",\n archived: \"Archived\",\n}\n\ntype BadgeVariant = NonNullable<BadgeProps[\"variant\"]>\n\n/**\n * Color group keyed on the semantic status. Used to pick the indicator dot\n * class and the underlying Badge variant.\n */\ntype StatusColorGroup =\n | \"success\"\n | \"warning\"\n | \"destructive\"\n | \"info\"\n | \"neutral\"\n\nconst STATUS_COLOR_GROUP: Record<StatusBadgeStatus, StatusColorGroup> = {\n healthy: \"success\",\n complete: \"success\",\n degraded: \"warning\",\n pending: \"warning\",\n down: \"destructive\",\n failed: \"destructive\",\n running: \"info\",\n queued: \"neutral\",\n idle: \"neutral\",\n // Admin-ui event tones — map to existing semantic groups.\n // live: active/in-progress positive event → success accent\n // warn: needs attention but not failing → warning\n // scheduled: upcoming/committed, distinct from draft's muted grey → info\n // sold: positive completed outcome → success\n // draft: unpublished/muted → neutral\n live: \"success\",\n warn: \"warning\",\n scheduled: \"info\",\n sold: \"success\",\n draft: \"neutral\",\n // CRM / pipeline stages (VI-492) — bind to existing semantic groups.\n // No new tokens: stages that share a meaning share a color.\n // prospect: new informational lead → info\n // pitched: awaiting response, needs attention → warning\n // contracted / active / completed: positive, in-good-standing → success\n // paused: temporarily on hold, needs attention → warning\n // archived: closed/muted, grouped with draft → neutral\n prospect: \"info\",\n pitched: \"warning\",\n contracted: \"success\",\n active: \"success\",\n paused: \"warning\",\n completed: \"success\",\n archived: \"neutral\",\n}\n\nconst SUBTLE_VARIANT: Record<StatusColorGroup, BadgeVariant> = {\n success: \"success\",\n warning: \"warning\",\n destructive: \"destructive\",\n info: \"info\",\n neutral: \"neutral\",\n}\n\nconst FILLED_VARIANT: Record<StatusColorGroup, BadgeVariant> = {\n success: \"filled-success\",\n warning: \"filled-warning\",\n destructive: \"filled-destructive\",\n info: \"filled-info\",\n neutral: \"filled-neutral\",\n}\n\nconst INDICATOR_CLASS: Record<StatusColorGroup, string> = {\n success: styles.indicatorSuccess,\n warning: styles.indicatorWarning,\n destructive: styles.indicatorDestructive,\n info: styles.indicatorInfo,\n neutral: styles.indicatorNeutral,\n}\n\nconst StatusBadge = React.forwardRef<HTMLSpanElement, StatusBadgeProps>(\n (\n {\n className,\n status,\n label,\n tone = \"subtle\",\n indicator = true,\n pulse = false,\n ...props\n },\n ref\n ) => {\n const group = STATUS_COLOR_GROUP[status]\n const variant: BadgeVariant =\n tone === \"filled\" ? FILLED_VARIANT[group] : SUBTLE_VARIANT[group]\n const visibleLabel = label ?? statusBadgeLabels[status]\n\n return (\n <Badge\n ref={ref}\n variant={variant}\n data-slot=\"status-badge\"\n data-status={status}\n data-tone={tone}\n className={cn(className)}\n {...props}\n >\n {indicator ? (\n <span\n data-slot=\"status-badge-indicator\"\n aria-hidden=\"true\"\n className={cn(\n styles.indicator,\n INDICATOR_CLASS[group],\n pulse && styles.pulse\n )}\n />\n ) : null}\n <span className={styles.srOnly}>Status: </span>\n <span data-slot=\"status-badge-label\">{visibleLabel}</span>\n </Badge>\n )\n }\n)\nStatusBadge.displayName = \"StatusBadge\"\n\nexport { StatusBadge }\n"
|
|
2940
2991
|
},
|
|
2941
2992
|
{
|
|
2942
2993
|
"path": "components/ui/status-badge/status-badge.module.css",
|
|
@@ -3858,7 +3909,9 @@
|
|
|
3858
3909
|
"@loworbitstudio/visor-core"
|
|
3859
3910
|
],
|
|
3860
3911
|
"registryDependencies": [
|
|
3861
|
-
"utils"
|
|
3912
|
+
"utils"
|
|
3913
|
+
],
|
|
3914
|
+
"suggestedDependencies": [
|
|
3862
3915
|
"breadcrumb",
|
|
3863
3916
|
"dropdown-menu",
|
|
3864
3917
|
"sidebar"
|
|
@@ -3934,6 +3987,34 @@
|
|
|
3934
3987
|
}
|
|
3935
3988
|
]
|
|
3936
3989
|
},
|
|
3990
|
+
{
|
|
3991
|
+
"name": "admin-detail",
|
|
3992
|
+
"type": "registry:block",
|
|
3993
|
+
"description": "Full-page, read-oriented detail RECORD for the admin-shell main column. Composes an identity header (media + title + StatusBadge + actions), N key-value sections built on KeyValueList, an optional sensitive/reveal panel gated behind a Switch, and optional sub-list slots for ledgers or history. The full-page sibling to admin-detail-drawer.",
|
|
3994
|
+
"category": "admin",
|
|
3995
|
+
"dependencies": [
|
|
3996
|
+
"@loworbitstudio/visor-core"
|
|
3997
|
+
],
|
|
3998
|
+
"registryDependencies": [
|
|
3999
|
+
"utils",
|
|
4000
|
+
"key-value-list",
|
|
4001
|
+
"status-badge",
|
|
4002
|
+
"switch",
|
|
4003
|
+
"separator"
|
|
4004
|
+
],
|
|
4005
|
+
"files": [
|
|
4006
|
+
{
|
|
4007
|
+
"path": "blocks/admin-detail/admin-detail.tsx",
|
|
4008
|
+
"type": "registry:block",
|
|
4009
|
+
"content": "\"use client\"\n\nimport * as React from \"react\"\n\nimport { cn } from \"../../lib/utils\"\nimport {\n KeyValueList,\n type KeyValueItem,\n} from \"../../components/ui/key-value-list/key-value-list\"\nimport {\n StatusBadge,\n statusBadgeLabels,\n type StatusBadgeStatus,\n} from \"../../components/ui/status-badge/status-badge\"\nimport { Switch } from \"../../components/ui/switch/switch\"\nimport { Separator } from \"../../components/ui/separator/separator\"\nimport styles from \"./admin-detail.module.css\"\n\n// ─── Shared KeyValueList passthrough ─────────────────────────────────────────\n\n/** KeyValueList configuration shared by record sections and the sensitive panel. */\ninterface KeyValueConfig {\n /** Label/value pairs rendered via the composed `KeyValueList`. */\n items?: KeyValueItem[]\n /** Grid column count forwarded to `KeyValueList`. Defaults to 2. */\n columns?: 1 | 2 | 3 | 4\n /** Label placement forwarded to `KeyValueList`. Defaults to `stacked`. */\n orientation?: \"horizontal\" | \"stacked\"\n /** Row density forwarded to `KeyValueList`. Defaults to `editorial`. */\n density?: \"compact\" | \"default\" | \"editorial\"\n}\n\n// ─── Types ───────────────────────────────────────────────────────────────────\n\nexport interface AdminDetailSection extends KeyValueConfig {\n /** Stable identifier — becomes the section's DOM `id` anchor. */\n id?: string\n /** Small uppercase label rendered above the section title. */\n eyebrow?: React.ReactNode\n /** Section heading. */\n title?: React.ReactNode\n /** Supporting copy rendered below the section title. */\n description?: React.ReactNode\n /** Right-aligned action slot for the section header (edit link, menu, etc.). */\n actions?: React.ReactNode\n /**\n * Arbitrary sub-list content rendered below the key-value pairs — invoice\n * ledger rows, booking history, or any bespoke table. Renders after `items`.\n */\n content?: React.ReactNode\n}\n\nexport interface AdminDetailSensitivePanel extends KeyValueConfig {\n /** Stable identifier — becomes the panel's DOM `id` anchor. */\n id?: string\n /** Small uppercase label rendered above the panel title. */\n eyebrow?: React.ReactNode\n /** Panel heading — e.g. \"Tax & Banking\". */\n title?: React.ReactNode\n /** Supporting copy rendered below the panel title. */\n description?: React.ReactNode\n /** Extra content revealed alongside `items` when the panel is unlocked. */\n content?: React.ReactNode\n /** Label paired with the reveal switch. Defaults to \"Reveal\". */\n revealLabel?: React.ReactNode\n /** Note shown while the panel is hidden. Defaults to \"Hidden for privacy.\" */\n hiddenNote?: React.ReactNode\n /** Controlled reveal state. Omit for uncontrolled behavior. */\n revealed?: boolean\n /** Reveal-state change handler (fires in both controlled and uncontrolled modes). */\n onRevealedChange?: (revealed: boolean) => void\n /** Initial reveal state when uncontrolled. Defaults to false. */\n defaultRevealed?: boolean\n}\n\nexport interface AdminDetailProps\n extends Omit<React.HTMLAttributes<HTMLDivElement>, \"title\"> {\n // ── Identity header ───────────────────────────────────────────────────────\n /** Small uppercase label rendered above the record title. */\n eyebrow?: React.ReactNode\n /** Record title — the primary identity of the page. */\n title: React.ReactNode\n /** Supporting line beneath the title (email, handle, category, etc.). */\n subtitle?: React.ReactNode\n /** Leading media slot — Avatar, logo plate, or icon. */\n media?: React.ReactNode\n /**\n * Record status. A `StatusBadgeStatus` string renders a composed\n * `StatusBadge`; any other node renders as-is.\n */\n status?: StatusBadgeStatus | React.ReactNode\n /** Breadcrumb node rendered above the identity row. */\n breadcrumb?: React.ReactNode\n /** Right-aligned header action slot (edit, archive, overflow menu). */\n actions?: React.ReactNode\n /** Replace the default identity header entirely with custom chrome. */\n header?: React.ReactNode\n /** Suppress the hairline divider beneath the identity header. */\n hideHeaderDivider?: boolean\n\n // ── Body ──────────────────────────────────────────────────────────────────\n /** Key-value record sections, each composing a `KeyValueList`. */\n sections?: AdminDetailSection[]\n /** Optional sensitive/reveal panel gated behind a reveal switch. */\n sensitive?: AdminDetailSensitivePanel\n /** Arbitrary trailing content appended after the sections. */\n children?: React.ReactNode\n\n // ── Layout ────────────────────────────────────────────────────────────────\n /** Max-width of the record column. Number is treated as pixels. */\n maxWidth?: number | string\n}\n\n// ─── Helpers ─────────────────────────────────────────────────────────────────\n\nfunction isStatusBadgeStatus(\n value: React.ReactNode\n): value is StatusBadgeStatus {\n return typeof value === \"string\" && value in statusBadgeLabels\n}\n\nfunction resolveSize(\n value: number | string | undefined,\n fallback: string\n): string {\n if (value == null) return fallback\n return typeof value === \"number\" ? `${value}px` : value\n}\n\n// ─── Section renderer ────────────────────────────────────────────────────────\n\nfunction SectionHeader({\n eyebrow,\n title,\n description,\n actions,\n}: {\n eyebrow?: React.ReactNode\n title?: React.ReactNode\n description?: React.ReactNode\n actions?: React.ReactNode\n}) {\n if (!eyebrow && !title && !description && !actions) return null\n return (\n <div className={styles.sectionHeader} data-slot=\"admin-detail-section-header\">\n <div className={styles.sectionHeaderText}>\n {eyebrow ? (\n <p className={styles.sectionEyebrow}>{eyebrow}</p>\n ) : null}\n {title ? <h2 className={styles.sectionTitle}>{title}</h2> : null}\n {description ? (\n <p className={styles.sectionDescription}>{description}</p>\n ) : null}\n </div>\n {actions ? (\n <div className={styles.sectionActions}>{actions}</div>\n ) : null}\n </div>\n )\n}\n\nfunction RecordSection({ section }: { section: AdminDetailSection }) {\n const hasItems = section.items != null && section.items.length > 0\n return (\n <section\n id={section.id}\n className={styles.section}\n data-slot=\"admin-detail-section\"\n >\n <SectionHeader\n eyebrow={section.eyebrow}\n title={section.title}\n description={section.description}\n actions={section.actions}\n />\n {hasItems ? (\n <KeyValueList\n items={section.items!}\n columns={section.columns ?? 2}\n orientation={section.orientation ?? \"stacked\"}\n density={section.density ?? \"editorial\"}\n />\n ) : null}\n {section.content}\n </section>\n )\n}\n\n// ─── Sensitive panel ─────────────────────────────────────────────────────────\n\nfunction SensitivePanel({ panel }: { panel: AdminDetailSensitivePanel }) {\n const isControlled = panel.revealed !== undefined\n const [internalRevealed, setInternalRevealed] = React.useState(\n panel.defaultRevealed ?? false\n )\n const revealed = isControlled\n ? (panel.revealed as boolean)\n : internalRevealed\n const switchId = React.useId()\n const contentId = React.useId()\n\n const handleChange = React.useCallback(\n (next: boolean) => {\n if (!isControlled) setInternalRevealed(next)\n panel.onRevealedChange?.(next)\n },\n [isControlled, panel]\n )\n\n const hasItems = panel.items != null && panel.items.length > 0\n const revealLabel = panel.revealLabel ?? \"Reveal\"\n const hiddenNote = panel.hiddenNote ?? \"Hidden for privacy.\"\n\n return (\n <section\n id={panel.id}\n className={styles.sensitive}\n data-slot=\"admin-detail-sensitive\"\n data-revealed={revealed ? \"\" : undefined}\n >\n <div className={styles.sensitiveHeader}>\n <div className={styles.sectionHeaderText}>\n {panel.eyebrow ? (\n <p className={styles.sectionEyebrow}>{panel.eyebrow}</p>\n ) : null}\n {panel.title ? (\n <h2 className={styles.sectionTitle}>{panel.title}</h2>\n ) : null}\n {panel.description ? (\n <p className={styles.sectionDescription}>{panel.description}</p>\n ) : null}\n </div>\n <div\n className={styles.revealControl}\n data-slot=\"admin-detail-reveal\"\n >\n <label htmlFor={switchId} className={styles.revealLabel}>\n {revealLabel}\n </label>\n <Switch\n id={switchId}\n checked={revealed}\n onCheckedChange={handleChange}\n aria-controls={contentId}\n />\n </div>\n </div>\n\n <div\n id={contentId}\n className={styles.sensitiveBody}\n data-slot=\"admin-detail-sensitive-body\"\n >\n {revealed ? (\n <>\n {hasItems ? (\n <KeyValueList\n items={panel.items!}\n columns={panel.columns ?? 2}\n orientation={panel.orientation ?? \"stacked\"}\n density={panel.density ?? \"editorial\"}\n />\n ) : null}\n {panel.content}\n </>\n ) : (\n <p className={styles.sensitiveNote}>{hiddenNote}</p>\n )}\n </div>\n </section>\n )\n}\n\n// ─── AdminDetail ─────────────────────────────────────────────────────────────\n\nconst AdminDetail = React.forwardRef<HTMLDivElement, AdminDetailProps>(\n function AdminDetail(\n {\n eyebrow,\n title,\n subtitle,\n media,\n status,\n breadcrumb,\n actions,\n header,\n hideHeaderDivider = false,\n sections,\n sensitive,\n children,\n maxWidth,\n className,\n style,\n ...rest\n },\n ref\n ) {\n const resolvedStatus = isStatusBadgeStatus(status) ? (\n <StatusBadge status={status} />\n ) : (\n status\n )\n\n const rootStyle = {\n ...style,\n [\"--admin-detail-max-width\" as string]: resolveSize(maxWidth, \"none\"),\n } as React.CSSProperties\n\n // Collect body regions so dividers interleave cleanly.\n const regions: React.ReactNode[] = []\n sections?.forEach((section, i) => {\n regions.push(\n <RecordSection key={section.id ?? `section-${i}`} section={section} />\n )\n })\n if (sensitive) {\n regions.push(\n <SensitivePanel key={sensitive.id ?? \"sensitive\"} panel={sensitive} />\n )\n }\n if (children != null) {\n regions.push(\n <div\n key=\"extra\"\n className={styles.section}\n data-slot=\"admin-detail-extra\"\n >\n {children}\n </div>\n )\n }\n\n return (\n <div\n ref={ref}\n className={cn(styles.root, className)}\n style={rootStyle}\n data-slot=\"admin-detail\"\n {...rest}\n >\n {header ?? (\n <header\n className={cn(\n styles.identity,\n !hideHeaderDivider && styles.identityDivided\n )}\n data-slot=\"admin-detail-header\"\n >\n {breadcrumb ? (\n <div\n className={styles.breadcrumb}\n data-slot=\"admin-detail-breadcrumb\"\n >\n {breadcrumb}\n </div>\n ) : null}\n <div className={styles.identityRow}>\n {media ? (\n <div\n className={styles.media}\n data-slot=\"admin-detail-media\"\n >\n {media}\n </div>\n ) : null}\n <div className={styles.identityText}>\n {eyebrow ? (\n <p className={styles.eyebrow}>{eyebrow}</p>\n ) : null}\n <div className={styles.titleRow}>\n <h1\n className={styles.title}\n data-slot=\"admin-detail-title\"\n >\n {title}\n </h1>\n {resolvedStatus ? (\n <span\n className={styles.status}\n data-slot=\"admin-detail-status\"\n >\n {resolvedStatus}\n </span>\n ) : null}\n </div>\n {subtitle ? (\n <p className={styles.subtitle}>{subtitle}</p>\n ) : null}\n </div>\n {actions ? (\n <div\n className={styles.actions}\n data-slot=\"admin-detail-actions\"\n >\n {actions}\n </div>\n ) : null}\n </div>\n </header>\n )}\n\n {regions.length > 0 ? (\n <div className={styles.body} data-slot=\"admin-detail-body\">\n {regions.map((node, i) => (\n <React.Fragment key={i}>\n {i > 0 ? (\n <Separator className={styles.divider} decorative />\n ) : null}\n {node}\n </React.Fragment>\n ))}\n </div>\n ) : null}\n </div>\n )\n }\n)\n\nAdminDetail.displayName = \"AdminDetail\"\n\nexport { AdminDetail }\n"
|
|
4010
|
+
},
|
|
4011
|
+
{
|
|
4012
|
+
"path": "blocks/admin-detail/admin-detail.module.css",
|
|
4013
|
+
"type": "registry:block",
|
|
4014
|
+
"content": "/* Admin Detail\n * Full-page, read-oriented detail RECORD for the admin-shell main column.\n * Vertical stack: identity header → N key-value sections (composed\n * KeyValueList) → optional sensitive/reveal panel → optional sub-list slots,\n * separated by hairline dividers. The natural sibling to admin-list-page and\n * admin-detail-drawer — a page, never a drawer.\n */\n\n.root {\n container-type: inline-size;\n display: flex;\n flex-direction: column;\n width: 100%;\n max-width: var(--admin-detail-max-width, none);\n gap: var(--spacing-8, 2rem);\n color: var(--text-primary, #111827);\n}\n\n/* ── Identity header ──────────────────────────────────────────────── */\n.identity {\n display: flex;\n flex-direction: column;\n gap: var(--spacing-4, 1rem);\n}\n\n.identityDivided {\n padding-bottom: var(--spacing-8, 2rem);\n border-bottom: var(--stroke-width-thin, 1px) solid\n var(--border-muted, #e5e7eb);\n}\n\n.breadcrumb {\n min-width: 0;\n}\n\n.identityRow {\n display: flex;\n flex-direction: row;\n align-items: flex-start;\n gap: var(--spacing-4, 1rem);\n}\n\n.media {\n flex: 0 0 auto;\n display: flex;\n align-items: center;\n}\n\n.identityText {\n flex: 1 1 auto;\n min-width: 0;\n display: flex;\n flex-direction: column;\n gap: var(--spacing-2, 0.5rem);\n}\n\n.eyebrow {\n margin: 0;\n font-size: var(--font-size-xs, 0.6875rem);\n font-weight: var(--font-weight-medium, 500);\n letter-spacing: 0.12em;\n text-transform: uppercase;\n color: var(--text-tertiary, #6b7280);\n line-height: 1.4;\n}\n\n.titleRow {\n display: flex;\n flex-direction: row;\n align-items: center;\n flex-wrap: wrap;\n gap: var(--spacing-3, 0.75rem);\n min-width: 0;\n}\n\n.title {\n margin: 0;\n font-size: var(--font-size-2xl, 1.5rem);\n font-weight: var(--font-weight-semibold, 600);\n line-height: 1.2;\n color: var(--text-primary, #111827);\n}\n\n.status {\n flex: 0 0 auto;\n display: inline-flex;\n align-items: center;\n}\n\n.subtitle {\n margin: 0;\n font-size: var(--font-size-sm, 0.875rem);\n color: var(--text-secondary, #6b7280);\n line-height: 1.5;\n}\n\n.actions {\n flex: 0 0 auto;\n display: flex;\n flex-direction: row;\n align-items: center;\n gap: var(--spacing-2, 0.5rem);\n}\n\n/* ── Body / sections ──────────────────────────────────────────────── */\n.body {\n display: flex;\n flex-direction: column;\n gap: var(--spacing-8, 2rem);\n min-width: 0;\n}\n\n.section {\n display: flex;\n flex-direction: column;\n gap: var(--spacing-5, 1.25rem);\n min-width: 0;\n}\n\n.sectionHeader {\n display: flex;\n flex-direction: row;\n align-items: flex-start;\n justify-content: space-between;\n gap: var(--spacing-4, 1rem);\n min-width: 0;\n}\n\n.sectionHeaderText {\n flex: 1 1 auto;\n min-width: 0;\n display: flex;\n flex-direction: column;\n gap: var(--spacing-1, 0.25rem);\n}\n\n.sectionEyebrow {\n margin: 0;\n font-size: var(--font-size-xs, 0.6875rem);\n font-weight: var(--font-weight-medium, 500);\n letter-spacing: 0.12em;\n text-transform: uppercase;\n color: var(--text-tertiary, #6b7280);\n line-height: 1.4;\n}\n\n.sectionTitle {\n margin: 0;\n font-size: var(--font-size-lg, 1.125rem);\n font-weight: var(--font-weight-semibold, 600);\n line-height: 1.3;\n color: var(--text-primary, #111827);\n}\n\n.sectionDescription {\n margin: 0;\n font-size: var(--font-size-sm, 0.875rem);\n color: var(--text-secondary, #6b7280);\n line-height: 1.5;\n}\n\n.sectionActions {\n flex: 0 0 auto;\n display: flex;\n flex-direction: row;\n align-items: center;\n gap: var(--spacing-2, 0.5rem);\n}\n\n/* Divider between body regions. */\n.divider {\n margin: 0;\n}\n\n/* ── Sensitive / reveal panel ─────────────────────────────────────── */\n.sensitive {\n display: flex;\n flex-direction: column;\n gap: var(--spacing-4, 1rem);\n padding: var(--spacing-5, 1.25rem);\n border: var(--stroke-width-thin, 1px) solid var(--border-muted, #e5e7eb);\n border-radius: var(--radius-lg, 0.75rem);\n background-color: var(--surface-subtle, #f9fafb);\n}\n\n.sensitiveHeader {\n display: flex;\n flex-direction: row;\n align-items: flex-start;\n justify-content: space-between;\n gap: var(--spacing-4, 1rem);\n min-width: 0;\n}\n\n.revealControl {\n flex: 0 0 auto;\n display: flex;\n flex-direction: row;\n align-items: center;\n gap: var(--spacing-2, 0.5rem);\n}\n\n.revealLabel {\n font-size: var(--font-size-sm, 0.875rem);\n font-weight: var(--font-weight-medium, 500);\n color: var(--text-secondary, #6b7280);\n cursor: pointer;\n user-select: none;\n}\n\n.sensitiveBody {\n min-width: 0;\n}\n\n.sensitiveNote {\n margin: 0;\n font-size: var(--font-size-sm, 0.875rem);\n font-style: italic;\n color: var(--text-tertiary, #6b7280);\n}\n\n/* ── Responsive ───────────────────────────────────────────────────── */\n@container (max-width: 640px) {\n .identityRow {\n flex-direction: column;\n }\n\n .actions {\n width: 100%;\n }\n\n .sectionHeader,\n .sensitiveHeader {\n flex-direction: column;\n }\n}\n"
|
|
4015
|
+
}
|
|
4016
|
+
]
|
|
4017
|
+
},
|
|
3937
4018
|
{
|
|
3938
4019
|
"name": "admin-detail-drawer",
|
|
3939
4020
|
"type": "registry:block",
|
|
@@ -4103,6 +4184,31 @@
|
|
|
4103
4184
|
}
|
|
4104
4185
|
]
|
|
4105
4186
|
},
|
|
4187
|
+
{
|
|
4188
|
+
"name": "month-calendar",
|
|
4189
|
+
"type": "registry:block",
|
|
4190
|
+
"description": "Theme-portable month event-grid (scheduler) block: a 6×7 day-cell grid under a localized weekday header, with prev/next month navigation, a view-mode segmented control (Month / Week / Day), and per-cell event chips. Each chip carries a status-dot slot (success / warning / danger / info) and an optional series tint (1–5) keyed to the theme's chart ramp. Days outside the displayed month are dimmed; optional today and selected-day highlighting. Fully token-driven and theme-agnostic.",
|
|
4191
|
+
"category": "data-display",
|
|
4192
|
+
"dependencies": [
|
|
4193
|
+
"@loworbitstudio/visor-core",
|
|
4194
|
+
"@phosphor-icons/react"
|
|
4195
|
+
],
|
|
4196
|
+
"registryDependencies": [
|
|
4197
|
+
"utils"
|
|
4198
|
+
],
|
|
4199
|
+
"files": [
|
|
4200
|
+
{
|
|
4201
|
+
"path": "blocks/month-calendar/month-calendar.tsx",
|
|
4202
|
+
"type": "registry:block",
|
|
4203
|
+
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport { CaretLeft, CaretRight } from \"@phosphor-icons/react\"\n\nimport { cn } from \"../../lib/utils\"\nimport styles from \"./month-calendar.module.css\"\n\n/**\n * Status tone driving the leading dot on an event chip. Each tone binds to a\n * Visor semantic status token so the dot adopts the active theme's palette.\n */\nexport type MonthCalendarStatus =\n | \"default\"\n | \"success\"\n | \"warning\"\n | \"danger\"\n | \"info\"\n\n/** Series tint index — keyed to the theme's five-stop chart ramp. */\nexport type MonthCalendarSeries = 1 | 2 | 3 | 4 | 5\n\nexport interface MonthCalendarEvent {\n /** Stable key for the event. */\n id: string\n /**\n * The day this event lands in. Only the calendar date (year/month/day) is\n * used for placement — any time component is ignored.\n */\n date: Date\n /** Chip label. */\n title: string\n /**\n * Status tone for the leading dot. Omit for a neutral dot. Binds to the\n * `--surface-{success,warning,error,info}-default` semantic tokens.\n */\n status?: MonthCalendarStatus\n /**\n * Series tint (1–5). Events sharing an index render with the same background\n * tint and accent bar, keyed to the theme's chart color ramp — the standard\n * way to color-code a recurring series or a resource lane.\n */\n series?: MonthCalendarSeries\n}\n\nexport interface MonthCalendarViewOption {\n /** Machine value emitted via `onViewChange`. */\n value: string\n /** Human label rendered in the segment. */\n label: React.ReactNode\n}\n\nexport interface MonthCalendarProps\n extends Omit<React.HTMLAttributes<HTMLDivElement>, \"onSelect\"> {\n /** Any date within the month to display. Controlled. */\n month: Date\n /**\n * Fired with the first day of the previous / next month when the month-nav\n * arrows are used.\n */\n onMonthChange?: (month: Date) => void\n /** Events to place into day cells. */\n events?: MonthCalendarEvent[]\n /**\n * The day the grid marks as \"today\". Omit to mark none — the block never\n * reads the system clock itself, keeping server and client render identical\n * (no hydration mismatch). Pass `new Date()` from a client boundary to opt in.\n */\n today?: Date\n /** Selected day, highlighted distinctly from today. */\n selectedDate?: Date\n /** Fired when a day cell is activated (only when provided — cells are inert otherwise). */\n onSelectDate?: (date: Date) => void\n /** Fired when an event chip is activated (only when provided — chips are inert otherwise). */\n onEventSelect?: (event: MonthCalendarEvent) => void\n /** First column of the week: 0 = Sunday (default), 1 = Monday. */\n weekStartsOn?: 0 | 1\n /** Max chips shown per day before collapsing to a \"+N more\" row. Default 3. */\n maxChipsPerDay?: number\n /** BCP-47 locale for the month title and weekday headers. Default `\"en-US\"`. */\n locale?: string\n /** View-mode options for the segmented control. Default Month / Week / Day. */\n viewOptions?: MonthCalendarViewOption[]\n /** Active view value (controlled). Falls back to internal state when omitted. */\n view?: string\n /** Fired when a view-mode segment is chosen. */\n onViewChange?: (view: string) => void\n}\n\nconst DEFAULT_VIEW_OPTIONS: MonthCalendarViewOption[] = [\n { value: \"month\", label: \"Month\" },\n { value: \"week\", label: \"Week\" },\n { value: \"day\", label: \"Day\" },\n]\n\nconst DAYS_IN_GRID = 42\n// 2023-01-01 was a Sunday — a fixed anchor for deriving localized weekday names.\nconst WEEKDAY_ANCHOR_YEAR = 2023\n\nfunction firstOfMonth(date: Date): Date {\n return new Date(date.getFullYear(), date.getMonth(), 1)\n}\n\nfunction addMonths(date: Date, delta: number): Date {\n return new Date(date.getFullYear(), date.getMonth() + delta, 1)\n}\n\nfunction isSameDay(a: Date | undefined, b: Date): boolean {\n return (\n a !== undefined &&\n a.getFullYear() === b.getFullYear() &&\n a.getMonth() === b.getMonth() &&\n a.getDate() === b.getDate()\n )\n}\n\nfunction dayKey(date: Date): string {\n return `${date.getFullYear()}-${date.getMonth()}-${date.getDate()}`\n}\n\nconst MonthCalendar = React.forwardRef<HTMLDivElement, MonthCalendarProps>(\n function MonthCalendar(\n {\n month,\n onMonthChange,\n events = [],\n today,\n selectedDate,\n onSelectDate,\n onEventSelect,\n weekStartsOn = 0,\n maxChipsPerDay = 3,\n locale = \"en-US\",\n viewOptions = DEFAULT_VIEW_OPTIONS,\n view,\n onViewChange,\n className,\n ...rest\n },\n ref\n ) {\n const [internalView, setInternalView] = React.useState<string>(\n () => view ?? viewOptions[0]?.value ?? \"month\"\n )\n const activeView = view ?? internalView\n\n const handleViewSelect = React.useCallback(\n (next: string) => {\n if (view === undefined) setInternalView(next)\n onViewChange?.(next)\n },\n [view, onViewChange]\n )\n\n const monthLabel = React.useMemo(\n () =>\n new Intl.DateTimeFormat(locale, {\n month: \"long\",\n year: \"numeric\",\n }).format(month),\n [locale, month]\n )\n\n const weekdayLabels = React.useMemo(() => {\n const formatter = new Intl.DateTimeFormat(locale, { weekday: \"short\" })\n return Array.from({ length: 7 }, (_, i) =>\n formatter.format(\n new Date(WEEKDAY_ANCHOR_YEAR, 0, 1 + ((weekStartsOn + i) % 7))\n )\n )\n }, [locale, weekStartsOn])\n\n const cells = React.useMemo(() => {\n const first = firstOfMonth(month)\n const offset = (first.getDay() - weekStartsOn + 7) % 7\n return Array.from({ length: DAYS_IN_GRID }, (_, i) =>\n new Date(first.getFullYear(), first.getMonth(), 1 - offset + i)\n )\n }, [month, weekStartsOn])\n\n const eventsByDay = React.useMemo(() => {\n const map = new Map<string, MonthCalendarEvent[]>()\n for (const event of events) {\n const key = dayKey(event.date)\n const bucket = map.get(key)\n if (bucket) bucket.push(event)\n else map.set(key, [event])\n }\n return map\n }, [events])\n\n const dayLabelFormatter = React.useMemo(\n () =>\n new Intl.DateTimeFormat(locale, {\n month: \"long\",\n day: \"numeric\",\n year: \"numeric\",\n }),\n [locale]\n )\n\n const displayedMonth = month.getMonth()\n\n return (\n <div\n ref={ref}\n className={cn(styles.root, className)}\n data-slot=\"month-calendar\"\n {...rest}\n >\n <div className={styles.header} data-slot=\"month-calendar-header\">\n <div className={styles.nav} data-slot=\"month-calendar-nav\">\n <button\n type=\"button\"\n className={styles.navButton}\n onClick={() => onMonthChange?.(addMonths(month, -1))}\n disabled={!onMonthChange}\n aria-label=\"Previous month\"\n data-slot=\"month-calendar-prev\"\n >\n <CaretLeft weight=\"bold\" aria-hidden />\n </button>\n <h2 className={styles.monthLabel} data-slot=\"month-calendar-label\">\n {monthLabel}\n </h2>\n <button\n type=\"button\"\n className={styles.navButton}\n onClick={() => onMonthChange?.(addMonths(month, 1))}\n disabled={!onMonthChange}\n aria-label=\"Next month\"\n data-slot=\"month-calendar-next\"\n >\n <CaretRight weight=\"bold\" aria-hidden />\n </button>\n </div>\n\n {viewOptions.length > 0 ? (\n <div\n className={styles.segmented}\n role=\"group\"\n aria-label=\"Calendar view\"\n data-slot=\"month-calendar-view\"\n >\n {viewOptions.map((option) => (\n <button\n key={option.value}\n type=\"button\"\n className={styles.segment}\n data-active={option.value === activeView ? \"true\" : undefined}\n aria-pressed={option.value === activeView}\n onClick={() => handleViewSelect(option.value)}\n >\n {option.label}\n </button>\n ))}\n </div>\n ) : null}\n </div>\n\n <div className={styles.weekdays} data-slot=\"month-calendar-weekdays\">\n {weekdayLabels.map((label, i) => (\n <div key={i} className={styles.weekday} aria-hidden=\"true\">\n {label}\n </div>\n ))}\n </div>\n\n <div className={styles.grid} data-slot=\"month-calendar-grid\">\n {cells.map((cell) => {\n const dayEvents = eventsByDay.get(dayKey(cell)) ?? []\n const visible = dayEvents.slice(0, maxChipsPerDay)\n const overflow = dayEvents.length - visible.length\n const outside = cell.getMonth() !== displayedMonth\n const isToday = isSameDay(today, cell)\n const isSelected = isSameDay(selectedDate, cell)\n const dayLabel = dayLabelFormatter.format(cell)\n const cellLabel =\n dayEvents.length > 0\n ? `${dayLabel}, ${dayEvents.length} event${dayEvents.length === 1 ? \"\" : \"s\"}`\n : dayLabel\n\n const DayTag: React.ElementType = onSelectDate ? \"button\" : \"div\"\n\n return (\n <div\n key={dayKey(cell)}\n className={styles.cell}\n data-slot=\"month-calendar-day\"\n data-outside={outside ? \"true\" : undefined}\n data-today={isToday ? \"true\" : undefined}\n data-selected={isSelected ? \"true\" : undefined}\n >\n <DayTag\n className={styles.dayHead}\n {...(onSelectDate\n ? {\n type: \"button\" as const,\n onClick: () => onSelectDate(cell),\n \"aria-label\": cellLabel,\n \"aria-pressed\": isSelected,\n }\n : {})}\n data-slot=\"month-calendar-day-head\"\n >\n <span className={styles.dayNumber}>{cell.getDate()}</span>\n </DayTag>\n\n {dayEvents.length > 0 ? (\n <div\n className={styles.events}\n data-slot=\"month-calendar-day-events\"\n >\n {visible.map((event) => {\n const ChipTag: React.ElementType = onEventSelect\n ? \"button\"\n : \"div\"\n return (\n <ChipTag\n key={event.id}\n className={styles.chip}\n data-status={event.status ?? \"default\"}\n data-series={event.series}\n data-slot=\"month-calendar-event\"\n {...(onEventSelect\n ? {\n type: \"button\" as const,\n onClick: () => onEventSelect(event),\n }\n : {})}\n >\n <span\n className={styles.dot}\n data-status={event.status ?? \"default\"}\n aria-hidden=\"true\"\n />\n <span className={styles.chipLabel}>{event.title}</span>\n </ChipTag>\n )\n })}\n {overflow > 0 ? (\n <div className={styles.overflow}>+{overflow} more</div>\n ) : null}\n </div>\n ) : null}\n </div>\n )\n })}\n </div>\n </div>\n )\n }\n)\n\nMonthCalendar.displayName = \"MonthCalendar\"\n\nexport { MonthCalendar }\n"
|
|
4204
|
+
},
|
|
4205
|
+
{
|
|
4206
|
+
"path": "blocks/month-calendar/month-calendar.module.css",
|
|
4207
|
+
"type": "registry:block",
|
|
4208
|
+
"content": "/* Month Calendar\n * A theme-portable month event-grid (scheduler) block: a 6×7 day-cell grid\n * under a weekday header, with month navigation, a view-mode segmented control,\n * and per-cell event chips carrying a status dot and an optional series tint.\n *\n * Every color, size, spacing, stroke, radius, and motion value binds to a Visor\n * token so the block adopts the active theme without modification. Selectors are\n * anchored on a local class to satisfy the docs bundler's pure-selector rule.\n */\n\n.root {\n display: flex;\n flex-direction: column;\n background: var(--surface-card);\n border: var(--stroke-width-thin) solid var(--border-muted);\n border-radius: var(--radius-lg);\n overflow: hidden;\n color: var(--text-primary);\n}\n\n/* ── Header: month nav + view segmented control ── */\n.header {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n justify-content: space-between;\n gap: var(--spacing-3, 0.75rem);\n padding: var(--spacing-4, 1rem);\n}\n\n.nav {\n display: flex;\n align-items: center;\n gap: var(--spacing-2, 0.5rem);\n}\n\n.navButton {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n /* 32px — icon-button sizing; sizing tokens deferred (Rule 5 exception). */\n width: 2rem;\n height: 2rem;\n padding: 0;\n color: var(--text-secondary);\n background: transparent;\n border: var(--stroke-width-thin) solid transparent;\n border-radius: var(--radius-md);\n cursor: pointer;\n}\n\n.navButton:hover:not(:disabled) {\n background: var(--surface-interactive-hover);\n color: var(--text-primary);\n}\n\n.navButton:disabled {\n opacity: var(--opacity-40);\n cursor: default;\n}\n\n.navButton:focus-visible {\n outline: var(--focus-ring-width) solid var(--border-focus);\n outline-offset: var(--focus-ring-offset);\n}\n\n.monthLabel {\n margin: 0;\n min-width: 10ch;\n text-align: center;\n font-size: var(--font-size-lg, 1.125rem);\n font-weight: var(--font-weight-semibold, 600);\n color: var(--text-primary);\n}\n\n/* ── View-mode segmented control ── */\n.segmented {\n display: inline-flex;\n align-items: center;\n gap: var(--stroke-width-thin);\n padding: var(--spacing-1, 0.25rem);\n background: var(--surface-subtle);\n border: var(--stroke-width-thin) solid var(--border-muted);\n border-radius: var(--radius-md);\n}\n\n.segment {\n appearance: none;\n padding: var(--spacing-1, 0.25rem) var(--spacing-3, 0.75rem);\n font-size: var(--font-size-sm, 0.875rem);\n font-weight: var(--font-weight-medium, 500);\n color: var(--text-secondary);\n background: transparent;\n border: none;\n border-radius: var(--radius-sm);\n cursor: pointer;\n transition: color var(--motion-duration-fast, 100ms)\n var(--motion-easing-default, ease-in-out),\n background-color var(--motion-duration-fast, 100ms)\n var(--motion-easing-default, ease-in-out);\n}\n\n.segment:hover:not([data-active]) {\n color: var(--text-primary);\n}\n\n.segment[data-active] {\n color: var(--text-primary);\n background: var(--surface-card);\n box-shadow: var(--shadow-xs);\n}\n\n.segment:focus-visible {\n outline: var(--focus-ring-width) solid var(--border-focus);\n outline-offset: var(--focus-ring-offset);\n}\n\n/* ── Weekday header row ── */\n.weekdays {\n display: grid;\n grid-template-columns: repeat(7, minmax(0, 1fr));\n border-top: var(--stroke-width-thin) solid var(--border-muted);\n background: var(--surface-subtle);\n}\n\n.weekday {\n padding: var(--spacing-2, 0.5rem);\n text-align: center;\n font-size: var(--font-size-xs, 0.75rem);\n font-weight: var(--font-weight-medium, 500);\n letter-spacing: 0.04em;\n text-transform: uppercase;\n color: var(--text-tertiary);\n}\n\n/* ── 6×7 day grid ── */\n/* The hairline background shows through the 1px gaps to draw the grid lines. */\n.grid {\n display: grid;\n grid-template-columns: repeat(7, minmax(0, 1fr));\n grid-auto-rows: minmax(0, 1fr);\n gap: var(--stroke-width-thin);\n background: var(--border-muted);\n border-top: var(--stroke-width-thin) solid var(--border-muted);\n}\n\n.cell {\n display: flex;\n flex-direction: column;\n gap: var(--spacing-1, 0.25rem);\n /* 104px — day-cell min height; sizing tokens deferred (Rule 5 exception). */\n min-height: 6.5rem;\n padding: var(--spacing-1, 0.25rem);\n background: var(--surface-card);\n}\n\n.cell[data-outside] {\n background: var(--surface-subtle);\n}\n\n.cell[data-outside] .dayNumber {\n color: var(--text-disabled);\n}\n\n.cell[data-selected] {\n background: var(--interactive-primary-soft);\n}\n\n/* ── Day number head (button when the day is selectable) ── */\n.dayHead {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n align-self: flex-start;\n /* 24px — day-number badge sizing; sizing tokens deferred (Rule 5 exception). */\n min-width: 1.5rem;\n height: 1.5rem;\n padding: 0 var(--spacing-1, 0.25rem);\n font: inherit;\n color: inherit;\n background: transparent;\n border: none;\n border-radius: var(--radius-full);\n cursor: default;\n}\n\n.dayHead:is(button) {\n cursor: pointer;\n}\n\n.dayHead:is(button):hover {\n background: var(--surface-interactive-hover);\n}\n\n.dayHead:focus-visible {\n outline: var(--focus-ring-width) solid var(--border-focus);\n outline-offset: var(--focus-ring-offset);\n}\n\n.dayNumber {\n font-size: var(--font-size-sm, 0.875rem);\n font-weight: var(--font-weight-medium, 500);\n color: var(--text-secondary);\n font-variant-numeric: tabular-nums;\n}\n\n.cell[data-today] .dayHead {\n background: var(--interactive-primary-bg);\n}\n\n.cell[data-today] .dayNumber {\n color: var(--interactive-primary-text);\n font-weight: var(--font-weight-semibold, 600);\n}\n\n/* ── Event chips ── */\n.events {\n display: flex;\n flex-direction: column;\n gap: var(--spacing-1, 0.25rem);\n min-width: 0;\n}\n\n.chip {\n display: flex;\n align-items: center;\n gap: var(--spacing-1, 0.25rem);\n width: 100%;\n padding: var(--spacing-1, 0.25rem) var(--spacing-2, 0.5rem);\n font-size: var(--font-size-xs, 0.75rem);\n font-weight: var(--font-weight-medium, 500);\n text-align: start;\n color: var(--text-primary);\n background: var(--surface-subtle);\n border: none;\n border-radius: var(--radius-sm);\n cursor: default;\n}\n\n.chip:is(button) {\n cursor: pointer;\n}\n\n.chip:is(button):hover {\n background: var(--surface-interactive-hover);\n}\n\n.chip:focus-visible {\n outline: var(--focus-ring-width) solid var(--border-focus);\n outline-offset: var(--focus-ring-offset);\n}\n\n/* Series tint — background wash + leading accent bar keyed to the chart ramp. */\n.chip[data-series=\"1\"] {\n --mc-series: var(--chart-1);\n}\n.chip[data-series=\"2\"] {\n --mc-series: var(--chart-2);\n}\n.chip[data-series=\"3\"] {\n --mc-series: var(--chart-3);\n}\n.chip[data-series=\"4\"] {\n --mc-series: var(--chart-4);\n}\n.chip[data-series=\"5\"] {\n --mc-series: var(--chart-5);\n}\n\n.chip[data-series] {\n background: color-mix(in srgb, var(--mc-series) 16%, transparent);\n box-shadow: inset var(--stroke-width-medium) 0 0 0 var(--mc-series);\n}\n\n.chip[data-series]:is(button):hover {\n background: color-mix(in srgb, var(--mc-series) 28%, transparent);\n}\n\n.chipLabel {\n flex: 1;\n min-width: 0;\n overflow: hidden;\n white-space: nowrap;\n text-overflow: ellipsis;\n}\n\n/* ── Status dot ── */\n.dot {\n flex-shrink: 0;\n /* 8px dot — bound to the spacing scale. */\n width: var(--spacing-2, 0.5rem);\n height: var(--spacing-2, 0.5rem);\n border-radius: var(--radius-full);\n background: var(--text-tertiary);\n}\n\n.dot[data-status=\"success\"] {\n background: var(--surface-success-default);\n}\n.dot[data-status=\"warning\"] {\n background: var(--surface-warning-default);\n}\n.dot[data-status=\"danger\"] {\n background: var(--surface-error-default);\n}\n.dot[data-status=\"info\"] {\n background: var(--surface-info-default);\n}\n\n/* ── Overflow row ── */\n.overflow {\n padding: 0 var(--spacing-2, 0.5rem);\n font-size: var(--font-size-xs, 0.75rem);\n font-weight: var(--font-weight-medium, 500);\n color: var(--text-tertiary);\n}\n\n@media (prefers-reduced-motion: reduce) {\n .segment {\n transition: none;\n }\n}\n"
|
|
4209
|
+
}
|
|
4210
|
+
]
|
|
4211
|
+
},
|
|
4106
4212
|
{
|
|
4107
4213
|
"name": "workspace-switcher",
|
|
4108
4214
|
"type": "registry:block",
|
|
@@ -4982,7 +5088,7 @@
|
|
|
4982
5088
|
{
|
|
4983
5089
|
"path": "components/devtools/source-inspector/visor-component-names.generated.ts",
|
|
4984
5090
|
"type": "registry:devtool",
|
|
4985
|
-
"content": "// THIS FILE IS GENERATED BY scripts/generate-visor-component-names.ts.\n// Do not edit by hand. Re-run `npm run generate:component-names` after\n// adding, removing, or renaming a Visor component.\n//\n// Source of truth: registry/registry-{ui,blocks,deck,visual,devtools}.ts\n// Used by: components/devtools/source-inspector/* (VI-311)\n\nexport const VISOR_COMPONENT_NAMES: ReadonlySet<string> = new Set([\n \"AccessibilitySection\",\n \"AccessibilitySlide\",\n \"AccessibilitySpecimen\",\n \"Accordion\",\n \"AccordionContent\",\n \"AccordionItem\",\n \"AccordionTrigger\",\n \"ActivityFeed\",\n \"ActivityFeedContext\",\n \"ActivityFeedItem\",\n \"ActivityFeedRoot\",\n \"AdminDashboard\",\n \"AdminDetailDrawer\",\n \"AdminListPage\",\n \"AdminListPageInner\",\n \"AdminSettingsPage\",\n \"AdminShell\",\n \"AdminTabbedEditor\",\n \"AdminWizard\",\n \"Alert\",\n \"AlertActions\",\n \"AlertDescription\",\n \"AlertTitle\",\n \"AmbientGlow\",\n \"Avatar\",\n \"AvatarFallback\",\n \"AvatarImage\",\n \"AvatarStack\",\n \"Badge\",\n \"Banner\",\n \"BannerAction\",\n \"BannerDescription\",\n \"BannerTitle\",\n \"BentoGrid\",\n \"BentoTile\",\n \"BentoTileBody\",\n \"BentoTileDescription\",\n \"BentoTileFigure\",\n \"BentoTileHeadline\",\n \"BentoTileMedia\",\n \"BentoTileMeta\",\n \"BentoTileTitle\",\n \"Box\",\n \"Breadcrumb\",\n \"BreadcrumbEllipsis\",\n \"BreadcrumbItem\",\n \"BreadcrumbLink\",\n \"BreadcrumbList\",\n \"BreadcrumbPage\",\n \"BreadcrumbSeparator\",\n \"BrowserFrame\",\n \"BulkActionBar\",\n \"Button\",\n \"ButtonSpecimenSection\",\n \"ButtonSpecimenSlide\",\n \"Calendar\",\n \"Card\",\n \"CardContent\",\n \"CardDescription\",\n \"CardFooter\",\n \"CardGrid\",\n \"CardHeader\",\n \"CardLift\",\n \"CardTitle\",\n \"Carousel\",\n \"CarouselContent\",\n \"CarouselContext\",\n \"CarouselGallery\",\n \"CarouselItem\",\n \"CarouselNext\",\n \"CarouselPrevious\",\n \"ChallengeCard\",\n \"ChallengeCardAction\",\n \"ChallengeCardActions\",\n \"ChallengeCardBody\",\n \"ChallengeCardGate\",\n \"ChallengeCardHeader\",\n \"ChartContainer\",\n \"ChartContext\",\n \"ChartLegend\",\n \"ChartLegendContent\",\n \"ChartStyle\",\n \"ChartTooltip\",\n \"ChartTooltipContent\",\n \"Checkbox\",\n \"CheckGroup\",\n \"CheckRow\",\n \"Chip\",\n \"ChipGroup\",\n \"ChipGroupContext\",\n \"ChipGroupItem\",\n \"ChoiceChip\",\n \"ChromeButton\",\n \"ClosingSlide\",\n \"CodeBlock\",\n \"Collapsible\",\n \"CollapsibleContent\",\n \"CollapsibleTrigger\",\n \"ColorBar\",\n \"ColorPaletteSection\",\n \"ColorPicker\",\n \"ColorPickerSurface\",\n \"ColorSwatch\",\n \"ColorSwatchGrid\",\n \"Combobox\",\n \"ComboboxContent\",\n \"ComboboxContext\",\n \"ComboboxEmpty\",\n \"ComboboxGroup\",\n \"ComboboxInput\",\n \"ComboboxItem\",\n \"ComboboxSeparator\",\n \"Command\",\n \"CommandDialog\",\n \"CommandEmpty\",\n \"CommandGroup\",\n \"CommandInput\",\n \"CommandItem\",\n \"CommandList\",\n \"CommandLoading\",\n \"CommandSeparator\",\n \"CommandShortcut\",\n \"ComponentShowcaseContent\",\n \"ComponentShowcaseSection\",\n \"ComponentShowcaseSlide\",\n \"Composer\",\n \"ComposerContext\",\n \"ComposerField\",\n \"ComposerSend\",\n \"ComposerSpacer\",\n \"ComposerToolbar\",\n \"ComposerToolButton\",\n \"ConceptSlide\",\n \"ConfigurationPanel\",\n \"ConfirmDialog\",\n \"ConflictBanner\",\n \"Container\",\n \"ContextMenu\",\n \"ContextMenuCheckboxItem\",\n \"ContextMenuContent\",\n \"ContextMenuGroup\",\n \"ContextMenuItem\",\n \"ContextMenuLabel\",\n \"ContextMenuPortal\",\n \"ContextMenuRadioGroup\",\n \"ContextMenuRadioItem\",\n \"ContextMenuSeparator\",\n \"ContextMenuShortcut\",\n \"ContextMenuSub\",\n \"ContextMenuSubContent\",\n \"ContextMenuSubTrigger\",\n \"ContextMenuTrigger\",\n \"Controls\",\n \"CtaSection\",\n \"DataTable\",\n \"DataTableInner\",\n \"DatePicker\",\n \"DateRangePicker\",\n \"DeckContext\",\n \"DeckFooter\",\n \"DeckLayout\",\n \"DeckProvider\",\n \"DeckRenderer\",\n \"DesignSystemDeck\",\n \"DesignSystemSpecimen\",\n \"Dialog\",\n \"DialogClose\",\n \"DialogContent\",\n \"DialogDescription\",\n \"DialogFooter\",\n \"DialogHeader\",\n \"DialogOverlay\",\n \"DialogPortal\",\n \"DialogTitle\",\n \"DialogTrigger\",\n \"DotNav\",\n \"DropdownMenu\",\n \"DropdownMenuCheckboxItem\",\n \"DropdownMenuContent\",\n \"DropdownMenuGroup\",\n \"DropdownMenuItem\",\n \"DropdownMenuLabel\",\n \"DropdownMenuPortal\",\n \"DropdownMenuRadioGroup\",\n \"DropdownMenuRadioItem\",\n \"DropdownMenuSeparator\",\n \"DropdownMenuShortcut\",\n \"DropdownMenuSub\",\n \"DropdownMenuSubContent\",\n \"DropdownMenuSubTrigger\",\n \"DropdownMenuTrigger\",\n \"EditableBlock\",\n \"ElevationCard\",\n \"ElevationSlide\",\n \"EmptyState\",\n \"ErrorPlacard\",\n \"ExportMenu\",\n \"FeaturesGrid\",\n \"Field\",\n \"FieldDescription\",\n \"FieldError\",\n \"FieldLabel\",\n \"Fieldset\",\n \"FieldsetLegend\",\n \"FileUpload\",\n \"FilterBar\",\n \"FilterChip\",\n \"FontShowcase\",\n \"FontShowcaseGrid\",\n \"Footer\",\n \"FooterSection\",\n \"Form\",\n \"FormError\",\n \"FormErrorDescription\",\n \"FormErrorTitle\",\n \"FormField\",\n \"FormSpecimenSection\",\n \"FormSpecimenSlide\",\n \"FullscreenOverlay\",\n \"FullscreenOverlayContent\",\n \"FullscreenOverlayTrigger\",\n \"GrainOverlay\",\n \"Grid\",\n \"Header\",\n \"Heading\",\n \"HeroGlow\",\n \"HeroSection\",\n \"HeroSlide\",\n \"HoverCard\",\n \"HoverCardContent\",\n \"HoverCardTrigger\",\n \"IconGrid\",\n \"IconGridSection\",\n \"IconSizeRow\",\n \"IconsSlide\",\n \"Image\",\n \"InfographicBar\",\n \"Inline\",\n \"Input\",\n \"Kbd\",\n \"KeyValueList\",\n \"Label\",\n \"Landing\",\n \"Lightbox\",\n \"LightboxContent\",\n \"LightboxContext\",\n \"LightboxTrigger\",\n \"LoginForm\",\n \"Marquee\",\n \"MarqueeBandRenderer\",\n \"MatrixCell\",\n \"MatrixTable\",\n \"MatrixTableInner\",\n \"Menubar\",\n \"MenubarCheckboxItem\",\n \"MenubarContent\",\n \"MenubarGroup\",\n \"MenubarItem\",\n \"MenubarLabel\",\n \"MenubarMenu\",\n \"MenubarRadioGroup\",\n \"MenubarRadioItem\",\n \"MenubarSeparator\",\n \"MenubarShortcut\",\n \"MenubarSub\",\n \"MenubarSubContent\",\n \"MenubarSubTrigger\",\n \"MenubarTrigger\",\n \"MotionDuration\",\n \"MotionDurationSection\",\n \"MotionEasing\",\n \"MotionEasingSection\",\n \"MotionSlide\",\n \"NameRoster\",\n \"NameRosterItem\",\n \"Navbar\",\n \"NavbarBrand\",\n \"NavbarContent\",\n \"NavbarItem\",\n \"NavbarLink\",\n \"NavbarToggle\",\n \"NumberInput\",\n \"OfflineBanner\",\n \"OpacityBar\",\n \"OpacitySlide\",\n \"OTPInput\",\n \"PageHeader\",\n \"Pagination\",\n \"PaginationContent\",\n \"PaginationEllipsis\",\n \"PaginationItem\",\n \"PaginationLink\",\n \"PaginationNext\",\n \"PaginationPrevious\",\n \"PasswordInput\",\n \"PhoneInput\",\n \"Popover\",\n \"PopoverAnchor\",\n \"PopoverContent\",\n \"PopoverFooter\",\n \"PopoverSelectionItem\",\n \"PopoverSelectionLabel\",\n \"PopoverSelectionList\",\n \"PopoverTrigger\",\n \"PricingSection\",\n \"ProfileMenu\",\n \"Progress\",\n \"PrototypeReview\",\n \"QuickActions\",\n \"RadioGroup\",\n \"RadioGroupItem\",\n \"RadiusScale\",\n \"RadiusSection\",\n \"RadiusSlide\",\n \"RightRailList\",\n \"ScoreIndicator\",\n \"ScrollArea\",\n \"ScrollBar\",\n \"SearchInput\",\n \"SectionHeader\",\n \"SectionIntro\",\n \"SectionNav\",\n \"SectionNavItem\",\n \"SegmentedProgress\",\n \"Select\",\n \"SelectContent\",\n \"SelectGroup\",\n \"SelectionListContext\",\n \"SelectItem\",\n \"SelectLabel\",\n \"SelectScrollDownButton\",\n \"SelectScrollUpButton\",\n \"SelectSeparator\",\n \"SelectTrigger\",\n \"SelectValue\",\n \"SemanticColorGrid\",\n \"SemanticColorItem\",\n \"SemanticTokensSlide\",\n \"Separator\",\n \"SessionTimeout\",\n \"ShadowSection\",\n \"Sheet\",\n \"SheetClose\",\n \"SheetContent\",\n \"SheetDescription\",\n \"SheetFooter\",\n \"SheetHeader\",\n \"SheetOverlay\",\n \"SheetPortal\",\n \"SheetTitle\",\n \"SheetTrigger\",\n \"Sidebar\",\n \"SidebarContent\",\n \"SidebarContext\",\n \"SidebarFooter\",\n \"SidebarGroup\",\n \"SidebarGroupAction\",\n \"SidebarGroupContent\",\n \"SidebarGroupLabel\",\n \"SidebarHeader\",\n \"SidebarInset\",\n \"SidebarMenu\",\n \"SidebarMenuAction\",\n \"SidebarMenuBadge\",\n \"SidebarMenuButton\",\n \"SidebarMenuItem\",\n \"SidebarMenuSub\",\n \"SidebarMenuSubButton\",\n \"SidebarMenuSubItem\",\n \"SidebarProvider\",\n \"SidebarRail\",\n \"SidebarSeparator\",\n \"SidebarTrigger\",\n \"Skeleton\",\n \"SkeletonDetail\",\n \"SkeletonList\",\n \"SkeletonTable\",\n \"Slide\",\n \"SlideHeader\",\n \"Slider\",\n \"SliderControl\",\n \"SlideThemeContext\",\n \"SlideThemeProvider\",\n \"SlowNetworkBar\",\n \"SourceInspector\",\n \"SourceInspectorContext\",\n \"SourceInspectorDevImpl\",\n \"SourceInspectorProvider\",\n \"SourceInspectorRunner\",\n \"SourceInspectorToggle\",\n \"SpacingScale\",\n \"SpacingSection\",\n \"SpacingSlide\",\n \"Sparkline\",\n \"SpecimenCard\",\n \"SpecimenCardFooter\",\n \"Sphere\",\n \"SpherePlayground\",\n \"Spinner\",\n \"Stack\",\n \"Stage\",\n \"StatCard\",\n \"StatHero\",\n \"StationSpectrum\",\n \"StatusBadge\",\n \"StatusColorsSlide\",\n \"StatusDot\",\n \"Stepper\",\n \"StepperContext\",\n \"StepperDescription\",\n \"StepperItem\",\n \"StepperSeparator\",\n \"StepperTitle\",\n \"StepperTrigger\",\n \"StepsSection\",\n \"StructuredPrompt\",\n \"StructuredPromptBody\",\n \"StructuredPromptHeader\",\n \"StructuredPromptHint\",\n \"StructuredPromptSlot\",\n \"SuccessLiveRegion\",\n \"SurfaceRow\",\n \"SurfaceScaleStack\",\n \"SurfaceSection\",\n \"Switch\",\n \"Table\",\n \"TableBody\",\n \"TableCaption\",\n \"TableCell\",\n \"TableFooter\",\n \"TableHead\",\n \"TableHeader\",\n \"TableRow\",\n \"Tabs\",\n \"TabsContent\",\n \"TabsList\",\n \"TabsTrigger\",\n \"TagInput\",\n \"TestimonialAttribution\",\n \"TestimonialSection\",\n \"Text\",\n \"Textarea\",\n \"ThemeArchitectureSlide\",\n \"ThemeColorsSlide\",\n \"ThemeSwitcher\",\n \"Timeline\",\n \"TimelineContent\",\n \"TimelineDescription\",\n \"TimelineIcon\",\n \"TimelineItem\",\n \"TimelineTimestamp\",\n \"TimelineTitle\",\n \"TitleSlide\",\n \"ToastCard\",\n \"ToastCardStack\",\n \"Toaster\",\n \"TOCSlide\",\n \"ToggleButton\",\n \"ToggleDevImpl\",\n \"ToggleGroup\",\n \"ToggleGroupContext\",\n \"ToggleGroupItem\",\n \"Tooltip\",\n \"TooltipContent\",\n \"TooltipProvider\",\n \"TooltipTrigger\",\n \"TypeBodySlide\",\n \"TypeDisplaySlide\",\n \"TypeScaleStack\",\n \"TypeSpecimen\",\n \"TypographySection\",\n \"Vignette\",\n \"WorkspaceSwitcher\",\n])\n"
|
|
5091
|
+
"content": "// THIS FILE IS GENERATED BY scripts/generate-visor-component-names.ts.\n// Do not edit by hand. Re-run `npm run generate:component-names` after\n// adding, removing, or renaming a Visor component.\n//\n// Source of truth: registry/registry-{ui,blocks,deck,visual,devtools}.ts\n// Used by: components/devtools/source-inspector/* (VI-311)\n\nexport const VISOR_COMPONENT_NAMES: ReadonlySet<string> = new Set([\n \"AccessibilitySection\",\n \"AccessibilitySlide\",\n \"AccessibilitySpecimen\",\n \"Accordion\",\n \"AccordionContent\",\n \"AccordionItem\",\n \"AccordionTrigger\",\n \"ActivityFeed\",\n \"ActivityFeedContext\",\n \"ActivityFeedItem\",\n \"ActivityFeedRoot\",\n \"AdminDashboard\",\n \"AdminDetail\",\n \"AdminDetailDrawer\",\n \"AdminListPage\",\n \"AdminListPageInner\",\n \"AdminSettingsPage\",\n \"AdminShell\",\n \"AdminTabbedEditor\",\n \"AdminWizard\",\n \"Alert\",\n \"AlertActions\",\n \"AlertDescription\",\n \"AlertTitle\",\n \"AmbientGlow\",\n \"Avatar\",\n \"AvatarFallback\",\n \"AvatarImage\",\n \"AvatarStack\",\n \"Badge\",\n \"Banner\",\n \"BannerAction\",\n \"BannerDescription\",\n \"BannerTitle\",\n \"BentoGrid\",\n \"BentoTile\",\n \"BentoTileBody\",\n \"BentoTileDescription\",\n \"BentoTileFigure\",\n \"BentoTileHeadline\",\n \"BentoTileMedia\",\n \"BentoTileMeta\",\n \"BentoTileTitle\",\n \"Box\",\n \"Breadcrumb\",\n \"BreadcrumbEllipsis\",\n \"BreadcrumbItem\",\n \"BreadcrumbLink\",\n \"BreadcrumbList\",\n \"BreadcrumbPage\",\n \"BreadcrumbSeparator\",\n \"BrowserFrame\",\n \"BulkActionBar\",\n \"Button\",\n \"ButtonSpecimenSection\",\n \"ButtonSpecimenSlide\",\n \"Calendar\",\n \"Card\",\n \"CardContent\",\n \"CardDescription\",\n \"CardFooter\",\n \"CardGrid\",\n \"CardHeader\",\n \"CardLift\",\n \"CardTitle\",\n \"Carousel\",\n \"CarouselContent\",\n \"CarouselContext\",\n \"CarouselGallery\",\n \"CarouselItem\",\n \"CarouselNext\",\n \"CarouselPrevious\",\n \"ChallengeCard\",\n \"ChallengeCardAction\",\n \"ChallengeCardActions\",\n \"ChallengeCardBody\",\n \"ChallengeCardGate\",\n \"ChallengeCardHeader\",\n \"ChartContainer\",\n \"ChartContext\",\n \"ChartLegend\",\n \"ChartLegendContent\",\n \"ChartStyle\",\n \"ChartTooltip\",\n \"ChartTooltipContent\",\n \"Checkbox\",\n \"CheckGroup\",\n \"CheckRow\",\n \"Chip\",\n \"ChipGroup\",\n \"ChipGroupContext\",\n \"ChipGroupItem\",\n \"ChoiceChip\",\n \"ChromeButton\",\n \"ClosingSlide\",\n \"CodeBlock\",\n \"Collapsible\",\n \"CollapsibleContent\",\n \"CollapsibleTrigger\",\n \"ColorBar\",\n \"ColorPaletteSection\",\n \"ColorPicker\",\n \"ColorPickerSurface\",\n \"ColorSwatch\",\n \"ColorSwatchGrid\",\n \"Combobox\",\n \"ComboboxContent\",\n \"ComboboxContext\",\n \"ComboboxEmpty\",\n \"ComboboxGroup\",\n \"ComboboxInput\",\n \"ComboboxItem\",\n \"ComboboxSeparator\",\n \"Command\",\n \"CommandDialog\",\n \"CommandEmpty\",\n \"CommandGroup\",\n \"CommandInput\",\n \"CommandItem\",\n \"CommandList\",\n \"CommandLoading\",\n \"CommandSeparator\",\n \"CommandShortcut\",\n \"ComponentShowcaseContent\",\n \"ComponentShowcaseSection\",\n \"ComponentShowcaseSlide\",\n \"Composer\",\n \"ComposerContext\",\n \"ComposerField\",\n \"ComposerSend\",\n \"ComposerSpacer\",\n \"ComposerToolbar\",\n \"ComposerToolButton\",\n \"ConceptSlide\",\n \"ConfigurationPanel\",\n \"ConfirmDialog\",\n \"ConflictBanner\",\n \"Container\",\n \"ContextMenu\",\n \"ContextMenuCheckboxItem\",\n \"ContextMenuContent\",\n \"ContextMenuGroup\",\n \"ContextMenuItem\",\n \"ContextMenuLabel\",\n \"ContextMenuPortal\",\n \"ContextMenuRadioGroup\",\n \"ContextMenuRadioItem\",\n \"ContextMenuSeparator\",\n \"ContextMenuShortcut\",\n \"ContextMenuSub\",\n \"ContextMenuSubContent\",\n \"ContextMenuSubTrigger\",\n \"ContextMenuTrigger\",\n \"Controls\",\n \"CtaSection\",\n \"DataTable\",\n \"DataTableInner\",\n \"DatePicker\",\n \"DateRangePicker\",\n \"DeckContext\",\n \"DeckFooter\",\n \"DeckLayout\",\n \"DeckProvider\",\n \"DeckRenderer\",\n \"DesignSystemDeck\",\n \"DesignSystemSpecimen\",\n \"Dialog\",\n \"DialogClose\",\n \"DialogContent\",\n \"DialogDescription\",\n \"DialogFooter\",\n \"DialogHeader\",\n \"DialogOverlay\",\n \"DialogPortal\",\n \"DialogTitle\",\n \"DialogTrigger\",\n \"DocFrame\",\n \"DocFrameBrand\",\n \"DocNav\",\n \"DocNavGroup\",\n \"DocNavPill\",\n \"DotNav\",\n \"DropdownMenu\",\n \"DropdownMenuCheckboxItem\",\n \"DropdownMenuContent\",\n \"DropdownMenuGroup\",\n \"DropdownMenuItem\",\n \"DropdownMenuLabel\",\n \"DropdownMenuPortal\",\n \"DropdownMenuRadioGroup\",\n \"DropdownMenuRadioItem\",\n \"DropdownMenuSeparator\",\n \"DropdownMenuShortcut\",\n \"DropdownMenuSub\",\n \"DropdownMenuSubContent\",\n \"DropdownMenuSubTrigger\",\n \"DropdownMenuTrigger\",\n \"EditableBlock\",\n \"ElevationCard\",\n \"ElevationSlide\",\n \"EmptyState\",\n \"ErrorPlacard\",\n \"ExportMenu\",\n \"FeaturesGrid\",\n \"Field\",\n \"FieldDescription\",\n \"FieldError\",\n \"FieldLabel\",\n \"Fieldset\",\n \"FieldsetLegend\",\n \"FileUpload\",\n \"FilterBar\",\n \"FilterChip\",\n \"FontShowcase\",\n \"FontShowcaseGrid\",\n \"Footer\",\n \"FooterSection\",\n \"Form\",\n \"FormError\",\n \"FormErrorDescription\",\n \"FormErrorTitle\",\n \"FormField\",\n \"FormSpecimenSection\",\n \"FormSpecimenSlide\",\n \"FullscreenOverlay\",\n \"FullscreenOverlayContent\",\n \"FullscreenOverlayTrigger\",\n \"GrainOverlay\",\n \"Grid\",\n \"Header\",\n \"Heading\",\n \"HeroGlow\",\n \"HeroSection\",\n \"HeroSlide\",\n \"HoverCard\",\n \"HoverCardContent\",\n \"HoverCardTrigger\",\n \"IconGrid\",\n \"IconGridSection\",\n \"IconSizeRow\",\n \"IconsSlide\",\n \"Image\",\n \"InfographicBar\",\n \"Inline\",\n \"Input\",\n \"Kbd\",\n \"KeyValueList\",\n \"Label\",\n \"Landing\",\n \"Lightbox\",\n \"LightboxContent\",\n \"LightboxContext\",\n \"LightboxTrigger\",\n \"LoginForm\",\n \"Marquee\",\n \"MarqueeBandRenderer\",\n \"MatrixCell\",\n \"MatrixTable\",\n \"MatrixTableInner\",\n \"Menubar\",\n \"MenubarCheckboxItem\",\n \"MenubarContent\",\n \"MenubarGroup\",\n \"MenubarItem\",\n \"MenubarLabel\",\n \"MenubarMenu\",\n \"MenubarRadioGroup\",\n \"MenubarRadioItem\",\n \"MenubarSeparator\",\n \"MenubarShortcut\",\n \"MenubarSub\",\n \"MenubarSubContent\",\n \"MenubarSubTrigger\",\n \"MenubarTrigger\",\n \"MonthCalendar\",\n \"MotionDuration\",\n \"MotionDurationSection\",\n \"MotionEasing\",\n \"MotionEasingSection\",\n \"MotionSlide\",\n \"NameRoster\",\n \"NameRosterItem\",\n \"Navbar\",\n \"NavbarBrand\",\n \"NavbarContent\",\n \"NavbarItem\",\n \"NavbarLink\",\n \"NavbarToggle\",\n \"NumberInput\",\n \"OfflineBanner\",\n \"OpacityBar\",\n \"OpacitySlide\",\n \"OTPInput\",\n \"PageHeader\",\n \"Pagination\",\n \"PaginationContent\",\n \"PaginationEllipsis\",\n \"PaginationItem\",\n \"PaginationLink\",\n \"PaginationNext\",\n \"PaginationPrevious\",\n \"PasswordInput\",\n \"PhoneInput\",\n \"Popover\",\n \"PopoverAnchor\",\n \"PopoverContent\",\n \"PopoverFooter\",\n \"PopoverSelectionItem\",\n \"PopoverSelectionLabel\",\n \"PopoverSelectionList\",\n \"PopoverTrigger\",\n \"PricingSection\",\n \"ProfileMenu\",\n \"Progress\",\n \"PrototypeReview\",\n \"QuickActions\",\n \"RadioGroup\",\n \"RadioGroupItem\",\n \"RadiusScale\",\n \"RadiusSection\",\n \"RadiusSlide\",\n \"RecordSection\",\n \"RightRailList\",\n \"ScoreIndicator\",\n \"ScrollArea\",\n \"ScrollBar\",\n \"SearchInput\",\n \"SectionHeader\",\n \"SectionIntro\",\n \"SectionNav\",\n \"SectionNavItem\",\n \"SegmentedProgress\",\n \"Select\",\n \"SelectContent\",\n \"SelectGroup\",\n \"SelectionListContext\",\n \"SelectItem\",\n \"SelectLabel\",\n \"SelectScrollDownButton\",\n \"SelectScrollUpButton\",\n \"SelectSeparator\",\n \"SelectTrigger\",\n \"SelectValue\",\n \"SemanticColorGrid\",\n \"SemanticColorItem\",\n \"SemanticTokensSlide\",\n \"SensitivePanel\",\n \"Separator\",\n \"SessionTimeout\",\n \"ShadowSection\",\n \"Sheet\",\n \"SheetClose\",\n \"SheetContent\",\n \"SheetDescription\",\n \"SheetFooter\",\n \"SheetHeader\",\n \"SheetOverlay\",\n \"SheetPortal\",\n \"SheetTitle\",\n \"SheetTrigger\",\n \"Sidebar\",\n \"SidebarContent\",\n \"SidebarContext\",\n \"SidebarFooter\",\n \"SidebarGroup\",\n \"SidebarGroupAction\",\n \"SidebarGroupContent\",\n \"SidebarGroupLabel\",\n \"SidebarHeader\",\n \"SidebarInset\",\n \"SidebarMenu\",\n \"SidebarMenuAction\",\n \"SidebarMenuBadge\",\n \"SidebarMenuButton\",\n \"SidebarMenuItem\",\n \"SidebarMenuSub\",\n \"SidebarMenuSubButton\",\n \"SidebarMenuSubItem\",\n \"SidebarProvider\",\n \"SidebarRail\",\n \"SidebarSeparator\",\n \"SidebarTrigger\",\n \"Skeleton\",\n \"SkeletonDetail\",\n \"SkeletonList\",\n \"SkeletonTable\",\n \"Slide\",\n \"SlideHeader\",\n \"Slider\",\n \"SliderControl\",\n \"SlideThemeContext\",\n \"SlideThemeProvider\",\n \"SlowNetworkBar\",\n \"SourceInspector\",\n \"SourceInspectorContext\",\n \"SourceInspectorDevImpl\",\n \"SourceInspectorProvider\",\n \"SourceInspectorRunner\",\n \"SourceInspectorToggle\",\n \"SpacingScale\",\n \"SpacingSection\",\n \"SpacingSlide\",\n \"Sparkline\",\n \"SpecimenCard\",\n \"SpecimenCardFooter\",\n \"Sphere\",\n \"SpherePlayground\",\n \"Spinner\",\n \"Stack\",\n \"Stage\",\n \"StatCard\",\n \"StatHero\",\n \"StationSpectrum\",\n \"StatusBadge\",\n \"StatusColorsSlide\",\n \"StatusDot\",\n \"Stepper\",\n \"StepperContext\",\n \"StepperDescription\",\n \"StepperItem\",\n \"StepperSeparator\",\n \"StepperTitle\",\n \"StepperTrigger\",\n \"StepsSection\",\n \"StructuredPrompt\",\n \"StructuredPromptBody\",\n \"StructuredPromptHeader\",\n \"StructuredPromptHint\",\n \"StructuredPromptSlot\",\n \"SuccessLiveRegion\",\n \"SurfaceRow\",\n \"SurfaceScaleStack\",\n \"SurfaceSection\",\n \"Switch\",\n \"Table\",\n \"TableBody\",\n \"TableCaption\",\n \"TableCell\",\n \"TableFooter\",\n \"TableHead\",\n \"TableHeader\",\n \"TableRow\",\n \"Tabs\",\n \"TabsContent\",\n \"TabsList\",\n \"TabsTrigger\",\n \"TagInput\",\n \"TestimonialAttribution\",\n \"TestimonialSection\",\n \"Text\",\n \"Textarea\",\n \"ThemeArchitectureSlide\",\n \"ThemeColorsSlide\",\n \"ThemeSwitcher\",\n \"Timeline\",\n \"TimelineContent\",\n \"TimelineDescription\",\n \"TimelineIcon\",\n \"TimelineItem\",\n \"TimelineTimestamp\",\n \"TimelineTitle\",\n \"TitleSlide\",\n \"ToastCard\",\n \"ToastCardStack\",\n \"Toaster\",\n \"TOCSlide\",\n \"ToggleButton\",\n \"ToggleDevImpl\",\n \"ToggleGroup\",\n \"ToggleGroupContext\",\n \"ToggleGroupItem\",\n \"Tooltip\",\n \"TooltipContent\",\n \"TooltipProvider\",\n \"TooltipTrigger\",\n \"TypeBodySlide\",\n \"TypeDisplaySlide\",\n \"TypeScaleStack\",\n \"TypeSpecimen\",\n \"TypographySection\",\n \"Vignette\",\n \"WorkspaceSwitcher\",\n])\n"
|
|
4986
5092
|
}
|
|
4987
5093
|
]
|
|
4988
5094
|
},
|