@loworbitstudio/visor 0.10.0 → 1.0.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 +31 -1
- package/dist/index.js +32 -22
- package/dist/registry.json +196 -22
- package/dist/visor-manifest.json +385 -8
- package/package.json +2 -2
package/dist/registry.json
CHANGED
|
@@ -246,7 +246,7 @@
|
|
|
246
246
|
{
|
|
247
247
|
"path": "components/ui/checkbox/checkbox.module.css",
|
|
248
248
|
"type": "registry:ui",
|
|
249
|
-
"content": "/* Checkbox root styles */\n.root {\n position: relative;\n display: flex;\n width: 1rem;\n height: 1rem;\n flex-shrink: 0;\n align-items: center;\n justify-content: center;\n border-radius: var(--radius-sm, 0.25rem);\n border: 1px solid var(--border-default, #e5e7eb);\n background-color: transparent;\n transition: border-color var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out), background-color var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out), box-shadow var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out);\n outline: none;\n cursor: pointer;\n}\n\n@media (hover: hover) {\n .root:hover:not(:focus-visible):not(:disabled):not([aria-invalid=\"true\"]) {\n border-color: var(--border-strong, #d1d5db);\n }\n}\n\n.root:focus-visible {\n border-color: var(--border-focus, #111827);\n box-shadow: 0 0 0 var(--focus-ring-width, 2px) color-mix(in srgb, var(--border-focus, #111827) 15%, transparent);\n}\n\n.root:disabled {\n cursor: not-allowed;\n opacity: 0.5;\n}\n\n.root[aria-invalid=\"true\"] {\n border-color: var(--border-error, #ef4444);\n box-shadow: 0 0 0 var(--focus-ring-width, 2px) color-mix(in srgb, var(--border-error, #ef4444) 15%, transparent);\n}\n\n.root[data-state=\"checked\"] {\n border-color: var(--interactive-primary-bg, #111827);\n background-color: var(--interactive-primary-bg, #111827);\n color: var(--interactive-primary-text, #f9fafb);\n}\n\n/* Indicator */\n.indicator {\n display: grid;\n place-content: center;\n color: currentColor;\n transition: none;\n}\n\n.icon {\n width: 0.875rem;\n height: 0.875rem;\n}\n"
|
|
249
|
+
"content": "/* Checkbox root styles */\n.root {\n position: relative;\n display: flex;\n width: 1rem;\n height: 1rem;\n flex-shrink: 0;\n align-items: center;\n justify-content: center;\n border-radius: var(--radius-sm, 0.25rem);\n border: 1px solid var(--checkbox-border, var(--border-default, #e5e7eb));\n background-color: var(--checkbox-bg, transparent);\n transition: border-color var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out), background-color var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out), box-shadow var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out);\n outline: none;\n cursor: pointer;\n}\n\n@media (hover: hover) {\n .root:hover:not(:focus-visible):not(:disabled):not([aria-invalid=\"true\"]) {\n border-color: var(--checkbox-border-hover, var(--border-strong, #d1d5db));\n background-color: var(--checkbox-bg-hover, transparent);\n }\n}\n\n.root:focus-visible {\n border-color: var(--border-focus, #111827);\n box-shadow: 0 0 0 var(--focus-ring-width, 2px) color-mix(in srgb, var(--border-focus, #111827) 15%, transparent);\n}\n\n.root:disabled {\n cursor: not-allowed;\n opacity: 0.5;\n}\n\n.root[aria-invalid=\"true\"] {\n border-color: var(--border-error, #ef4444);\n box-shadow: 0 0 0 var(--focus-ring-width, 2px) color-mix(in srgb, var(--border-error, #ef4444) 15%, transparent);\n}\n\n.root[data-state=\"checked\"] {\n border-color: var(--checkbox-border-checked, var(--interactive-primary-bg, #111827));\n background-color: var(--checkbox-bg-checked, var(--interactive-primary-bg, #111827));\n color: var(--interactive-primary-text, #f9fafb);\n}\n\n.root[data-state=\"indeterminate\"] {\n border-color: var(--checkbox-border-checked, var(--interactive-primary-bg, #111827));\n background-color: var(--checkbox-bg-checked, var(--interactive-primary-bg, #111827));\n color: var(--interactive-primary-text, #f9fafb);\n}\n\n/* Indicator */\n.indicator {\n display: grid;\n place-content: center;\n color: currentColor;\n transition: none;\n}\n\n.icon {\n width: 0.875rem;\n height: 0.875rem;\n}\n"
|
|
250
250
|
}
|
|
251
251
|
]
|
|
252
252
|
},
|
|
@@ -398,7 +398,28 @@
|
|
|
398
398
|
{
|
|
399
399
|
"path": "components/ui/badge/badge.module.css",
|
|
400
400
|
"type": "registry:ui",
|
|
401
|
-
"content": "/* Badge base */\n.base {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n height: 1.25rem;\n width: fit-content;\n flex-shrink: 0;\n gap: var(--spacing-1, 0.25rem);\n overflow: hidden;\n border-radius: var(--radius-full, 9999px);\n border: 1px solid transparent;\n padding: calc(var(--spacing-1, 0.25rem) / 2) var(--spacing-2, 0.5rem);\n font-size: var(--font-size-xs, 0.75rem);\n font-weight: var(--font-weight-medium, 500);\n white-space: nowrap;\n transition: background-color var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out), color var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out), border-color var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out);\n}\n\n.base: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/* Variants */\n.variantDefault {\n background-color: var(--interactive-primary-bg,
|
|
401
|
+
"content": "/* Badge base */\n.base {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n height: 1.25rem;\n width: fit-content;\n flex-shrink: 0;\n gap: var(--spacing-1, 0.25rem);\n overflow: hidden;\n border-radius: var(--radius-full, 9999px);\n border: 1px solid transparent;\n padding: calc(var(--spacing-1, 0.25rem) / 2) var(--spacing-2, 0.5rem);\n font-size: var(--font-size-xs, 0.75rem);\n font-weight: var(--font-weight-medium, 500);\n white-space: nowrap;\n transition: background-color var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out), color var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out), border-color var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out);\n}\n\n.base: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/* Variants */\n.variantDefault {\n background-color: var(--interactive-primary-bg, currentColor);\n color: var(--interactive-primary-text, var(--text-inverse, #f9fafb));\n border-color: transparent;\n}\n\n.variantSecondary {\n background-color: var(--interactive-secondary-bg, var(--surface-subtle, transparent));\n color: var(--interactive-secondary-text, var(--text-primary, currentColor));\n border-color: transparent;\n}\n\n.variantOutline {\n background-color: transparent;\n color: var(--text-primary, #111827);\n border-color: var(--border-default, #e5e7eb);\n}\n\n.variantDestructive {\n background-color: var(--surface-error-subtle, transparent);\n color: var(--text-error, currentColor);\n border-color: transparent;\n}\n\n.variantSuccess {\n background-color: var(--surface-success-subtle, transparent);\n color: var(--text-success, currentColor);\n border-color: transparent;\n}\n\n.variantWarning {\n background-color: var(--surface-warning-subtle, transparent);\n color: var(--text-warning, currentColor);\n border-color: transparent;\n}\n\n.variantInfo {\n background-color: var(--surface-info-subtle, transparent);\n color: var(--text-info, currentColor);\n border-color: transparent;\n}\n\n/* Filled variants — step 9 bg + contrast text */\n.variantFilledDestructive {\n background-color: var(--surface-error-default, transparent);\n color: var(--text-inverse, currentColor);\n border-color: transparent;\n}\n\n.variantFilledSuccess {\n background-color: var(--surface-success-default, transparent);\n color: var(--text-inverse, currentColor);\n border-color: transparent;\n}\n\n.variantFilledWarning {\n background-color: var(--surface-warning-default, transparent);\n color: var(--text-inverse, currentColor);\n border-color: transparent;\n}\n\n.variantFilledInfo {\n background-color: var(--surface-info-default, transparent);\n color: var(--text-inverse, currentColor);\n border-color: transparent;\n}\n"
|
|
402
|
+
}
|
|
403
|
+
]
|
|
404
|
+
},
|
|
405
|
+
{
|
|
406
|
+
"name": "sparkline",
|
|
407
|
+
"type": "registry:ui",
|
|
408
|
+
"description": "A decorative inline SVG mini-trend chart for stat-card trend slots and dense data contexts. Renders a single polyline from a numeric series with zero dependencies; defaults to 96×22, accent-primary stroke.",
|
|
409
|
+
"category": "data-display",
|
|
410
|
+
"dependencies": [
|
|
411
|
+
"@loworbitstudio/visor-core"
|
|
412
|
+
],
|
|
413
|
+
"files": [
|
|
414
|
+
{
|
|
415
|
+
"path": "components/ui/sparkline/sparkline.tsx",
|
|
416
|
+
"type": "registry:ui",
|
|
417
|
+
"content": "import * as React from \"react\"\nimport styles from \"./sparkline.module.css\"\n\nexport interface SparklineProps\n extends Omit<React.SVGAttributes<SVGSVGElement>, \"viewBox\" | \"values\"> {\n /** Numeric series — minimum 2 values required to render. */\n values: number[]\n /** SVG width in px. Defaults to 96. */\n width?: number\n /** SVG height in px. Defaults to 22. */\n height?: number\n /** Stroke color — accepts CSS var, hex, hsl. Defaults to `var(--accent-primary)`. */\n color?: string\n /** Stroke width in px. Defaults to 1.5. */\n strokeWidth?: number\n /**\n * When true, the rendered `<svg>` omits its `width` attribute so it fills its\n * container (the `viewBox` preserves the aspect ratio). A CSS class forces\n * `width: 100%; height: auto; display: block;`. Defaults to `false`.\n */\n fluid?: boolean\n /** When supplied, the sparkline becomes a labeled image instead of decorative. */\n \"aria-label\"?: string\n}\n\nconst Sparkline = React.forwardRef<SVGSVGElement, SparklineProps>(\n (\n {\n values,\n width = 96,\n height = 22,\n color = \"var(--accent-primary)\",\n strokeWidth = 1.5,\n fluid = false,\n className,\n \"aria-label\": ariaLabel,\n ...props\n },\n ref\n ) => {\n if (!values || values.length < 2) return null\n\n const min = Math.min(...values)\n const max = Math.max(...values)\n const range = max - min || 1\n const stepX = width / (values.length - 1)\n const points = values\n .map((v, i) => {\n const x = i * stepX\n const y = height - ((v - min) / range) * height\n return `${x.toFixed(1)},${y.toFixed(1)}`\n })\n .join(\" \")\n\n const isLabeled = typeof ariaLabel === \"string\" && ariaLabel.length > 0\n\n return (\n <svg\n ref={ref}\n data-slot=\"sparkline\"\n className={[styles.svg, fluid && styles.svgFluid, className]\n .filter(Boolean)\n .join(\" \")}\n {...(fluid ? {} : { width })}\n height={height}\n viewBox={`0 0 ${width} ${height}`}\n role=\"img\"\n {...(isLabeled\n ? { \"aria-label\": ariaLabel }\n : { \"aria-hidden\": true })}\n {...props}\n >\n <polyline\n points={points}\n fill=\"none\"\n stroke={color}\n strokeWidth={strokeWidth}\n strokeLinejoin=\"round\"\n strokeLinecap=\"round\"\n />\n </svg>\n )\n }\n)\nSparkline.displayName = \"Sparkline\"\n\nexport { Sparkline }\n"
|
|
418
|
+
},
|
|
419
|
+
{
|
|
420
|
+
"path": "components/ui/sparkline/sparkline.module.css",
|
|
421
|
+
"type": "registry:ui",
|
|
422
|
+
"content": "/* Sparkline — inline SVG mini-trend chart */\n.svg {\n display: block;\n}\n\n.svgFluid {\n width: 100%;\n height: auto;\n display: block;\n}\n"
|
|
402
423
|
}
|
|
403
424
|
]
|
|
404
425
|
},
|
|
@@ -568,12 +589,12 @@
|
|
|
568
589
|
{
|
|
569
590
|
"path": "components/ui/progress/progress.tsx",
|
|
570
591
|
"type": "registry:ui",
|
|
571
|
-
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport * as ProgressPrimitive from \"@radix-ui/react-progress\"\nimport { cn } from \"../../../lib/utils\"\nimport styles from \"./progress.module.css\"\n\nconst Progress = React.forwardRef<\n React.ElementRef<typeof ProgressPrimitive.Root>,\n
|
|
592
|
+
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport * as ProgressPrimitive from \"@radix-ui/react-progress\"\nimport { cn } from \"../../../lib/utils\"\nimport styles from \"./progress.module.css\"\n\nexport interface ProgressProps\n extends React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root> {\n /** Track size. `\"thin\"` renders a 4px-tall variant for static admin chrome. */\n size?: \"default\" | \"thin\"\n /** Whether the indicator transitions on value change. Defaults to `true`. */\n animate?: boolean\n}\n\nconst Progress = React.forwardRef<\n React.ElementRef<typeof ProgressPrimitive.Root>,\n ProgressProps\n>(({ className, value, size = \"default\", animate = true, ...props }, ref) => {\n return (\n <ProgressPrimitive.Root\n ref={ref}\n data-slot=\"progress\"\n data-size={size === \"thin\" ? \"thin\" : undefined}\n data-animate={animate === false ? \"false\" : undefined}\n className={cn(styles.root, className)}\n {...props}\n >\n <ProgressPrimitive.Indicator\n data-slot=\"progress-indicator\"\n className={styles.indicator}\n style={{ transform: `translateX(-${100 - (value || 0)}%)` }}\n />\n </ProgressPrimitive.Root>\n )\n})\nProgress.displayName = \"Progress\"\n\nexport { Progress }\n"
|
|
572
593
|
},
|
|
573
594
|
{
|
|
574
595
|
"path": "components/ui/progress/progress.module.css",
|
|
575
596
|
"type": "registry:ui",
|
|
576
|
-
"content": "/* Progress root */\n.root {\n position: relative;\n display: flex;\n width: 100%;\n height: 0.75rem;\n align-items: center;\n overflow: hidden;\n border-radius: var(--radius-full, 9999px);\n background-color: var(--surface-muted, #f3f4f6);\n}\n\n/* Progress indicator */\n.indicator {\n width: 100%;\n height: 100%;\n flex: 1;\n background-color: var(--interactive-primary-bg, #111827);\n transition: transform var(--motion-duration-300, 300ms) var(--motion-easing-default, ease-in-out);\n}\n"
|
|
597
|
+
"content": "/* Progress root */\n.root {\n position: relative;\n display: flex;\n width: 100%;\n height: 0.75rem;\n align-items: center;\n overflow: hidden;\n border-radius: var(--radius-full, 9999px);\n background-color: var(--surface-muted, #f3f4f6);\n}\n\n/* Progress indicator */\n.indicator {\n width: 100%;\n height: 100%;\n flex: 1;\n background-color: var(--interactive-primary-bg, #111827);\n transition: transform var(--motion-duration-300, 300ms) var(--motion-easing-default, ease-in-out);\n}\n\n/* Thin static-chrome variant — 4px capacity bar for admin KPI strips */\n.root[data-size=\"thin\"] {\n height: 4px;\n background-color: var(--surface-interactive-active, #e5e7eb);\n}\n\n/* animate={false} — drop the indicator transition for static chrome */\n.root[data-animate=\"false\"] .indicator {\n transition: none;\n}\n"
|
|
577
598
|
}
|
|
578
599
|
]
|
|
579
600
|
},
|
|
@@ -939,7 +960,7 @@
|
|
|
939
960
|
{
|
|
940
961
|
"path": "components/ui/table/table.module.css",
|
|
941
962
|
"type": "registry:ui",
|
|
942
|
-
"content": "/* Table container */\n.container {\n position: relative;\n width: 100%;\n overflow-x: auto;\n overflow-y: hidden;\n border-radius: var(--radius-lg, 0.5rem);\n box-shadow: var(--shadow-sm);\n}\n\n/* Table */\n.table {\n width: 100%;\n caption-side: bottom;\n border-collapse: collapse;\n font-size: var(--font-size-sm, 0.875rem);\n color: var(--text-primary, #111827);\n}\n\n/* TableHeader */\n.header tr {\n border-bottom: 1px solid var(--border-default, #e5e7eb);\n}\n\n/* TableBody */\n.body tr:last-child {\n border-bottom: none;\n}\n\n/* TableFooter */\n.footer {\n border-top: 1px solid var(--border-default, #e5e7eb);\n background-color: var(--surface-muted, #f3f4f6);\n font-weight: var(--font-weight-medium, 500);\n}\n\n.footer tr:last-child {\n border-bottom: none;\n}\n\n/* TableRow */\n.row {\n border-bottom: 1px solid var(--border-default, #e5e7eb);\n transition: background-color var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out);\n}\n\n.row:hover {\n background-color: var(--surface-muted, #f3f4f6);\n}\n\n.row[data-state=\"selected\"] {\n background-color: var(--surface-muted, #f3f4f6);\n}\n\n/* TableHead */\n.head {\n height: 3rem;\n padding-left: var(--spacing-3, 0.75rem);\n padding-right: var(--spacing-3, 0.75rem);\n text-align: left;\n vertical-align: middle;\n font-weight: var(--font-weight-medium, 500);\n white-space: nowrap;\n color: var(--text-primary, #111827);\n}\n\n/* TableCell */\n.cell {\n padding: var(--spacing-3, 0.75rem);\n vertical-align: middle;\n color: var(--text-primary, #111827);\n}\n\n/* TableCaption */\n.caption {\n margin-top: var(--spacing-4, 1rem);\n font-size: var(--font-size-sm, 0.875rem);\n color: var(--text-secondary, #6b7280);\n}\n"
|
|
963
|
+
"content": "/* Table container */\n.container {\n position: relative;\n width: 100%;\n overflow-x: auto;\n overflow-y: hidden;\n border-radius: var(--radius-lg, 0.5rem);\n box-shadow: var(--shadow-sm);\n}\n\n/* Table */\n.table {\n width: 100%;\n caption-side: bottom;\n border-collapse: collapse;\n font-size: var(--font-size-sm, 0.875rem);\n color: var(--text-primary, #111827);\n}\n\n/* TableHeader */\n.header tr {\n border-bottom: 1px solid var(--border-default, #e5e7eb);\n}\n\n/* TableBody */\n.body tr:last-child {\n border-bottom: none;\n}\n\n/* TableFooter */\n.footer {\n border-top: 1px solid var(--border-default, #e5e7eb);\n background-color: var(--surface-muted, #f3f4f6);\n font-weight: var(--font-weight-medium, 500);\n}\n\n.footer tr:last-child {\n border-bottom: none;\n}\n\n/* TableRow */\n.row {\n border-bottom: 1px solid var(--border-default, #e5e7eb);\n transition: background-color var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out);\n}\n\n.row:not([data-slot=\"data-table-group-row\"]):hover {\n background-color: var(--surface-muted, #f3f4f6);\n}\n\n.row[data-state=\"selected\"] {\n background-color: var(--surface-muted, #f3f4f6);\n}\n\n/* TableHead */\n.head {\n height: 3rem;\n padding-left: var(--spacing-3, 0.75rem);\n padding-right: var(--spacing-3, 0.75rem);\n text-align: left;\n vertical-align: middle;\n font-weight: var(--font-weight-medium, 500);\n white-space: nowrap;\n color: var(--text-primary, #111827);\n}\n\n/* TableCell */\n.cell {\n padding: var(--spacing-3, 0.75rem);\n vertical-align: middle;\n color: var(--text-primary, #111827);\n}\n\n/* TableCaption */\n.caption {\n margin-top: var(--spacing-4, 1rem);\n font-size: var(--font-size-sm, 0.875rem);\n color: var(--text-secondary, #6b7280);\n}\n"
|
|
943
964
|
}
|
|
944
965
|
]
|
|
945
966
|
},
|
|
@@ -2052,6 +2073,32 @@
|
|
|
2052
2073
|
}
|
|
2053
2074
|
]
|
|
2054
2075
|
},
|
|
2076
|
+
{
|
|
2077
|
+
"name": "chrome-button",
|
|
2078
|
+
"type": "registry:ui",
|
|
2079
|
+
"description": "A 28px-tall button primitive for topbar and chrome contexts with an optional leading icon and trailing Kbd shortcut hint. Two variants (default, primary). Composes Button-like behavior at chrome density — not a fork of Button.",
|
|
2080
|
+
"category": "admin",
|
|
2081
|
+
"dependencies": [
|
|
2082
|
+
"class-variance-authority",
|
|
2083
|
+
"@loworbitstudio/visor-core"
|
|
2084
|
+
],
|
|
2085
|
+
"registryDependencies": [
|
|
2086
|
+
"utils",
|
|
2087
|
+
"kbd"
|
|
2088
|
+
],
|
|
2089
|
+
"files": [
|
|
2090
|
+
{
|
|
2091
|
+
"path": "components/ui/chrome-button/chrome-button.tsx",
|
|
2092
|
+
"type": "registry:ui",
|
|
2093
|
+
"content": "import * as React from \"react\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\nimport { cn } from \"../../../lib/utils\"\nimport { Kbd } from \"../kbd/kbd\"\nimport styles from \"./chrome-button.module.css\"\n\nconst chromeButtonVariants = cva(styles.root, {\n variants: {\n variant: {\n default: styles.variantDefault,\n primary: styles.variantPrimary,\n },\n },\n defaultVariants: {\n variant: \"default\",\n },\n})\n\nexport interface ChromeButtonProps\n extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, \"children\">,\n VariantProps<typeof chromeButtonVariants> {\n /** Leading icon node — typically a 14px Phosphor icon. */\n icon?: React.ReactNode\n /** Trailing keyboard shortcut hint, e.g. `[\"⌘\", \"K\"]`. Rendered as `<Kbd keys={...} size=\"sm\" />`. */\n keys?: string[]\n children: React.ReactNode\n}\n\n/**\n * ChromeButton — 28px button primitive for topbar and chrome contexts.\n *\n * Composes an optional leading icon, a label, and an optional trailing Kbd\n * shortcut hint. Two variants: `default` (muted interactive surface) and\n * `primary` (accent surface). Inherits all native `<button>` behavior — pass\n * `aria-label` via spread props for icon-only usage.\n *\n * Not a replacement for Button — Button is full-scale body chrome (32/40/48px);\n * ChromeButton is dense topbar chrome (28px) with the inline Kbd slot pattern\n * admin shells repeat.\n */\nconst ChromeButton = React.forwardRef<HTMLButtonElement, ChromeButtonProps>(\n (\n { className, variant, icon, keys, type = \"button\", children, ...props },\n ref\n ) => {\n return (\n <button\n ref={ref}\n type={type}\n data-slot=\"chrome-button\"\n data-variant={variant ?? \"default\"}\n className={cn(chromeButtonVariants({ variant }), className)}\n {...props}\n >\n {icon ? (\n <span\n data-slot=\"chrome-button-icon\"\n className={styles.icon}\n aria-hidden=\"true\"\n >\n {icon}\n </span>\n ) : null}\n <span data-slot=\"chrome-button-label\" className={styles.label}>\n {children}\n </span>\n {keys && keys.length > 0 ? (\n <span data-slot=\"chrome-button-kbd\" className={styles.keys}>\n <Kbd keys={keys} size=\"sm\" />\n </span>\n ) : null}\n </button>\n )\n }\n)\nChromeButton.displayName = \"ChromeButton\"\n\nexport { ChromeButton, chromeButtonVariants }\n"
|
|
2094
|
+
},
|
|
2095
|
+
{
|
|
2096
|
+
"path": "components/ui/chrome-button/chrome-button.module.css",
|
|
2097
|
+
"type": "registry:ui",
|
|
2098
|
+
"content": "/* ChromeButton — 28px topbar/chrome button primitive.\n *\n * Compact dimensions for admin chrome contexts (topbar, toolbar). Composes a\n * leading icon, a label, and a trailing Kbd hint at chrome density. Fully\n * theme-portable — all colors, motion, and radius bind to Visor tokens.\n */\n.root {\n display: inline-flex;\n align-items: center;\n gap: var(--spacing-2, 0.5rem);\n height: 1.75rem; /* 28px */\n padding: 0 var(--spacing-3, 0.75rem);\n border-radius: var(--radius-md, 0.375rem);\n border: 1px solid transparent;\n font-size: var(--font-size-sm, 0.875rem);\n font-weight: var(--font-weight-medium, 500);\n line-height: 1;\n cursor: pointer;\n outline: none;\n white-space: nowrap;\n transition:\n background-color var(--motion-duration-150, 150ms)\n var(--motion-easing-default, ease-in-out),\n color var(--motion-duration-150, 150ms)\n var(--motion-easing-default, ease-in-out),\n border-color var(--motion-duration-150, 150ms)\n var(--motion-easing-default, ease-in-out),\n opacity var(--motion-duration-150, 150ms)\n var(--motion-easing-default, ease-in-out);\n}\n\n.root: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.root:active:not(:disabled) {\n transform: translateY(1px);\n transition: none;\n}\n\n.root:disabled {\n pointer-events: none;\n opacity: var(--opacity-50, 0.5);\n}\n\n/* Variants */\n.variantDefault {\n background-color: var(--surface-interactive-default, #f3f4f6);\n color: var(--text-primary, #111827);\n}\n\n.variantPrimary {\n background-color: var(--interactive-primary-bg, var(--accent-primary, #111827));\n color: var(--interactive-primary-text, var(--text-inverse, #f9fafb));\n font-weight: var(--font-weight-semibold, 600);\n}\n\n@media (hover: hover) {\n .variantDefault:hover {\n background-color: var(--surface-interactive-hover, #e5e7eb);\n }\n\n .variantPrimary:hover {\n background-color: var(--interactive-primary-bg-hover, color-mix(in srgb, var(--accent-primary, #111827) 85%, white));\n }\n}\n\n/* Slots */\n.icon {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n font-size: var(--font-size-sm, 0.875rem);\n line-height: 1;\n}\n\n.icon svg {\n display: block;\n}\n\n.label {\n display: inline-flex;\n align-items: center;\n line-height: 1;\n}\n\n.keys {\n display: inline-flex;\n align-items: center;\n margin-left: calc(var(--spacing-1, 0.25rem) / 2);\n}\n"
|
|
2099
|
+
}
|
|
2100
|
+
]
|
|
2101
|
+
},
|
|
2055
2102
|
{
|
|
2056
2103
|
"name": "confirm-dialog",
|
|
2057
2104
|
"type": "registry:ui",
|
|
@@ -2104,12 +2151,12 @@
|
|
|
2104
2151
|
{
|
|
2105
2152
|
"path": "components/ui/data-table/data-table.tsx",
|
|
2106
2153
|
"type": "registry:ui",
|
|
2107
|
-
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport {\n flexRender,\n getCoreRowModel,\n getFilteredRowModel,\n getPaginationRowModel,\n getSortedRowModel,\n useReactTable,\n type ColumnDef,\n type OnChangeFn,\n type PaginationState,\n type RowSelectionState,\n type SortingState,\n type Table as TanstackTable,\n} from \"@tanstack/react-table\"\nimport {\n CaretDownIcon,\n CaretLeftIcon,\n CaretRightIcon,\n CaretUpIcon,\n CaretUpDownIcon,\n} from \"@phosphor-icons/react\"\n\nimport { cn } from \"../../../lib/utils\"\nimport {\n Table,\n TableBody,\n TableCell,\n TableHead,\n TableHeader,\n TableRow,\n} from \"../table/table\"\nimport { Button } from \"../button/button\"\nimport { Checkbox } from \"../checkbox/checkbox\"\nimport { Skeleton } from \"../skeleton/skeleton\"\nimport { EmptyState } from \"../empty-state/empty-state\"\nimport styles from \"./data-table.module.css\"\n\nexport type {\n ColumnDef,\n SortingState,\n RowSelectionState,\n PaginationState,\n OnChangeFn,\n}\n\nexport interface DataTableGroupRow {\n kind: \"group\"\n id: string\n label: string\n count?: number\n}\n\nexport interface DataTableDataRow<TData> {\n kind: \"data\"\n id: string\n row: TData\n}\n\nexport type DataTableRow<TData> = DataTableGroupRow | DataTableDataRow<TData>\n\nexport interface DataTableProps<TData, TValue = unknown>\n extends Omit<React.HTMLAttributes<HTMLDivElement>, \"onChange\"> {\n columns: ColumnDef<TData, TValue>[]\n data?: TData[]\n\n // Mixed render order with group-head separators. When provided, the caller\n // owns sort/grouping/windowing — sort UI and pagination footer are\n // suppressed. Group rows are excluded from selection state.\n rows?: DataTableRow<TData>[]\n groupRowRenderer?: (group: DataTableGroupRow) => React.ReactNode\n\n // Sorting\n sorting?: SortingState\n onSortingChange?: OnChangeFn<SortingState>\n defaultSorting?: SortingState\n\n // Pagination\n pagination?: PaginationState\n onPaginationChange?: OnChangeFn<PaginationState>\n pageSize?: number\n pageSizeOptions?: number[]\n\n // Selection\n enableRowSelection?: boolean\n rowSelection?: RowSelectionState\n onRowSelectionChange?: OnChangeFn<RowSelectionState>\n getRowId?: (row: TData, index: number) => string\n\n // Global filter\n globalFilter?: string\n onGlobalFilterChange?: (value: string) => void\n\n // States\n loading?: boolean\n emptyState?: React.ReactNode\n\n // Layout\n stickyHeader?: boolean\n}\n\nconst DEFAULT_PAGE_SIZE_OPTIONS = [10, 25, 50, 100]\n\nfunction DataTableInner<TData, TValue = unknown>(\n props: DataTableProps<TData, TValue>,\n ref: React.ForwardedRef<HTMLDivElement>\n) {\n const {\n columns: userColumns,\n data,\n rows,\n groupRowRenderer,\n sorting: controlledSorting,\n onSortingChange,\n defaultSorting,\n pagination: controlledPagination,\n onPaginationChange,\n pageSize = 10,\n pageSizeOptions = DEFAULT_PAGE_SIZE_OPTIONS,\n enableRowSelection = false,\n rowSelection: controlledRowSelection,\n onRowSelectionChange,\n getRowId,\n globalFilter: controlledGlobalFilter,\n onGlobalFilterChange,\n loading = false,\n emptyState,\n stickyHeader = false,\n className,\n ...rest\n } = props\n\n // When rows is provided, the caller owns sort/grouping/windowing. We bypass\n // TanStack pagination and column sort UI, but keep the table instance for\n // selection state and cell rendering on data rows.\n const hasRows = rows != null\n const dataItems = React.useMemo(() => {\n if (hasRows) {\n return rows!.flatMap((item) =>\n item.kind === \"data\" ? [item.row] : []\n )\n }\n return data ?? []\n }, [hasRows, rows, data])\n\n const internalGetRowId = React.useMemo(() => {\n if (getRowId) return getRowId\n if (hasRows) {\n const ids = rows!.flatMap((item) =>\n item.kind === \"data\" ? [item.id] : []\n )\n return (_row: TData, index: number) => ids[index] ?? String(index)\n }\n return undefined\n }, [getRowId, hasRows, rows])\n\n // Uncontrolled sorting state\n const [internalSorting, setInternalSorting] = React.useState<SortingState>(\n defaultSorting ?? []\n )\n const sortingIsControlled = controlledSorting !== undefined\n const sorting = sortingIsControlled ? controlledSorting : internalSorting\n const handleSortingChange: OnChangeFn<SortingState> = (updater) => {\n if (!sortingIsControlled) {\n setInternalSorting((prev) =>\n typeof updater === \"function\" ? updater(prev) : updater\n )\n }\n onSortingChange?.(updater)\n }\n\n // Uncontrolled pagination state\n const [internalPagination, setInternalPagination] =\n React.useState<PaginationState>({ pageIndex: 0, pageSize })\n const paginationIsControlled = controlledPagination !== undefined\n const pagination = paginationIsControlled\n ? controlledPagination\n : internalPagination\n const handlePaginationChange: OnChangeFn<PaginationState> = (updater) => {\n if (!paginationIsControlled) {\n setInternalPagination((prev) =>\n typeof updater === \"function\" ? updater(prev) : updater\n )\n }\n onPaginationChange?.(updater)\n }\n\n // Uncontrolled selection state\n const [internalRowSelection, setInternalRowSelection] =\n React.useState<RowSelectionState>({})\n const selectionIsControlled = controlledRowSelection !== undefined\n const rowSelection = selectionIsControlled\n ? controlledRowSelection\n : internalRowSelection\n const handleRowSelectionChange: OnChangeFn<RowSelectionState> = (updater) => {\n if (!selectionIsControlled) {\n setInternalRowSelection((prev) =>\n typeof updater === \"function\" ? updater(prev) : updater\n )\n }\n onRowSelectionChange?.(updater)\n }\n\n // Global filter\n const [internalGlobalFilter, setInternalGlobalFilter] = React.useState(\"\")\n const globalFilterIsControlled = controlledGlobalFilter !== undefined\n const globalFilter = globalFilterIsControlled\n ? controlledGlobalFilter\n : internalGlobalFilter\n const handleGlobalFilterChange = (value: unknown) => {\n const next = typeof value === \"function\" ? (value as (p: string) => string)(globalFilter) : (value as string)\n if (!globalFilterIsControlled) {\n setInternalGlobalFilter(next ?? \"\")\n }\n onGlobalFilterChange?.(next ?? \"\")\n }\n\n // Inject a selection column when enabled\n const columns = React.useMemo<ColumnDef<TData, TValue>[]>(() => {\n if (!enableRowSelection) return userColumns\n const selectionColumn: ColumnDef<TData, TValue> = {\n id: \"__select\",\n enableSorting: false,\n size: 40,\n header: ({ table }) => (\n <Checkbox\n aria-label=\"Select all rows\"\n checked={\n table.getIsAllPageRowsSelected()\n ? true\n : table.getIsSomePageRowsSelected()\n ? \"indeterminate\"\n : false\n }\n onCheckedChange={(value) =>\n table.toggleAllPageRowsSelected(value === true)\n }\n />\n ),\n cell: ({ row }) => (\n <Checkbox\n aria-label=\"Select row\"\n checked={row.getIsSelected()}\n disabled={!row.getCanSelect()}\n onCheckedChange={(value) => row.toggleSelected(value === true)}\n />\n ),\n }\n return [selectionColumn, ...userColumns]\n }, [enableRowSelection, userColumns])\n\n const table: TanstackTable<TData> = useReactTable<TData>({\n data: dataItems,\n columns,\n state: {\n sorting,\n pagination,\n rowSelection,\n globalFilter,\n },\n enableRowSelection,\n getRowId: internalGetRowId,\n onSortingChange: handleSortingChange,\n onPaginationChange: handlePaginationChange,\n onRowSelectionChange: handleRowSelectionChange,\n onGlobalFilterChange: handleGlobalFilterChange,\n getCoreRowModel: getCoreRowModel(),\n getSortedRowModel: getSortedRowModel(),\n getPaginationRowModel: getPaginationRowModel(),\n getFilteredRowModel: getFilteredRowModel(),\n })\n\n const totalRows = table.getFilteredRowModel().rows.length\n const pageRows = table.getRowModel().rows\n const colCount = columns.length\n const pageIndex = table.getState().pagination.pageIndex\n const currentPageSize = table.getState().pagination.pageSize\n const pageCount = table.getPageCount()\n const firstRow = totalRows === 0 ? 0 : pageIndex * currentPageSize + 1\n const lastRow = Math.min((pageIndex + 1) * currentPageSize, totalRows)\n\n const isEmpty = !loading && dataItems.length === 0 && !hasRows\n const defaultEmpty = <EmptyState heading=\"No results\" tone=\"subtle\" />\n\n const defaultGroupRowContent = (group: DataTableGroupRow) => (\n <span data-slot=\"data-table-group-label\" className={styles.groupLabel}>\n {group.label}\n {group.count != null && (\n <span className={styles.groupCount}>{group.count}</span>\n )}\n </span>\n )\n\n return (\n <div\n ref={ref}\n data-slot=\"data-table\"\n className={cn(styles.root, className)}\n {...rest}\n >\n <Table>\n <TableHeader\n className={cn(stickyHeader && styles.stickyHeader)}\n data-sticky={stickyHeader || undefined}\n >\n {table.getHeaderGroups().map((headerGroup) => (\n <TableRow key={headerGroup.id}>\n {headerGroup.headers.map((header) => {\n const canSort = header.column.getCanSort() && !hasRows\n const sortDir = header.column.getIsSorted()\n const ariaSort: React.AriaAttributes[\"aria-sort\"] = hasRows\n ? undefined\n : sortDir === \"asc\"\n ? \"ascending\"\n : sortDir === \"desc\"\n ? \"descending\"\n : canSort\n ? \"none\"\n : undefined\n const headerContent = header.isPlaceholder\n ? null\n : flexRender(\n header.column.columnDef.header,\n header.getContext()\n )\n const columnLabel =\n typeof header.column.columnDef.header === \"string\"\n ? (header.column.columnDef.header as string)\n : header.column.id\n const nextSortStateLabel =\n sortDir === \"asc\"\n ? \"descending\"\n : sortDir === \"desc\"\n ? \"unsorted\"\n : \"ascending\"\n return (\n <TableHead\n key={header.id}\n aria-sort={ariaSort}\n style={{\n width:\n header.column.id === \"__select\" ? \"40px\" : undefined,\n }}\n >\n {canSort ? (\n <button\n type=\"button\"\n className={styles.sortButton}\n onClick={header.column.getToggleSortingHandler()}\n aria-label={`${columnLabel}, sort ${nextSortStateLabel}`}\n >\n <span className={styles.sortLabel}>\n {headerContent}\n </span>\n <span className={styles.sortIcon} aria-hidden=\"true\">\n {sortDir === \"asc\" ? (\n <CaretUpIcon weight=\"bold\" />\n ) : sortDir === \"desc\" ? (\n <CaretDownIcon weight=\"bold\" />\n ) : (\n <CaretUpDownIcon weight=\"bold\" />\n )}\n </span>\n </button>\n ) : (\n headerContent\n )}\n </TableHead>\n )\n })}\n </TableRow>\n ))}\n </TableHeader>\n <TableBody>\n {loading ? (\n Array.from({ length: currentPageSize }).map((_, rowIdx) => (\n <TableRow key={`skeleton-${rowIdx}`} data-slot=\"data-table-skeleton-row\">\n {columns.map((_col, colIdx) => (\n <TableCell key={`skeleton-${rowIdx}-${colIdx}`}>\n <Skeleton className={styles.skeletonCell} />\n </TableCell>\n ))}\n </TableRow>\n ))\n ) : isEmpty ? (\n <TableRow data-slot=\"data-table-empty-row\">\n <TableCell colSpan={colCount} className={styles.emptyCell}>\n {emptyState ?? defaultEmpty}\n </TableCell>\n </TableRow>\n ) : hasRows ? (\n rows!.map((item) => {\n if (item.kind === \"group\") {\n return (\n <TableRow\n key={`group-${item.id}`}\n data-slot=\"data-table-group-row\"\n className={styles.groupRow}\n >\n <TableCell\n colSpan={colCount}\n className={styles.groupCell}\n >\n {groupRowRenderer\n ? groupRowRenderer(item)\n : defaultGroupRowContent(item)}\n </TableCell>\n </TableRow>\n )\n }\n const tsRow = table.getRow(item.id)\n return (\n <TableRow\n key={item.id}\n data-state={tsRow.getIsSelected() ? \"selected\" : undefined}\n >\n {tsRow.getVisibleCells().map((cell) => (\n <TableCell key={cell.id}>\n {flexRender(\n cell.column.columnDef.cell,\n cell.getContext()\n )}\n </TableCell>\n ))}\n </TableRow>\n )\n })\n ) : pageRows.length === 0 ? (\n <TableRow data-slot=\"data-table-empty-row\">\n <TableCell colSpan={colCount} className={styles.emptyCell}>\n {emptyState ?? defaultEmpty}\n </TableCell>\n </TableRow>\n ) : (\n pageRows.map((row) => (\n <TableRow\n key={row.id}\n data-state={row.getIsSelected() ? \"selected\" : undefined}\n >\n {row.getVisibleCells().map((cell) => (\n <TableCell key={cell.id}>\n {flexRender(cell.column.columnDef.cell, cell.getContext())}\n </TableCell>\n ))}\n </TableRow>\n ))\n )}\n </TableBody>\n </Table>\n\n {!hasRows && (\n <div className={styles.footer} data-slot=\"data-table-footer\">\n <div className={styles.footerInfo} aria-live=\"polite\">\n {totalRows === 0\n ? \"No results\"\n : `Showing ${firstRow} to ${lastRow} of ${totalRows}`}\n </div>\n <div className={styles.footerControls}>\n <label className={styles.pageSizeLabel}>\n <span className={styles.pageSizeLabelText}>Rows per page</span>\n <select\n className={styles.pageSizeSelect}\n value={currentPageSize}\n onChange={(e) => table.setPageSize(Number(e.target.value))}\n aria-label=\"Rows per page\"\n >\n {pageSizeOptions.map((opt) => (\n <option key={opt} value={opt}>\n {opt}\n </option>\n ))}\n </select>\n </label>\n <div className={styles.pageNav}>\n <span className={styles.pageCounter}>\n Page {pageCount === 0 ? 0 : pageIndex + 1} of {pageCount}\n </span>\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n onClick={() => table.previousPage()}\n disabled={!table.getCanPreviousPage()}\n aria-label=\"Previous page\"\n >\n <CaretLeftIcon weight=\"bold\" aria-hidden=\"true\" />\n </Button>\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n onClick={() => table.nextPage()}\n disabled={!table.getCanNextPage()}\n aria-label=\"Next page\"\n >\n <CaretRightIcon weight=\"bold\" aria-hidden=\"true\" />\n </Button>\n </div>\n </div>\n </div>\n )}\n </div>\n )\n}\n\n// forwardRef with generics — preserve TData through the cast\nconst DataTable = React.forwardRef(DataTableInner) as <\n TData,\n TValue = unknown,\n>(\n props: DataTableProps<TData, TValue> & {\n ref?: React.ForwardedRef<HTMLDivElement>\n }\n) => ReturnType<typeof DataTableInner>\n\n;(DataTable as unknown as { displayName: string }).displayName = \"DataTable\"\n\nexport { DataTable }\n"
|
|
2154
|
+
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport {\n flexRender,\n getCoreRowModel,\n getFilteredRowModel,\n getPaginationRowModel,\n getSortedRowModel,\n useReactTable,\n type ColumnDef,\n type OnChangeFn,\n type PaginationState,\n type RowSelectionState,\n type SortingState,\n type Table as TanstackTable,\n} from \"@tanstack/react-table\"\nimport {\n CaretDownIcon,\n CaretLeftIcon,\n CaretRightIcon,\n CaretUpIcon,\n CaretUpDownIcon,\n} from \"@phosphor-icons/react\"\n\nimport { cn } from \"../../../lib/utils\"\nimport {\n Table,\n TableBody,\n TableCell,\n TableHead,\n TableHeader,\n TableRow,\n} from \"../table/table\"\nimport { Button } from \"../button/button\"\nimport { Checkbox } from \"../checkbox/checkbox\"\nimport { Skeleton } from \"../skeleton/skeleton\"\nimport { EmptyState } from \"../empty-state/empty-state\"\nimport styles from \"./data-table.module.css\"\n\nexport type {\n ColumnDef,\n SortingState,\n RowSelectionState,\n PaginationState,\n OnChangeFn,\n}\n\nexport interface DataTableGroupRow {\n kind: \"group\"\n id: string\n label: string\n count?: number\n}\n\nexport interface DataTableDataRow<TData> {\n kind: \"data\"\n id: string\n row: TData\n}\n\nexport type DataTableRow<TData> = DataTableGroupRow | DataTableDataRow<TData>\n\n/**\n * Semantic per-row tone keys. Map to subtle background tints via CSS — see\n * `data-table.module.css`. Mirrors the tone vocabulary used by `StatusBadge`\n * / `StatusDot` so a row tagged \"live\" reads as one signal with a \"live\"\n * badge inside the row.\n */\nexport type DataTableRowTone =\n | \"live\"\n | \"warn\"\n | \"scheduled\"\n | \"sold\"\n | \"draft\"\n | \"danger\"\n | \"info\"\n\nexport interface DataTableProps<TData, TValue = unknown>\n extends Omit<React.HTMLAttributes<HTMLDivElement>, \"onChange\"> {\n columns: ColumnDef<TData, TValue>[]\n data?: TData[]\n\n // Mixed render order with group-head separators. When provided, the caller\n // owns sort/grouping/windowing — sort UI and pagination footer are\n // suppressed. Group rows are excluded from selection state.\n rows?: DataTableRow<TData>[]\n groupRowRenderer?: (group: DataTableGroupRow) => React.ReactNode\n\n /**\n * Map each data row to a semantic tone for a subtle background tint. When\n * the callback returns `undefined`, the row renders on the default surface.\n * Tones resolve to Visor surface tokens at the CSS layer — see\n * `data-table.module.css`.\n */\n rowTone?: (row: TData) => DataTableRowTone | undefined\n\n /**\n * When supplied, every data row becomes a keyboard-activatable target:\n * `role=\"button\"`, `tabIndex={0}`, click + Enter/Space dispatch the\n * handler, and a `data-clickable=\"true\"` attribute drives the hover/focus\n * affordance. The injected selection checkbox cell stops propagation, so\n * clicking it does not trigger `onRowClick`.\n */\n onRowClick?: (row: TData) => void\n\n // Sorting\n sorting?: SortingState\n onSortingChange?: OnChangeFn<SortingState>\n defaultSorting?: SortingState\n\n // Pagination\n pagination?: PaginationState\n onPaginationChange?: OnChangeFn<PaginationState>\n pageSize?: number\n pageSizeOptions?: number[]\n\n // Selection\n enableRowSelection?: boolean\n rowSelection?: RowSelectionState\n onRowSelectionChange?: OnChangeFn<RowSelectionState>\n getRowId?: (row: TData, index: number) => string\n\n // Global filter\n globalFilter?: string\n onGlobalFilterChange?: (value: string) => void\n\n // States\n loading?: boolean\n emptyState?: React.ReactNode\n\n // Layout\n stickyHeader?: boolean\n}\n\nconst DEFAULT_PAGE_SIZE_OPTIONS = [10, 25, 50, 100]\n\nfunction DataTableInner<TData, TValue = unknown>(\n props: DataTableProps<TData, TValue>,\n ref: React.ForwardedRef<HTMLDivElement>\n) {\n const {\n columns: userColumns,\n data,\n rows,\n groupRowRenderer,\n sorting: controlledSorting,\n onSortingChange,\n defaultSorting,\n pagination: controlledPagination,\n onPaginationChange,\n pageSize = 10,\n pageSizeOptions = DEFAULT_PAGE_SIZE_OPTIONS,\n enableRowSelection = false,\n rowSelection: controlledRowSelection,\n onRowSelectionChange,\n getRowId,\n globalFilter: controlledGlobalFilter,\n onGlobalFilterChange,\n loading = false,\n emptyState,\n stickyHeader = false,\n rowTone,\n onRowClick,\n className,\n ...rest\n } = props\n\n // When rows is provided, the caller owns sort/grouping/windowing. We bypass\n // TanStack pagination and column sort UI, but keep the table instance for\n // selection state and cell rendering on data rows.\n const hasRows = rows != null\n const dataItems = React.useMemo(() => {\n if (hasRows) {\n return rows!.flatMap((item) =>\n item.kind === \"data\" ? [item.row] : []\n )\n }\n return data ?? []\n }, [hasRows, rows, data])\n\n const internalGetRowId = React.useMemo(() => {\n if (getRowId) return getRowId\n if (hasRows) {\n const ids = rows!.flatMap((item) =>\n item.kind === \"data\" ? [item.id] : []\n )\n return (_row: TData, index: number) => ids[index] ?? String(index)\n }\n return undefined\n }, [getRowId, hasRows, rows])\n\n // Uncontrolled sorting state\n const [internalSorting, setInternalSorting] = React.useState<SortingState>(\n defaultSorting ?? []\n )\n const sortingIsControlled = controlledSorting !== undefined\n const sorting = sortingIsControlled ? controlledSorting : internalSorting\n const handleSortingChange: OnChangeFn<SortingState> = (updater) => {\n if (!sortingIsControlled) {\n setInternalSorting((prev) =>\n typeof updater === \"function\" ? updater(prev) : updater\n )\n }\n onSortingChange?.(updater)\n }\n\n // Uncontrolled pagination state\n const [internalPagination, setInternalPagination] =\n React.useState<PaginationState>({ pageIndex: 0, pageSize })\n const paginationIsControlled = controlledPagination !== undefined\n const pagination = paginationIsControlled\n ? controlledPagination\n : internalPagination\n const handlePaginationChange: OnChangeFn<PaginationState> = (updater) => {\n if (!paginationIsControlled) {\n setInternalPagination((prev) =>\n typeof updater === \"function\" ? updater(prev) : updater\n )\n }\n onPaginationChange?.(updater)\n }\n\n // Uncontrolled selection state\n const [internalRowSelection, setInternalRowSelection] =\n React.useState<RowSelectionState>({})\n const selectionIsControlled = controlledRowSelection !== undefined\n const rowSelection = selectionIsControlled\n ? controlledRowSelection\n : internalRowSelection\n const handleRowSelectionChange: OnChangeFn<RowSelectionState> = (updater) => {\n if (!selectionIsControlled) {\n setInternalRowSelection((prev) =>\n typeof updater === \"function\" ? updater(prev) : updater\n )\n }\n onRowSelectionChange?.(updater)\n }\n\n // Global filter\n const [internalGlobalFilter, setInternalGlobalFilter] = React.useState(\"\")\n const globalFilterIsControlled = controlledGlobalFilter !== undefined\n const globalFilter = globalFilterIsControlled\n ? controlledGlobalFilter\n : internalGlobalFilter\n const handleGlobalFilterChange = (value: unknown) => {\n const next = typeof value === \"function\" ? (value as (p: string) => string)(globalFilter) : (value as string)\n if (!globalFilterIsControlled) {\n setInternalGlobalFilter(next ?? \"\")\n }\n onGlobalFilterChange?.(next ?? \"\")\n }\n\n // Inject a selection column when enabled\n const columns = React.useMemo<ColumnDef<TData, TValue>[]>(() => {\n if (!enableRowSelection) return userColumns\n const selectionColumn: ColumnDef<TData, TValue> = {\n id: \"__select\",\n enableSorting: false,\n size: 40,\n header: ({ table }) => (\n <Checkbox\n aria-label=\"Select all rows\"\n checked={\n table.getIsAllPageRowsSelected()\n ? true\n : table.getIsSomePageRowsSelected()\n ? \"indeterminate\"\n : false\n }\n onCheckedChange={(value) =>\n table.toggleAllPageRowsSelected(value === true)\n }\n />\n ),\n cell: ({ row }) => (\n // Stop click/keydown from bubbling to the parent <tr>, otherwise\n // toggling the checkbox would also fire `onRowClick`. The wrapper\n // is presentational — focus and ARIA continue to live on the\n // underlying Checkbox.\n <div\n data-slot=\"data-table-selection-cell\"\n onClick={(e) => e.stopPropagation()}\n onKeyDown={(e) => {\n if (e.key === \"Enter\" || e.key === \" \") e.stopPropagation()\n }}\n >\n <Checkbox\n aria-label=\"Select row\"\n checked={row.getIsSelected()}\n disabled={!row.getCanSelect()}\n onCheckedChange={(value) => row.toggleSelected(value === true)}\n />\n </div>\n ),\n }\n return [selectionColumn, ...userColumns]\n }, [enableRowSelection, userColumns])\n\n const table: TanstackTable<TData> = useReactTable<TData>({\n data: dataItems,\n columns,\n state: {\n sorting,\n pagination,\n rowSelection,\n globalFilter,\n },\n enableRowSelection,\n getRowId: internalGetRowId,\n onSortingChange: handleSortingChange,\n onPaginationChange: handlePaginationChange,\n onRowSelectionChange: handleRowSelectionChange,\n onGlobalFilterChange: handleGlobalFilterChange,\n getCoreRowModel: getCoreRowModel(),\n getSortedRowModel: getSortedRowModel(),\n getPaginationRowModel: getPaginationRowModel(),\n getFilteredRowModel: getFilteredRowModel(),\n })\n\n const totalRows = table.getFilteredRowModel().rows.length\n const pageRows = table.getRowModel().rows\n const colCount = columns.length\n const pageIndex = table.getState().pagination.pageIndex\n const currentPageSize = table.getState().pagination.pageSize\n const pageCount = table.getPageCount()\n const firstRow = totalRows === 0 ? 0 : pageIndex * currentPageSize + 1\n const lastRow = Math.min((pageIndex + 1) * currentPageSize, totalRows)\n\n const isEmpty = !loading && dataItems.length === 0 && !hasRows\n const defaultEmpty = <EmptyState heading=\"No results\" tone=\"subtle\" />\n\n const defaultGroupRowContent = (group: DataTableGroupRow) => (\n <span data-slot=\"data-table-group-label\" className={styles.groupLabel}>\n {group.label}\n {group.count != null && (\n <span className={styles.groupCount}>{group.count}</span>\n )}\n </span>\n )\n\n // Build the per-data-row props (tone, clickable affordance, keyboard\n // activation). Shared between the `rows`-driven path and the standard\n // pageRows path so the two stay in lockstep.\n //\n // Note on role=\"button\": axe flags nested-interactive when a `<tr>` carries\n // `role=\"button\"` and also contains an interactive control (the selection\n // checkbox cell). When selection is enabled, the row stays semantically a\n // table row — click + keyboard activation still work via the explicit\n // handlers, but the role override is dropped to keep `<tr>` semantics and\n // satisfy WCAG nested-interactive. When selection is off, the row is a\n // pure click target and `role=\"button\"` is safe.\n const getDataRowProps = (rowData: TData) => {\n const tone = rowTone?.(rowData)\n const clickable = onRowClick != null\n const handleClick = clickable\n ? () => onRowClick!(rowData)\n : undefined\n const handleKeyDown = clickable\n ? (e: React.KeyboardEvent<HTMLTableRowElement>) => {\n if (e.key === \"Enter\" || e.key === \" \") {\n e.preventDefault()\n onRowClick!(rowData)\n }\n }\n : undefined\n const useButtonRole = clickable && !enableRowSelection\n return {\n className: styles.dataRow,\n \"data-tone\": tone,\n \"data-clickable\": clickable ? \"true\" : undefined,\n role: useButtonRole ? (\"button\" as const) : undefined,\n tabIndex: clickable ? 0 : undefined,\n onClick: handleClick,\n onKeyDown: handleKeyDown,\n }\n }\n\n return (\n <div\n ref={ref}\n data-slot=\"data-table\"\n className={cn(styles.root, className)}\n {...rest}\n >\n <Table>\n <TableHeader\n className={cn(stickyHeader && styles.stickyHeader)}\n data-sticky={stickyHeader || undefined}\n >\n {table.getHeaderGroups().map((headerGroup) => (\n <TableRow key={headerGroup.id}>\n {headerGroup.headers.map((header) => {\n const canSort = header.column.getCanSort() && !hasRows\n const sortDir = header.column.getIsSorted()\n const ariaSort: React.AriaAttributes[\"aria-sort\"] = hasRows\n ? undefined\n : sortDir === \"asc\"\n ? \"ascending\"\n : sortDir === \"desc\"\n ? \"descending\"\n : canSort\n ? \"none\"\n : undefined\n const headerContent = header.isPlaceholder\n ? null\n : flexRender(\n header.column.columnDef.header,\n header.getContext()\n )\n const columnLabel =\n typeof header.column.columnDef.header === \"string\"\n ? (header.column.columnDef.header as string)\n : header.column.id\n const nextSortStateLabel =\n sortDir === \"asc\"\n ? \"descending\"\n : sortDir === \"desc\"\n ? \"unsorted\"\n : \"ascending\"\n return (\n <TableHead\n key={header.id}\n aria-sort={ariaSort}\n style={{\n width:\n header.column.id === \"__select\" ? \"40px\" : undefined,\n }}\n >\n {canSort ? (\n <button\n type=\"button\"\n className={styles.sortButton}\n onClick={header.column.getToggleSortingHandler()}\n aria-label={`${columnLabel}, sort ${nextSortStateLabel}`}\n >\n <span className={styles.sortLabel}>\n {headerContent}\n </span>\n <span className={styles.sortIcon} aria-hidden=\"true\">\n {sortDir === \"asc\" ? (\n <CaretUpIcon weight=\"bold\" />\n ) : sortDir === \"desc\" ? (\n <CaretDownIcon weight=\"bold\" />\n ) : (\n <CaretUpDownIcon weight=\"bold\" />\n )}\n </span>\n </button>\n ) : (\n headerContent\n )}\n </TableHead>\n )\n })}\n </TableRow>\n ))}\n </TableHeader>\n <TableBody>\n {loading ? (\n Array.from({ length: currentPageSize }).map((_, rowIdx) => (\n <TableRow key={`skeleton-${rowIdx}`} data-slot=\"data-table-skeleton-row\">\n {columns.map((_col, colIdx) => (\n <TableCell key={`skeleton-${rowIdx}-${colIdx}`}>\n <Skeleton className={styles.skeletonCell} />\n </TableCell>\n ))}\n </TableRow>\n ))\n ) : isEmpty ? (\n <TableRow data-slot=\"data-table-empty-row\">\n <TableCell colSpan={colCount} className={styles.emptyCell}>\n {emptyState ?? defaultEmpty}\n </TableCell>\n </TableRow>\n ) : hasRows ? (\n rows!.map((item) => {\n if (item.kind === \"group\") {\n return (\n <TableRow\n key={`group-${item.id}`}\n data-slot=\"data-table-group-row\"\n className={styles.groupRow}\n >\n <TableCell\n colSpan={colCount}\n className={styles.groupCell}\n >\n {groupRowRenderer\n ? groupRowRenderer(item)\n : defaultGroupRowContent(item)}\n </TableCell>\n </TableRow>\n )\n }\n const tsRow = table.getRow(item.id)\n const rowProps = getDataRowProps(item.row)\n return (\n <TableRow\n key={item.id}\n data-state={tsRow.getIsSelected() ? \"selected\" : undefined}\n {...rowProps}\n >\n {tsRow.getVisibleCells().map((cell) => (\n <TableCell key={cell.id}>\n {flexRender(\n cell.column.columnDef.cell,\n cell.getContext()\n )}\n </TableCell>\n ))}\n </TableRow>\n )\n })\n ) : pageRows.length === 0 ? (\n <TableRow data-slot=\"data-table-empty-row\">\n <TableCell colSpan={colCount} className={styles.emptyCell}>\n {emptyState ?? defaultEmpty}\n </TableCell>\n </TableRow>\n ) : (\n pageRows.map((row) => {\n const rowProps = getDataRowProps(row.original)\n return (\n <TableRow\n key={row.id}\n data-state={row.getIsSelected() ? \"selected\" : undefined}\n {...rowProps}\n >\n {row.getVisibleCells().map((cell) => (\n <TableCell key={cell.id}>\n {flexRender(\n cell.column.columnDef.cell,\n cell.getContext()\n )}\n </TableCell>\n ))}\n </TableRow>\n )\n })\n )}\n </TableBody>\n </Table>\n\n {!hasRows && (\n <div className={styles.footer} data-slot=\"data-table-footer\">\n <div className={styles.footerInfo} aria-live=\"polite\">\n {totalRows === 0\n ? \"No results\"\n : `Showing ${firstRow} to ${lastRow} of ${totalRows}`}\n </div>\n <div className={styles.footerControls}>\n <label className={styles.pageSizeLabel}>\n <span className={styles.pageSizeLabelText}>Rows per page</span>\n <select\n className={styles.pageSizeSelect}\n value={currentPageSize}\n onChange={(e) => table.setPageSize(Number(e.target.value))}\n aria-label=\"Rows per page\"\n >\n {pageSizeOptions.map((opt) => (\n <option key={opt} value={opt}>\n {opt}\n </option>\n ))}\n </select>\n </label>\n <div className={styles.pageNav}>\n <span className={styles.pageCounter}>\n Page {pageCount === 0 ? 0 : pageIndex + 1} of {pageCount}\n </span>\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n onClick={() => table.previousPage()}\n disabled={!table.getCanPreviousPage()}\n aria-label=\"Previous page\"\n >\n <CaretLeftIcon weight=\"bold\" aria-hidden=\"true\" />\n </Button>\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n onClick={() => table.nextPage()}\n disabled={!table.getCanNextPage()}\n aria-label=\"Next page\"\n >\n <CaretRightIcon weight=\"bold\" aria-hidden=\"true\" />\n </Button>\n </div>\n </div>\n </div>\n )}\n </div>\n )\n}\n\n// forwardRef with generics — preserve TData through the cast\nconst DataTable = React.forwardRef(DataTableInner) as <\n TData,\n TValue = unknown,\n>(\n props: DataTableProps<TData, TValue> & {\n ref?: React.ForwardedRef<HTMLDivElement>\n }\n) => ReturnType<typeof DataTableInner>\n\n;(DataTable as unknown as { displayName: string }).displayName = \"DataTable\"\n\nexport { DataTable }\n"
|
|
2108
2155
|
},
|
|
2109
2156
|
{
|
|
2110
2157
|
"path": "components/ui/data-table/data-table.module.css",
|
|
2111
2158
|
"type": "registry:ui",
|
|
2112
|
-
"content": "/* DataTable root — inline-size container for responsive collapse */\n.root {\n display: flex;\n flex-direction: column;\n width: 100%;\n min-width: 0;\n gap: var(--spacing-3, 0.75rem);\n container-type: inline-size;\n container-name: data-table;\n color: var(--text-primary, #111827);\n}\n\n/* Sticky header wrapper — toggled when stickyHeader prop is true */\n.stickyHeader {\n position: sticky;\n top: 0;\n z-index: 1;\n background: var(--surface-card, #ffffff);\n}\n\n/* Sort button — renders inside the <th> */\n.sortButton {\n display: inline-flex;\n align-items: center;\n gap: var(--spacing-1, 0.25rem);\n padding: 0;\n margin: 0;\n background: transparent;\n border: 0;\n color: inherit;\n font: inherit;\n font-weight: var(--font-weight-semibold, 600);\n cursor: pointer;\n line-height: var(--line-height-tight, 1.25);\n border-radius: var(--radius-sm, 0.25rem);\n transition:\n 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.sortButton:hover {\n color: var(--text-primary, #111827);\n}\n\n.sortButton:focus-visible {\n outline: var(--focus-ring-width, 2px) solid\n var(--focus-ring-color, #2563eb);\n outline-offset: var(--focus-ring-offset, 2px);\n}\n\n.sortLabel {\n display: inline-block;\n}\n\n.sortIcon {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: var(--spacing-4, 1rem);\n height: var(--spacing-4, 1rem);\n color: var(--text-tertiary, #6b7280);\n flex-shrink: 0;\n}\n\n.sortIcon svg {\n width: 100%;\n height: 100%;\n}\n\n/* Skeleton cell fills the cell width */\n.skeletonCell {\n width: 100%;\n height: var(--spacing-4, 1rem);\n}\n\n/* Empty-row cell centers the EmptyState slot */\n.emptyCell {\n padding: var(--spacing-6, 1.5rem) var(--spacing-4, 1rem);\n text-align: center;\n}\n\n/* Footer — info on the left, controls on the right */\n.footer {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: var(--spacing-4, 1rem);\n padding: var(--spacing-2, 0.5rem) var(--spacing-1, 0.25rem);\n min-width: 0;\n flex-wrap: wrap;\n}\n\n.footerInfo {\n font-size: var(--font-size-sm, 0.875rem);\n color: var(--text-secondary, #6b7280);\n min-width: 0;\n}\n\n.footerControls {\n display: flex;\n align-items: center;\n gap: var(--spacing-4, 1rem);\n flex-wrap: wrap;\n}\n\n.pageSizeLabel {\n display: inline-flex;\n align-items: center;\n gap: var(--spacing-2, 0.5rem);\n font-size: var(--font-size-sm, 0.875rem);\n color: var(--text-secondary, #6b7280);\n}\n\n.pageSizeLabelText {\n white-space: nowrap;\n}\n\n/* Native <select> styled to match Visor input controls */\n.pageSizeSelect {\n appearance: none;\n -webkit-appearance: none;\n -moz-appearance: none;\n height: var(--spacing-8, 2rem);\n padding: 0 var(--spacing-6, 1.5rem) 0 var(--spacing-2, 0.5rem);\n border-radius: var(--radius-md, 0.375rem);\n border: 1px solid var(--border-default, #e5e7eb);\n background-color: var(--surface-card, #ffffff);\n background-image: linear-gradient(\n 45deg,\n transparent 50%,\n var(--text-tertiary, #6b7280) 50%\n ),\n linear-gradient(\n 135deg,\n var(--text-tertiary, #6b7280) 50%,\n transparent 50%\n );\n background-position:\n right var(--spacing-3, 0.75rem) center,\n right var(--spacing-2, 0.5rem) center;\n background-size:\n 5px 5px,\n 5px 5px;\n background-repeat: no-repeat;\n color: var(--text-primary, #111827);\n font-size: var(--font-size-sm, 0.875rem);\n line-height: var(--line-height-tight, 1.25);\n cursor: pointer;\n transition:\n border-color var(--motion-duration-fast, 150ms)\n var(--motion-easing-standard, ease),\n box-shadow var(--motion-duration-fast, 150ms)\n var(--motion-easing-standard, ease);\n}\n\n.pageSizeSelect:hover {\n border-color: var(--border-strong, #d1d5db);\n}\n\n.pageSizeSelect:focus-visible {\n outline: var(--focus-ring-width, 2px) solid\n var(--focus-ring-color, #2563eb);\n outline-offset: var(--focus-ring-offset, 2px);\n}\n\n.pageNav {\n display: inline-flex;\n align-items: center;\n gap: var(--spacing-2, 0.5rem);\n}\n\n.pageCounter {\n font-size: var(--font-size-sm, 0.875rem);\n color: var(--text-secondary, #6b7280);\n white-space: nowrap;\n}\n\n/* Group-head row — full-width separator inside the table body. The colSpan\n cell carries the visual; the inner span owns the sticky positioning so it\n pins to the top of the scroll container as data rows scroll beneath. */\n.groupRow {\n
|
|
2159
|
+
"content": "/* DataTable root — inline-size container for responsive collapse */\n.root {\n display: flex;\n flex-direction: column;\n width: 100%;\n min-width: 0;\n gap: var(--spacing-3, 0.75rem);\n container-type: inline-size;\n container-name: data-table;\n color: var(--text-primary, #111827);\n}\n\n/* Sticky header wrapper — toggled when stickyHeader prop is true */\n.stickyHeader {\n position: sticky;\n top: 0;\n z-index: 1;\n background: var(--surface-card, #ffffff);\n}\n\n/* Sort button — renders inside the <th> */\n.sortButton {\n display: inline-flex;\n align-items: center;\n gap: var(--spacing-1, 0.25rem);\n padding: 0;\n margin: 0;\n background: transparent;\n border: 0;\n color: inherit;\n font: inherit;\n font-weight: var(--font-weight-semibold, 600);\n cursor: pointer;\n line-height: var(--line-height-tight, 1.25);\n border-radius: var(--radius-sm, 0.25rem);\n transition:\n 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.sortButton:hover {\n color: var(--text-primary, #111827);\n}\n\n.sortButton:focus-visible {\n outline: var(--focus-ring-width, 2px) solid\n var(--focus-ring-color, #2563eb);\n outline-offset: var(--focus-ring-offset, 2px);\n}\n\n.sortLabel {\n display: inline-block;\n}\n\n.sortIcon {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: var(--spacing-4, 1rem);\n height: var(--spacing-4, 1rem);\n color: var(--text-tertiary, #6b7280);\n flex-shrink: 0;\n}\n\n.sortIcon svg {\n width: 100%;\n height: 100%;\n}\n\n/* Skeleton cell fills the cell width */\n.skeletonCell {\n width: 100%;\n height: var(--spacing-4, 1rem);\n}\n\n/* Empty-row cell centers the EmptyState slot */\n.emptyCell {\n padding: var(--spacing-6, 1.5rem) var(--spacing-4, 1rem);\n text-align: center;\n}\n\n/* Footer — info on the left, controls on the right */\n.footer {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: var(--spacing-4, 1rem);\n padding: var(--spacing-2, 0.5rem) var(--spacing-1, 0.25rem);\n min-width: 0;\n flex-wrap: wrap;\n}\n\n.footerInfo {\n font-size: var(--font-size-sm, 0.875rem);\n color: var(--text-secondary, #6b7280);\n min-width: 0;\n}\n\n.footerControls {\n display: flex;\n align-items: center;\n gap: var(--spacing-4, 1rem);\n flex-wrap: wrap;\n}\n\n.pageSizeLabel {\n display: inline-flex;\n align-items: center;\n gap: var(--spacing-2, 0.5rem);\n font-size: var(--font-size-sm, 0.875rem);\n color: var(--text-secondary, #6b7280);\n}\n\n.pageSizeLabelText {\n white-space: nowrap;\n}\n\n/* Native <select> styled to match Visor input controls */\n.pageSizeSelect {\n appearance: none;\n -webkit-appearance: none;\n -moz-appearance: none;\n height: var(--spacing-8, 2rem);\n padding: 0 var(--spacing-6, 1.5rem) 0 var(--spacing-2, 0.5rem);\n border-radius: var(--radius-md, 0.375rem);\n border: 1px solid var(--border-default, #e5e7eb);\n background-color: var(--surface-card, #ffffff);\n background-image: linear-gradient(\n 45deg,\n transparent 50%,\n var(--text-tertiary, #6b7280) 50%\n ),\n linear-gradient(\n 135deg,\n var(--text-tertiary, #6b7280) 50%,\n transparent 50%\n );\n background-position:\n right var(--spacing-3, 0.75rem) center,\n right var(--spacing-2, 0.5rem) center;\n background-size:\n 5px 5px,\n 5px 5px;\n background-repeat: no-repeat;\n color: var(--text-primary, #111827);\n font-size: var(--font-size-sm, 0.875rem);\n line-height: var(--line-height-tight, 1.25);\n cursor: pointer;\n transition:\n border-color var(--motion-duration-fast, 150ms)\n var(--motion-easing-standard, ease),\n box-shadow var(--motion-duration-fast, 150ms)\n var(--motion-easing-standard, ease);\n}\n\n.pageSizeSelect:hover {\n border-color: var(--border-strong, #d1d5db);\n}\n\n.pageSizeSelect:focus-visible {\n outline: var(--focus-ring-width, 2px) solid\n var(--focus-ring-color, #2563eb);\n outline-offset: var(--focus-ring-offset, 2px);\n}\n\n.pageNav {\n display: inline-flex;\n align-items: center;\n gap: var(--spacing-2, 0.5rem);\n}\n\n.pageCounter {\n font-size: var(--font-size-sm, 0.875rem);\n color: var(--text-secondary, #6b7280);\n white-space: nowrap;\n}\n\n/* Data row — base layer for the per-row data-state / data-tone / data-clickable\n variants below. The selectors are scoped to `.dataRow` so the tone/selected\n tints never bleed onto group-head rows or skeleton/empty rows. */\n.dataRow {\n transition: background-color var(--motion-duration-fast, 150ms)\n var(--motion-easing-standard, ease);\n}\n\n/* Selected row — wires the latent `data-state=\"selected\"` attribute (already\n emitted by TanStack on selected rows) to a subtle accent fill. Fixes a\n latent bug where selection had no visual feedback. */\n.dataRow[data-state=\"selected\"] {\n background-color: var(\n --surface-selected,\n var(--surface-interactive-active, #e5e7eb)\n );\n}\n\n/* Per-row tone tints — semantic background hints for editorial status.\n Mirrors the tone token mapping used by StatusBadge / StatusDot so a\n row tinted \"live\" reads consistently with a \"live\" badge inline.\n `scheduled` and `draft` intentionally have no tint — they render on\n the default surface to keep visual signal focused on actionable rows. */\n.dataRow[data-tone=\"live\"],\n.dataRow[data-tone=\"sold\"] {\n background-color: var(--surface-success-subtle, #ecfdf5);\n}\n\n.dataRow[data-tone=\"warn\"] {\n background-color: var(--surface-warning-subtle, #fffbeb);\n}\n\n.dataRow[data-tone=\"danger\"] {\n background-color: var(--surface-error-subtle, #fef2f2);\n}\n\n.dataRow[data-tone=\"info\"] {\n background-color: var(--surface-info-subtle, #eff6ff);\n}\n\n/* Selected state should win over tone tint — selection is a stronger\n signal than editorial status. */\n.dataRow[data-state=\"selected\"][data-tone] {\n background-color: var(\n --surface-selected,\n var(--surface-interactive-active, #e5e7eb)\n );\n}\n\n/* Clickable rows — opt-in affordance when `onRowClick` is supplied. */\n.dataRow[data-clickable=\"true\"] {\n cursor: pointer;\n}\n\n.dataRow[data-clickable=\"true\"]:hover {\n background-color: var(--surface-interactive-default, #f3f4f6);\n}\n\n.dataRow[data-clickable=\"true\"]:focus-visible {\n outline: var(--focus-ring-width, 2px) solid\n var(--focus-ring-color, #2563eb);\n outline-offset: calc(var(--focus-ring-offset, 2px) * -1);\n}\n\n/* Group-head row — full-width separator inside the table body. The colSpan\n cell carries the visual; the inner span owns the sticky positioning so it\n pins to the top of the scroll container as data rows scroll beneath. */\n.groupRow {\n background-color: transparent;\n cursor: default;\n}\n\n.groupRow:hover {\n background-color: transparent;\n}\n\n.groupCell {\n padding: 0;\n}\n\n.groupLabel {\n display: flex;\n align-items: center;\n gap: var(--spacing-2, 0.5rem);\n height: 28px;\n padding: 0 var(--spacing-4, 1rem);\n background: var(--surface-subtle, #f3f4f6);\n font-size: var(--font-size-xs, 0.6875rem);\n font-weight: var(--font-weight-medium, 500);\n letter-spacing: 0.14em;\n text-transform: uppercase;\n color: var(--text-tertiary, #6b7280);\n position: sticky;\n top: 0;\n z-index: 1;\n}\n\n.groupCount {\n font-size: var(--font-size-xs, 0.6875rem);\n color: var(--text-quaternary, #9ca3af);\n font-variant-numeric: tabular-nums;\n letter-spacing: 0;\n}\n\n/* Narrow containers — stack footer sections */\n@container data-table (max-width: 560px) {\n .footer {\n flex-direction: column;\n align-items: stretch;\n }\n\n .footerControls {\n justify-content: space-between;\n }\n}\n"
|
|
2113
2160
|
}
|
|
2114
2161
|
]
|
|
2115
2162
|
},
|
|
@@ -2207,12 +2254,61 @@
|
|
|
2207
2254
|
{
|
|
2208
2255
|
"path": "components/ui/page-header/page-header.tsx",
|
|
2209
2256
|
"type": "registry:ui",
|
|
2210
|
-
"content": "import * as React from \"react\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\nimport { cn } from \"../../../lib/utils\"\nimport styles from \"./page-header.module.css\"\n\nconst pageHeaderVariants = cva(styles.base, {\n variants: {\n size: {\n sm: styles.sizeSm,\n md: styles.sizeMd,\n lg: styles.sizeLg,\n },\n },\n defaultVariants: {\n size: \"md\",\n },\n})\n\ntype PageHeaderElement = \"header\" | \"section\" | \"div\"\ntype TitleElement = \"h1\" | \"h2\" | \"h3\"\n\nexport interface PageHeaderProps\n extends Omit<React.HTMLAttributes<HTMLElement>, \"title\">,\n VariantProps<typeof pageHeaderVariants> {\n /** Optional small uppercase label rendered above the title. */\n eyebrow?: React.ReactNode\n /** Page heading content. Rendered in the element given by `titleAs`. */\n title: React.ReactNode\n /** Optional supporting copy rendered below the title. */\n description?: React.ReactNode\n /** Optional ReactNode rendered above the title row (typically a Breadcrumb). */\n breadcrumb?: React.ReactNode\n /** Optional ReactNode rendered on the right side of the title row. */\n actions?: React.ReactNode\n /** Root element tag. Defaults to `header`. */\n as?: PageHeaderElement\n /** Heading level for the title. Defaults to `h1`. */\n titleAs?: TitleElement\n}\n\nconst PageHeader = React.forwardRef<HTMLElement, PageHeaderProps>(\n (\n {\n className,\n size,\n eyebrow,\n title,\n description,\n breadcrumb,\n actions,\n as = \"header\",\n titleAs = \"h1\",\n ...props\n },\n ref\n ) => {\n const Root = as as React.ElementType\n const Title = titleAs as React.ElementType\n\n return (\n <Root\n ref={ref}\n data-slot=\"page-header\"\n className={cn(pageHeaderVariants({ size }), className)}\n {...props}\n >\n {breadcrumb ? (\n <div data-slot=\"page-header-breadcrumb\" className={styles.breadcrumb}>\n {breadcrumb}\n </div>\n ) : null}\n <div data-slot=\"page-header-row\" className={styles.row}>\n <div data-slot=\"page-header-text\" className={styles.text}>\n {eyebrow ? (\n <div data-slot=\"page-header-eyebrow\" className={styles.eyebrow}>\n {eyebrow}\n </div>\n ) : null}\n <Title
|
|
2257
|
+
"content": "import * as React from \"react\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\nimport { cn } from \"../../../lib/utils\"\nimport styles from \"./page-header.module.css\"\n\nconst pageHeaderVariants = cva(styles.base, {\n variants: {\n size: {\n sm: styles.sizeSm,\n md: styles.sizeMd,\n lg: styles.sizeLg,\n },\n },\n defaultVariants: {\n size: \"md\",\n },\n})\n\ntype PageHeaderElement = \"header\" | \"section\" | \"div\"\ntype TitleElement = \"h1\" | \"h2\" | \"h3\"\n\n/** Token preset values for the `titleSize` prop. */\nconst TITLE_SIZE_TOKENS = [\"default\", \"marquee\"] as const\ntype TitleSizeToken = (typeof TITLE_SIZE_TOKENS)[number]\n\n/** Token preset values for the `titleFamily` prop. */\nconst TITLE_FAMILY_TOKENS = [\"heading\", \"display\"] as const\ntype TitleFamilyToken = (typeof TITLE_FAMILY_TOKENS)[number]\n\nfunction isTitleSizeToken(value: string): value is TitleSizeToken {\n return (TITLE_SIZE_TOKENS as readonly string[]).includes(value)\n}\n\nfunction isTitleFamilyToken(value: string): value is TitleFamilyToken {\n return (TITLE_FAMILY_TOKENS as readonly string[]).includes(value)\n}\n\nexport interface PageHeaderProps\n extends Omit<React.HTMLAttributes<HTMLElement>, \"title\">,\n VariantProps<typeof pageHeaderVariants> {\n /** Optional small uppercase label rendered above the title. */\n eyebrow?: React.ReactNode\n /** Page heading content. Rendered in the element given by `titleAs`. */\n title: React.ReactNode\n /** Optional supporting copy rendered below the title. */\n description?: React.ReactNode\n /** Optional ReactNode rendered above the title row (typically a Breadcrumb). */\n breadcrumb?: React.ReactNode\n /** Optional ReactNode rendered on the right side of the title row. */\n actions?: React.ReactNode\n /** Root element tag. Defaults to `header`. */\n as?: PageHeaderElement\n /** Heading level for the title. Defaults to `h1`. */\n titleAs?: TitleElement\n /**\n * Title font-size override. Token presets (`\"default\" | \"marquee\"`) map to\n * `data-title-size` attributes; any other string is forwarded as a raw CSS\n * length on the `--page-header-title-size` custom property.\n */\n titleSize?: \"default\" | \"marquee\" | (string & {})\n /**\n * Title font-family override. Token presets (`\"heading\" | \"display\"`) map\n * to `data-title-family` attributes; any other string is forwarded as a\n * raw CSS family on the `--page-header-title-family` custom property.\n */\n titleFamily?: \"heading\" | \"display\" | (string & {})\n}\n\nconst PageHeader = React.forwardRef<HTMLElement, PageHeaderProps>(\n (\n {\n className,\n size,\n eyebrow,\n title,\n description,\n breadcrumb,\n actions,\n as = \"header\",\n titleAs = \"h1\",\n titleSize,\n titleFamily,\n ...props\n },\n ref\n ) => {\n const Root = as as React.ElementType\n const Title = titleAs as React.ElementType\n\n const titleSizeToken =\n typeof titleSize === \"string\" && isTitleSizeToken(titleSize)\n ? titleSize\n : undefined\n const titleFamilyToken =\n typeof titleFamily === \"string\" && isTitleFamilyToken(titleFamily)\n ? titleFamily\n : undefined\n\n const rawTitleSize =\n typeof titleSize === \"string\" && !titleSizeToken ? titleSize : undefined\n const rawTitleFamily =\n typeof titleFamily === \"string\" && !titleFamilyToken\n ? titleFamily\n : undefined\n\n const titleStyle: React.CSSProperties | undefined =\n rawTitleSize || rawTitleFamily\n ? {\n ...(rawTitleSize\n ? ({\n \"--page-header-title-size\": rawTitleSize,\n } as React.CSSProperties)\n : null),\n ...(rawTitleFamily\n ? ({\n \"--page-header-title-family\": rawTitleFamily,\n } as React.CSSProperties)\n : null),\n }\n : undefined\n\n // When a raw string is supplied, switch the title into the \"marquee\" /\n // \"display\" rules that consume the custom property so the override\n // actually takes effect. Token values map directly to data-attributes.\n const resolvedTitleSizeAttr =\n titleSizeToken ?? (rawTitleSize ? \"marquee\" : undefined)\n const resolvedTitleFamilyAttr =\n titleFamilyToken ?? (rawTitleFamily ? \"display\" : undefined)\n\n return (\n <Root\n ref={ref}\n data-slot=\"page-header\"\n className={cn(pageHeaderVariants({ size }), className)}\n {...props}\n >\n {breadcrumb ? (\n <div data-slot=\"page-header-breadcrumb\" className={styles.breadcrumb}>\n {breadcrumb}\n </div>\n ) : null}\n <div data-slot=\"page-header-row\" className={styles.row}>\n <div data-slot=\"page-header-text\" className={styles.text}>\n {eyebrow ? (\n <div data-slot=\"page-header-eyebrow\" className={styles.eyebrow}>\n {eyebrow}\n </div>\n ) : null}\n <Title\n data-slot=\"page-header-title\"\n data-title-size={resolvedTitleSizeAttr}\n data-title-family={resolvedTitleFamilyAttr}\n className={styles.title}\n style={titleStyle}\n >\n {title}\n </Title>\n {description ? (\n <p\n data-slot=\"page-header-description\"\n className={styles.description}\n >\n {description}\n </p>\n ) : null}\n </div>\n {actions ? (\n <div data-slot=\"page-header-actions\" className={styles.actions}>\n {actions}\n </div>\n ) : null}\n </div>\n </Root>\n )\n }\n)\nPageHeader.displayName = \"PageHeader\"\n\nexport { PageHeader, pageHeaderVariants }\n"
|
|
2211
2258
|
},
|
|
2212
2259
|
{
|
|
2213
2260
|
"path": "components/ui/page-header/page-header.module.css",
|
|
2214
2261
|
"type": "registry:ui",
|
|
2215
|
-
"content": "/* Page Header base: container query wrapper */\n.base {\n display: flex;\n flex-direction: column;\n width: 100%;\n min-width: 0;\n gap: var(--spacing-4, 1rem);\n container-type: inline-size;\n container-name: page-header;\n color: var(--text-primary, #111827);\n}\n\n/* Size variants control vertical rhythm */\n.sizeSm {\n gap: var(--spacing-3, 0.75rem);\n}\n\n.sizeMd {\n gap: var(--spacing-4, 1rem);\n}\n\n.sizeLg {\n gap: var(--spacing-5, 1.25rem);\n}\n\n/* Breadcrumb slot sits above the title row */\n.breadcrumb {\n display: flex;\n align-items: center;\n min-width: 0;\n}\n\n/* Main row: text block on the left, actions on the right */\n.row {\n display: flex;\n align-items: flex-end;\n justify-content: space-between;\n gap: var(--spacing-6, 1.5rem);\n min-width: 0;\n}\n\n/* Text block: eyebrow, title, description stacked */\n.text {\n display: flex;\n flex-direction: column;\n gap: var(--spacing-2, 0.5rem);\n min-width: 0;\n flex: 1 1 auto;\n}\n\n.sizeSm .text {\n gap: var(--spacing-1, 0.25rem);\n}\n\n.sizeLg .text {\n gap: var(--spacing-3, 0.75rem);\n}\n\n/* Eyebrow: small uppercase label above the title */\n.eyebrow {\n font-size: var(--font-size-xs, 0.75rem);\n font-weight: var(--font-weight-semibold, 600);\n letter-spacing: var(--letter-spacing-wide, 0.05em);\n text-transform: uppercase;\n color: var(--text-tertiary, #6b7280);\n line-height: var(--line-height-tight, 1.25);\n}\n\n/* Title */\n.title {\n margin: 0;\n font-family: var(--font-family-heading, inherit);\n font-size: var(--font-size-2xl, 1.5rem);\n font-weight: var(--font-weight-semibold, 600);\n line-height: var(--line-height-tight, 1.2);\n color: var(--text-primary, #111827);\n letter-spacing: var(--letter-spacing-tight, -0.01em);\n}\n\n.sizeSm .title {\n font-size: var(--font-size-xl, 1.25rem);\n}\n\n.sizeLg .title {\n font-size: var(--font-size-3xl, 1.875rem);\n}\n\n/* Description */\n.description {\n margin: 0;\n font-size: var(--font-size-sm, 0.875rem);\n line-height: var(--line-height-relaxed, 1.6);\n color: var(--text-secondary, #6b7280);\n max-width: 65ch;\n}\n\n.sizeLg .description {\n font-size: var(--font-size-base, 1rem);\n}\n\n/* Actions slot: cluster on the right */\n.actions {\n display: flex;\n align-items: center;\n gap: var(--spacing-2, 0.5rem);\n flex: 0 0 auto;\n flex-wrap: wrap;\n justify-content: flex-end;\n}\n\n/* Container query: stack to column below a small container width */\n@container page-header (max-width: 480px) {\n .row {\n flex-direction: column;\n align-items: flex-start;\n gap: var(--spacing-4, 1rem);\n }\n\n .actions {\n width: 100%;\n justify-content: flex-start;\n }\n}\n"
|
|
2262
|
+
"content": "/* Page Header base: container query wrapper */\n.base {\n /* Override hooks for title typography. Themes (or consumers via a wrapping\n className) can set these to retune the marquee title without forking the\n component. Props are sugar over the same custom properties. */\n --page-header-title-size: 3.5rem;\n --page-header-title-family: var(--font-display, var(--font-family-heading, inherit));\n\n display: flex;\n flex-direction: column;\n width: 100%;\n min-width: 0;\n gap: var(--spacing-4, 1rem);\n container-type: inline-size;\n container-name: page-header;\n color: var(--text-primary, #111827);\n}\n\n/* Size variants control vertical rhythm */\n.sizeSm {\n gap: var(--spacing-3, 0.75rem);\n}\n\n.sizeMd {\n gap: var(--spacing-4, 1rem);\n}\n\n.sizeLg {\n gap: var(--spacing-5, 1.25rem);\n}\n\n/* Breadcrumb slot sits above the title row */\n.breadcrumb {\n display: flex;\n align-items: center;\n min-width: 0;\n}\n\n/* Main row: text block on the left, actions on the right */\n.row {\n display: flex;\n align-items: flex-end;\n justify-content: space-between;\n gap: var(--spacing-6, 1.5rem);\n min-width: 0;\n}\n\n/* Text block: eyebrow, title, description stacked */\n.text {\n display: flex;\n flex-direction: column;\n gap: var(--spacing-2, 0.5rem);\n min-width: 0;\n flex: 1 1 auto;\n}\n\n.sizeSm .text {\n gap: var(--spacing-1, 0.25rem);\n}\n\n.sizeLg .text {\n gap: var(--spacing-3, 0.75rem);\n}\n\n/* Eyebrow: small uppercase label above the title */\n.eyebrow {\n font-size: var(--font-size-xs, 0.75rem);\n font-weight: var(--font-weight-semibold, 600);\n letter-spacing: var(--letter-spacing-wide, 0.05em);\n text-transform: uppercase;\n color: var(--text-tertiary, #6b7280);\n line-height: var(--line-height-tight, 1.25);\n}\n\n/* Title */\n.title {\n margin: 0;\n font-family: var(--font-family-heading, inherit);\n font-size: var(--font-size-2xl, 1.5rem);\n font-weight: var(--font-weight-semibold, 600);\n line-height: var(--line-height-tight, 1.2);\n color: var(--text-primary, #111827);\n letter-spacing: var(--letter-spacing-tight, -0.01em);\n}\n\n.sizeSm .title {\n font-size: var(--font-size-xl, 1.25rem);\n}\n\n.sizeLg .title {\n font-size: var(--font-size-3xl, 1.875rem);\n}\n\n/* Title typography overrides (orthogonal to size variants).\n `data-title-size=\"marquee\"` pulls the title up to a hero scale via the\n `--page-header-title-size` custom property. Raw-string consumers pass an\n inline `--page-header-title-size` and rely on the same rule. */\n.title[data-title-size=\"marquee\"] {\n font-size: var(--page-header-title-size, 3.5rem);\n line-height: 1;\n letter-spacing: var(--letter-spacing-tight, -0.01em);\n}\n\n/* `data-title-family=\"display\"` switches to the display family with a\n graceful fallback to the heading family when no display font is themed. */\n.title[data-title-family=\"display\"] {\n font-family: var(--page-header-title-family, var(--font-display, var(--font-family-heading, inherit)));\n}\n\n/* Description */\n.description {\n margin: 0;\n font-size: var(--font-size-sm, 0.875rem);\n line-height: var(--line-height-relaxed, 1.6);\n color: var(--text-secondary, #6b7280);\n max-width: 65ch;\n}\n\n.sizeLg .description {\n font-size: var(--font-size-base, 1rem);\n}\n\n/* Actions slot: cluster on the right */\n.actions {\n display: flex;\n align-items: center;\n gap: var(--spacing-2, 0.5rem);\n flex: 0 0 auto;\n flex-wrap: wrap;\n justify-content: flex-end;\n}\n\n/* Container query: stack to column below a small container width */\n@container page-header (max-width: 480px) {\n .row {\n flex-direction: column;\n align-items: flex-start;\n gap: var(--spacing-4, 1rem);\n }\n\n .actions {\n width: 100%;\n justify-content: flex-start;\n }\n}\n"
|
|
2263
|
+
}
|
|
2264
|
+
]
|
|
2265
|
+
},
|
|
2266
|
+
{
|
|
2267
|
+
"name": "quick-actions",
|
|
2268
|
+
"type": "registry:ui",
|
|
2269
|
+
"description": "A vertical list of action rows pairing a left-aligned label with a right-aligned Kbd shortcut. Display-only by default; opt-in interactive mode via onActivate makes rows activatable buttons with click, Enter, and Space activation.",
|
|
2270
|
+
"category": "navigation",
|
|
2271
|
+
"dependencies": [
|
|
2272
|
+
"@loworbitstudio/visor-core"
|
|
2273
|
+
],
|
|
2274
|
+
"registryDependencies": [
|
|
2275
|
+
"utils",
|
|
2276
|
+
"kbd"
|
|
2277
|
+
],
|
|
2278
|
+
"files": [
|
|
2279
|
+
{
|
|
2280
|
+
"path": "components/ui/quick-actions/quick-actions.tsx",
|
|
2281
|
+
"type": "registry:ui",
|
|
2282
|
+
"content": "import * as React from \"react\"\nimport { cn } from \"../../../lib/utils\"\nimport { Kbd } from \"../kbd/kbd\"\nimport styles from \"./quick-actions.module.css\"\n\nexport interface QuickAction {\n id: string\n label: React.ReactNode\n /** Shortcut keys, forwarded to `<Kbd keys={...} />`. */\n keys: string[]\n}\n\nexport interface QuickActionsProps\n extends Omit<React.HTMLAttributes<HTMLUListElement>, \"onClick\"> {\n actions: QuickAction[]\n /**\n * When supplied, rows render as activatable buttons that fire this handler\n * on click, Enter, or Space. When omitted, rows render as plain list items\n * with no interactive affordance — the user reads the label and presses the\n * actual key on the keyboard.\n */\n onActivate?: (id: string) => void\n}\n\n/**\n * QuickActions — a vertical list of action rows pairing a left-aligned label\n * with a right-aligned `Kbd` shortcut. Sized for dashboard side-rail \"quick\"\n * panels and command-palette previews.\n *\n * Display-only by default: rows render as `<li>` and the keys are presented as\n * semantic `<kbd>` chrome. Supply `onActivate` to opt rows into interactive\n * mode — they become `role=\"button\"` with `tabIndex={0}` and respond to click,\n * Enter, and Space.\n */\nconst QuickActions = React.forwardRef<HTMLUListElement, QuickActionsProps>(\n ({ className, actions, onActivate, ...props }, ref) => {\n const interactive = typeof onActivate === \"function\"\n\n return (\n <ul\n ref={ref}\n data-slot=\"quick-actions\"\n role={interactive ? \"group\" : \"list\"}\n className={cn(styles.root, className)}\n {...props}\n >\n {actions.map((action) => {\n if (interactive) {\n const handleClick = () => onActivate(action.id)\n const handleKeyDown = (\n event: React.KeyboardEvent<HTMLLIElement>\n ) => {\n if (event.key === \"Enter\" || event.key === \" \") {\n event.preventDefault()\n onActivate(action.id)\n }\n }\n\n return (\n <li\n key={action.id}\n data-slot=\"quick-actions-row\"\n data-interactive=\"true\"\n role=\"button\"\n tabIndex={0}\n className={styles.row}\n onClick={handleClick}\n onKeyDown={handleKeyDown}\n >\n <span\n data-slot=\"quick-actions-label\"\n className={styles.label}\n >\n {action.label}\n </span>\n <span data-slot=\"quick-actions-keys\" className={styles.keys}>\n <Kbd keys={action.keys} size=\"sm\" />\n </span>\n </li>\n )\n }\n\n return (\n <li\n key={action.id}\n data-slot=\"quick-actions-row\"\n className={styles.row}\n >\n <span data-slot=\"quick-actions-label\" className={styles.label}>\n {action.label}\n </span>\n <span data-slot=\"quick-actions-keys\" className={styles.keys}>\n <Kbd keys={action.keys} size=\"sm\" />\n </span>\n </li>\n )\n })}\n </ul>\n )\n }\n)\nQuickActions.displayName = \"QuickActions\"\n\nexport { QuickActions }\n"
|
|
2283
|
+
},
|
|
2284
|
+
{
|
|
2285
|
+
"path": "components/ui/quick-actions/quick-actions.module.css",
|
|
2286
|
+
"type": "registry:ui",
|
|
2287
|
+
"content": "/* QuickActions — vertical list of label + Kbd rows.\n * Ported from admin-v7-r3 reference: 6px gap, 12-16px padding,\n * 13-14px label sized off --font-size-sm, --text-secondary color.\n *\n * No --spacing-1.5 token exists, so the 6px gap uses\n * calc(--spacing-1 + --spacing-1/2) — mirrors the pattern used in kbd. */\n.root {\n display: flex;\n flex-direction: column;\n gap: calc(var(--spacing-1, 0.25rem) + var(--spacing-1, 0.25rem) / 2);\n padding: var(--spacing-3, 0.75rem) var(--spacing-4, 1rem);\n background-color: var(--surface-card, #ffffff);\n margin: 0;\n list-style: none;\n}\n\n.row {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: var(--spacing-3, 0.75rem);\n font-size: var(--font-size-sm, 0.875rem);\n line-height: 1.25;\n border-radius: var(--radius-sm, 0.25rem);\n}\n\n.label {\n color: var(--text-secondary, #6b7280);\n min-width: 0;\n}\n\n.keys {\n display: inline-flex;\n align-items: center;\n flex-shrink: 0;\n}\n\n/* Interactive mode — rows opt in via the onActivate prop, which adds\n * data-interactive=\"true\" and role=\"button\". Adds hover/focus affordances\n * and a tap target boost so a click feels like a button. */\n.row[data-interactive=\"true\"] {\n cursor: pointer;\n padding: var(--spacing-1, 0.25rem) var(--spacing-2, 0.5rem);\n margin: calc(-1 * var(--spacing-1, 0.25rem))\n calc(-1 * var(--spacing-2, 0.5rem));\n transition:\n background-color var(--motion-duration-150, 150ms)\n var(--motion-easing-default, ease-in-out),\n color var(--motion-duration-150, 150ms)\n var(--motion-easing-default, ease-in-out);\n}\n\n@media (hover: hover) {\n .row[data-interactive=\"true\"]:hover {\n background-color: var(--surface-muted, #f3f4f6);\n }\n\n .row[data-interactive=\"true\"]:hover .label {\n color: var(--text-primary, #111827);\n }\n}\n\n.row[data-interactive=\"true\"]: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"
|
|
2288
|
+
}
|
|
2289
|
+
]
|
|
2290
|
+
},
|
|
2291
|
+
{
|
|
2292
|
+
"name": "section-header",
|
|
2293
|
+
"type": "registry:ui",
|
|
2294
|
+
"description": "A compact section-divider primitive with an uppercase title and optional right-aligned meta label. Sized for in-page content sectioning, distinct from the page-level PageHeader.",
|
|
2295
|
+
"category": "navigation",
|
|
2296
|
+
"dependencies": [
|
|
2297
|
+
"@loworbitstudio/visor-core"
|
|
2298
|
+
],
|
|
2299
|
+
"registryDependencies": [
|
|
2300
|
+
"utils"
|
|
2301
|
+
],
|
|
2302
|
+
"files": [
|
|
2303
|
+
{
|
|
2304
|
+
"path": "components/ui/section-header/section-header.tsx",
|
|
2305
|
+
"type": "registry:ui",
|
|
2306
|
+
"content": "import * as React from \"react\"\nimport { cn } from \"../../../lib/utils\"\nimport styles from \"./section-header.module.css\"\n\ntype SectionHeaderElement = \"header\" | \"div\" | \"section\"\n\nexport interface SectionHeaderProps\n extends Omit<React.HTMLAttributes<HTMLElement>, \"title\"> {\n /** Uppercase tracking label rendered on the left. */\n title: React.ReactNode\n /** Optional right-aligned meta — count, timestamp, status. */\n meta?: React.ReactNode\n /** Root element tag. Defaults to `header`. */\n as?: SectionHeaderElement\n}\n\nconst SectionHeader = React.forwardRef<HTMLElement, SectionHeaderProps>(\n ({ className, title, meta, as = \"header\", ...props }, ref) => {\n const Root = as as React.ElementType\n\n return (\n <Root\n ref={ref}\n data-slot=\"section-header\"\n className={cn(styles.root, className)}\n {...props}\n >\n <span data-slot=\"section-header-title\" className={styles.title}>\n {title}\n </span>\n {meta ? (\n <span data-slot=\"section-header-meta\" className={styles.meta}>\n {meta}\n </span>\n ) : null}\n </Root>\n )\n }\n)\nSectionHeader.displayName = \"SectionHeader\"\n\nexport { SectionHeader }\n"
|
|
2307
|
+
},
|
|
2308
|
+
{
|
|
2309
|
+
"path": "components/ui/section-header/section-header.module.css",
|
|
2310
|
+
"type": "registry:ui",
|
|
2311
|
+
"content": "/* Section divider header — 36px row, surface-subtle background.\n Sits between PageHeader (page-level hero) and Heading (in-content h2/h3). */\n.root {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: var(--spacing-3, 0.75rem);\n height: 36px;\n padding: 0 var(--spacing-4, 1rem);\n background: var(--surface-subtle, #f9fafb);\n}\n\n/* Left: uppercase eyebrow-style label. */\n.title {\n font-size: var(--font-size-xs, 0.6875rem);\n font-weight: var(--font-weight-medium, 500);\n letter-spacing: 0.14em;\n text-transform: uppercase;\n color: var(--text-tertiary, #6b7280);\n min-width: 0;\n}\n\n/* Right: optional meta — count, timestamp, status. */\n.meta {\n font-size: var(--font-size-sm, 0.8125rem);\n color: var(--text-tertiary, #6b7280);\n font-variant-numeric: tabular-nums;\n flex: 0 0 auto;\n}\n"
|
|
2216
2312
|
}
|
|
2217
2313
|
]
|
|
2218
2314
|
},
|
|
@@ -2232,12 +2328,12 @@
|
|
|
2232
2328
|
{
|
|
2233
2329
|
"path": "components/ui/stat-card/stat-card.tsx",
|
|
2234
2330
|
"type": "registry:ui",
|
|
2235
|
-
"content": "import * as React from \"react\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\nimport { cn } from \"../../../lib/utils\"\nimport styles from \"./stat-card.module.css\"\n\nconst statCardVariants = cva(styles.base, {\n variants: {\n variant: {\n default: styles.variantDefault,\n highlight: styles.variantHighlight,\n },\n size: {\n sm: styles.sizeSm,\n md: styles.sizeMd,\n },\n },\n defaultVariants: {\n variant: \"default\",\n size: \"md\",\n },\n})\n\nexport type StatCardDeltaDirection = \"up\" | \"down\" | \"flat\"\n\nexport interface StatCardDelta {\n /** Display value, e.g. \"+12.4%\" or \"-$2.1K\". */\n value: React.ReactNode\n /** Semantic direction of the change. Drives color and glyph. */\n direction: StatCardDeltaDirection\n /** Optional context label, e.g. \"vs last month\". Visible and announced. */\n label?: string\n}\n\ntype StatCardElement = \"article\" | \"section\" | \"div\"\n\nexport interface StatCardProps\n extends React.HTMLAttributes<HTMLElement>,\n VariantProps<typeof statCardVariants> {\n /** Small uppercase label describing the metric, e.g. \"Total Revenue\". */\n label: React.ReactNode\n /** Prominent metric value, e.g. \"$48,120\". */\n value: React.ReactNode\n /** Optional change indicator. */\n delta?: StatCardDelta\n /** Optional slot for a sparkline, chart, or icon. */\n trend?: React.ReactNode\n /** Optional sublabel or link rendered beneath the value. */\n footer?: React.ReactNode\n /** Root element tag. Defaults to `article` for landmark semantics. */\n as?: StatCardElement\n /** Typography scale for the value. \"hero\" = marquee display treatment. Defaults to \"default\". */\n valueAs?: \"default\" | \"hero\" | \"compact\"\n /** Additional class names forwarded to the value element. */\n valueClassName?: string\n}\n\nconst DELTA_GLYPH: Record<StatCardDeltaDirection, string> = {\n up: \"\\u2191\",\n down: \"\\u2193\",\n flat: \"\\u2192\",\n}\n\nconst DELTA_WORD: Record<StatCardDeltaDirection, string> = {\n up: \"up\",\n down: \"down\",\n flat: \"flat\",\n}\n\nconst StatCard = React.forwardRef<HTMLElement, StatCardProps>(\n (\n {\n className,\n variant,\n size,\n label,\n value,\n delta,\n trend,\n footer,\n as = \"article\",\n valueAs,\n valueClassName,\n ...props\n },\n ref\n ) => {\n const Root = as as React.ElementType\n\n return (\n <Root\n ref={ref}\n data-slot=\"stat-card\"\n data-variant={variant ?? \"default\"}\n data-size={size ?? \"md\"}\n className={cn(statCardVariants({ variant, size }), className)}\n {...props}\n >\n <div data-slot=\"stat-card-header\" className={styles.header}>\n <p data-slot=\"stat-card-label\" className={styles.label}>\n {label}\n </p>\n {trend ? (\n <div\n data-slot=\"stat-card-trend\"\n className={styles.trend}\n aria-hidden=\"true\"\n >\n {trend}\n </div>\n ) : null}\n </div>\n\n <p\n data-slot=\"stat-card-value\"\n data-value-as={valueAs}\n className={cn(styles.value, valueClassName)}\n >\n {value}\n </p>\n\n {delta ? (\n <div\n data-slot=\"stat-card-delta\"\n data-direction={delta.direction}\n className={styles.delta}\n >\n <span className={styles.deltaGlyph} aria-hidden=\"true\">\n {DELTA_GLYPH[delta.direction]}\n </span>\n <span className={styles.deltaValue}>{delta.value}</span>\n {delta.label ? (\n <span className={styles.deltaLabel}>{delta.label}</span>\n ) : null}\n <span className={styles.srOnly}>\n {DELTA_WORD[delta.direction]}\n {delta.label ? ` ${delta.label}` : \"\"}\n </span>\n </div>\n ) : null}\n\n {footer ? (\n <div data-slot=\"stat-card-footer\" className={styles.footer}>\n {footer}\n </div>\n ) : null}\n </Root>\n )\n }\n)\nStatCard.displayName = \"StatCard\"\n\nexport { StatCard, statCardVariants }\n"
|
|
2331
|
+
"content": "import * as React from \"react\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\nimport { cn } from \"../../../lib/utils\"\nimport styles from \"./stat-card.module.css\"\n\nconst statCardVariants = cva(styles.base, {\n variants: {\n variant: {\n default: styles.variantDefault,\n highlight: styles.variantHighlight,\n },\n size: {\n sm: styles.sizeSm,\n md: styles.sizeMd,\n },\n },\n defaultVariants: {\n variant: \"default\",\n size: \"md\",\n },\n})\n\nexport type StatCardDeltaDirection = \"up\" | \"down\" | \"flat\"\n\nexport interface StatCardDelta {\n /** Display value, e.g. \"+12.4%\" or \"-$2.1K\". */\n value: React.ReactNode\n /** Semantic direction of the change. Drives color and glyph. */\n direction: StatCardDeltaDirection\n /** Optional context label, e.g. \"vs last month\". Visible and announced. */\n label?: string\n}\n\ntype StatCardElement = \"article\" | \"section\" | \"div\"\n\nexport interface StatCardProps\n extends React.HTMLAttributes<HTMLElement>,\n VariantProps<typeof statCardVariants> {\n /** Small uppercase label describing the metric, e.g. \"Total Revenue\". */\n label: React.ReactNode\n /** Prominent metric value, e.g. \"$48,120\". */\n value: React.ReactNode\n /** Optional change indicator. */\n delta?: StatCardDelta\n /** Optional slot for a sparkline, chart, or icon. */\n trend?: React.ReactNode\n /**\n * Where to render the `trend` slot.\n * - `\"footer\"` (default) — direct child of the card root, after value/delta and\n * before the `footer` slot. Full card width, padded above. Best for\n * Progress bars, full-width sparklines, and anything that competes with the\n * label for header space.\n * - `\"header\"` — inside the header row, right-aligned next to the label.\n * Legacy layout, useful for compact icons or thumbnail-sized sparklines.\n * Defaults to `\"footer\"`.\n */\n trendPosition?: \"header\" | \"footer\"\n /** Optional sublabel or link rendered beneath the value. */\n footer?: React.ReactNode\n /** Root element tag. Defaults to `article` for landmark semantics. */\n as?: StatCardElement\n /** Typography scale for the value. \"hero\" = marquee display treatment. Defaults to \"default\". */\n valueAs?: \"default\" | \"hero\" | \"compact\"\n /** Additional class names forwarded to the value element. */\n valueClassName?: string\n}\n\nconst DELTA_GLYPH: Record<StatCardDeltaDirection, string> = {\n up: \"\\u2191\",\n down: \"\\u2193\",\n flat: \"\\u2192\",\n}\n\nconst DELTA_WORD: Record<StatCardDeltaDirection, string> = {\n up: \"up\",\n down: \"down\",\n flat: \"flat\",\n}\n\nconst StatCard = React.forwardRef<HTMLElement, StatCardProps>(\n (\n {\n className,\n variant,\n size,\n label,\n value,\n delta,\n trend,\n trendPosition = \"footer\",\n footer,\n as = \"article\",\n valueAs,\n valueClassName,\n ...props\n },\n ref\n ) => {\n const Root = as as React.ElementType\n\n return (\n <Root\n ref={ref}\n data-slot=\"stat-card\"\n data-variant={variant ?? \"default\"}\n data-size={size ?? \"md\"}\n className={cn(statCardVariants({ variant, size }), className)}\n {...props}\n >\n <div data-slot=\"stat-card-header\" className={styles.header}>\n <p data-slot=\"stat-card-label\" className={styles.label}>\n {label}\n </p>\n {trend && trendPosition === \"header\" ? (\n <div\n data-slot=\"stat-card-trend\"\n data-trend-position=\"header\"\n className={styles.trend}\n aria-hidden=\"true\"\n >\n {trend}\n </div>\n ) : null}\n </div>\n\n <p\n data-slot=\"stat-card-value\"\n data-value-as={valueAs}\n className={cn(styles.value, valueClassName)}\n >\n {value}\n </p>\n\n {delta ? (\n <div\n data-slot=\"stat-card-delta\"\n data-direction={delta.direction}\n className={styles.delta}\n >\n <span className={styles.deltaGlyph} aria-hidden=\"true\">\n {DELTA_GLYPH[delta.direction]}\n </span>\n <span className={styles.deltaValue}>{delta.value}</span>\n {delta.label ? (\n <span className={styles.deltaLabel}>{delta.label}</span>\n ) : null}\n <span className={styles.srOnly}>\n {DELTA_WORD[delta.direction]}\n {delta.label ? ` ${delta.label}` : \"\"}\n </span>\n </div>\n ) : null}\n\n {trend && trendPosition === \"footer\" ? (\n <div\n data-slot=\"stat-card-trend\"\n data-trend-position=\"footer\"\n className={styles.trendFooter}\n aria-hidden=\"true\"\n >\n {trend}\n </div>\n ) : null}\n\n {footer ? (\n <div data-slot=\"stat-card-footer\" className={styles.footer}>\n {footer}\n </div>\n ) : null}\n </Root>\n )\n }\n)\nStatCard.displayName = \"StatCard\"\n\nexport { StatCard, statCardVariants }\n"
|
|
2236
2332
|
},
|
|
2237
2333
|
{
|
|
2238
2334
|
"path": "components/ui/stat-card/stat-card.module.css",
|
|
2239
2335
|
"type": "registry:ui",
|
|
2240
|
-
"content": "/* Stat Card base: self-contained dashboard metric card\n *\n * Sizing strategy: hug content by default. Place stat cards in a grid\n * (`grid-template-columns: repeat(N, 1fr)`) or a flex row with `flex: 1`\n * per child when uniform widths are needed across a dashboard. A single\n * stat card in a generic flex-row parent renders at its natural content\n * width — the big value token drives the minimum width.\n *\n * No inline-size containment here: inline-size containment prevents the\n * element's intrinsic width from depending on its children, which means\n * it collapses to 0 inside a flex-row parent with `flex: 0 1 auto`. Since\n * the card's responsive variants are already driven by explicit props,\n * the container query buys nothing and costs the natural sizing behavior.\n */\n.base {\n --stat-card-value-font: var(--font-display, var(--font-family-heading, inherit));\n --stat-card-value-size: var(--font-size-3xl, 1.875rem);\n\n display: flex;\n flex-direction: column;\n gap: var(--spacing-2, 0.5rem);\n padding: var(--spacing-5, 1.25rem);\n background-color: var(--surface-card, #ffffff);\n border: 1px solid var(--border-default, #e5e7eb);\n border-radius: var(--radius-lg, 0.75rem);\n color: var(--text-primary, #111827);\n box-shadow: var(--shadow-xs, 0 1px 2px 0 rgb(0 0 0 / 0.05));\n transition:\n box-shadow var(--motion-duration-150, 150ms)\n var(--motion-easing-standard, ease),\n border-color var(--motion-duration-150, 150ms)\n var(--motion-easing-standard, ease);\n}\n\n/* Default variant — just the base styles */\n.variantDefault {\n /* inherits base */\n}\n\n/* Highlight variant — accent-tinted surface with elevation\n *\n * Uses color-mix over the theme's brand accent (--interactive-primary-bg)\n * plus the theme's card surface, so the variant reads as \"featured / primary\"\n * regardless of whether the active theme is light or dark and regardless of\n * which brand color the theme ships. No hardcoded gradient — gradients\n * conflict with dark modes where lighter colors at the top look like flares.\n */\n.variantHighlight {\n background-color: color-mix(\n in srgb,\n var(--interactive-primary-bg, #2563eb) 6%,\n var(--surface-card, #ffffff)\n );\n border-color: color-mix(\n in srgb,\n var(--interactive-primary-bg, #2563eb) 40%,\n var(--border-default, #e5e7eb)\n );\n box-shadow:\n var(--shadow-md, 0 4px 6px -1px rgb(0 0 0 / 0.1)),\n 0 0 24px -8px\n color-mix(\n in srgb,\n var(--interactive-primary-bg, #2563eb) 25%,\n transparent\n );\n}\n\n/* Size md — default — no overrides; uses the base padding/gap/font-size */\n.sizeMd {\n /* inherits base */\n}\n\n/* Size sm — denser padding, smaller value */\n.sizeSm {\n padding: var(--spacing-4, 1rem);\n gap: var(--spacing-1, 0.25rem);\n}\n\n/* Header row: label on the left, trend slot on the right */\n.header {\n display: flex;\n align-items: flex-start;\n justify-content: space-between;\n gap: var(--spacing-3, 0.75rem);\n min-width: 0;\n}\n\n/* Label — small uppercase muted text */\n.label {\n margin: 0;\n font-size: var(--font-size-xs, 0.75rem);\n font-weight: var(--font-weight-semibold, 600);\n letter-spacing: var(--letter-spacing-wide, 0.05em);\n text-transform: uppercase;\n color: var(--text-tertiary, #6b7280);\n line-height: var(--line-height-tight, 1.25);\n min-width: 0;\n}\n\n/* Trend slot — sparkline / icon container */\n.trend {\n display: flex;\n align-items: center;\n justify-content: flex-end;\n color: var(--text-tertiary, #6b7280);\n flex: 0 0 auto;\n}\n\n/* Value — big numeric display */\n.value {\n margin: 0;\n font-family: var(--font-family-heading, inherit);\n font-size: var(--font-size-3xl, 1.875rem);\n font-weight: var(--font-weight-semibold, 600);\n line-height: var(--line-height-tight, 1.1);\n letter-spacing: var(--letter-spacing-tight, -0.02em);\n color: var(--text-primary, #111827);\n font-variant-numeric: tabular-nums;\n}\n\n/* valueAs=\"hero\" — marquee display treatment */\n.value[data-value-as=\"hero\"] {\n font-family: var(--stat-card-value-font);\n font-size: var(--stat-card-value-size, 3.5rem);\n font-weight: var(--font-weight-normal, 400);\n letter-spacing: 0;\n line-height: 1;\n}\n\n/* valueAs=\"compact\" — tighter than default (2xl) */\n.value[data-value-as=\"compact\"] {\n font-size: var(--font-size-2xl, 1.5rem);\n}\n\n.sizeSm .value {\n font-size: var(--font-size-2xl, 1.5rem);\n}\n\n.sizeSm .label {\n font-size: var(--font-size-xs, 0.75rem);\n}\n\n.variantHighlight .value {\n font-size: var(--font-size-4xl, 2.25rem);\n}\n\n/* Delta row — change indicator */\n.delta {\n display: inline-flex;\n align-items: center;\n gap: var(--spacing-1, 0.25rem);\n font-size: var(--font-size-sm, 0.875rem);\n font-weight: var(--font-weight-medium, 500);\n line-height: var(--line-height-tight, 1.25);\n color: var(--text-secondary, #6b7280);\n flex-wrap: wrap;\n}\n\n.delta[data-direction=\"up\"] {\n color: var(--text-success, #16a34a);\n}\n\n.delta[data-direction=\"down\"] {\n color: var(--text-danger, #dc2626);\n}\n\n.delta[data-direction=\"flat\"] {\n color: var(--text-tertiary, #6b7280);\n}\n\n.deltaGlyph {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n font-size: 1em;\n line-height: 1;\n}\n\n.deltaValue {\n font-variant-numeric: tabular-nums;\n}\n\n.deltaLabel {\n color: var(--text-tertiary, #6b7280);\n font-weight: var(--font-weight-regular, 400);\n margin-left: var(--spacing-1, 0.25rem);\n}\n\n/* Visually hidden but announced to screen readers */\n.srOnly {\n position: absolute;\n width: 1px;\n height: 1px;\n padding: 0;\n margin: -1px;\n overflow: hidden;\n clip: rect(0, 0, 0, 0);\n white-space: nowrap;\n border: 0;\n}\n\n/* Footer slot */\n.footer {\n margin-top: var(--spacing-1, 0.25rem);\n padding-top: var(--spacing-3, 0.75rem);\n border-top: 1px solid var(--border-default, #e5e7eb);\n font-size: var(--font-size-sm, 0.875rem);\n color: var(--text-secondary, #6b7280);\n line-height: var(--line-height-normal, 1.5);\n}\n\n.sizeSm .footer {\n padding-top: var(--spacing-2, 0.5rem);\n font-size: var(--font-size-xs, 0.75rem);\n}\n\n/* Respect reduced motion */\n@media (prefers-reduced-motion: reduce) {\n .base {\n transition: none;\n }\n}\n"
|
|
2336
|
+
"content": "/* Stat Card base: self-contained dashboard metric card\n *\n * Sizing strategy: hug content by default. Place stat cards in a grid\n * (`grid-template-columns: repeat(N, 1fr)`) or a flex row with `flex: 1`\n * per child when uniform widths are needed across a dashboard. A single\n * stat card in a generic flex-row parent renders at its natural content\n * width — the big value token drives the minimum width.\n *\n * No inline-size containment here: inline-size containment prevents the\n * element's intrinsic width from depending on its children, which means\n * it collapses to 0 inside a flex-row parent with `flex: 0 1 auto`. Since\n * the card's responsive variants are already driven by explicit props,\n * the container query buys nothing and costs the natural sizing behavior.\n */\n.base {\n --stat-card-value-font: var(--font-display, var(--font-family-heading, inherit));\n --stat-card-value-size: var(--font-size-3xl, 1.875rem);\n --stat-card-value-size-hero: var(--font-size-6xl, 3.5rem);\n\n display: flex;\n flex-direction: column;\n gap: var(--spacing-2, 0.5rem);\n padding: var(--spacing-5, 1.25rem);\n background-color: var(--surface-card, #ffffff);\n border: 1px solid var(--border-default, #e5e7eb);\n border-radius: var(--radius-lg, 0.75rem);\n color: var(--text-primary, #111827);\n box-shadow: var(--shadow-xs, 0 1px 2px 0 rgb(0 0 0 / 0.05));\n transition:\n box-shadow var(--motion-duration-150, 150ms)\n var(--motion-easing-standard, ease),\n border-color var(--motion-duration-150, 150ms)\n var(--motion-easing-standard, ease);\n}\n\n/* Default variant — just the base styles */\n.variantDefault {\n /* inherits base */\n}\n\n/* Highlight variant — accent-tinted surface with elevation\n *\n * Uses color-mix over the theme's brand accent (--interactive-primary-bg)\n * plus the theme's card surface, so the variant reads as \"featured / primary\"\n * regardless of whether the active theme is light or dark and regardless of\n * which brand color the theme ships. No hardcoded gradient — gradients\n * conflict with dark modes where lighter colors at the top look like flares.\n */\n.variantHighlight {\n background-color: color-mix(\n in srgb,\n var(--interactive-primary-bg, #2563eb) 6%,\n var(--surface-card, #ffffff)\n );\n border-color: color-mix(\n in srgb,\n var(--interactive-primary-bg, #2563eb) 40%,\n var(--border-default, #e5e7eb)\n );\n box-shadow:\n var(--shadow-md, 0 4px 6px -1px rgb(0 0 0 / 0.1)),\n 0 0 24px -8px\n color-mix(\n in srgb,\n var(--interactive-primary-bg, #2563eb) 25%,\n transparent\n );\n}\n\n/* Size md — default — no overrides; uses the base padding/gap/font-size */\n.sizeMd {\n /* inherits base */\n}\n\n/* Size sm — denser padding, smaller value */\n.sizeSm {\n padding: var(--spacing-4, 1rem);\n gap: var(--spacing-1, 0.25rem);\n}\n\n/* Header row: label on the left, trend slot on the right */\n.header {\n display: flex;\n align-items: flex-start;\n justify-content: space-between;\n gap: var(--spacing-3, 0.75rem);\n min-width: 0;\n}\n\n/* Label — small uppercase muted text */\n.label {\n margin: 0;\n font-size: var(--font-size-xs, 0.75rem);\n font-weight: var(--font-weight-semibold, 600);\n letter-spacing: var(--letter-spacing-wide, 0.05em);\n text-transform: uppercase;\n color: var(--text-tertiary, #6b7280);\n line-height: var(--line-height-tight, 1.25);\n min-width: 0;\n}\n\n/* Trend slot — header position (legacy): right-aligned sparkline / icon container */\n.trend {\n display: flex;\n align-items: center;\n justify-content: flex-end;\n color: var(--text-tertiary, #6b7280);\n flex: 0 0 auto;\n}\n\n/* Trend slot — footer position (default): full-width, padded above the value row.\n *\n * Rendered as a direct child of the card root after the value/delta and before the\n * footer slot. Full card width, no fixed flex sizing — so Progress bars, full-width\n * sparklines, and anything that competes with the label for header space have room\n * to breathe. The `--stat-card-trend-padding-top` hook lets consumers tune the gap\n * to the value row without touching internal CSS.\n */\n.trendFooter {\n display: flex;\n align-items: center;\n width: 100%;\n padding-top: var(--stat-card-trend-padding-top, var(--spacing-3, 0.75rem));\n color: var(--text-tertiary, #6b7280);\n}\n\n/* Value — big numeric display */\n.value {\n margin: 0;\n font-family: var(--font-family-heading, inherit);\n font-size: var(--font-size-3xl, 1.875rem);\n font-weight: var(--font-weight-semibold, 600);\n line-height: var(--line-height-tight, 1.1);\n letter-spacing: var(--letter-spacing-tight, -0.02em);\n color: var(--text-primary, #111827);\n font-variant-numeric: tabular-nums;\n}\n\n/* valueAs=\"hero\" — marquee display treatment */\n.value[data-value-as=\"hero\"] {\n font-family: var(--stat-card-value-font);\n font-size: var(--stat-card-value-size-hero, var(--stat-card-value-size, 3.5rem));\n font-weight: var(--font-weight-normal, 400);\n letter-spacing: 0;\n line-height: 1;\n}\n\n/* valueAs=\"compact\" — tighter than default (2xl) */\n.value[data-value-as=\"compact\"] {\n font-size: var(--font-size-2xl, 1.5rem);\n}\n\n.sizeSm .value {\n font-size: var(--font-size-2xl, 1.5rem);\n}\n\n.sizeSm .label {\n font-size: var(--font-size-xs, 0.75rem);\n}\n\n.variantHighlight .value {\n font-size: var(--font-size-4xl, 2.25rem);\n}\n\n/* Delta row — change indicator */\n.delta {\n display: inline-flex;\n align-items: center;\n gap: var(--spacing-1, 0.25rem);\n font-size: var(--font-size-sm, 0.875rem);\n font-weight: var(--font-weight-medium, 500);\n line-height: var(--line-height-tight, 1.25);\n color: var(--text-secondary, #6b7280);\n flex-wrap: wrap;\n}\n\n.delta[data-direction=\"up\"] {\n color: var(--text-success, #16a34a);\n}\n\n.delta[data-direction=\"down\"] {\n color: var(--text-danger, #dc2626);\n}\n\n.delta[data-direction=\"flat\"] {\n color: var(--text-tertiary, #6b7280);\n}\n\n.deltaGlyph {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n font-size: 1em;\n line-height: 1;\n}\n\n.deltaValue {\n font-variant-numeric: tabular-nums;\n}\n\n.deltaLabel {\n color: var(--text-tertiary, #6b7280);\n font-weight: var(--font-weight-regular, 400);\n margin-left: var(--spacing-1, 0.25rem);\n}\n\n/* Visually hidden but announced to screen readers */\n.srOnly {\n position: absolute;\n width: 1px;\n height: 1px;\n padding: 0;\n margin: -1px;\n overflow: hidden;\n clip: rect(0, 0, 0, 0);\n white-space: nowrap;\n border: 0;\n}\n\n/* Footer slot */\n.footer {\n margin-top: var(--spacing-1, 0.25rem);\n padding-top: var(--spacing-3, 0.75rem);\n border-top: 1px solid var(--border-default, #e5e7eb);\n font-size: var(--font-size-sm, 0.875rem);\n color: var(--text-secondary, #6b7280);\n line-height: var(--line-height-normal, 1.5);\n}\n\n.sizeSm .footer {\n padding-top: var(--spacing-2, 0.5rem);\n font-size: var(--font-size-xs, 0.75rem);\n}\n\n/* Respect reduced motion */\n@media (prefers-reduced-motion: reduce) {\n .base {\n transition: none;\n }\n}\n"
|
|
2241
2337
|
}
|
|
2242
2338
|
]
|
|
2243
2339
|
},
|
|
@@ -2282,7 +2378,7 @@
|
|
|
2282
2378
|
{
|
|
2283
2379
|
"path": "components/ui/status-badge/status-badge.tsx",
|
|
2284
2380
|
"type": "registry:ui",
|
|
2285
|
-
"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\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}\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}\n\nconst SUBTLE_VARIANT: Record<StatusColorGroup, BadgeVariant> = {\n success: \"success\",\n warning: \"warning\",\n destructive: \"destructive\",\n info: \"info\",\n neutral: \"secondary\",\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 // No `filled-secondary` exists on Badge — fall back to secondary.\n neutral: \"secondary\",\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"
|
|
2381
|
+
"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\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}\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/planned, visually grouped with draft → neutral\n // sold: positive completed outcome → success\n // draft: unpublished/muted → neutral\n live: \"success\",\n warn: \"warning\",\n scheduled: \"neutral\",\n sold: \"success\",\n draft: \"neutral\",\n}\n\nconst SUBTLE_VARIANT: Record<StatusColorGroup, BadgeVariant> = {\n success: \"success\",\n warning: \"warning\",\n destructive: \"destructive\",\n info: \"info\",\n neutral: \"secondary\",\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 // No `filled-secondary` exists on Badge — fall back to secondary.\n neutral: \"secondary\",\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"
|
|
2286
2382
|
},
|
|
2287
2383
|
{
|
|
2288
2384
|
"path": "components/ui/status-badge/status-badge.module.css",
|
|
@@ -2291,6 +2387,30 @@
|
|
|
2291
2387
|
}
|
|
2292
2388
|
]
|
|
2293
2389
|
},
|
|
2390
|
+
{
|
|
2391
|
+
"name": "status-dot",
|
|
2392
|
+
"type": "registry:ui",
|
|
2393
|
+
"description": "A 6×6px tone-tinted indicator dot. Five tones (mint, warn, muted, danger, info) resolve from Visor semantic tokens. Composes inside badges, table rows, activity feed items, and inline status text. Decorative by default; supplying aria-label flips it into a labeled image.",
|
|
2394
|
+
"category": "data-display",
|
|
2395
|
+
"dependencies": [
|
|
2396
|
+
"@loworbitstudio/visor-core"
|
|
2397
|
+
],
|
|
2398
|
+
"registryDependencies": [
|
|
2399
|
+
"utils"
|
|
2400
|
+
],
|
|
2401
|
+
"files": [
|
|
2402
|
+
{
|
|
2403
|
+
"path": "components/ui/status-dot/status-dot.tsx",
|
|
2404
|
+
"type": "registry:ui",
|
|
2405
|
+
"content": "import * as React from \"react\"\nimport { cn } from \"../../../lib/utils\"\nimport styles from \"./status-dot.module.css\"\n\nexport type StatusDotTone = \"mint\" | \"warn\" | \"muted\" | \"danger\" | \"info\"\n\nexport interface StatusDotProps\n extends React.HTMLAttributes<HTMLSpanElement> {\n /** Tone. Defaults to `\"muted\"`. */\n tone?: StatusDotTone\n /**\n * When supplied, the dot is announced as a labeled image instead of being\n * marked decorative. Use only when the dot stands alone — when it's\n * paired with adjacent status text, leave `aria-label` unset and let the\n * surrounding label carry the meaning.\n */\n \"aria-label\"?: string\n}\n\n/**\n * StatusDot — a 6×6px tone-tinted indicator dot.\n *\n * Composes inside `Badge`, `ActivityFeed` leading slots, table status cells,\n * and inline status text. Decorative by default (`aria-hidden=\"true\"`);\n * supplying `aria-label` flips it into a labeled `role=\"img\"` so screen\n * readers announce the standalone state.\n */\nconst StatusDot = React.forwardRef<HTMLSpanElement, StatusDotProps>(\n ({ className, tone = \"muted\", ...props }, ref) => {\n const hasLabel = props[\"aria-label\"] !== undefined\n return (\n <span\n ref={ref}\n data-slot=\"status-dot\"\n data-tone={tone}\n role={hasLabel ? \"img\" : undefined}\n aria-hidden={hasLabel ? undefined : true}\n className={cn(styles.dot, className)}\n {...props}\n />\n )\n }\n)\nStatusDot.displayName = \"StatusDot\"\n\nexport { StatusDot }\n"
|
|
2406
|
+
},
|
|
2407
|
+
{
|
|
2408
|
+
"path": "components/ui/status-dot/status-dot.module.css",
|
|
2409
|
+
"type": "registry:ui",
|
|
2410
|
+
"content": "/*\n * StatusDot — a 6×6px tone-tinted indicator dot.\n *\n * The dot mirrors StatusBadge's indicator CSS pattern (size, radius, fill\n * sourced from saturated surface tokens) but stands on its own as a\n * reusable primitive that composes inside Badge, ActivityFeed items, and\n * table status cells. Tone colors resolve from the same semantic tokens\n * StatusBadge uses for its leading indicator so the two read as one\n * coherent system across surfaces.\n */\n.dot {\n display: inline-block;\n width: 6px;\n height: 6px;\n flex-shrink: 0;\n border-radius: var(--radius-full, 9999px);\n}\n\n.dot[data-tone=\"mint\"] {\n background-color: var(--surface-success-default, #22c55e);\n}\n\n.dot[data-tone=\"warn\"] {\n background-color: var(--surface-warning-default, #f59e0b);\n}\n\n.dot[data-tone=\"muted\"] {\n background-color: var(--text-tertiary, #6b7280);\n}\n\n.dot[data-tone=\"danger\"] {\n background-color: var(--surface-error-default, #ef4444);\n}\n\n.dot[data-tone=\"info\"] {\n background-color: var(--surface-info-default, #0ea5e9);\n}\n"
|
|
2411
|
+
}
|
|
2412
|
+
]
|
|
2413
|
+
},
|
|
2294
2414
|
{
|
|
2295
2415
|
"name": "station-spectrum",
|
|
2296
2416
|
"type": "registry:ui",
|
|
@@ -3018,12 +3138,12 @@
|
|
|
3018
3138
|
{
|
|
3019
3139
|
"path": "blocks/admin-dashboard/admin-dashboard.tsx",
|
|
3020
3140
|
"type": "registry:block",
|
|
3021
|
-
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport { cn } from \"../../lib/utils\"\nimport {\n PageHeader,\n type PageHeaderProps,\n} from \"../../components/ui/page-header/page-header\"\nimport {\n StatCard,\n type StatCardDelta,\n type StatCardProps,\n} from \"../../components/ui/stat-card/stat-card\"\nimport {\n ActivityFeed,\n ActivityFeedItem,\n} from \"../../components/ui/activity-feed/activity-feed\"\nimport { EmptyState } from \"../../components/ui/empty-state/empty-state\"\nimport styles from \"./admin-dashboard.module.css\"\n\nexport interface AdminDashboardStat {\n id: string\n label: React.ReactNode\n value: React.ReactNode\n delta?: StatCardDelta\n trend?: React.ReactNode\n variant?: \"default\" | \"highlight\"\n size?: \"sm\" | \"md\"\n valueAs?: StatCardProps[\"valueAs\"]\n}\n\nexport interface AdminDashboardActivity {\n id: string\n leading?: React.ReactNode\n title: React.ReactNode\n description?: React.ReactNode\n actor?: React.ReactNode\n timestamp: React.ReactNode\n trailing?: React.ReactNode\n}\n\nexport interface AdminDashboardProps\n extends Omit<React.HTMLAttributes<HTMLDivElement>, \"title\"> {\n /** Page title rendered inside the PageHeader. */\n title: React.ReactNode\n /** Optional small uppercase label above the title. */\n eyebrow?: React.ReactNode\n /** Optional supporting copy rendered below the title. */\n description?: React.ReactNode\n /** Optional actions slot rendered on the right side of the header. */\n actions?: React.ReactNode\n\n /** Stat cards rendered in the responsive grid. */\n stats: AdminDashboardStat[]\n\n /** Heading rendered above the activity feed. Defaults to \"Recent activity\". */\n activityTitle?: React.ReactNode\n /** Activity events rendered in the feed. */\n activities: AdminDashboardActivity[]\n /** If provided, renders a \"View all\" link in the activity section header. */\n activityViewAllHref?: string\n /** Replaces the default EmptyState when `activities` is empty. */\n activityEmptyState?: React.ReactNode\n /** Variant forwarded to the underlying ActivityFeed. */\n activityVariant?: \"default\" | \"compact\" | \"timeline\"\n\n
|
|
3141
|
+
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport { cn } from \"../../lib/utils\"\nimport {\n PageHeader,\n type PageHeaderProps,\n} from \"../../components/ui/page-header/page-header\"\nimport {\n StatCard,\n type StatCardDelta,\n type StatCardProps,\n} from \"../../components/ui/stat-card/stat-card\"\nimport {\n ActivityFeed,\n ActivityFeedItem,\n} from \"../../components/ui/activity-feed/activity-feed\"\nimport { EmptyState } from \"../../components/ui/empty-state/empty-state\"\nimport styles from \"./admin-dashboard.module.css\"\n\nexport interface AdminDashboardStat {\n id: string\n label: React.ReactNode\n value: React.ReactNode\n delta?: StatCardDelta\n trend?: React.ReactNode\n variant?: \"default\" | \"highlight\"\n size?: \"sm\" | \"md\"\n valueAs?: StatCardProps[\"valueAs\"]\n}\n\nexport interface AdminDashboardActivity {\n id: string\n leading?: React.ReactNode\n title: React.ReactNode\n description?: React.ReactNode\n actor?: React.ReactNode\n timestamp: React.ReactNode\n trailing?: React.ReactNode\n}\n\nexport interface AdminDashboardProps\n extends Omit<React.HTMLAttributes<HTMLDivElement>, \"title\"> {\n /** Page title rendered inside the PageHeader. */\n title: React.ReactNode\n /** Optional small uppercase label above the title. */\n eyebrow?: React.ReactNode\n /** Optional supporting copy rendered below the title. */\n description?: React.ReactNode\n /** Optional actions slot rendered on the right side of the header. */\n actions?: React.ReactNode\n\n /** Stat cards rendered in the responsive grid. */\n stats: AdminDashboardStat[]\n\n /**\n * Body layout mode.\n * - `\"single\"` (default) renders the existing flow: stat grid → optional\n * `secondaryRegion` → activity section. Backwards-compatible — every\n * existing consumer renders unchanged when `layout` is omitted.\n * - `\"split\"` renders a 2-column body grid (`mainCol` left, `sideCol` right)\n * below the stat strip. In split mode you compose the body yourself —\n * the default activity feed and `secondaryRegion` are not rendered.\n */\n layout?: \"single\" | \"split\"\n\n /** Left column content. Only rendered when `layout=\"split\"`. */\n mainCol?: React.ReactNode\n /** Right column content. Only rendered when `layout=\"split\"`. */\n sideCol?: React.ReactNode\n\n /** Heading rendered above the activity feed. Defaults to \"Recent activity\". */\n activityTitle?: React.ReactNode\n /** Activity events rendered in the feed. Ignored when `layout=\"split\"`. */\n activities: AdminDashboardActivity[]\n /** If provided, renders a \"View all\" link in the activity section header. */\n activityViewAllHref?: string\n /** Replaces the default EmptyState when `activities` is empty. */\n activityEmptyState?: React.ReactNode\n /** Variant forwarded to the underlying ActivityFeed. */\n activityVariant?: \"default\" | \"compact\" | \"timeline\"\n\n /**\n * Optional region rendered below the stat grid and above the activity feed.\n * Ignored when `layout=\"split\"` — compose body content via `mainCol`/`sideCol`.\n */\n secondaryRegion?: React.ReactNode\n\n /** Heading level used for the PageHeader title. Defaults to `h1`. */\n titleAs?: PageHeaderProps[\"titleAs\"]\n}\n\nexport function AdminDashboard({\n title,\n eyebrow,\n description,\n actions,\n stats,\n layout = \"single\",\n mainCol,\n sideCol,\n activityTitle = \"Recent activity\",\n activities,\n activityViewAllHref,\n activityEmptyState,\n activityVariant = \"default\",\n secondaryRegion,\n titleAs = \"h1\",\n className,\n ...props\n}: AdminDashboardProps) {\n const activityHeadingId = React.useId()\n const hasActivities = activities.length > 0\n const isSplit = layout === \"split\"\n\n // Dev-mode warning: in split mode the caller composes the body via mainCol /\n // sideCol, so any `activities` / `secondaryRegion` props are silently dropped.\n // Flag the mismatch loudly during development so consumers notice before\n // shipping. Suppressed in production to avoid noisy logs.\n if (\n isSplit &&\n process.env.NODE_ENV !== \"production\" &&\n (activities.length > 0 || secondaryRegion !== undefined)\n ) {\n // eslint-disable-next-line no-console\n console.warn(\n \"[AdminDashboard] layout=\\\"split\\\" ignores `activities` and `secondaryRegion`. \" +\n \"Compose body content inside `mainCol` / `sideCol` instead.\"\n )\n }\n\n return (\n <div\n className={cn(styles.root, className)}\n data-slot=\"admin-dashboard\"\n data-layout={layout}\n {...props}\n >\n <PageHeader\n eyebrow={eyebrow}\n title={title}\n description={description}\n actions={actions}\n titleAs={titleAs}\n />\n\n {stats.length > 0 ? (\n <div\n className={styles.statGrid}\n data-slot=\"admin-dashboard-stats\"\n data-count={stats.length}\n >\n {stats.map((stat) => (\n <StatCard\n key={stat.id}\n label={stat.label}\n value={stat.value}\n delta={stat.delta}\n trend={stat.trend}\n variant={stat.variant}\n size={stat.size}\n valueAs={stat.valueAs}\n />\n ))}\n </div>\n ) : null}\n\n {isSplit ? (\n <div\n className={styles.body}\n data-slot=\"admin-dashboard-body\"\n data-layout=\"split\"\n >\n <div\n className={styles.mainCol}\n data-slot=\"admin-dashboard-main-col\"\n >\n {mainCol}\n </div>\n <aside\n className={styles.sideCol}\n data-slot=\"admin-dashboard-side-col\"\n >\n {sideCol}\n </aside>\n </div>\n ) : (\n <>\n {secondaryRegion ? (\n <div\n className={styles.secondaryRegion}\n data-slot=\"admin-dashboard-secondary\"\n >\n {secondaryRegion}\n </div>\n ) : null}\n\n <section\n className={styles.activitySection}\n data-slot=\"admin-dashboard-activity\"\n aria-labelledby={activityHeadingId}\n >\n <header className={styles.activityHeader}>\n <h2 id={activityHeadingId} className={styles.activityTitle}>\n {activityTitle}\n </h2>\n {activityViewAllHref ? (\n <a\n href={activityViewAllHref}\n className={styles.activityViewAll}\n data-slot=\"admin-dashboard-activity-view-all\"\n >\n View all\n </a>\n ) : null}\n </header>\n\n {hasActivities ? (\n <ActivityFeed variant={activityVariant}>\n {activities.map((activity) => (\n <ActivityFeedItem\n key={activity.id}\n leading={activity.leading}\n title={activity.title}\n description={activity.description}\n actor={activity.actor}\n timestamp={activity.timestamp}\n trailing={activity.trailing}\n />\n ))}\n </ActivityFeed>\n ) : activityEmptyState !== undefined ? (\n activityEmptyState\n ) : (\n <EmptyState\n heading=\"No recent activity\"\n description=\"New events will show up here as they happen.\"\n tone=\"subtle\"\n />\n )}\n </section>\n </>\n )}\n </div>\n )\n}\n"
|
|
3022
3142
|
},
|
|
3023
3143
|
{
|
|
3024
3144
|
"path": "blocks/admin-dashboard/admin-dashboard.module.css",
|
|
3025
3145
|
"type": "registry:block",
|
|
3026
|
-
"content": ".root {\n display: flex;\n flex-direction: column;\n gap: var(--spacing-8, 2rem);\n width: 100%;\n container-type: inline-size;\n container-name: admin-dashboard;\n color: var(--text-primary, #111827);\n}\n\n/* Responsive stat grid driven entirely by container queries. */\n.statGrid {\n display: grid;\n gap: var(--spacing-4, 1rem);\n grid-template-columns: 1fr;\n}\n\n@container admin-dashboard (min-width: 36rem) {\n .statGrid {\n grid-template-columns: repeat(2, minmax(0, 1fr));\n }\n}\n\n@container admin-dashboard (min-width: 56rem) {\n .statGrid {\n grid-template-columns: repeat(3, minmax(0, 1fr));\n gap: var(--spacing-5, 1.25rem);\n }\n}\n\n@container admin-dashboard (min-width: 72rem) {\n .statGrid {\n grid-template-columns: repeat(4, minmax(0, 1fr));\n gap: var(--spacing-6, 1.5rem);\n }\n}\n\n.secondaryRegion {\n display: flex;\n flex-direction: column;\n gap: var(--spacing-4, 1rem);\n min-width: 0;\n}\n\n.activitySection {\n display: flex;\n flex-direction: column;\n gap: var(--spacing-4, 1rem);\n padding: var(--spacing-6, 1.5rem);\n background: var(--surface-card, #ffffff);\n border: 1px solid var(--border-default, #e5e7eb);\n border-radius: var(--radius-lg, 0.75rem);\n box-shadow: var(--shadow-xs, 0 1px 2px 0 rgb(0 0 0 / 0.05));\n min-width: 0;\n}\n\n.activityHeader {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: var(--spacing-3, 0.75rem);\n min-width: 0;\n}\n\n.activityTitle {\n margin: 0;\n font-family: var(--font-family-heading, inherit);\n font-size: var(--font-size-lg, 1.125rem);\n font-weight: var(--font-weight-semibold, 600);\n line-height: var(--line-height-tight, 1.25);\n letter-spacing: var(--letter-spacing-tight, -0.01em);\n color: var(--text-primary, #111827);\n min-width: 0;\n}\n\n.activityViewAll {\n flex-shrink: 0;\n font-size: var(--font-size-sm, 0.875rem);\n font-weight: var(--font-weight-medium, 500);\n color: var(--text-link, var(--accent-primary, #3b82f6));\n text-decoration: none;\n border-radius: var(--radius-sm, 0.25rem);\n transition:\n color var(--motion-duration-fast, 150ms)\n var(--motion-easing-standard, ease);\n}\n\n.activityViewAll:hover {\n text-decoration: underline;\n}\n\n.activityViewAll:focus-visible {\n outline: var(--focus-ring-width, 2px) solid\n var(--focus-ring-color, var(--accent-primary, #3b82f6));\n outline-offset: var(--focus-ring-offset, 2px);\n}\n\n@container admin-dashboard (max-width: 36rem) {\n .activitySection {\n padding: var(--spacing-4, 1rem);\n }\n}\n"
|
|
3146
|
+
"content": ".root {\n /* Tunable knobs — themes (and one-off pages) can override these without\n reaching into block internals. Defaults match the r3 reference build.\n\n Note: --admin-dashboard-stack-bp is exposed as a custom property for\n theme/documentation discoverability, but CSS @container queries cannot\n dereference custom properties today, so the breakpoint below is the\n hard-coded fallback. Override the @container rule in a theme layer if\n you need a different stack point. */\n --admin-dashboard-side-col-width: 320px;\n --admin-dashboard-stack-bp: 960px;\n\n display: flex;\n flex-direction: column;\n gap: var(--spacing-8, 2rem);\n width: 100%;\n container-type: inline-size;\n container-name: admin-dashboard;\n color: var(--text-primary, #111827);\n}\n\n/* Responsive stat grid driven entirely by container queries. */\n.statGrid {\n display: grid;\n gap: var(--spacing-4, 1rem);\n grid-template-columns: 1fr;\n}\n\n@container admin-dashboard (min-width: 36rem) {\n .statGrid {\n grid-template-columns: repeat(2, minmax(0, 1fr));\n }\n}\n\n@container admin-dashboard (min-width: 56rem) {\n .statGrid {\n grid-template-columns: repeat(3, minmax(0, 1fr));\n gap: var(--spacing-5, 1.25rem);\n }\n}\n\n@container admin-dashboard (min-width: 72rem) {\n .statGrid {\n grid-template-columns: repeat(4, minmax(0, 1fr));\n gap: var(--spacing-6, 1.5rem);\n }\n}\n\n.secondaryRegion {\n display: flex;\n flex-direction: column;\n gap: var(--spacing-4, 1rem);\n min-width: 0;\n}\n\n/* ─── Split body layout ─────────────────────────────────────────────────────\n When layout=\"split\", the activity / secondaryRegion flow is replaced by a\n 2-column grid below the KPI strip. Defaults below mirror the r3 dashboard\n reference (1fr | 320px, 20px gutter). Sub-960px stacks side-col below. */\n\n.body {\n display: flex;\n flex-direction: column;\n gap: var(--spacing-5, 1.25rem);\n min-width: 0;\n}\n\n.body[data-layout=\"split\"] {\n display: grid;\n grid-template-columns: minmax(0, 1fr) var(--admin-dashboard-side-col-width);\n gap: var(--spacing-5, 1.25rem);\n}\n\n.mainCol,\n.sideCol {\n display: flex;\n flex-direction: column;\n gap: var(--spacing-5, 1.25rem);\n min-width: 0;\n}\n\n@container admin-dashboard (max-width: 60rem) {\n /* Default stack breakpoint = 960px (60rem). Override the rule in a theme\n layer if --admin-dashboard-stack-bp is retuned. */\n .body[data-layout=\"split\"] {\n grid-template-columns: minmax(0, 1fr);\n }\n}\n\n.activitySection {\n display: flex;\n flex-direction: column;\n gap: var(--spacing-4, 1rem);\n padding: var(--spacing-6, 1.5rem);\n background: var(--surface-card, #ffffff);\n border: 1px solid var(--border-default, #e5e7eb);\n border-radius: var(--radius-lg, 0.75rem);\n box-shadow: var(--shadow-xs, 0 1px 2px 0 rgb(0 0 0 / 0.05));\n min-width: 0;\n}\n\n.activityHeader {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: var(--spacing-3, 0.75rem);\n min-width: 0;\n}\n\n.activityTitle {\n margin: 0;\n font-family: var(--font-family-heading, inherit);\n font-size: var(--font-size-lg, 1.125rem);\n font-weight: var(--font-weight-semibold, 600);\n line-height: var(--line-height-tight, 1.25);\n letter-spacing: var(--letter-spacing-tight, -0.01em);\n color: var(--text-primary, #111827);\n min-width: 0;\n}\n\n.activityViewAll {\n flex-shrink: 0;\n font-size: var(--font-size-sm, 0.875rem);\n font-weight: var(--font-weight-medium, 500);\n color: var(--text-link, var(--accent-primary, #3b82f6));\n text-decoration: none;\n border-radius: var(--radius-sm, 0.25rem);\n transition:\n color var(--motion-duration-fast, 150ms)\n var(--motion-easing-standard, ease);\n}\n\n.activityViewAll:hover {\n text-decoration: underline;\n}\n\n.activityViewAll:focus-visible {\n outline: var(--focus-ring-width, 2px) solid\n var(--focus-ring-color, var(--accent-primary, #3b82f6));\n outline-offset: var(--focus-ring-offset, 2px);\n}\n\n@container admin-dashboard (max-width: 36rem) {\n .activitySection {\n padding: var(--spacing-4, 1rem);\n }\n}\n"
|
|
3027
3147
|
}
|
|
3028
3148
|
]
|
|
3029
3149
|
},
|
|
@@ -3048,12 +3168,12 @@
|
|
|
3048
3168
|
{
|
|
3049
3169
|
"path": "blocks/admin-list-page/admin-list-page.tsx",
|
|
3050
3170
|
"type": "registry:block",
|
|
3051
|
-
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport {\n type ColumnDef,\n type OnChangeFn,\n type PaginationState,\n type RowSelectionState,\n type SortingState,\n} from \"@tanstack/react-table\"\n\nimport { cn } from \"../../lib/utils\"\nimport {\n PageHeader,\n type PageHeaderProps,\n} from \"../../components/ui/page-header/page-header\"\nimport { FilterBar } from \"../../components/ui/filter-bar/filter-bar\"\nimport { DataTable } from \"../../components/ui/data-table/data-table\"\nimport { BulkActionBar } from \"../../components/ui/bulk-action-bar/bulk-action-bar\"\nimport styles from \"./admin-list-page.module.css\"\n\nexport interface AdminListPageProps<TData>\n extends Omit<React.HTMLAttributes<HTMLDivElement>, \"title\"> {\n // ── Header ───────────────────────────────────────────────────────────────\n /** Page title rendered inside the PageHeader. */\n title: React.ReactNode\n /** Optional small uppercase label rendered above the title. */\n eyebrow?: React.ReactNode\n /** Optional supporting copy rendered below the title. */\n description?: React.ReactNode\n /** Optional actions slot rendered on the right side of the header. */\n actions?: React.ReactNode\n /** Optional breadcrumb rendered above the title row. */\n breadcrumb?: React.ReactNode\n /** Heading level used for the PageHeader title. Defaults to `h1`. */\n titleAs?: PageHeaderProps[\"titleAs\"]\n\n // ── Filter bar ───────────────────────────────────────────────────────────\n /**\n * Controlled search value. Pass with `onSearchChange` to enable the search\n * input. You own the state and filtering logic:\n * @example\n * const [search, setSearch] = React.useState('');\n * const filtered = data.filter(row => row.name.includes(search));\n * <AdminListPage searchValue={search} onSearchChange={setSearch} data={filtered} />\n */\n searchValue?: string\n /** Handler invoked when the search input changes. Pair with `searchValue`. */\n onSearchChange?: (value: string) => void\n /** Placeholder for the search input. Defaults to \"Search...\". */\n searchPlaceholder?: string\n /** Filter controls rendered inside the FilterBar. */\n filters?: React.ReactNode\n /** Active filter chips rendered below the FilterBar. */\n activeFilters?: Array<{\n id: string\n label: React.ReactNode\n onRemove: () => void\n }>\n /** Handler invoked when the clear-all filters button is clicked. */\n onClearFilters?: () => void\n /** Meta text rendered on the far right of the FilterBar, e.g. \"42 results\". */\n resultsCount?: React.ReactNode\n /** Hide the FilterBar entirely. Defaults to `false`. */\n hideFilterBar?: boolean\n\n // ── Data table ───────────────────────────────────────────────────────────\n columns: ColumnDef<TData, unknown>[]\n data: TData[]\n getRowId?: (row: TData, index: number) => string\n\n sorting?: SortingState\n onSortingChange?: OnChangeFn<SortingState>\n\n pagination?: PaginationState\n onPaginationChange?: OnChangeFn<PaginationState>\n pageSize?: number\n pageSizeOptions?: number[]\n\n rowSelection?: RowSelectionState\n onRowSelectionChange?: OnChangeFn<RowSelectionState>\n /** Enable row selection checkboxes. Defaults to `false`. */\n enableRowSelection?: boolean\n\n loading?: boolean\n /** Custom empty state forwarded to the DataTable. */\n emptyState?: React.ReactNode\n\n // ── Bulk actions ─────────────────────────────────────────────────────────\n /** Action buttons cluster — rendered only when selection count > 0. */\n bulkActions?: React.ReactNode\n /** Render the BulkActionBar inline (non-sticky). Defaults to `false`. */\n bulkActionBarInline?: boolean\n /** Selection label renderer. Defaults to `(n) => `${n} selected``. */\n bulkActionLabel?: (count: number) => React.ReactNode\n}\n\nfunction countSelectedRows(state: RowSelectionState): number {\n let count = 0\n for (const key in state) {\n if (state[key]) count += 1\n }\n return count\n}\n\nfunction AdminListPageInner<TData>(\n props: AdminListPageProps<TData>,\n ref: React.Ref<HTMLDivElement>\n) {\n const {\n // Header\n title,\n eyebrow,\n description,\n actions,\n breadcrumb,\n titleAs = \"h1\",\n // Filter bar\n searchValue,\n onSearchChange,\n searchPlaceholder,\n filters,\n activeFilters,\n onClearFilters,\n resultsCount,\n hideFilterBar = false,\n // Data table\n columns,\n data,\n getRowId,\n sorting,\n onSortingChange,\n pagination,\n onPaginationChange,\n pageSize,\n pageSizeOptions,\n rowSelection: controlledRowSelection,\n onRowSelectionChange,\n enableRowSelection = false,\n loading,\n emptyState,\n // Bulk actions\n bulkActions,\n bulkActionBarInline = false,\n bulkActionLabel,\n // Root\n className,\n ...rest\n } = props\n\n // Uncontrolled row selection state, mirroring DataTable's pattern so the\n // block can drive the BulkActionBar without requiring consumers to lift state.\n const [internalRowSelection, setInternalRowSelection] =\n React.useState<RowSelectionState>({})\n const selectionIsControlled = controlledRowSelection !== undefined\n const rowSelection = selectionIsControlled\n ? controlledRowSelection\n : internalRowSelection\n\n const handleRowSelectionChange: OnChangeFn<RowSelectionState> = (updater) => {\n if (!selectionIsControlled) {\n setInternalRowSelection((prev) =>\n typeof updater === \"function\" ? updater(prev) : updater\n )\n }\n onRowSelectionChange?.(updater)\n }\n\n const handleClearSelection = React.useCallback(() => {\n if (!selectionIsControlled) {\n setInternalRowSelection({})\n }\n onRowSelectionChange?.({})\n }, [selectionIsControlled, onRowSelectionChange])\n\n const selectedCount = countSelectedRows(rowSelection)\n const showBulkActionBar = Boolean(bulkActions) && selectedCount > 0\n\n return (\n <div\n ref={ref}\n data-slot=\"admin-list-page\"\n className={cn(styles.root, className)}\n {...rest}\n >\n <header className={styles.header} data-slot=\"admin-list-page-header\">\n <PageHeader\n eyebrow={eyebrow}\n title={title}\n description={description}\n actions={actions}\n breadcrumb={breadcrumb}\n titleAs={titleAs}\n />\n\n {hideFilterBar ? null : (\n <FilterBar\n searchValue={searchValue}\n onSearchChange={onSearchChange}\n searchPlaceholder={searchPlaceholder}\n activeFilters={activeFilters}\n onClearAll={onClearFilters}\n resultsCount={resultsCount}\n >\n {filters}\n </FilterBar>\n )}\n </header>\n\n <section\n className={styles.tableSection}\n data-slot=\"admin-list-page-table\"\n aria-label={typeof title === \"string\" ? title : undefined}\n >\n <DataTable<TData>\n columns={columns}\n data={data}\n getRowId={getRowId}\n sorting={sorting}\n onSortingChange={onSortingChange}\n pagination={pagination}\n onPaginationChange={onPaginationChange}\n pageSize={pageSize}\n pageSizeOptions={pageSizeOptions}\n enableRowSelection={enableRowSelection}\n rowSelection={rowSelection}\n onRowSelectionChange={handleRowSelectionChange}\n loading={loading}\n emptyState={emptyState}\n />\n\n {showBulkActionBar ? (\n <BulkActionBar\n count={selectedCount}\n inline={bulkActionBarInline}\n label={bulkActionLabel}\n onClear={handleClearSelection}\n >\n {bulkActions}\n </BulkActionBar>\n ) : null}\n </section>\n </div>\n )\n}\n\n// Generic forwardRef — cast preserves TData through the forwardRef boundary.\nconst AdminListPage = React.forwardRef(AdminListPageInner) as <TData>(\n props: AdminListPageProps<TData> & { ref?: React.Ref<HTMLDivElement> }\n) => React.ReactElement\n\n;(AdminListPage as unknown as { displayName: string }).displayName =\n \"AdminListPage\"\n\nexport { AdminListPage }\n"
|
|
3171
|
+
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport {\n type ColumnDef,\n type OnChangeFn,\n type PaginationState,\n type RowSelectionState,\n type SortingState,\n} from \"@tanstack/react-table\"\n\nimport { cn } from \"../../lib/utils\"\nimport {\n PageHeader,\n type PageHeaderProps,\n} from \"../../components/ui/page-header/page-header\"\nimport { FilterBar } from \"../../components/ui/filter-bar/filter-bar\"\nimport {\n DataTable,\n type DataTableGroupRow,\n type DataTableRow,\n type DataTableRowTone,\n} from \"../../components/ui/data-table/data-table\"\nimport { BulkActionBar } from \"../../components/ui/bulk-action-bar/bulk-action-bar\"\nimport styles from \"./admin-list-page.module.css\"\n\nexport interface AdminListPageProps<TData>\n extends Omit<React.HTMLAttributes<HTMLDivElement>, \"title\"> {\n // ── Header ───────────────────────────────────────────────────────────────\n /** Page title rendered inside the PageHeader. */\n title: React.ReactNode\n /** Optional small uppercase label rendered above the title. */\n eyebrow?: React.ReactNode\n /** Optional supporting copy rendered below the title. */\n description?: React.ReactNode\n /** Optional actions slot rendered on the right side of the header. */\n actions?: React.ReactNode\n /** Optional breadcrumb rendered above the title row. */\n breadcrumb?: React.ReactNode\n /** Heading level used for the PageHeader title. Defaults to `h1`. */\n titleAs?: PageHeaderProps[\"titleAs\"]\n\n // ── Filter bar ───────────────────────────────────────────────────────────\n /**\n * Controlled search value. Pass with `onSearchChange` to enable the search\n * input. You own the state and filtering logic:\n * @example\n * const [search, setSearch] = React.useState('');\n * const filtered = data.filter(row => row.name.includes(search));\n * <AdminListPage searchValue={search} onSearchChange={setSearch} data={filtered} />\n */\n searchValue?: string\n /** Handler invoked when the search input changes. Pair with `searchValue`. */\n onSearchChange?: (value: string) => void\n /** Placeholder for the search input. Defaults to \"Search...\". */\n searchPlaceholder?: string\n /** Filter controls rendered inside the FilterBar. */\n filters?: React.ReactNode\n /** Active filter chips rendered below the FilterBar. */\n activeFilters?: Array<{\n id: string\n label: React.ReactNode\n onRemove: () => void\n }>\n /** Handler invoked when the clear-all filters button is clicked. */\n onClearFilters?: () => void\n /** Meta text rendered on the far right of the FilterBar, e.g. \"42 results\". */\n resultsCount?: React.ReactNode\n /** Hide the FilterBar entirely. Defaults to `false`. */\n hideFilterBar?: boolean\n /**\n * Replaces the default `<FilterBar>` entirely. When provided, all FilterBar-\n * specific props (`searchValue`, `onSearchChange`, `searchPlaceholder`,\n * `filters`, `activeFilters`, `onClearFilters`, `resultsCount`) are ignored.\n * Renders inside the header region with a\n * `data-slot=\"admin-list-page-custom-filter-bar\"` wrapper. `hideFilterBar`\n * still wins over both default and custom bars.\n *\n * Dev-mode `console.warn` fires if `customFilterBar` is supplied alongside\n * any FilterBar-specific prop.\n */\n customFilterBar?: React.ReactNode\n\n // ── Data table ───────────────────────────────────────────────────────────\n columns: ColumnDef<TData, unknown>[]\n /**\n * Flat row data forwarded to DataTable. Optional — omit when supplying\n * `rows` (the discriminated-union grouped API) or when rendering a fully\n * custom table via `customFilterBar` + a non-table body. Defaults to `[]`\n * when omitted, which yields DataTable's empty state.\n */\n data?: TData[]\n /**\n * Mixed-order discriminated-union row list forwarded to DataTable. Use\n * this to interleave group headers and data rows (e.g., \"Tonight\",\n * \"This week\" sections). When `rows` is supplied, `data` is ignored —\n * dev-mode `console.warn` fires if both are passed.\n *\n * @example\n * <AdminListPage\n * rows={[\n * { kind: \"group\", id: \"tonight\", label: \"Tonight\" },\n * { kind: \"data\", id: \"evt-1\", row: event1 },\n * { kind: \"group\", id: \"week\", label: \"This week\", count: 3 },\n * { kind: \"data\", id: \"evt-2\", row: event2 },\n * ]}\n * />\n */\n rows?: DataTableRow<TData>[]\n /**\n * Optional renderer for group rows. Receives the group descriptor and\n * returns the cell contents. Forwarded as-is to DataTable.\n */\n groupRowRenderer?: (group: DataTableGroupRow) => React.ReactNode\n /**\n * Map each data row to a semantic tone for a subtle background tint.\n * Forwarded as-is to DataTable — tones resolve to Visor surface tokens at\n * the CSS layer (see `data-table.module.css`).\n */\n rowTone?: (row: TData) => DataTableRowTone | undefined\n /**\n * When supplied, every data row becomes a keyboard-activatable target\n * (click + Enter/Space dispatch the handler). Forwarded as-is to\n * DataTable. Typical use: open a detail drawer for the clicked row.\n */\n onRowClick?: (row: TData) => void\n getRowId?: (row: TData, index: number) => string\n\n sorting?: SortingState\n onSortingChange?: OnChangeFn<SortingState>\n\n pagination?: PaginationState\n onPaginationChange?: OnChangeFn<PaginationState>\n pageSize?: number\n pageSizeOptions?: number[]\n\n rowSelection?: RowSelectionState\n onRowSelectionChange?: OnChangeFn<RowSelectionState>\n /** Enable row selection checkboxes. Defaults to `false`. */\n enableRowSelection?: boolean\n\n loading?: boolean\n /** Custom empty state forwarded to the DataTable. */\n emptyState?: React.ReactNode\n\n // ── Bulk actions ─────────────────────────────────────────────────────────\n /** Action buttons cluster — rendered only when selection count > 0. */\n bulkActions?: React.ReactNode\n /** Render the BulkActionBar inline (non-sticky). Defaults to `false`. */\n bulkActionBarInline?: boolean\n /** Selection label renderer. Defaults to `(n) => `${n} selected``. */\n bulkActionLabel?: (count: number) => React.ReactNode\n\n // ── Footer status ────────────────────────────────────────────────────────\n /**\n * Always-on info row rendered below the table, as a sibling of the table\n * section (direct child of the block root). Independent of `BulkActionBar`\n * — the two can coexist. Typical content is a selection count, total, and\n * Kbd hint cluster.\n *\n * Wrapped in a `data-slot=\"admin-list-page-footer-status\"` element for CSS\n * hooks. Consumer composes the row layout (flat slot, not structured).\n *\n * BREAKING (VI-404): previously rendered inside the table section; now\n * rendered as a sibling. Consumers using descendant selectors of the form\n * `[data-slot=\"admin-list-page-table\"] [data-slot=\"admin-list-page-footer-status\"]`\n * will silently stop matching — drop the `admin-list-page-table` ancestor.\n */\n footerStatus?: React.ReactNode\n}\n\nfunction countSelectedRows(state: RowSelectionState): number {\n let count = 0\n for (const key in state) {\n if (state[key]) count += 1\n }\n return count\n}\n\nfunction AdminListPageInner<TData>(\n props: AdminListPageProps<TData>,\n ref: React.Ref<HTMLDivElement>\n) {\n const {\n // Header\n title,\n eyebrow,\n description,\n actions,\n breadcrumb,\n titleAs = \"h1\",\n // Filter bar\n searchValue,\n onSearchChange,\n searchPlaceholder,\n filters,\n activeFilters,\n onClearFilters,\n resultsCount,\n hideFilterBar = false,\n customFilterBar,\n // Data table\n columns,\n data,\n rows,\n groupRowRenderer,\n rowTone,\n onRowClick,\n getRowId,\n sorting,\n onSortingChange,\n pagination,\n onPaginationChange,\n pageSize,\n pageSizeOptions,\n rowSelection: controlledRowSelection,\n onRowSelectionChange,\n enableRowSelection = false,\n loading,\n emptyState,\n // Bulk actions\n bulkActions,\n bulkActionBarInline = false,\n bulkActionLabel,\n // Footer status\n footerStatus,\n // Root\n className,\n ...rest\n } = props\n\n // Dev-mode warning when both `rows` and `data` are supplied. `rows` wins\n // silently inside DataTable; the warning surfaces the mistake without\n // breaking render output.\n if (\n process.env.NODE_ENV !== \"production\" &&\n rows !== undefined &&\n data !== undefined\n ) {\n // eslint-disable-next-line no-console\n console.warn(\n \"AdminListPage: both `rows` and `data` were supplied. The `rows` prop replaces `data` and wins; the `data` prop is ignored. Pass only one.\"\n )\n }\n\n // Dev-mode warning when customFilterBar is mixed with FilterBar-specific props.\n // The custom bar wins silently; the warning surfaces the mistake without\n // breaking render output.\n if (process.env.NODE_ENV !== \"production\" && customFilterBar !== undefined) {\n const conflicting: string[] = []\n if (searchValue !== undefined) conflicting.push(\"searchValue\")\n if (onSearchChange !== undefined) conflicting.push(\"onSearchChange\")\n if (searchPlaceholder !== undefined) conflicting.push(\"searchPlaceholder\")\n if (filters !== undefined) conflicting.push(\"filters\")\n if (activeFilters !== undefined) conflicting.push(\"activeFilters\")\n if (onClearFilters !== undefined) conflicting.push(\"onClearFilters\")\n if (resultsCount !== undefined) conflicting.push(\"resultsCount\")\n if (conflicting.length > 0) {\n // eslint-disable-next-line no-console\n console.warn(\n `AdminListPage: \\`customFilterBar\\` was supplied alongside FilterBar-specific prop(s): ${conflicting.join(\", \")}. The custom filter bar replaces the default FilterBar entirely; the FilterBar-specific props are ignored.`\n )\n }\n }\n\n // Uncontrolled row selection state, mirroring DataTable's pattern so the\n // block can drive the BulkActionBar without requiring consumers to lift state.\n const [internalRowSelection, setInternalRowSelection] =\n React.useState<RowSelectionState>({})\n const selectionIsControlled = controlledRowSelection !== undefined\n const rowSelection = selectionIsControlled\n ? controlledRowSelection\n : internalRowSelection\n\n const handleRowSelectionChange: OnChangeFn<RowSelectionState> = (updater) => {\n if (!selectionIsControlled) {\n setInternalRowSelection((prev) =>\n typeof updater === \"function\" ? updater(prev) : updater\n )\n }\n onRowSelectionChange?.(updater)\n }\n\n const handleClearSelection = React.useCallback(() => {\n if (!selectionIsControlled) {\n setInternalRowSelection({})\n }\n onRowSelectionChange?.({})\n }, [selectionIsControlled, onRowSelectionChange])\n\n const selectedCount = countSelectedRows(rowSelection)\n const showBulkActionBar = Boolean(bulkActions) && selectedCount > 0\n\n return (\n <div\n ref={ref}\n data-slot=\"admin-list-page\"\n className={cn(styles.root, className)}\n {...rest}\n >\n <header className={styles.header} data-slot=\"admin-list-page-header\">\n <PageHeader\n eyebrow={eyebrow}\n title={title}\n description={description}\n actions={actions}\n breadcrumb={breadcrumb}\n titleAs={titleAs}\n />\n\n {hideFilterBar ? null : customFilterBar !== undefined ? (\n <div data-slot=\"admin-list-page-custom-filter-bar\">\n {customFilterBar}\n </div>\n ) : (\n <FilterBar\n searchValue={searchValue}\n onSearchChange={onSearchChange}\n searchPlaceholder={searchPlaceholder}\n activeFilters={activeFilters}\n onClearAll={onClearFilters}\n resultsCount={resultsCount}\n >\n {filters}\n </FilterBar>\n )}\n </header>\n\n <section\n className={styles.tableSection}\n data-slot=\"admin-list-page-table\"\n aria-label={typeof title === \"string\" ? title : undefined}\n >\n <DataTable<TData>\n columns={columns}\n data={data ?? []}\n rows={rows}\n groupRowRenderer={groupRowRenderer}\n rowTone={rowTone}\n onRowClick={onRowClick}\n getRowId={getRowId}\n sorting={sorting}\n onSortingChange={onSortingChange}\n pagination={pagination}\n onPaginationChange={onPaginationChange}\n pageSize={pageSize}\n pageSizeOptions={pageSizeOptions}\n enableRowSelection={enableRowSelection}\n rowSelection={rowSelection}\n onRowSelectionChange={handleRowSelectionChange}\n loading={loading}\n emptyState={emptyState}\n />\n\n {showBulkActionBar ? (\n <BulkActionBar\n count={selectedCount}\n inline={bulkActionBarInline}\n label={bulkActionLabel}\n onClear={handleClearSelection}\n >\n {bulkActions}\n </BulkActionBar>\n ) : null}\n </section>\n\n {footerStatus !== undefined ? (\n <div\n data-slot=\"admin-list-page-footer-status\"\n className={styles.footerStatus}\n >\n {footerStatus}\n </div>\n ) : null}\n </div>\n )\n}\n\n// Generic forwardRef — cast preserves TData through the forwardRef boundary.\nconst AdminListPage = React.forwardRef(AdminListPageInner) as <TData>(\n props: AdminListPageProps<TData> & { ref?: React.Ref<HTMLDivElement> }\n) => React.ReactElement\n\n;(AdminListPage as unknown as { displayName: string }).displayName =\n \"AdminListPage\"\n\nexport { AdminListPage }\n"
|
|
3052
3172
|
},
|
|
3053
3173
|
{
|
|
3054
3174
|
"path": "blocks/admin-list-page/admin-list-page.module.css",
|
|
3055
3175
|
"type": "registry:block",
|
|
3056
|
-
"content": ".root {\n display: flex;\n flex-direction: column;\n gap: var(--spacing-6, 1.5rem);\n width: 100%;\n container-type: inline-size;\n container-name: admin-list-page;\n color: var(--text-primary, #111827);\n}\n\n.header {\n display: flex;\n flex-direction: column;\n gap: var(--spacing-5, 1.25rem);\n min-width: 0;\n}\n\n.tableSection {\n display: flex;\n flex-direction: column;\n gap: var(--spacing-4, 1rem);\n min-width: 0;\n}\n\n@container admin-list-page (min-width: 56rem) {\n .root {\n gap: var(--spacing-8, 2rem);\n }\n\n .header {\n gap: var(--spacing-6, 1.5rem);\n }\n}\n"
|
|
3176
|
+
"content": ".root {\n display: flex;\n flex-direction: column;\n gap: var(--spacing-6, 1.5rem);\n width: 100%;\n container-type: inline-size;\n container-name: admin-list-page;\n color: var(--text-primary, #111827);\n}\n\n.header {\n display: flex;\n flex-direction: column;\n gap: var(--spacing-5, 1.25rem);\n min-width: 0;\n}\n\n.tableSection {\n background-color: var(--admin-list-page-table-bg, transparent);\n border-radius: var(--admin-list-page-table-radius, 0);\n display: flex;\n flex-direction: column;\n gap: var(--spacing-4, 1rem);\n min-width: 0;\n}\n\n.footerStatus {\n background-color: var(--admin-list-page-footer-bg, transparent);\n border-radius: var(--admin-list-page-footer-radius, 0);\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: var(--spacing-3, 0.75rem);\n padding: var(--admin-list-page-footer-padding, var(--spacing-3, 0.75rem) 0);\n border-top: var(\n --admin-list-page-footer-border-top,\n var(--stroke-width-thin, 1px) solid\n var(--border-subtle, var(--border-default, #e5e7eb))\n );\n color: var(--text-tertiary, var(--text-muted, #6b7280));\n font-size: var(--text-xs, 0.6875rem);\n line-height: var(--leading-snug, 1.4);\n min-width: 0;\n}\n\n@container admin-list-page (min-width: 56rem) {\n .root {\n gap: var(--spacing-8, 2rem);\n }\n\n .header {\n gap: var(--spacing-6, 1.5rem);\n }\n}\n"
|
|
3057
3177
|
}
|
|
3058
3178
|
]
|
|
3059
3179
|
},
|
|
@@ -3085,6 +3205,36 @@
|
|
|
3085
3205
|
}
|
|
3086
3206
|
]
|
|
3087
3207
|
},
|
|
3208
|
+
{
|
|
3209
|
+
"name": "command-dialog",
|
|
3210
|
+
"type": "registry:block",
|
|
3211
|
+
"description": "⌘K command palette composing Command + Dialog primitives. Exposes named slots for scope chip, grouped results with hit-highlighting, per-item meta + Kbd shortcut, and a footer hint row with derived result count. Binds ⌘K / Ctrl+K to toggle open by default.",
|
|
3212
|
+
"category": "navigation",
|
|
3213
|
+
"dependencies": [
|
|
3214
|
+
"cmdk",
|
|
3215
|
+
"@radix-ui/react-dialog",
|
|
3216
|
+
"@phosphor-icons/react",
|
|
3217
|
+
"@loworbitstudio/visor-core"
|
|
3218
|
+
],
|
|
3219
|
+
"registryDependencies": [
|
|
3220
|
+
"utils",
|
|
3221
|
+
"command",
|
|
3222
|
+
"dialog",
|
|
3223
|
+
"kbd"
|
|
3224
|
+
],
|
|
3225
|
+
"files": [
|
|
3226
|
+
{
|
|
3227
|
+
"path": "blocks/command-dialog/command-dialog.tsx",
|
|
3228
|
+
"type": "registry:block",
|
|
3229
|
+
"content": "\"use client\"\n\nimport * as React from \"react\"\n\nimport { cn } from \"../../lib/utils\"\nimport {\n Command,\n CommandEmpty,\n CommandGroup,\n CommandInput,\n CommandItem,\n CommandList,\n} from \"../../components/ui/command/command\"\nimport {\n Dialog,\n DialogContent,\n DialogTitle,\n} from \"../../components/ui/dialog/dialog\"\nimport { Kbd } from \"../../components/ui/kbd/kbd\"\nimport styles from \"./command-dialog.module.css\"\n\n// ── Types ─────────────────────────────────────────────────────────────────\n\nexport interface CommandDialogItem {\n /** Stable identifier — used as React key. */\n id: string\n /** Plain-text value that cmdk uses for its built-in filter. */\n value: string\n /**\n * Label content. Pass a ReactNode with `<span data-hit>` spans inside to\n * render hit-highlighting. The block does not auto-highlight — that's the\n * consumer's job (server-side, local fuzzy match, etc.).\n */\n label: React.ReactNode\n /** Optional leading icon node. */\n icon?: React.ReactNode\n /** Optional meta line shown after the label (e.g. \"Tonight · 22:00 · House of Yes\"). */\n meta?: React.ReactNode\n /** Optional trailing keyboard shortcut. Renders as `<Kbd size=\"sm\">`. */\n shortcut?: string\n /** Optional select handler — fired when the item is activated (Enter or click). */\n onSelect?: (value: string) => void\n}\n\nexport interface CommandDialogGroup {\n /** Stable identifier — used as React key. */\n id: string\n /** Group heading content. */\n heading: React.ReactNode\n /** Optional count rendered next to the heading. */\n count?: number\n items: CommandDialogItem[]\n}\n\nexport interface CommandDialogFooterHint {\n /** Single key string or array of keys (multi-key chords). */\n keys: string | string[]\n /** Hint label, e.g. \"navigate\". */\n label: React.ReactNode\n}\n\nexport interface CommandDialogProps {\n /** Controlled open state. */\n open: boolean\n /** Open-state change handler. */\n onOpenChange: (open: boolean) => void\n\n /** Input placeholder. Defaults to \"Type a command, search…\". */\n placeholder?: string\n\n /**\n * Optional scope chip rendered top-right of the input. Pass a string (\"Events\")\n * for a default \"in Events\" rendering, or a ReactNode for full control.\n */\n scope?: React.ReactNode\n\n /** Grouped results. Each group renders a heading and a list of items. */\n groups: CommandDialogGroup[]\n\n /**\n * Empty-state content rendered when no items match the active filter.\n * Defaults to \"No results.\"\n */\n emptyMessage?: React.ReactNode\n\n /** Footer hint rows (e.g. ↑↓ navigate, ↵ open). When omitted, no hints render. */\n footerHints?: CommandDialogFooterHint[]\n\n /**\n * Total result count rendered in the bottom-right of the footer. When omitted,\n * the count is derived from `groups`. Pass an explicit number to override\n * (e.g. server-paginated results where the visible groups are a window).\n */\n resultCount?: number\n\n /** Hide the derived/explicit result count in the footer. Defaults to `false`. */\n hideResultCount?: boolean\n\n /** Bind ⌘K / Ctrl+K to toggle `open`. Defaults to `true`. */\n enableShortcut?: boolean\n\n /** Additional className merged onto DialogContent. */\n className?: string\n}\n\n// ── Helpers ──────────────────────────────────────────────────────────────\n\nfunction normalizeKeys(keys: string | string[]): string[] {\n return Array.isArray(keys) ? keys : [keys]\n}\n\nfunction deriveResultCount(groups: CommandDialogGroup[]): number {\n return groups.reduce((total, group) => total + group.items.length, 0)\n}\n\nfunction renderScope(scope: React.ReactNode): React.ReactNode {\n if (typeof scope === \"string\") {\n return (\n <>\n <span\n data-slot=\"command-dialog-scope-label\"\n className={styles.scopeLabel}\n >\n in\n </span>{\" \"}\n {scope}\n </>\n )\n }\n return scope\n}\n\n// ── Component ────────────────────────────────────────────────────────────\n\nconst CommandDialog = React.forwardRef<HTMLDivElement, CommandDialogProps>(\n function CommandDialog(\n {\n open,\n onOpenChange,\n placeholder = \"Type a command, search…\",\n scope,\n groups,\n emptyMessage = \"No results.\",\n footerHints,\n resultCount,\n hideResultCount = false,\n enableShortcut = true,\n className,\n },\n ref\n ) {\n // ⌘K / Ctrl+K — toggle open. Cleans up on unmount.\n React.useEffect(() => {\n if (!enableShortcut) return\n const handler = (event: KeyboardEvent) => {\n if (event.key === \"k\" && (event.metaKey || event.ctrlKey)) {\n event.preventDefault()\n onOpenChange(!open)\n }\n }\n document.addEventListener(\"keydown\", handler)\n return () => document.removeEventListener(\"keydown\", handler)\n }, [enableShortcut, onOpenChange, open])\n\n const totalResults = resultCount ?? deriveResultCount(groups)\n const showResultCount = !hideResultCount\n const hasFooterHints = Boolean(footerHints && footerHints.length > 0)\n const showFooter = hasFooterHints || showResultCount\n\n return (\n <Dialog open={open} onOpenChange={onOpenChange}>\n <DialogContent\n ref={ref}\n className={cn(styles.dialog, className)}\n data-slot=\"command-dialog\"\n aria-describedby={undefined}\n >\n <DialogTitle className={styles.srOnly}>Command Palette</DialogTitle>\n\n <Command className={styles.command}>\n <div\n className={styles.inputRow}\n data-slot=\"command-dialog-input-row\"\n >\n <CommandInput\n placeholder={placeholder}\n className={styles.input}\n />\n {scope ? (\n <span\n data-slot=\"command-dialog-scope-chip\"\n className={styles.scope}\n >\n {renderScope(scope)}\n </span>\n ) : null}\n <Kbd size=\"sm\" className={styles.escKbd}>\n Esc\n </Kbd>\n </div>\n\n <CommandList className={styles.list}>\n <CommandEmpty className={styles.empty}>\n {emptyMessage}\n </CommandEmpty>\n\n {groups.map((group) => (\n <CommandGroup\n key={group.id}\n className={styles.group}\n heading={\n <span\n data-slot=\"command-dialog-group-heading\"\n className={styles.groupHead}\n >\n {group.heading}\n {typeof group.count === \"number\" ? (\n <span className={styles.groupCount}>{group.count}</span>\n ) : null}\n </span>\n }\n >\n {group.items.map((item) => (\n <CommandItem\n key={item.id}\n value={item.value}\n onSelect={item.onSelect}\n className={styles.item}\n >\n {item.icon ? (\n <span\n className={styles.itemIcon}\n data-slot=\"command-dialog-item-icon\"\n aria-hidden=\"true\"\n >\n {item.icon}\n </span>\n ) : null}\n <span\n className={styles.itemLabel}\n data-slot=\"command-dialog-item-label\"\n >\n {item.label}\n </span>\n {item.meta ? (\n <span\n className={styles.itemMeta}\n data-slot=\"command-dialog-item-meta\"\n >\n {item.meta}\n </span>\n ) : null}\n {item.shortcut ? (\n <Kbd\n size=\"sm\"\n data-slot=\"command-dialog-item-kbd\"\n >\n {item.shortcut}\n </Kbd>\n ) : null}\n </CommandItem>\n ))}\n </CommandGroup>\n ))}\n </CommandList>\n\n {showFooter ? (\n <footer\n className={styles.footer}\n data-slot=\"command-dialog-footer\"\n >\n {hasFooterHints ? (\n <span\n className={styles.footerHints}\n data-slot=\"command-dialog-footer-hints\"\n >\n {footerHints!.map((hint, idx) => {\n const keys = normalizeKeys(hint.keys)\n return (\n <span\n key={idx}\n className={styles.hint}\n data-slot=\"command-dialog-footer-hint\"\n >\n {keys.length > 1 ? (\n <Kbd size=\"sm\" keys={keys} />\n ) : (\n <Kbd size=\"sm\">{keys[0]}</Kbd>\n )}{\" \"}\n {hint.label}\n </span>\n )\n })}\n </span>\n ) : null}\n <span className={styles.spacer} />\n {showResultCount ? (\n <span\n className={styles.resultCount}\n data-slot=\"command-dialog-result-count\"\n >\n {totalResults} {totalResults === 1 ? \"result\" : \"results\"}\n </span>\n ) : null}\n </footer>\n ) : null}\n </Command>\n </DialogContent>\n </Dialog>\n )\n }\n)\n\nCommandDialog.displayName = \"CommandDialog\"\n\nexport { CommandDialog }\n"
|
|
3230
|
+
},
|
|
3231
|
+
{
|
|
3232
|
+
"path": "blocks/command-dialog/command-dialog.module.css",
|
|
3233
|
+
"type": "registry:block",
|
|
3234
|
+
"content": "/* CommandDialog block — ⌘K palette composing Command + Dialog primitives.\n * Ports the r3 admin-ui palette visual contract onto Visor tokens; no new\n * tokens are introduced (everything resolves through the active theme).\n */\n\n/* ── Dialog content override ─────────────────────────────────────────── */\n\n/* DialogContent reset — transparent, no padding, no border. The Command\n * primitive paints its own surface. Tight max width matches r3 (640px). */\n.dialog {\n background: transparent;\n padding: 0;\n border: 0;\n width: 40rem;\n max-width: 90vw;\n box-shadow: none;\n}\n\n/* Hide DialogContent's built-in close X — palette closes via Esc only. */\n.dialog > button:last-child {\n display: none;\n}\n\n/* Visually-hidden DialogTitle for a11y (Radix requires one). */\n.srOnly {\n position: absolute;\n width: 1px;\n height: 1px;\n padding: 0;\n margin: -1px;\n overflow: hidden;\n clip: rect(0, 0, 0, 0);\n white-space: nowrap;\n border-width: 0;\n}\n\n/* ── Command root ────────────────────────────────────────────────────── */\n\n.command {\n background: var(--surface-card, #ffffff);\n box-shadow: var(--shadow-xl, 0 30px 80px rgba(0, 0, 0, 0.6));\n border-radius: var(--radius-lg, 0.5rem);\n display: flex;\n flex-direction: column;\n overflow: hidden;\n}\n\n/* ── Input row ──────────────────────────────────────────────────────── */\n\n.inputRow {\n position: relative;\n display: flex;\n align-items: center;\n background: var(--surface-interactive-default, transparent);\n}\n\n.inputRow :global([data-slot=\"command-input-wrapper\"]) {\n flex: 1;\n border-bottom: 0;\n padding: 0.875rem 1.25rem;\n gap: 0.625rem;\n background: transparent;\n}\n\n.input {\n font-size: var(--text-16, 1rem);\n color: var(--text-primary, #111827);\n background: transparent;\n}\n\n.input::placeholder {\n color: var(--text-tertiary, #9ca3af);\n}\n\n.scope {\n display: inline-flex;\n align-items: center;\n gap: 0.25rem;\n height: 1.375rem;\n padding: 0 0.5rem;\n border-radius: var(--radius-sm, 0.25rem);\n background: var(--surface-interactive-active, rgba(0, 0, 0, 0.04));\n font-size: var(--text-11, 0.6875rem);\n color: var(--text-primary, #111827);\n margin-right: 0.625rem;\n}\n\n.scopeLabel {\n color: var(--text-tertiary, #9ca3af);\n}\n\n.escKbd {\n margin-right: 1rem;\n}\n\n/* ── Results list ───────────────────────────────────────────────────── */\n\n.list {\n max-height: 27rem;\n padding: 0.375rem 0;\n background: var(--surface-card, #ffffff);\n}\n\n.empty {\n padding: 1.5rem;\n font-size: var(--text-13, 0.8125rem);\n color: var(--text-tertiary, #9ca3af);\n text-align: center;\n}\n\n/* ── Group ──────────────────────────────────────────────────────────── */\n\n.group {\n padding: 0;\n}\n\n.group :global([cmdk-group-heading]) {\n padding: 0.375rem 1.25rem;\n font-size: var(--text-11, 0.6875rem);\n font-weight: 500;\n text-transform: uppercase;\n letter-spacing: 0.18em;\n color: var(--text-tertiary, #9ca3af);\n}\n\n.groupHead {\n display: inline-flex;\n align-items: baseline;\n gap: 0.375rem;\n}\n\n.groupCount {\n color: var(--text-tertiary, #9ca3af);\n font-size: var(--text-11, 0.6875rem);\n}\n\n/* ── Item ───────────────────────────────────────────────────────────── */\n\n.item {\n display: flex;\n align-items: center;\n gap: 0.75rem;\n height: 2.25rem;\n padding: 0 1.25rem;\n border-radius: 0;\n font-size: var(--text-14, 0.875rem);\n color: var(--text-primary, #111827);\n}\n\n.item[data-selected=\"true\"] {\n background: var(--surface-selected, rgba(0, 0, 0, 0.04));\n color: var(--text-primary, #111827);\n}\n\n.item[data-selected=\"true\"] .itemIcon {\n color: var(--text-success, #10b981);\n}\n\n.itemIcon {\n flex: 0 0 auto;\n width: 1rem;\n height: 1rem;\n color: var(--text-tertiary, #9ca3af);\n display: inline-flex;\n align-items: center;\n justify-content: center;\n}\n\n.itemLabel {\n flex: 0 0 auto;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n min-width: 0;\n}\n\n.itemMeta {\n font-size: var(--text-13, 0.8125rem);\n color: var(--text-tertiary, #9ca3af);\n margin-left: 0.5rem;\n flex: 1 1 auto;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n min-width: 0;\n}\n\n/* Hit-highlight inside labels — callers wrap matched substrings in\n * `<span data-hit>` and the rule paints them with the accent token. */\n.itemLabel :global([data-hit]) {\n color: var(--text-success, #10b981);\n}\n\n/* ── Footer ─────────────────────────────────────────────────────────── */\n\n.footer {\n display: flex;\n align-items: center;\n gap: 1rem;\n min-height: 2.25rem;\n padding: 0 1rem;\n background: var(--surface-interactive-default, transparent);\n font-size: var(--text-13, 0.8125rem);\n color: var(--text-tertiary, #9ca3af);\n}\n\n.footerHints {\n display: inline-flex;\n align-items: center;\n gap: 1rem;\n}\n\n.hint {\n display: inline-flex;\n align-items: center;\n gap: 0.375rem;\n}\n\n.spacer {\n flex: 1;\n}\n\n.resultCount {\n color: var(--text-tertiary, #9ca3af);\n}\n"
|
|
3235
|
+
}
|
|
3236
|
+
]
|
|
3237
|
+
},
|
|
3088
3238
|
{
|
|
3089
3239
|
"name": "admin-tabbed-editor",
|
|
3090
3240
|
"type": "registry:block",
|
|
@@ -3135,12 +3285,12 @@
|
|
|
3135
3285
|
{
|
|
3136
3286
|
"path": "blocks/admin-settings-page/admin-settings-page.tsx",
|
|
3137
3287
|
"type": "registry:block",
|
|
3138
|
-
"content": "\"use client\"\n\nimport * as React from \"react\"\n\nimport { cn } from \"../../lib/utils\"\nimport { PageHeader } from \"../../components/ui/page-header/page-header\"\nimport { Heading } from \"../../components/ui/heading/heading\"\nimport { Text } from \"../../components/ui/text/text\"\nimport { Separator } from \"../../components/ui/separator/separator\"\nimport { Button } from \"../../components/ui/button/button\"\nimport { ConfirmDialog } from \"../../components/ui/confirm-dialog/confirm-dialog\"\nimport styles from \"./admin-settings-page.module.css\"\n\nexport interface AdminSettingsSection {\n /** Stable identifier — used as DOM id anchor and nav highlight key. */\n id: string\n /** Label rendered inside the left nav / top tab bar. */\n label: React.ReactNode\n /** Section heading rendered above the content. */\n title: React.ReactNode\n /** Optional supporting copy rendered below the heading. */\n description?: React.ReactNode\n /** Optional leading icon rendered before the label in the nav. */\n icon?: React.ReactNode\n /** Section body — form fields or arbitrary content. */\n content: React.ReactNode\n\n // ── Per-section save mode ──────────────────────────────────────────────\n /** Dirty flag — only meaningful when the page runs in `perSectionSave` mode. */\n dirty?: boolean\n /** Busy flag — only meaningful when the page runs in `perSectionSave` mode. */\n busy?: boolean\n /** Save handler — only called in `perSectionSave` mode. Async-aware. */\n onSave?: () => void | Promise<void>\n /** Revert handler — only called in `perSectionSave` mode. */\n onRevert?: () => void\n /** Override the per-section save button label. Defaults to \"Save\". */\n saveLabel?: React.ReactNode\n /** Override the per-section revert button label. Defaults to \"Revert\". */\n revertLabel?: React.ReactNode\n\n // ── Grouped nav extras ─────────────────────────────────────────────────\n /** Trailing badge in the nav item — e.g. member count \"8\" or status \"default\".\n * Rendered as a dim trailing element in the side rail; suppressed in the top chip bar. */\n meta?: React.ReactNode\n /** Mutes the nav item's text color via `--text-tertiary` — used for \"add\" / utility actions. */\n muted?: boolean\n}\n\nexport interface AdminSettingsSectionGroup {\n /** Categorical eyebrow label rendered above this group's nav items.\n * Omit to render an ungrouped cluster (no label). */\n label?: React.ReactNode\n /** Ordered list of settings sections that belong to this group. */\n sections: AdminSettingsSection[]\n}\n\nexport interface AdminSettingsPageProps\n extends Omit<React.HTMLAttributes<HTMLDivElement>, \"title\"> {\n // ── Header ──────────────────────────────────────────────────────────────\n /** Page title rendered inside the PageHeader. */\n title: React.ReactNode\n /** Optional eyebrow rendered above the title. */\n eyebrow?: React.ReactNode\n /** Optional supporting copy rendered below the title. */\n description?: React.ReactNode\n /** Optional breadcrumb node rendered above the title row. */\n breadcrumb?: React.ReactNode\n /** Optional header actions slot rendered on the right side of the header. */\n headerActions?: React.ReactNode\n\n // ── Sections ────────────────────────────────────────────────────────────\n /** Flat ordered list of settings sections. Use when grouping is not needed. */\n sections?: AdminSettingsSection[]\n /** Grouped sections with categorical eyebrow labels.\n * Mutually exclusive with `sections` — if both are passed, `sectionGroups` wins\n * and a dev-mode console.warn fires. */\n sectionGroups?: AdminSettingsSectionGroup[]\n\n // ── Navigation ──────────────────────────────────────────────────────────\n /** Show the section nav. Defaults to true when there is more than one section. */\n showNav?: boolean\n /** Nav position — \"left\" (sticky side rail) or \"top\" (horizontal chip bar). Defaults to \"left\". */\n navPosition?: \"left\" | \"top\"\n\n // ── Mode ────────────────────────────────────────────────────────────────\n /**\n * If true, each section renders its own save/revert footer row and the global\n * sticky footer is not rendered. Defaults to false (global single-save mode).\n */\n perSectionSave?: boolean\n\n // ── Global save footer (single-save mode only) ──────────────────────────\n /** Save handler. Async-aware — a returned Promise drives the save button's pending state. */\n onSave?: () => void | Promise<void>\n /** Cancel handler. Protected by the unsaved-changes guard when `dirty` is true. */\n onCancel?: () => void\n /** Save button label. Defaults to \"Save changes\". */\n saveLabel?: React.ReactNode\n /** Cancel button label. Defaults to \"Cancel\". */\n cancelLabel?: React.ReactNode\n /** Dirty flag — drives the save button disabled state and the cancel guard. */\n dirty?: boolean\n /** Externally-controlled busy state. Overrides internal async pending detection. */\n busy?: boolean\n /** Middle slot inside the sticky footer — e.g. \"Last saved 2 minutes ago\". */\n footerStatus?: React.ReactNode\n /** Hide the global footer. */\n hideFooter?: boolean\n\n // ── Unsaved guard (global cancel only) ──────────────────────────────────\n /** Title of the unsaved-changes confirm dialog. */\n unsavedGuardTitle?: React.ReactNode\n /** Description of the unsaved-changes confirm dialog. */\n unsavedGuardDescription?: React.ReactNode\n /** Confirm (discard) label. Defaults to \"Discard\". */\n unsavedGuardConfirmLabel?: React.ReactNode\n /** Cancel (keep editing) label. Defaults to \"Keep editing\". */\n unsavedGuardCancelLabel?: React.ReactNode\n}\n\nconst DEFAULT_UNSAVED_DESCRIPTION =\n \"You have unsaved changes that will be lost if you leave this page.\"\n\n/** Internal helper: returns the flat section list used by the observer, refs,\n * and main-column renderer. When `sectionGroups` is provided it wins; `sections`\n * is the flat fallback. */\nfunction flattenSections(\n sections: AdminSettingsSection[] | undefined,\n sectionGroups: AdminSettingsSectionGroup[] | undefined\n): AdminSettingsSection[] {\n if (sectionGroups) return sectionGroups.flatMap((g) => g.sections)\n return sections ?? []\n}\n\nconst AdminSettingsPage = React.forwardRef<\n HTMLDivElement,\n AdminSettingsPageProps\n>(function AdminSettingsPage(\n {\n title,\n eyebrow,\n description,\n breadcrumb,\n headerActions,\n sections,\n sectionGroups,\n showNav,\n navPosition = \"left\",\n perSectionSave = false,\n onSave,\n onCancel,\n saveLabel = \"Save changes\",\n cancelLabel = \"Cancel\",\n dirty = false,\n busy,\n footerStatus,\n hideFooter = false,\n unsavedGuardTitle = \"Discard unsaved changes?\",\n unsavedGuardDescription = DEFAULT_UNSAVED_DESCRIPTION,\n unsavedGuardConfirmLabel = \"Discard\",\n unsavedGuardCancelLabel = \"Keep editing\",\n className,\n ...rest\n },\n ref\n) {\n // Dev-mode warning when both props are provided.\n if (\n process.env.NODE_ENV !== \"production\" &&\n sections !== undefined &&\n sectionGroups !== undefined\n ) {\n console.warn(\n \"[AdminSettingsPage] Both `sections` and `sectionGroups` were provided. \" +\n \"`sectionGroups` takes precedence and `sections` is ignored. \" +\n \"Pass only one of these props.\"\n )\n }\n\n const flatSections = flattenSections(sections, sectionGroups)\n const shouldShowNav = showNav ?? flatSections.length > 1\n const firstSectionId = flatSections[0]?.id\n const [activeId, setActiveId] = React.useState<string | undefined>(\n firstSectionId\n )\n const [isGlobalPending, setIsGlobalPending] = React.useState(false)\n const [pendingCancel, setPendingCancel] = React.useState(false)\n const sectionRefs = React.useRef<Map<string, HTMLElement>>(new Map())\n\n const effectiveBusy = busy ?? isGlobalPending\n\n // Intersection observer to highlight the nav item whose section is in view.\n React.useEffect(() => {\n if (!shouldShowNav) return\n if (typeof window === \"undefined\") return\n if (typeof IntersectionObserver === \"undefined\") return\n\n const elements = Array.from(sectionRefs.current.values())\n if (elements.length === 0) return\n\n const observer = new IntersectionObserver(\n (entries) => {\n // Find the topmost visible entry.\n const visible = entries\n .filter((e) => e.isIntersecting)\n .sort(\n (a, b) =>\n a.boundingClientRect.top - b.boundingClientRect.top\n )\n if (visible[0]) {\n const id = (visible[0].target as HTMLElement).id\n if (id) setActiveId(id)\n }\n },\n {\n // Trigger when a section enters the top portion of the scroll container.\n rootMargin: \"0px 0px -60% 0px\",\n threshold: [0, 0.1, 0.5, 1],\n }\n )\n\n elements.forEach((el) => observer.observe(el))\n return () => observer.disconnect()\n }, [shouldShowNav, flatSections])\n\n const registerSectionRef = React.useCallback(\n (id: string) => (node: HTMLElement | null) => {\n const map = sectionRefs.current\n if (node) {\n map.set(id, node)\n } else {\n map.delete(id)\n }\n },\n []\n )\n\n const handleNavClick = React.useCallback(\n (event: React.MouseEvent<HTMLAnchorElement>, id: string) => {\n event.preventDefault()\n const target = sectionRefs.current.get(id)\n if (target) {\n target.scrollIntoView({ block: \"start\", behavior: \"smooth\" })\n // Update hash without jumping.\n if (typeof window !== \"undefined\") {\n window.history.replaceState(null, \"\", `#${id}`)\n }\n setActiveId(id)\n }\n },\n []\n )\n\n const handleGlobalCancelClick = React.useCallback(() => {\n if (effectiveBusy) return\n if (dirty) {\n setPendingCancel(true)\n return\n }\n onCancel?.()\n }, [dirty, effectiveBusy, onCancel])\n\n const handleGlobalSaveClick = React.useCallback(async () => {\n if (!onSave) return\n const result = onSave()\n if (result && typeof (result as Promise<void>).then === \"function\") {\n setIsGlobalPending(true)\n try {\n await result\n setIsGlobalPending(false)\n } catch (err) {\n setIsGlobalPending(false)\n throw err\n }\n }\n }, [onSave])\n\n const handleGuardConfirm = React.useCallback(() => {\n if (pendingCancel) {\n setPendingCancel(false)\n onCancel?.()\n }\n }, [pendingCancel, onCancel])\n\n const handleGuardCancel = React.useCallback(() => {\n setPendingCancel(false)\n }, [])\n\n const globalSaveDisabled = effectiveBusy || !dirty\n\n return (\n <>\n <div\n ref={ref}\n className={cn(\n styles.root,\n shouldShowNav && navPosition === \"left\"\n ? styles.withLeftNav\n : styles.noLeftNav,\n className\n )}\n data-slot=\"admin-settings-page\"\n data-nav-position={shouldShowNav ? navPosition : undefined}\n {...rest}\n >\n {firstSectionId ? (\n <a className={styles.skipLink} href={`#${firstSectionId}`}>\n Skip to content\n </a>\n ) : null}\n\n <PageHeader\n className={styles.header}\n eyebrow={eyebrow}\n title={title}\n description={description}\n breadcrumb={breadcrumb}\n actions={headerActions}\n />\n\n {shouldShowNav && navPosition === \"top\" ? (\n <nav\n aria-label=\"Settings sections\"\n className={styles.topNav}\n data-slot=\"admin-settings-page-nav\"\n >\n <ul className={styles.topNavList}>\n {(sectionGroups ?? (flatSections.length > 0 ? [{ sections: flatSections }] : [])).map((group, gi) => (\n <React.Fragment key={gi}>\n {gi > 0 ? (\n <li\n role=\"separator\"\n aria-orientation=\"vertical\"\n aria-label={typeof group.label === \"string\" ? group.label : undefined}\n className={styles.topNavSeparator}\n />\n ) : null}\n {group.sections.map((section) => {\n const isActive = section.id === activeId\n return (\n <li key={section.id} className={styles.topNavItem}>\n <a\n href={`#${section.id}`}\n className={cn(\n styles.topNavLink,\n isActive && styles.navLinkActive\n )}\n aria-current={isActive ? \"true\" : undefined}\n onClick={(e) => handleNavClick(e, section.id)}\n >\n {section.icon ? (\n <span\n className={styles.navIcon}\n aria-hidden=\"true\"\n >\n {section.icon}\n </span>\n ) : null}\n <span className={styles.navLabel}>\n {section.label}\n </span>\n </a>\n </li>\n )\n })}\n </React.Fragment>\n ))}\n </ul>\n </nav>\n ) : null}\n\n <div className={styles.body}>\n {shouldShowNav && navPosition === \"left\" ? (\n <nav\n aria-label=\"Settings sections\"\n className={styles.sideNav}\n data-slot=\"admin-settings-page-nav\"\n >\n <ul className={styles.sideNavList}>\n {(sectionGroups ?? (flatSections.length > 0 ? [{ sections: flatSections }] : [])).map((group, gi) => (\n <li key={gi} className={styles.sideNavGroup}>\n {group.label ? (\n <div\n className={styles.navGroupLabel}\n aria-hidden=\"true\"\n >\n {group.label}\n </div>\n ) : null}\n <ul\n className={styles.sideNavGroupList}\n role=\"list\"\n aria-label={typeof group.label === \"string\" ? group.label : undefined}\n >\n {group.sections.map((section) => {\n const isActive = section.id === activeId\n return (\n <li key={section.id} className={styles.sideNavItem}>\n <a\n href={`#${section.id}`}\n className={cn(\n styles.sideNavLink,\n isActive && styles.navLinkActive,\n section.muted && styles.navItemMuted\n )}\n aria-current={isActive ? \"true\" : undefined}\n onClick={(e) => handleNavClick(e, section.id)}\n >\n {section.icon ? (\n <span\n className={styles.navIcon}\n aria-hidden=\"true\"\n >\n {section.icon}\n </span>\n ) : null}\n <span className={styles.navLabel}>\n {section.label}\n </span>\n {section.meta !== undefined ? (\n <span className={styles.navItemMeta}>\n {section.meta}\n </span>\n ) : null}\n </a>\n </li>\n )\n })}\n </ul>\n </li>\n ))}\n </ul>\n </nav>\n ) : null}\n\n <div\n className={styles.main}\n data-slot=\"admin-settings-page-main\"\n >\n {flatSections.map((section, index) => {\n const titleId = `${section.id}-title`\n const showSectionFooter =\n perSectionSave && (section.onSave || section.onRevert)\n const sectionSaveDisabled =\n (section.busy ?? false) || !(section.dirty ?? false)\n\n return (\n <React.Fragment key={section.id}>\n {index > 0 ? (\n <Separator className={styles.sectionSeparator} />\n ) : null}\n <section\n id={section.id}\n ref={registerSectionRef(section.id)}\n aria-labelledby={titleId}\n className={styles.section}\n data-slot=\"admin-settings-page-section\"\n >\n <header className={styles.sectionHeader}>\n <Heading\n level={2}\n size=\"lg\"\n id={titleId}\n className={styles.sectionTitle}\n >\n {section.title}\n </Heading>\n {section.description ? (\n <Text\n size=\"sm\"\n color=\"secondary\"\n className={styles.sectionDescription}\n >\n {section.description}\n </Text>\n ) : null}\n </header>\n\n <div className={styles.sectionContent}>\n {section.content}\n </div>\n\n {showSectionFooter ? (\n <div\n className={styles.sectionFooter}\n role=\"group\"\n aria-label=\"Section actions\"\n data-slot=\"admin-settings-page-section-footer\"\n >\n {section.onRevert ? (\n <Button\n type=\"button\"\n variant=\"outline\"\n onClick={section.onRevert}\n disabled={\n (section.busy ?? false) ||\n !(section.dirty ?? false)\n }\n data-slot=\"admin-settings-page-section-revert\"\n >\n {section.revertLabel ?? \"Revert\"}\n </Button>\n ) : null}\n {section.onSave ? (\n <Button\n type=\"button\"\n onClick={() => {\n const result = section.onSave?.()\n if (\n result &&\n typeof (result as Promise<void>).then ===\n \"function\"\n ) {\n // Consumer owns busy state in per-section mode.\n void result\n }\n }}\n disabled={sectionSaveDisabled}\n aria-busy={\n (section.busy ?? false) || undefined\n }\n data-slot=\"admin-settings-page-section-save\"\n >\n {section.saveLabel ?? \"Save\"}\n </Button>\n ) : null}\n </div>\n ) : null}\n </section>\n </React.Fragment>\n )\n })}\n </div>\n </div>\n\n {!perSectionSave && !hideFooter ? (\n <div\n className={styles.footer}\n data-slot=\"admin-settings-page-footer\"\n role=\"group\"\n aria-label=\"Settings actions\"\n >\n <div className={styles.footerCancel}>\n <Button\n type=\"button\"\n variant=\"outline\"\n onClick={handleGlobalCancelClick}\n disabled={effectiveBusy}\n data-slot=\"admin-settings-page-cancel\"\n >\n {cancelLabel}\n </Button>\n </div>\n {footerStatus ? (\n <div\n className={styles.footerStatus}\n data-slot=\"admin-settings-page-status\"\n >\n {footerStatus}\n </div>\n ) : null}\n <div className={styles.footerSave}>\n <Button\n type=\"button\"\n onClick={handleGlobalSaveClick}\n disabled={globalSaveDisabled}\n aria-busy={effectiveBusy || undefined}\n data-slot=\"admin-settings-page-save\"\n >\n {saveLabel}\n </Button>\n </div>\n </div>\n ) : null}\n </div>\n\n {!perSectionSave ? (\n <ConfirmDialog\n open={pendingCancel}\n onOpenChange={(next) => {\n if (!next) handleGuardCancel()\n }}\n severity=\"warning\"\n title={unsavedGuardTitle}\n description={unsavedGuardDescription}\n confirmLabel={unsavedGuardConfirmLabel}\n cancelLabel={unsavedGuardCancelLabel}\n onConfirm={handleGuardConfirm}\n onCancel={handleGuardCancel}\n />\n ) : null}\n </>\n )\n})\n\nAdminSettingsPage.displayName = \"AdminSettingsPage\"\n\nexport { AdminSettingsPage }\n"
|
|
3288
|
+
"content": "\"use client\"\n\nimport * as React from \"react\"\n\nimport { cn } from \"../../lib/utils\"\nimport { PageHeader } from \"../../components/ui/page-header/page-header\"\nimport { Heading } from \"../../components/ui/heading/heading\"\nimport { Text } from \"../../components/ui/text/text\"\nimport { Separator } from \"../../components/ui/separator/separator\"\nimport { Button } from \"../../components/ui/button/button\"\nimport { ConfirmDialog } from \"../../components/ui/confirm-dialog/confirm-dialog\"\nimport styles from \"./admin-settings-page.module.css\"\n\nexport interface AdminSettingsSection {\n /** Stable identifier — used as DOM id anchor and nav highlight key. */\n id: string\n /** Label rendered inside the left nav / top tab bar. */\n label: React.ReactNode\n /** Section heading rendered above the content. */\n title: React.ReactNode\n /** Optional small uppercase label rendered above the section title.\n * Mirrors PageHeader's eyebrow (VI-303) — same token contract. */\n eyebrow?: React.ReactNode\n /** Section title font-size override. `\"default\"` preserves the current h2\n * scale; `\"lg\" | \"xl\" | \"marquee\"` step up to display sizes. Mirrors\n * PageHeader's `titleSize` token presets (VI-303). */\n titleSize?: \"default\" | \"lg\" | \"xl\" | \"marquee\"\n /** Section title font-family override. `\"body\"` (default) inherits the body\n * family; `\"marquee\"` binds to the display family with a graceful fallback\n * to the heading family. Mirrors PageHeader's `titleFamily` (VI-303). */\n titleFamily?: \"body\" | \"marquee\"\n /** Optional supporting copy rendered below the heading. */\n description?: React.ReactNode\n /** Optional leading icon rendered before the label in the nav. */\n icon?: React.ReactNode\n /** Section body — form fields or arbitrary content. */\n content: React.ReactNode\n\n // ── Per-section save mode ──────────────────────────────────────────────\n /** Dirty flag — only meaningful when the page runs in `perSectionSave` mode. */\n dirty?: boolean\n /** Busy flag — only meaningful when the page runs in `perSectionSave` mode. */\n busy?: boolean\n /** Save handler — only called in `perSectionSave` mode. Async-aware. */\n onSave?: () => void | Promise<void>\n /** Revert handler — only called in `perSectionSave` mode. */\n onRevert?: () => void\n /** Override the per-section save button label. Defaults to \"Save\". */\n saveLabel?: React.ReactNode\n /** Override the per-section revert button label. Defaults to \"Revert\". */\n revertLabel?: React.ReactNode\n\n // ── Grouped nav extras ─────────────────────────────────────────────────\n /** Trailing badge in the nav item — e.g. member count \"8\" or status \"default\".\n * Rendered as a dim trailing element in the side rail; suppressed in the top chip bar. */\n meta?: React.ReactNode\n /** Mutes the nav item's text color via `--text-tertiary` — used for \"add\" / utility actions. */\n muted?: boolean\n}\n\nexport interface AdminSettingsSectionGroup {\n /** Categorical eyebrow label rendered above this group's nav items.\n * Omit to render an ungrouped cluster (no label). */\n label?: React.ReactNode\n /** Ordered list of settings sections that belong to this group. */\n sections: AdminSettingsSection[]\n}\n\nexport interface AdminSettingsPageProps\n extends Omit<React.HTMLAttributes<HTMLDivElement>, \"title\"> {\n // ── Header ──────────────────────────────────────────────────────────────\n /** Page title rendered inside the PageHeader. */\n title: React.ReactNode\n /** Optional eyebrow rendered above the title. */\n eyebrow?: React.ReactNode\n /** Optional supporting copy rendered below the title. */\n description?: React.ReactNode\n /** Optional breadcrumb node rendered above the title row. */\n breadcrumb?: React.ReactNode\n /** Optional header actions slot rendered on the right side of the header. */\n headerActions?: React.ReactNode\n\n // ── Sections ────────────────────────────────────────────────────────────\n /** Flat ordered list of settings sections. Use when grouping is not needed. */\n sections?: AdminSettingsSection[]\n /** Grouped sections with categorical eyebrow labels.\n * Mutually exclusive with `sections` — if both are passed, `sectionGroups` wins\n * and a dev-mode console.warn fires. */\n sectionGroups?: AdminSettingsSectionGroup[]\n\n // ── Navigation ──────────────────────────────────────────────────────────\n /** Show the section nav. Defaults to true when there is more than one section. */\n showNav?: boolean\n /** Nav position — \"left\" (sticky side rail) or \"top\" (horizontal chip bar). Defaults to \"left\". */\n navPosition?: \"left\" | \"top\"\n\n // ── Mode ────────────────────────────────────────────────────────────────\n /**\n * If true, each section renders its own save/revert footer row and the global\n * sticky footer is not rendered. Defaults to false (global single-save mode).\n */\n perSectionSave?: boolean\n\n // ── Global save footer (single-save mode only) ──────────────────────────\n /** Save handler. Async-aware — a returned Promise drives the save button's pending state. */\n onSave?: () => void | Promise<void>\n /** Cancel handler. Protected by the unsaved-changes guard when `dirty` is true. */\n onCancel?: () => void\n /** Save button label. Defaults to \"Save changes\". */\n saveLabel?: React.ReactNode\n /** Cancel button label. Defaults to \"Cancel\". */\n cancelLabel?: React.ReactNode\n /** Dirty flag — drives the save button disabled state and the cancel guard. */\n dirty?: boolean\n /** Externally-controlled busy state. Overrides internal async pending detection. */\n busy?: boolean\n /** Middle slot inside the sticky footer — e.g. \"Last saved 2 minutes ago\". */\n footerStatus?: React.ReactNode\n /** Hide the global footer. */\n hideFooter?: boolean\n /**\n * Replace the default footer entirely. When set (and `hideFooter` is false and\n * `perSectionSave` is false), the block renders this node inside a\n * `data-slot=\"admin-settings-page-custom-footer\"` wrapper instead of the default\n * Cancel + status + Save row. The caller owns layout and wiring — `onSave` /\n * `onCancel` / `saveLabel` / `cancelLabel` / `dirty` / `busy` / `footerStatus`\n * are ignored when `customFooter` is supplied.\n *\n * Order of precedence: `hideFooter` > `customFooter` > default footer.\n *\n * The unsaved-changes ConfirmDialog guard remains active — wire your custom\n * Cancel handler through the block's `onCancel` prop to preserve it.\n *\n * Mirrors `admin-detail-drawer`'s `customHeader` precedent.\n */\n customFooter?: React.ReactNode\n\n // ── Unsaved guard (global cancel only) ──────────────────────────────────\n /** Title of the unsaved-changes confirm dialog. */\n unsavedGuardTitle?: React.ReactNode\n /** Description of the unsaved-changes confirm dialog. */\n unsavedGuardDescription?: React.ReactNode\n /** Confirm (discard) label. Defaults to \"Discard\". */\n unsavedGuardConfirmLabel?: React.ReactNode\n /** Cancel (keep editing) label. Defaults to \"Keep editing\". */\n unsavedGuardCancelLabel?: React.ReactNode\n}\n\nconst DEFAULT_UNSAVED_DESCRIPTION =\n \"You have unsaved changes that will be lost if you leave this page.\"\n\n/** Internal helper: returns the flat section list used by the observer, refs,\n * and main-column renderer. When `sectionGroups` is provided it wins; `sections`\n * is the flat fallback. */\nfunction flattenSections(\n sections: AdminSettingsSection[] | undefined,\n sectionGroups: AdminSettingsSectionGroup[] | undefined\n): AdminSettingsSection[] {\n if (sectionGroups) return sectionGroups.flatMap((g) => g.sections)\n return sections ?? []\n}\n\nconst AdminSettingsPage = React.forwardRef<\n HTMLDivElement,\n AdminSettingsPageProps\n>(function AdminSettingsPage(\n {\n title,\n eyebrow,\n description,\n breadcrumb,\n headerActions,\n sections,\n sectionGroups,\n showNav,\n navPosition = \"left\",\n perSectionSave = false,\n onSave,\n onCancel,\n saveLabel = \"Save changes\",\n cancelLabel = \"Cancel\",\n dirty = false,\n busy,\n footerStatus,\n hideFooter = false,\n customFooter,\n unsavedGuardTitle = \"Discard unsaved changes?\",\n unsavedGuardDescription = DEFAULT_UNSAVED_DESCRIPTION,\n unsavedGuardConfirmLabel = \"Discard\",\n unsavedGuardCancelLabel = \"Keep editing\",\n className,\n ...rest\n },\n ref\n) {\n // Dev-mode warning when both props are provided.\n if (\n process.env.NODE_ENV !== \"production\" &&\n sections !== undefined &&\n sectionGroups !== undefined\n ) {\n console.warn(\n \"[AdminSettingsPage] Both `sections` and `sectionGroups` were provided. \" +\n \"`sectionGroups` takes precedence and `sections` is ignored. \" +\n \"Pass only one of these props.\"\n )\n }\n\n // Dev-mode warning when `customFooter` is combined with the default-footer\n // sugar props it overrides. `customFooter` wins; the sugar props are ignored.\n if (\n process.env.NODE_ENV !== \"production\" &&\n customFooter !== undefined &&\n footerStatus !== undefined\n ) {\n console.warn(\n \"[AdminSettingsPage] Both `customFooter` and `footerStatus` were provided. \" +\n \"`customFooter` replaces the default footer entirely, so `footerStatus` \" +\n \"is ignored. Compose the status into your custom footer node instead.\"\n )\n }\n\n // Dev-mode warning when `customFooter` is combined with `perSectionSave`. The\n // block does not render a global footer in per-section mode, so the custom\n // footer would never render.\n if (\n process.env.NODE_ENV !== \"production\" &&\n customFooter !== undefined &&\n perSectionSave\n ) {\n console.warn(\n \"[AdminSettingsPage] `customFooter` is ignored when `perSectionSave` is \" +\n \"true — per-section footers render inline and no block-level footer is \" +\n \"shown. Drop `customFooter` or disable `perSectionSave`.\"\n )\n }\n\n const flatSections = flattenSections(sections, sectionGroups)\n const shouldShowNav = showNav ?? flatSections.length > 1\n const firstSectionId = flatSections[0]?.id\n const [activeId, setActiveId] = React.useState<string | undefined>(\n firstSectionId\n )\n const [isGlobalPending, setIsGlobalPending] = React.useState(false)\n const [pendingCancel, setPendingCancel] = React.useState(false)\n const sectionRefs = React.useRef<Map<string, HTMLElement>>(new Map())\n\n const effectiveBusy = busy ?? isGlobalPending\n\n // Intersection observer to highlight the nav item whose section is in view.\n React.useEffect(() => {\n if (!shouldShowNav) return\n if (typeof window === \"undefined\") return\n if (typeof IntersectionObserver === \"undefined\") return\n\n const elements = Array.from(sectionRefs.current.values())\n if (elements.length === 0) return\n\n const observer = new IntersectionObserver(\n (entries) => {\n // Find the topmost visible entry.\n const visible = entries\n .filter((e) => e.isIntersecting)\n .sort(\n (a, b) =>\n a.boundingClientRect.top - b.boundingClientRect.top\n )\n if (visible[0]) {\n const id = (visible[0].target as HTMLElement).id\n if (id) setActiveId(id)\n }\n },\n {\n // Trigger when a section enters the top portion of the scroll container.\n rootMargin: \"0px 0px -60% 0px\",\n threshold: [0, 0.1, 0.5, 1],\n }\n )\n\n elements.forEach((el) => observer.observe(el))\n return () => observer.disconnect()\n }, [shouldShowNav, flatSections])\n\n const registerSectionRef = React.useCallback(\n (id: string) => (node: HTMLElement | null) => {\n const map = sectionRefs.current\n if (node) {\n map.set(id, node)\n } else {\n map.delete(id)\n }\n },\n []\n )\n\n const handleNavClick = React.useCallback(\n (event: React.MouseEvent<HTMLAnchorElement>, id: string) => {\n event.preventDefault()\n const target = sectionRefs.current.get(id)\n if (target) {\n target.scrollIntoView({ block: \"start\", behavior: \"smooth\" })\n // Update hash without jumping.\n if (typeof window !== \"undefined\") {\n window.history.replaceState(null, \"\", `#${id}`)\n }\n setActiveId(id)\n }\n },\n []\n )\n\n const handleGlobalCancelClick = React.useCallback(() => {\n if (effectiveBusy) return\n if (dirty) {\n setPendingCancel(true)\n return\n }\n onCancel?.()\n }, [dirty, effectiveBusy, onCancel])\n\n const handleGlobalSaveClick = React.useCallback(async () => {\n if (!onSave) return\n const result = onSave()\n if (result && typeof (result as Promise<void>).then === \"function\") {\n setIsGlobalPending(true)\n try {\n await result\n setIsGlobalPending(false)\n } catch (err) {\n setIsGlobalPending(false)\n throw err\n }\n }\n }, [onSave])\n\n const handleGuardConfirm = React.useCallback(() => {\n if (pendingCancel) {\n setPendingCancel(false)\n onCancel?.()\n }\n }, [pendingCancel, onCancel])\n\n const handleGuardCancel = React.useCallback(() => {\n setPendingCancel(false)\n }, [])\n\n const globalSaveDisabled = effectiveBusy || !dirty\n\n return (\n <>\n <div\n ref={ref}\n className={cn(\n styles.root,\n shouldShowNav && navPosition === \"left\"\n ? styles.withLeftNav\n : styles.noLeftNav,\n className\n )}\n data-slot=\"admin-settings-page\"\n data-nav-position={shouldShowNav ? navPosition : undefined}\n {...rest}\n >\n {firstSectionId ? (\n <a className={styles.skipLink} href={`#${firstSectionId}`}>\n Skip to content\n </a>\n ) : null}\n\n <PageHeader\n className={styles.header}\n eyebrow={eyebrow}\n title={title}\n description={description}\n breadcrumb={breadcrumb}\n actions={headerActions}\n />\n\n {shouldShowNav && navPosition === \"top\" ? (\n <nav\n aria-label=\"Settings sections\"\n className={styles.topNav}\n data-slot=\"admin-settings-page-nav\"\n >\n <ul className={styles.topNavList}>\n {(sectionGroups ?? (flatSections.length > 0 ? [{ sections: flatSections }] : [])).map((group, gi) => (\n <React.Fragment key={gi}>\n {gi > 0 ? (\n <li\n role=\"separator\"\n aria-orientation=\"vertical\"\n aria-label={typeof group.label === \"string\" ? group.label : undefined}\n className={styles.topNavSeparator}\n />\n ) : null}\n {group.sections.map((section) => {\n const isActive = section.id === activeId\n return (\n <li key={section.id} className={styles.topNavItem}>\n <a\n href={`#${section.id}`}\n className={cn(\n styles.topNavLink,\n isActive && styles.navLinkActive\n )}\n aria-current={isActive ? \"true\" : undefined}\n onClick={(e) => handleNavClick(e, section.id)}\n >\n {section.icon ? (\n <span\n className={styles.navIcon}\n aria-hidden=\"true\"\n >\n {section.icon}\n </span>\n ) : null}\n <span className={styles.navLabel}>\n {section.label}\n </span>\n </a>\n </li>\n )\n })}\n </React.Fragment>\n ))}\n </ul>\n </nav>\n ) : null}\n\n <div className={styles.body}>\n {shouldShowNav && navPosition === \"left\" ? (\n <nav\n aria-label=\"Settings sections\"\n className={styles.sideNav}\n data-slot=\"admin-settings-page-nav\"\n >\n <div\n className={styles.sideNavSticky}\n data-slot=\"admin-settings-page-side-nav-sticky\"\n >\n <ul className={styles.sideNavList}>\n {(sectionGroups ?? (flatSections.length > 0 ? [{ sections: flatSections }] : [])).map((group, gi) => (\n <li key={gi} className={styles.sideNavGroup}>\n {group.label ? (\n <div\n className={styles.navGroupLabel}\n aria-hidden=\"true\"\n >\n {group.label}\n </div>\n ) : null}\n <ul\n className={styles.sideNavGroupList}\n role=\"list\"\n aria-label={typeof group.label === \"string\" ? group.label : undefined}\n >\n {group.sections.map((section) => {\n const isActive = section.id === activeId\n return (\n <li key={section.id} className={styles.sideNavItem}>\n <a\n href={`#${section.id}`}\n className={cn(\n styles.sideNavLink,\n isActive && styles.navLinkActive,\n section.muted && styles.navItemMuted\n )}\n aria-current={isActive ? \"true\" : undefined}\n onClick={(e) => handleNavClick(e, section.id)}\n >\n {section.icon ? (\n <span\n className={styles.navIcon}\n aria-hidden=\"true\"\n >\n {section.icon}\n </span>\n ) : null}\n <span className={styles.navLabel}>\n {section.label}\n </span>\n {section.meta !== undefined ? (\n <span className={styles.navItemMeta}>\n {section.meta}\n </span>\n ) : null}\n </a>\n </li>\n )\n })}\n </ul>\n </li>\n ))}\n </ul>\n </div>\n </nav>\n ) : null}\n\n <div\n className={styles.main}\n data-slot=\"admin-settings-page-main\"\n >\n {flatSections.map((section, index) => {\n const titleId = `${section.id}-title`\n const showSectionFooter =\n perSectionSave && (section.onSave || section.onRevert)\n const sectionSaveDisabled =\n (section.busy ?? false) || !(section.dirty ?? false)\n\n return (\n <React.Fragment key={section.id}>\n {index > 0 ? (\n <Separator className={styles.sectionSeparator} />\n ) : null}\n <section\n id={section.id}\n ref={registerSectionRef(section.id)}\n aria-labelledby={titleId}\n className={styles.section}\n data-slot=\"admin-settings-page-section\"\n >\n <header\n className={styles.sectionHeader}\n data-slot=\"admin-settings-section-header\"\n >\n {section.eyebrow ? (\n <p\n data-slot=\"admin-settings-section-eyebrow\"\n className={styles.sectionEyebrow}\n >\n {section.eyebrow}\n </p>\n ) : null}\n <Heading\n level={2}\n size=\"lg\"\n id={titleId}\n data-slot=\"admin-settings-section-title\"\n data-title-size={section.titleSize}\n data-title-family={section.titleFamily}\n className={styles.sectionTitle}\n >\n {section.title}\n </Heading>\n {section.description ? (\n <Text\n size=\"sm\"\n color=\"secondary\"\n className={styles.sectionDescription}\n >\n {section.description}\n </Text>\n ) : null}\n </header>\n\n <div className={styles.sectionContent}>\n {section.content}\n </div>\n\n {showSectionFooter ? (\n <div\n className={styles.sectionFooter}\n role=\"group\"\n aria-label=\"Section actions\"\n data-slot=\"admin-settings-page-section-footer\"\n >\n {section.onRevert ? (\n <Button\n type=\"button\"\n variant=\"outline\"\n onClick={section.onRevert}\n disabled={\n (section.busy ?? false) ||\n !(section.dirty ?? false)\n }\n data-slot=\"admin-settings-page-section-revert\"\n >\n {section.revertLabel ?? \"Revert\"}\n </Button>\n ) : null}\n {section.onSave ? (\n <Button\n type=\"button\"\n onClick={() => {\n const result = section.onSave?.()\n if (\n result &&\n typeof (result as Promise<void>).then ===\n \"function\"\n ) {\n // Consumer owns busy state in per-section mode.\n void result\n }\n }}\n disabled={sectionSaveDisabled}\n aria-busy={\n (section.busy ?? false) || undefined\n }\n data-slot=\"admin-settings-page-section-save\"\n >\n {section.saveLabel ?? \"Save\"}\n </Button>\n ) : null}\n </div>\n ) : null}\n </section>\n </React.Fragment>\n )\n })}\n </div>\n </div>\n\n {!perSectionSave && !hideFooter ? (\n customFooter !== undefined ? (\n <div\n className={styles.customFooter}\n data-slot=\"admin-settings-page-custom-footer\"\n >\n {customFooter}\n </div>\n ) : (\n <div\n className={styles.footer}\n data-slot=\"admin-settings-page-footer\"\n role=\"group\"\n aria-label=\"Settings actions\"\n >\n <div className={styles.footerCancel}>\n <Button\n type=\"button\"\n variant=\"outline\"\n onClick={handleGlobalCancelClick}\n disabled={effectiveBusy}\n data-slot=\"admin-settings-page-cancel\"\n >\n {cancelLabel}\n </Button>\n </div>\n {footerStatus ? (\n <div\n className={styles.footerStatus}\n data-slot=\"admin-settings-page-status\"\n >\n {footerStatus}\n </div>\n ) : null}\n <div className={styles.footerSave}>\n <Button\n type=\"button\"\n onClick={handleGlobalSaveClick}\n disabled={globalSaveDisabled}\n aria-busy={effectiveBusy || undefined}\n data-slot=\"admin-settings-page-save\"\n >\n {saveLabel}\n </Button>\n </div>\n </div>\n )\n ) : null}\n </div>\n\n {!perSectionSave ? (\n <ConfirmDialog\n open={pendingCancel}\n onOpenChange={(next) => {\n if (!next) handleGuardCancel()\n }}\n severity=\"warning\"\n title={unsavedGuardTitle}\n description={unsavedGuardDescription}\n confirmLabel={unsavedGuardConfirmLabel}\n cancelLabel={unsavedGuardCancelLabel}\n onConfirm={handleGuardConfirm}\n onCancel={handleGuardCancel}\n />\n ) : null}\n </>\n )\n})\n\nAdminSettingsPage.displayName = \"AdminSettingsPage\"\n\nexport { AdminSettingsPage }\n"
|
|
3139
3289
|
},
|
|
3140
3290
|
{
|
|
3141
3291
|
"path": "blocks/admin-settings-page/admin-settings-page.module.css",
|
|
3142
3292
|
"type": "registry:block",
|
|
3143
|
-
"content": "/* Admin Settings Page\n * Long scrollable settings archetype: PageHeader → optional nav + main\n * column of stacked sections → optional sticky global save footer. Nav can\n * live to the left (sticky rail) or above (horizontal chip bar). At narrow\n * container widths the side nav collapses above the main column.\n */\n\n.root {\n container-type: inline-size;\n display: flex;\n flex-direction: column;\n min-height: 0;\n width: 100%;\n gap: 0;\n background-color: var(--surface-page, #ffffff);\n color: var(--text-primary, #111827);\n scroll-behavior: smooth;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .root {\n scroll-behavior: auto;\n }\n}\n\n/* Visually hidden skip link — becomes visible on focus so keyboard users\n * can jump past the header and nav into the first section. */\n.skipLink {\n position: absolute;\n left: var(--spacing-2, 0.5rem);\n top: var(--spacing-2, 0.5rem);\n z-index: 2;\n padding: var(--spacing-2, 0.5rem) var(--spacing-3, 0.75rem);\n background-color: var(--surface-page, #ffffff);\n color: var(--text-primary, #111827);\n border: 1px solid var(--border-default, #d1d5db);\n border-radius: var(--radius-md, 0.5rem);\n box-shadow: var(--shadow-sm);\n font-size: var(--font-size-sm, 0.875rem);\n text-decoration: none;\n transform: translateY(-200%);\n transition:\n transform var(--motion-duration-fast, 150ms)\n var(--motion-easing-standard, ease);\n}\n\n.skipLink:focus-visible {\n transform: translateY(0);\n outline: var(--focus-ring-width, 2px) solid\n var(--focus-ring-color, #2563eb);\n outline-offset: var(--focus-ring-offset, 2px);\n}\n\n.header {\n padding: var(--spacing-6, 1.5rem);\n border-bottom: 1px solid var(--border-muted, #e5e7eb);\n}\n\n/* Top nav variant — horizontal chip bar rendered below the header. */\n.topNav {\n flex: 0 0 auto;\n padding: var(--spacing-3, 0.75rem) var(--spacing-6, 1.5rem);\n border-bottom: 1px solid var(--border-muted, #e5e7eb);\n overflow-x: auto;\n overflow-y: hidden;\n}\n\n.topNavList {\n display: flex;\n flex-direction: row;\n align-items: center;\n gap: var(--spacing-2, 0.5rem);\n margin: 0;\n padding: 0;\n list-style: none;\n}\n\n.topNavItem {\n flex: 0 0 auto;\n}\n\n/* Group separator in the top chip bar — 1px vertical hairline between clusters. */\n.topNavSeparator {\n flex: 0 0 auto;\n width: var(--stroke-width-thin, 1px);\n align-self: stretch;\n background-color: var(--border-muted, #e5e7eb);\n margin: 0 var(--spacing-2, 0.5rem);\n list-style: none;\n}\n\n.topNavLink {\n display: inline-flex;\n align-items: center;\n gap: var(--spacing-2, 0.5rem);\n padding: var(--spacing-2, 0.5rem) var(--spacing-3, 0.75rem);\n border-radius: var(--radius-md, 0.5rem);\n color: var(--text-secondary, #6b7280);\n font-size: var(--font-size-sm, 0.875rem);\n font-weight: 500;\n text-decoration: none;\n white-space: nowrap;\n transition:\n background-color var(--motion-duration-fast, 150ms)\n var(--motion-easing-standard, ease),\n color var(--motion-duration-fast, 150ms)\n var(--motion-easing-standard, ease);\n}\n\n.topNavLink:hover {\n background-color: var(--surface-interactive-hover, #f3f4f6);\n color: var(--text-primary, #111827);\n}\n\n.topNavLink:focus-visible {\n outline: var(--focus-ring-width, 2px) solid\n var(--focus-ring-color, #2563eb);\n outline-offset: var(--focus-ring-offset, 2px);\n}\n\n/* Body = optional left nav + main column. */\n.body {\n flex: 1 1 auto;\n min-height: 0;\n display: grid;\n grid-template-columns: 1fr;\n width: 100%;\n}\n\n.withLeftNav .body {\n grid-template-columns: minmax(12rem, 16rem) 1fr;\n}\n\n/* Side nav — sticky rail pinned to the top of the scroll container. */\n.sideNav {\n position: sticky;\n top: 0;\n align-self: start;\n padding: var(--spacing-6, 1.5rem) var(--spacing-4, 1rem)\n var(--spacing-6, 1.5rem) var(--spacing-6, 1.5rem);\n border-right: 1px solid var(--border-muted, #e5e7eb);\n max-height: 100vh;\n overflow-y: auto;\n}\n\n.sideNavList {\n display: flex;\n flex-direction: column;\n gap: 0;\n margin: 0;\n padding: 0;\n list-style: none;\n}\n\n/* Group container — wraps the optional eyebrow label + a list of nav items. */\n.sideNavGroup {\n display: flex;\n flex-direction: column;\n}\n\n/* Categorical eyebrow label above a group's nav items.\n * Reuses the same token set as PageHeader.eyebrow for visual consistency. */\n.navGroupLabel {\n font-size: var(--font-size-xs, 0.75rem);\n font-weight: var(--font-weight-semibold, 600);\n letter-spacing: var(--letter-spacing-wide, 0.05em);\n text-transform: uppercase;\n color: var(--text-tertiary, #6b7280);\n padding: var(--spacing-5, 1.25rem) var(--spacing-3, 0.75rem)\n var(--spacing-2, 0.5rem);\n line-height: var(--line-height-tight, 1.25);\n}\n\n/* Nested list of sections within a group — no bullet, no extra padding. */\n.sideNavGroupList {\n display: flex;\n flex-direction: column;\n gap: var(--spacing-1, 0.25rem);\n margin: 0;\n padding: 0;\n list-style: none;\n}\n\n.sideNavItem {\n display: block;\n}\n\n/* Muted nav item — dim text for \"add\" / utility actions. */\n.navItemMuted.sideNavLink {\n color: var(--text-tertiary, #6b7280);\n}\n\n/* Trailing badge inside a side-nav link — e.g. member count or status. */\n.navItemMeta {\n margin-left: auto;\n flex: 0 0 auto;\n font-size: var(--font-size-xs, 0.75rem);\n color: var(--text-tertiary, #6b7280);\n line-height: 1;\n}\n\n.sideNavLink {\n display: flex;\n align-items: center;\n gap: var(--spacing-2, 0.5rem);\n width: 100%;\n padding: var(--spacing-2, 0.5rem) var(--spacing-3, 0.75rem);\n border-radius: var(--radius-md, 0.5rem);\n color: var(--text-secondary, #6b7280);\n font-size: var(--font-size-sm, 0.875rem);\n font-weight: 500;\n text-decoration: none;\n transition:\n background-color var(--motion-duration-fast, 150ms)\n var(--motion-easing-standard, ease),\n color var(--motion-duration-fast, 150ms)\n var(--motion-easing-standard, ease);\n}\n\n.sideNavLink:hover {\n background-color: var(--surface-interactive-hover, #f3f4f6);\n color: var(--text-primary, #111827);\n}\n\n.sideNavLink:focus-visible {\n outline: var(--focus-ring-width, 2px) solid\n var(--focus-ring-color, #2563eb);\n outline-offset: var(--focus-ring-offset, 2px);\n}\n\n.navLinkActive {\n background-color: var(--surface-interactive-active, #e5e7eb);\n color: var(--text-primary, #111827);\n font-weight: 600;\n}\n\n.navIcon {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex: 0 0 auto;\n width: 1em;\n height: 1em;\n font-size: var(--font-size-base, 1rem);\n line-height: 1;\n}\n\n.navLabel {\n display: inline-flex;\n align-items: center;\n min-width: 0;\n}\n\n/* Main column — stacked sections. */\n.main {\n min-width: 0;\n display: flex;\n flex-direction: column;\n padding: var(--spacing-6, 1.5rem);\n gap: var(--spacing-6, 1.5rem);\n}\n\n.section {\n display: flex;\n flex-direction: column;\n gap: var(--spacing-4, 1rem);\n scroll-margin-top: var(--spacing-6, 1.5rem);\n}\n\n.sectionHeader {\n display: flex;\n flex-direction: column;\n gap: var(--spacing-1, 0.25rem);\n}\n\n.sectionTitle {\n margin: 0;\n}\n\n.sectionDescription {\n margin: 0;\n}\n\n.sectionContent {\n display: flex;\n flex-direction: column;\n gap: var(--spacing-4, 1rem);\n}\n\n.sectionSeparator {\n margin: 0;\n}\n\n.sectionFooter {\n display: flex;\n flex-direction: row;\n align-items: center;\n justify-content: flex-end;\n gap: var(--spacing-2, 0.5rem);\n padding-top: var(--spacing-2, 0.5rem);\n}\n\n/* Sticky global footer — pinned to the bottom of the scroll container. */\n.footer {\n position: sticky;\n bottom: 0;\n z-index: 1;\n flex: 0 0 auto;\n display: flex;\n flex-direction: row;\n align-items: center;\n gap: var(--spacing-3, 0.75rem);\n padding: var(--spacing-4, 1rem) var(--spacing-6, 1.5rem);\n border-top: 1px solid var(--border-muted, #e5e7eb);\n background-color: var(--surface-page, #ffffff);\n box-shadow: var(--shadow-sm);\n}\n\n.footerCancel {\n flex: 0 0 auto;\n}\n\n.footerStatus {\n flex: 1 1 auto;\n min-width: 0;\n font-size: var(--font-size-sm, 0.875rem);\n color: var(--text-secondary, #6b7280);\n text-align: center;\n}\n\n.footerSave {\n flex: 0 0 auto;\n margin-left: auto;\n}\n\n.footerStatus + .footerSave {\n margin-left: 0;\n}\n\n/* Narrow container — collapse left nav above the main column and let the\n * footer wrap the status below the action buttons. */\n@container (max-width: 48rem) {\n .withLeftNav .body {\n grid-template-columns: 1fr;\n }\n\n .sideNav {\n position: static;\n max-height: none;\n padding: var(--spacing-3, 0.75rem) var(--spacing-6, 1.5rem);\n border-right: none;\n border-bottom: 1px solid var(--border-muted, #e5e7eb);\n overflow-x: auto;\n overflow-y: hidden;\n }\n\n .sideNavList {\n flex-direction: row;\n gap: var(--spacing-2, 0.5rem);\n align-items: center;\n }\n\n /* Groups collapse into an inline row — no visible grouping at narrow width. */\n .sideNavGroup {\n flex-direction: row;\n gap: var(--spacing-2, 0.5rem);\n align-items: center;\n }\n\n /* Hide eyebrow labels in collapsed horizontal strip — no room for them. */\n .navGroupLabel {\n display: none;\n }\n\n .sideNavGroupList {\n flex-direction: row;\n gap: var(--spacing-2, 0.5rem);\n }\n\n .sideNavItem {\n flex: 0 0 auto;\n }\n\n /* Hide meta badges in collapsed strip — space is too constrained. */\n .navItemMeta {\n display: none;\n }\n\n .footer {\n flex-wrap: wrap;\n }\n\n .footerStatus {\n order: 3;\n flex-basis: 100%;\n text-align: left;\n }\n}\n"
|
|
3293
|
+
"content": "/* Admin Settings Page\n * Long scrollable settings archetype: PageHeader → optional nav + main\n * column of stacked sections → optional sticky global save footer. Nav can\n * live to the left (sticky rail) or above (horizontal chip bar). At narrow\n * container widths the side nav collapses above the main column.\n */\n\n.root {\n container-type: inline-size;\n display: flex;\n flex-direction: column;\n min-height: 0;\n width: 100%;\n gap: 0;\n background-color: var(--surface-page, #ffffff);\n color: var(--text-primary, #111827);\n scroll-behavior: smooth;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .root {\n scroll-behavior: auto;\n }\n}\n\n/* Visually hidden skip link — becomes visible on focus so keyboard users\n * can jump past the header and nav into the first section. */\n.skipLink {\n position: absolute;\n left: var(--spacing-2, 0.5rem);\n top: var(--spacing-2, 0.5rem);\n z-index: 2;\n padding: var(--spacing-2, 0.5rem) var(--spacing-3, 0.75rem);\n background-color: var(--surface-page, #ffffff);\n color: var(--text-primary, #111827);\n border: 1px solid var(--border-default, #d1d5db);\n border-radius: var(--radius-md, 0.5rem);\n box-shadow: var(--shadow-sm);\n font-size: var(--font-size-sm, 0.875rem);\n text-decoration: none;\n transform: translateY(-200%);\n transition:\n transform var(--motion-duration-fast, 150ms)\n var(--motion-easing-standard, ease);\n}\n\n.skipLink:focus-visible {\n transform: translateY(0);\n outline: var(--focus-ring-width, 2px) solid\n var(--focus-ring-color, #2563eb);\n outline-offset: var(--focus-ring-offset, 2px);\n}\n\n.header {\n padding: var(--spacing-6, 1.5rem);\n border-bottom: 1px solid var(--border-muted, #e5e7eb);\n}\n\n/* Top nav variant — horizontal chip bar rendered below the header. */\n.topNav {\n flex: 0 0 auto;\n padding: var(--spacing-3, 0.75rem) var(--spacing-6, 1.5rem);\n border-bottom: 1px solid var(--border-muted, #e5e7eb);\n overflow-x: auto;\n overflow-y: hidden;\n}\n\n.topNavList {\n display: flex;\n flex-direction: row;\n align-items: center;\n gap: var(--spacing-2, 0.5rem);\n margin: 0;\n padding: 0;\n list-style: none;\n}\n\n.topNavItem {\n flex: 0 0 auto;\n}\n\n/* Group separator in the top chip bar — 1px vertical hairline between clusters. */\n.topNavSeparator {\n flex: 0 0 auto;\n width: var(--stroke-width-thin, 1px);\n align-self: stretch;\n background-color: var(--border-muted, #e5e7eb);\n margin: 0 var(--spacing-2, 0.5rem);\n list-style: none;\n}\n\n.topNavLink {\n display: inline-flex;\n align-items: center;\n gap: var(--spacing-2, 0.5rem);\n padding: var(--spacing-2, 0.5rem) var(--spacing-3, 0.75rem);\n border-radius: var(--radius-md, 0.5rem);\n color: var(--text-secondary, #6b7280);\n font-size: var(--font-size-sm, 0.875rem);\n font-weight: 500;\n text-decoration: none;\n white-space: nowrap;\n transition:\n background-color var(--motion-duration-fast, 150ms)\n var(--motion-easing-standard, ease),\n color var(--motion-duration-fast, 150ms)\n var(--motion-easing-standard, ease);\n}\n\n.topNavLink:hover {\n background-color: var(--surface-interactive-hover, #f3f4f6);\n color: var(--text-primary, #111827);\n}\n\n.topNavLink:focus-visible {\n outline: var(--focus-ring-width, 2px) solid\n var(--focus-ring-color, #2563eb);\n outline-offset: var(--focus-ring-offset, 2px);\n}\n\n/* Body = optional left nav + main column. */\n.body {\n flex: 1 1 auto;\n min-height: 0;\n display: grid;\n grid-template-columns: 1fr;\n width: 100%;\n}\n\n.withLeftNav .body {\n grid-template-columns: var(--admin-settings-page-nav-width, minmax(12rem, 16rem)) 1fr;\n}\n\n/* Side nav — stretching grid track. The outer rail fills the grid track so\n * its surface (border-right, background) extends to the bottom of the body.\n * The inner `.sideNavSticky` div carries the sticky scroll-anchor behavior. */\n.sideNav {\n align-self: stretch;\n min-height: 100%;\n border-right: 1px solid var(--border-muted, #e5e7eb);\n display: flex;\n flex-direction: column;\n}\n\n/* Inner sticky pane — pinned to the top of the scroll container, scrolls\n * internally when the nav contents exceed the viewport height. */\n.sideNavSticky {\n position: sticky;\n top: 0;\n padding: var(--spacing-6, 1.5rem) var(--spacing-4, 1rem)\n var(--spacing-6, 1.5rem) var(--spacing-6, 1.5rem);\n max-height: 100vh;\n overflow-y: auto;\n}\n\n.sideNavList {\n display: flex;\n flex-direction: column;\n gap: 0;\n margin: 0;\n padding: 0;\n list-style: none;\n}\n\n/* Group container — wraps the optional eyebrow label + a list of nav items. */\n.sideNavGroup {\n display: flex;\n flex-direction: column;\n}\n\n/* Categorical eyebrow label above a group's nav items.\n * Reuses the same token set as PageHeader.eyebrow for visual consistency. */\n.navGroupLabel {\n font-size: var(--font-size-xs, 0.75rem);\n font-weight: var(--font-weight-semibold, 600);\n letter-spacing: var(--letter-spacing-wide, 0.05em);\n text-transform: uppercase;\n color: var(--text-tertiary, #6b7280);\n padding: var(--spacing-5, 1.25rem) var(--spacing-3, 0.75rem)\n var(--spacing-2, 0.5rem);\n line-height: var(--line-height-tight, 1.25);\n}\n\n/* Nested list of sections within a group — no bullet, no extra padding. */\n.sideNavGroupList {\n display: flex;\n flex-direction: column;\n gap: var(--spacing-1, 0.25rem);\n margin: 0;\n padding: 0;\n list-style: none;\n}\n\n.sideNavItem {\n display: block;\n}\n\n/* Muted nav item — dim text for \"add\" / utility actions. */\n.navItemMuted.sideNavLink {\n color: var(--text-tertiary, #6b7280);\n}\n\n/* Trailing badge inside a side-nav link — e.g. member count or status. */\n.navItemMeta {\n margin-left: auto;\n flex: 0 0 auto;\n font-size: var(--font-size-xs, 0.75rem);\n color: var(--text-tertiary, #6b7280);\n line-height: 1;\n}\n\n.sideNavLink {\n display: flex;\n align-items: center;\n gap: var(--spacing-2, 0.5rem);\n width: 100%;\n padding: var(--spacing-2, 0.5rem) var(--spacing-3, 0.75rem);\n border-radius: var(--radius-md, 0.5rem);\n color: var(--text-secondary, #6b7280);\n font-size: var(--font-size-sm, 0.875rem);\n font-weight: 500;\n text-decoration: none;\n transition:\n background-color var(--motion-duration-fast, 150ms)\n var(--motion-easing-standard, ease),\n color var(--motion-duration-fast, 150ms)\n var(--motion-easing-standard, ease);\n}\n\n.sideNavLink:hover {\n background-color: var(--surface-interactive-hover, #f3f4f6);\n color: var(--text-primary, #111827);\n}\n\n.sideNavLink:focus-visible {\n outline: var(--focus-ring-width, 2px) solid\n var(--focus-ring-color, #2563eb);\n outline-offset: var(--focus-ring-offset, 2px);\n}\n\n.navLinkActive {\n background-color: var(--surface-interactive-active, #e5e7eb);\n color: var(--text-primary, #111827);\n font-weight: 600;\n}\n\n.navIcon {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex: 0 0 auto;\n width: 1em;\n height: 1em;\n font-size: var(--font-size-base, 1rem);\n line-height: 1;\n}\n\n.navLabel {\n display: inline-flex;\n align-items: center;\n min-width: 0;\n}\n\n/* Main column — stacked sections. */\n.main {\n min-width: 0;\n display: flex;\n flex-direction: column;\n padding: var(--spacing-6, 1.5rem);\n gap: var(--spacing-6, 1.5rem);\n}\n\n.section {\n display: flex;\n flex-direction: column;\n gap: var(--spacing-4, 1rem);\n scroll-margin-top: var(--spacing-6, 1.5rem);\n}\n\n.sectionHeader {\n /* Override hooks for section-title typography. Themes (or consumers via a\n wrapping className) can set these to retune the marquee title without\n forking the block. Props are sugar over the same custom properties. */\n --admin-settings-section-title-size: 3rem;\n --admin-settings-section-title-family: var(\n --font-family-display,\n var(--font-family-heading, inherit)\n );\n\n display: flex;\n flex-direction: column;\n gap: var(--spacing-1, 0.25rem);\n}\n\n/* Eyebrow: small uppercase label above the section title.\n * Reuses the same token set as PageHeader.eyebrow (VI-303) for visual\n * consistency — no new theme tokens. */\n.sectionEyebrow {\n margin: 0;\n font-size: var(--font-size-xs, 0.75rem);\n font-weight: var(--font-weight-semibold, 600);\n letter-spacing: var(--letter-spacing-wide, 0.05em);\n text-transform: uppercase;\n color: var(--text-tertiary, #6b7280);\n line-height: var(--line-height-tight, 1.25);\n}\n\n.sectionTitle {\n margin: 0;\n}\n\n/* Section title typography overrides (orthogonal to the default h2 scale).\n `data-title-size=\"lg\" | \"xl\" | \"marquee\"` step the title up through the\n display scale; `data-title-size=\"default\"` is a no-op preserving the\n current h2 lg size. Mirrors PageHeader's pattern (VI-303). */\n.sectionTitle[data-title-size=\"lg\"] {\n font-size: var(--font-size-2xl, 1.5rem);\n line-height: var(--line-height-tight, 1.2);\n}\n\n.sectionTitle[data-title-size=\"xl\"] {\n font-size: var(--font-size-3xl, 1.875rem);\n line-height: var(--line-height-tight, 1.2);\n}\n\n.sectionTitle[data-title-size=\"marquee\"] {\n font-size: var(--admin-settings-section-title-size, 3rem);\n line-height: 1;\n letter-spacing: var(--letter-spacing-tight, -0.01em);\n}\n\n/* `data-title-family=\"marquee\"` switches to the display family with a\n graceful fallback to the heading family when no display font is themed. */\n.sectionTitle[data-title-family=\"marquee\"] {\n font-family: var(\n --admin-settings-section-title-family,\n var(--font-family-display, var(--font-family-heading, inherit))\n );\n}\n\n.sectionDescription {\n margin: 0;\n}\n\n.sectionContent {\n display: flex;\n flex-direction: column;\n gap: var(--spacing-4, 1rem);\n}\n\n.sectionSeparator {\n margin: 0;\n}\n\n.sectionFooter {\n display: flex;\n flex-direction: row;\n align-items: center;\n justify-content: flex-end;\n gap: var(--spacing-2, 0.5rem);\n padding-top: var(--spacing-2, 0.5rem);\n}\n\n/* Sticky global footer — pinned to the bottom of the scroll container. */\n.footer {\n position: sticky;\n bottom: 0;\n z-index: 1;\n flex: 0 0 auto;\n display: flex;\n flex-direction: row;\n align-items: center;\n gap: var(--spacing-3, 0.75rem);\n padding: var(--spacing-4, 1rem) var(--spacing-6, 1.5rem);\n border-top: 1px solid var(--border-muted, #e5e7eb);\n background-color: var(--surface-page, #ffffff);\n box-shadow: var(--shadow-sm);\n}\n\n.footerCancel {\n flex: 0 0 auto;\n}\n\n.footerStatus {\n flex: 1 1 auto;\n min-width: 0;\n font-size: var(--font-size-sm, 0.875rem);\n color: var(--text-secondary, #6b7280);\n text-align: center;\n}\n\n.footerSave {\n flex: 0 0 auto;\n margin-left: auto;\n}\n\n.footerStatus + .footerSave {\n margin-left: 0;\n}\n\n/* Custom footer slot — replaces the default footer entirely. Pinned to the\n * bottom of the scroll container with the same surface treatment as the default\n * footer; layout inside is fully owned by the consumer. */\n.customFooter {\n position: sticky;\n bottom: 0;\n z-index: 1;\n flex: 0 0 auto;\n width: 100%;\n border-top: 1px solid var(--border-muted, #e5e7eb);\n background-color: var(--surface-page, #ffffff);\n box-shadow: var(--shadow-sm);\n}\n\n/* Narrow container — collapse left nav above the main column and let the\n * footer wrap the status below the action buttons. */\n@container (max-width: 48rem) {\n .withLeftNav .body {\n grid-template-columns: 1fr;\n }\n\n .sideNav {\n align-self: auto;\n min-height: 0;\n border-right: none;\n border-bottom: 1px solid var(--border-muted, #e5e7eb);\n display: block;\n }\n\n /* Drop the inner sticky pane back to static flow at narrow widths so the\n * collapsed horizontal strip scrolls with the page. Be explicit — don't\n * rely on inheritance from `.sideNav`. */\n .sideNavSticky {\n position: static;\n max-height: none;\n padding: var(--spacing-3, 0.75rem) var(--spacing-6, 1.5rem);\n overflow-x: auto;\n overflow-y: visible;\n }\n\n .sideNavList {\n flex-direction: row;\n gap: var(--spacing-2, 0.5rem);\n align-items: center;\n }\n\n /* Groups collapse into an inline row — no visible grouping at narrow width. */\n .sideNavGroup {\n flex-direction: row;\n gap: var(--spacing-2, 0.5rem);\n align-items: center;\n }\n\n /* Hide eyebrow labels in collapsed horizontal strip — no room for them. */\n .navGroupLabel {\n display: none;\n }\n\n .sideNavGroupList {\n flex-direction: row;\n gap: var(--spacing-2, 0.5rem);\n }\n\n .sideNavItem {\n flex: 0 0 auto;\n }\n\n /* Hide meta badges in collapsed strip — space is too constrained. */\n .navItemMeta {\n display: none;\n }\n\n .footer {\n flex-wrap: wrap;\n }\n\n .footerStatus {\n order: 3;\n flex-basis: 100%;\n text-align: left;\n }\n}\n"
|
|
3144
3294
|
}
|
|
3145
3295
|
]
|
|
3146
3296
|
},
|
|
@@ -3172,6 +3322,30 @@
|
|
|
3172
3322
|
}
|
|
3173
3323
|
]
|
|
3174
3324
|
},
|
|
3325
|
+
{
|
|
3326
|
+
"name": "right-rail-list",
|
|
3327
|
+
"type": "registry:block",
|
|
3328
|
+
"description": "Compact vertical list block tuned for admin dashboard side rails. Each row pairs an optional leading slot (label / avatar / badge / status dot), a primary label (typically a link), and an optional trailing meta value (count, value, tone-tinted status word). Trailing accepts default / mint / muted / warn / danger / info tones bound to shared semantic text tokens. Supports compact density and ul / ol / div root element variants.",
|
|
3329
|
+
"category": "data-display",
|
|
3330
|
+
"dependencies": [
|
|
3331
|
+
"@loworbitstudio/visor-core"
|
|
3332
|
+
],
|
|
3333
|
+
"registryDependencies": [
|
|
3334
|
+
"utils"
|
|
3335
|
+
],
|
|
3336
|
+
"files": [
|
|
3337
|
+
{
|
|
3338
|
+
"path": "blocks/right-rail-list/right-rail-list.tsx",
|
|
3339
|
+
"type": "registry:block",
|
|
3340
|
+
"content": "import * as React from \"react\"\n\nimport { cn } from \"../../lib/utils\"\nimport styles from \"./right-rail-list.module.css\"\n\nexport type RightRailListTone =\n | \"default\"\n | \"mint\"\n | \"muted\"\n | \"warn\"\n\nexport interface RightRailRow {\n /** Stable key for the row. */\n id: string\n /**\n * Optional leading slot — short label (e.g. \"Sun\"), avatar, badge,\n * status dot, or any other compact stamp anchoring the row's left edge.\n */\n leading?: React.ReactNode\n /**\n * Primary content — typically a link or plain text. Truncates with\n * ellipsis when overflowing the row.\n */\n primary: React.ReactNode\n /**\n * Optional trailing slot — count, value, or status word rendered at the\n * row's right edge.\n */\n trailing?: React.ReactNode\n /**\n * Tone applied to the trailing element via `data-tone`. Defaults to\n * `\"default\"`. Tones bind to the shared semantic text tokens so they\n * resolve correctly under every theme.\n */\n trailingTone?: RightRailListTone\n}\n\ntype RootElement = \"ul\" | \"ol\" | \"div\"\n\nexport interface RightRailListProps\n extends Omit<React.HTMLAttributes<HTMLElement>, \"children\"> {\n /** Row data — each entry renders as a single list row. */\n rows: RightRailRow[]\n /**\n * Tighter vertical padding for high-density rails. Defaults to `false`.\n */\n compact?: boolean\n /**\n * Root element. Defaults to `\"ul\"`. Use `\"ol\"` for ordered rankings\n * (e.g. top promoters) or `\"div\"` when the surrounding context already\n * provides list semantics.\n */\n as?: RootElement\n}\n\nconst RightRailList = React.forwardRef<HTMLElement, RightRailListProps>(\n function RightRailList(\n { rows, compact = false, as = \"ul\", className, ...rest },\n ref\n ) {\n const Root = as as React.ElementType\n const RowTag: React.ElementType = as === \"div\" ? \"div\" : \"li\"\n\n return (\n <Root\n ref={ref as React.Ref<HTMLElement>}\n className={cn(styles.root, compact && styles.rootCompact, className)}\n data-slot=\"right-rail-list\"\n data-compact={compact ? \"true\" : undefined}\n {...rest}\n >\n {rows.map((row) => (\n <RowTag\n key={row.id}\n className={styles.row}\n data-slot=\"right-rail-list-row\"\n >\n {row.leading !== undefined && row.leading !== null ? (\n <span\n className={styles.leading}\n data-slot=\"right-rail-list-leading\"\n >\n {row.leading}\n </span>\n ) : null}\n <span\n className={styles.primary}\n data-slot=\"right-rail-list-primary\"\n >\n {row.primary}\n </span>\n {row.trailing !== undefined && row.trailing !== null ? (\n <span\n className={styles.trailing}\n data-slot=\"right-rail-list-trailing\"\n data-tone={row.trailingTone ?? \"default\"}\n >\n {row.trailing}\n </span>\n ) : null}\n </RowTag>\n ))}\n </Root>\n )\n }\n)\n\nRightRailList.displayName = \"RightRailList\"\n\nexport { RightRailList }\n"
|
|
3341
|
+
},
|
|
3342
|
+
{
|
|
3343
|
+
"path": "blocks/right-rail-list/right-rail-list.module.css",
|
|
3344
|
+
"type": "registry:block",
|
|
3345
|
+
"content": "/* Right Rail List\n * Compact vertical list block tuned for dashboard side rails.\n * Each row pairs an optional leading stamp (label / avatar / badge),\n * a primary label (typically a link), and an optional trailing meta\n * value (count / value / tone-tinted status word).\n *\n * Ports the r3 admin-v7-r3 RightRailList component into a theme-portable\n * block — every color, size, and spacing value binds to a Visor token so\n * the block adopts the active theme without modification.\n */\n\n.root {\n display: flex;\n flex-direction: column;\n background: var(--surface-card);\n /* Reset list semantics chrome when root is a <ul> / <ol>. */\n margin: 0;\n padding: 0;\n list-style: none;\n}\n\n.row {\n display: flex;\n align-items: center;\n gap: var(--spacing-3, 0.75rem);\n padding: var(--spacing-2-5, 0.625rem) var(--spacing-4, 1rem);\n font-size: var(--font-size-sm, 0.8125rem);\n /* Hairline divider between rows — matches the subtle separator pattern\n * used in the r3 reference. The first row has no top border so the\n * block sits flush against any panel chrome above it. */\n border-top: var(--stroke-width-thin, 1px) solid var(--border-subtle, transparent);\n}\n\n.row:first-child {\n border-top: 0;\n}\n\n/* Subtle hover affordance — rows are not always interactive, but a\n * gentle background shift signals where focus would land if a primary\n * link is present. */\n.row:hover {\n background-color: var(--surface-hover, transparent);\n}\n\n/* Compact density — shaves vertical padding for tighter rails. */\n.rootCompact .row {\n padding: var(--spacing-2, 0.5rem) var(--spacing-4, 1rem);\n}\n\n.leading {\n flex-shrink: 0;\n display: inline-flex;\n align-items: center;\n font-size: var(--font-size-xs, 0.6875rem);\n color: var(--text-tertiary);\n font-variant-numeric: tabular-nums;\n letter-spacing: 0.04em;\n text-transform: uppercase;\n min-width: var(--spacing-9, 2.25rem);\n}\n\n.primary {\n flex: 1;\n min-width: 0;\n color: var(--text-primary);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n.trailing {\n flex-shrink: 0;\n font-variant-numeric: tabular-nums;\n color: var(--text-tertiary);\n}\n\n/* Tone variants — each tone binds to a Visor semantic text token so the\n * block adopts the active theme's color palette without modification.\n * Pattern mirrors status-badge.module.css. */\n.trailing[data-tone=\"mint\"] {\n color: var(--text-success);\n font-weight: 500;\n letter-spacing: 0.06em;\n text-transform: uppercase;\n font-size: var(--font-size-xs, 0.6875rem);\n}\n\n.trailing[data-tone=\"muted\"] {\n color: var(--text-tertiary);\n}\n\n.trailing[data-tone=\"warn\"] {\n color: var(--text-warning);\n font-weight: 500;\n}\n\n.trailing[data-tone=\"danger\"] {\n color: var(--text-danger, var(--text-error));\n font-weight: 500;\n}\n\n.trailing[data-tone=\"info\"] {\n color: var(--text-info);\n font-weight: 500;\n}\n\n/* Respect users who prefer reduced motion — the hover transition is\n * cosmetic, but we omit it under the preference. */\n@media (prefers-reduced-motion: no-preference) {\n .row {\n transition: background-color var(--motion-duration-100, 120ms)\n var(--motion-easing-standard, ease-out);\n }\n}\n"
|
|
3346
|
+
}
|
|
3347
|
+
]
|
|
3348
|
+
},
|
|
3175
3349
|
{
|
|
3176
3350
|
"name": "workspace-switcher",
|
|
3177
3351
|
"type": "registry:block",
|
|
@@ -3470,7 +3644,7 @@
|
|
|
3470
3644
|
{
|
|
3471
3645
|
"path": "blocks/design-system-specimen/specimen-data.ts",
|
|
3472
3646
|
"type": "registry:block",
|
|
3473
|
-
"content": "/**\n * Design System Specimen — Data\n *\n * Typed data arrays for all specimen sections.\n * Values sourced from packages/tokens/src/tokens/primitives.ts and semantic.ts.\n */\n\n// ─── Interfaces ──────────────────────────────────────────────────────────────\n\nexport interface ColorSwatchData {\n token: string\n hex: string\n name: string\n lightText?: boolean\n /** When true, ColorSwatch reads the live computed value instead of displaying the fallback hex */\n dynamic?: boolean\n}\n\nexport interface ColorScaleData {\n name: string\n swatches: ColorSwatchData[]\n /** When set, renders a featured brand swatch above the scale reading this token */\n brandToken?: string\n}\n\nexport interface SemanticColorData {\n token: string\n label: string\n category: string\n}\n\nexport interface TypeSpecimenData {\n token: string\n label: string\n sizePx: number\n sampleText: string\n}\n\nexport interface SpacingStepData {\n token: string\n name: string\n px: number\n rem: string\n}\n\nexport interface ShadowLevelData {\n token: string\n name: string\n value: string\n}\n\nexport interface SurfaceData {\n token: string\n name: string\n lightText?: boolean\n}\n\nexport interface RadiusStepData {\n token: string\n name: string\n px: number\n}\n\nexport interface MotionDurationData {\n token: string\n name: string\n ms: number\n}\n\nexport interface EasingData {\n token: string\n name: string\n value: string\n}\n\nexport interface ContrastPairData {\n fgToken: string\n bgToken: string\n fgLabel: string\n bgLabel: string\n ratio: number\n wcagAA: boolean\n wcagAAA: boolean\n}\n\nexport interface IconSpecimenData {\n name: string\n phosphorName: string\n usage: string\n}\n\nexport interface FontWeightData {\n label: string\n value: number\n}\n\nexport interface FontFamilyData {\n /** CSS custom property token (e.g. \"--font-heading\") */\n token: string\n /** Display role (e.g. \"Heading & Body\", \"Monospace\") */\n role: string\n /** Font family display name — omit to read dynamically from the CSS token */\n familyName?: string\n /** Available weights */\n weights: FontWeightData[]\n}\n\n// ─── Color Scales ────────────────────────────────────────────────────────────\n\nexport interface StatusColorScaleData extends ColorScaleData {\n /** Semantic role label (e.g. \"Success\", \"Warning\") */\n role: string\n}\n\nexport const THEME_COLOR_SCALES: ColorScaleData[] = [\n {\n name: \"Primary\",\n brandToken: \"--interactive-primary-bg\",\n swatches: [\n { token: \"--color-primary-100\", hex: \"#cfdfe7\", name: \"100\", dynamic: true },\n { token: \"--color-primary-200\", hex: \"#adc8d5\", name: \"200\", dynamic: true },\n { token: \"--color-primary-300\", hex: \"#89aec0\", name: \"300\", dynamic: true },\n { token: \"--color-primary-400\", hex: \"#6093aa\", name: \"400\", dynamic: true },\n { token: \"--color-primary-500\", hex: \"#397a96\", name: \"500\", lightText: true, dynamic: true },\n { token: \"--color-primary-600\", hex: \"#2a647c\", name: \"600\", lightText: true, dynamic: true },\n { token: \"--color-primary-700\", hex: \"#1a4e64\", name: \"700\", lightText: true, dynamic: true },\n { token: \"--color-primary-800\", hex: \"#0b3a4c\", name: \"800\", lightText: true, dynamic: true },\n { token: \"--color-primary-900\", hex: \"#002938\", name: \"900\", lightText: true, dynamic: true },\n { token: \"--color-primary-950\", hex: \"#001c29\", name: \"950\", lightText: true, dynamic: true },\n ],\n },\n {\n name: \"Neutral\",\n swatches: [\n { token: \"--color-gray-100\", hex: \"#f3f4f6\", name: \"100\" },\n { token: \"--color-gray-200\", hex: \"#e5e7eb\", name: \"200\" },\n { token: \"--color-gray-300\", hex: \"#d1d5db\", name: \"300\" },\n { token: \"--color-gray-400\", hex: \"#9ca3af\", name: \"400\" },\n { token: \"--color-gray-500\", hex: \"#6b7280\", name: \"500\", lightText: true },\n { token: \"--color-gray-600\", hex: \"#4b5563\", name: \"600\", lightText: true },\n { token: \"--color-gray-700\", hex: \"#374151\", name: \"700\", lightText: true },\n { token: \"--color-gray-800\", hex: \"#1f2937\", name: \"800\", lightText: true },\n { token: \"--color-gray-900\", hex: \"#111827\", name: \"900\", lightText: true },\n { token: \"--color-gray-950\", hex: \"#030712\", name: \"950\", lightText: true },\n ],\n },\n]\n\nexport const STATUS_COLOR_SCALES: StatusColorScaleData[] = [\n {\n name: \"Success\",\n role: \"Success\",\n swatches: [\n { token: \"--color-green-50\", hex: \"#f0fdf4\", name: \"50\" },\n { token: \"--color-green-100\", hex: \"#dcfce7\", name: \"100\" },\n { token: \"--color-green-500\", hex: \"#22c55e\", name: \"500\", lightText: true },\n { token: \"--color-green-600\", hex: \"#16a34a\", name: \"600\", lightText: true },\n { token: \"--color-green-700\", hex: \"#15803d\", name: \"700\", lightText: true },\n { token: \"--color-green-900\", hex: \"#14532d\", name: \"900\", lightText: true },\n ],\n },\n {\n name: \"Warning\",\n role: \"Warning\",\n swatches: [\n { token: \"--color-amber-50\", hex: \"#fffbeb\", name: \"50\" },\n { token: \"--color-amber-100\", hex: \"#fef3c7\", name: \"100\" },\n { token: \"--color-amber-500\", hex: \"#f59e0b\", name: \"500\" },\n { token: \"--color-amber-600\", hex: \"#d97706\", name: \"600\", lightText: true },\n { token: \"--color-amber-700\", hex: \"#b45309\", name: \"700\", lightText: true },\n { token: \"--color-amber-900\", hex: \"#78350f\", name: \"900\", lightText: true },\n ],\n },\n {\n name: \"Error\",\n role: \"Error\",\n swatches: [\n { token: \"--color-red-50\", hex: \"#fef2f2\", name: \"50\" },\n { token: \"--color-red-100\", hex: \"#fee2e2\", name: \"100\" },\n { token: \"--color-red-500\", hex: \"#ef4444\", name: \"500\", lightText: true },\n { token: \"--color-red-600\", hex: \"#dc2626\", name: \"600\", lightText: true },\n { token: \"--color-red-700\", hex: \"#b91c1c\", name: \"700\", lightText: true },\n { token: \"--color-red-900\", hex: \"#7f1d1d\", name: \"900\", lightText: true },\n ],\n },\n {\n name: \"Info\",\n role: \"Info\",\n swatches: [\n { token: \"--color-sky-50\", hex: \"#f0f9ff\", name: \"50\" },\n { token: \"--color-sky-100\", hex: \"#e0f2fe\", name: \"100\" },\n { token: \"--color-sky-500\", hex: \"#0ea5e9\", name: \"500\", lightText: true },\n { token: \"--color-sky-600\", hex: \"#0284c7\", name: \"600\", lightText: true },\n { token: \"--color-sky-700\", hex: \"#0369a1\", name: \"700\", lightText: true },\n { token: \"--color-sky-900\", hex: \"#0c4a6e\", name: \"900\", lightText: true },\n ],\n },\n]\n\nexport const SEMANTIC_COLORS: SemanticColorData[] = [\n // Text\n { token: \"--text-primary\", label: \"text-primary\", category: \"Text\" },\n { token: \"--text-secondary\", label: \"text-secondary\", category: \"Text\" },\n { token: \"--text-tertiary\", label: \"text-tertiary\", category: \"Text\" },\n { token: \"--text-disabled\", label: \"text-disabled\", category: \"Text\" },\n { token: \"--text-inverse\", label: \"text-inverse\", category: \"Text\" },\n { token: \"--text-link\", label: \"text-link\", category: \"Text\" },\n { token: \"--text-success\", label: \"text-success\", category: \"Text\" },\n { token: \"--text-warning\", label: \"text-warning\", category: \"Text\" },\n { token: \"--text-error\", label: \"text-error\", category: \"Text\" },\n { token: \"--text-info\", label: \"text-info\", category: \"Text\" },\n // Surface\n { token: \"--surface-page\", label: \"surface-page\", category: \"Surface\" },\n { token: \"--surface-card\", label: \"surface-card\", category: \"Surface\" },\n { token: \"--surface-subtle\", label: \"surface-subtle\", category: \"Surface\" },\n { token: \"--surface-muted\", label: \"surface-muted\", category: \"Surface\" },\n { token: \"--surface-overlay\", label: \"surface-overlay\", category: \"Surface\" },\n { token: \"--surface-accent-subtle\", label: \"surface-accent-subtle\", category: \"Surface\" },\n { token: \"--surface-accent-default\", label: \"surface-accent-default\", category: \"Surface\" },\n { token: \"--surface-accent-strong\", label: \"surface-accent-strong\", category: \"Surface\" },\n // Border\n { token: \"--border-default\", label: \"border-default\", category: \"Border\" },\n { token: \"--border-muted\", label: \"border-muted\", category: \"Border\" },\n { token: \"--border-strong\", label: \"border-strong\", category: \"Border\" },\n { token: \"--border-focus\", label: \"border-focus\", category: \"Border\" },\n]\n\n// ─── Typography ──────────────────────────────────────────────────────────────\n\n/**\n * Canonical CSS weight labels keyed by numeric weight. Used to label specimen\n * rows derived from a theme's actual loaded weights (VI-356, D2). Uses the\n * standard CSS Fonts Module names — \"Regular\" not \"Book\", \"Semibold\" not\n * \"Demibold\" — so the operator sees one consistent vocabulary regardless of\n * what a foundry calls the cut.\n */\nexport const WEIGHT_LABELS: Record<number, string> = {\n 100: \"Thin\",\n 200: \"Extra Light\",\n 300: \"Light\",\n 400: \"Regular\",\n 500: \"Medium\",\n 600: \"Semibold\",\n 700: \"Bold\",\n 800: \"Extra Bold\",\n 900: \"Black\",\n}\n\nexport function labelForWeight(weight: number): string {\n return WEIGHT_LABELS[weight] ?? `W${weight}`\n}\n\n/** Typography slot data — matches the manifest shape emitted by the docs site for private themes (VI-356). */\nexport interface ThemeTypographySlot {\n family: string\n weights: number[]\n}\n\nexport interface ThemeTypographyManifest {\n heading?: ThemeTypographySlot\n display?: ThemeTypographySlot\n body?: ThemeTypographySlot\n mono?: ThemeTypographySlot\n}\n\n/**\n * Derive Font Families specimen rows from a theme's typography manifest.\n *\n * - `--font-heading` row uses the body slot's weights when present (the theme's\n * --font-heading variable resolves to body family in the engine), falling\n * back to display or heading slot weights if body is absent.\n * - `--font-mono` row uses the mono slot's weights when present, falling back\n * to body weights, then the default.\n *\n * Any slot the manifest doesn't cover gets the corresponding fallback row from\n * `defaults`. This keeps stock themes (no `weights` in YAML) on the existing\n * 4+3 grid while themes like Blacklight render their five actual weights.\n */\nexport function deriveFontFamiliesFromTypography(\n manifest: ThemeTypographyManifest | undefined,\n defaults: FontFamilyData[],\n): FontFamilyData[] {\n if (!manifest) return defaults\n\n const headingDefault = defaults.find((f) => f.token === \"--font-heading\")\n const monoDefault = defaults.find((f) => f.token === \"--font-mono\")\n\n const headingSlot = manifest.body ?? manifest.display ?? manifest.heading\n const monoSlot = manifest.mono ?? manifest.body ?? manifest.display ?? manifest.heading\n\n const next: FontFamilyData[] = []\n if (headingDefault) {\n next.push(\n headingSlot\n ? {\n token: \"--font-heading\",\n role: headingDefault.role,\n familyName: headingSlot.family || headingDefault.familyName,\n weights: headingSlot.weights.map((w) => ({ label: labelForWeight(w), value: w })),\n }\n : headingDefault,\n )\n }\n if (monoDefault) {\n next.push(\n monoSlot\n ? {\n token: \"--font-mono\",\n role: monoDefault.role,\n familyName: monoSlot.family || monoDefault.familyName,\n weights: monoSlot.weights.map((w) => ({ label: labelForWeight(w), value: w })),\n }\n : monoDefault,\n )\n }\n return next\n}\n\n/**\n * Fallback weight rows for the Font Families specimen, used when a theme does\n * not declare per-slot `weights` in its `.visor.yaml`. Stock public themes\n * (Blackout, Borderless, Modern Minimal, Neutral, Space) all fall through to\n * these defaults since they rely on system-ui / default weights.\n */\nexport const FONT_FAMILIES: FontFamilyData[] = [\n {\n token: \"--font-heading\",\n role: \"Heading & Body\",\n weights: [\n { label: \"Regular\", value: 400 },\n { label: \"Medium\", value: 500 },\n { label: \"Semibold\", value: 600 },\n { label: \"Bold\", value: 700 },\n ],\n },\n {\n token: \"--font-mono\",\n role: \"Monospace\",\n weights: [\n { label: \"Regular\", value: 400 },\n { label: \"Medium\", value: 500 },\n { label: \"Bold\", value: 700 },\n ],\n },\n]\n\nexport const TYPE_SPECIMENS: TypeSpecimenData[] = [\n { token: \"--font-size-4xl\", label: \"4xl\", sizePx: 36, sampleText: \"Display text\" },\n { token: \"--font-size-3xl\", label: \"3xl\", sizePx: 30, sampleText: \"Page heading\" },\n { token: \"--font-size-2xl\", label: \"2xl\", sizePx: 24, sampleText: \"Section heading\" },\n { token: \"--font-size-xl\", label: \"xl\", sizePx: 20, sampleText: \"Subsection heading\" },\n { token: \"--font-size-lg\", label: \"lg\", sizePx: 18, sampleText: \"Large body text\" },\n { token: \"--font-size-base\", label: \"base\", sizePx: 16, sampleText: \"Default body text for reading\" },\n { token: \"--font-size-sm\", label: \"sm\", sizePx: 14, sampleText: \"Small text, labels, and captions\" },\n { token: \"--font-size-xs\", label: \"xs\", sizePx: 12, sampleText: \"Fine print and metadata\" },\n]\n\n// ─── Spacing ─────────────────────────────────────────────────────────────────\n\nexport const SPACING_STEPS: SpacingStepData[] = [\n { token: \"--spacing-0\", name: \"0\", px: 0, rem: \"0\" },\n { token: \"--spacing-1\", name: \"1\", px: 4, rem: \"0.25rem\" },\n { token: \"--spacing-2\", name: \"2\", px: 8, rem: \"0.5rem\" },\n { token: \"--spacing-3\", name: \"3\", px: 12, rem: \"0.75rem\" },\n { token: \"--spacing-4\", name: \"4\", px: 16, rem: \"1rem\" },\n { token: \"--spacing-5\", name: \"5\", px: 20, rem: \"1.25rem\" },\n { token: \"--spacing-6\", name: \"6\", px: 24, rem: \"1.5rem\" },\n { token: \"--spacing-8\", name: \"8\", px: 32, rem: \"2rem\" },\n { token: \"--spacing-10\", name: \"10\", px: 40, rem: \"2.5rem\" },\n { token: \"--spacing-12\", name: \"12\", px: 48, rem: \"3rem\" },\n { token: \"--spacing-16\", name: \"16\", px: 64, rem: \"4rem\" },\n { token: \"--spacing-20\", name: \"20\", px: 80, rem: \"5rem\" },\n { token: \"--spacing-24\", name: \"24\", px: 96, rem: \"6rem\" },\n]\n\n// ─── Shadows ─────────────────────────────────────────────────────────────────\n\nexport const SHADOW_LEVELS: ShadowLevelData[] = [\n { token: \"--shadow-xs\", name: \"xs\", value: \"0 1px 1px 0 rgba(0, 0, 0, 0.04)\" },\n { token: \"--shadow-sm\", name: \"sm\", value: \"0 1px 2px 0 rgba(0, 0, 0, 0.05)\" },\n { token: \"--shadow-md\", name: \"md\", value: \"0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1)\" },\n { token: \"--shadow-lg\", name: \"lg\", value: \"0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgba(0, 0, 0, 0.1)\" },\n { token: \"--shadow-xl\", name: \"xl\", value: \"0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1)\" },\n]\n\n// ─── Surfaces ────────────────────────────────────────────────────────────────\n\nexport const SURFACES: SurfaceData[] = [\n { token: \"--surface-page\", name: \"Page\" },\n { token: \"--surface-card\", name: \"Card\" },\n { token: \"--surface-subtle\", name: \"Subtle\" },\n { token: \"--surface-muted\", name: \"Muted\" },\n { token: \"--surface-overlay\", name: \"Overlay\", lightText: true },\n { token: \"--surface-accent-subtle\", name: \"Accent Subtle\" },\n { token: \"--surface-accent-default\", name: \"Accent Default\", lightText: true },\n { token: \"--surface-accent-strong\", name: \"Accent Strong\", lightText: true },\n]\n\n// ─── Border Radius ───────────────────────────────────────────────────────────\n\nexport const RADIUS_STEPS: RadiusStepData[] = [\n { token: \"--radius-none\", name: \"none\", px: 0 },\n { token: \"--radius-sm\", name: \"sm\", px: 2 },\n { token: \"--radius-md\", name: \"md\", px: 4 },\n { token: \"--radius-lg\", name: \"lg\", px: 8 },\n { token: \"--radius-xl\", name: \"xl\", px: 12 },\n { token: \"--radius-2xl\", name: \"2xl\", px: 16 },\n { token: \"--radius-3xl\", name: \"3xl\", px: 24 },\n { token: \"--radius-full\", name: \"full\", px: 9999 },\n]\n\n// ─── Motion ──────────────────────────────────────────────────────────────────\n\nexport const MOTION_DURATIONS: MotionDurationData[] = [\n { token: \"--motion-duration-100\", name: \"100\", ms: 100 },\n { token: \"--motion-duration-150\", name: \"150\", ms: 150 },\n { token: \"--motion-duration-200\", name: \"200\", ms: 200 },\n { token: \"--motion-duration-300\", name: \"300\", ms: 300 },\n { token: \"--motion-duration-500\", name: \"500\", ms: 500 },\n { token: \"--motion-duration-800\", name: \"800\", ms: 800 },\n]\n\nexport const EASINGS: EasingData[] = [\n { token: \"--motion-easing-linear\", name: \"linear\", value: \"linear\" },\n { token: \"--motion-easing-ease-in\", name: \"ease-in\", value: \"cubic-bezier(0.4, 0, 1, 1)\" },\n { token: \"--motion-easing-ease-out\", name: \"ease-out\", value: \"cubic-bezier(0, 0, 0.2, 1)\" },\n { token: \"--motion-easing-ease-in-out\", name: \"ease-in-out\", value: \"cubic-bezier(0.4, 0, 0.2, 1)\" },\n { token: \"--motion-easing-spring\", name: \"spring\", value: \"cubic-bezier(0.34, 1.56, 0.64, 1)\" },\n]\n\n// ─── Accessibility ───────────────────────────────────────────────────────────\n\nexport const CONTRAST_PAIRS: ContrastPairData[] = [\n { fgToken: \"--text-primary\", bgToken: \"--surface-page\", fgLabel: \"text-primary\", bgLabel: \"surface-page\", ratio: 15.4, wcagAA: true, wcagAAA: true },\n { fgToken: \"--text-primary\", bgToken: \"--surface-card\", fgLabel: \"text-primary\", bgLabel: \"surface-card\", ratio: 15.4, wcagAA: true, wcagAAA: true },\n { fgToken: \"--text-secondary\", bgToken: \"--surface-page\", fgLabel: \"text-secondary\", bgLabel: \"surface-page\", ratio: 5.74, wcagAA: true, wcagAAA: false },\n { fgToken: \"--text-tertiary\", bgToken: \"--surface-page\", fgLabel: \"text-tertiary\", bgLabel: \"surface-page\", ratio: 4.75, wcagAA: true, wcagAAA: false },\n { fgToken: \"--text-link\", bgToken: \"--surface-page\", fgLabel: \"text-link\", bgLabel: \"surface-page\", ratio: 4.62, wcagAA: true, wcagAAA: false },\n { fgToken: \"--text-inverse\", bgToken: \"--surface-overlay\", fgLabel: \"text-inverse\", bgLabel: \"surface-overlay\", ratio: 14.7, wcagAA: true, wcagAAA: true },\n { fgToken: \"--text-success\", bgToken: \"--surface-page\", fgLabel: \"text-success\", bgLabel: \"surface-page\", ratio: 4.49, wcagAA: true, wcagAAA: false },\n { fgToken: \"--text-error\", bgToken: \"--surface-page\", fgLabel: \"text-error\", bgLabel: \"surface-page\", ratio: 5.25, wcagAA: true, wcagAAA: false },\n { fgToken: \"--text-warning\", bgToken: \"--surface-page\", fgLabel: \"text-warning\", bgLabel: \"surface-page\", ratio: 4.01, wcagAA: true, wcagAAA: false },\n { fgToken: \"--text-primary\", bgToken: \"--surface-subtle\", fgLabel: \"text-primary\", bgLabel: \"surface-subtle\", ratio: 14.9, wcagAA: true, wcagAAA: true },\n { fgToken: \"--text-primary\", bgToken: \"--surface-muted\", fgLabel: \"text-primary\", bgLabel: \"surface-muted\", ratio: 13.8, wcagAA: true, wcagAAA: true },\n]\n\n// ─── Icons ───────────────────────────────────────────────────────────────────\n\nexport const ICON_SPECIMENS: IconSpecimenData[] = [\n { name: \"House\", phosphorName: \"House\", usage: \"Home / dashboard\" },\n { name: \"MagnifyingGlass\", phosphorName: \"MagnifyingGlass\", usage: \"Search\" },\n { name: \"Gear\", phosphorName: \"Gear\", usage: \"Settings\" },\n { name: \"User\", phosphorName: \"User\", usage: \"Profile / account\" },\n { name: \"Bell\", phosphorName: \"Bell\", usage: \"Notifications\" },\n { name: \"EnvelopeSimple\", phosphorName: \"EnvelopeSimple\", usage: \"Messages / email\" },\n { name: \"Plus\", phosphorName: \"Plus\", usage: \"Add / create\" },\n { name: \"X\", phosphorName: \"X\", usage: \"Close / dismiss\" },\n { name: \"Check\", phosphorName: \"Check\", usage: \"Confirm / success\" },\n { name: \"Warning\", phosphorName: \"Warning\", usage: \"Warning / caution\" },\n { name: \"Info\", phosphorName: \"Info\", usage: \"Information\" },\n { name: \"ArrowRight\", phosphorName: \"ArrowRight\", usage: \"Navigate / next\" },\n { name: \"CaretDown\", phosphorName: \"CaretDown\", usage: \"Expand / dropdown\" },\n { name: \"DotsThree\", phosphorName: \"DotsThree\", usage: \"More actions\" },\n { name: \"PencilSimple\", phosphorName: \"PencilSimple\", usage: \"Edit\" },\n { name: \"Trash\", phosphorName: \"Trash\", usage: \"Delete\" },\n]\n\n// ─── Icon sizes for the size scale demo ──────────────────────────────────────\n\nexport const ICON_SIZES = [16, 20, 24, 32] as const\n"
|
|
3647
|
+
"content": "/**\n * Design System Specimen — Data\n *\n * Typed data arrays for all specimen sections.\n * Values sourced from packages/tokens/src/tokens/primitives.ts and semantic.ts.\n */\n\n// ─── Interfaces ──────────────────────────────────────────────────────────────\n\nexport interface ColorSwatchData {\n token: string\n hex: string\n name: string\n lightText?: boolean\n /** When true, ColorSwatch reads the live computed value instead of displaying the fallback hex */\n dynamic?: boolean\n}\n\nexport interface ColorScaleData {\n name: string\n swatches: ColorSwatchData[]\n /** When set, renders a featured brand swatch above the scale reading this token */\n brandToken?: string\n}\n\nexport interface SemanticColorData {\n token: string\n label: string\n category: string\n}\n\nexport interface TypeSpecimenData {\n token: string\n label: string\n sizePx: number\n sampleText: string\n}\n\nexport interface SpacingStepData {\n token: string\n name: string\n px: number\n rem: string\n}\n\nexport interface ShadowLevelData {\n token: string\n name: string\n value: string\n}\n\nexport interface SurfaceData {\n token: string\n name: string\n lightText?: boolean\n}\n\nexport interface RadiusStepData {\n token: string\n name: string\n px: number\n}\n\nexport interface MotionDurationData {\n token: string\n name: string\n ms: number\n}\n\nexport interface EasingData {\n token: string\n name: string\n value: string\n}\n\nexport interface ContrastPairData {\n fgToken: string\n bgToken: string\n fgLabel: string\n bgLabel: string\n ratio: number\n wcagAA: boolean\n wcagAAA: boolean\n}\n\nexport interface IconSpecimenData {\n name: string\n phosphorName: string\n usage: string\n}\n\nexport interface FontWeightData {\n label: string\n value: number\n}\n\nexport interface FontFamilyData {\n /** CSS custom property token (e.g. \"--font-heading\") */\n token: string\n /** Display role (e.g. \"Heading & Body\", \"Monospace\") */\n role: string\n /** Font family display name — omit to read dynamically from the CSS token */\n familyName?: string\n /** Available weights */\n weights: FontWeightData[]\n}\n\n// ─── Color Scales ────────────────────────────────────────────────────────────\n\nexport interface StatusColorScaleData extends ColorScaleData {\n /** Semantic role label (e.g. \"Success\", \"Warning\") */\n role: string\n}\n\nexport const THEME_COLOR_SCALES: ColorScaleData[] = [\n {\n name: \"Primary\",\n brandToken: \"--interactive-primary-bg\",\n swatches: [\n { token: \"--color-primary-100\", hex: \"#cfdfe7\", name: \"100\", dynamic: true },\n { token: \"--color-primary-200\", hex: \"#adc8d5\", name: \"200\", dynamic: true },\n { token: \"--color-primary-300\", hex: \"#89aec0\", name: \"300\", dynamic: true },\n { token: \"--color-primary-400\", hex: \"#6093aa\", name: \"400\", dynamic: true },\n { token: \"--color-primary-500\", hex: \"#397a96\", name: \"500\", lightText: true, dynamic: true },\n { token: \"--color-primary-600\", hex: \"#2a647c\", name: \"600\", lightText: true, dynamic: true },\n { token: \"--color-primary-700\", hex: \"#1a4e64\", name: \"700\", lightText: true, dynamic: true },\n { token: \"--color-primary-800\", hex: \"#0b3a4c\", name: \"800\", lightText: true, dynamic: true },\n { token: \"--color-primary-900\", hex: \"#002938\", name: \"900\", lightText: true, dynamic: true },\n { token: \"--color-primary-950\", hex: \"#001c29\", name: \"950\", lightText: true, dynamic: true },\n ],\n },\n {\n name: \"Neutral\",\n swatches: [\n { token: \"--color-gray-100\", hex: \"#f3f4f6\", name: \"100\" },\n { token: \"--color-gray-200\", hex: \"#e5e7eb\", name: \"200\" },\n { token: \"--color-gray-300\", hex: \"#d1d5db\", name: \"300\" },\n { token: \"--color-gray-400\", hex: \"#9ca3af\", name: \"400\" },\n { token: \"--color-gray-500\", hex: \"#6b7280\", name: \"500\", lightText: true },\n { token: \"--color-gray-600\", hex: \"#4b5563\", name: \"600\", lightText: true },\n { token: \"--color-gray-700\", hex: \"#374151\", name: \"700\", lightText: true },\n { token: \"--color-gray-800\", hex: \"#1f2937\", name: \"800\", lightText: true },\n { token: \"--color-gray-900\", hex: \"#111827\", name: \"900\", lightText: true },\n { token: \"--color-gray-950\", hex: \"#030712\", name: \"950\", lightText: true },\n ],\n },\n]\n\nexport const STATUS_COLOR_SCALES: StatusColorScaleData[] = [\n {\n name: \"Success\",\n role: \"Success\",\n swatches: [\n { token: \"--color-green-50\", hex: \"#f0fdf4\", name: \"50\" },\n { token: \"--color-green-100\", hex: \"#dcfce7\", name: \"100\" },\n { token: \"--color-green-500\", hex: \"#22c55e\", name: \"500\", lightText: true },\n { token: \"--color-green-600\", hex: \"#16a34a\", name: \"600\", lightText: true },\n { token: \"--color-green-700\", hex: \"#15803d\", name: \"700\", lightText: true },\n { token: \"--color-green-900\", hex: \"#14532d\", name: \"900\", lightText: true },\n ],\n },\n {\n name: \"Warning\",\n role: \"Warning\",\n swatches: [\n { token: \"--color-amber-50\", hex: \"#fffbeb\", name: \"50\" },\n { token: \"--color-amber-100\", hex: \"#fef3c7\", name: \"100\" },\n { token: \"--color-amber-500\", hex: \"#f59e0b\", name: \"500\" },\n { token: \"--color-amber-600\", hex: \"#d97706\", name: \"600\", lightText: true },\n { token: \"--color-amber-700\", hex: \"#b45309\", name: \"700\", lightText: true },\n { token: \"--color-amber-900\", hex: \"#78350f\", name: \"900\", lightText: true },\n ],\n },\n {\n name: \"Error\",\n role: \"Error\",\n swatches: [\n { token: \"--color-red-50\", hex: \"#fef2f2\", name: \"50\" },\n { token: \"--color-red-100\", hex: \"#fee2e2\", name: \"100\" },\n { token: \"--color-red-500\", hex: \"#ef4444\", name: \"500\", lightText: true },\n { token: \"--color-red-600\", hex: \"#dc2626\", name: \"600\", lightText: true },\n { token: \"--color-red-700\", hex: \"#b91c1c\", name: \"700\", lightText: true },\n { token: \"--color-red-900\", hex: \"#7f1d1d\", name: \"900\", lightText: true },\n ],\n },\n {\n name: \"Info\",\n role: \"Info\",\n swatches: [\n { token: \"--color-sky-50\", hex: \"#f0f9ff\", name: \"50\" },\n { token: \"--color-sky-100\", hex: \"#e0f2fe\", name: \"100\" },\n { token: \"--color-sky-500\", hex: \"#0ea5e9\", name: \"500\", lightText: true },\n { token: \"--color-sky-600\", hex: \"#0284c7\", name: \"600\", lightText: true },\n { token: \"--color-sky-700\", hex: \"#0369a1\", name: \"700\", lightText: true },\n { token: \"--color-sky-900\", hex: \"#0c4a6e\", name: \"900\", lightText: true },\n ],\n },\n]\n\nexport const SEMANTIC_COLORS: SemanticColorData[] = [\n // Text\n { token: \"--text-primary\", label: \"text-primary\", category: \"Text\" },\n { token: \"--text-secondary\", label: \"text-secondary\", category: \"Text\" },\n { token: \"--text-tertiary\", label: \"text-tertiary\", category: \"Text\" },\n { token: \"--text-disabled\", label: \"text-disabled\", category: \"Text\" },\n { token: \"--text-inverse\", label: \"text-inverse\", category: \"Text\" },\n { token: \"--text-link\", label: \"text-link\", category: \"Text\" },\n { token: \"--text-success\", label: \"text-success\", category: \"Text\" },\n { token: \"--text-warning\", label: \"text-warning\", category: \"Text\" },\n { token: \"--text-error\", label: \"text-error\", category: \"Text\" },\n { token: \"--text-info\", label: \"text-info\", category: \"Text\" },\n // Surface\n { token: \"--surface-page\", label: \"surface-page\", category: \"Surface\" },\n { token: \"--surface-card\", label: \"surface-card\", category: \"Surface\" },\n { token: \"--surface-subtle\", label: \"surface-subtle\", category: \"Surface\" },\n { token: \"--surface-muted\", label: \"surface-muted\", category: \"Surface\" },\n { token: \"--surface-overlay\", label: \"surface-overlay\", category: \"Surface\" },\n { token: \"--surface-accent-subtle\", label: \"surface-accent-subtle\", category: \"Surface\" },\n { token: \"--surface-accent-default\", label: \"surface-accent-default\", category: \"Surface\" },\n { token: \"--surface-accent-strong\", label: \"surface-accent-strong\", category: \"Surface\" },\n // Border\n { token: \"--border-default\", label: \"border-default\", category: \"Border\" },\n { token: \"--border-muted\", label: \"border-muted\", category: \"Border\" },\n { token: \"--border-strong\", label: \"border-strong\", category: \"Border\" },\n { token: \"--border-focus\", label: \"border-focus\", category: \"Border\" },\n]\n\n// ─── Typography ──────────────────────────────────────────────────────────────\n\n/**\n * Canonical CSS weight labels keyed by numeric weight. Used to label specimen\n * rows derived from a theme's actual loaded weights (VI-356, D2). Uses the\n * standard CSS Fonts Module names — \"Regular\" not \"Book\", \"Semibold\" not\n * \"Demibold\" — so the operator sees one consistent vocabulary regardless of\n * what a foundry calls the cut.\n */\nexport const WEIGHT_LABELS: Record<number, string> = {\n 100: \"Thin\",\n 200: \"Extra Light\",\n 300: \"Light\",\n 400: \"Regular\",\n 500: \"Medium\",\n 600: \"Semibold\",\n 700: \"Bold\",\n 800: \"Extra Bold\",\n 900: \"Black\",\n}\n\nexport function labelForWeight(weight: number): string {\n return WEIGHT_LABELS[weight] ?? `W${weight}`\n}\n\n/** Typography slot data — matches the manifest shape emitted by the docs site for private themes (VI-356). */\nexport interface ThemeTypographySlot {\n family: string\n weights: number[]\n}\n\nexport interface ThemeTypographyManifest {\n heading?: ThemeTypographySlot\n display?: ThemeTypographySlot\n body?: ThemeTypographySlot\n mono?: ThemeTypographySlot\n}\n\n/**\n * Derive Font Families specimen rows from a theme's typography manifest.\n *\n * - `--font-heading` row uses the heading slot's family and weights when\n * present, falling back to display, then body. Post-VI-355 the engine\n * resolves `--font-heading` from `typography.heading.family` directly;\n * earlier the docs adapter aliased it to `var(--font-sans)` (body),\n * which is why this used to read body first.\n * - `--font-mono` row uses the mono slot's weights when present, falling back\n * to body weights, then the default.\n *\n * Any slot the manifest doesn't cover gets the corresponding fallback row from\n * `defaults`. This keeps stock themes (no `weights` in YAML) on the existing\n * 4+3 grid while themes like Blacklight render their five actual weights.\n */\nexport function deriveFontFamiliesFromTypography(\n manifest: ThemeTypographyManifest | undefined,\n defaults: FontFamilyData[],\n): FontFamilyData[] {\n if (!manifest) return defaults\n\n const headingDefault = defaults.find((f) => f.token === \"--font-heading\")\n const monoDefault = defaults.find((f) => f.token === \"--font-mono\")\n\n const headingSlot = manifest.heading ?? manifest.display ?? manifest.body\n const monoSlot = manifest.mono ?? manifest.body ?? manifest.display ?? manifest.heading\n\n const next: FontFamilyData[] = []\n if (headingDefault) {\n next.push(\n headingSlot\n ? {\n token: \"--font-heading\",\n role: headingDefault.role,\n familyName: headingSlot.family || headingDefault.familyName,\n weights: headingSlot.weights.map((w) => ({ label: labelForWeight(w), value: w })),\n }\n : headingDefault,\n )\n }\n if (monoDefault) {\n next.push(\n monoSlot\n ? {\n token: \"--font-mono\",\n role: monoDefault.role,\n familyName: monoSlot.family || monoDefault.familyName,\n weights: monoSlot.weights.map((w) => ({ label: labelForWeight(w), value: w })),\n }\n : monoDefault,\n )\n }\n return next\n}\n\n/**\n * Fallback weight rows for the Font Families specimen, used when a theme does\n * not declare per-slot `weights` in its `.visor.yaml`. Stock public themes\n * (Blackout, Borderless, Modern Minimal, Neutral, Space) all fall through to\n * these defaults since they rely on system-ui / default weights.\n */\nexport const FONT_FAMILIES: FontFamilyData[] = [\n {\n token: \"--font-heading\",\n role: \"Heading & Body\",\n weights: [\n { label: \"Regular\", value: 400 },\n { label: \"Medium\", value: 500 },\n { label: \"Semibold\", value: 600 },\n { label: \"Bold\", value: 700 },\n ],\n },\n {\n token: \"--font-mono\",\n role: \"Monospace\",\n weights: [\n { label: \"Regular\", value: 400 },\n { label: \"Medium\", value: 500 },\n { label: \"Bold\", value: 700 },\n ],\n },\n]\n\nexport const TYPE_SPECIMENS: TypeSpecimenData[] = [\n { token: \"--font-size-4xl\", label: \"4xl\", sizePx: 36, sampleText: \"Display text\" },\n { token: \"--font-size-3xl\", label: \"3xl\", sizePx: 30, sampleText: \"Page heading\" },\n { token: \"--font-size-2xl\", label: \"2xl\", sizePx: 24, sampleText: \"Section heading\" },\n { token: \"--font-size-xl\", label: \"xl\", sizePx: 20, sampleText: \"Subsection heading\" },\n { token: \"--font-size-lg\", label: \"lg\", sizePx: 18, sampleText: \"Large body text\" },\n { token: \"--font-size-base\", label: \"base\", sizePx: 16, sampleText: \"Default body text for reading\" },\n { token: \"--font-size-sm\", label: \"sm\", sizePx: 14, sampleText: \"Small text, labels, and captions\" },\n { token: \"--font-size-xs\", label: \"xs\", sizePx: 12, sampleText: \"Fine print and metadata\" },\n]\n\n// ─── Spacing ─────────────────────────────────────────────────────────────────\n\nexport const SPACING_STEPS: SpacingStepData[] = [\n { token: \"--spacing-0\", name: \"0\", px: 0, rem: \"0\" },\n { token: \"--spacing-1\", name: \"1\", px: 4, rem: \"0.25rem\" },\n { token: \"--spacing-2\", name: \"2\", px: 8, rem: \"0.5rem\" },\n { token: \"--spacing-3\", name: \"3\", px: 12, rem: \"0.75rem\" },\n { token: \"--spacing-4\", name: \"4\", px: 16, rem: \"1rem\" },\n { token: \"--spacing-5\", name: \"5\", px: 20, rem: \"1.25rem\" },\n { token: \"--spacing-6\", name: \"6\", px: 24, rem: \"1.5rem\" },\n { token: \"--spacing-8\", name: \"8\", px: 32, rem: \"2rem\" },\n { token: \"--spacing-10\", name: \"10\", px: 40, rem: \"2.5rem\" },\n { token: \"--spacing-12\", name: \"12\", px: 48, rem: \"3rem\" },\n { token: \"--spacing-16\", name: \"16\", px: 64, rem: \"4rem\" },\n { token: \"--spacing-20\", name: \"20\", px: 80, rem: \"5rem\" },\n { token: \"--spacing-24\", name: \"24\", px: 96, rem: \"6rem\" },\n]\n\n// ─── Shadows ─────────────────────────────────────────────────────────────────\n\nexport const SHADOW_LEVELS: ShadowLevelData[] = [\n { token: \"--shadow-xs\", name: \"xs\", value: \"0 1px 1px 0 rgba(0, 0, 0, 0.04)\" },\n { token: \"--shadow-sm\", name: \"sm\", value: \"0 1px 2px 0 rgba(0, 0, 0, 0.05)\" },\n { token: \"--shadow-md\", name: \"md\", value: \"0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1)\" },\n { token: \"--shadow-lg\", name: \"lg\", value: \"0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgba(0, 0, 0, 0.1)\" },\n { token: \"--shadow-xl\", name: \"xl\", value: \"0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1)\" },\n]\n\n// ─── Surfaces ────────────────────────────────────────────────────────────────\n\nexport const SURFACES: SurfaceData[] = [\n { token: \"--surface-page\", name: \"Page\" },\n { token: \"--surface-card\", name: \"Card\" },\n { token: \"--surface-subtle\", name: \"Subtle\" },\n { token: \"--surface-muted\", name: \"Muted\" },\n { token: \"--surface-overlay\", name: \"Overlay\", lightText: true },\n { token: \"--surface-accent-subtle\", name: \"Accent Subtle\" },\n { token: \"--surface-accent-default\", name: \"Accent Default\", lightText: true },\n { token: \"--surface-accent-strong\", name: \"Accent Strong\", lightText: true },\n]\n\n// ─── Border Radius ───────────────────────────────────────────────────────────\n\nexport const RADIUS_STEPS: RadiusStepData[] = [\n { token: \"--radius-none\", name: \"none\", px: 0 },\n { token: \"--radius-sm\", name: \"sm\", px: 2 },\n { token: \"--radius-md\", name: \"md\", px: 4 },\n { token: \"--radius-lg\", name: \"lg\", px: 8 },\n { token: \"--radius-xl\", name: \"xl\", px: 12 },\n { token: \"--radius-2xl\", name: \"2xl\", px: 16 },\n { token: \"--radius-3xl\", name: \"3xl\", px: 24 },\n { token: \"--radius-full\", name: \"full\", px: 9999 },\n]\n\n// ─── Motion ──────────────────────────────────────────────────────────────────\n\nexport const MOTION_DURATIONS: MotionDurationData[] = [\n { token: \"--motion-duration-100\", name: \"100\", ms: 100 },\n { token: \"--motion-duration-150\", name: \"150\", ms: 150 },\n { token: \"--motion-duration-200\", name: \"200\", ms: 200 },\n { token: \"--motion-duration-300\", name: \"300\", ms: 300 },\n { token: \"--motion-duration-500\", name: \"500\", ms: 500 },\n { token: \"--motion-duration-800\", name: \"800\", ms: 800 },\n]\n\nexport const EASINGS: EasingData[] = [\n { token: \"--motion-easing-linear\", name: \"linear\", value: \"linear\" },\n { token: \"--motion-easing-ease-in\", name: \"ease-in\", value: \"cubic-bezier(0.4, 0, 1, 1)\" },\n { token: \"--motion-easing-ease-out\", name: \"ease-out\", value: \"cubic-bezier(0, 0, 0.2, 1)\" },\n { token: \"--motion-easing-ease-in-out\", name: \"ease-in-out\", value: \"cubic-bezier(0.4, 0, 0.2, 1)\" },\n { token: \"--motion-easing-spring\", name: \"spring\", value: \"cubic-bezier(0.34, 1.56, 0.64, 1)\" },\n]\n\n// ─── Accessibility ───────────────────────────────────────────────────────────\n\nexport const CONTRAST_PAIRS: ContrastPairData[] = [\n { fgToken: \"--text-primary\", bgToken: \"--surface-page\", fgLabel: \"text-primary\", bgLabel: \"surface-page\", ratio: 15.4, wcagAA: true, wcagAAA: true },\n { fgToken: \"--text-primary\", bgToken: \"--surface-card\", fgLabel: \"text-primary\", bgLabel: \"surface-card\", ratio: 15.4, wcagAA: true, wcagAAA: true },\n { fgToken: \"--text-secondary\", bgToken: \"--surface-page\", fgLabel: \"text-secondary\", bgLabel: \"surface-page\", ratio: 5.74, wcagAA: true, wcagAAA: false },\n { fgToken: \"--text-tertiary\", bgToken: \"--surface-page\", fgLabel: \"text-tertiary\", bgLabel: \"surface-page\", ratio: 4.75, wcagAA: true, wcagAAA: false },\n { fgToken: \"--text-link\", bgToken: \"--surface-page\", fgLabel: \"text-link\", bgLabel: \"surface-page\", ratio: 4.62, wcagAA: true, wcagAAA: false },\n { fgToken: \"--text-inverse\", bgToken: \"--surface-overlay\", fgLabel: \"text-inverse\", bgLabel: \"surface-overlay\", ratio: 14.7, wcagAA: true, wcagAAA: true },\n { fgToken: \"--text-success\", bgToken: \"--surface-page\", fgLabel: \"text-success\", bgLabel: \"surface-page\", ratio: 4.49, wcagAA: true, wcagAAA: false },\n { fgToken: \"--text-error\", bgToken: \"--surface-page\", fgLabel: \"text-error\", bgLabel: \"surface-page\", ratio: 5.25, wcagAA: true, wcagAAA: false },\n { fgToken: \"--text-warning\", bgToken: \"--surface-page\", fgLabel: \"text-warning\", bgLabel: \"surface-page\", ratio: 4.01, wcagAA: true, wcagAAA: false },\n { fgToken: \"--text-primary\", bgToken: \"--surface-subtle\", fgLabel: \"text-primary\", bgLabel: \"surface-subtle\", ratio: 14.9, wcagAA: true, wcagAAA: true },\n { fgToken: \"--text-primary\", bgToken: \"--surface-muted\", fgLabel: \"text-primary\", bgLabel: \"surface-muted\", ratio: 13.8, wcagAA: true, wcagAAA: true },\n]\n\n// ─── Icons ───────────────────────────────────────────────────────────────────\n\nexport const ICON_SPECIMENS: IconSpecimenData[] = [\n { name: \"House\", phosphorName: \"House\", usage: \"Home / dashboard\" },\n { name: \"MagnifyingGlass\", phosphorName: \"MagnifyingGlass\", usage: \"Search\" },\n { name: \"Gear\", phosphorName: \"Gear\", usage: \"Settings\" },\n { name: \"User\", phosphorName: \"User\", usage: \"Profile / account\" },\n { name: \"Bell\", phosphorName: \"Bell\", usage: \"Notifications\" },\n { name: \"EnvelopeSimple\", phosphorName: \"EnvelopeSimple\", usage: \"Messages / email\" },\n { name: \"Plus\", phosphorName: \"Plus\", usage: \"Add / create\" },\n { name: \"X\", phosphorName: \"X\", usage: \"Close / dismiss\" },\n { name: \"Check\", phosphorName: \"Check\", usage: \"Confirm / success\" },\n { name: \"Warning\", phosphorName: \"Warning\", usage: \"Warning / caution\" },\n { name: \"Info\", phosphorName: \"Info\", usage: \"Information\" },\n { name: \"ArrowRight\", phosphorName: \"ArrowRight\", usage: \"Navigate / next\" },\n { name: \"CaretDown\", phosphorName: \"CaretDown\", usage: \"Expand / dropdown\" },\n { name: \"DotsThree\", phosphorName: \"DotsThree\", usage: \"More actions\" },\n { name: \"PencilSimple\", phosphorName: \"PencilSimple\", usage: \"Edit\" },\n { name: \"Trash\", phosphorName: \"Trash\", usage: \"Delete\" },\n]\n\n// ─── Icon sizes for the size scale demo ──────────────────────────────────────\n\nexport const ICON_SIZES = [16, 20, 24, 32] as const\n"
|
|
3474
3648
|
},
|
|
3475
3649
|
{
|
|
3476
3650
|
"path": "blocks/design-system-specimen/token-specimens.tsx",
|
|
@@ -3824,7 +3998,7 @@
|
|
|
3824
3998
|
{
|
|
3825
3999
|
"path": "components/devtools/source-inspector/visor-component-names.generated.ts",
|
|
3826
4000
|
"type": "registry:devtool",
|
|
3827
|
-
"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 \"AlertDescription\",\n \"AlertTitle\",\n \"Avatar\",\n \"AvatarFallback\",\n \"AvatarImage\",\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 \"Breadcrumb\",\n \"BreadcrumbEllipsis\",\n \"BreadcrumbItem\",\n \"BreadcrumbLink\",\n \"BreadcrumbList\",\n \"BreadcrumbPage\",\n \"BreadcrumbSeparator\",\n \"BulkActionBar\",\n \"Button\",\n \"ButtonSpecimenSection\",\n \"ButtonSpecimenSlide\",\n \"Calendar\",\n \"Card\",\n \"CardContent\",\n \"CardDescription\",\n \"CardFooter\",\n \"CardGrid\",\n \"CardHeader\",\n \"CardTitle\",\n \"Carousel\",\n \"CarouselContent\",\n \"CarouselContext\",\n \"CarouselGallery\",\n \"CarouselItem\",\n \"CarouselNext\",\n \"CarouselPrevious\",\n \"ChartContainer\",\n \"ChartContext\",\n \"ChartLegend\",\n \"ChartLegendContent\",\n \"ChartStyle\",\n \"ChartTooltip\",\n \"ChartTooltipContent\",\n \"Checkbox\",\n \"Chip\",\n \"ChipGroup\",\n \"ChipGroupContext\",\n \"ChipGroupItem\",\n \"ChoiceChip\",\n \"ClosingSlide\",\n \"CodeBlock\",\n \"Collapsible\",\n \"CollapsibleContent\",\n \"CollapsibleTrigger\",\n \"ColorBar\",\n \"ColorPaletteSection\",\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 \"ConceptSlide\",\n \"ConfigurationPanel\",\n \"ConfirmDialog\",\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 \"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 \"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 \"ElevationCard\",\n \"ElevationSlide\",\n \"EmptyState\",\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 \"FooterSection\",\n \"Form\",\n \"FormField\",\n \"FormSpecimenSection\",\n \"FormSpecimenSlide\",\n \"FullscreenOverlay\",\n \"FullscreenOverlayContent\",\n \"FullscreenOverlayTrigger\",\n \"Heading\",\n \"HeroSection\",\n \"HeroSlide\",\n \"HoverCard\",\n \"HoverCardContent\",\n \"HoverCardTrigger\",\n \"IconGrid\",\n \"IconGridSection\",\n \"IconSizeRow\",\n \"IconsSlide\",\n \"Image\",\n \"Input\",\n \"Kbd\",\n \"Label\",\n \"Lightbox\",\n \"LightboxContent\",\n \"LightboxContext\",\n \"LightboxTrigger\",\n \"LoginForm\",\n \"Marquee\",\n \"MarqueeBandRenderer\",\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 \"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 \"PopoverTrigger\",\n \"PricingSection\",\n \"Progress\",\n \"RadioGroup\",\n \"RadioGroupItem\",\n \"RadiusScale\",\n \"RadiusSection\",\n \"RadiusSlide\",\n \"ScrollArea\",\n \"ScrollBar\",\n \"SearchInput\",\n \"Select\",\n \"SelectContent\",\n \"SelectGroup\",\n \"SelectItem\",\n \"SelectLabel\",\n \"SelectScrollDownButton\",\n \"SelectScrollUpButton\",\n \"SelectSeparator\",\n \"SelectTrigger\",\n \"SelectValue\",\n \"SemanticColorGrid\",\n \"SemanticColorItem\",\n \"SemanticTokensSlide\",\n \"Separator\",\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 \"Slide\",\n \"SlideHeader\",\n \"Slider\",\n \"SliderControl\",\n \"SlideThemeContext\",\n \"SlideThemeProvider\",\n \"SourceInspector\",\n \"SourceInspectorContext\",\n \"SourceInspectorDevImpl\",\n \"SourceInspectorProvider\",\n \"SourceInspectorRunner\",\n \"SourceInspectorToggle\",\n \"SpacingScale\",\n \"SpacingSection\",\n \"SpacingSlide\",\n \"Sphere\",\n \"SpherePlayground\",\n \"StatCard\",\n \"StatHero\",\n \"StationSpectrum\",\n \"StatusBadge\",\n \"StatusColorsSlide\",\n \"Stepper\",\n \"StepperContext\",\n \"StepperDescription\",\n \"StepperItem\",\n \"StepperSeparator\",\n \"StepperTitle\",\n \"StepperTrigger\",\n \"StepsSection\",\n \"SurfaceRow\",\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 \"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 \"TypeSpecimen\",\n \"TypographySection\",\n \"WorkspaceSwitcher\",\n])\n"
|
|
4001
|
+
"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 \"AlertDescription\",\n \"AlertTitle\",\n \"Avatar\",\n \"AvatarFallback\",\n \"AvatarImage\",\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 \"Breadcrumb\",\n \"BreadcrumbEllipsis\",\n \"BreadcrumbItem\",\n \"BreadcrumbLink\",\n \"BreadcrumbList\",\n \"BreadcrumbPage\",\n \"BreadcrumbSeparator\",\n \"BulkActionBar\",\n \"Button\",\n \"ButtonSpecimenSection\",\n \"ButtonSpecimenSlide\",\n \"Calendar\",\n \"Card\",\n \"CardContent\",\n \"CardDescription\",\n \"CardFooter\",\n \"CardGrid\",\n \"CardHeader\",\n \"CardTitle\",\n \"Carousel\",\n \"CarouselContent\",\n \"CarouselContext\",\n \"CarouselGallery\",\n \"CarouselItem\",\n \"CarouselNext\",\n \"CarouselPrevious\",\n \"ChartContainer\",\n \"ChartContext\",\n \"ChartLegend\",\n \"ChartLegendContent\",\n \"ChartStyle\",\n \"ChartTooltip\",\n \"ChartTooltipContent\",\n \"Checkbox\",\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 \"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 \"ConceptSlide\",\n \"ConfigurationPanel\",\n \"ConfirmDialog\",\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 \"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 \"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 \"ElevationCard\",\n \"ElevationSlide\",\n \"EmptyState\",\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 \"FooterSection\",\n \"Form\",\n \"FormField\",\n \"FormSpecimenSection\",\n \"FormSpecimenSlide\",\n \"FullscreenOverlay\",\n \"FullscreenOverlayContent\",\n \"FullscreenOverlayTrigger\",\n \"Heading\",\n \"HeroSection\",\n \"HeroSlide\",\n \"HoverCard\",\n \"HoverCardContent\",\n \"HoverCardTrigger\",\n \"IconGrid\",\n \"IconGridSection\",\n \"IconSizeRow\",\n \"IconsSlide\",\n \"Image\",\n \"Input\",\n \"Kbd\",\n \"Label\",\n \"Lightbox\",\n \"LightboxContent\",\n \"LightboxContext\",\n \"LightboxTrigger\",\n \"LoginForm\",\n \"Marquee\",\n \"MarqueeBandRenderer\",\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 \"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 \"PopoverTrigger\",\n \"PricingSection\",\n \"Progress\",\n \"QuickActions\",\n \"RadioGroup\",\n \"RadioGroupItem\",\n \"RadiusScale\",\n \"RadiusSection\",\n \"RadiusSlide\",\n \"RightRailList\",\n \"ScrollArea\",\n \"ScrollBar\",\n \"SearchInput\",\n \"SectionHeader\",\n \"Select\",\n \"SelectContent\",\n \"SelectGroup\",\n \"SelectItem\",\n \"SelectLabel\",\n \"SelectScrollDownButton\",\n \"SelectScrollUpButton\",\n \"SelectSeparator\",\n \"SelectTrigger\",\n \"SelectValue\",\n \"SemanticColorGrid\",\n \"SemanticColorItem\",\n \"SemanticTokensSlide\",\n \"Separator\",\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 \"Slide\",\n \"SlideHeader\",\n \"Slider\",\n \"SliderControl\",\n \"SlideThemeContext\",\n \"SlideThemeProvider\",\n \"SourceInspector\",\n \"SourceInspectorContext\",\n \"SourceInspectorDevImpl\",\n \"SourceInspectorProvider\",\n \"SourceInspectorRunner\",\n \"SourceInspectorToggle\",\n \"SpacingScale\",\n \"SpacingSection\",\n \"SpacingSlide\",\n \"Sparkline\",\n \"Sphere\",\n \"SpherePlayground\",\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 \"SurfaceRow\",\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 \"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 \"TypeSpecimen\",\n \"TypographySection\",\n \"WorkspaceSwitcher\",\n])\n"
|
|
3828
4002
|
}
|
|
3829
4003
|
]
|
|
3830
4004
|
},
|
|
@@ -4323,8 +4497,8 @@
|
|
|
4323
4497
|
{
|
|
4324
4498
|
"name": "SectionHeader",
|
|
4325
4499
|
"type": "registry:ui",
|
|
4326
|
-
"description": "
|
|
4327
|
-
"category": "
|
|
4500
|
+
"description": "Compact section-divider primitive with an uppercase title and optional right-aligned meta label. Sized for in-page content sectioning, distinct from the page-level PageHeader.",
|
|
4501
|
+
"category": "navigation",
|
|
4328
4502
|
"target": "flutter",
|
|
4329
4503
|
"pubDependencies": [
|
|
4330
4504
|
{
|