@loworbitstudio/visor 1.6.0 → 1.10.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 +79 -1
- package/dist/registry.json +353 -38
- package/dist/visor-manifest.json +1148 -71
- package/package.json +2 -2
package/dist/registry.json
CHANGED
|
@@ -66,12 +66,12 @@
|
|
|
66
66
|
{
|
|
67
67
|
"path": "components/ui/stepper/stepper.tsx",
|
|
68
68
|
"type": "registry:ui",
|
|
69
|
-
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport { Check } from \"@phosphor-icons/react\"\nimport { cn } from \"../../../lib/utils\"\nimport styles from \"./stepper.module.css\"\n\n/* ─── Context ──────────────────────────────────────────────────────────────── */\n\ninterface StepperContextValue {\n activeStep: number\n orientation: \"horizontal\" | \"vertical\"\n}\n\nconst StepperContext = React.createContext<StepperContextValue>({\n activeStep: 0,\n orientation: \"horizontal\",\n})\n\nfunction useStepperContext() {\n return React.useContext(StepperContext)\n}\n\n/* ─── Stepper ──────────────────────────────────────────────────────────────── */\n\nexport interface StepperProps extends React.HTMLAttributes<HTMLDivElement> {\n activeStep?: number\n orientation?: \"horizontal\" | \"vertical\"\n}\n\nconst Stepper = React.forwardRef<HTMLDivElement, StepperProps>(\n ({ className, activeStep = 0, orientation = \"horizontal\", children, ...props }, ref) => {\n const contextValue = React.useMemo(\n () => ({ activeStep, orientation }),\n [activeStep, orientation]\n )\n\n return (\n <StepperContext.Provider value={contextValue}>\n <div\n ref={ref}\n role=\"group\"\n aria-label=\"Progress\"\n data-slot=\"stepper\"\n data-orientation={orientation}\n className={cn(\n styles.stepper,\n orientation === \"vertical\" && styles.stepperVertical,\n className\n )}\n {...props}\n >\n {children}\n </div>\n </StepperContext.Provider>\n )\n }\n)\nStepper.displayName = \"Stepper\"\n\n/* ─── StepperItem ──────────────────────────────────────────────────────────── */\n\nexport interface StepperItemProps extends React.HTMLAttributes<HTMLDivElement> {\n step: number\n status?: \"complete\" | \"active\" | \"upcoming\"\n}\n\nconst StepperItem = React.forwardRef<HTMLDivElement, StepperItemProps>(\n ({ className, step, status, children, ...props }, ref) => {\n const { activeStep, orientation } = useStepperContext()\n\n const resolvedStatus =\n status ?? (step < activeStep ? \"complete\" : step === activeStep ? \"active\" : \"upcoming\")\n\n return (\n <div\n ref={ref}\n data-slot=\"stepper-item\"\n data-step={step}\n data-status={resolvedStatus}\n data-orientation={orientation}\n aria-current={resolvedStatus === \"active\" ? \"step\" : undefined}\n className={cn(\n styles.item,\n orientation === \"vertical\" && styles.itemVertical,\n className\n )}\n {...props}\n >\n {children}\n </div>\n )\n }\n)\nStepperItem.displayName = \"StepperItem\"\n\n/* ─── StepperTrigger ───────────────────────────────────────────────────────── */\n\nexport interface StepperTriggerProps\n extends React.ButtonHTMLAttributes<HTMLButtonElement> {\n step: number\n status?: \"complete\" | \"active\" | \"upcoming\"\n}\n\nconst StepperTrigger = React.forwardRef<HTMLButtonElement, StepperTriggerProps>(\n ({ className, step, status, children, ...props }, ref) => {\n const { activeStep } = useStepperContext()\n\n const resolvedStatus =\n status ?? (step < activeStep ? \"complete\" : step === activeStep ? \"active\" : \"upcoming\")\n\n return (\n <button\n ref={ref}\n type=\"button\"\n data-slot=\"stepper-trigger\"\n data-status={resolvedStatus}\n className={cn(\n styles.trigger,\n styles[`trigger--${resolvedStatus}`],\n className\n )}\n {...props}\n >\n {resolvedStatus === \"complete\" ? (\n <Check size={14} weight=\"bold\" aria-hidden=\"true\" />\n ) : (\n children ?? step + 1\n )}\n <span className={styles.srOnly}>\n {resolvedStatus === \"complete\"
|
|
69
|
+
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport { Check, Lock } from \"@phosphor-icons/react\"\nimport { cn } from \"../../../lib/utils\"\nimport styles from \"./stepper.module.css\"\n\n/* ─── Context ──────────────────────────────────────────────────────────────── */\n\ninterface StepperContextValue {\n activeStep: number\n orientation: \"horizontal\" | \"vertical\"\n}\n\nconst StepperContext = React.createContext<StepperContextValue>({\n activeStep: 0,\n orientation: \"horizontal\",\n})\n\nfunction useStepperContext() {\n return React.useContext(StepperContext)\n}\n\n/* ─── Stepper ──────────────────────────────────────────────────────────────── */\n\nexport interface StepperProps extends React.HTMLAttributes<HTMLDivElement> {\n activeStep?: number\n orientation?: \"horizontal\" | \"vertical\"\n}\n\nconst Stepper = React.forwardRef<HTMLDivElement, StepperProps>(\n ({ className, activeStep = 0, orientation = \"horizontal\", children, ...props }, ref) => {\n const contextValue = React.useMemo(\n () => ({ activeStep, orientation }),\n [activeStep, orientation]\n )\n\n return (\n <StepperContext.Provider value={contextValue}>\n <div\n ref={ref}\n role=\"group\"\n aria-label=\"Progress\"\n data-slot=\"stepper\"\n data-orientation={orientation}\n className={cn(\n styles.stepper,\n orientation === \"vertical\" && styles.stepperVertical,\n className\n )}\n {...props}\n >\n {children}\n </div>\n </StepperContext.Provider>\n )\n }\n)\nStepper.displayName = \"Stepper\"\n\n/* ─── StepperItem ──────────────────────────────────────────────────────────── */\n\nexport interface StepperItemProps extends React.HTMLAttributes<HTMLDivElement> {\n step: number\n status?: \"complete\" | \"active\" | \"upcoming\" | \"locked\"\n}\n\nconst StepperItem = React.forwardRef<HTMLDivElement, StepperItemProps>(\n ({ className, step, status, children, ...props }, ref) => {\n const { activeStep, orientation } = useStepperContext()\n\n const resolvedStatus =\n status ?? (step < activeStep ? \"complete\" : step === activeStep ? \"active\" : \"upcoming\")\n\n return (\n <div\n ref={ref}\n data-slot=\"stepper-item\"\n data-step={step}\n data-status={resolvedStatus}\n data-orientation={orientation}\n aria-current={resolvedStatus === \"active\" ? \"step\" : undefined}\n className={cn(\n styles.item,\n orientation === \"vertical\" && styles.itemVertical,\n className\n )}\n {...props}\n >\n {children}\n </div>\n )\n }\n)\nStepperItem.displayName = \"StepperItem\"\n\n/* ─── StepperTrigger ───────────────────────────────────────────────────────── */\n\nexport interface StepperTriggerProps\n extends React.ButtonHTMLAttributes<HTMLButtonElement> {\n step: number\n status?: \"complete\" | \"active\" | \"upcoming\" | \"locked\"\n}\n\nconst StepperTrigger = React.forwardRef<HTMLButtonElement, StepperTriggerProps>(\n ({ className, step, status, children, onClick, ...props }, ref) => {\n const { activeStep } = useStepperContext()\n\n const resolvedStatus =\n status ?? (step < activeStep ? \"complete\" : step === activeStep ? \"active\" : \"upcoming\")\n\n const isLocked = resolvedStatus === \"locked\"\n\n const handleClick = isLocked\n ? undefined\n : onClick\n\n return (\n <button\n ref={ref}\n type=\"button\"\n data-slot=\"stepper-trigger\"\n data-status={resolvedStatus}\n aria-disabled={isLocked || undefined}\n tabIndex={isLocked ? -1 : undefined}\n className={cn(\n styles.trigger,\n styles[`trigger--${resolvedStatus}`],\n className\n )}\n onClick={handleClick}\n {...props}\n >\n {resolvedStatus === \"complete\" ? (\n <Check size={14} weight=\"bold\" aria-hidden=\"true\" />\n ) : resolvedStatus === \"locked\" ? (\n <Lock size={12} weight=\"bold\" aria-hidden=\"true\" />\n ) : (\n children ?? step + 1\n )}\n <span className={styles.srOnly}>\n {resolvedStatus === \"complete\"\n ? \"Completed\"\n : resolvedStatus === \"locked\"\n ? `Step ${step + 1}, locked`\n : `Step ${step + 1}`}\n </span>\n </button>\n )\n }\n)\nStepperTrigger.displayName = \"StepperTrigger\"\n\n/* ─── StepperTitle ─────────────────────────────────────────────────────────── */\n\nconst StepperTitle = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(\n ({ className, ...props }, ref) => {\n return (\n <div\n ref={ref}\n data-slot=\"stepper-title\"\n className={cn(styles.title, className)}\n {...props}\n />\n )\n }\n)\nStepperTitle.displayName = \"StepperTitle\"\n\n/* ─── StepperDescription ───────────────────────────────────────────────────── */\n\nconst StepperDescription = React.forwardRef<\n HTMLParagraphElement,\n React.HTMLAttributes<HTMLParagraphElement>\n>(({ className, ...props }, ref) => {\n return (\n <p\n ref={ref}\n data-slot=\"stepper-description\"\n className={cn(styles.description, className)}\n {...props}\n />\n )\n})\nStepperDescription.displayName = \"StepperDescription\"\n\n/* ─── StepperSeparator ─────────────────────────────────────────────────────── */\n\nexport interface StepperSeparatorProps extends React.HTMLAttributes<HTMLDivElement> {\n complete?: boolean\n}\n\nconst StepperSeparator = React.forwardRef<HTMLDivElement, StepperSeparatorProps>(\n ({ className, complete = false, ...props }, ref) => {\n const { orientation } = useStepperContext()\n\n return (\n <div\n ref={ref}\n role=\"separator\"\n data-slot=\"stepper-separator\"\n data-orientation={orientation}\n data-complete={complete || undefined}\n className={cn(\n styles.separator,\n orientation === \"vertical\" && styles.separatorVertical,\n complete && styles.separatorComplete,\n className\n )}\n {...props}\n />\n )\n }\n)\nStepperSeparator.displayName = \"StepperSeparator\"\n\nexport {\n Stepper,\n StepperItem,\n StepperTrigger,\n StepperTitle,\n StepperDescription,\n StepperSeparator,\n}\n"
|
|
70
70
|
},
|
|
71
71
|
{
|
|
72
72
|
"path": "components/ui/stepper/stepper.module.css",
|
|
73
73
|
"type": "registry:ui",
|
|
74
|
-
"content": "/* Stepper container */\n.stepper {\n display: flex;\n align-items: flex-start;\n gap: 0;\n width: 100%;\n}\n\n.stepperVertical {\n flex-direction: column;\n}\n\n/* Stepper item */\n.item {\n display: flex;\n align-items: center;\n gap: var(--spacing-2, 0.5rem);\n flex: 1;\n min-width: 0;\n}\n\n.itemVertical {\n flex-direction: row;\n align-items: flex-start;\n}\n\n/* Trigger (step indicator circle) */\n.trigger {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n width: 2rem;\n height: 2rem;\n border-radius: var(--radius-full, 9999px);\n font-size: var(--font-size-sm, 0.875rem);\n font-weight: var(--font-weight-medium, 500);\n border: 2px solid transparent;\n cursor: pointer;\n transition: background-color var(--motion-duration-200, 200ms) var(--motion-easing-default, ease-in-out),\n border-color var(--motion-duration-200, 200ms) var(--motion-easing-default, ease-in-out),\n color var(--motion-duration-200, 200ms) var(--motion-easing-default, ease-in-out);\n}\n\n.trigger: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/* Trigger status variants */\n.trigger--complete {\n background-color: var(--interactive-primary-bg, var(--primary, #111827));\n border-color: var(--interactive-primary-bg, var(--primary, #111827));\n color: var(--interactive-primary-text, #f9fafb);\n}\n\n.trigger--active {\n background-color: transparent;\n border-color: var(--interactive-primary-bg, var(--primary, #111827));\n color: var(--interactive-primary-bg, var(--primary, #111827));\n}\n\n.trigger--upcoming {\n background-color: transparent;\n border-color: var(--border-default, #e5e7eb);\n color: var(--text-tertiary, #9ca3af);\n}\n\n/* Title */\n.title {\n font-size: var(--font-size-sm, 0.875rem);\n font-weight: var(--font-weight-medium, 500);\n color: var(--text-primary, #111827);\n line-height: var(--line-height-snug, 1.375);\n white-space: nowrap;\n}\n\n/* Description */\n.description {\n font-size: var(--font-size-xs, 0.75rem);\n color: var(--text-secondary, #6b7280);\n line-height: var(--line-height-normal, 1.5);\n margin: 0;\n}\n\n/* Separator */\n.separator {\n flex: 1;\n height: 2px;\n min-width: var(--spacing-4, 1rem);\n background-color: var(--border-muted, #e5e7eb);\n align-self: center;\n margin: 0 var(--spacing-2, 0.5rem);\n transition: background-color var(--motion-duration-200, 200ms) var(--motion-easing-default, ease-in-out);\n}\n\n.separatorVertical {\n width: 2px;\n height: var(--spacing-6, 1.5rem);\n min-width: auto;\n margin: var(--spacing-1, 0.25rem) 0 var(--spacing-1, 0.25rem) calc(1rem - 1px);\n align-self: flex-start;\n flex: none;\n}\n\n.separatorComplete {\n background-color: var(--interactive-primary-bg, var(--primary, #111827));\n}\n\n/* Screen reader only */\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"
|
|
74
|
+
"content": "/* Stepper container */\n.stepper {\n display: flex;\n align-items: flex-start;\n gap: 0;\n width: 100%;\n}\n\n.stepperVertical {\n flex-direction: column;\n}\n\n/* Stepper item */\n.item {\n display: flex;\n align-items: center;\n gap: var(--spacing-2, 0.5rem);\n flex: 1;\n min-width: 0;\n}\n\n.itemVertical {\n flex-direction: row;\n align-items: flex-start;\n}\n\n/* A bare single-line title centers against the 2rem trigger; wrapped\n * title+description blocks keep the flex-start top alignment. */\n.itemVertical > .title {\n line-height: 2rem;\n}\n\n/* Trigger (step indicator circle) */\n.trigger {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n width: 2rem;\n height: 2rem;\n border-radius: var(--radius-full, 9999px);\n font-size: var(--font-size-sm, 0.875rem);\n font-weight: var(--font-weight-medium, 500);\n border: 2px solid transparent;\n cursor: pointer;\n transition: background-color var(--motion-duration-200, 200ms) var(--motion-easing-default, ease-in-out),\n border-color var(--motion-duration-200, 200ms) var(--motion-easing-default, ease-in-out),\n color var(--motion-duration-200, 200ms) var(--motion-easing-default, ease-in-out);\n}\n\n.trigger: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/* Trigger status variants */\n.trigger--complete {\n background-color: var(--interactive-primary-bg, var(--primary, #111827));\n border-color: var(--interactive-primary-bg, var(--primary, #111827));\n color: var(--interactive-primary-text, #f9fafb);\n}\n\n.trigger--active {\n background-color: transparent;\n border-color: var(--interactive-primary-bg, var(--primary, #111827));\n color: var(--interactive-primary-bg, var(--primary, #111827));\n}\n\n.trigger--upcoming {\n background-color: transparent;\n border-color: var(--border-default, #e5e7eb);\n color: var(--text-tertiary, #9ca3af);\n}\n\n.trigger--locked {\n background-color: transparent;\n border-color: var(--border-muted, #e5e7eb);\n color: var(--text-tertiary, #9ca3af);\n cursor: not-allowed;\n opacity: var(--opacity-60, 0.6);\n}\n\n/* Locked item — muted label */\n.item[data-status=\"locked\"] .title {\n color: var(--text-secondary, #6b7280);\n font-weight: var(--font-weight-normal, 400);\n}\n\n/* Title */\n.title {\n font-size: var(--font-size-sm, 0.875rem);\n font-weight: var(--font-weight-medium, 500);\n color: var(--text-primary, #111827);\n line-height: var(--line-height-snug, 1.375);\n white-space: nowrap;\n}\n\n/* Description */\n.description {\n font-size: var(--font-size-xs, 0.75rem);\n color: var(--text-secondary, #6b7280);\n line-height: var(--line-height-normal, 1.5);\n margin: 0;\n}\n\n/* Separator */\n.separator {\n flex: 1;\n height: 2px;\n min-width: var(--spacing-4, 1rem);\n background-color: var(--border-muted, #e5e7eb);\n align-self: center;\n margin: 0 var(--spacing-2, 0.5rem);\n transition: background-color var(--motion-duration-200, 200ms) var(--motion-easing-default, ease-in-out);\n}\n\n.separatorVertical {\n width: 2px;\n height: var(--spacing-6, 1.5rem);\n min-width: auto;\n margin: var(--spacing-1, 0.25rem) 0 var(--spacing-1, 0.25rem) calc(1rem - 1px);\n align-self: flex-start;\n flex: none;\n}\n\n.separatorComplete {\n background-color: var(--interactive-primary-bg, var(--primary, #111827));\n}\n\n/* Screen reader only */\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"
|
|
75
75
|
}
|
|
76
76
|
]
|
|
77
77
|
},
|
|
@@ -146,7 +146,7 @@
|
|
|
146
146
|
{
|
|
147
147
|
"path": "components/ui/button/button.module.css",
|
|
148
148
|
"type": "registry:ui",
|
|
149
|
-
"content": "/* Button base styles */\n.base {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n gap: var(--spacing-2, 0.5rem);\n white-space: nowrap;\n border-radius: var(--radius-md, 0.375rem);\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 border: 1px solid transparent;\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), opacity var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out), transform var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out);\n text-decoration: none;\n outline: none;\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.base:active:not(:disabled) {\n transform: translateY(1px);\n transition: none;\n}\n\n.base:disabled {\n pointer-events: none;\n opacity: 0.5;\n}\n\n/* Variants */\n.variantDefault {\n background-color: var(--interactive-primary-bg, var(--primary, #111827));\n color: var(--interactive-primary-text, #f9fafb);\n}\n\n.variantSecondary {\n background-color: var(--interactive-secondary-bg, #f3f4f6);\n color: var(--interactive-secondary-text, #111827);\n}\n\n.variantOutline {\n background-color: transparent;\n color: var(--text-primary, #111827);\n border-color: var(--border-strong, #e5e7eb);\n}\n\n.variantGhost {\n background-color: var(--interactive-ghost-bg, transparent);\n color: var(--text-primary, #111827);\n}\n\n.variantDestructive {\n background-color: var(--interactive-destructive-bg, #ef4444);\n color: var(--interactive-destructive-text, #fef2f2);\n}\n\n@media (hover: hover) {\n .variantDefault:hover {\n background-color: var(--interactive-primary-bg-hover, #1f2937);\n }\n\n .variantSecondary:hover {\n background-color: var(--interactive-secondary-bg-hover, #e5e7eb);\n }\n\n .variantOutline:hover {\n background-color: var(--surface-muted, #f3f4f6);\n }\n\n .variantGhost:hover {\n background-color: var(--interactive-ghost-bg-hover, var(--surface-muted, #f3f4f6));\n }\n\n .variantDestructive:hover {\n background-color: var(--interactive-destructive-bg-hover, #dc2626);\n }\n}\n\n/* Gated state — orthogonal to variant, works with all variants */\n.base[data-gated=\"true\"] {\n opacity: var(--opacity-40, 0.4);\n cursor: not-allowed;\n border-color: var(--border-default, #e5e7eb);\n}\n\n/* Suppress active transform when gated */\n.base[data-gated=\"true\"]:active {\n transform: none;\n transition: opacity var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out);\n}\n\n/* Sizes */\n.sizeSm {\n height: 2rem;\n padding: 0 var(--spacing-3, 0.75rem);\n font-size: var(--font-size-xs, 0.75rem);\n}\n\n.sizeMd {\n height: 2.5rem;\n padding: 0 var(--spacing-4, 1rem);\n}\n\n.sizeLg {\n height: 3rem;\n padding: 0 var(--spacing-6, 1.5rem);\n font-size: var(--font-size-base, 1rem);\n}\n\n/* VI-510: form controls do not inherit font-family from the page; opt back in (matches textarea). */\n.base {\n font-family: inherit;\n}\n"
|
|
149
|
+
"content": "/* Button base styles */\n.base {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n gap: var(--spacing-2, 0.5rem);\n white-space: nowrap;\n border-radius: var(--radius-md, 0.375rem);\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 border: 1px solid transparent;\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), opacity var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out), transform var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out);\n text-decoration: none;\n outline: none;\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.base:active:not(:disabled) {\n transform: translateY(1px);\n transition: none;\n}\n\n.base:disabled {\n pointer-events: none;\n opacity: 0.5;\n}\n\n/* Variants */\n.variantDefault {\n background-color: var(--interactive-primary-bg, var(--primary, #111827));\n color: var(--interactive-primary-text, #f9fafb);\n}\n\n.variantSecondary {\n background-color: var(--interactive-secondary-bg, #f3f4f6);\n color: var(--interactive-secondary-text, #111827);\n}\n\n/* Editorial density: secondary on the card tier, ghost truly transparent.\n * Direct values — the editorial design language owns control surfaces, so the\n * theme's --interactive-secondary-bg / --interactive-ghost-bg are deliberately\n * bypassed (matches the blessed overlay, which out-cascaded the palette). */\n:global([data-density=\"editorial\"]) .variantSecondary {\n background-color: var(--surface-card, #ffffff);\n}\n\n:global([data-density=\"editorial\"]) .variantGhost {\n background-color: transparent;\n}\n\n.variantOutline {\n background-color: transparent;\n color: var(--text-primary, #111827);\n border-color: var(--border-strong, #e5e7eb);\n}\n\n.variantGhost {\n background-color: var(--interactive-ghost-bg, transparent);\n color: var(--text-primary, #111827);\n}\n\n.variantDestructive {\n background-color: var(--interactive-destructive-bg, #ef4444);\n color: var(--interactive-destructive-text, #fef2f2);\n}\n\n@media (hover: hover) {\n .variantDefault:hover {\n background-color: var(--interactive-primary-bg-hover, #1f2937);\n }\n\n .variantSecondary:hover {\n background-color: var(--interactive-secondary-bg-hover, #e5e7eb);\n }\n\n .variantOutline:hover {\n background-color: var(--surface-muted, #f3f4f6);\n }\n\n .variantGhost:hover {\n background-color: var(--interactive-ghost-bg-hover, var(--surface-muted, #f3f4f6));\n }\n\n .variantDestructive:hover {\n background-color: var(--interactive-destructive-bg-hover, #dc2626);\n }\n}\n\n/* Open / active state — ghost variant only. OPT-IN, strictly additive.\n A ghost Button reads as \"held\" while its popover/menu is open so the\n trigger no longer needs an inline style. Three opt-in triggers, all\n resolving to the same held look:\n - data-state=\"open\" — forwarded automatically by Radix when the\n Button is the trigger via <Trigger asChild> (Popover/DropdownMenu).\n - data-active=\"true\" — manual non-Radix triggers (mirrors the\n sidebar `data-active` convention).\n - .isActive — className opt-in (mirrors sidebar/section-nav).\n Default ghost rendering is untouched: none of these match unless a\n consumer explicitly sets them. Matches the prototype-overlay held look:\n background var(--surface-subtle), color var(--text-primary). */\n.variantGhost[data-state=\"open\"],\n.variantGhost[data-active=\"true\"],\n.variantGhost.isActive {\n background-color: var(--surface-subtle, var(--surface-muted, #f3f4f6));\n color: var(--text-primary, #111827);\n}\n\n/* Gated state — orthogonal to variant, works with all variants */\n.base[data-gated=\"true\"] {\n opacity: var(--opacity-40, 0.4);\n cursor: not-allowed;\n border-color: var(--border-default, #e5e7eb);\n}\n\n/* Editorial density: gated reads like plain disabled (the blessed admin\n builds render gated actions at the 0.5 disabled opacity). */\n:global([data-density=\"editorial\"]) .base[data-gated=\"true\"] {\n opacity: 0.5;\n}\n\n/* Suppress active transform when gated */\n.base[data-gated=\"true\"]:active {\n transform: none;\n transition: opacity var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out);\n}\n\n/* Sizes\n Default (no data-density attr) rendering is byte-identical to the prior\n canonical values. Under data-density=\"editorial\" (set on any ancestor,\n typically the app root), the editorial density axis re-drives specific\n metrics to the blessed admin values. */\n.sizeSm {\n height: 2rem;\n padding: 0 var(--spacing-3, 0.75rem);\n font-size: var(--font-size-xs, 0.75rem);\n}\n\n.sizeMd {\n height: 2.5rem;\n padding: 0 var(--spacing-4, 1rem);\n line-height: 1;\n}\n\n:global([data-density=\"editorial\"]) .sizeSm {\n font-size: 0.8125rem;\n}\n\n:global([data-density=\"editorial\"]) .sizeMd {\n height: 2.125rem;\n line-height: normal;\n}\n\n.sizeLg {\n height: 3rem;\n padding: 0 var(--spacing-6, 1.5rem);\n font-size: var(--font-size-base, 1rem);\n}\n\n/* VI-510: form controls do not inherit font-family from the page; opt back in (matches textarea). */\n.base {\n font-family: inherit;\n}\n"
|
|
150
150
|
}
|
|
151
151
|
]
|
|
152
152
|
},
|
|
@@ -172,7 +172,7 @@
|
|
|
172
172
|
{
|
|
173
173
|
"path": "components/ui/input/input.module.css",
|
|
174
174
|
"type": "registry:ui",
|
|
175
|
-
"content": "/* Input base styles */\n.base {\n width: 100%;\n min-width: 0;\n border: 1px solid var(--input-border, var(--border-default, #e5e7eb));\n background-color: var(--input-bg, var(--surface-interactive-default, #f9fafb));\n line-height: 1.5;\n color: var(--text-primary, #111827);\n transition: border-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}\n\n.base::placeholder {\n color: var(--text-secondary, #9ca3af);\n}\n\n@media (hover: hover) {\n .base:hover:not(:focus-visible):not(:disabled):not([aria-invalid=\"true\"]) {\n border-color: var(--border-strong, #d1d5db);\n }\n}\n\n.base: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.base:disabled {\n pointer-events: none;\n cursor: not-allowed;\n opacity: 0.5;\n}\n\n.base[aria-invalid=\"true\"] {\n border-color: transparent;\n background-color: var(--input-bg-invalid, var(--surface-error-subtle, color-mix(in srgb, var(--border-error, #ef4444) 6%, var(--input-bg, #f9fafb))));\n box-shadow: inset 0 0 0 1.5px var(--border-error, #ef4444);\n}\n\n.base[aria-invalid=\"true\"]:focus-visible {\n border-color: transparent;\n outline-offset: var(--focus-ring-offset, 2px);\n box-shadow: inset 0 0 0 1.5px var(--border-error, #ef4444),\n 0 0 0 var(--focus-ring-width, 2px) color-mix(in srgb, var(--border-error, #ef4444) 30%, transparent);\n}\n\n/* File input styles */\n.base[type=\"file\"] {\n font-size: var(--font-size-sm, 0.875rem);\n}\n\n/* Leading-icon wrapper\n *\n * When the consumer passes a leadingIcon node, Input wraps its <input> in a\n * <span> and positions the icon absolutely inside it. The <input> itself\n * picks up .hasLeadingIcon, which adds enough left padding to clear the\n * icon (sized in em so it scales with the field's font-size). */\n.leadingIconWrapper {\n position: relative;\n display: flex;\n align-items: center;\n width: 100%;\n min-width: 0;\n}\n\n.leadingIcon {\n position: absolute;\n left: var(--spacing-3, 0.75rem);\n top: 50%;\n transform: translateY(-50%);\n display: inline-flex;\n align-items: center;\n justify-content: center;\n color: var(--text-secondary, #9ca3af);\n pointer-events: none;\n font-size: 1.125em;\n line-height: 1;\n}\n\n/* Size variants */\n.sizeSm {\n height: 2.25rem;\n padding: var(--spacing-1, 0.25rem) var(--spacing-3, 0.75rem);\n font-size: var(--font-size-sm, 0.875rem);\n border-radius: var(--radius-sm, 0.25rem);\n}\n\n.sizeMd {\n height: auto;\n padding: var(--spacing-3_5, 0.875rem) var(--spacing-4, 1rem);\n font-size: var(--font-size-sm, 0.875rem);\n border-radius: var(--radius-sm, 0.5rem);\n}\n\n.sizeLg {\n height: auto;\n padding: var(--spacing-4_5, 1.125rem) var(--spacing-5, 1.25rem);\n font-size: var(--font-size-base, 1rem);\n border-radius: var(--radius-sm, 0.5rem);\n}\n\n/* Leading-icon left-padding override.\n *\n * Declared AFTER the size variants so the padding-left value wins the\n * cascade against the shorthand `padding` each size sets. Double-class\n * selector bumps specificity above the size classes for good measure. */\n.sizeSm.hasLeadingIcon {\n padding-left: calc(var(--spacing-3, 0.75rem) * 2 + 1.125em);\n}\n\n.sizeMd.hasLeadingIcon {\n padding-left: calc(var(--spacing-4, 1rem) + var(--spacing-3, 0.75rem) + 1.125em);\n}\n\n.sizeLg.hasLeadingIcon {\n padding-left: calc(var(--spacing-5, 1.25rem) + var(--spacing-3, 0.75rem) + 1.125em);\n}\n\n/* VI-510: form controls do not inherit font-family from the page; opt back in (matches textarea). */\n.base {\n font-family: inherit;\n}\n"
|
|
175
|
+
"content": "/* Input base styles */\n.base {\n width: 100%;\n min-width: 0;\n border: 1px solid var(--input-border, var(--border-default, #e5e7eb));\n /* --field-control-bg is the shared \"what surface does a form control sit on\"\n role. It defaults to var(--input-bg), so default inputs are unchanged. A\n form context (for example an in-dialog wrapper) can set --field-control-bg:\n var(--surface-subtle) to lift controls onto a different surface without a\n per-component prop. Under data-density=\"editorial\", the fallback chain\n defaults to var(--surface-card) while still honoring any scoped\n --field-control-bg override from a form context. */\n background-color: var(--field-control-bg, var(--input-bg, var(--surface-interactive-default, #f9fafb)));\n line-height: 1.5;\n color: var(--text-primary, #111827);\n transition: border-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}\n\n:global([data-density=\"editorial\"]) .base {\n background-color: var(--field-control-bg, var(--surface-card));\n}\n\n.base::placeholder {\n color: var(--input-placeholder-color, var(--text-secondary, #9ca3af));\n}\n\n@media (hover: hover) {\n .base:hover:not(:focus-visible):not(:disabled):not([aria-invalid=\"true\"]) {\n border-color: var(--border-strong, #d1d5db);\n }\n}\n\n.base: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.base:disabled {\n pointer-events: none;\n cursor: not-allowed;\n opacity: 0.5;\n}\n\n.base[aria-invalid=\"true\"] {\n border-color: transparent;\n background-color: var(--input-bg-invalid, var(--surface-error-subtle, color-mix(in srgb, var(--border-error, #ef4444) 6%, var(--field-control-bg, var(--input-bg, #f9fafb)))));\n box-shadow: inset 0 0 0 1.5px var(--border-error, #ef4444);\n}\n\n:global([data-density=\"editorial\"]) .base[aria-invalid=\"true\"] {\n background-color: var(--input-bg-invalid, var(--surface-error-subtle, color-mix(in srgb, var(--border-error, #ef4444) 6%, var(--field-control-bg, var(--surface-card)))));\n}\n\n.base[aria-invalid=\"true\"]:focus-visible {\n border-color: transparent;\n outline-offset: var(--focus-ring-offset, 2px);\n box-shadow: inset 0 0 0 1.5px var(--border-error, #ef4444),\n 0 0 0 var(--focus-ring-width, 2px) color-mix(in srgb, var(--border-error, #ef4444) 30%, transparent);\n}\n\n/* File input styles */\n.base[type=\"file\"] {\n font-size: var(--font-size-sm, 0.875rem);\n}\n\n/* Leading-icon wrapper\n *\n * When the consumer passes a leadingIcon node, Input wraps its <input> in a\n * <span> and positions the icon absolutely inside it. The <input> itself\n * picks up .hasLeadingIcon, which adds enough left padding to clear the\n * icon (sized in em so it scales with the field's font-size). */\n.leadingIconWrapper {\n position: relative;\n display: flex;\n align-items: center;\n width: 100%;\n min-width: 0;\n}\n\n.leadingIcon {\n position: absolute;\n left: var(--spacing-3, 0.75rem);\n top: 50%;\n transform: translateY(-50%);\n display: inline-flex;\n align-items: center;\n justify-content: center;\n color: var(--text-secondary, #9ca3af);\n pointer-events: none;\n font-size: 1.125em;\n line-height: 1;\n}\n\n/* Size variants\n *\n * Default rendering uses canonical values directly. Under data-density=\"editorial\"\n * (set on any ancestor), the editorial density axis bakes in the blessed admin\n * values — sm inputs at 33.5px height with --radius-md corners. Padding and the\n * md/lg sizes are not driven by the editorial treatment and remain canonical. */\n.sizeSm {\n height: 2.25rem;\n padding: var(--input-padding-sm, var(--spacing-1, 0.25rem) var(--spacing-3, 0.75rem));\n font-size: var(--font-size-sm, 0.875rem);\n border-radius: var(--radius-sm, 0.25rem);\n}\n\n:global([data-density=\"editorial\"]) .sizeSm {\n height: 33.5px;\n border-radius: var(--radius-md);\n}\n\n.sizeMd {\n height: var(--input-height-md, auto);\n padding: var(--input-padding-md, var(--spacing-3_5, 0.875rem) var(--spacing-4, 1rem));\n font-size: var(--font-size-sm, 0.875rem);\n border-radius: var(--input-radius-md, var(--radius-sm, 0.5rem));\n}\n\n.sizeLg {\n height: var(--input-height-lg, auto);\n padding: var(--input-padding-lg, var(--spacing-4_5, 1.125rem) var(--spacing-5, 1.25rem));\n font-size: var(--font-size-base, 1rem);\n border-radius: var(--input-radius-lg, var(--radius-sm, 0.5rem));\n}\n\n/* Leading-icon left-padding override.\n *\n * Declared AFTER the size variants so the padding-left value wins the\n * cascade against the shorthand `padding` each size sets. Double-class\n * selector bumps specificity above the size classes for good measure. */\n.sizeSm.hasLeadingIcon {\n padding-left: calc(var(--spacing-3, 0.75rem) * 2 + 1.125em);\n}\n\n.sizeMd.hasLeadingIcon {\n padding-left: calc(var(--spacing-4, 1rem) + var(--spacing-3, 0.75rem) + 1.125em);\n}\n\n.sizeLg.hasLeadingIcon {\n padding-left: calc(var(--spacing-5, 1.25rem) + var(--spacing-3, 0.75rem) + 1.125em);\n}\n\n/* VI-510: form controls do not inherit font-family from the page; opt back in (matches textarea). */\n.base {\n font-family: inherit;\n}\n"
|
|
176
176
|
}
|
|
177
177
|
]
|
|
178
178
|
},
|
|
@@ -249,7 +249,7 @@
|
|
|
249
249
|
{
|
|
250
250
|
"path": "components/ui/checkbox/checkbox.module.css",
|
|
251
251
|
"type": "registry:ui",
|
|
252
|
-
"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(--
|
|
252
|
+
"content": "/* Checkbox root styles\n Default rendering uses canonical values directly. Under data-density=\"editorial\"\n (set on any ancestor), the editorial density axis bakes in the blessed admin\n values: 1.125rem box, 4px radius, var(--surface-subtle) background, and\n var(--hairline-strong) border. */\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:global([data-density=\"editorial\"]) .root {\n width: 1.125rem;\n height: 1.125rem;\n border-radius: 4px;\n border-color: var(--hairline-strong, #e5e7eb);\n background-color: var(--surface-subtle);\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 background-color: transparent;\n }\n}\n\n@media (hover: hover) {\n :global([data-density=\"editorial\"]) .root:hover:not(:focus-visible):not(:disabled):not([aria-invalid=\"true\"]) {\n background-color: var(--surface-subtle);\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, var(--primary, #111827)));\n background-color: var(--checkbox-bg-checked, var(--interactive-primary-bg, var(--primary, #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, var(--primary, #111827)));\n background-color: var(--checkbox-bg-checked, var(--interactive-primary-bg, var(--primary, #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"
|
|
253
253
|
}
|
|
254
254
|
]
|
|
255
255
|
},
|
|
@@ -270,12 +270,12 @@
|
|
|
270
270
|
{
|
|
271
271
|
"path": "components/ui/select/select.tsx",
|
|
272
272
|
"type": "registry:ui",
|
|
273
|
-
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport * as SelectPrimitive from \"@radix-ui/react-select\"\nimport { CaretDownIcon, CaretUpIcon, CheckIcon } from \"@phosphor-icons/react\"\nimport { cn } from \"../../../lib/utils\"\nimport styles from \"./select.module.css\"\n\nconst Select = SelectPrimitive.Root\n\nconst SelectGroup = SelectPrimitive.Group\n\nconst SelectValue = SelectPrimitive.Value\n\nconst SelectTrigger = React.forwardRef<\n React.ElementRef<typeof SelectPrimitive.Trigger>,\n React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger> & {\n size?: \"sm\" | \"md\" | \"lg\"\n }\n>(({ className, size = \"md\", children, ...props }, ref) => {\n const sizeClass = {\n sm: styles.triggerSizeSm,\n md: styles.triggerSizeMd,\n lg: styles.triggerSizeLg,\n }[size]\n\n return (\n <SelectPrimitive.Trigger\n data-slot=\"select-trigger\"\n data-size={size}\n className={cn(\n styles.trigger,\n sizeClass,\n className\n )}\n ref={ref}\n {...props}\n >\n {children}\n <SelectPrimitive.Icon asChild>\n <CaretDownIcon className={styles.triggerIcon} />\n </SelectPrimitive.Icon>\n </SelectPrimitive.Trigger>\n )\n})\nSelectTrigger.displayName = SelectPrimitive.Trigger.displayName\n\nconst SelectContent = React.forwardRef<\n React.ElementRef<typeof SelectPrimitive.Content>,\n React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>\n>(({ className, children, position = \"popper\", ...props }, ref) => {\n return (\n <SelectPrimitive.Portal>\n <SelectPrimitive.Content\n data-slot=\"select-content\"\n className={cn(styles.content, className)}\n position={position}\n ref={ref}\n {...props}\n >\n <SelectScrollUpButton />\n <SelectPrimitive.Viewport\n className={cn(\n styles.viewport,\n position === \"popper\" && styles.viewportPopper\n )}\n >\n {children}\n </SelectPrimitive.Viewport>\n <SelectScrollDownButton />\n </SelectPrimitive.Content>\n </SelectPrimitive.Portal>\n )\n})\nSelectContent.displayName = SelectPrimitive.Content.displayName\n\nconst SelectLabel = React.forwardRef<\n React.ElementRef<typeof SelectPrimitive.Label>,\n React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>\n>(({ className, ...props }, ref) => {\n return (\n <SelectPrimitive.Label\n data-slot=\"select-label\"\n className={cn(styles.label, className)}\n ref={ref}\n {...props}\n />\n )\n})\nSelectLabel.displayName = SelectPrimitive.Label.displayName\n\nconst SelectItem = React.forwardRef<\n React.ElementRef<typeof SelectPrimitive.Item>,\n React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>\n>(({ className, children, ...props }, ref) => {\n return (\n <SelectPrimitive.Item\n data-slot=\"select-item\"\n className={cn(styles.item, className)}\n ref={ref}\n {...props}\n >\n <span className={styles.itemIndicatorWrapper}>\n <SelectPrimitive.ItemIndicator>\n <CheckIcon className={styles.itemIndicatorIcon} />\n </SelectPrimitive.ItemIndicator>\n </span>\n <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>\n </SelectPrimitive.Item>\n )\n})\nSelectItem.displayName = SelectPrimitive.Item.displayName\n\nconst SelectSeparator = React.forwardRef<\n React.ElementRef<typeof SelectPrimitive.Separator>,\n React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>\n>(({ className, ...props }, ref) => {\n return (\n <SelectPrimitive.Separator\n data-slot=\"select-separator\"\n className={cn(styles.separator, className)}\n ref={ref}\n {...props}\n />\n )\n})\nSelectSeparator.displayName = SelectPrimitive.Separator.displayName\n\nconst SelectScrollUpButton = React.forwardRef<\n React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,\n React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>\n>(({ className, ...props }, ref) => {\n return (\n <SelectPrimitive.ScrollUpButton\n data-slot=\"select-scroll-up-button\"\n className={cn(styles.scrollButton, className)}\n ref={ref}\n {...props}\n >\n <CaretUpIcon className={styles.scrollButtonIcon} />\n </SelectPrimitive.ScrollUpButton>\n )\n})\nSelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName\n\nconst SelectScrollDownButton = React.forwardRef<\n React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,\n React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>\n>(({ className, ...props }, ref) => {\n return (\n <SelectPrimitive.ScrollDownButton\n data-slot=\"select-scroll-down-button\"\n className={cn(styles.scrollButton, className)}\n ref={ref}\n {...props}\n >\n <CaretDownIcon className={styles.scrollButtonIcon} />\n </SelectPrimitive.ScrollDownButton>\n )\n})\nSelectScrollDownButton.displayName =\n SelectPrimitive.ScrollDownButton.displayName\n\nexport {\n Select,\n SelectContent,\n SelectGroup,\n SelectItem,\n SelectLabel,\n SelectScrollDownButton,\n SelectScrollUpButton,\n SelectSeparator,\n SelectTrigger,\n SelectValue,\n}\n"
|
|
273
|
+
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport * as SelectPrimitive from \"@radix-ui/react-select\"\nimport { CaretDownIcon, CaretUpIcon, CheckIcon } from \"@phosphor-icons/react\"\nimport { cn } from \"../../../lib/utils\"\nimport styles from \"./select.module.css\"\n\nconst Select = SelectPrimitive.Root\n\nconst SelectGroup = SelectPrimitive.Group\n\nconst SelectValue = SelectPrimitive.Value\n\nconst SelectTrigger = React.forwardRef<\n React.ElementRef<typeof SelectPrimitive.Trigger>,\n React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger> & {\n size?: \"sm\" | \"md\" | \"lg\"\n /**\n * Visual variant of the closed trigger. `\"default\"` keeps the 1px border on\n * the standard control surface. `\"borderless\"` drops the border and sits on\n * `--field-control-bg`, matching a borderless Input inside an in-dialog form\n * context (one that sets `--field-control-bg: var(--surface-subtle)`).\n * Opt-in — default is unchanged.\n */\n variant?: \"default\" | \"borderless\"\n }\n>(({ className, size = \"md\", variant = \"default\", children, ...props }, ref) => {\n const sizeClass = {\n sm: styles.triggerSizeSm,\n md: styles.triggerSizeMd,\n lg: styles.triggerSizeLg,\n }[size]\n\n return (\n <SelectPrimitive.Trigger\n data-slot=\"select-trigger\"\n data-size={size}\n data-variant={variant}\n className={cn(\n styles.trigger,\n sizeClass,\n variant === \"borderless\" && styles.triggerBorderless,\n className\n )}\n ref={ref}\n {...props}\n >\n {children}\n <SelectPrimitive.Icon asChild>\n <CaretDownIcon className={styles.triggerIcon} />\n </SelectPrimitive.Icon>\n </SelectPrimitive.Trigger>\n )\n})\nSelectTrigger.displayName = SelectPrimitive.Trigger.displayName\n\nconst SelectContent = React.forwardRef<\n React.ElementRef<typeof SelectPrimitive.Content>,\n React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>\n>(({ className, children, position = \"popper\", ...props }, ref) => {\n return (\n <SelectPrimitive.Portal>\n <SelectPrimitive.Content\n data-slot=\"select-content\"\n className={cn(styles.content, className)}\n position={position}\n ref={ref}\n {...props}\n >\n <SelectScrollUpButton />\n <SelectPrimitive.Viewport\n className={cn(\n styles.viewport,\n position === \"popper\" && styles.viewportPopper\n )}\n >\n {children}\n </SelectPrimitive.Viewport>\n <SelectScrollDownButton />\n </SelectPrimitive.Content>\n </SelectPrimitive.Portal>\n )\n})\nSelectContent.displayName = SelectPrimitive.Content.displayName\n\nconst SelectLabel = React.forwardRef<\n React.ElementRef<typeof SelectPrimitive.Label>,\n React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>\n>(({ className, ...props }, ref) => {\n return (\n <SelectPrimitive.Label\n data-slot=\"select-label\"\n className={cn(styles.label, className)}\n ref={ref}\n {...props}\n />\n )\n})\nSelectLabel.displayName = SelectPrimitive.Label.displayName\n\nconst SelectItem = React.forwardRef<\n React.ElementRef<typeof SelectPrimitive.Item>,\n React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>\n>(({ className, children, ...props }, ref) => {\n return (\n <SelectPrimitive.Item\n data-slot=\"select-item\"\n className={cn(styles.item, className)}\n ref={ref}\n {...props}\n >\n <span className={styles.itemIndicatorWrapper}>\n <SelectPrimitive.ItemIndicator>\n <CheckIcon className={styles.itemIndicatorIcon} />\n </SelectPrimitive.ItemIndicator>\n </span>\n <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>\n </SelectPrimitive.Item>\n )\n})\nSelectItem.displayName = SelectPrimitive.Item.displayName\n\nconst SelectSeparator = React.forwardRef<\n React.ElementRef<typeof SelectPrimitive.Separator>,\n React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>\n>(({ className, ...props }, ref) => {\n return (\n <SelectPrimitive.Separator\n data-slot=\"select-separator\"\n className={cn(styles.separator, className)}\n ref={ref}\n {...props}\n />\n )\n})\nSelectSeparator.displayName = SelectPrimitive.Separator.displayName\n\nconst SelectScrollUpButton = React.forwardRef<\n React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,\n React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>\n>(({ className, ...props }, ref) => {\n return (\n <SelectPrimitive.ScrollUpButton\n data-slot=\"select-scroll-up-button\"\n className={cn(styles.scrollButton, className)}\n ref={ref}\n {...props}\n >\n <CaretUpIcon className={styles.scrollButtonIcon} />\n </SelectPrimitive.ScrollUpButton>\n )\n})\nSelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName\n\nconst SelectScrollDownButton = React.forwardRef<\n React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,\n React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>\n>(({ className, ...props }, ref) => {\n return (\n <SelectPrimitive.ScrollDownButton\n data-slot=\"select-scroll-down-button\"\n className={cn(styles.scrollButton, className)}\n ref={ref}\n {...props}\n >\n <CaretDownIcon className={styles.scrollButtonIcon} />\n </SelectPrimitive.ScrollDownButton>\n )\n})\nSelectScrollDownButton.displayName =\n SelectPrimitive.ScrollDownButton.displayName\n\nexport {\n Select,\n SelectContent,\n SelectGroup,\n SelectItem,\n SelectLabel,\n SelectScrollDownButton,\n SelectScrollUpButton,\n SelectSeparator,\n SelectTrigger,\n SelectValue,\n}\n"
|
|
274
274
|
},
|
|
275
275
|
{
|
|
276
276
|
"path": "components/ui/select/select.module.css",
|
|
277
277
|
"type": "registry:ui",
|
|
278
|
-
"content": "/* Select Trigger */\n.trigger {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: calc(var(--spacing-1, 0.25rem) * 1.5);\n width: fit-content;\n border-radius: var(--radius-md, 0.375rem);\n border: 1px solid var(--select-border, var(--border-default, #e5e7eb));\n background-color: var(--select-bg, var(--surface-interactive-default, #f9fafb));\n padding: var(--spacing-2, 0.5rem) var(--spacing-3, 0.75rem);\n font-size: var(--font-size-sm, 0.875rem);\n /* Match Input's line-height so the trigger's height tracks the text field at\n the same size. Without this the trigger inherited line-height 2.0, rendering\n ~7px taller than a same-size Input. */\n line-height: 1.5;\n white-space: nowrap;\n color: var(--text-primary, #111827);\n transition: border-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 .trigger:hover:not(:focus-visible):not(:disabled):not([aria-invalid=\"true\"]) {\n border-color: var(--border-strong, #d1d5db);\n }\n}\n\n.trigger: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.trigger:disabled {\n cursor: not-allowed;\n opacity: 0.5;\n}\n\n.trigger[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.trigger[data-placeholder] {\n color: var(--text-secondary, #9ca3af);\n}\n\n.triggerSizeSm {\n height: 2.25rem;\n border-radius: var(--radius-sm, 0.25rem);\n}\n\n.triggerSizeMd {\n height: auto;\n padding: var(--spacing-3_5, 0.875rem) var(--spacing-4, 1rem);\n font-size: var(--font-size-sm, 0.875rem);\n border-radius: var(--radius-sm, 0.5rem);\n}\n\n.triggerSizeLg {\n height: auto;\n padding: var(--spacing-4_5, 1.125rem) var(--spacing-5, 1.25rem);\n font-size: var(--font-size-base, 1rem);\n border-radius: var(--radius-sm, 0.5rem);\n}\n\n.triggerIcon {\n pointer-events: none;\n width: 1rem;\n height: 1rem;\n flex-shrink: 0;\n color: var(--text-secondary, #9ca3af);\n}\n\n/* Select Content */\n.content {\n position: relative;\n z-index: 50;\n max-height: var(--radix-select-content-available-height, 300px);\n min-width: 9rem;\n overflow: hidden;\n border-radius: var(--radius-lg, 0.5rem);\n background-color: var(--field-menu-bg, var(--surface-popover, var(--surface-card, #ffffff)));\n color: var(--text-primary, #111827);\n box-shadow: var(--shadow-lg);\n}\n\n/* Select Viewport */\n.viewport {\n padding: var(--spacing-1, 0.25rem);\n}\n\n.viewportPopper {\n height: var(--radix-select-trigger-height);\n width: 100%;\n min-width: var(--radix-select-trigger-width);\n}\n\n/* Select Label */\n.label {\n padding: var(--spacing-2, 0.5rem) var(--spacing-3, 0.75rem);\n font-size: var(--font-size-xs, 0.75rem);\n color: var(--text-secondary, #9ca3af);\n}\n\n/* Select Item */\n.item {\n position: relative;\n display: flex;\n width: 100%;\n cursor: default;\n align-items: center;\n gap: calc(var(--spacing-2, 0.5rem) + var(--spacing-1, 0.25rem) / 2);\n border-radius: var(--radius-sm, 0.25rem);\n padding: var(--spacing-2, 0.5rem) var(--spacing-8, 2rem) var(--spacing-2, 0.5rem) var(--spacing-3, 0.75rem);\n font-size: var(--font-size-sm, 0.875rem);\n outline: none;\n user-select: none;\n}\n\n.item:focus,\n.item[data-highlighted] {\n background-color: var(--surface-interactive-hover, #f3f4f6);\n color: var(--text-primary, #111827);\n}\n\n.item[data-disabled] {\n pointer-events: none;\n opacity: 0.5;\n}\n\n.itemIndicatorWrapper {\n position: absolute;\n right: 0.5rem;\n display: flex;\n width: 1rem;\n height: 1rem;\n align-items: center;\n justify-content: center;\n}\n\n.itemIndicatorIcon {\n width: 0.875rem;\n height: 0.875rem;\n pointer-events: none;\n}\n\n/* Select Separator */\n.separator {\n pointer-events: none;\n margin: var(--spacing-1, 0.25rem) calc(-1 * var(--spacing-1, 0.25rem));\n height: 1px;\n background-color: var(--border-default, #e5e7eb);\n}\n\n/* Scroll Buttons */\n.scrollButton {\n display: flex;\n cursor: default;\n align-items: center;\n justify-content: center;\n padding: var(--spacing-1, 0.25rem);\n background-color: var(--field-menu-bg, var(--surface-popover, var(--surface-card, #ffffff)));\n}\n\n.scrollButtonIcon {\n width: 1rem;\n height: 1rem;\n}\n\n/* VI-510: form controls do not inherit font-family from the page; opt back in (matches textarea). */\n.trigger {\n font-family: inherit;\n}\n"
|
|
278
|
+
"content": "/* Select Trigger */\n.trigger {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: calc(var(--spacing-1, 0.25rem) * 1.5);\n width: fit-content;\n border-radius: var(--radius-md, 0.375rem);\n border: 1px solid var(--select-border, var(--border-default, #e5e7eb));\n background-color: var(--field-control-bg, var(--select-bg, var(--surface-interactive-default, #f9fafb)));\n padding: var(--spacing-2, 0.5rem) var(--spacing-3, 0.75rem);\n font-size: var(--font-size-sm, 0.875rem);\n /* Match Input's line-height so the trigger's height tracks the text field at\n the same size. Without this the trigger inherited line-height 2.0, rendering\n ~7px taller than a same-size Input. */\n line-height: 1.5;\n white-space: nowrap;\n color: var(--text-primary, #111827);\n transition: border-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 .trigger:hover:not(:focus-visible):not(:disabled):not([aria-invalid=\"true\"]) {\n border-color: var(--border-strong, #d1d5db);\n }\n}\n\n.trigger: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.trigger:disabled {\n cursor: not-allowed;\n opacity: 0.5;\n}\n\n.trigger[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.trigger[data-placeholder] {\n color: var(--text-secondary, #9ca3af);\n}\n\n/* Borderless trigger variant (opt-in via <SelectTrigger variant=\"borderless\">).\n *\n * Closed trigger has no 1px border and sits on --field-control-bg — the same\n * shared form-control surface role Input uses — so a borderless Select matches a\n * borderless Input inside an in-dialog form context that sets\n * --field-control-bg: var(--surface-subtle). Default Select is unchanged. The\n * border is kept transparent (rather than removed) so the trigger's box size\n * stays identical to the default trigger and the focus ring keeps its geometry.\n * Declared after the base .trigger so these overrides win the cascade. */\n.triggerBorderless {\n border-color: transparent;\n background-color: var(--field-control-bg, var(--select-bg, var(--surface-interactive-default, #f9fafb)));\n /* In-form borderless selects match the Input they sit beside: full width and\n the same md height/padding (the prototype paired select and input controls inside its field rows). */\n width: 100%;\n padding: var(--spacing-3_5, 0.875rem) var(--spacing-4, 1rem);\n}\n\n@media (hover: hover) {\n .triggerBorderless:hover:not(:focus-visible):not(:disabled):not([aria-invalid=\"true\"]) {\n border-color: transparent;\n }\n}\n\n.triggerBorderless:focus-visible {\n border-color: transparent;\n box-shadow: 0 0 0 var(--focus-ring-width, 2px) color-mix(in srgb, var(--border-focus, #111827) 15%, transparent);\n}\n\n:global([data-density=\"editorial\"]) .trigger {\n background-color: var(--field-control-bg, var(--surface-card));\n}\n\n:global([data-density=\"editorial\"]) .triggerBorderless {\n background-color: var(--field-control-bg, var(--surface-card));\n}\n\n.triggerSizeSm {\n height: 2.25rem;\n border-radius: var(--radius-sm, 0.25rem);\n}\n\n.triggerSizeMd {\n height: auto;\n padding: var(--spacing-3_5, 0.875rem) var(--spacing-4, 1rem);\n font-size: var(--font-size-sm, 0.875rem);\n border-radius: var(--radius-sm, 0.5rem);\n}\n\n.triggerSizeLg {\n height: auto;\n padding: var(--spacing-4_5, 1.125rem) var(--spacing-5, 1.25rem);\n font-size: var(--font-size-base, 1rem);\n border-radius: var(--radius-sm, 0.5rem);\n}\n\n.triggerIcon {\n pointer-events: none;\n width: 1rem;\n height: 1rem;\n flex-shrink: 0;\n color: var(--text-secondary, #9ca3af);\n}\n\n/* Select Content */\n.content {\n position: relative;\n z-index: 50;\n max-height: var(--radix-select-content-available-height, 300px);\n min-width: 9rem;\n overflow: hidden;\n border-radius: var(--radius-lg, 0.5rem);\n background-color: var(--field-menu-bg, var(--surface-popover, var(--surface-card, #ffffff)));\n color: var(--text-primary, #111827);\n box-shadow: var(--shadow-lg);\n}\n\n/* Select Viewport */\n.viewport {\n padding: var(--spacing-1, 0.25rem);\n}\n\n.viewportPopper {\n height: var(--radix-select-trigger-height);\n width: 100%;\n min-width: var(--radix-select-trigger-width);\n}\n\n/* Select Label */\n.label {\n padding: var(--spacing-2, 0.5rem) var(--spacing-3, 0.75rem);\n font-size: var(--font-size-xs, 0.75rem);\n color: var(--text-secondary, #9ca3af);\n}\n\n/* Select Item */\n.item {\n position: relative;\n display: flex;\n width: 100%;\n cursor: default;\n align-items: center;\n gap: calc(var(--spacing-2, 0.5rem) + var(--spacing-1, 0.25rem) / 2);\n border-radius: var(--radius-sm, 0.25rem);\n padding: var(--spacing-2, 0.5rem) var(--spacing-8, 2rem) var(--spacing-2, 0.5rem) var(--spacing-3, 0.75rem);\n font-size: var(--font-size-sm, 0.875rem);\n outline: none;\n user-select: none;\n}\n\n.item:focus,\n.item[data-highlighted] {\n background-color: var(--surface-interactive-hover, #f3f4f6);\n color: var(--text-primary, #111827);\n}\n\n.item[data-disabled] {\n pointer-events: none;\n opacity: 0.5;\n}\n\n.itemIndicatorWrapper {\n position: absolute;\n right: 0.5rem;\n display: flex;\n width: 1rem;\n height: 1rem;\n align-items: center;\n justify-content: center;\n}\n\n.itemIndicatorIcon {\n width: 0.875rem;\n height: 0.875rem;\n pointer-events: none;\n}\n\n/* Select Separator */\n.separator {\n pointer-events: none;\n margin: var(--spacing-1, 0.25rem) calc(-1 * var(--spacing-1, 0.25rem));\n height: 1px;\n background-color: var(--border-default, #e5e7eb);\n}\n\n/* Scroll Buttons */\n.scrollButton {\n display: flex;\n cursor: default;\n align-items: center;\n justify-content: center;\n padding: var(--spacing-1, 0.25rem);\n background-color: var(--field-menu-bg, var(--surface-popover, var(--surface-card, #ffffff)));\n}\n\n.scrollButtonIcon {\n width: 1rem;\n height: 1rem;\n}\n\n/* VI-510: form controls do not inherit font-family from the page; opt back in (matches textarea). */\n.trigger {\n font-family: inherit;\n}\n"
|
|
279
279
|
}
|
|
280
280
|
]
|
|
281
281
|
},
|
|
@@ -322,12 +322,12 @@
|
|
|
322
322
|
{
|
|
323
323
|
"path": "components/ui/field/field.tsx",
|
|
324
324
|
"type": "registry:ui",
|
|
325
|
-
"content": "import * as React from \"react\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\nimport { cn } from \"../../../lib/utils\"\nimport { Label } from \"../label/label\"\nimport styles from \"./field.module.css\"\n\n/* ─── Field ────────────────────────────────────────────────────────── */\n\nconst fieldVariants = cva(styles.field, {\n variants: {\n orientation: {\n vertical: styles.fieldVertical,\n horizontal: styles.fieldHorizontal,\n },\n },\n defaultVariants: {\n orientation: \"vertical\",\n },\n})\n\nexport interface FieldProps\n extends React.HTMLAttributes<HTMLDivElement>,\n VariantProps<typeof fieldVariants> {}\n\nconst Field = React.forwardRef<HTMLDivElement, FieldProps>(\n ({ className, orientation, ...props }, ref) => {\n return (\n <div\n role=\"group\"\n data-slot=\"field\"\n data-orientation={orientation ?? \"vertical\"}\n className={cn(fieldVariants({ orientation }), className)}\n ref={ref}\n {...props}\n />\n )\n }\n)\nField.displayName = \"Field\"\n\n/* ─── FieldLabel ────────────────────────────────────────────────────── */\n\nexport type FieldLabelProps = React.ComponentPropsWithoutRef<typeof Label>\n\nconst FieldLabel = React.forwardRef<\n React.ElementRef<typeof Label>,\n FieldLabelProps\n>(({ className, ...props }, ref) => {\n return (\n <Label\n data-slot=\"field-label\"\n className={cn(styles.fieldLabel, className)}\n ref={ref}\n {...props}\n />\n )\n})\nFieldLabel.displayName = \"FieldLabel\"\n\n/* ─── FieldDescription ──────────────────────────────────────────────── */\n\nexport type FieldDescriptionProps = React.HTMLAttributes<HTMLParagraphElement>\n\nconst FieldDescription = React.forwardRef<\n HTMLParagraphElement,\n FieldDescriptionProps\n>(({ className, ...props }, ref) => {\n return (\n <p\n data-slot=\"field-description\"\n className={cn(styles.fieldDescription, className)}\n ref={ref}\n {...props}\n />\n )\n})\nFieldDescription.displayName = \"FieldDescription\"\n\n/* ─── FieldError ────────────────────────────────────────────────────── */\n\nexport interface FieldErrorProps extends React.HTMLAttributes<HTMLDivElement> {\n errors?: Array<string | { message?: string } | undefined>\n}\n\nconst FieldError = React.forwardRef<HTMLDivElement, FieldErrorProps>(\n ({ className, children, errors, ...props }, ref) => {\n let content: React.ReactNode = children\n\n if (!content && errors?.length) {\n const messages = errors\n .map((e) => (typeof e === \"string\" ? e : e?.message))\n .filter(Boolean) as string[]\n const uniqueMessages = [...new Set(messages)]\n if (uniqueMessages.length === 1) {\n content = uniqueMessages[0]\n } else if (uniqueMessages.length > 1) {\n content = (\n <ul className={styles.fieldErrorList}>\n {uniqueMessages.map((message, index) => (\n <li key={index}>{message}</li>\n ))}\n </ul>\n )\n }\n }\n\n if (!content) return null\n\n return (\n <div\n role=\"alert\"\n data-slot=\"field-error\"\n className={cn(styles.fieldError, className)}\n ref={ref}\n {...props}\n >\n {content}\n </div>\n )\n }\n)\nFieldError.displayName = \"FieldError\"\n\nexport { Field, FieldLabel, FieldDescription, FieldError, fieldVariants }\n"
|
|
325
|
+
"content": "import * as React from \"react\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\nimport { cn } from \"../../../lib/utils\"\nimport { Label } from \"../label/label\"\nimport styles from \"./field.module.css\"\n\n/* ─── Field ────────────────────────────────────────────────────────── */\n\nconst fieldVariants = cva(styles.field, {\n variants: {\n orientation: {\n vertical: styles.fieldVertical,\n horizontal: styles.fieldHorizontal,\n },\n },\n defaultVariants: {\n orientation: \"vertical\",\n },\n})\n\nexport interface FieldProps\n extends React.HTMLAttributes<HTMLDivElement>,\n VariantProps<typeof fieldVariants> {}\n\nconst Field = React.forwardRef<HTMLDivElement, FieldProps>(\n ({ className, orientation, ...props }, ref) => {\n return (\n <div\n role=\"group\"\n data-slot=\"field\"\n data-orientation={orientation ?? \"vertical\"}\n className={cn(fieldVariants({ orientation }), className)}\n ref={ref}\n {...props}\n />\n )\n }\n)\nField.displayName = \"Field\"\n\n/* ─── FieldLabel ────────────────────────────────────────────────────── */\n\nexport type FieldLabelProps = React.ComponentPropsWithoutRef<typeof Label>\n\nconst FieldLabel = React.forwardRef<\n React.ElementRef<typeof Label>,\n FieldLabelProps\n>(({ className, ...props }, ref) => {\n return (\n <Label\n data-slot=\"field-label\"\n className={cn(styles.fieldLabel, className)}\n ref={ref}\n {...props}\n />\n )\n})\nFieldLabel.displayName = \"FieldLabel\"\n\n/* ─── FieldDescription ──────────────────────────────────────────────── */\n\nexport type FieldDescriptionProps = React.HTMLAttributes<HTMLParagraphElement>\n\nconst FieldDescription = React.forwardRef<\n HTMLParagraphElement,\n FieldDescriptionProps\n>(({ className, ...props }, ref) => {\n return (\n <p\n data-slot=\"field-description\"\n className={cn(styles.fieldDescription, className)}\n ref={ref}\n {...props}\n />\n )\n})\nFieldDescription.displayName = \"FieldDescription\"\n\n/* ─── FieldError ────────────────────────────────────────────────────── */\n\nexport interface FieldErrorProps extends React.HTMLAttributes<HTMLDivElement> {\n errors?: Array<string | { message?: string } | undefined>\n /**\n * Optional leading icon rendered before the error message (e.g. a 14px\n * Phosphor `WarningCircle`). When present, the error lays out as a centered\n * icon + message row; without it the error renders exactly as before (plain\n * inline text). The icon is `aria-hidden` — `role=\"alert\"` already conveys\n * the error to assistive tech.\n */\n icon?: React.ReactNode\n}\n\nconst FieldError = React.forwardRef<HTMLDivElement, FieldErrorProps>(\n ({ className, children, errors, icon, ...props }, ref) => {\n let content: React.ReactNode = children\n\n if (!content && errors?.length) {\n const messages = errors\n .map((e) => (typeof e === \"string\" ? e : e?.message))\n .filter(Boolean) as string[]\n const uniqueMessages = [...new Set(messages)]\n if (uniqueMessages.length === 1) {\n content = uniqueMessages[0]\n } else if (uniqueMessages.length > 1) {\n content = (\n <ul className={styles.fieldErrorList}>\n {uniqueMessages.map((message, index) => (\n <li key={index}>{message}</li>\n ))}\n </ul>\n )\n }\n }\n\n if (!content) return null\n\n return (\n <div\n role=\"alert\"\n data-slot=\"field-error\"\n className={cn(styles.fieldError, icon && styles.fieldErrorWithIcon, className)}\n ref={ref}\n {...props}\n >\n {icon && (\n <span className={styles.fieldErrorIcon} data-slot=\"field-error-icon\" aria-hidden=\"true\">\n {icon}\n </span>\n )}\n {content}\n </div>\n )\n }\n)\nFieldError.displayName = \"FieldError\"\n\nexport { Field, FieldLabel, FieldDescription, FieldError, fieldVariants }\n"
|
|
326
326
|
},
|
|
327
327
|
{
|
|
328
328
|
"path": "components/ui/field/field.module.css",
|
|
329
329
|
"type": "registry:ui",
|
|
330
|
-
"content": "/* Field */\n.field {\n display: flex;\n width: 100%;\n gap: var(--spacing-2, 0.5rem);\n}\n\n.fieldVertical {\n flex-direction: column;\n}\n\n.fieldHorizontal {\n flex-direction: row;\n align-items: center;\n}\n\n/* Field Label */\n.fieldLabel {\n font-size: var(--font-size-sm, 0.875rem);\n font-weight: var(--font-weight-medium, 500);\n color: var(--text-primary, currentColor);\n line-height: 1.4;\n}\n\n/* Field Description */\n.fieldDescription {\n font-size: var(--font-size-xs, 0.75rem);\n font-weight: var(--font-weight-normal, 400);\n color: var(--text-secondary, currentColor);\n line-height: 1.5;\n margin: 0;\n}\n\n/* Field Error */\n.fieldError {\n font-size: var(--font-size-sm, 0.875rem);\n font-weight: var(--font-weight-normal, 400);\n color: var(--text-error, currentColor);\n}\n\n.fieldErrorList {\n margin-left: var(--spacing-4, 1rem);\n list-style: disc;\n display: flex;\n flex-direction: column;\n gap: var(--spacing-1, 0.25rem);\n}\n"
|
|
330
|
+
"content": "/* Field */\n.field {\n display: flex;\n width: 100%;\n gap: var(--spacing-2, 0.5rem);\n}\n\n.fieldVertical {\n flex-direction: column;\n}\n\n.fieldHorizontal {\n flex-direction: row;\n align-items: center;\n}\n\n/* Field Label\n * Default: canonical 14px / text-primary. Density axis: editorial tightens\n * to 13px / text-secondary. Switch via data-density=\"editorial\" on any ancestor. */\n.fieldLabel {\n font-size: var(--font-size-sm, 0.875rem);\n font-weight: var(--font-weight-medium, 500);\n color: var(--text-primary, currentColor);\n line-height: 1.4;\n}\n\n:global([data-density=\"editorial\"]) .fieldLabel {\n font-size: 13px;\n color: var(--text-secondary);\n}\n\n/* Field Description\n * Default: canonical 12px / text-secondary. Density axis: editorial tightens\n * to 11px / text-tertiary. */\n.fieldDescription {\n font-size: var(--font-size-xs, 0.75rem);\n font-weight: var(--font-weight-normal, 400);\n color: var(--text-secondary, currentColor);\n line-height: 1.5;\n margin: 0;\n}\n\n:global([data-density=\"editorial\"]) .fieldDescription {\n font-size: 11px;\n color: var(--text-tertiary);\n}\n\n/* Field Error\n * Default: canonical 14px. Density axis: editorial tightens to 13px. */\n.fieldError {\n font-size: var(--font-size-sm, 0.875rem);\n font-weight: var(--font-weight-normal, 400);\n color: var(--text-error, currentColor);\n}\n\n:global([data-density=\"editorial\"]) .fieldError {\n font-size: 13px;\n}\n\n/* Leading-icon layout — opt-in. Only applied when the consumer passes an\n * `icon` to FieldError, so the default (text-only) error is unchanged. Lays\n * the icon + message out as a centered row; the icon sizes itself in em so it\n * tracks the error's font-size (a 14px WarningCircle at the editorial 13px\n * ramp), matching the prototype's field-error icon sizing (14px). */\n.fieldErrorWithIcon {\n display: flex;\n align-items: center;\n /* 6px gap — no --spacing-1.5 token exists, so the 6px step uses\n calc(--spacing-1 + --spacing-1/2) (the same pattern badge/kbd use). */\n gap: calc(var(--spacing-1, 0.25rem) + var(--spacing-1, 0.25rem) / 2);\n}\n\n.fieldErrorIcon {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n font-size: 1.077em;\n line-height: 1;\n}\n\n.fieldErrorList {\n margin-left: var(--spacing-4, 1rem);\n list-style: disc;\n display: flex;\n flex-direction: column;\n gap: var(--spacing-1, 0.25rem);\n}\n"
|
|
331
331
|
}
|
|
332
332
|
]
|
|
333
333
|
},
|
|
@@ -396,12 +396,12 @@
|
|
|
396
396
|
{
|
|
397
397
|
"path": "components/ui/badge/badge.tsx",
|
|
398
398
|
"type": "registry:ui",
|
|
399
|
-
"content": "import * as React from \"react\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\nimport { cn } from \"../../../lib/utils\"\nimport styles from \"./badge.module.css\"\n\nconst badgeVariants = cva(styles.base, {\n variants: {\n variant: {\n default: styles.variantDefault,\n secondary: styles.variantSecondary,\n outline: styles.variantOutline,\n destructive: styles.variantDestructive,\n success: styles.variantSuccess,\n warning: styles.variantWarning,\n info: styles.variantInfo,\n neutral: styles.variantNeutral,\n \"filled-destructive\": styles.variantFilledDestructive,\n \"filled-success\": styles.variantFilledSuccess,\n \"filled-warning\": styles.variantFilledWarning,\n \"filled-info\": styles.variantFilledInfo,\n \"filled-neutral\": styles.variantFilledNeutral,\n },\n size: {\n sm: styles.sizeSm,\n md: styles.sizeMd,\n lg: styles.sizeLg,\n },\n },\n defaultVariants: {\n variant: \"default\",\n size: \"md\",\n },\n})\n\nexport interface BadgeProps\n extends React.HTMLAttributes<HTMLSpanElement>,\n VariantProps<typeof badgeVariants> {\n /** Render the label in uppercase. Also controllable via the\n * `--badge-text-transform` CSS custom property from a theme. */\n uppercase?: boolean\n}\n\nconst Badge = React.forwardRef<HTMLSpanElement, BadgeProps>(\n ({ className, variant, size, uppercase, ...props }
|
|
399
|
+
"content": "import * as React from \"react\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\nimport { cn } from \"../../../lib/utils\"\nimport styles from \"./badge.module.css\"\n\nconst badgeVariants = cva(styles.base, {\n variants: {\n variant: {\n default: styles.variantDefault,\n secondary: styles.variantSecondary,\n outline: styles.variantOutline,\n destructive: styles.variantDestructive,\n success: styles.variantSuccess,\n warning: styles.variantWarning,\n info: styles.variantInfo,\n neutral: styles.variantNeutral,\n \"filled-destructive\": styles.variantFilledDestructive,\n \"filled-success\": styles.variantFilledSuccess,\n \"filled-warning\": styles.variantFilledWarning,\n \"filled-info\": styles.variantFilledInfo,\n \"filled-neutral\": styles.variantFilledNeutral,\n },\n size: {\n sm: styles.sizeSm,\n md: styles.sizeMd,\n lg: styles.sizeLg,\n },\n /* `case=\"sentence\"` opts a sm/md badge OUT of the editorial uppercase +\n tracking treatment (lg already opts out via its size class). Omit for the\n default theme-driven casing. */\n case: {\n sentence: styles.caseSentence,\n },\n },\n defaultVariants: {\n variant: \"default\",\n size: \"md\",\n },\n})\n\nexport interface BadgeProps\n extends React.HTMLAttributes<HTMLSpanElement>,\n VariantProps<typeof badgeVariants> {\n /** Render the label in uppercase. Also controllable via the\n * `--badge-text-transform` CSS custom property from a theme. */\n uppercase?: boolean\n /** Render as a circular, fixed-square chip carrying a single centered glyph\n * (e.g. a filled-success check). Opt-in; default badges are unaffected. */\n iconOnly?: boolean\n}\n\nconst Badge = React.forwardRef<HTMLSpanElement, BadgeProps>(\n (\n { className, variant, size, case: textCase, uppercase, iconOnly, ...props },\n ref\n ) => {\n return (\n <span\n ref={ref}\n data-slot=\"badge\"\n data-variant={variant ?? \"default\"}\n data-size={size ?? \"md\"}\n data-icon-only={iconOnly ? \"\" : undefined}\n className={cn(\n badgeVariants({ variant, size, case: textCase }),\n uppercase && styles.uppercase,\n iconOnly && styles.iconOnly,\n className\n )}\n {...props}\n />\n )\n }\n)\nBadge.displayName = \"Badge\"\n\nexport { Badge, badgeVariants }\n"
|
|
400
400
|
},
|
|
401
401
|
{
|
|
402
402
|
"path": "components/ui/badge/badge.module.css",
|
|
403
403
|
"type": "registry:ui",
|
|
404
|
-
"content": "/* Badge base — sizing (height, padding, font-size, gap) is driven by the\n * size classes below. Height is intrinsic (padding + line-height) so badges\n * stay correct when text wraps or icon slots are introduced. */\n.base {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: fit-content;\n flex-shrink: 0;\n overflow: hidden;\n border-radius: var(--radius-full, 9999px);\n border: 1px solid transparent;\n line-height: 1;\n font-weight: var(--font-weight-medium, 500);\n
|
|
404
|
+
"content": "/* Badge base — sizing (height, padding, font-size, gap) is driven by the\n * size classes below. Height is intrinsic (padding + line-height) so badges\n * stay correct when text wraps or icon slots are introduced. */\n.base {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: fit-content;\n flex-shrink: 0;\n overflow: hidden;\n border-radius: var(--radius-full, 9999px);\n border: 1px solid transparent;\n line-height: 1;\n /* Typography defaults. Under data-density=\"editorial\" on any ancestor,\n the density rules below override to uppercase/heavier/tracked. */\n font-weight: var(--font-weight-medium, 500);\n text-transform: none;\n letter-spacing: normal;\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/* Editorial density: uppercase + heavier + tracked. Any ancestor with\n * data-density=\"editorial\" (for example the app root) activates this. */\n:global([data-density=\"editorial\"]) .base {\n /* Theme-invariant: the editorial treatment runs 600 even when a theme maps\n --font-weight-semibold heavier (entr: 700). density-literal */\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 0.04em;\n}\n\n/* Editorial density opt-out: sizeLg is intrinsically a label (never tag),\n * and caseSentence lets sm/md opt out per-usage. Both must win over the\n * editorial .base rule above (same specificity, placed after). */\n:global([data-density=\"editorial\"]) .sizeLg,\n:global([data-density=\"editorial\"]) .caseSentence {\n text-transform: none;\n letter-spacing: 0;\n}\n\n/* uppercase prop — directly sets text-transform so it composes cleanly with\n * the density axis. Specificity (0,2,0), placed after .base, wins over default. */\n.base.uppercase {\n text-transform: uppercase;\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/* Sizes — token-driven padding + font-size + gap on the 4px grid.\n * No --spacing-1.5 token exists, so the 6px step uses\n * calc(--spacing-1 + --spacing-1/2) — mirrors the pattern used in kbd. */\n.sizeSm {\n gap: var(--spacing-1, 0.25rem);\n padding: 0 calc(var(--spacing-1, 0.25rem) + var(--spacing-1, 0.25rem) / 2);\n font-size: var(--font-size-xs, 0.75rem);\n}\n\n/* md — matches canonical rendering (1.25rem total height). Under\n data-density=\"editorial\", font-size steps down to 11px. */\n.sizeMd {\n gap: var(--spacing-1, 0.25rem);\n padding: calc(var(--spacing-1, 0.25rem) / 2) var(--spacing-2, 0.5rem);\n font-size: var(--font-size-xs, 0.75rem);\n}\n\n:global([data-density=\"editorial\"]) .sizeMd {\n font-size: 11px;\n}\n\n.sizeLg {\n gap: calc(var(--spacing-1, 0.25rem) + var(--spacing-1, 0.25rem) / 2);\n padding: var(--spacing-1, 0.25rem) var(--spacing-3, 0.75rem);\n font-size: var(--font-size-sm, 0.875rem);\n}\n\n/* Sentence case — opt OUT of any uppercase/tracking applied by the density\n * axis or the uppercase prop. The large size always opts out (it is\n * intrinsically a label); the .caseSentence class lets sm/md opt out\n * per-usage. The editorial-density opt-out mirror lives above, after the\n * editorial .base rule, ensuring it wins at equal specificity. */\n.sizeLg,\n.caseSentence {\n text-transform: none;\n letter-spacing: 0;\n}\n\n/* Icon scaling — leading glyphs track the per-size font-size step. */\n.sizeSm > svg {\n width: 0.75rem;\n height: 0.75rem;\n}\n\n.sizeMd > svg {\n width: 0.875rem;\n height: 0.875rem;\n}\n\n.sizeLg > svg {\n width: 1rem;\n height: 1rem;\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/* Editorial density: secondary surfaces sit on the card tier. Direct value —\n * the editorial design language owns control surfaces, so the theme's\n * --interactive-secondary-bg is deliberately bypassed (matches the blessed\n * overlay, which out-cascaded the palette at :root). */\n:global([data-density=\"editorial\"]) .variantSecondary {\n background-color: var(--surface-card, #ffffff);\n}\n\n.variantOutline {\n background-color: transparent;\n color: var(--text-primary, #111827);\n border-color: var(--border-default, #e5e7eb);\n}\n\n/* Tonal variants — canonical surface tokens as plain values. Under\n * data-density=\"editorial\", backgrounds switch to color-mix tints for\n * higher saturation at editorial admin density. Text colors are unchanged. */\n.variantDestructive {\n background-color: var(--surface-error-subtle, transparent);\n color: var(--text-error, currentColor);\n border-color: transparent;\n}\n\n:global([data-density=\"editorial\"]) .variantDestructive {\n background-color: color-mix(in srgb, var(--destructive) 20%, 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:global([data-density=\"editorial\"]) .variantSuccess {\n background-color: color-mix(in srgb, var(--success) 18%, 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:global([data-density=\"editorial\"]) .variantWarning {\n background-color: color-mix(in srgb, var(--warning) 20%, 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:global([data-density=\"editorial\"]) .variantInfo {\n background-color: color-mix(in srgb, var(--info) 18%, transparent);\n}\n\n.variantNeutral {\n background-color: var(--surface-muted, #f3f4f6);\n color: var(--text-secondary, var(--text-primary, #374151));\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\n/* filled-neutral — the saturated counterpart to `neutral`. Neutral has no\n * brand `-default` surface, so it uses a fixed neutral-600 fill. Because that\n * fill is dark in BOTH modes, text must stay white (not `--text-inverse`,\n * which flips to a dark value in dark mode). */\n.variantFilledNeutral {\n background-color: var(--color-neutral-600, #4b5563);\n color: var(--color-white, #ffffff);\n border-color: transparent;\n}\n\n/* Icon-only — a circular, fixed-square chip carrying a single centered glyph\n * (for example the filled-success check used by the Roles matrix active cell +\n * legend). Zero padding and a fixed glyph size keep it crisp at any density.\n * The glyph centers via the base flex; per-size svg rules are overridden here. */\n.iconOnly {\n width: 1.375rem;\n height: 1.375rem;\n min-width: 1.375rem;\n padding: 0;\n gap: 0;\n border-radius: var(--radius-full, 9999px);\n}\n\n.iconOnly > svg {\n width: 0.875rem;\n height: 0.875rem;\n}\n"
|
|
405
405
|
}
|
|
406
406
|
]
|
|
407
407
|
},
|
|
@@ -443,12 +443,12 @@
|
|
|
443
443
|
{
|
|
444
444
|
"path": "components/ui/chip/chip.tsx",
|
|
445
445
|
"type": "registry:ui",
|
|
446
|
-
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport { XIcon } from \"@phosphor-icons/react\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\nimport { cn } from \"../../../lib/utils\"\nimport styles from \"./chip.module.css\"\n\n/* ─── Chip (base) ────────────────────────────────────────────────────── */\n\nconst chipVariants = cva(styles.base, {\n variants: {\n variant: {\n default: styles.variantDefault,\n outlined: styles.variantOutlined,\n },\n size: {\n sm: styles.sizeSm,\n md: styles.sizeMd,\n lg: styles.sizeLg,\n },\n },\n defaultVariants: {\n variant: \"default\",\n size: \"md\",\n },\n})\n\nexport interface ChipProps\n extends React.HTMLAttributes<HTMLDivElement>,\n VariantProps<typeof chipVariants> {\n /** Optional avatar/icon rendered before the label. */\n avatar?: React.ReactNode\n /** Optional leading icon rendered before the label (after avatar). */\n leadingIcon?: React.ReactNode\n /** Label content. Renders as children if omitted. */\n label?: React.ReactNode\n /** Custom delete icon. Defaults to XIcon. */\n deleteIcon?: React.ReactNode\n /** Called when the delete button is activated. When provided the delete button is shown. */\n onDeleted?: () => void\n /** Aria label for the delete button. Defaults to \"Remove\". */\n deleteLabel?: string\n}\n\nconst Chip = React.forwardRef<HTMLDivElement, ChipProps>(\n (\n {\n className,\n variant,\n size,\n avatar,\n leadingIcon,\n label,\n children,\n deleteIcon,\n onDeleted,\n deleteLabel = \"Remove\",\n ...props\n },\n ref,\n ) => {\n const content = label ?? children\n\n return (\n <div\n ref={ref}\n data-slot=\"chip\"\n data-variant={variant ?? \"default\"}\n data-size={size ?? \"md\"}\n className={cn(chipVariants({ variant, size }), className)}\n {...props}\n >\n {avatar ? (\n <span className={styles.avatar} aria-hidden=\"true\">\n {avatar}\n </span>\n ) : null}\n {leadingIcon ? (\n <span className={styles.leadingIcon} aria-hidden=\"true\">\n {leadingIcon}\n </span>\n ) : null}\n <span className={styles.label}>{content}</span>\n {onDeleted ? (\n <button\n type=\"button\"\n aria-label={deleteLabel}\n className={styles.deleteButton}\n onClick={(e) => {\n e.stopPropagation()\n onDeleted()\n }}\n tabIndex={0}\n >\n {deleteIcon ?? (\n <XIcon\n size={12}\n weight=\"bold\"\n aria-hidden=\"true\"\n className={styles.deleteIcon}\n />\n )}\n </button>\n ) : null}\n </div>\n )\n },\n)\nChip.displayName = \"Chip\"\n\n/* ─── ChoiceChip (radio-style single-select) ─────────────────────────── */\n\nexport interface ChoiceChipProps\n extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, \"onClick\">,\n VariantProps<typeof chipVariants> {\n /** Whether this chip is currently selected. */\n selected?: boolean\n /** Optional avatar/icon rendered before the label. */\n avatar?: React.ReactNode\n /** Optional leading icon rendered before the label. */\n leadingIcon?: React.ReactNode\n /** Label text or content. */\n label?: React.ReactNode\n /** Called when the chip is pressed. */\n onPressed?: () => void\n /** Value used when in a ChipGroup. */\n value?: string\n}\n\nconst ChoiceChip = React.forwardRef<HTMLButtonElement, ChoiceChipProps>(\n (\n {\n className,\n variant,\n size,\n selected = false,\n avatar,\n leadingIcon,\n label,\n children,\n onPressed,\n value,\n disabled,\n ...props\n },\n ref,\n ) => {\n const content = label ?? children\n\n return (\n <button\n ref={ref}\n type=\"button\"\n role=\"radio\"\n aria-checked={selected}\n data-slot=\"choice-chip\"\n data-variant={variant ?? \"default\"}\n data-size={size ?? \"md\"}\n data-selected={selected ? \"true\" : \"false\"}\n data-value={value}\n disabled={disabled}\n className={cn(\n chipVariants({ variant, size }),\n styles.interactive,\n selected && styles.selected,\n className,\n )}\n onClick={onPressed}\n {...props}\n >\n {avatar ? (\n <span className={styles.avatar} aria-hidden=\"true\">\n {avatar}\n </span>\n ) : null}\n {leadingIcon ? (\n <span className={styles.leadingIcon} aria-hidden=\"true\">\n {leadingIcon}\n </span>\n ) : null}\n <span className={styles.label}>{content}</span>\n </button>\n )\n },\n)\nChoiceChip.displayName = \"ChoiceChip\"\n\n/* ─── FilterChip (multi-select toggle-style) ─────────────────────────── */\n\nexport interface FilterChipProps\n extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, \"onClick\">,\n VariantProps<typeof chipVariants> {\n /** Whether this chip is currently active/selected. */\n selected?: boolean\n /** Optional leading icon. */\n leadingIcon?: React.ReactNode\n /** Label text or content. */\n label?: React.ReactNode\n /** Called when the chip is toggled. */\n onPressed?: () => void\n /** Value used when in a ChipGroup. */\n value?: string\n /** Optional count pill rendered after the label (e.g. \"47\", 132). */\n count?: React.ReactNode\n /** Tone for the count pill. \"neutral\" uses muted surface; \"primary\" uses accent surface. Defaults to \"neutral\". */\n countTone?: \"primary\" | \"neutral\"\n /**\n * Selected-state visual treatment.\n * - `\"accent\"` (default) — accent-tinted background + accent border + link text colour.\n * - `\"neutral\"` — neutral-elevated surface (--surface-card) + neutral border + primary text;\n * the count pill renders as a solid mint pill (--surface-success-default) when selected.\n * Use for editorial / admin contexts where accent bleed across shared tokens is undesirable.\n */\n selectedTreatment?: \"accent\" | \"neutral\"\n}\n\nconst FilterChip = React.forwardRef<HTMLButtonElement, FilterChipProps>(\n (\n {\n className,\n variant,\n size,\n selected = false,\n leadingIcon,\n label,\n children,\n onPressed,\n value,\n disabled,\n count,\n countTone = \"neutral\",\n selectedTreatment = \"accent\",\n ...props\n },\n ref,\n ) => {\n const content = label ?? children\n const isNeutral = selectedTreatment === \"neutral\"\n\n return (\n <button\n ref={ref}\n type=\"button\"\n role=\"checkbox\"\n aria-checked={selected}\n data-slot=\"filter-chip\"\n data-variant={variant ?? \"default\"}\n data-size={size ?? \"md\"}\n data-selected={selected ? \"true\" : \"false\"}\n data-selected-treatment={selectedTreatment}\n data-value={value}\n disabled={disabled}\n className={cn(\n chipVariants({ variant, size }),\n styles.interactive,\n selected && (isNeutral ? styles.selectedNeutral : styles.selected),\n className,\n )}\n onClick={onPressed}\n {...props}\n >\n {leadingIcon ? (\n <span className={styles.leadingIcon} aria-hidden=\"true\">\n {leadingIcon}\n </span>\n ) : null}\n <span className={styles.label}>{content}</span>\n {count != null ? (\n <span\n data-slot=\"filter-chip-count\"\n data-tone={countTone}\n aria-hidden=\"false\"\n className={cn(\n styles.count,\n isNeutral && selected\n ? styles.countSolid\n : countTone === \"primary\"\n ? styles.countPrimary\n : styles.countNeutral,\n )}\n >\n {count}\n </span>\n ) : null}\n </button>\n )\n },\n)\nFilterChip.displayName = \"FilterChip\"\n\nexport { Chip, chipVariants, ChoiceChip, FilterChip }\n"
|
|
446
|
+
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport { XIcon } from \"@phosphor-icons/react\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\nimport { cn } from \"../../../lib/utils\"\nimport styles from \"./chip.module.css\"\n\n/* ─── Chip (base) ────────────────────────────────────────────────────── */\n\nconst chipVariants = cva(styles.base, {\n variants: {\n variant: {\n default: styles.variantDefault,\n outlined: styles.variantOutlined,\n },\n size: {\n sm: styles.sizeSm,\n md: styles.sizeMd,\n lg: styles.sizeLg,\n },\n },\n defaultVariants: {\n variant: \"default\",\n size: \"md\",\n },\n})\n\nexport interface ChipProps\n extends React.HTMLAttributes<HTMLDivElement>,\n VariantProps<typeof chipVariants> {\n /** Optional avatar/icon rendered before the label. */\n avatar?: React.ReactNode\n /** Optional leading icon rendered before the label (after avatar). */\n leadingIcon?: React.ReactNode\n /** Label content. Renders as children if omitted. */\n label?: React.ReactNode\n /** Custom delete icon. Defaults to XIcon. */\n deleteIcon?: React.ReactNode\n /** Called when the delete button is activated. When provided the delete button is shown. */\n onDeleted?: () => void\n /** Aria label for the delete button. Defaults to \"Remove\". */\n deleteLabel?: string\n}\n\nconst Chip = React.forwardRef<HTMLDivElement, ChipProps>(\n (\n {\n className,\n variant,\n size,\n avatar,\n leadingIcon,\n label,\n children,\n deleteIcon,\n onDeleted,\n deleteLabel = \"Remove\",\n ...props\n },\n ref,\n ) => {\n const content = label ?? children\n\n return (\n <div\n ref={ref}\n data-slot=\"chip\"\n data-variant={variant ?? \"default\"}\n data-size={size ?? \"md\"}\n className={cn(chipVariants({ variant, size }), className)}\n {...props}\n >\n {avatar ? (\n <span className={styles.avatar} aria-hidden=\"true\">\n {avatar}\n </span>\n ) : null}\n {leadingIcon ? (\n <span className={styles.leadingIcon} aria-hidden=\"true\">\n {leadingIcon}\n </span>\n ) : null}\n <span className={styles.label}>{content}</span>\n {onDeleted ? (\n <button\n type=\"button\"\n aria-label={deleteLabel}\n className={styles.deleteButton}\n onClick={(e) => {\n e.stopPropagation()\n onDeleted()\n }}\n tabIndex={0}\n >\n {deleteIcon ?? (\n <XIcon\n size={12}\n weight=\"bold\"\n aria-hidden=\"true\"\n className={styles.deleteIcon}\n />\n )}\n </button>\n ) : null}\n </div>\n )\n },\n)\nChip.displayName = \"Chip\"\n\n/* ─── ChoiceChip (radio-style single-select) ─────────────────────────── */\n\nexport interface ChoiceChipProps\n extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, \"onClick\">,\n VariantProps<typeof chipVariants> {\n /** Whether this chip is currently selected. */\n selected?: boolean\n /** Optional avatar/icon rendered before the label. */\n avatar?: React.ReactNode\n /** Optional leading icon rendered before the label. */\n leadingIcon?: React.ReactNode\n /** Label text or content. */\n label?: React.ReactNode\n /** Called when the chip is pressed. */\n onPressed?: () => void\n /** Value used when in a ChipGroup. */\n value?: string\n}\n\nconst ChoiceChip = React.forwardRef<HTMLButtonElement, ChoiceChipProps>(\n (\n {\n className,\n variant,\n size,\n selected = false,\n avatar,\n leadingIcon,\n label,\n children,\n onPressed,\n value,\n disabled,\n ...props\n },\n ref,\n ) => {\n const content = label ?? children\n\n return (\n <button\n ref={ref}\n type=\"button\"\n role=\"radio\"\n aria-checked={selected}\n data-slot=\"choice-chip\"\n data-variant={variant ?? \"default\"}\n data-size={size ?? \"md\"}\n data-selected={selected ? \"true\" : \"false\"}\n data-value={value}\n disabled={disabled}\n className={cn(\n chipVariants({ variant, size }),\n styles.interactive,\n selected && styles.selected,\n className,\n )}\n onClick={onPressed}\n {...props}\n >\n {avatar ? (\n <span className={styles.avatar} aria-hidden=\"true\">\n {avatar}\n </span>\n ) : null}\n {leadingIcon ? (\n <span className={styles.leadingIcon} aria-hidden=\"true\">\n {leadingIcon}\n </span>\n ) : null}\n <span className={styles.label}>{content}</span>\n </button>\n )\n },\n)\nChoiceChip.displayName = \"ChoiceChip\"\n\n/* ─── FilterChip (multi-select toggle-style) ─────────────────────────── */\n\nexport interface FilterChipProps\n extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, \"onClick\">,\n VariantProps<typeof chipVariants> {\n /** Whether this chip is currently active/selected. */\n selected?: boolean\n /** Optional leading icon. */\n leadingIcon?: React.ReactNode\n /** Label text or content. */\n label?: React.ReactNode\n /** Called when the chip is toggled. */\n onPressed?: () => void\n /** Value used when in a ChipGroup. */\n value?: string\n /** Optional count pill rendered after the label (e.g. \"47\", 132). */\n count?: React.ReactNode\n /** Tone for the count pill. \"neutral\" uses muted surface; \"primary\" uses accent surface. Defaults to \"neutral\". */\n countTone?: \"primary\" | \"neutral\"\n /**\n * Selected-state visual treatment.\n * - `\"accent\"` (default) — accent-tinted background + accent border + link text colour.\n * - `\"neutral\"` — neutral-elevated surface (--surface-card) + neutral border + primary text;\n * the count pill renders as a solid mint pill (--surface-success-default) when selected.\n * Use for editorial / admin contexts where accent bleed across shared tokens is undesirable.\n */\n selectedTreatment?: \"accent\" | \"neutral\"\n /** Optional trailing icon rendered after the count (e.g. a dropdown caret) for \"filter-as-dropdown\" chips. Muted by default. */\n trailingIcon?: React.ReactNode\n}\n\nconst FilterChip = React.forwardRef<HTMLButtonElement, FilterChipProps>(\n (\n {\n className,\n variant,\n size,\n selected = false,\n leadingIcon,\n label,\n children,\n onPressed,\n value,\n disabled,\n count,\n countTone = \"neutral\",\n selectedTreatment = \"accent\",\n trailingIcon,\n ...props\n },\n ref,\n ) => {\n const content = label ?? children\n const isNeutral = selectedTreatment === \"neutral\"\n\n return (\n <button\n ref={ref}\n type=\"button\"\n role=\"checkbox\"\n aria-checked={selected}\n data-slot=\"filter-chip\"\n data-variant={variant ?? \"default\"}\n data-size={size ?? \"md\"}\n data-selected={selected ? \"true\" : \"false\"}\n data-selected-treatment={selectedTreatment}\n data-value={value}\n disabled={disabled}\n className={cn(\n chipVariants({ variant, size }),\n styles.interactive,\n styles.filterChip,\n selected && (isNeutral ? styles.selectedNeutral : styles.selected),\n className,\n )}\n onClick={onPressed}\n {...props}\n >\n {leadingIcon ? (\n <span className={styles.leadingIcon} aria-hidden=\"true\">\n {leadingIcon}\n </span>\n ) : null}\n <span className={styles.label}>{content}</span>\n {count != null ? (\n <span\n data-slot=\"filter-chip-count\"\n data-tone={countTone}\n aria-hidden=\"false\"\n className={cn(\n styles.count,\n isNeutral && selected\n ? styles.countSolid\n : countTone === \"primary\"\n ? styles.countPrimary\n : styles.countNeutral,\n )}\n >\n {count}\n </span>\n ) : null}\n {trailingIcon ? (\n <span className={styles.trailing} aria-hidden=\"true\">\n {trailingIcon}\n </span>\n ) : null}\n </button>\n )\n },\n)\nFilterChip.displayName = \"FilterChip\"\n\nexport { Chip, chipVariants, ChoiceChip, FilterChip }\n"
|
|
447
447
|
},
|
|
448
448
|
{
|
|
449
449
|
"path": "components/ui/chip/chip.module.css",
|
|
450
450
|
"type": "registry:ui",
|
|
451
|
-
"content": "/* Chip base */\n.base {\n display: inline-flex;\n align-items: center;\n gap: var(--spacing-1, 0.25rem);\n width: fit-content;\n flex-shrink: 0;\n border-radius: var(--radius-full, 9999px);\n border: var(--stroke-width-thin, 1px) solid var(--border-default, #e5e7eb);\n background-color: var(--surface-card, #ffffff);\n color: var(--text-primary, #111827);\n font-family: var(--font-body, inherit);\n font-weight: var(--font-weight-medium, 500);\n white-space: nowrap;\n user-select: none;\n transition:\n background-color var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out),\n color var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out),\n border-color var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out);\n}\n\n/* Variant: default (filled/tonal surface) */\n.variantDefault {\n background-color: var(--surface-muted, #f3f4f6);\n border-color: transparent;\n color: var(--text-primary, #111827);\n}\n\n/* Variant: outlined */\n.variantOutlined {\n background-color: transparent;\n border-color: var(--border-default, #e5e7eb);\n color: var(--text-primary, #111827);\n}\n\n/* Sizes */\n.sizeSm {\n height: 1.5rem;\n padding: 0 var(--spacing-2, 0.5rem);\n font-size: var(--font-size-xs, 0.75rem);\n gap: var(--spacing-1, 0.25rem);\n}\n\n.sizeMd {\n height: 2rem;\n padding: 0 var(--spacing-3, 0.75rem);\n font-size: var(--font-size-sm, 0.875rem);\n gap: var(--spacing-1, 0.25rem);\n}\n\n.sizeLg {\n height: 2.5rem;\n padding: 0 var(--spacing-4, 1rem);\n font-size: var(--font-size-base, 1rem);\n gap: var(--spacing-2, 0.5rem);\n}\n\n/* Sub-parts */\n.avatar {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n}\n\n.leadingIcon {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n}\n\n.label {\n flex: 1 1 auto;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.deleteButton {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n background: transparent;\n border: none;\n padding: 0;\n cursor: pointer;\n color: var(--text-secondary, #6b7280);\n border-radius: var(--radius-full, 9999px);\n transition: color var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out),\n background-color var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out);\n width: 1rem;\n height: 1rem;\n}\n\n@media (hover: hover) {\n .deleteButton:hover {\n color: var(--text-primary, #111827);\n background-color: color-mix(in srgb, var(--surface-interactive-hover, #f3f4f6) 60%, transparent);\n }\n}\n\n.deleteButton:focus-visible {\n outline: var(--focus-ring-width, 2px) solid var(--border-focus, #6b7280);\n outline-offset: var(--focus-ring-offset, 1px);\n}\n\n.deleteIcon {\n display: block;\n}\n\n/* Count pill (FilterChip only) */\n.count {\n display: inline-flex;\n align-items: center;\n flex-shrink: 0;\n border-radius: var(--radius-full, 9999px);\n padding: 0 var(--spacing-2, 0.5rem);\n font-size: var(--font-size-xs, 0.75rem);\n font-weight: var(--font-weight-medium, 500);\n font-variant-numeric: tabular-nums;\n line-height: 1;\n align-self: baseline;\n}\n\n.countNeutral {\n background-color: var(--surface-muted, #f3f4f6);\n color: var(--text-primary, #111827);\n}\n\n.countPrimary {\n background-color: var(--surface-accent-subtle, #eff6ff);\n color: var(--text-link, #2563eb);\n}\n\n/* Accent selected state overrides count pill (default behaviour) */\n.selected .count {\n background-color: var(--surface-card, #ffffff);\n color: var(--text-link, #2563eb);\n}\n\n/* Solid mint count pill — editorial/neutral selected treatment */\n.countSolid {\n background-color: var(--surface-success-default, #22c55e);\n color: var(--text-inverse, #ffffff);\n}\n\n/* Interactive chips (ChoiceChip, FilterChip) */\n.interactive {\n cursor: pointer;\n border: var(--stroke-width-thin, 1px) solid transparent;\n}\n\nbutton.base,\n.interactive {\n /* Inherit base layout; add button-specific resets */\n text-align: left;\n appearance: none;\n -webkit-appearance: none;\n}\n\n.interactive:disabled {\n cursor: not-allowed;\n opacity: var(--opacity-40, 0.4);\n pointer-events: none;\n}\n\n.interactive:focus-visible {\n outline: var(--focus-ring-width, 2px) solid var(--border-focus, #6b7280);\n outline-offset: var(--focus-ring-offset, 2px);\n}\n\n@media (hover: hover) {\n .interactive:hover:not(:disabled):not([data-selected=\"true\"]) {\n background-color: var(--surface-interactive-hover, #f3f4f6);\n border-color: var(--border-default, #e5e7eb);\n }\n}\n\n.interactive:active:not(:disabled) {\n transform: translateY(1px);\n transition: none;\n}\n\n/* Selected state */\n.selected {\n background-color: var(--surface-accent-subtle, #eff6ff);\n border-color: var(--surface-accent-default, #3b82f6);\n color: var(--text-link, #2563eb);\n}\n\n@media (hover: hover) {\n .interactive.selected:hover:not(:disabled) {\n background-color: color-mix(in srgb, var(--surface-accent-subtle, #eff6ff) 80%, var(--surface-interactive-hover, #f3f4f6) 20%);\n }\n}\n\n/* Editorial / neutral-elevated selected treatment\n * Uses --surface-card bg + neutral border instead of accent tint.\n * Compose with .selected to override the accent colours. */\n.selectedNeutral {\n background-color: var(--surface-card, #ffffff);\n border-color: var(--border-strong, #d1d5db);\n color: var(--text-primary, #111827);\n}\n\n.selectedNeutral .count {\n background-color: var(--surface-success-default, #22c55e);\n color: var(--text-inverse, #ffffff);\n}\n\n@media (hover: hover) {\n .interactive.selectedNeutral:hover:not(:disabled) {\n background-color: var(--surface-interactive-hover, #f3f4f6);\n }\n}\n"
|
|
451
|
+
"content": "/* Chip base */\n.base {\n display: inline-flex;\n align-items: center;\n gap: var(--spacing-1, 0.25rem);\n width: fit-content;\n flex-shrink: 0;\n border-radius: var(--radius-full, 9999px);\n border: var(--stroke-width-thin, 1px) solid var(--border-default, #e5e7eb);\n background-color: var(--surface-card, #ffffff);\n color: var(--text-primary, #111827);\n font-family: var(--font-body, inherit);\n font-weight: var(--font-weight-medium, 500);\n white-space: nowrap;\n user-select: none;\n transition:\n background-color var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out),\n color var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out),\n border-color var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out);\n}\n\n/* Variant: default (filled/tonal surface) */\n.variantDefault {\n background-color: var(--surface-muted, #f3f4f6);\n border-color: transparent;\n color: var(--text-primary, #111827);\n}\n\n/* Variant: outlined */\n.variantOutlined {\n background-color: transparent;\n border-color: var(--border-default, #e5e7eb);\n color: var(--text-primary, #111827);\n}\n\n/* Sizes */\n.sizeSm {\n height: 1.5rem;\n padding: 0 var(--spacing-2, 0.5rem);\n font-size: var(--font-size-xs, 0.75rem);\n gap: var(--spacing-1, 0.25rem);\n}\n\n.sizeMd {\n height: 2rem;\n padding: 0 var(--spacing-3, 0.75rem);\n font-size: var(--font-size-sm, 0.875rem);\n gap: var(--spacing-1, 0.25rem);\n}\n\n/* Editorial density: tighter font + gap for md chips. */\n:global([data-density=\"editorial\"]) .sizeMd {\n font-size: var(--font-size-xs);\n gap: var(--spacing-2);\n}\n\n.sizeLg {\n height: 2.5rem;\n padding: 0 var(--spacing-4, 1rem);\n font-size: var(--font-size-base, 1rem);\n gap: var(--spacing-2, 0.5rem);\n}\n\n/* Sub-parts */\n.avatar {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n}\n\n.leadingIcon {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n}\n\n.label {\n flex: 1 1 auto;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.deleteButton {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n background: transparent;\n border: none;\n padding: 0;\n cursor: pointer;\n color: var(--text-secondary, #6b7280);\n border-radius: var(--radius-full, 9999px);\n transition: color var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out),\n background-color var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out);\n width: 1rem;\n height: 1rem;\n}\n\n@media (hover: hover) {\n .deleteButton:hover {\n color: var(--text-primary, #111827);\n background-color: color-mix(in srgb, var(--surface-interactive-hover, #f3f4f6) 60%, transparent);\n }\n}\n\n.deleteButton:focus-visible {\n outline: var(--focus-ring-width, 2px) solid var(--border-focus, #6b7280);\n outline-offset: var(--focus-ring-offset, 1px);\n}\n\n.deleteIcon {\n display: block;\n}\n\n/* Count pill (FilterChip only). Under data-density=\"editorial\", it renders\n as a fixed circular semibold pill. */\n.count {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n min-width: auto;\n height: auto;\n border-radius: var(--radius-full, 9999px);\n padding: 0 var(--spacing-2, 0.5rem);\n font-size: var(--font-size-xs, 0.75rem);\n font-weight: var(--font-weight-medium, 500);\n font-variant-numeric: tabular-nums;\n line-height: 1;\n align-self: baseline;\n}\n\n:global([data-density=\"editorial\"]) .count {\n min-width: 1.25rem;\n height: 1.25rem;\n padding-inline: var(--spacing-1);\n font-weight: var(--font-weight-semibold);\n align-self: center;\n}\n\n.countNeutral {\n background-color: var(--surface-muted, #f3f4f6);\n color: var(--text-primary, #111827);\n}\n\n.countPrimary {\n background-color: var(--surface-accent-subtle, #eff6ff);\n color: var(--text-link, #2563eb);\n}\n\n/* Accent selected state overrides count pill (default behaviour). */\n.selected .count {\n background-color: var(--surface-card, #ffffff);\n color: var(--text-link, #2563eb);\n}\n\n/* Editorial density: selected count uses solid primary pill. */\n:global([data-density=\"editorial\"]) .selected .count {\n background-color: var(--primary);\n color: var(--primary-text);\n}\n\n/* Solid mint count pill — editorial/neutral selected treatment */\n.countSolid {\n background-color: var(--surface-success-default, #22c55e);\n color: var(--text-inverse, #ffffff);\n}\n\n/* Trailing icon (FilterChip dropdown caret) — muted, after the count. */\n.trailing {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n color: var(--text-tertiary, #9ca3af);\n font-size: 0.875rem;\n line-height: 1;\n}\n\n/* Interactive chips (ChoiceChip, FilterChip) */\n.interactive {\n cursor: pointer;\n border: var(--stroke-width-thin, 1px) solid transparent;\n}\n\nbutton.base,\n.interactive {\n /* Inherit base layout; add button-specific resets */\n text-align: left;\n appearance: none;\n -webkit-appearance: none;\n}\n\n.interactive:disabled {\n cursor: not-allowed;\n opacity: var(--opacity-40, 0.4);\n pointer-events: none;\n}\n\n.interactive:focus-visible {\n outline: var(--focus-ring-width, 2px) solid var(--border-focus, #6b7280);\n outline-offset: var(--focus-ring-offset, 2px);\n}\n\n@media (hover: hover) {\n .interactive:hover:not(:disabled):not([data-selected=\"true\"]) {\n background-color: var(--surface-interactive-hover, #f3f4f6);\n border-color: var(--border-default, #e5e7eb);\n }\n}\n\n.interactive:active:not(:disabled) {\n transform: translateY(1px);\n transition: none;\n}\n\n/* Selected state. Under data-density=\"editorial\", switches to neutral-elevated\n treatment (surface-elev bg + transparent border + primary text). */\n.selected {\n background-color: var(--surface-accent-subtle, #eff6ff);\n border-color: var(--surface-accent-default, #3b82f6);\n color: var(--text-link, #2563eb);\n}\n\n/* Editorial density: neutral-elevated selected treatment.\n --surface-elev is a theme role; the color-mix fallback matches how the\n retired overlay derived it. */\n:global([data-density=\"editorial\"]) .selected {\n background: var(--surface-elev, color-mix(in srgb, var(--surface-card), var(--surface-muted, var(--surface-card))));\n border-color: transparent;\n color: var(--text-primary);\n}\n\n@media (hover: hover) {\n .interactive.selected:hover:not(:disabled) {\n background-color: color-mix(in srgb, var(--surface-accent-subtle, #eff6ff) 80%, var(--surface-interactive-hover, #f3f4f6) 20%);\n }\n}\n\n/* Editorial / neutral-elevated selected treatment\n * Uses --surface-card bg + neutral border instead of accent tint.\n * Compose with .selected to override the accent colours. */\n.selectedNeutral {\n background-color: var(--surface-card, #ffffff);\n border-color: var(--border-strong, #d1d5db);\n color: var(--text-primary, #111827);\n}\n\n/* Re-assert selectedNeutral under editorial density: the density .selected rule\n * is (0,2,0) and would otherwise override .selectedNeutral at (0,1,0). */\n:global([data-density=\"editorial\"]) .selectedNeutral {\n background-color: var(--surface-card, #ffffff);\n border-color: var(--border-strong, #d1d5db);\n color: var(--text-primary, #111827);\n}\n\n.selectedNeutral .count {\n background-color: var(--surface-success-default, #22c55e);\n color: var(--text-inverse, #ffffff);\n}\n\n/* Re-assert selectedNeutral count under editorial density: the density\n * .selected .count rule at (0,3,0) would otherwise override at (0,2,0). */\n:global([data-density=\"editorial\"]) .selectedNeutral .count {\n background-color: var(--surface-success-default, #22c55e);\n color: var(--text-inverse, #ffffff);\n}\n\n@media (hover: hover) {\n .interactive.selectedNeutral:hover:not(:disabled) {\n background-color: var(--surface-interactive-hover, #f3f4f6);\n }\n}\n\n/* FilterChip resting (unselected) surface. Under editorial density, uses\n --surface-elev so the filter row reads as one consistent elevated set. */\n.filterChip[data-selected=\"false\"] {\n background-color: var(--surface-muted, #f3f4f6);\n}\n\n:global([data-density=\"editorial\"]) .filterChip[data-selected=\"false\"] {\n background-color: var(--surface-elev, color-mix(in srgb, var(--surface-card), var(--surface-muted, var(--surface-card))));\n}\n"
|
|
452
452
|
}
|
|
453
453
|
]
|
|
454
454
|
},
|
|
@@ -473,7 +473,7 @@
|
|
|
473
473
|
{
|
|
474
474
|
"path": "components/ui/avatar/avatar.module.css",
|
|
475
475
|
"type": "registry:ui",
|
|
476
|
-
"content": "/* Avatar */\n.avatar {\n position: relative;\n display: flex;\n width: 2rem;\n height: 2rem;\n flex-shrink: 0;\n border-radius: var(--radius-full, 9999px);\n user-select: none;\n overflow: hidden;\n}\n\n.avatarSm {\n width: 1.5rem;\n height: 1.5rem;\n}\n\n.avatarLg {\n width: 2.5rem;\n height: 2.5rem;\n}\n\n/* AvatarImage */\n.avatarImage {\n aspect-ratio: 1 / 1;\n width: 100%;\n height: 100%;\n border-radius: var(--radius-full, 9999px);\n object-fit: cover;\n}\n\n/* AvatarFallback */\n.avatarFallback {\n display: flex;\n width: 100%;\n height: 100%;\n align-items: center;\n justify-content: center;\n border-radius: var(--radius-full, 9999px);\n background-color: var(--surface-muted, #f3f4f6);\n color: var(--text-secondary, #6b7280);\n font-size: var(--font-size-sm, 0.875rem);\n font-weight: var(--font-weight-medium, 500);\n}\n"
|
|
476
|
+
"content": "/* Avatar */\n.avatar {\n position: relative;\n display: flex;\n width: 2rem;\n height: 2rem;\n flex-shrink: 0;\n border-radius: var(--radius-full, 9999px);\n user-select: none;\n overflow: hidden;\n}\n\n.avatarSm {\n width: 1.5rem;\n height: 1.5rem;\n}\n\n.avatarLg {\n width: 2.5rem;\n height: 2.5rem;\n}\n\n/* Editorial density — the admin prototype runs avatars a notch smaller\n (sm 22 / default 28; lg stays 40) with per-size fallback initials. */\n:global([data-density=\"editorial\"]) .avatar {\n width: 28px;\n height: 28px;\n}\n\n:global([data-density=\"editorial\"]) .avatarSm {\n width: 22px;\n height: 22px;\n}\n\n:global([data-density=\"editorial\"]) .avatarLg {\n width: 2.5rem;\n height: 2.5rem;\n}\n\n:global([data-density=\"editorial\"]) .avatarSm .avatarFallback {\n font-size: 11px;\n}\n\n:global([data-density=\"editorial\"]) .avatarLg .avatarFallback {\n font-size: 13px;\n}\n\n/* AvatarImage */\n.avatarImage {\n aspect-ratio: 1 / 1;\n width: 100%;\n height: 100%;\n border-radius: var(--radius-full, 9999px);\n object-fit: cover;\n}\n\n/* AvatarFallback */\n.avatarFallback {\n display: flex;\n width: 100%;\n height: 100%;\n align-items: center;\n justify-content: center;\n border-radius: var(--radius-full, 9999px);\n background-color: var(--surface-muted, #f3f4f6);\n color: var(--text-secondary, #6b7280);\n font-size: var(--font-size-sm, 0.875rem);\n font-weight: var(--font-weight-medium, 500);\n}\n"
|
|
477
477
|
},
|
|
478
478
|
{
|
|
479
479
|
"path": "components/ui/avatar/avatar-stack.module.css",
|
|
@@ -527,7 +527,32 @@
|
|
|
527
527
|
{
|
|
528
528
|
"path": "components/ui/skeleton/skeleton.module.css",
|
|
529
529
|
"type": "registry:ui",
|
|
530
|
-
"content": "/* Skeleton */\n@keyframes shimmer {\n from {\n background-position: -200% 0;\n }\n to {\n background-position: 200% 0;\n }\n}\n\n.skeleton {\n --_shimmer-duration: 1.5s;\n border-radius: var(--radius-md, 0.375rem);\n background: linear-gradient(\n 90deg,\n var(--skeleton-from, #f3f4f6),\n var(--skeleton-to, #e5e7eb),\n var(--skeleton-from, #f3f4f6)\n );\n background-size: 200% 100%;\n animation: shimmer var(--_shimmer-duration) var(--motion-easing-linear, linear) infinite;\n}\n"
|
|
530
|
+
"content": "/* Skeleton */\n@keyframes shimmer {\n from {\n background-position: -200% 0;\n }\n to {\n background-position: 200% 0;\n }\n}\n\n.skeleton {\n --_shimmer-duration: 1.5s;\n border-radius: var(--radius-md, 0.375rem);\n /* Default (light) gradient — theme-tunable, falls back to the muted\n surface ramp (Tailwind Gray) */\n background: linear-gradient(\n 90deg,\n var(--skeleton-from, #f3f4f6),\n var(--skeleton-to, #e5e7eb),\n var(--skeleton-from, #f3f4f6)\n );\n background-size: 200% 100%;\n animation: shimmer var(--_shimmer-duration) var(--motion-easing-linear, linear) infinite;\n}\n\n/* Density axis — editorial: dim, text-tinted gradient for dark admin surfaces */\n:global([data-density=\"editorial\"]) .skeleton {\n background: linear-gradient(\n 90deg,\n color-mix(in srgb, var(--text-primary) 6%, transparent),\n color-mix(in srgb, var(--text-primary) 10%, transparent),\n color-mix(in srgb, var(--text-primary) 6%, transparent)\n );\n}\n\n/* ── Shape variants (VI-516, opt-in) ─────────────────────────────────────\n Additive modifier classes layered on top of `.skeleton` via `className`.\n They set only a default geometry (width / height / radius) so callers can\n still override via `style`/`className`. The fill + animation are inherited\n from `.skeleton`, so a shape never changes the base look — it only changes\n the silhouette. Each dimension is a token-backed custom property so a theme\n can retune the editorial admin's placeholder geometry without forking the\n component. Defaults match the approved org-management edge capture. */\n\n/* Logo plate — ~104×24, radius-sm. The org-name cell's brand-mark placeholder. */\n.shapeLogo {\n width: var(--skeleton-logo-width, 104px);\n height: var(--skeleton-logo-height, 24px);\n border-radius: var(--radius-sm, 0.25rem);\n}\n\n/* Badge pill — ~64×18, radius-full. The plan/status badge placeholder. */\n.shapePill {\n width: var(--skeleton-pill-width, 64px);\n height: var(--skeleton-pill-height, 18px);\n border-radius: var(--radius-full, 999px);\n}\n\n/* Circle — square footprint, fully rounded. Avatar / icon placeholder. */\n.shapeCircle {\n border-radius: var(--radius-full, 999px);\n}\n"
|
|
531
|
+
}
|
|
532
|
+
]
|
|
533
|
+
},
|
|
534
|
+
{
|
|
535
|
+
"name": "spinner",
|
|
536
|
+
"type": "registry:ui",
|
|
537
|
+
"description": "Inline loading spinner — a rotating border ring with a subtle track and tone-colored leading edge. Three sizes (xs/sm/md), two tones (default/primary), accessible label contract, and full reduced-motion support.",
|
|
538
|
+
"category": "data-display",
|
|
539
|
+
"dependencies": [
|
|
540
|
+
"class-variance-authority",
|
|
541
|
+
"@loworbitstudio/visor-core"
|
|
542
|
+
],
|
|
543
|
+
"registryDependencies": [
|
|
544
|
+
"utils"
|
|
545
|
+
],
|
|
546
|
+
"files": [
|
|
547
|
+
{
|
|
548
|
+
"path": "components/ui/spinner/spinner.tsx",
|
|
549
|
+
"type": "registry:ui",
|
|
550
|
+
"content": "import * as React from \"react\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\nimport { cn } from \"../../../lib/utils\"\nimport styles from \"./spinner.module.css\"\n\nconst spinnerVariants = cva(styles.root, {\n variants: {\n size: {\n xs: styles.sizeXs,\n sm: styles.sizeSm,\n md: styles.sizeMd,\n },\n tone: {\n default: styles.toneDefault,\n primary: styles.tonePrimary,\n },\n },\n defaultVariants: {\n size: \"md\",\n tone: \"default\",\n },\n})\n\nexport interface SpinnerProps\n extends Omit<React.HTMLAttributes<HTMLSpanElement>, \"children\">,\n VariantProps<typeof spinnerVariants> {\n /** Visual size of the spinner ring. @default \"md\" */\n size?: \"xs\" | \"sm\" | \"md\"\n /** Color tone for the leading edge. @default \"default\" */\n tone?: \"default\" | \"primary\"\n /** Accessible label. When provided, renders role=\"status\" with visually-hidden text. When omitted, aria-hidden=\"true\". */\n label?: string\n}\n\nconst Spinner = React.forwardRef<HTMLSpanElement, SpinnerProps>(\n ({ className, size = \"md\", tone = \"default\", label, ...props }, ref) => {\n return (\n <span\n ref={ref}\n data-slot=\"spinner\"\n data-size={size}\n data-tone={tone}\n className={cn(spinnerVariants({ size, tone }), className)}\n {...(label\n ? { role: \"status\", \"aria-label\": label }\n : { \"aria-hidden\": \"true\" })}\n {...props}\n >\n {label ? (\n <span className={styles.srOnly}>{label}</span>\n ) : null}\n </span>\n )\n }\n)\nSpinner.displayName = \"Spinner\"\n\nexport { Spinner, spinnerVariants }\n"
|
|
551
|
+
},
|
|
552
|
+
{
|
|
553
|
+
"path": "components/ui/spinner/spinner.module.css",
|
|
554
|
+
"type": "registry:ui",
|
|
555
|
+
"content": "/* Spinner\n *\n * Inline loading spinner — a border-based rotating ring with a subtle track\n * and a tone-colored leading edge. Sizes: xs (12px), sm (16px), md (24px).\n *\n * All colors flow through CSS custom properties so themes can override the\n * track and edge colors without forking the component. Stroke widths are\n * sourced from the --stroke-width-* token set (thin/regular/medium/thick).\n */\n\n@keyframes spin {\n from {\n transform: rotate(0deg);\n }\n to {\n transform: rotate(360deg);\n }\n}\n\n.root {\n --spinner-track-color: var(--border-default, #e5e7eb);\n --spinner-edge-color: var(--text-tertiary, #6b7280);\n\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n border-radius: var(--radius-full, 9999px);\n border-style: solid;\n border-color: var(--spinner-track-color);\n border-top-color: var(--spinner-edge-color);\n animation: spin var(--motion-duration-1500, 1500ms) var(--motion-easing-linear, linear) infinite;\n box-sizing: border-box;\n}\n\n/* Visually-hidden label text — accessible to screen readers, not visible */\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/* Size xs — 12px ring, stroke-width-thin (1px) */\n.root.sizeXs {\n width: 12px;\n height: 12px;\n border-width: var(--stroke-width-thin, 1px);\n}\n\n/* Size sm — 16px ring, stroke-width-regular (1.5px) */\n.root.sizeSm {\n width: 16px;\n height: 16px;\n border-width: var(--stroke-width-regular, 1.5px);\n}\n\n/* Size md — 24px ring, stroke-width-medium (2px) */\n.root.sizeMd {\n width: 24px;\n height: 24px;\n border-width: var(--stroke-width-medium, 2px);\n}\n\n/* Tone: default — tertiary text for leading edge */\n.root.toneDefault {\n --spinner-edge-color: var(--text-tertiary, #6b7280);\n}\n\n/* Tone: primary — primary brand color for leading edge */\n.root.tonePrimary {\n --spinner-edge-color: var(--primary, #111827);\n}\n\n/* Respect reduced motion — stop rotation */\n@media (prefers-reduced-motion: reduce) {\n .root {\n animation-play-state: paused;\n }\n}\n"
|
|
531
556
|
}
|
|
532
557
|
]
|
|
533
558
|
},
|
|
@@ -581,6 +606,31 @@
|
|
|
581
606
|
}
|
|
582
607
|
]
|
|
583
608
|
},
|
|
609
|
+
{
|
|
610
|
+
"name": "challenge-card",
|
|
611
|
+
"type": "registry:ui",
|
|
612
|
+
"description": "Adversarial challenge message card — an AI pushes back on the user's input and the human holds the gate. Distinct from Alert: ChallengeCard is for messages requiring an explicit human decision, not passive notices.",
|
|
613
|
+
"category": "feedback",
|
|
614
|
+
"dependencies": [
|
|
615
|
+
"@phosphor-icons/react",
|
|
616
|
+
"@loworbitstudio/visor-core"
|
|
617
|
+
],
|
|
618
|
+
"registryDependencies": [
|
|
619
|
+
"utils"
|
|
620
|
+
],
|
|
621
|
+
"files": [
|
|
622
|
+
{
|
|
623
|
+
"path": "components/ui/challenge-card/challenge-card.tsx",
|
|
624
|
+
"type": "registry:ui",
|
|
625
|
+
"content": "import * as React from \"react\"\nimport { Flag, Lock, Check } from \"@phosphor-icons/react\"\nimport { cn } from \"../../../lib/utils\"\nimport styles from \"./challenge-card.module.css\"\n\n// ChallengeCard — adversarial challenge message with a human gate affordance.\n//\n// Distinct from Alert: ChallengeCard is for AI-generated push-back that requires\n// an explicit human decision. Alert is for passive informational notices.\n//\n// Built standalone (not on Alert internals) because the warning-toned filled\n// primary action and gate affordance are outside Alert's scope. Alert wraps\n// a <div role=\"alert\"> with variant color slots; ChallengeCard is a compound\n// component with its own action + gate semantics.\n\n// ---------------------------------------------------------------------------\n// Root\n// ---------------------------------------------------------------------------\n\nexport type ChallengeCardProps = React.HTMLAttributes<HTMLDivElement>\n\nconst ChallengeCard = React.forwardRef<HTMLDivElement, ChallengeCardProps>(\n ({ className, ...props }, ref) => {\n return (\n <div\n ref={ref}\n data-slot=\"challenge-card\"\n role=\"alert\"\n className={cn(styles.root, className)}\n {...props}\n />\n )\n }\n)\nChallengeCard.displayName = \"ChallengeCard\"\n\n// ---------------------------------------------------------------------------\n// Header\n// ---------------------------------------------------------------------------\n\nexport interface ChallengeCardHeaderProps extends React.HTMLAttributes<HTMLDivElement> {\n /** Override the default Flag icon. Pass null to suppress the icon. */\n icon?: React.ReactNode | null\n}\n\nconst ChallengeCardHeader = React.forwardRef<HTMLDivElement, ChallengeCardHeaderProps>(\n ({ className, icon, children, ...props }, ref) => {\n const resolvedIcon = icon === undefined ? <Flag weight=\"fill\" /> : icon\n return (\n <div\n ref={ref}\n data-slot=\"challenge-card-header\"\n className={cn(styles.header, className)}\n {...props}\n >\n {resolvedIcon !== null && (\n <span className={styles.headerIcon} aria-hidden=\"true\">\n {resolvedIcon}\n </span>\n )}\n {children}\n </div>\n )\n }\n)\nChallengeCardHeader.displayName = \"ChallengeCardHeader\"\n\n// ---------------------------------------------------------------------------\n// Body\n// ---------------------------------------------------------------------------\n\nexport type ChallengeCardBodyProps = React.HTMLAttributes<HTMLDivElement>\n\nconst ChallengeCardBody = React.forwardRef<HTMLDivElement, ChallengeCardBodyProps>(\n ({ className, ...props }, ref) => {\n return (\n <div\n ref={ref}\n data-slot=\"challenge-card-body\"\n className={cn(styles.body, className)}\n {...props}\n />\n )\n }\n)\nChallengeCardBody.displayName = \"ChallengeCardBody\"\n\n// ---------------------------------------------------------------------------\n// Actions row\n// ---------------------------------------------------------------------------\n\nexport type ChallengeCardActionsProps = React.HTMLAttributes<HTMLDivElement>\n\nconst ChallengeCardActions = React.forwardRef<HTMLDivElement, ChallengeCardActionsProps>(\n ({ className, ...props }, ref) => {\n return (\n <div\n ref={ref}\n data-slot=\"challenge-card-actions\"\n className={cn(styles.actions, className)}\n {...props}\n />\n )\n }\n)\nChallengeCardActions.displayName = \"ChallengeCardActions\"\n\n// ---------------------------------------------------------------------------\n// Action button\n// ---------------------------------------------------------------------------\n\nexport interface ChallengeCardActionProps\n extends React.ButtonHTMLAttributes<HTMLButtonElement> {\n /** \"primary\" = filled warning-toned; \"ghost\" = transparent with border. */\n variant?: \"primary\" | \"ghost\"\n /** Optional Phosphor icon. Primary defaults to a Check icon. */\n icon?: React.ReactNode | null\n}\n\nconst ChallengeCardAction = React.forwardRef<HTMLButtonElement, ChallengeCardActionProps>(\n ({ className, variant = \"primary\", icon, children, ...props }, ref) => {\n const defaultIcon =\n variant === \"primary\" ? <Check weight=\"bold\" /> : undefined\n const resolvedIcon = icon === undefined ? defaultIcon : icon\n\n return (\n <button\n ref={ref}\n data-slot=\"challenge-card-action\"\n data-variant={variant}\n className={cn(\n styles.action,\n variant === \"primary\" ? styles.actionPrimary : styles.actionGhost,\n className\n )}\n {...props}\n >\n {resolvedIcon !== null && resolvedIcon !== undefined && (\n <span className={styles.actionIcon} aria-hidden=\"true\">\n {resolvedIcon}\n </span>\n )}\n {children}\n </button>\n )\n }\n)\nChallengeCardAction.displayName = \"ChallengeCardAction\"\n\n// ---------------------------------------------------------------------------\n// Gate indicator\n// ---------------------------------------------------------------------------\n\nexport type ChallengeCardGateProps = React.HTMLAttributes<HTMLSpanElement>\n\nconst ChallengeCardGate = React.forwardRef<HTMLSpanElement, ChallengeCardGateProps>(\n ({ className, children, ...props }, ref) => {\n return (\n <span\n ref={ref}\n data-slot=\"challenge-card-gate\"\n className={cn(styles.gate, className)}\n {...props}\n >\n <span className={styles.gateIcon} aria-hidden=\"true\">\n <Lock weight=\"fill\" />\n </span>\n {children ?? \"You hold the gate\"}\n </span>\n )\n }\n)\nChallengeCardGate.displayName = \"ChallengeCardGate\"\n\n// ---------------------------------------------------------------------------\n// Exports\n// ---------------------------------------------------------------------------\n\nexport {\n ChallengeCard,\n ChallengeCardHeader,\n ChallengeCardBody,\n ChallengeCardActions,\n ChallengeCardAction,\n ChallengeCardGate,\n}\n"
|
|
626
|
+
},
|
|
627
|
+
{
|
|
628
|
+
"path": "components/ui/challenge-card/challenge-card.module.css",
|
|
629
|
+
"type": "registry:ui",
|
|
630
|
+
"content": "/* ChallengeCard\n *\n * First-class adversarial challenge message — an AI pushes back on the user's\n * input and the HUMAN holds the gate. Distinct from Alert (passive notice):\n * ChallengeCard carries its own action semantics and gate affordance.\n *\n * Visual spec: docs/design/brand-workbench/elicit-core (the adversarial\n * challenge card section, around line 261-273)\n */\n\n/* Root card */\n.root {\n /* Component-scoped hooks — consumers can override any of these on the wrapper. */\n --challenge-card-action-primary-text: var(--color-warning-900, var(--text-warning, #78350f));\n\n width: 100%;\n border-radius: var(--radius-xl, 1rem);\n background-color: var(--surface-warning-subtle, #fffbeb);\n border: var(--stroke-width-thin, 1px) solid var(--border-warning, #d97706);\n overflow: hidden;\n}\n\n/* Header — flag icon + uppercase warning-colored title */\n.header {\n display: flex;\n align-items: center;\n gap: var(--spacing-2, 0.5rem);\n padding: var(--spacing-2-5, 0.625rem) var(--spacing-4, 1rem);\n font-size: 11.5px;\n font-weight: 800;\n letter-spacing: 0.05em;\n text-transform: uppercase;\n color: var(--text-warning, #b45309);\n}\n\n.headerIcon {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 14px;\n height: 14px;\n flex-shrink: 0;\n color: var(--text-warning, #b45309);\n}\n\n/* Body — prose */\n.body {\n padding: 0 var(--spacing-4, 1rem) var(--spacing-3, 0.75rem);\n font-size: 14.5px;\n line-height: 1.55;\n color: var(--text-primary, #111827);\n}\n\n/* Actions row */\n.actions {\n display: flex;\n align-items: center;\n gap: var(--spacing-2, 0.5rem);\n padding: 0 var(--spacing-4, 1rem) var(--spacing-3-5, 0.875rem);\n}\n\n/* Action button base */\n.action {\n height: 32px;\n padding: 0 var(--spacing-3-5, 0.875rem);\n border-radius: var(--radius-lg, 0.5rem);\n font-family: inherit;\n font-size: 12.5px;\n font-weight: 700;\n cursor: pointer;\n display: inline-flex;\n align-items: center;\n gap: var(--spacing-1-5, 0.375rem);\n transition:\n opacity var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out),\n box-shadow var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out);\n outline: none;\n}\n\n.action:focus-visible {\n outline: var(--focus-ring-width, 2px) solid var(--text-warning, #b45309);\n outline-offset: var(--focus-ring-offset, 2px);\n}\n\n.action:disabled {\n opacity: var(--opacity-40, 0.4);\n cursor: not-allowed;\n}\n\n/* Primary — filled warning-toned */\n.actionPrimary {\n background-color: var(--warning, #f59e0b);\n /* Warning amber is a light/warm amber — dark warm-900 text has the highest contrast */\n color: var(--challenge-card-action-primary-text);\n border: none;\n}\n\n.actionPrimary:hover:not(:disabled) {\n opacity: var(--opacity-80, 0.8);\n}\n\n/* Ghost — transparent with border */\n.actionGhost {\n background-color: transparent;\n color: var(--text-secondary, #6b7280);\n border: var(--stroke-width-thin, 1px) solid var(--border-default, #e5e7eb);\n}\n\n.actionGhost:hover:not(:disabled) {\n background-color: color-mix(in srgb, var(--surface-card, #ffffff) 80%, transparent);\n}\n\n/* Action icon */\n.actionIcon {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 13px;\n height: 13px;\n flex-shrink: 0;\n}\n\n/* Gate indicator — pushed right via margin-left: auto */\n.gate {\n margin-left: auto;\n font-size: 11px;\n color: var(--text-tertiary, #9ca3af);\n display: inline-flex;\n align-items: center;\n gap: var(--spacing-1, 0.25rem);\n white-space: nowrap;\n flex-shrink: 0;\n}\n\n.gateIcon {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 12px;\n height: 12px;\n flex-shrink: 0;\n}\n\n/* Respect reduced motion */\n@media (prefers-reduced-motion: reduce) {\n .action {\n transition: none;\n }\n}\n"
|
|
631
|
+
}
|
|
632
|
+
]
|
|
633
|
+
},
|
|
584
634
|
{
|
|
585
635
|
"name": "progress",
|
|
586
636
|
"type": "registry:ui",
|
|
@@ -606,6 +656,31 @@
|
|
|
606
656
|
}
|
|
607
657
|
]
|
|
608
658
|
},
|
|
659
|
+
{
|
|
660
|
+
"name": "segmented-progress",
|
|
661
|
+
"type": "registry:ui",
|
|
662
|
+
"description": "Discrete per-step progress meter — N equal pill segments in a row, each individually expressing done, current, or pending state. Designed for multi-step flows where individual step completion matters.",
|
|
663
|
+
"category": "data-display",
|
|
664
|
+
"dependencies": [
|
|
665
|
+
"class-variance-authority",
|
|
666
|
+
"@loworbitstudio/visor-core"
|
|
667
|
+
],
|
|
668
|
+
"registryDependencies": [
|
|
669
|
+
"utils"
|
|
670
|
+
],
|
|
671
|
+
"files": [
|
|
672
|
+
{
|
|
673
|
+
"path": "components/ui/segmented-progress/segmented-progress.tsx",
|
|
674
|
+
"type": "registry:ui",
|
|
675
|
+
"content": "import * as React from \"react\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\nimport { cn } from \"../../../lib/utils\"\nimport styles from \"./segmented-progress.module.css\"\n\nconst segmentedProgressVariants = cva(styles.root, {\n variants: {\n size: {\n sm: styles.sizeSm,\n md: styles.sizeMd,\n },\n },\n defaultVariants: {\n size: \"sm\",\n },\n})\n\nexport interface SegmentedProgressProps\n extends Omit<React.HTMLAttributes<HTMLDivElement>, \"children\">,\n VariantProps<typeof segmentedProgressVariants> {\n /** Total number of segments. */\n total: number\n /** Count of completed segments (indices 0..value-1 render done). */\n value: number\n /**\n * Optional 0-based index of the in-progress segment.\n * Renders with a primary→muted gradient. Typically equals `value`.\n */\n current?: number\n /** Visual size of each segment. @default \"sm\" */\n size?: \"sm\" | \"md\"\n /** Accessible name for the progress bar. Required. */\n \"aria-label\": string\n}\n\nconst SegmentedProgress = React.forwardRef<\n HTMLDivElement,\n SegmentedProgressProps\n>(\n (\n {\n className,\n total,\n value,\n current,\n size = \"sm\",\n \"aria-label\": ariaLabel,\n ...props\n },\n ref\n ) => {\n const safeTotal = Math.max(total, 1)\n const clampedValue = Math.min(Math.max(value, 0), safeTotal)\n\n return (\n <div\n ref={ref}\n data-slot=\"segmented-progress\"\n data-size={size}\n role=\"progressbar\"\n aria-valuemin={0}\n aria-valuemax={safeTotal}\n aria-valuenow={clampedValue}\n aria-label={ariaLabel}\n className={cn(segmentedProgressVariants({ size }), className)}\n {...props}\n >\n {Array.from({ length: safeTotal }, (_, i) => {\n const state =\n i < clampedValue\n ? \"done\"\n : i === current\n ? \"current\"\n : \"pending\"\n return (\n <span\n key={i}\n data-slot=\"segmented-progress-segment\"\n data-state={state}\n className={cn(\n styles.segment,\n state === \"done\" && styles.segmentDone,\n state === \"current\" && styles.segmentCurrent,\n state === \"pending\" && styles.segmentPending\n )}\n />\n )\n })}\n </div>\n )\n }\n)\nSegmentedProgress.displayName = \"SegmentedProgress\"\n\nexport { SegmentedProgress, segmentedProgressVariants }\n"
|
|
676
|
+
},
|
|
677
|
+
{
|
|
678
|
+
"path": "components/ui/segmented-progress/segmented-progress.module.css",
|
|
679
|
+
"type": "registry:ui",
|
|
680
|
+
"content": "/* SegmentedProgress\n *\n * Discrete per-step progress meter: N equal segments in a row, each\n * individually expressing done, current, or pending state.\n *\n * Segment heights (6px sm / 8px md) are component-intrinsic dimensions —\n * no spacing token maps to these values (same precedent as status-dot's 6px).\n * All other spacing, radius, color, and motion values use tokens.\n */\n\n.root {\n display: flex;\n gap: var(--spacing-1, 0.25rem);\n width: 100%;\n}\n\n/* Segment — base pill shape */\n.segment {\n flex: 1;\n border-radius: var(--radius-full, 9999px);\n transition: background var(--motion-duration-150, 150ms)\n var(--motion-easing-default, ease-in-out);\n}\n\n/* State: done — solid primary fill */\n.segment.segmentDone {\n background: var(--interactive-primary-bg, var(--primary, #111827));\n}\n\n/* State: current — gradient from primary into muted (~55%/45% split) */\n.segment.segmentCurrent {\n background: linear-gradient(\n 90deg,\n var(--interactive-primary-bg, var(--primary, #111827)) 55%,\n var(--surface-muted, #e5e7eb) 55%\n );\n}\n\n/* State: pending — muted surface */\n.segment.segmentPending {\n background: var(--surface-muted, #e5e7eb);\n}\n\n/* Size sm — 6px tall (matches the brand-workbench prototype) */\n.sizeSm .segment {\n height: 6px;\n}\n\n/* Size md — 8px tall */\n.sizeMd .segment {\n height: 8px;\n}\n\n/* Respect reduced motion — disable transitions */\n@media (prefers-reduced-motion: reduce) {\n .segment {\n transition: none;\n }\n}\n"
|
|
681
|
+
}
|
|
682
|
+
]
|
|
683
|
+
},
|
|
609
684
|
{
|
|
610
685
|
"name": "dialog",
|
|
611
686
|
"type": "registry:ui",
|
|
@@ -623,12 +698,12 @@
|
|
|
623
698
|
{
|
|
624
699
|
"path": "components/ui/dialog/dialog.tsx",
|
|
625
700
|
"type": "registry:ui",
|
|
626
|
-
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport * as DialogPrimitive from \"@radix-ui/react-dialog\"\nimport { XIcon } from \"@phosphor-icons/react\"\nimport { cn } from \"../../../lib/utils\"\nimport styles from \"./dialog.module.css\"\n\nfunction Dialog({ ...props }: React.ComponentProps<typeof DialogPrimitive.Root>) {\n return <DialogPrimitive.Root data-slot=\"dialog\" {...props} />\n}\nDialog.displayName = \"Dialog\"\n\nfunction DialogTrigger({ ...props }: React.ComponentProps<typeof DialogPrimitive.Trigger>) {\n return <DialogPrimitive.Trigger data-slot=\"dialog-trigger\" {...props} />\n}\nDialogTrigger.displayName = \"DialogTrigger\"\n\nfunction DialogClose({ ...props }: React.ComponentProps<typeof DialogPrimitive.Close>) {\n return <DialogPrimitive.Close data-slot=\"dialog-close\" {...props} />\n}\nDialogClose.displayName = \"DialogClose\"\n\nfunction DialogPortal({ ...props }: React.ComponentProps<typeof DialogPrimitive.Portal>) {\n return <DialogPrimitive.Portal data-slot=\"dialog-portal\" {...props} />\n}\nDialogPortal.displayName = \"DialogPortal\"\n\nconst DialogOverlay = React.forwardRef<\n React.ComponentRef<typeof DialogPrimitive.Overlay>,\n React.ComponentProps<typeof DialogPrimitive.Overlay>\n>(({ className, ...props }, ref) => (\n <DialogPrimitive.Overlay\n ref={ref}\n data-slot=\"dialog-overlay\"\n className={cn(styles.overlay, className)}\n {...props}\n />\n))\nDialogOverlay.displayName = \"DialogOverlay\"\n\nconst DialogContent = React.forwardRef<\n React.ComponentRef<typeof DialogPrimitive.Content>,\n React.ComponentProps<typeof DialogPrimitive.Content>\n>(({ className, children, ...props }, ref) => (\n <DialogPortal>\n <DialogOverlay />\n <DialogPrimitive.Content\n ref={ref}\n data-slot=\"dialog-content\"\n className={cn(styles.content, className)}\n {...props}\n >\n {children}\n <DialogPrimitive.Close className={styles.closeButton}>\n <XIcon className={styles.closeIcon} />\n <span className={styles.srOnly}>Close</span>\n </DialogPrimitive.Close>\n </DialogPrimitive.Content>\n </DialogPortal>\n))\nDialogContent.displayName = \"DialogContent\"\n\nfunction DialogHeader({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"dialog-header\"\n className={cn(styles.header, className)}\n {...props}\n />\n )\n}\nDialogHeader.displayName = \"DialogHeader\"\n\nconst DialogTitle = React.forwardRef<\n React.ComponentRef<typeof DialogPrimitive.Title>,\n React.ComponentProps<typeof DialogPrimitive.Title>\n>(({ className, ...props }, ref) => (\n <DialogPrimitive.Title\n ref={ref}\n data-slot=\"dialog-title\"\n className={cn(styles.title, className)}\n {...props}\n />\n))\nDialogTitle.displayName = \"DialogTitle\"\n\nconst DialogDescription = React.forwardRef<\n React.ComponentRef<typeof DialogPrimitive.Description>,\n React.ComponentProps<typeof DialogPrimitive.Description>\n>(({ className, ...props }, ref) => (\n <DialogPrimitive.Description\n ref={ref}\n data-slot=\"dialog-description\"\n className={cn(styles.description, className)}\n {...props}\n />\n))\nDialogDescription.displayName = \"DialogDescription\"\n\nexport {\n Dialog,\n DialogTrigger,\n DialogClose,\n DialogPortal,\n DialogOverlay,\n DialogContent,\n DialogHeader,\n DialogTitle,\n DialogDescription,\n}\n"
|
|
701
|
+
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport * as DialogPrimitive from \"@radix-ui/react-dialog\"\nimport { XIcon } from \"@phosphor-icons/react\"\nimport { cn } from \"../../../lib/utils\"\nimport styles from \"./dialog.module.css\"\n\nfunction Dialog({ ...props }: React.ComponentProps<typeof DialogPrimitive.Root>) {\n return <DialogPrimitive.Root data-slot=\"dialog\" {...props} />\n}\nDialog.displayName = \"Dialog\"\n\nfunction DialogTrigger({ ...props }: React.ComponentProps<typeof DialogPrimitive.Trigger>) {\n return <DialogPrimitive.Trigger data-slot=\"dialog-trigger\" {...props} />\n}\nDialogTrigger.displayName = \"DialogTrigger\"\n\nfunction DialogClose({ ...props }: React.ComponentProps<typeof DialogPrimitive.Close>) {\n return <DialogPrimitive.Close data-slot=\"dialog-close\" {...props} />\n}\nDialogClose.displayName = \"DialogClose\"\n\nfunction DialogPortal({ ...props }: React.ComponentProps<typeof DialogPrimitive.Portal>) {\n return <DialogPrimitive.Portal data-slot=\"dialog-portal\" {...props} />\n}\nDialogPortal.displayName = \"DialogPortal\"\n\nconst DialogOverlay = React.forwardRef<\n React.ComponentRef<typeof DialogPrimitive.Overlay>,\n React.ComponentProps<typeof DialogPrimitive.Overlay>\n>(({ className, ...props }, ref) => (\n <DialogPrimitive.Overlay\n ref={ref}\n data-slot=\"dialog-overlay\"\n className={cn(styles.overlay, className)}\n {...props}\n />\n))\nDialogOverlay.displayName = \"DialogOverlay\"\n\nconst DialogContent = React.forwardRef<\n React.ComponentRef<typeof DialogPrimitive.Content>,\n React.ComponentProps<typeof DialogPrimitive.Content>\n>(({ className, children, ...props }, ref) => (\n <DialogPortal>\n <DialogOverlay />\n <DialogPrimitive.Content\n ref={ref}\n data-slot=\"dialog-content\"\n className={cn(styles.content, className)}\n {...props}\n >\n {children}\n <DialogPrimitive.Close className={styles.closeButton}>\n <XIcon className={styles.closeIcon} />\n <span className={styles.srOnly}>Close</span>\n </DialogPrimitive.Close>\n </DialogPrimitive.Content>\n </DialogPortal>\n))\nDialogContent.displayName = \"DialogContent\"\n\nfunction DialogHeader({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"dialog-header\"\n className={cn(styles.header, className)}\n {...props}\n />\n )\n}\nDialogHeader.displayName = \"DialogHeader\"\n\nfunction DialogFooter({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"dialog-footer\"\n className={cn(styles.footer, className)}\n {...props}\n />\n )\n}\nDialogFooter.displayName = \"DialogFooter\"\n\nconst DialogTitle = React.forwardRef<\n React.ComponentRef<typeof DialogPrimitive.Title>,\n React.ComponentProps<typeof DialogPrimitive.Title>\n>(({ className, ...props }, ref) => (\n <DialogPrimitive.Title\n ref={ref}\n data-slot=\"dialog-title\"\n className={cn(styles.title, className)}\n {...props}\n />\n))\nDialogTitle.displayName = \"DialogTitle\"\n\nconst DialogDescription = React.forwardRef<\n React.ComponentRef<typeof DialogPrimitive.Description>,\n React.ComponentProps<typeof DialogPrimitive.Description>\n>(({ className, ...props }, ref) => (\n <DialogPrimitive.Description\n ref={ref}\n data-slot=\"dialog-description\"\n className={cn(styles.description, className)}\n {...props}\n />\n))\nDialogDescription.displayName = \"DialogDescription\"\n\nexport {\n Dialog,\n DialogTrigger,\n DialogClose,\n DialogPortal,\n DialogOverlay,\n DialogContent,\n DialogHeader,\n DialogFooter,\n DialogTitle,\n DialogDescription,\n}\n"
|
|
627
702
|
},
|
|
628
703
|
{
|
|
629
704
|
"path": "components/ui/dialog/dialog.module.css",
|
|
630
705
|
"type": "registry:ui",
|
|
631
|
-
"content": "/* Dialog overlay */\n.overlay {\n position: fixed;\n inset: 0;\n z-index: 50;\n background-color: var(--overlay-bg, rgba(0, 0, 0, 0.8));\n backdrop-filter: blur(4px);\n -webkit-backdrop-filter: blur(4px);\n}\n\n.overlay[data-state=\"open\"] {\n animation: fadeIn var(--motion-duration-150, 150ms) var(--motion-easing-enter, ease-out);\n}\n\n.overlay[data-state=\"closed\"] {\n animation: fadeOut var(--motion-duration-150, 150ms) var(--motion-easing-exit, ease-in);\n}\n\n/* Dialog content */\n.content {\n position: fixed;\n top: 50%;\n left: 50%;\n z-index: 50;\n width: 100%;\n max-width: 32rem;\n transform: translate(-50%, -50%);\n border-radius: var(--radius-lg, 0.5rem);\n background-color: var(--surface-page, #ffffff);\n padding: var(--spacing-6, 1.5rem);\n box-shadow: var(--shadow-xl);\n transition: opacity var(--motion-duration-normal, 200ms) var(--motion-easing-default, ease-in-out), transform var(--motion-duration-normal, 200ms) var(--motion-easing-default, ease-in-out);\n}\n\n.content[data-state=\"open\"] {\n animation: contentShow var(--motion-duration-normal, 200ms) var(--motion-easing-enter, ease-out);\n}\n\n.content[data-state=\"closed\"] {\n animation: contentHide var(--motion-duration-150, 150ms) var(--motion-easing-exit, ease-in);\n}\n\n/* Close button */\n.closeButton {\n position: absolute;\n top: 1rem;\n right: 1rem;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n border-radius: var(--radius-sm, 0.25rem);\n border: none;\n background: transparent;\n cursor: pointer;\n opacity: 0.7;\n transition: opacity var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out);\n padding: var(--spacing-1, 0.25rem);\n color: var(--text-primary, #111827);\n}\n\n.closeButton:hover {\n opacity: 1;\n}\n\n.closeButton: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.closeIcon {\n width: 1rem;\n height: 1rem;\n}\n\n/* Header */\n.header {\n display: flex;\n flex-direction: column;\n gap: calc(var(--spacing-1, 0.25rem) * 1.5);\n margin-bottom: var(--spacing-4, 1rem);\n}\n\n/* Title */\n.title {\n font-size: var(--font-size-lg, 1.125rem);\n font-weight: var(--font-weight-semibold, 600);\n color: var(--text-primary, #111827);\n margin: 0;\n}\n\n/* Description */\n.description {\n font-size: var(--font-size-sm, 0.875rem);\n color: var(--text-secondary, #6b7280);\n margin: 0;\n}\n\n/* Screen reader only */\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@keyframes fadeIn {\n from { opacity: 0; }\n to { opacity: 1; }\n}\n\n@keyframes fadeOut {\n from { opacity: 1; }\n to { opacity: 0; }\n}\n\n@keyframes contentShow {\n from {\n opacity: 0;\n transform: translate(-50%, calc(-50% + 8px));\n }\n to {\n opacity: 1;\n transform: translate(-50%, -50%);\n }\n}\n\n@keyframes contentHide {\n from {\n opacity: 1;\n transform: translate(-50%, -50%);\n }\n to {\n opacity: 0;\n transform: translate(-50%, calc(-50% + 8px));\n }\n}\n"
|
|
706
|
+
"content": "/* Dialog overlay\n * Default: 4px backdrop blur. Density axis: editorial uses 2px. */\n.overlay {\n position: fixed;\n inset: 0;\n z-index: 50;\n background-color: var(--overlay-bg, rgba(0, 0, 0, 0.8));\n backdrop-filter: blur(4px);\n -webkit-backdrop-filter: blur(4px);\n}\n\n:global([data-density=\"editorial\"]) .overlay {\n backdrop-filter: blur(2px);\n -webkit-backdrop-filter: blur(2px);\n}\n\n.overlay[data-state=\"open\"] {\n animation: fadeIn var(--motion-duration-150, 150ms) var(--motion-easing-enter, ease-out);\n}\n\n.overlay[data-state=\"closed\"] {\n animation: fadeOut var(--motion-duration-150, 150ms) var(--motion-easing-exit, ease-in);\n}\n\n/* Dialog content\n * Default: 32rem max-width, radius-lg, surface-page, spacing-6, shadow-xl.\n * Density axis: editorial uses 480px, radius-xl, surface-card, spacing-8, shadow-lg. */\n.content {\n position: fixed;\n top: 50%;\n left: 50%;\n z-index: 50;\n width: 100%;\n max-width: 32rem;\n transform: translate(-50%, -50%);\n border-radius: var(--radius-lg, 0.5rem);\n background-color: var(--surface-page, #ffffff);\n padding: var(--spacing-6, 1.5rem);\n box-shadow: var(--shadow-xl);\n transition: opacity var(--motion-duration-normal, 200ms) var(--motion-easing-default, ease-in-out), transform var(--motion-duration-normal, 200ms) var(--motion-easing-default, ease-in-out);\n}\n\n:global([data-density=\"editorial\"]) .content {\n background-color: var(--surface-card);\n max-width: 480px;\n border-radius: var(--radius-xl);\n padding: var(--spacing-8);\n box-shadow: var(--shadow-lg);\n}\n\n.content[data-state=\"open\"] {\n animation: contentShow var(--motion-duration-normal, 200ms) var(--motion-easing-enter, ease-out);\n}\n\n.content[data-state=\"closed\"] {\n animation: contentHide var(--motion-duration-150, 150ms) var(--motion-easing-exit, ease-in);\n}\n\n/* Close button */\n.closeButton {\n position: absolute;\n top: 1rem;\n right: 1rem;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n border-radius: var(--radius-sm, 0.25rem);\n border: none;\n background: transparent;\n cursor: pointer;\n opacity: 0.7;\n transition: opacity var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out);\n padding: var(--spacing-1, 0.25rem);\n color: var(--text-primary, #111827);\n}\n\n.closeButton:hover {\n opacity: 1;\n}\n\n.closeButton: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.closeIcon {\n width: 1rem;\n height: 1rem;\n}\n\n/* Header */\n.header {\n display: flex;\n flex-direction: column;\n gap: calc(var(--spacing-1, 0.25rem) * 1.5);\n margin-bottom: var(--spacing-4, 1rem);\n}\n\n/* Title\n * Default: font-size-lg / semibold. Density axis: editorial uses font-size-2xl / 700. */\n.title {\n font-size: var(--font-size-lg, 1.125rem);\n font-weight: var(--font-weight-semibold, 600);\n color: var(--text-primary, #111827);\n margin: 0;\n}\n\n:global([data-density=\"editorial\"]) .title {\n font-size: var(--font-size-2xl);\n font-weight: var(--font-weight-bold, 700);\n}\n\n/* Description\n * Default: text-secondary. Density axis: editorial uses text-tertiary. */\n.description {\n font-size: var(--font-size-sm, 0.875rem);\n color: var(--text-secondary, #6b7280);\n margin: 0;\n}\n\n:global([data-density=\"editorial\"]) .description {\n color: var(--text-tertiary);\n}\n\n/* Footer */\n.footer {\n display: flex;\n justify-content: flex-end;\n align-items: center;\n gap: var(--spacing-3, 0.75rem);\n padding-top: var(--spacing-3, 0.75rem);\n margin-top: var(--spacing-2, 0.5rem);\n border-top: var(--hairline-width, 1px) solid var(--hairline, var(--border-subtle, rgba(0, 0, 0, 0.06)));\n}\n\n/* Screen reader only */\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@keyframes fadeIn {\n from { opacity: 0; }\n to { opacity: 1; }\n}\n\n@keyframes fadeOut {\n from { opacity: 1; }\n to { opacity: 0; }\n}\n\n@keyframes contentShow {\n from {\n opacity: 0;\n transform: translate(-50%, calc(-50% + 8px));\n }\n to {\n opacity: 1;\n transform: translate(-50%, -50%);\n }\n}\n\n@keyframes contentHide {\n from {\n opacity: 1;\n transform: translate(-50%, -50%);\n }\n to {\n opacity: 0;\n transform: translate(-50%, calc(-50% + 8px));\n }\n}\n"
|
|
632
707
|
}
|
|
633
708
|
]
|
|
634
709
|
},
|
|
@@ -681,7 +756,7 @@
|
|
|
681
756
|
{
|
|
682
757
|
"path": "components/ui/dropdown-menu/dropdown-menu.module.css",
|
|
683
758
|
"type": "registry:ui",
|
|
684
|
-
"content": "/* Dropdown menu content */\n.content {\n z-index: 50;\n min-width: 12rem;\n overflow: hidden;\n border-radius: var(--radius-lg, 0.5rem);\n background-color: var(--surface-popover, var(--surface-card, #ffffff));\n color: var(--text-primary, #111827);\n padding: var(--spacing-1, 0.25rem);\n box-shadow: var(--shadow-lg);\n}\n\n.content[data-state=\"open\"] {\n animation: contentShow var(--motion-duration-fast, 100ms) var(--motion-easing-enter, ease-out);\n}\n\n.content[data-state=\"closed\"] {\n animation: contentHide var(--motion-duration-fast, 100ms) var(--motion-easing-exit, ease-in);\n}\n\n/* Breakout variant — use inside scroll-clipped containers (ex: DataTable rows) */\n.contentBreakout {\n z-index: 200;\n box-shadow: var(--shadow-xl, 0 28px 64px rgba(0, 0, 0, 0.55), 0 8px 16px rgba(0, 0, 0, 0.30), 0 0 0 1px rgba(255, 255, 255, 0.06));\n}\n\n/* Sub content */\n.subContent {\n z-index: 50;\n min-width: 9rem;\n overflow: hidden;\n border-radius: var(--radius-lg, 0.5rem);\n background-color: var(--surface-popover, var(--surface-card, #ffffff));\n color: var(--text-primary, #111827);\n padding: var(--spacing-1, 0.25rem);\n box-shadow: var(--shadow-lg);\n}\n\n.subContent[data-state=\"open\"] {\n animation: contentShow var(--motion-duration-fast, 100ms) var(--motion-easing-enter, ease-out);\n}\n\n.subContent[data-state=\"closed\"] {\n animation: contentHide var(--motion-duration-fast, 100ms) var(--motion-easing-exit, ease-in);\n}\n\n/* Menu item */\n.item {\n position: relative;\n display: flex;\n cursor: default;\n user-select: none;\n align-items: center;\n gap: calc(var(--spacing-2, 0.5rem) + var(--spacing-1, 0.25rem) / 2);\n border-radius: var(--radius-md, 0.375rem);\n padding: var(--spacing-2, 0.5rem) var(--spacing-3, 0.75rem);\n font-size: var(--font-size-sm, 0.875rem);\n outline: none;\n transition: background-color var(--motion-duration-fast, 100ms) var(--motion-easing-default, ease-in-out), color var(--motion-duration-fast, 100ms) var(--motion-easing-default, ease-in-out);\n}\n\n.item:focus,\n.item[data-highlighted] {\n background-color: var(--surface-interactive-hover, #f3f4f6);\n color: var(--text-primary, #111827);\n}\n\n.item[data-disabled] {\n pointer-events: none;\n opacity: 0.5;\n}\n\n.itemDestructive {\n color: var(--text-error, #ef4444);\n}\n\n.itemDestructive:focus,\n.itemDestructive[data-highlighted] {\n background-color: var(--surface-error-subtle, rgba(239, 68, 68, 0.1));\n color: var(--text-error, #ef4444);\n}\n\n.itemInset {\n padding-left: calc(var(--spacing-8, 2rem) + var(--spacing-1, 0.25rem) * 1.5);\n}\n\n/* Checkbox and radio items */\n.checkboxItem,\n.radioItem {\n position: relative;\n display: flex;\n cursor: default;\n user-select: none;\n align-items: center;\n gap: calc(var(--spacing-2, 0.5rem) + var(--spacing-1, 0.25rem) / 2);\n border-radius: var(--radius-md, 0.375rem);\n padding: var(--spacing-2, 0.5rem) var(--spacing-3, 0.75rem);\n padding-right: var(--spacing-8, 2rem);\n font-size: var(--font-size-sm, 0.875rem);\n outline: none;\n transition: background-color var(--motion-duration-fast, 100ms) var(--motion-easing-default, ease-in-out), color var(--motion-duration-fast, 100ms) var(--motion-easing-default, ease-in-out);\n}\n\n.checkboxItem:focus,\n.checkboxItem[data-highlighted],\n.radioItem:focus,\n.radioItem[data-highlighted] {\n background-color: var(--surface-interactive-hover, #f3f4f6);\n color: var(--text-primary, #111827);\n}\n\n.checkboxItem[data-disabled],\n.radioItem[data-disabled] {\n pointer-events: none;\n opacity: 0.5;\n}\n\n/* Item indicator */\n.itemIndicator {\n pointer-events: none;\n position: absolute;\n right: 0.5rem;\n display: flex;\n align-items: center;\n justify-content: center;\n width: 1rem;\n height: 1rem;\n}\n\n/* Label */\n.label {\n padding: calc(var(--spacing-2, 0.5rem) + var(--spacing-1, 0.25rem) / 2) var(--spacing-3, 0.75rem);\n font-size: var(--font-size-xs, 0.75rem);\n color: var(--text-secondary, #6b7280);\n}\n\n/* Separator */\n.separator {\n height: 1px;\n margin: var(--spacing-1, 0.25rem) calc(-1 * var(--spacing-1, 0.25rem));\n background-color: var(--border-default, #e5e7eb);\n}\n\n/* Shortcut */\n.shortcut {\n margin-left: auto;\n font-size: var(--font-size-xs, 0.75rem);\n letter-spacing: 0.1em;\n color: var(--text-secondary, #6b7280);\n}\n\n/* Sub trigger */\n.subTrigger {\n display: flex;\n cursor: default;\n user-select: none;\n align-items: center;\n gap: var(--spacing-2, 0.5rem);\n border-radius: var(--radius-md, 0.375rem);\n padding: var(--spacing-2, 0.5rem) var(--spacing-3, 0.75rem);\n font-size: var(--font-size-sm, 0.875rem);\n outline: none;\n transition: background-color var(--motion-duration-fast, 100ms) var(--motion-easing-default, ease-in-out), color var(--motion-duration-fast, 100ms) var(--motion-easing-default, ease-in-out);\n}\n\n.subTrigger:focus,\n.subTrigger[data-state=\"open\"] {\n background-color: var(--surface-interactive-hover, #f3f4f6);\n color: var(--text-primary, #111827);\n}\n\n.subTriggerIcon {\n margin-left: auto;\n width: 1rem;\n height: 1rem;\n}\n\n@keyframes contentShow {\n from {\n opacity: 0;\n transform: scale(0.95);\n }\n to {\n opacity: 1;\n transform: scale(1);\n }\n}\n\n@keyframes contentHide {\n from {\n opacity: 1;\n transform: scale(1);\n }\n to {\n opacity: 0;\n transform: scale(0.95);\n }\n}\n"
|
|
759
|
+
"content": "/* Dropdown menu content\n *\n * Density axis: editorial admin values are baked in as a first-class density\n * layer, switched by data-density=\"editorial\" on any ancestor. Default rendering\n * is byte-identical to the pre-refactor canonical fallbacks. Theme-agnostic:\n * editorial values resolve from palette tokens (--surface-elev, --hairline,\n * --text-tertiary, --text-muted, --destructive) — correct on entr AND animal. */\n.content {\n z-index: 50;\n min-width: 12rem;\n overflow: hidden;\n display: flex;\n flex-direction: column;\n gap: 0;\n border-radius: var(--radius-lg, 0.5rem);\n background-color: var(--surface-popover, var(--surface-card, #ffffff));\n color: var(--text-primary, #111827);\n padding: var(--spacing-1, 0.25rem);\n box-shadow:\n 0 0 #0000,\n var(--shadow-lg);\n}\n\n:global([data-density=\"editorial\"]) .content {\n gap: var(--stroke-width-thin, 1px);\n border-radius: var(--radius-md);\n background: var(--surface-elev, color-mix(in srgb, var(--surface-card), var(--surface-muted, var(--surface-card))));\n padding: calc(var(--spacing-1, 0.25rem) * 1.5);\n box-shadow:\n inset 0 0 0 1px var(--hairline, var(--border-subtle, transparent)),\n var(--shadow-lg);\n}\n\n.content[data-state=\"open\"] {\n animation: contentShow var(--motion-duration-fast, 100ms) var(--motion-easing-enter, ease-out);\n}\n\n.content[data-state=\"closed\"] {\n animation: contentHide var(--motion-duration-fast, 100ms) var(--motion-easing-exit, ease-in);\n}\n\n/* Breakout variant — use inside scroll-clipped containers (ex: DataTable rows) */\n.contentBreakout {\n z-index: 200;\n box-shadow: var(--shadow-xl, 0 28px 64px rgba(0, 0, 0, 0.55), 0 8px 16px rgba(0, 0, 0, 0.30), 0 0 0 1px rgba(255, 255, 255, 0.06));\n}\n\n/* Sub content */\n.subContent {\n z-index: 50;\n min-width: 9rem;\n overflow: hidden;\n display: flex;\n flex-direction: column;\n gap: 0;\n border-radius: var(--radius-lg, 0.5rem);\n background-color: var(--surface-popover, var(--surface-card, #ffffff));\n color: var(--text-primary, #111827);\n padding: var(--spacing-1, 0.25rem);\n box-shadow:\n 0 0 #0000,\n var(--shadow-lg);\n}\n\n:global([data-density=\"editorial\"]) .subContent {\n gap: var(--stroke-width-thin, 1px);\n border-radius: var(--radius-md);\n background: var(--surface-elev, color-mix(in srgb, var(--surface-card), var(--surface-muted, var(--surface-card))));\n padding: calc(var(--spacing-1, 0.25rem) * 1.5);\n box-shadow:\n inset 0 0 0 1px var(--hairline, var(--border-subtle, transparent)),\n var(--shadow-lg);\n}\n\n.subContent[data-state=\"open\"] {\n animation: contentShow var(--motion-duration-fast, 100ms) var(--motion-easing-enter, ease-out);\n}\n\n.subContent[data-state=\"closed\"] {\n animation: contentHide var(--motion-duration-fast, 100ms) var(--motion-easing-exit, ease-in);\n}\n\n/* Menu item\n * Default: canonical spacing, radius-md, font-size-sm, interactive-hover bg.\n * Density axis: editorial uses 13px, 8px 10px padding, radius-sm, surface-subtle hover. */\n.item {\n position: relative;\n display: flex;\n cursor: default;\n user-select: none;\n align-items: center;\n gap: calc(var(--spacing-2, 0.5rem) + var(--spacing-1, 0.25rem) / 2);\n border-radius: var(--radius-md, 0.375rem);\n padding: var(--spacing-2, 0.5rem) var(--spacing-3, 0.75rem);\n font-size: var(--font-size-sm, 0.875rem);\n outline: none;\n transition: background-color var(--motion-duration-fast, 100ms) var(--motion-easing-default, ease-in-out), color var(--motion-duration-fast, 100ms) var(--motion-easing-default, ease-in-out);\n}\n\n:global([data-density=\"editorial\"]) .item {\n font-size: 13px;\n padding: var(--spacing-2, 0.5rem) calc(var(--spacing-2, 0.5rem) + var(--spacing-1, 0.25rem) / 2);\n gap: var(--spacing-3);\n border-radius: var(--radius-sm);\n}\n\n/* Leading icon — default: inherit item color and 1em size.\n * Density axis: editorial uses text-tertiary color and 16px size. */\n.item > svg {\n flex-shrink: 0;\n color: currentColor;\n width: 1em;\n height: 1em;\n}\n\n:global([data-density=\"editorial\"]) .item > svg {\n color: var(--text-tertiary);\n width: 16px;\n height: 16px;\n}\n\n.item:focus,\n.item[data-highlighted] {\n background-color: var(--surface-interactive-hover, #f3f4f6);\n color: var(--text-primary, #111827);\n}\n\n:global([data-density=\"editorial\"]) .item:focus,\n:global([data-density=\"editorial\"]) .item[data-highlighted] {\n background-color: var(--surface-subtle);\n}\n\n.item[data-disabled] {\n pointer-events: none;\n opacity: 0.5;\n}\n\n.itemDestructive {\n color: var(--text-error, var(--destructive, #ef4444));\n}\n\n/* Destructive icons track the destructive color, overriding the tertiary tint. */\n.itemDestructive > svg {\n color: var(--text-error, var(--destructive, #ef4444));\n}\n\n.itemDestructive:focus,\n.itemDestructive[data-highlighted] {\n background-color: var(--surface-error-subtle, rgba(239, 68, 68, 0.1));\n color: var(--text-error, var(--destructive, #ef4444));\n}\n\n:global([data-density=\"editorial\"]) .itemDestructive:focus,\n:global([data-density=\"editorial\"]) .itemDestructive[data-highlighted] {\n background-color: color-mix(in srgb, var(--destructive) 14%, transparent);\n}\n\n.itemInset {\n padding-left: calc(var(--spacing-8, 2rem) + var(--spacing-1, 0.25rem) * 1.5);\n}\n\n/* Checkbox and radio items */\n.checkboxItem,\n.radioItem {\n position: relative;\n display: flex;\n cursor: default;\n user-select: none;\n align-items: center;\n gap: calc(var(--spacing-2, 0.5rem) + var(--spacing-1, 0.25rem) / 2);\n border-radius: var(--radius-md, 0.375rem);\n padding: var(--spacing-2, 0.5rem) var(--spacing-3, 0.75rem);\n padding-right: var(--spacing-8, 2rem);\n font-size: var(--font-size-sm, 0.875rem);\n outline: none;\n transition: background-color var(--motion-duration-fast, 100ms) var(--motion-easing-default, ease-in-out), color var(--motion-duration-fast, 100ms) var(--motion-easing-default, ease-in-out);\n}\n\n:global([data-density=\"editorial\"]) .checkboxItem,\n:global([data-density=\"editorial\"]) .radioItem {\n font-size: 13px;\n padding: var(--spacing-2, 0.5rem) calc(var(--spacing-2, 0.5rem) + var(--spacing-1, 0.25rem) / 2);\n padding-right: var(--spacing-8, 2rem);\n gap: var(--spacing-3);\n border-radius: var(--radius-sm);\n}\n\n.checkboxItem:focus,\n.checkboxItem[data-highlighted],\n.radioItem:focus,\n.radioItem[data-highlighted] {\n background-color: var(--surface-interactive-hover, #f3f4f6);\n color: var(--text-primary, #111827);\n}\n\n:global([data-density=\"editorial\"]) .checkboxItem:focus,\n:global([data-density=\"editorial\"]) .checkboxItem[data-highlighted],\n:global([data-density=\"editorial\"]) .radioItem:focus,\n:global([data-density=\"editorial\"]) .radioItem[data-highlighted] {\n background-color: var(--surface-subtle);\n}\n\n.checkboxItem[data-disabled],\n.radioItem[data-disabled] {\n pointer-events: none;\n opacity: 0.5;\n}\n\n/* Item indicator */\n.itemIndicator {\n pointer-events: none;\n position: absolute;\n right: 0.5rem;\n display: flex;\n align-items: center;\n justify-content: center;\n width: 1rem;\n height: 1rem;\n}\n\n/* Label\n * Default: canonical 12px / text-secondary / sentence-case.\n * Density axis: editorial uses 11px / uppercase / 0.08em tracking / text-tertiary / 6px 10px 2px padding. */\n.label {\n padding: calc(var(--spacing-2, 0.5rem) + var(--spacing-1, 0.25rem) / 2) var(--spacing-3, 0.75rem);\n font-size: var(--font-size-xs, 0.75rem);\n font-weight: inherit;\n letter-spacing: normal;\n text-transform: none;\n color: var(--text-secondary, #6b7280);\n}\n\n:global([data-density=\"editorial\"]) .label {\n font-size: 11px;\n text-transform: uppercase;\n letter-spacing: 0.08em;\n color: var(--text-tertiary);\n padding: calc(var(--spacing-1, 0.25rem) * 1.5) calc(var(--spacing-2, 0.5rem) + var(--spacing-1, 0.25rem) / 2) calc(var(--spacing-1, 0.25rem) / 2);\n}\n\n/* Separator\n * Default: spacing-1 margin, border-default color.\n * Density axis: editorial uses 4px 2px margin, hairline color. */\n.separator {\n height: 1px;\n margin: var(--spacing-1, 0.25rem) calc(-1 * var(--spacing-1, 0.25rem));\n background-color: var(--border-default, #e5e7eb);\n}\n\n:global([data-density=\"editorial\"]) .separator {\n background-color: var(--hairline, var(--border-subtle, transparent));\n margin: var(--spacing-1, 0.25rem) calc(var(--spacing-1, 0.25rem) / 2);\n}\n\n/* Shortcut\n * Default: 12px / 0.1em tracking / text-secondary.\n * Density axis: editorial uses 11px / 0.04em tracking / text-muted. */\n.shortcut {\n margin-left: auto;\n font-size: var(--font-size-xs, 0.75rem);\n letter-spacing: 0.1em;\n color: var(--text-secondary, #6b7280);\n}\n\n:global([data-density=\"editorial\"]) .shortcut {\n font-size: 11px;\n color: var(--text-muted);\n letter-spacing: 0.04em;\n}\n\n/* Sub trigger */\n.subTrigger {\n display: flex;\n cursor: default;\n user-select: none;\n align-items: center;\n gap: var(--spacing-2, 0.5rem);\n border-radius: var(--radius-md, 0.375rem);\n padding: var(--spacing-2, 0.5rem) var(--spacing-3, 0.75rem);\n font-size: var(--font-size-sm, 0.875rem);\n outline: none;\n transition: background-color var(--motion-duration-fast, 100ms) var(--motion-easing-default, ease-in-out), color var(--motion-duration-fast, 100ms) var(--motion-easing-default, ease-in-out);\n}\n\n:global([data-density=\"editorial\"]) .subTrigger {\n font-size: 13px;\n padding: var(--spacing-2, 0.5rem) calc(var(--spacing-2, 0.5rem) + var(--spacing-1, 0.25rem) / 2);\n gap: var(--spacing-3);\n border-radius: var(--radius-sm);\n}\n\n.subTrigger > svg:not(.subTriggerIcon) {\n flex-shrink: 0;\n color: currentColor;\n width: 1em;\n height: 1em;\n}\n\n:global([data-density=\"editorial\"]) .subTrigger > svg:not(.subTriggerIcon) {\n color: var(--text-tertiary);\n width: 16px;\n height: 16px;\n}\n\n.subTrigger:focus,\n.subTrigger[data-state=\"open\"] {\n background-color: var(--surface-interactive-hover, #f3f4f6);\n color: var(--text-primary, #111827);\n}\n\n:global([data-density=\"editorial\"]) .subTrigger:focus,\n:global([data-density=\"editorial\"]) .subTrigger[data-state=\"open\"] {\n background-color: var(--surface-subtle);\n}\n\n.subTriggerIcon {\n margin-left: auto;\n width: 1rem;\n height: 1rem;\n color: currentColor;\n}\n\n@keyframes contentShow {\n from {\n opacity: 0;\n transform: scale(0.95);\n }\n to {\n opacity: 1;\n transform: scale(1);\n }\n}\n\n@keyframes contentHide {\n from {\n opacity: 1;\n transform: scale(1);\n }\n to {\n opacity: 0;\n transform: scale(0.95);\n }\n}\n"
|
|
685
760
|
}
|
|
686
761
|
]
|
|
687
762
|
},
|
|
@@ -702,12 +777,12 @@
|
|
|
702
777
|
{
|
|
703
778
|
"path": "components/ui/tabs/tabs.tsx",
|
|
704
779
|
"type": "registry:ui",
|
|
705
|
-
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport * as TabsPrimitive from \"@radix-ui/react-tabs\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\nimport { cn } from \"../../../lib/utils\"\nimport styles from \"./tabs.module.css\"\n\nconst Tabs = React.forwardRef<\n React.ComponentRef<typeof TabsPrimitive.Root>,\n React.ComponentProps<typeof TabsPrimitive.Root>\n>(({ className, ...props }, ref) => (\n <TabsPrimitive.Root\n ref={ref}\n data-slot=\"tabs\"\n className={cn(styles.root, className)}\n {...props}\n />\n))\nTabs.displayName = \"Tabs\"\n\nconst tabsListVariants = cva(styles.list, {\n variants: {\n variant: {\n default: styles.listVariantDefault,\n line: styles.listVariantLine,\n },\n },\n defaultVariants: {\n variant: \"default\",\n },\n})\n\nexport interface TabsListProps\n extends React.ComponentProps<typeof TabsPrimitive.List>,\n VariantProps<typeof tabsListVariants> {}\n\nconst TabsList = React.forwardRef<\n React.ComponentRef<typeof TabsPrimitive.List>,\n TabsListProps\n>(({ className, variant = \"default\", children, ...props }, ref) => {\n const internalRef = React.useRef<HTMLDivElement>(null)\n const [indicatorStyle, setIndicatorStyle] = React.useState<{\n left: number\n width: number\n } | null>(null)\n\n // Merge forwarded ref with internal ref\n const mergedRef = React.useCallback(\n (node: HTMLDivElement | null) => {\n internalRef.current = node\n if (typeof ref === \"function\") {\n ref(node)\n } else if (ref) {\n ref.current = node\n }\n },\n [ref]\n )\n\n const updateIndicator = React.useCallback(() => {\n if (variant !== \"line\" || !internalRef.current) return\n const activeTab = internalRef.current.querySelector<HTMLElement>(\n '[data-state=\"active\"]'\n )\n if (activeTab) {\n setIndicatorStyle({\n left: activeTab.offsetLeft,\n width: activeTab.offsetWidth,\n })\n }\n }, [variant])\n\n React.useEffect(() => {\n if (variant !== \"line\" || !internalRef.current) return\n updateIndicator()\n\n const observer = new MutationObserver(updateIndicator)\n observer.observe(internalRef.current, {\n attributes: true,\n attributeFilter: [\"data-state\"],\n subtree: true,\n })\n\n window.addEventListener(\"resize\", updateIndicator)\n return () => {\n observer.disconnect()\n window.removeEventListener(\"resize\", updateIndicator)\n }\n }, [variant, updateIndicator])\n\n return (\n <TabsPrimitive.List\n ref={mergedRef}\n data-slot=\"tabs-list\"\n data-variant={variant}\n className={cn(tabsListVariants({ variant }), className)}\n {...props}\n >\n {children}\n {variant === \"line\" && indicatorStyle && (\n <span\n className={styles.indicator}\n style={{\n left: indicatorStyle.left,\n width: indicatorStyle.width,\n }}\n />\n )}\n </TabsPrimitive.List>\n )\n})\nTabsList.displayName = \"TabsList\"\n\nexport interface TabsTriggerProps\n extends React.ComponentProps<typeof TabsPrimitive.Trigger> {\n /** Inline count pill rendered after the label. Accepts strings, numbers, or elements. */\n count?: React.ReactNode\n /** Visual tone of the count pill. Defaults to \"neutral\". Active state re-tones automatically. */\n countTone?: \"primary\" | \"neutral\"\n}\n\nconst TabsTrigger = React.forwardRef<\n React.ComponentRef<typeof TabsPrimitive.Trigger>,\n TabsTriggerProps\n>(({ className, children, count, countTone = \"neutral\", ...props }, ref) => (\n <TabsPrimitive.Trigger\n ref={ref}\n data-slot=\"tabs-trigger\"\n className={cn(styles.trigger, className)}\n {...props}\n >\n <span className={styles.label}>{children}</span
|
|
780
|
+
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport * as TabsPrimitive from \"@radix-ui/react-tabs\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\nimport { cn } from \"../../../lib/utils\"\nimport styles from \"./tabs.module.css\"\n\nconst Tabs = React.forwardRef<\n React.ComponentRef<typeof TabsPrimitive.Root>,\n React.ComponentProps<typeof TabsPrimitive.Root>\n>(({ className, ...props }, ref) => (\n <TabsPrimitive.Root\n ref={ref}\n data-slot=\"tabs\"\n className={cn(styles.root, className)}\n {...props}\n />\n))\nTabs.displayName = \"Tabs\"\n\nconst tabsListVariants = cva(styles.list, {\n variants: {\n variant: {\n default: styles.listVariantDefault,\n line: styles.listVariantLine,\n },\n },\n defaultVariants: {\n variant: \"default\",\n },\n})\n\nexport interface TabsListProps\n extends React.ComponentProps<typeof TabsPrimitive.List>,\n VariantProps<typeof tabsListVariants> {}\n\nconst TabsList = React.forwardRef<\n React.ComponentRef<typeof TabsPrimitive.List>,\n TabsListProps\n>(({ className, variant = \"default\", children, ...props }, ref) => {\n const internalRef = React.useRef<HTMLDivElement>(null)\n const [indicatorStyle, setIndicatorStyle] = React.useState<{\n left: number\n width: number\n } | null>(null)\n\n // Merge forwarded ref with internal ref\n const mergedRef = React.useCallback(\n (node: HTMLDivElement | null) => {\n internalRef.current = node\n if (typeof ref === \"function\") {\n ref(node)\n } else if (ref) {\n ref.current = node\n }\n },\n [ref]\n )\n\n const updateIndicator = React.useCallback(() => {\n if (variant !== \"line\" || !internalRef.current) return\n const activeTab = internalRef.current.querySelector<HTMLElement>(\n '[data-state=\"active\"]'\n )\n if (activeTab) {\n setIndicatorStyle({\n left: activeTab.offsetLeft,\n width: activeTab.offsetWidth,\n })\n }\n }, [variant])\n\n React.useEffect(() => {\n if (variant !== \"line\" || !internalRef.current) return\n updateIndicator()\n\n const observer = new MutationObserver(updateIndicator)\n observer.observe(internalRef.current, {\n attributes: true,\n attributeFilter: [\"data-state\"],\n subtree: true,\n })\n\n window.addEventListener(\"resize\", updateIndicator)\n return () => {\n observer.disconnect()\n window.removeEventListener(\"resize\", updateIndicator)\n }\n }, [variant, updateIndicator])\n\n return (\n <TabsPrimitive.List\n ref={mergedRef}\n data-slot=\"tabs-list\"\n data-variant={variant}\n className={cn(tabsListVariants({ variant }), className)}\n {...props}\n >\n {children}\n {variant === \"line\" && indicatorStyle && (\n <span\n className={styles.indicator}\n style={{\n left: indicatorStyle.left,\n width: indicatorStyle.width,\n }}\n />\n )}\n </TabsPrimitive.List>\n )\n})\nTabsList.displayName = \"TabsList\"\n\nexport interface TabsTriggerProps\n extends React.ComponentProps<typeof TabsPrimitive.Trigger> {\n /** Inline count pill rendered after the label. Accepts strings, numbers, or elements. */\n count?: React.ReactNode\n /** Visual tone of the count pill. Defaults to \"neutral\". Active state re-tones automatically. */\n countTone?: \"primary\" | \"neutral\"\n}\n\nconst TabsTrigger = React.forwardRef<\n React.ComponentRef<typeof TabsPrimitive.Trigger>,\n TabsTriggerProps\n>(({ className, children, count, countTone = \"neutral\", ...props }, ref) => (\n <TabsPrimitive.Trigger\n ref={ref}\n data-slot=\"tabs-trigger\"\n className={cn(styles.trigger, className)}\n {...props}\n >\n {/* The .label wrapper exists to pair the label with the count pill\n (truncation + flex sizing). Without a count, render children directly\n so the trigger's flex gap applies between consumer-provided icon and\n label elements. */}\n {count != null ? <span className={styles.label}>{children}</span> : children}\n {count != null && (\n <span\n className={cn(\n styles.count,\n countTone === \"primary\" ? styles.countPrimary : styles.countNeutral,\n )}\n data-slot=\"tabs-trigger-count\"\n data-tone={countTone}\n aria-hidden=\"false\"\n >\n {count}\n </span>\n )}\n </TabsPrimitive.Trigger>\n))\nTabsTrigger.displayName = \"TabsTrigger\"\n\nconst TabsContent = React.forwardRef<\n React.ComponentRef<typeof TabsPrimitive.Content>,\n React.ComponentProps<typeof TabsPrimitive.Content>\n>(({ className, ...props }, ref) => (\n <TabsPrimitive.Content\n ref={ref}\n data-slot=\"tabs-content\"\n className={cn(styles.content, className)}\n {...props}\n />\n))\nTabsContent.displayName = \"TabsContent\"\n\nexport { Tabs, TabsList, TabsTrigger, TabsContent, tabsListVariants }\n"
|
|
706
781
|
},
|
|
707
782
|
{
|
|
708
783
|
"path": "components/ui/tabs/tabs.module.css",
|
|
709
784
|
"type": "registry:ui",
|
|
710
|
-
"content": "/* Tabs root */\n.root {\n display: flex;\n flex-direction: column;\n gap: var(--spacing-2, 0.5rem);\n}\n\n/* Tabs list */\n.list {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n border-radius: var(--radius-full, 9999px);\n padding: calc(var(--spacing-1, 0.25rem) * 0.75);\n color: var(--text-secondary, #6b7280);\n height: 2.25rem;\n width: fit-content;\n}\n\n.listVariantDefault {\n background-color: var(--surface-muted, #f3f4f6);\n}\n\n.listVariantLine {\n position: relative;\n gap: var(--spacing-
|
|
785
|
+
"content": "/* Tabs root */\n.root {\n display: flex;\n flex-direction: column;\n gap: var(--spacing-2, 0.5rem);\n}\n\n/* Tabs list */\n.list {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n border-radius: var(--radius-full, 9999px);\n padding: calc(var(--spacing-1, 0.25rem) * 0.75);\n color: var(--text-secondary, #6b7280);\n height: 2.25rem;\n width: fit-content;\n}\n\n.listVariantDefault {\n background-color: var(--surface-muted, #f3f4f6);\n}\n\n.listVariantLine {\n position: relative;\n gap: var(--spacing-2, 0.5rem);\n background-color: transparent;\n border-radius: 0;\n border-bottom: 1px solid var(--hairline, var(--border-default, #e5e7eb));\n height: auto;\n padding-bottom: 0;\n width: 100%;\n justify-content: flex-start;\n}\n\n/* Line variant: content-width triggers with comfortable padding — the strip\n reads as an underlined rail, not a segmented control. */\n.listVariantLine .trigger {\n flex: 0 0 auto;\n padding: var(--spacing-2, 0.5rem) var(--spacing-3, 0.75rem);\n}\n\n/* Tabs trigger */\n.trigger {\n position: relative;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n gap: calc(var(--spacing-1, 0.25rem) * 1.5);\n border-radius: var(--radius-md, 0.375rem);\n border: 1px solid transparent;\n padding: var(--spacing-1, 0.25rem) var(--spacing-2, 0.5rem);\n font-size: var(--font-size-sm, 0.875rem);\n font-weight: var(--font-weight-medium, 500);\n /* <button> doesn't inherit font-family — without this the trigger falls back\n to the UA default (Arial), not the theme font. */\n font-family: inherit;\n white-space: nowrap;\n color: var(--text-secondary, #6b7280);\n background: transparent;\n cursor: pointer;\n transition: 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), border-color var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out), transform var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out);\n outline: none;\n flex: 1;\n height: calc(100% - 2px);\n}\n\n.trigger:active:not(:disabled) {\n transform: translateY(1px);\n transition: none;\n}\n\n@media (hover: hover) {\n .trigger:hover {\n color: var(--text-primary, #111827);\n }\n}\n\n.trigger: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.trigger:disabled {\n pointer-events: none;\n opacity: 0.5;\n}\n\n.trigger[data-state=\"active\"] {\n background-color: var(--surface-page, #ffffff);\n color: var(--text-primary, #111827);\n box-shadow: var(--shadow-sm);\n}\n\n/* Trigger label — wraps children to allow count pill alongside */\n.label {\n flex: 1 1 auto;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n/* Count pill */\n.count {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n border-radius: var(--radius-full, 9999px);\n padding: 0 var(--spacing-2, 0.5rem);\n font-size: var(--font-size-xs, 0.75rem);\n font-weight: var(--font-weight-medium, 500);\n font-variant-numeric: tabular-nums;\n line-height: 1.5;\n vertical-align: baseline;\n}\n\n.countNeutral {\n background-color: var(--surface-muted, #f3f4f6);\n color: var(--text-secondary, #6b7280);\n}\n\n.countPrimary {\n background-color: var(--surface-accent-subtle, #eff6ff);\n color: var(--text-link, #2563eb);\n}\n\n/* Active-state re-tone: regardless of countTone, re-tone to primary on active */\n.trigger[data-state=\"active\"] .count {\n background-color: var(--surface-accent-subtle, #eff6ff);\n color: var(--text-link, #2563eb);\n}\n\n/* Tabs content */\n.content {\n flex: 1;\n font-size: var(--font-size-sm, 0.875rem);\n outline: none;\n}\n\n.content: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/* Line variant: active trigger loses shadow/bg, just text color */\n.listVariantLine .trigger[data-state=\"active\"] {\n background-color: transparent;\n box-shadow: none;\n}\n\n/* Animated sliding indicator for line variant */\n.indicator {\n position: absolute;\n bottom: -1px;\n height: 2px;\n /* Default: text-primary */\n background-color: var(--text-primary, #111827);\n border-radius: var(--radius-full, 9999px);\n transition: left var(--motion-duration-normal, 200ms) var(--motion-easing-default, ease-in-out),\n width var(--motion-duration-normal, 200ms) var(--motion-easing-default, ease-in-out);\n}\n\n/* Density axis — editorial: brand primary underline for dark admin surfaces */\n:global([data-density=\"editorial\"]) .indicator {\n background-color: var(--primary);\n}\n"
|
|
711
786
|
}
|
|
712
787
|
]
|
|
713
788
|
},
|
|
@@ -969,7 +1044,7 @@
|
|
|
969
1044
|
{
|
|
970
1045
|
"path": "components/ui/table/table.module.css",
|
|
971
1046
|
"type": "registry:ui",
|
|
972
|
-
"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"
|
|
1047
|
+
"content": "/* Table container — radius/shadow are theme-driven so a consumer that wraps the\n table in its own rounded shell (for example with a bulk bar on top) can flatten them.\n Under the editorial density axis these default to 0/none (flush admin shell). */\n.container {\n position: relative;\n width: 100%;\n overflow-x: auto;\n overflow-y: hidden;\n border-radius: var(--dt-container-radius, var(--radius-lg, 0.5rem));\n box-shadow: var(--dt-container-shadow, var(--shadow-sm));\n}\n\n/* Editorial density — bakes the editorial-admin surface values into the density\n axis. Ancestor [data-density=\"editorial\"] (set by DataTable's density prop or\n an app-root attribute) triggers these rules. The var() reads keep --dt-* hooks\n working for per-theme overrides; the fallbacks resolve to the editorial values\n when no hook is set.\n Turbopack pure-selector rule: never bare :global — always paired with a local class. */\n:global([data-density=\"editorial\"]) .container {\n border-radius: var(--dt-container-radius, 0);\n box-shadow: var(--dt-container-shadow, none);\n}\n\n:global([data-density=\"editorial\"]) .head {\n background-color: var(--dt-header-bg, var(--surface-card));\n}\n\n:global([data-density=\"editorial\"]) .cell {\n background-color: var(--dt-row-bg, color-mix(in srgb, var(--surface-page) 75%, var(--surface-card) 25%));\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 — bg via --dt-header-bg (theme-driven; transparent default). */\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 background-color: var(--dt-header-bg, transparent);\n}\n\n/* TableCell — bg via --dt-row-bg (theme-driven; transparent default). Painting\n on the cell gives a uniform row surface regardless of tr selected/hover state. */\n.cell {\n padding: var(--spacing-3, 0.75rem);\n vertical-align: middle;\n color: var(--text-primary, #111827);\n background-color: var(--dt-row-bg, transparent);\n border-top: 1px solid var(--hairline, transparent);\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"
|
|
973
1048
|
}
|
|
974
1049
|
]
|
|
975
1050
|
},
|
|
@@ -1152,6 +1227,31 @@
|
|
|
1152
1227
|
}
|
|
1153
1228
|
]
|
|
1154
1229
|
},
|
|
1230
|
+
{
|
|
1231
|
+
"name": "composer",
|
|
1232
|
+
"type": "registry:ui",
|
|
1233
|
+
"description": "AI-chat composer — a rounded card container with an auto-growing text field, a tools row for icon buttons and status chips, and a circular primary send button. Compound API: Composer, ComposerField, ComposerToolbar, ComposerToolButton, ComposerSpacer, ComposerSend.",
|
|
1234
|
+
"category": "form",
|
|
1235
|
+
"dependencies": [
|
|
1236
|
+
"@phosphor-icons/react",
|
|
1237
|
+
"@loworbitstudio/visor-core"
|
|
1238
|
+
],
|
|
1239
|
+
"registryDependencies": [
|
|
1240
|
+
"utils"
|
|
1241
|
+
],
|
|
1242
|
+
"files": [
|
|
1243
|
+
{
|
|
1244
|
+
"path": "components/ui/composer/composer.tsx",
|
|
1245
|
+
"type": "registry:ui",
|
|
1246
|
+
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport { ArrowUp } from \"@phosphor-icons/react\"\nimport { cn } from \"../../../lib/utils\"\nimport styles from \"./composer.module.css\"\n\n/* ─── Context ─────────────────────────────────────────────────────────── */\n\ninterface ComposerContextValue {\n value: string\n setValue: (v: string) => void\n disabled: boolean\n onSubmit: ((value: string) => void) | undefined\n}\n\nconst ComposerContext = React.createContext<ComposerContextValue | null>(null)\n\nfunction useComposer() {\n const ctx = React.useContext(ComposerContext)\n if (!ctx) throw new Error(\"Composer sub-components must be used inside <Composer>\")\n return ctx\n}\n\n/* ─── Composer (root) ─────────────────────────────────────────────────── */\n\nexport interface ComposerProps extends Omit<React.HTMLAttributes<HTMLDivElement>, \"onSubmit\"> {\n /** Controlled value. */\n value?: string\n /** Called when the value changes (controlled mode). */\n onValueChange?: (value: string) => void\n /** Called when the field is submitted (Enter or send button). */\n onSubmit?: (value: string) => void\n /** Disables all interactive children. */\n disabled?: boolean\n}\n\nconst Composer = React.forwardRef<HTMLDivElement, ComposerProps>(\n (\n {\n className,\n value,\n onValueChange,\n onSubmit,\n disabled = false,\n children,\n ...props\n },\n ref\n ) => {\n const isControlled = value !== undefined\n const [internalValue, setInternalValue] = React.useState(\"\")\n\n const currentValue = isControlled ? value : internalValue\n\n const setValue = React.useCallback(\n (v: string) => {\n if (!isControlled) setInternalValue(v)\n onValueChange?.(v)\n },\n [isControlled, onValueChange]\n )\n\n const handleSubmit = React.useCallback(\n (v: string) => {\n if (!v.trim()) return\n onSubmit?.(v)\n // In uncontrolled mode, clear after submit\n if (!isControlled) setInternalValue(\"\")\n else onValueChange?.(\"\")\n },\n [isControlled, onSubmit, onValueChange]\n )\n\n return (\n <ComposerContext.Provider\n value={{ value: currentValue, setValue, disabled, onSubmit: handleSubmit }}\n >\n <div\n ref={ref}\n data-slot=\"composer\"\n data-disabled={disabled || undefined}\n className={cn(styles.root, className)}\n {...props}\n >\n {children}\n </div>\n </ComposerContext.Provider>\n )\n }\n)\nComposer.displayName = \"Composer\"\n\n/* ─── ComposerField ───────────────────────────────────────────────────── */\n\nexport interface ComposerFieldProps\n extends Omit<React.TextareaHTMLAttributes<HTMLTextAreaElement>, \"value\" | \"onChange\"> {\n /** Placeholder text. */\n placeholder?: string\n /** Disables the field. Falls back to Composer's disabled state. */\n disabled?: boolean\n}\n\nconst ComposerField = React.forwardRef<HTMLTextAreaElement, ComposerFieldProps>(\n ({ className, placeholder = \"Message…\", disabled: fieldDisabled, ...props }, ref) => {\n const { value, setValue, disabled: ctxDisabled, onSubmit } = useComposer()\n const isDisabled = fieldDisabled ?? ctxDisabled\n\n // Auto-grow via hidden replica (field-sizing not yet universal)\n const replicaRef = React.useRef<HTMLDivElement>(null)\n const textareaRef = React.useRef<HTMLTextAreaElement>(null)\n\n // Merge forwarded ref with local ref\n React.useImperativeHandle(ref, () => textareaRef.current!)\n\n // Sync height: replica carries same text + trailing newline to force the\n // textarea to match its scrollHeight without reading layout.\n React.useLayoutEffect(() => {\n const replica = replicaRef.current\n const textarea = textareaRef.current\n if (!replica || !textarea) return\n replica.textContent = value + \"\\n\"\n textarea.style.height = replica.offsetHeight + \"px\"\n }, [value])\n\n const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {\n if (e.key === \"Enter\" && !e.shiftKey) {\n e.preventDefault()\n onSubmit?.(value)\n }\n props.onKeyDown?.(e)\n }\n\n return (\n <div className={styles.fieldWrap}>\n {/* Hidden replica used only for height measurement */}\n <div\n ref={replicaRef}\n className={cn(styles.fieldReplica, styles.field)}\n aria-hidden=\"true\"\n />\n <textarea\n ref={textareaRef}\n data-slot=\"composer-field\"\n rows={1}\n value={value}\n disabled={isDisabled}\n placeholder={placeholder}\n className={cn(styles.field, className)}\n onChange={(e) => setValue(e.target.value)}\n onKeyDown={handleKeyDown}\n {...props}\n />\n </div>\n )\n }\n)\nComposerField.displayName = \"ComposerField\"\n\n/* ─── ComposerToolbar ─────────────────────────────────────────────────── */\n\nexport type ComposerToolbarProps = React.HTMLAttributes<HTMLDivElement>\n\nconst ComposerToolbar = React.forwardRef<HTMLDivElement, ComposerToolbarProps>(\n ({ className, children, ...props }, ref) => (\n <div\n ref={ref}\n data-slot=\"composer-toolbar\"\n className={cn(styles.toolbar, className)}\n {...props}\n >\n {children}\n </div>\n )\n)\nComposerToolbar.displayName = \"ComposerToolbar\"\n\n/* ─── ComposerToolButton ──────────────────────────────────────────────── */\n\nexport interface ComposerToolButtonProps\n extends React.ButtonHTMLAttributes<HTMLButtonElement> {\n /** Phosphor icon element to render inside the button. */\n icon: React.ReactNode\n /** Accessible label (required). */\n \"aria-label\": string\n}\n\nconst ComposerToolButton = React.forwardRef<HTMLButtonElement, ComposerToolButtonProps>(\n ({ className, icon, disabled: btnDisabled, ...props }, ref) => {\n const { disabled: ctxDisabled } = useComposer()\n const isDisabled = btnDisabled ?? ctxDisabled\n\n return (\n <button\n ref={ref}\n type=\"button\"\n data-slot=\"composer-tool-button\"\n disabled={isDisabled}\n className={cn(styles.toolBtn, className)}\n {...props}\n >\n {icon}\n </button>\n )\n }\n)\nComposerToolButton.displayName = \"ComposerToolButton\"\n\n/* ─── ComposerSpacer ──────────────────────────────────────────────────── */\n\nexport type ComposerSpacerProps = React.HTMLAttributes<HTMLDivElement>\n\nconst ComposerSpacer = React.forwardRef<HTMLDivElement, ComposerSpacerProps>(\n ({ className, ...props }, ref) => (\n <div\n ref={ref}\n data-slot=\"composer-spacer\"\n className={cn(styles.spacer, className)}\n {...props}\n />\n )\n)\nComposerSpacer.displayName = \"ComposerSpacer\"\n\n/* ─── ComposerSend ────────────────────────────────────────────────────── */\n\nexport interface ComposerSendProps\n extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, \"type\"> {\n /** Accessible label. @default \"Send\" */\n \"aria-label\"?: string\n}\n\nconst ComposerSend = React.forwardRef<HTMLButtonElement, ComposerSendProps>(\n (\n {\n className,\n \"aria-label\": ariaLabel = \"Send\",\n disabled: btnDisabled,\n onClick,\n ...props\n },\n ref\n ) => {\n const { value, onSubmit, disabled: ctxDisabled } = useComposer()\n // Explicitly disabled by prop or context overrides auto-empty check\n const isDisabled =\n btnDisabled !== undefined\n ? btnDisabled\n : ctxDisabled || !value.trim()\n\n const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {\n onClick?.(e)\n if (!e.defaultPrevented) {\n onSubmit?.(value)\n }\n }\n\n return (\n <button\n ref={ref}\n type=\"button\"\n data-slot=\"composer-send\"\n aria-label={ariaLabel}\n disabled={isDisabled}\n className={cn(styles.send, className)}\n onClick={handleClick}\n {...props}\n >\n <ArrowUp weight=\"bold\" />\n </button>\n )\n }\n)\nComposerSend.displayName = \"ComposerSend\"\n\n/* ─── Exports ─────────────────────────────────────────────────────────── */\n\nexport {\n Composer,\n ComposerField,\n ComposerToolbar,\n ComposerToolButton,\n ComposerSpacer,\n ComposerSend,\n}\n"
|
|
1247
|
+
},
|
|
1248
|
+
{
|
|
1249
|
+
"path": "components/ui/composer/composer.module.css",
|
|
1250
|
+
"type": "registry:ui",
|
|
1251
|
+
"content": "/* Composer\n *\n * AI-chat composer — a rounded card container holding a multi-line\n * auto-growing text field and a tools row with icon buttons, an arbitrary\n * status-chip slot, and a circular primary send button.\n *\n * Visual source: docs/design/brand-workbench/elicit-core (cbox selector)\n *\n * All values flow through CSS custom properties. No hard-coded colors or\n * magic numbers — every value traces to a design token or is documented.\n */\n\n/* ─── Root container ────────────────────────────────────────────────────── */\n\n.root {\n background: var(--surface-card, #ffffff);\n border: var(--stroke-width-thin, 1px) solid var(--border-default, #e5e7eb);\n border-radius: var(--radius-2xl, 1.5rem); /* ~24px, matches cbox prototype */\n box-shadow: var(--shadow-md, 0 4px 6px -1px rgb(0 0 0 / 0.1));\n padding: var(--spacing-3, 0.75rem) var(--spacing-4, 1rem); /* prototype 14/16/11px, snapped to 4px grid */\n display: flex;\n flex-direction: column;\n gap: 0;\n}\n\n.root[data-disabled] {\n opacity: var(--opacity-50, 0.5);\n pointer-events: none;\n}\n\n/* ─── Field row ─────────────────────────────────────────────────────────── */\n\n.fieldWrap {\n position: relative;\n /* The textarea and replica share the same box; replica drives the height */\n}\n\n/* Shared typographic baseline for both field and replica */\n.field {\n font-family: inherit;\n font-size: var(--font-size-base, 1rem); /* ~15px in prototype */\n line-height: var(--line-height-normal, 1.5);\n color: var(--text-primary, #111827);\n background: transparent;\n border: none;\n outline: none;\n resize: none;\n padding: 0;\n margin: 0;\n width: 100%;\n display: block;\n min-height: 22px; /* prototype: min-height 22px */\n overflow: hidden;\n}\n\n/* Hidden replica — same metrics as textarea, invisible, used for auto-grow */\n.fieldReplica {\n position: absolute;\n top: 0;\n left: 0;\n pointer-events: none;\n visibility: hidden;\n white-space: pre-wrap;\n word-break: break-word;\n overflow: hidden;\n}\n\n/* Textarea itself */\n.field:not(.fieldReplica) {\n position: relative;\n color: var(--text-primary, #111827);\n caret-color: var(--primary, #111827);\n}\n\n.field:not(.fieldReplica)::placeholder {\n color: var(--text-tertiary, #6b7280);\n}\n\n.field:not(.fieldReplica):focus-visible {\n outline: var(--focus-ring-width, 2px) solid var(--primary, #111827);\n outline-offset: var(--focus-ring-offset, 2px);\n border-radius: var(--radius-sm, 0.25rem);\n}\n\n/* ─── Toolbar row ───────────────────────────────────────────────────────── */\n\n.toolbar {\n display: flex;\n align-items: center;\n gap: var(--spacing-2, 0.5rem); /* prototype: 7px ≈ spacing-2 */\n margin-top: var(--spacing-3, 0.75rem); /* prototype 11px, snapped to 4px grid */\n}\n\n/* ─── Tool button ───────────────────────────────────────────────────────── */\n\n.toolBtn {\n width: 32px;\n height: 32px;\n border-radius: var(--radius-full, 9999px);\n border: var(--stroke-width-thin, 1px) solid var(--border-default, #e5e7eb);\n background: transparent;\n display: grid;\n place-items: center;\n color: var(--text-secondary, #374151);\n flex: none;\n cursor: pointer;\n transition:\n background-color var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out),\n border-color var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out),\n color var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out);\n}\n\n.toolBtn:hover:not(:disabled) {\n background: var(--surface-hover, color-mix(in srgb, var(--border-default, #e5e7eb) 40%, transparent));\n border-color: var(--border-strong, #9ca3af);\n}\n\n.toolBtn:focus-visible {\n outline: var(--focus-ring-width, 2px) solid var(--primary, #111827);\n outline-offset: var(--focus-ring-offset, 2px);\n}\n\n.toolBtn:disabled {\n opacity: var(--opacity-40, 0.4);\n cursor: not-allowed;\n}\n\n/* Icon inside tool button — 16px per prototype */\n.toolBtn > svg,\n.toolBtn > * > svg {\n width: 16px;\n height: 16px;\n}\n\n/* ─── Spacer ────────────────────────────────────────────────────────────── */\n\n.spacer {\n flex: 1;\n}\n\n/* ─── Send button ───────────────────────────────────────────────────────── */\n\n.send {\n width: 34px;\n height: 34px;\n border-radius: var(--radius-full, 9999px);\n background: var(--primary, #111827);\n color: var(--primary-foreground, #ffffff);\n display: grid;\n place-items: center;\n box-shadow: var(--shadow-sm, 0 1px 2px 0 rgb(0 0 0 / 0.05));\n flex: none;\n border: none;\n cursor: pointer;\n transition:\n background-color var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out),\n opacity var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out),\n box-shadow var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out);\n}\n\n.send:hover:not(:disabled) {\n background: color-mix(in srgb, var(--primary, #111827) 88%, white);\n box-shadow: var(--shadow-md, 0 4px 6px -1px rgb(0 0 0 / 0.1));\n}\n\n.send:focus-visible {\n outline: var(--focus-ring-width, 2px) solid var(--primary, #111827);\n outline-offset: var(--focus-ring-offset, 2px);\n}\n\n.send:disabled {\n opacity: var(--opacity-40, 0.4);\n cursor: not-allowed;\n}\n\n/* Icon inside send button — 17px per prototype */\n.send > svg,\n.send > * > svg {\n width: 17px;\n height: 17px;\n}\n\n/* ─── Reduced motion ────────────────────────────────────────────────────── */\n\n@media (prefers-reduced-motion: reduce) {\n .toolBtn,\n .send {\n transition: none;\n }\n}\n"
|
|
1252
|
+
}
|
|
1253
|
+
]
|
|
1254
|
+
},
|
|
1155
1255
|
{
|
|
1156
1256
|
"name": "pagination",
|
|
1157
1257
|
"type": "registry:ui",
|
|
@@ -1620,12 +1720,12 @@
|
|
|
1620
1720
|
{
|
|
1621
1721
|
"path": "components/ui/matrix-table/matrix-table.tsx",
|
|
1622
1722
|
"type": "registry:ui",
|
|
1623
|
-
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport {
|
|
1723
|
+
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport { Check } from \"@phosphor-icons/react/dist/ssr\"\nimport { cn } from \"../../../lib/utils\"\nimport styles from \"./matrix-table.module.css\"\n\n// ─── Types ────────────────────────────────────────────────────────────────────\n\nexport interface MatrixColumn {\n /**\n * Stable key indexing the per-row cell map (`MatrixRow[\"cells\"]`) and used as\n * the React key. This is the blessed (look-spec) field.\n *\n * `id` is accepted as a compatible alias — when `key` is absent, `id` is used.\n * Provide one or the other.\n */\n key?: string\n /**\n * Compatible alias for {@link MatrixColumn.key}. Prefer `key`; `id` is kept so\n * callers written against canonical's prior API keep working.\n */\n id?: string\n /** Column header label (role name). Rendered 600/13px, primary, no tracking. */\n label: React.ReactNode\n /** Secondary count beneath the label (11px, tertiary). Optional. */\n count?: React.ReactNode\n /** Tooltip shown when hovering an active cell in this column. Optional. */\n description?: React.ReactNode\n}\n\n/**\n * A single matrix cell value.\n *\n * - `true` → renders the active filled-success disc with a check glyph\n * - `false` → renders the muted \"not assigned\" dash\n * - `string` → renders the text in the standard cell typography\n */\nexport type MatrixCellValue = string | boolean\n\nexport interface MatrixRow<TIdentity = unknown> {\n /** Stable row id (React key). */\n id: string\n /**\n * Optional identity payload. Convenience for callers whose `renderIdentity`\n * reads structured identity data (e.g. `(row) => row.identity.name`). The\n * component never inspects this — it is passed straight to `renderIdentity`\n * via the row.\n */\n identity?: TIdentity\n /**\n * Per-column cell values keyed by `MatrixColumn[\"key\"]` (or `id`). Each value\n * is a {@link MatrixCellValue}: `true`/`false` render the boolean indicator,\n * a `string` renders as plain text in the standard cell style. Missing = false.\n *\n * When both `cells` and `activeColumns` are present, a `cells` entry takes\n * precedence over `activeColumns` for that column.\n */\n cells?: Record<string, MatrixCellValue>\n /**\n * Compatible alias for the boolean half of `cells`: a set/array of column keys\n * that should render the active check indicator. Columns absent here render as\n * \"not assigned\". Kept so callers written against canonical's prior API keep\n * working; new callers should prefer `cells`.\n */\n activeColumns?: Set<string> | string[]\n}\n\nexport interface MatrixTableProps<TIdentity = unknown>\n extends Omit<React.HTMLAttributes<HTMLDivElement>, \"children\"> {\n /** Column (role) definitions — each carries a label + optional count. */\n columns: MatrixColumn[]\n /** Row data — identity metadata lives in `renderIdentity`; truth in `cells`. */\n rows: MatrixRow<TIdentity>[]\n /**\n * Renders the sticky-left identity cell for a row. Receives the whole row\n * (blessed shape) — read `row.identity` for structured identity data.\n */\n renderIdentity: (row: MatrixRow<TIdentity>) => React.ReactNode\n /** Header label for the sticky identity column. Defaults to \"Member\". */\n identityLabel?: React.ReactNode\n /** Accessible label for the table element. */\n \"aria-label\"?: string\n}\n\n// ─── Helpers ──────────────────────────────────────────────────────────────────\n\n/** Resolve a column's stable key, preferring `key` then the `id` alias. */\nfunction columnKey(column: MatrixColumn): string {\n return (column.key ?? column.id) as string\n}\n\n/**\n * Resolve a row's value for a column. A `cells` entry wins; otherwise fall back\n * to `activeColumns` membership (boolean). Missing everywhere → `false`.\n */\nfunction cellValue(row: MatrixRow, key: string): MatrixCellValue {\n if (row.cells != null && key in row.cells) {\n return row.cells[key]\n }\n if (row.activeColumns != null) {\n const set =\n row.activeColumns instanceof Set\n ? row.activeColumns\n : new Set(row.activeColumns)\n return set.has(key)\n }\n return false\n}\n\n// ─── MatrixTable ─────────────────────────────────────────────────────────────\n\n/**\n * MatrixTable — a members×roles assignment grid.\n *\n * A sticky-left identity column pins through horizontal scroll; the remaining\n * columns are centered boolean assignments. An \"active\" cell renders a filled\n * success disc with a check glyph; \"not assigned\" renders a muted dash. A\n * string cell renders plain text in the standard cell typography.\n *\n * This is NOT the sortable/paginated DataTable — there is no selection column,\n * no sort affordance, and no pagination footer. It is a flat presentational\n * grid driven entirely by `columns` + `rows`.\n */\nfunction MatrixTableInner<TIdentity = unknown>(\n props: MatrixTableProps<TIdentity>,\n ref: React.ForwardedRef<HTMLDivElement>\n) {\n const {\n columns,\n rows,\n renderIdentity,\n identityLabel = \"Member\",\n className,\n \"aria-label\": ariaLabel,\n ...rest\n } = props\n\n return (\n <div\n ref={ref}\n data-slot=\"matrix-table\"\n className={cn(styles.container, className)}\n {...rest}\n >\n <table className={styles.table} aria-label={ariaLabel}>\n <thead>\n <tr>\n <th\n scope=\"col\"\n data-slot=\"matrix-table-identity-head\"\n className={styles.identityHead}\n >\n {identityLabel}\n </th>\n {columns.map((column) => (\n <th\n key={columnKey(column)}\n scope=\"col\"\n data-slot=\"matrix-table-column-head\"\n className={styles.columnHead}\n >\n <span className={styles.columnHeader}>\n <span className={styles.columnLabel}>{column.label}</span>\n {column.count != null ? (\n <span className={styles.columnCount}>{column.count}</span>\n ) : null}\n </span>\n </th>\n ))}\n </tr>\n </thead>\n <tbody>\n {rows.map((row) => (\n <tr key={row.id} data-slot=\"matrix-table-row\">\n <td\n data-slot=\"matrix-table-identity-cell\"\n className={styles.identityCell}\n >\n {renderIdentity(row)}\n </td>\n {columns.map((column) => {\n const key = columnKey(column)\n const value = cellValue(row, key)\n\n // String cell — plain text in the standard cell typography.\n if (typeof value !== \"boolean\") {\n return (\n <td\n key={key}\n data-slot=\"matrix-table-cell\"\n className={styles.cell}\n aria-label={`${String(column.label)}: ${value}`}\n >\n <span className={styles.textCell}>{value}</span>\n </td>\n )\n }\n\n // Boolean cell — filled-success disc (active) or muted dash.\n return (\n <td\n key={key}\n data-slot=\"matrix-table-cell\"\n className={styles.cell}\n >\n <MatrixCell active={value} description={column.description} />\n </td>\n )\n })}\n </tr>\n ))}\n </tbody>\n </table>\n </div>\n )\n}\n\n// forwardRef with generics — preserve TIdentity through the cast\nconst MatrixTable = React.forwardRef(MatrixTableInner) as (<\n TIdentity = unknown,\n>(\n props: MatrixTableProps<TIdentity> & {\n ref?: React.ForwardedRef<HTMLDivElement>\n }\n) => ReturnType<typeof MatrixTableInner>) & { displayName?: string }\n\nMatrixTable.displayName = \"MatrixTable\"\n\n// ─── MatrixCell ──────────────────────────────────────────────────────────────\n\nfunction MatrixCell({\n active,\n description,\n}: {\n active: boolean\n description?: React.ReactNode\n}) {\n if (!active) {\n return (\n <span\n data-slot=\"matrix-cell\"\n data-active=\"false\"\n className={styles.markInactive}\n aria-label=\"Not assigned\"\n >\n —\n </span>\n )\n }\n return (\n <span\n data-slot=\"matrix-cell\"\n data-active=\"true\"\n className={styles.markActive}\n aria-label=\"Assigned\"\n >\n <span className={styles.disc} aria-hidden=\"true\">\n <Check weight=\"bold\" />\n </span>\n {description != null ? (\n <span role=\"tooltip\" className={styles.tooltip}>\n {description}\n </span>\n ) : null}\n </span>\n )\n}\n\nexport { MatrixTable }\n"
|
|
1624
1724
|
},
|
|
1625
1725
|
{
|
|
1626
1726
|
"path": "components/ui/matrix-table/matrix-table.module.css",
|
|
1627
1727
|
"type": "registry:ui",
|
|
1628
|
-
"content": "/* MatrixTable — members×roles
|
|
1728
|
+
"content": "/* MatrixTable — members×roles assignment grid.\n *\n * A flat presentational grid (NOT the sortable/paginated DataTable). A\n * sticky-left identity column pins through horizontal scroll; the remaining\n * columns are centered boolean assignments rendered as a filled success disc\n * (active) or a muted dash (not assigned). A string cell renders plain text.\n *\n * Theme-agnostic: every value is a literal or derives from a palette/role\n * token (--surface-card, --surface-subtle, --success, --primary-text,\n * --text-tertiary, --text-muted, --hairline, --spacing-*, --radius-*), so the\n * grid renders correctly under ANY Visor theme. Shares the density axis with\n * DataTable/Table — the --dt-* hooks (--dt-container-radius / --dt-container-shadow\n * / --dt-header-bg / --dt-row-bg) are consumed here so the matrix reads as a\n * sibling of the list table under any density, including editorial.\n */\n\n/* Container — horizontal scroll for the role columns; the consumer shell owns\n rounding, so flatten via the same --dt-container-* hooks the table uses.\n Under the editorial density axis these default to 0/none. */\n.container {\n position: relative;\n width: 100%;\n overflow-x: auto;\n overflow-y: hidden;\n border-radius: var(--dt-container-radius, var(--radius-lg, 0.5rem));\n box-shadow: var(--dt-container-shadow, var(--shadow-sm));\n color: var(--text-primary, #111827);\n}\n\n/* Editorial density — bakes the editorial-admin surface values into the density\n axis. Ancestor [data-density=\"editorial\"] (set by DataTable's density prop or\n an app-root attribute) triggers these rules. Header bg editorial fallback\n (var(--surface-card)) is identical to the canonical default so no header rule\n is needed. Container goes flush; row bg uses the page/card mix.\n Turbopack pure-selector rule: never bare :global — always paired with a local class. */\n:global([data-density=\"editorial\"]) .container {\n border-radius: var(--dt-container-radius, 0);\n box-shadow: var(--dt-container-shadow, none);\n}\n\n:global([data-density=\"editorial\"]) .cell {\n background-color: var(--dt-row-bg, color-mix(in srgb, var(--surface-page) 75%, var(--surface-card) 25%));\n}\n\n.table {\n width: 100%;\n border-collapse: collapse;\n font-size: var(--font-size-sm, 0.875rem);\n color: var(--text-primary, #111827);\n}\n\n/* ── Header ───────────────────────────────────────────────────────────────\n Editorial cell rhythm (spacing-5 horizontal) matching the data-table\n editorial preset. Header sits on the card tier; rows a hair above the page. */\n.identityHead,\n.columnHead {\n padding: var(--spacing-3, 0.75rem) var(--spacing-5, 1.25rem);\n vertical-align: middle;\n white-space: nowrap;\n background-color: var(--dt-header-bg, var(--surface-card, #ffffff));\n border-bottom: var(--hairline-width, 1px) solid var(--hairline, transparent);\n}\n\n/* Boolean columns are centered. */\n.columnHead {\n text-align: center;\n}\n\n/* Sticky-left identity header — pins above the body sticky cell (z 2 > 1).\n Carries the editorial eyebrow treatment (11px, tracked, uppercase, tertiary)\n matching the data-table editorial column-header so \"MEMBER\" reads as a peer\n of the list table — while the role columns deliberately opt OUT of it. */\n.identityHead {\n position: sticky;\n left: 0;\n z-index: 2;\n min-width: 240px;\n text-align: left;\n font-size: 11px;\n letter-spacing: 0.08em;\n text-transform: uppercase;\n font-weight: var(--font-weight-medium, 500);\n color: var(--text-tertiary, #6b7280);\n}\n\n/* Multi-line centered column header: role name 600/13px over an 11px tertiary\n count. Deliberately NOT the editorial uppercase/tracked th treatment. */\n.columnHeader {\n display: flex;\n flex-direction: column;\n gap: calc(var(--spacing-1, 0.25rem) / 2);\n align-items: center;\n}\n\n.columnLabel {\n font-weight: var(--font-weight-semibold, 600);\n color: var(--text-primary, #111827);\n text-transform: none;\n letter-spacing: 0;\n font-size: 13px;\n line-height: 1.2;\n}\n\n.columnCount {\n font-size: 11px;\n color: var(--text-tertiary, #6b7280);\n text-transform: none;\n letter-spacing: 0;\n font-variant-numeric: tabular-nums;\n line-height: 1.2;\n}\n\n/* ── Body cells ───────────────────────────────────────────────────────────── */\n.identityCell,\n.cell {\n vertical-align: middle;\n background-color: var(--dt-row-bg, transparent);\n border-top: var(--hairline-width, 1px) solid var(--hairline, transparent);\n}\n\n/* Sticky-left identity cell — tracks row hover, sits above body cells but below\n the header (z 1). The matching background paints over scrolled role cells. */\n.identityCell {\n position: sticky;\n left: 0;\n z-index: 1;\n min-width: 240px;\n padding: var(--spacing-4, 1rem) var(--spacing-5, 1.25rem);\n background-color: var(--surface-card, #ffffff);\n}\n\n.table tbody tr:hover .identityCell {\n background-color: var(--surface-subtle, #f3f4f6);\n}\n\n/* Centered boolean cell. */\n.cell {\n padding: var(--spacing-3, 0.75rem);\n text-align: center;\n}\n\n/* String cell — centered editorial value (for example \"Unlimited\", \"Up to 5\", \"10\").\n Matches the blessed feature-comparison string treatment: 13px medium,\n line-height inherited from the cell. */\n.textCell {\n display: inline-block;\n font-size: 13px;\n font-weight: var(--font-weight-medium, 500);\n color: var(--text-primary, #111827);\n}\n\n/* ── Mark ─────────────────────────────────────────────────────────────────── */\n.markActive,\n.markInactive {\n display: inline-grid;\n place-items: center;\n width: 32px;\n height: 24px;\n position: relative;\n}\n\n.markActive {\n cursor: help;\n}\n\n/* Active = 22px circular FILLED success disc with a centered check glyph.\n Mirrors the badge filled-success variant (var(--success) fill, var(--primary-text)\n glyph) so the disc reads identically on every palette. */\n.disc {\n display: inline-grid;\n place-content: center;\n width: 22px;\n height: 22px;\n padding: 0;\n border-radius: var(--radius-full, 999px);\n background: var(--success, #22c55e);\n color: var(--primary-text, #ffffff);\n font-size: 12px;\n line-height: 1;\n}\n\n.disc svg {\n width: 0.75rem;\n height: 0.75rem;\n color: var(--primary-text, #ffffff);\n}\n\n/* Not assigned = muted dash. */\n.markInactive {\n color: var(--text-muted, #9ca3af);\n font-size: var(--font-size-base, 1rem);\n}\n\n/* ── Tooltip ──────────────────────────────────────────────────────────────── */\n.tooltip {\n position: absolute;\n bottom: calc(100% + 6px);\n left: 50%;\n transform: translateX(-50%);\n background: var(--surface-elev, #ffffff);\n color: var(--text-primary, #111827);\n padding: calc(var(--spacing-1, 0.25rem) * 1.5) calc(var(--spacing-2, 0.5rem) + var(--spacing-1, 0.25rem) / 2);\n border-radius: var(--radius-sm, 0.25rem);\n font-size: 11px;\n white-space: nowrap;\n opacity: 0;\n pointer-events: none;\n transition: opacity var(--motion-duration-100, 80ms) var(--motion-easing-default, ease);\n box-shadow: var(--shadow-md, 0 4px 6px -1px rgb(0 0 0 / 0.1));\n z-index: 3;\n}\n\n.markActive:hover .tooltip {\n opacity: 1;\n}\n"
|
|
1629
1729
|
}
|
|
1630
1730
|
]
|
|
1631
1731
|
},
|
|
@@ -1983,6 +2083,30 @@
|
|
|
1983
2083
|
}
|
|
1984
2084
|
]
|
|
1985
2085
|
},
|
|
2086
|
+
{
|
|
2087
|
+
"name": "specimen-card",
|
|
2088
|
+
"type": "registry:ui",
|
|
2089
|
+
"description": "Labeled frame that pairs a context token + 'feel' descriptor with arbitrary live component children — proves how a brand voice/tone renders on real components. Covers both the tone-by-context grid card and the Speaking block with optional voice-key footer.",
|
|
2090
|
+
"category": "specimen",
|
|
2091
|
+
"dependencies": [
|
|
2092
|
+
"@loworbitstudio/visor-core"
|
|
2093
|
+
],
|
|
2094
|
+
"registryDependencies": [
|
|
2095
|
+
"utils"
|
|
2096
|
+
],
|
|
2097
|
+
"files": [
|
|
2098
|
+
{
|
|
2099
|
+
"path": "components/ui/specimen-card/specimen-card.tsx",
|
|
2100
|
+
"type": "registry:ui",
|
|
2101
|
+
"content": "import * as React from \"react\"\nimport { cn } from \"../../../lib/utils\"\nimport styles from \"./specimen-card.module.css\"\n\n// ─── SpecimenCard ────────────────────────────────────────────────────────────\n\nexport interface SpecimenCardProps extends React.HTMLAttributes<HTMLDivElement> {\n /** Context token label (e.g. \"error\", \"onboarding\", \"success\"). Rendered uppercase. */\n context: string\n /** Italic feel descriptor (e.g. \"warm, accountable\"). Optional. */\n feel?: string\n}\n\nconst SpecimenCard = React.forwardRef<HTMLDivElement, SpecimenCardProps>(\n ({ className, context, feel, children, ...props }, ref) => {\n return (\n <div\n ref={ref}\n data-slot=\"specimen-card\"\n className={cn(styles.card, className)}\n {...props}\n >\n <div data-slot=\"specimen-card-label\" className={styles.labelRow}>\n <span className={styles.contextLabel}>{context}</span>\n {feel ? (\n <span className={styles.feelText}>{feel}</span>\n ) : null}\n </div>\n <div data-slot=\"specimen-card-body\" className={styles.body}>\n {children}\n </div>\n </div>\n )\n }\n)\nSpecimenCard.displayName = \"SpecimenCard\"\n\n// ─── SpecimenCardFooter ──────────────────────────────────────────────────────\n\nexport type SpecimenCardFooterProps = React.HTMLAttributes<HTMLDivElement>\n\nconst SpecimenCardFooter = React.forwardRef<\n HTMLDivElement,\n SpecimenCardFooterProps\n>(({ className, children, ...props }, ref) => {\n return (\n <div\n ref={ref}\n data-slot=\"specimen-card-footer\"\n className={cn(styles.footer, className)}\n {...props}\n >\n {children}\n </div>\n )\n})\nSpecimenCardFooter.displayName = \"SpecimenCardFooter\"\n\nexport { SpecimenCard, SpecimenCardFooter }\n"
|
|
2102
|
+
},
|
|
2103
|
+
{
|
|
2104
|
+
"path": "components/ui/specimen-card/specimen-card.module.css",
|
|
2105
|
+
"type": "registry:ui",
|
|
2106
|
+
"content": "/* Specimen Card\n *\n * A labeled frame that pairs a context label + \"feel\" descriptor with\n * arbitrary live component children — proving how a brand voice/tone\n * renders on real components.\n *\n * Two uses:\n * (a) Tone-by-context grid — card frame with uppercase context token +\n * italic feel descriptor above the live specimen.\n * (b) Speaking block — same frame with multiple specimens stacked and\n * an optional voice-key attribution footer.\n *\n * All values flow through CSS custom properties so themes can tune\n * without forking the component.\n */\n\n.card {\n background: var(--surface-card, #ffffff);\n border: var(--stroke-width-thin, 1px) solid var(--hairline, #e5e7eb);\n border-radius: var(--radius-lg, 0.5rem);\n padding: var(--spacing-3, 0.75rem);\n box-shadow: var(--shadow-sm, 0 1px 2px rgba(0,0,0,0.06));\n display: flex;\n flex-direction: column;\n gap: var(--spacing-2, 0.5rem);\n}\n\n/* Label row — context token + feel descriptor */\n.labelRow {\n display: flex;\n align-items: center;\n gap: var(--spacing-2, 0.5rem);\n}\n\n/* Context label — uppercase small caps, secondary color */\n.contextLabel {\n font-size: 11px;\n font-weight: 800;\n letter-spacing: 0.04em;\n text-transform: uppercase;\n color: var(--text-secondary, #6b7280);\n line-height: 1;\n}\n\n/* Feel descriptor — italic, tertiary, pushed to the end */\n.feelText {\n margin-left: auto;\n font-size: 11px;\n color: var(--text-tertiary, #9ca3af);\n font-style: italic;\n line-height: 1;\n}\n\n/* Body — vertical stack of live specimen children */\n.body {\n display: flex;\n flex-direction: column;\n gap: var(--spacing-2, 0.5rem);\n}\n\n/* Footer — hairline-separated voice key attribution */\n.footer {\n border-top: var(--stroke-width-thin, 1px) solid var(--hairline, #e5e7eb);\n margin-top: var(--spacing-1, 0.25rem);\n padding-top: var(--spacing-2, 0.5rem);\n font-size: 10.5px;\n color: var(--text-tertiary, #9ca3af);\n display: flex;\n align-items: center;\n gap: var(--spacing-1-5, 0.375rem);\n line-height: 1.4;\n}\n"
|
|
2107
|
+
}
|
|
2108
|
+
]
|
|
2109
|
+
},
|
|
1986
2110
|
{
|
|
1987
2111
|
"name": "surface-row",
|
|
1988
2112
|
"type": "registry:ui",
|
|
@@ -2153,7 +2277,7 @@
|
|
|
2153
2277
|
{
|
|
2154
2278
|
"path": "components/ui/bulk-action-bar/bulk-action-bar.module.css",
|
|
2155
2279
|
"type": "registry:ui",
|
|
2156
|
-
"content": "/* Bulk Action Bar: appears when rows are selected. Sticky by default. */\n.base {\n display: flex;\n flex-direction: row;\n align-items: center;\n gap: var(--spacing-4, 1rem);\n padding: var(--spacing-3, 0.75rem) var(--spacing-4, 1rem);\n background-color: var(--surface-card, #ffffff);\n border-top: 1px solid var(--border-default, #e5e7eb);\n color: var(--text-primary, #111827);\n box-shadow: var(--shadow-md, 0 4px 6px -1px rgb(0 0 0 / 0.1));\n border-radius: var(--radius-lg, 0.75rem);\n}\n\n/* Sticky variant — pinned to the viewport bottom. */\n.sticky {\n position: fixed;\n bottom: var(--spacing-4, 1rem);\n left: var(--spacing-4, 1rem);\n right: var(--spacing-4, 1rem);\n /*\n * Toolbar layer. No --z-index-toolbar token exists yet in\n * @loworbitstudio/visor-core — promote to a token once the z-index scale\n * is formalized.\n */\n z-index: 50;\n animation: bulk-action-bar-enter var(--motion-duration-moderate, 300ms)\n var(--motion-easing-standard, ease-out);\n}\n\n/* Inline variant — flows in the document, no fixed positioning. */\n.inline {\n position: relative;\n width: 100%;\n}\n\n/* Flat/embedded variant — no shadow, no radius, border-top hairline only.\n Use inside table cards or panels where the floating card look is wrong. */\n.flat {\n box-shadow: none;\n border-radius: 0;\n}\n\n/* Selection count label */\n.count {\n font-size: var(--font-size-sm, 0.875rem);\n font-weight: var(--font-weight-medium, 500);\n color: var(--text-primary, #111827);\n line-height: var(--line-height-tight, 1.25);\n white-space: nowrap;\n flex: 0 0 auto;\n}\n\n/* Actions cluster — horizontal, wraps on narrow rows */\n.actions {\n display: flex;\n flex-direction: row;\n align-items: center;\n flex-wrap: wrap;\n gap: var(--spacing-2, 0.5rem);\n flex: 1 1 auto;\n min-width: 0;\n}\n\n/* Dismiss button — sits flush on the right */\n.dismiss {\n flex: 0 0 auto;\n margin-left: auto;\n}\n\n.dismiss svg {\n width: var(--spacing-4, 1rem);\n height: var(--spacing-4, 1rem);\n}\n\n/* Entrance animation: slide up + fade in. */\n@keyframes bulk-action-bar-enter {\n from {\n opacity: 0;\n transform: translateY(var(--spacing-4, 1rem));\n }\n to {\n opacity: 1;\n transform: translateY(0);\n }\n}\n\n/* Respect prefers-reduced-motion — skip the slide, keep it mounted. */\n@media (prefers-reduced-motion: reduce) {\n .sticky {\n animation: none;\n }\n}\n"
|
|
2280
|
+
"content": "/* Bulk Action Bar: appears when rows are selected. Sticky by default. */\n.base {\n display: flex;\n flex-direction: row;\n align-items: center;\n gap: var(--spacing-4, 1rem);\n padding: var(--spacing-3, 0.75rem) var(--spacing-4, 1rem);\n background-color: var(--surface-card, #ffffff);\n border-top: 1px solid var(--border-default, #e5e7eb);\n color: var(--text-primary, #111827);\n box-shadow: var(--shadow-md, 0 4px 6px -1px rgb(0 0 0 / 0.1));\n border-radius: var(--radius-lg, 0.75rem);\n}\n\n/* Sticky variant — pinned to the viewport bottom. */\n.sticky {\n position: fixed;\n bottom: var(--spacing-4, 1rem);\n left: var(--spacing-4, 1rem);\n right: var(--spacing-4, 1rem);\n /*\n * Toolbar layer. No --z-index-toolbar token exists yet in\n * @loworbitstudio/visor-core — promote to a token once the z-index scale\n * is formalized.\n */\n z-index: 50;\n animation: bulk-action-bar-enter var(--motion-duration-moderate, 300ms)\n var(--motion-easing-standard, ease-out);\n}\n\n/* Inline variant — flows in the document, no fixed positioning. */\n.inline {\n position: relative;\n width: 100%;\n}\n\n/* Flat/embedded variant — no shadow, no radius, border-top hairline only.\n Use inside table cards or panels where the floating card look is wrong. */\n.flat {\n box-shadow: none;\n border-radius: 0;\n}\n\n/* Editorial density — the blessed admin strip: the inline bar sheds the\n floating-card chrome entirely (flat, card-tier fill, hairline below, dense\n padding), the count reads 13px tertiary, and actions are pushed to the right\n edge and read muted. Matches the blessed admin-ui reference builds. */\n:global([data-density=\"editorial\"]) .inline {\n background-color: var(--surface-card, #ffffff);\n box-shadow: none;\n border-radius: 0;\n border-top: none;\n border-bottom: 1px solid var(--hairline, var(--border-default, #e5e7eb));\n padding: var(--spacing-2, 0.5rem) var(--spacing-5, 1.25rem);\n}\n\n:global([data-density=\"editorial\"]) .count {\n font-size: 0.8125rem;\n color: var(--text-tertiary, #6b7280);\n}\n\n:global([data-density=\"editorial\"]) .actions {\n justify-content: flex-end;\n}\n\n/* Action buttons read muted under editorial; a destructive action keeps its\n own inline color (style attribute wins over this). */\n:global([data-density=\"editorial\"]) .actions button {\n color: var(--text-tertiary, #6b7280);\n}\n\n/* Selection count label */\n.count {\n font-size: var(--font-size-sm, 0.875rem);\n font-weight: var(--font-weight-medium, 500);\n color: var(--text-primary, #111827);\n line-height: var(--line-height-tight, 1.25);\n white-space: nowrap;\n flex: 0 0 auto;\n}\n\n/* Actions cluster — horizontal, wraps on narrow rows */\n.actions {\n display: flex;\n flex-direction: row;\n align-items: center;\n flex-wrap: wrap;\n gap: var(--spacing-2, 0.5rem);\n flex: 1 1 auto;\n min-width: 0;\n}\n\n/* Dismiss button — sits flush on the right */\n.dismiss {\n flex: 0 0 auto;\n margin-left: auto;\n}\n\n.dismiss svg {\n width: var(--spacing-4, 1rem);\n height: var(--spacing-4, 1rem);\n}\n\n/* Entrance animation: slide up + fade in. */\n@keyframes bulk-action-bar-enter {\n from {\n opacity: 0;\n transform: translateY(var(--spacing-4, 1rem));\n }\n to {\n opacity: 1;\n transform: translateY(0);\n }\n}\n\n/* Respect prefers-reduced-motion — skip the slide, keep it mounted. */\n@media (prefers-reduced-motion: reduce) {\n .sticky {\n animation: none;\n }\n}\n"
|
|
2157
2281
|
}
|
|
2158
2282
|
]
|
|
2159
2283
|
},
|
|
@@ -2202,12 +2326,12 @@
|
|
|
2202
2326
|
{
|
|
2203
2327
|
"path": "components/ui/confirm-dialog/confirm-dialog.tsx",
|
|
2204
2328
|
"type": "registry:ui",
|
|
2205
|
-
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport {\n InfoIcon,\n WarningIcon,\n WarningOctagonIcon,\n} from \"@phosphor-icons/react\"\nimport { cn } from \"../../../lib/utils\"\nimport {\n Dialog,\n DialogContent,\n DialogDescription,\n DialogHeader,\n DialogTitle,\n DialogTrigger,\n} from \"../dialog/dialog\"\nimport { Button } from \"../button/button\"\nimport styles from \"./confirm-dialog.module.css\"\n\nexport type ConfirmDialogSeverity =\n | \"info\"\n | \"warning\"\n | \"destructive\"\n /**\n * @deprecated Use `\"destructive\"` instead. `\"danger\"` will be removed in the next major version.\n */\n | \"danger\"\n\n/** Internal normalized severity — `\"danger\"` is collapsed to `\"destructive\"`. */\ntype NormalizedSeverity = \"info\" | \"warning\" | \"destructive\"\n\nfunction normalizeSeverity(s: ConfirmDialogSeverity): NormalizedSeverity {\n return s === \"danger\" ? \"destructive\" : s\n}\n\nexport interface ConfirmDialogProps {\n /** Controlled open state. */\n open?: boolean\n /** Uncontrolled initial open state. */\n defaultOpen?: boolean\n /** Called when open state changes. */\n onOpenChange?: (open: boolean) => void\n\n /** Optional trigger — wrapped in DialogTrigger. Omit for fully-controlled usage. */\n trigger?: React.ReactNode\n\n /** Dialog title — rendered in DialogTitle next to the severity icon. */\n title: React.ReactNode\n /** Short body above the action row. Used as DialogDescription. */\n description?: React.ReactNode\n /** Richer body slot — overrides description when provided. */\n children?: React.ReactNode\n\n /** Severity — drives icon color, confirm button variant. Default \"warning\". */\n severity?: ConfirmDialogSeverity\n\n /** Confirm button label. Defaults: \"Delete\" for danger, \"Confirm\" otherwise. */\n confirmLabel?: React.ReactNode\n /** Cancel button label. Default \"Cancel\". */\n cancelLabel?: React.ReactNode\n\n /** If set, user must type this exact string to enable the confirm button. */\n confirmText?: string\n /** Label for the confirm-text input. Default: `Type ${confirmText} to confirm`. */\n confirmTextLabel?: React.ReactNode\n\n /** Confirm handler. Async-aware: returning a Promise puts the dialog into pending state. */\n onConfirm?: () => void | Promise<void>\n /** Cancel handler. */\n onCancel?: () => void\n\n /** Externally-controlled busy state — overrides internal async pending detection. */\n busy?: boolean\n\n /** Additional className on DialogContent. */\n className?: string\n}\n\nfunction getSeverityIcon(severity: NormalizedSeverity): React.ReactNode {\n switch (severity) {\n case \"info\":\n return <InfoIcon weight=\"fill\" aria-hidden=\"true\" />\n case \"destructive\":\n return <WarningOctagonIcon weight=\"fill\" aria-hidden=\"true\" />\n case \"warning\":\n default:\n return <WarningIcon weight=\"fill\" aria-hidden=\"true\" />\n }\n}\n\nfunction getSeverityPlateClass(severity: NormalizedSeverity): string {\n switch (severity) {\n case \"info\":\n return styles.plateInfo\n case \"destructive\":\n return styles.plateDestructive\n case \"warning\":\n default:\n return styles.plateWarning\n }\n}\n\nfunction getConfirmButtonVariant(\n severity: NormalizedSeverity\n): \"default\" | \"destructive\" {\n return severity === \"destructive\" ? \"destructive\" : \"default\"\n}\n\nfunction getDefaultConfirmLabel(severity: NormalizedSeverity): string {\n return severity === \"destructive\" ? \"Delete\" : \"Confirm\"\n}\n\nconst ConfirmDialog = React.forwardRef<\n React.ComponentRef<typeof DialogContent>,\n ConfirmDialogProps\n>(\n (\n {\n open: openProp,\n defaultOpen,\n onOpenChange,\n trigger,\n title,\n description,\n children,\n severity = \"warning\",\n confirmLabel,\n cancelLabel = \"Cancel\",\n confirmText,\n confirmTextLabel,\n onConfirm,\n onCancel,\n busy,\n className,\n },\n ref\n ) => {\n const isControlled = openProp !== undefined\n const [internalOpen, setInternalOpen] = React.useState<boolean>(\n defaultOpen ?? false\n )\n const actualOpen = isControlled ? (openProp as boolean) : internalOpen\n\n const [isPending, setIsPending] = React.useState<boolean>(false)\n const [typed, setTyped] = React.useState<string>(\"\")\n\n const cancelButtonRef = React.useRef<HTMLButtonElement | null>(null)\n\n const normalizedSeverity = normalizeSeverity(severity)\n\n const handleOpenChange = React.useCallback(\n (next: boolean) => {\n if (!isControlled) {\n setInternalOpen(next)\n }\n onOpenChange?.(next)\n if (!next) {\n // Clear gate typing on close\n setTyped(\"\")\n }\n },\n [isControlled, onOpenChange]\n )\n\n const resolvedConfirmLabel =\n confirmLabel ?? getDefaultConfirmLabel(normalizedSeverity)\n const effectiveBusy = busy ?? isPending\n const gateSatisfied =\n confirmText == null || confirmText.length === 0 || typed === confirmText\n const confirmDisabled = effectiveBusy || !gateSatisfied\n const cancelDisabled = effectiveBusy\n\n const handleConfirmClick = React.useCallback(async () => {\n if (!onConfirm) {\n handleOpenChange(false)\n return\n }\n // Any synchronous throw propagates naturally to React's error boundary.\n const result = onConfirm()\n if (result && typeof (result as Promise<void>).then === \"function\") {\n setIsPending(true)\n try {\n await result\n setIsPending(false)\n handleOpenChange(false)\n } catch (err) {\n // Async rejection — clear pending state, keep dialog open, re-throw\n // so the consumer's error handling (error boundary, onUnhandledRejection)\n // can react to it.\n setIsPending(false)\n throw err\n }\n } else {\n handleOpenChange(false)\n }\n }, [onConfirm, handleOpenChange])\n\n const handleCancelClick = React.useCallback(() => {\n onCancel?.()\n handleOpenChange(false)\n }, [onCancel, handleOpenChange])\n\n const handleOpenAutoFocus = React.useCallback(\n (event: Event) => {\n if (normalizedSeverity === \"destructive\" && cancelButtonRef.current) {\n event.preventDefault()\n cancelButtonRef.current.focus()\n }\n },\n [normalizedSeverity]\n )\n\n const severityIcon = getSeverityIcon(normalizedSeverity)\n const severityPlateClass = getSeverityPlateClass(normalizedSeverity)\n const confirmVariant = getConfirmButtonVariant(normalizedSeverity)\n\n // Generate stable id for confirm gate input\n const generatedId = React.useId()\n const confirmGateInputId = `${generatedId}-confirm-input`\n\n const hasDescriptionForAria =\n description != null && description !== false && !children\n\n return (\n <Dialog\n open={actualOpen}\n defaultOpen={defaultOpen}\n onOpenChange={handleOpenChange}\n >\n {trigger ? <DialogTrigger asChild>{trigger}</DialogTrigger> : null}\n <DialogContent\n ref={ref}\n data-slot=\"confirm-dialog\"\n data-severity={normalizedSeverity}\n className={cn(styles.root, className)}\n onOpenAutoFocus={handleOpenAutoFocus}\n >\n <DialogHeader>\n <div className={styles.headerStack}>\n <span\n data-slot=\"confirm-dialog-icon-plate\"\n aria-hidden=\"true\"\n className={cn(styles.iconPlate, severityPlateClass)}\n >\n {severityIcon}\n </span>\n <DialogTitle>{title}</DialogTitle>\n {hasDescriptionForAria ? (\n <DialogDescription>{description}</DialogDescription>\n ) : null}\n </div>\n </DialogHeader>\n\n {children ? (\n <div data-slot=\"confirm-dialog-body\" className={styles.body}>\n {children}\n </div>\n ) : null}\n\n {confirmText ? (\n <div className={styles.confirmGate}>\n <label\n htmlFor={confirmGateInputId}\n className={styles.confirmGateLabel}\n >\n {confirmTextLabel ?? `Type ${confirmText} to confirm`}\n </label>\n <input\n id={confirmGateInputId}\n type=\"text\"\n autoComplete=\"off\"\n spellCheck={false}\n value={typed}\n onChange={(e) => setTyped(e.target.value)}\n disabled={effectiveBusy}\n className={styles.confirmGateInput}\n data-slot=\"confirm-dialog-gate-input\"\n />\n </div>\n ) : null}\n\n <div\n data-slot=\"confirm-dialog-actions\"\n className={styles.actions}\n role=\"group\"\n >\n <div className={styles.action}>\n <Button\n ref={cancelButtonRef}\n type=\"button\"\n variant=\"outline\"\n onClick={handleCancelClick}\n disabled={cancelDisabled}\n data-slot=\"confirm-dialog-cancel\"\n >\n {cancelLabel}\n </Button>\n </div>\n <div className={styles.action}>\n <Button\n type=\"button\"\n variant={confirmVariant}\n onClick={handleConfirmClick}\n disabled={confirmDisabled}\n aria-busy={effectiveBusy || undefined}\n data-slot=\"confirm-dialog-confirm\"\n >\n {resolvedConfirmLabel}\n </Button>\n </div>\n </div>\n </DialogContent>\n </Dialog>\n )\n }\n)\nConfirmDialog.displayName = \"ConfirmDialog\"\n\nexport { ConfirmDialog }\n"
|
|
2329
|
+
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport {\n InfoIcon,\n WarningIcon,\n WarningOctagonIcon,\n} from \"@phosphor-icons/react\"\nimport { cn } from \"../../../lib/utils\"\nimport {\n Dialog,\n DialogContent,\n DialogDescription,\n DialogHeader,\n DialogTitle,\n DialogTrigger,\n} from \"../dialog/dialog\"\nimport { Button, type ButtonProps } from \"../button/button\"\nimport styles from \"./confirm-dialog.module.css\"\n\nexport type ConfirmDialogSeverity =\n | \"info\"\n | \"warning\"\n | \"destructive\"\n /**\n * @deprecated Use `\"destructive\"` instead. `\"danger\"` will be removed in the next major version.\n */\n | \"danger\"\n\n/** Internal normalized severity — `\"danger\"` is collapsed to `\"destructive\"`. */\ntype NormalizedSeverity = \"info\" | \"warning\" | \"destructive\"\n\nfunction normalizeSeverity(s: ConfirmDialogSeverity): NormalizedSeverity {\n return s === \"danger\" ? \"destructive\" : s\n}\n\n/** Severity icon visual treatment.\n *\n * Leave unset for the canonical default — a ~2.5rem tinted circular plate\n * (`surface-*-subtle` tint) stacked ABOVE the title. This is the unchanged\n * default rendering of every existing ConfirmDialog consumer.\n *\n * Set explicitly to opt into the blessed editorial treatments:\n * - `\"plated\"` — a 40px circular plate with a `color-mix` severity wash,\n * leading the title column (golden organization-management Feedback screen).\n * - `\"inline\"` — a small leading icon next to the title (no plate). */\nexport type ConfirmDialogIconTreatment = \"inline\" | \"plated\"\n\n/** Rendering mode. `dialog` (default) wraps content in Radix\n * Dialog/Portal/Overlay with an auto X-close. `inline` renders just the\n * content surface in normal flow — no portal, no overlay, no auto X-close —\n * so consumers can stack multiple dialogs inside one shared scrim. Opt-in. */\nexport type ConfirmDialogMode = \"dialog\" | \"inline\"\n\nexport interface ConfirmDialogProps {\n /** Controlled open state. */\n open?: boolean\n /** Uncontrolled initial open state. */\n defaultOpen?: boolean\n /** Called when open state changes. */\n onOpenChange?: (open: boolean) => void\n\n /** Optional trigger — wrapped in DialogTrigger. Omit for fully-controlled usage. */\n trigger?: React.ReactNode\n\n /** Dialog title — rendered in DialogTitle next to the severity icon. */\n title: React.ReactNode\n /** Short body above the action row. Used as DialogDescription. */\n description?: React.ReactNode\n /** Richer body slot — overrides description when provided. */\n children?: React.ReactNode\n\n /** Severity — drives icon color, confirm button variant. Default \"warning\". */\n severity?: ConfirmDialogSeverity\n\n /** Custom severity icon — overrides the severity default. Opt-in; omit to\n * use the built-in icon for the active severity. Sizing/color is handled by\n * the icon container, so pass a bare Phosphor icon (no explicit size). */\n icon?: React.ReactNode\n\n /** Severity-icon treatment. Leave unset for the canonical default (a tinted\n * circular plate stacked above the title). Set \"plated\" for the blessed\n * color-mix plate, or \"inline\" for a small leading icon next to the title.\n * Default rendering (unset) is unchanged. */\n iconTreatment?: ConfirmDialogIconTreatment\n\n /** Rendering mode. \"dialog\" (default) uses Radix Dialog + Portal + Overlay +\n * auto X-close. \"inline\" renders only the content surface (no portal, no\n * overlay, no X-close) so multiple dialogs can stack in one shared scrim.\n * Opt-in — default rendering is unchanged. */\n mode?: ConfirmDialogMode\n\n /** Confirm button label. Defaults: \"Delete\" for danger, \"Confirm\" otherwise. */\n confirmLabel?: React.ReactNode\n /** Cancel button label. Default \"Cancel\". */\n cancelLabel?: React.ReactNode\n /** Cancel button variant. Default \"outline\". Golden screens use \"ghost\". */\n cancelVariant?: ButtonProps[\"variant\"]\n\n /** If set, user must type this exact string to enable the confirm button. */\n confirmText?: string\n /** Label for the confirm-text input. Default: `Type ${confirmText} to confirm`. */\n confirmTextLabel?: React.ReactNode\n\n /** Confirm handler. Async-aware: returning a Promise puts the dialog into pending state. */\n onConfirm?: () => void | Promise<void>\n /** Cancel handler. */\n onCancel?: () => void\n\n /** Externally-controlled busy state — overrides internal async pending detection. */\n busy?: boolean\n\n /** Additional className on DialogContent. */\n className?: string\n}\n\nfunction getSeverityIcon(severity: NormalizedSeverity): React.ReactNode {\n switch (severity) {\n case \"info\":\n return <InfoIcon weight=\"fill\" aria-hidden=\"true\" />\n case \"destructive\":\n return <WarningOctagonIcon weight=\"fill\" aria-hidden=\"true\" />\n case \"warning\":\n default:\n return <WarningIcon weight=\"fill\" aria-hidden=\"true\" />\n }\n}\n\n/** Canonical default plate tint (surface-*-subtle stacked plate). */\nfunction getSeverityPlateClass(severity: NormalizedSeverity): string {\n switch (severity) {\n case \"info\":\n return styles.plateInfo\n case \"destructive\":\n return styles.plateDestructive\n case \"warning\":\n default:\n return styles.plateWarning\n }\n}\n\n/** Inline severity icon color (small leading icon, no plate). */\nfunction getSeverityIconClass(severity: NormalizedSeverity): string {\n switch (severity) {\n case \"info\":\n return styles.iconInfo\n case \"destructive\":\n return styles.iconDanger\n case \"warning\":\n default:\n return styles.iconWarning\n }\n}\n\n/** Blessed plated tint (color-mix wash). */\nfunction getPlatedIconClass(severity: NormalizedSeverity): string {\n switch (severity) {\n case \"info\":\n return styles.iconPlatedInfo\n case \"destructive\":\n return styles.iconPlatedDanger\n case \"warning\":\n default:\n return styles.iconPlatedWarning\n }\n}\n\nfunction getConfirmButtonVariant(\n severity: NormalizedSeverity\n): \"default\" | \"destructive\" {\n return severity === \"destructive\" ? \"destructive\" : \"default\"\n}\n\nfunction getDefaultConfirmLabel(severity: NormalizedSeverity): string {\n return severity === \"destructive\" ? \"Delete\" : \"Confirm\"\n}\n\nconst ConfirmDialog = React.forwardRef<\n React.ComponentRef<typeof DialogContent>,\n ConfirmDialogProps\n>(\n (\n {\n open: openProp,\n defaultOpen,\n onOpenChange,\n trigger,\n title,\n description,\n children,\n severity = \"warning\",\n icon,\n iconTreatment,\n mode = \"dialog\",\n confirmLabel,\n cancelLabel = \"Cancel\",\n cancelVariant = \"outline\",\n confirmText,\n confirmTextLabel,\n onConfirm,\n onCancel,\n busy,\n className,\n },\n ref\n ) => {\n const isControlled = openProp !== undefined\n const [internalOpen, setInternalOpen] = React.useState<boolean>(\n defaultOpen ?? false\n )\n const actualOpen = isControlled ? (openProp as boolean) : internalOpen\n\n const [isPending, setIsPending] = React.useState<boolean>(false)\n const [typed, setTyped] = React.useState<string>(\"\")\n\n const cancelButtonRef = React.useRef<HTMLButtonElement | null>(null)\n\n const normalizedSeverity = normalizeSeverity(severity)\n\n const handleOpenChange = React.useCallback(\n (next: boolean) => {\n if (!isControlled) {\n setInternalOpen(next)\n }\n onOpenChange?.(next)\n if (!next) {\n // Clear gate typing on close\n setTyped(\"\")\n }\n },\n [isControlled, onOpenChange]\n )\n\n const resolvedConfirmLabel =\n confirmLabel ?? getDefaultConfirmLabel(normalizedSeverity)\n const effectiveBusy = busy ?? isPending\n const gateSatisfied =\n confirmText == null || confirmText.length === 0 || typed === confirmText\n const confirmDisabled = effectiveBusy || !gateSatisfied\n const cancelDisabled = effectiveBusy\n\n const handleConfirmClick = React.useCallback(async () => {\n if (!onConfirm) {\n handleOpenChange(false)\n return\n }\n // Any synchronous throw propagates naturally to React's error boundary.\n const result = onConfirm()\n if (result && typeof (result as Promise<void>).then === \"function\") {\n setIsPending(true)\n try {\n await result\n setIsPending(false)\n handleOpenChange(false)\n } catch (err) {\n // Async rejection — clear pending state, keep dialog open, re-throw\n // so the consumer's error handling (error boundary, onUnhandledRejection)\n // can react to it.\n setIsPending(false)\n throw err\n }\n } else {\n handleOpenChange(false)\n }\n }, [onConfirm, handleOpenChange])\n\n const handleCancelClick = React.useCallback(() => {\n onCancel?.()\n handleOpenChange(false)\n }, [onCancel, handleOpenChange])\n\n const handleOpenAutoFocus = React.useCallback(\n (event: Event) => {\n if (normalizedSeverity === \"destructive\" && cancelButtonRef.current) {\n event.preventDefault()\n cancelButtonRef.current.focus()\n }\n },\n [normalizedSeverity]\n )\n\n const severityIcon = icon ?? getSeverityIcon(normalizedSeverity)\n const confirmVariant = getConfirmButtonVariant(normalizedSeverity)\n const isInline = mode === \"inline\"\n\n // Treatment resolution:\n // - unset → canonical default: tinted plate stacked above title\n // (data-slot=\"confirm-dialog-icon-plate\"). Unchanged.\n // - \"plated\" → blessed plated: 40px color-mix plate leading the\n // title column (data-slot=\"confirm-dialog-icon\").\n // - \"inline\" → blessed inline: small leading icon (no plate).\n const isCanonicalPlate = iconTreatment === undefined\n const isBlessedPlated = iconTreatment === \"plated\"\n\n // Generate stable id for confirm gate input\n const generatedId = React.useId()\n const confirmGateInputId = `${generatedId}-confirm-input`\n\n const hasDescriptionForAria =\n description != null && description !== false && !children\n\n // Canonical default plate — a span carrying the tinted circle, stacked\n // above the title inside .headerStack. Preserved verbatim for backward\n // compatibility (existing consumers + tests query this slot/classes).\n const canonicalPlate = (\n <span\n data-slot=\"confirm-dialog-icon-plate\"\n aria-hidden=\"true\"\n className={cn(styles.iconPlate, getSeverityPlateClass(normalizedSeverity))}\n >\n {severityIcon}\n </span>\n )\n\n // Blessed icon span — used for explicit \"plated\"/\"inline\" treatments.\n const blessedIconClass = isBlessedPlated\n ? cn(styles.iconPlated, getPlatedIconClass(normalizedSeverity))\n : cn(styles.icon, getSeverityIconClass(normalizedSeverity))\n const blessedIconSpan = (\n <span\n data-slot=\"confirm-dialog-icon\"\n aria-hidden=\"true\"\n className={blessedIconClass}\n >\n {severityIcon}\n </span>\n )\n\n // Header layout class per treatment.\n // - canonical default → .headerStack (column: plate above title/description)\n // - blessed plated → .titleRowPlated (column: plate above title)\n // - blessed inline → .titleRow (row: icon next to title)\n const headerLayoutClass = isCanonicalPlate\n ? styles.headerStack\n : isBlessedPlated\n ? styles.titleRowPlated\n : styles.titleRow\n\n const iconElement = isCanonicalPlate ? canonicalPlate : blessedIconSpan\n\n // Dialog-mode header — uses Radix Title/Description (a11y + aria-labelledby).\n // Canonical plate stacks plate + title + description inside one .headerStack;\n // blessed treatments keep description outside the title row.\n const dialogHeader = isCanonicalPlate ? (\n <DialogHeader>\n <div className={headerLayoutClass}>\n {iconElement}\n <DialogTitle>{title}</DialogTitle>\n {hasDescriptionForAria ? (\n <DialogDescription>{description}</DialogDescription>\n ) : null}\n </div>\n </DialogHeader>\n ) : (\n <DialogHeader>\n <div className={headerLayoutClass}>\n {iconElement}\n <DialogTitle>{title}</DialogTitle>\n </div>\n {hasDescriptionForAria ? (\n <DialogDescription>{description}</DialogDescription>\n ) : null}\n </DialogHeader>\n )\n\n // Inline-mode header — no Dialog context, so render a plain heading/paragraph.\n const inlineHeaderEl = (\n <div className={styles.inlineHeader}>\n <div className={headerLayoutClass}>\n {iconElement}\n <h2 className={styles.inlineTitle}>{title}</h2>\n </div>\n {hasDescriptionForAria ? (\n <p className={styles.inlineDescription}>{description}</p>\n ) : null}\n </div>\n )\n\n const header = isInline ? inlineHeaderEl : dialogHeader\n\n const innerContent = (\n <>\n {header}\n\n {children ? (\n <div data-slot=\"confirm-dialog-body\" className={styles.body}>\n {children}\n </div>\n ) : null}\n\n {confirmText ? (\n <div className={styles.confirmGate}>\n <label\n htmlFor={confirmGateInputId}\n className={styles.confirmGateLabel}\n >\n {confirmTextLabel ?? `Type ${confirmText} to confirm`}\n </label>\n <input\n id={confirmGateInputId}\n type=\"text\"\n autoComplete=\"off\"\n spellCheck={false}\n value={typed}\n onChange={(e) => setTyped(e.target.value)}\n disabled={effectiveBusy}\n className={styles.confirmGateInput}\n data-slot=\"confirm-dialog-gate-input\"\n />\n </div>\n ) : null}\n\n <div\n data-slot=\"confirm-dialog-actions\"\n className={styles.actions}\n role=\"group\"\n >\n <div className={styles.action}>\n <Button\n ref={cancelButtonRef}\n type=\"button\"\n variant={cancelVariant}\n onClick={handleCancelClick}\n disabled={cancelDisabled}\n data-slot=\"confirm-dialog-cancel\"\n >\n {cancelLabel}\n </Button>\n </div>\n <div className={styles.action}>\n <Button\n type=\"button\"\n variant={confirmVariant}\n onClick={handleConfirmClick}\n disabled={confirmDisabled}\n aria-busy={effectiveBusy || undefined}\n data-slot=\"confirm-dialog-confirm\"\n >\n {resolvedConfirmLabel}\n </Button>\n </div>\n </div>\n </>\n )\n\n // Inline (non-portal) mode — render only the content surface. No Radix\n // Dialog/Portal/Overlay/X-close, so consumers can stack multiple dialogs\n // inside one shared scrim. Open/close is consumer-driven; respect `open`.\n if (isInline) {\n if (!actualOpen) {\n return null\n }\n return (\n <div\n ref={ref as React.Ref<HTMLDivElement>}\n role=\"alertdialog\"\n aria-modal={false}\n data-slot=\"confirm-dialog\"\n data-mode=\"inline\"\n data-severity={normalizedSeverity}\n className={cn(styles.root, styles.inlineSurface, className)}\n >\n {innerContent}\n </div>\n )\n }\n\n return (\n <Dialog\n open={actualOpen}\n defaultOpen={defaultOpen}\n onOpenChange={handleOpenChange}\n >\n {trigger ? <DialogTrigger asChild>{trigger}</DialogTrigger> : null}\n <DialogContent\n ref={ref}\n data-slot=\"confirm-dialog\"\n data-severity={normalizedSeverity}\n className={cn(styles.root, className)}\n onOpenAutoFocus={handleOpenAutoFocus}\n >\n {innerContent}\n </DialogContent>\n </Dialog>\n )\n }\n)\nConfirmDialog.displayName = \"ConfirmDialog\"\n\nexport { ConfirmDialog }\n"
|
|
2206
2330
|
},
|
|
2207
2331
|
{
|
|
2208
2332
|
"path": "components/ui/confirm-dialog/confirm-dialog.module.css",
|
|
2209
2333
|
"type": "registry:ui",
|
|
2210
|
-
"content": "/* ConfirmDialog — destructive-action confirmation compound wrapping Dialog. */\n\n.root {\n width: 100%;\n min-width: 0;\n container-type: inline-size;\n container-name: confirm-dialog;\n}\n\n/* Header layout — plate stacked above title/description */\n.headerStack {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: var(--spacing-3, 0.75rem);\n}\n\n/* Tinted circular icon plate (~2.5rem) stacked above the dialog title */\n.iconPlate {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n width: var(--spacing-10, 2.5rem);\n height: var(--spacing-10, 2.5rem);\n border-radius: 9999px;\n}\n\n.iconPlate svg {\n width: var(--spacing-5, 1.25rem);\n height: var(--spacing-5, 1.25rem);\n}\n\n.plateInfo {\n background: var(--surface-info-subtle, #f0f9ff);\n color: var(--text-info, #0369a1);\n}\n\n.plateWarning {\n background: var(--surface-warning-subtle, #fffbeb);\n color: var(--text-warning, #b45309);\n}\n\n.plateDestructive {\n background: var(--surface-error-subtle, #fef2f2);\n color: var(--text-error, #b91c1c);\n}\n\n/*
|
|
2334
|
+
"content": "/* ConfirmDialog — destructive-action confirmation compound wrapping Dialog. */\n\n.root {\n width: 100%;\n min-width: 0;\n container-type: inline-size;\n container-name: confirm-dialog;\n}\n\n/* ── Canonical default treatment (iconTreatment unset) ─────────────────────\n * A tinted circular plate (~2.5rem, surface-*-subtle wash) stacked above the\n * title/description. This is the unchanged default rendering. */\n\n/* Header layout — plate stacked above title/description */\n.headerStack {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: var(--spacing-3, 0.75rem);\n}\n\n/* Tinted circular icon plate (~2.5rem) stacked above the dialog title */\n.iconPlate {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n width: var(--spacing-10, 2.5rem);\n height: var(--spacing-10, 2.5rem);\n border-radius: 9999px;\n}\n\n.iconPlate svg {\n width: var(--spacing-5, 1.25rem);\n height: var(--spacing-5, 1.25rem);\n}\n\n.plateInfo {\n background: var(--surface-info-subtle, #f0f9ff);\n color: var(--text-info, #0369a1);\n}\n\n.plateWarning {\n background: var(--surface-warning-subtle, #fffbeb);\n color: var(--text-warning, #b45309);\n}\n\n.plateDestructive {\n background: var(--surface-error-subtle, #fef2f2);\n color: var(--text-error, #b91c1c);\n}\n\n/* ── Blessed editorial treatments (iconTreatment=\"inline\" | \"plated\") ───────\n * Brought in from the blessed admin build's confirm-dialog. Opt-in via the\n * iconTreatment prop; the canonical default (above) is unchanged. */\n\n/* Title row with leading severity icon — blessed inline treatment. */\n.titleRow {\n display: flex;\n align-items: center;\n gap: var(--spacing-2, 0.5rem);\n}\n\n/* Plated layout — opt-in via iconTreatment=\"plated\". Stacks a tinted circular\n * icon container ABOVE the title (golden organization-management Feedback\n * screen). */\n.titleRowPlated {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: var(--spacing-3, 0.75rem);\n}\n\n/* Plated severity icon container — ~40px tinted circle. */\n.iconPlated {\n width: var(--confirm-dialog-plate-size, 40px);\n height: var(--confirm-dialog-plate-size, 40px);\n border-radius: var(--radius-full, 999px);\n display: grid;\n place-items: center;\n flex-shrink: 0;\n}\n\n.iconPlated svg {\n width: var(--confirm-dialog-plate-icon-size, 22px);\n height: var(--confirm-dialog-plate-icon-size, 22px);\n}\n\n/* Plated severity tints — bg uses a tinted wash of the severity token;\n * foreground uses the solid severity token. Matches prototype-overlay\n * the prototype's warning / destructive icon plates exactly. */\n.iconPlatedInfo {\n background: color-mix(in srgb, var(--accent, #0369a1) 18%, transparent);\n color: var(--accent, #0369a1);\n}\n\n.iconPlatedWarning {\n background: color-mix(in srgb, var(--warning, #b45309) 22%, transparent);\n color: var(--warning, #b45309);\n}\n\n.iconPlatedDanger {\n background: color-mix(in srgb, var(--destructive, #b91c1c) 18%, transparent);\n color: var(--destructive, #b91c1c);\n}\n\n/* Inline severity icon — sized via svg to respect Phosphor sizing contract.\n * Also serves as the legacy `.iconInfo/.iconWarning/.iconDanger` color hook for\n * theme overrides targeting the old class names. */\n.icon {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n line-height: 1;\n}\n\n.icon svg {\n width: var(--spacing-5, 1.25rem);\n height: var(--spacing-5, 1.25rem);\n}\n\n.iconInfo {\n color: var(--text-info, #0369a1);\n}\n\n.iconWarning {\n color: var(--text-warning, #b45309);\n}\n\n.iconDanger {\n color: var(--text-error, #b91c1c);\n}\n\n/* Body slot — description paragraph or richer children */\n.body {\n margin-top: var(--spacing-2, 0.5rem);\n font-size: var(--font-size-sm, 0.875rem);\n color: var(--text-secondary, #6b7280);\n line-height: var(--line-height-relaxed, 1.6);\n}\n\n.body > :first-child {\n margin-top: 0;\n}\n\n.body > :last-child {\n margin-bottom: 0;\n}\n\n/* Confirm-text gate — labeled text input between body and actions */\n.confirmGate {\n display: flex;\n flex-direction: column;\n gap: var(--spacing-1, 0.25rem);\n margin-top: var(--spacing-4, 1rem);\n}\n\n.confirmGateLabel {\n font-size: var(--font-size-sm, 0.875rem);\n color: var(--text-primary, #111827);\n font-weight: var(--font-weight-medium, 500);\n}\n\n.confirmGateInput {\n width: 100%;\n padding: var(--spacing-2, 0.5rem) var(--spacing-3, 0.75rem);\n border-radius: var(--radius-md, 0.5rem);\n border: 1px solid var(--border-default, #e5e7eb);\n background-color: var(--surface-page, #ffffff);\n color: var(--text-primary, #111827);\n font-size: var(--font-size-sm, 0.875rem);\n font-family: inherit;\n line-height: var(--line-height-normal, 1.5);\n transition: border-color var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out);\n}\n\n.confirmGateInput:focus-visible {\n outline: var(--focus-ring-width, 2px) solid var(--border-focus, currentColor);\n outline-offset: var(--focus-ring-offset, 2px);\n border-color: var(--border-focus, currentColor);\n}\n\n.confirmGateInput:disabled {\n opacity: 0.6;\n cursor: not-allowed;\n}\n\n/* Action row — cancel + confirm, right-aligned on wide, stacked on narrow */\n.actions {\n display: flex;\n flex-direction: row;\n justify-content: flex-end;\n align-items: center;\n gap: var(--spacing-2, 0.5rem);\n margin-top: var(--spacing-6, 1.5rem);\n}\n\n.action {\n display: inline-flex;\n}\n\n/* Inline (non-portal) surface — opt-in via mode=\"inline\". Renders the dialog\n * content card in normal flow WITHOUT Radix DialogPortal/DialogOverlay/auto\n * X-close, so consumers can stack multiple dialogs inside one shared scrim.\n * Mirrors the dialog content card visuals (surface/radius/padding/shadow) but\n * uses static positioning instead of fixed centering.\n * Default: 32rem max-width, radius-lg, surface-card, spacing-6, shadow-xl.\n * Density axis: editorial uses 480px, radius-xl, surface-card, spacing-8, shadow-lg. */\n.inlineSurface {\n position: relative;\n width: 100%;\n max-width: 32rem;\n border-radius: var(--radius-lg, 0.5rem);\n background-color: var(--surface-card, var(--surface-page, #ffffff));\n padding: var(--spacing-6, 1.5rem);\n box-shadow: var(--shadow-xl);\n}\n\n/* Editorial density: the inline confirm card carries the editorial modal\n metrics (card surface, 480px, radius-xl, spacing-8, shadow-lg) — verified\n against the blessed user-states render. */\n:global([data-density=\"editorial\"]) .inlineSurface {\n background-color: var(--surface-card);\n max-width: 480px;\n border-radius: var(--radius-xl);\n padding: var(--spacing-8);\n box-shadow: var(--shadow-lg);\n}\n\n/* Inline header — replaces DialogHeader (which depends on the dialog stylesheet). */\n.inlineHeader {\n display: flex;\n flex-direction: column;\n gap: calc(var(--spacing-1, 0.25rem) * 1.5);\n margin-bottom: var(--spacing-4, 1rem);\n}\n\n/* Inline title — replaces Radix DialogTitle (no Dialog context in inline mode).\n * Default: font-size-lg / semibold. Density axis: editorial uses font-size-2xl / 700. */\n.inlineTitle {\n font-size: var(--font-size-lg, 1.125rem);\n font-weight: var(--font-weight-semibold, 600);\n color: var(--text-primary, #111827);\n margin: 0;\n}\n\n:global([data-density=\"editorial\"]) .inlineTitle {\n font-size: var(--font-size-2xl);\n font-weight: 700;\n}\n\n/* Inline description — replaces Radix DialogDescription.\n * Default: text-secondary. Density axis: editorial uses text-tertiary. */\n.inlineDescription {\n font-size: var(--font-size-sm, 0.875rem);\n color: var(--text-secondary, #6b7280);\n margin: 0;\n}\n\n:global([data-density=\"editorial\"]) .inlineDescription {\n color: var(--text-tertiary);\n}\n\n/* Container query: stack actions vertically on narrow dialog widths */\n@container confirm-dialog (max-width: 360px) {\n .actions {\n flex-direction: column-reverse;\n align-items: stretch;\n width: 100%;\n }\n\n .actions .action {\n width: 100%;\n }\n\n .actions .action > * {\n width: 100%;\n justify-content: center;\n }\n}\n"
|
|
2211
2335
|
}
|
|
2212
2336
|
]
|
|
2213
2337
|
},
|
|
@@ -2235,12 +2359,12 @@
|
|
|
2235
2359
|
{
|
|
2236
2360
|
"path": "components/ui/data-table/data-table.tsx",
|
|
2237
2361
|
"type": "registry:ui",
|
|
2238
|
-
"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 /**\n * Vertical row padding step. Maps to a `data-density` attribute on the root\n * which drives the `--dt-row-py` custom property the table's `<td>` cells\n * consume. Themes can override per-density values without forking the\n * component — see `data-table.module.css`.\n *\n * - `\"compact\"` — 8px (sub-content density: long lists, narrow viewports)\n * - `\"default\"` — 12px (current behaviour; no visual regression for existing\n * consumers)\n * - `\"editorial\"` — 20px (generous; each row reads as a card; high-design\n * admin patterns); column headers also render uppercase, `--font-size-xs`,\n * `--text-tertiary`, `letter-spacing: 0.08em`\n */\n density?: \"compact\" | \"default\" | \"editorial\"\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 density = \"default\",\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 data-density={density}\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} className={styles.sortBar}>\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"
|
|
2362
|
+
"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 /**\n * Opt-in per-column skeleton shapes for the loading state (VI-516). When\n * supplied, each skeleton row's cell contents are produced by this render\n * prop instead of the default uniform full-width bars — letting the caller\n * mirror the real row's silhouette (logo plates, badge pills, two-line id\n * stacks) for a calmer load. Return the cell *contents*; DataTable still owns\n * the `<tr>`/`<td>` structure, key, and `data-slot`. `colIndex` walks the\n * resolved column list (the injected selection column, when present, is\n * index 0). Return `null`/`undefined` to leave a cell empty.\n *\n * Default (prop omitted) is unchanged: every cell renders a full-width\n * `Skeleton` bar, exactly as today. Pair with the `Skeleton` shape classes\n * from `skeleton.module.css` (`shapeLogo`, `shapePill`, `shapeCircle`).\n */\n loadingSkeletonCell?: (args: {\n rowIndex: number\n colIndex: number\n columnId: string\n }) => React.ReactNode\n\n // Layout\n stickyHeader?: boolean\n\n /**\n * Vertical row padding step. Maps to a `data-density` attribute on the root\n * which drives the `--dt-row-py` custom property the table's `<td>` cells\n * consume. Themes can override per-density values without forking the\n * component — see `data-table.module.css`.\n *\n * - `\"compact\"` — 8px (sub-content density: long lists, narrow viewports)\n * - `\"default\"` — 12px (current behaviour; no visual regression for existing\n * consumers)\n * - `\"editorial\"` — 20px (generous; each row reads as a card; high-design\n * admin patterns); column headers also render uppercase, `--font-size-xs`,\n * `--text-tertiary`, `letter-spacing: 0.08em`\n */\n density?: \"compact\" | \"default\" | \"editorial\"\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 loadingSkeletonCell,\n stickyHeader = false,\n rowTone,\n onRowClick,\n density = \"default\",\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 data-density={density}\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} className={styles.sortBar}>\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 {loadingSkeletonCell ? (\n loadingSkeletonCell({\n rowIndex: rowIdx,\n colIndex: colIdx,\n columnId: col.id ?? String(colIdx),\n })\n ) : (\n <Skeleton className={styles.skeletonCell} />\n )}\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"
|
|
2239
2363
|
},
|
|
2240
2364
|
{
|
|
2241
2365
|
"path": "components/ui/data-table/data-table.module.css",
|
|
2242
2366
|
"type": "registry:ui",
|
|
2243
|
-
"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/* Density — vertical row padding step driven by `data-density` on the root.\n Cells consume `--dt-row-py` via the `.root td` rule below. Horizontal cell\n padding stays at TableCell's `padding: var(--spacing-3)` shorthand cascade\n — we only override the top/bottom longhand properties here, so `default`\n density renders identically to pre-VI-425 markup.\n\n Themes can override per-density values via their own selectors. Example:\n [data-theme=\"entr\"] [data-density=\"editorial\"] { --dt-row-py: var(--spacing-6); } */\n.root[data-density=\"compact\"] { --dt-row-py: var(--spacing-2, 0.5rem); }\n.root[data-density=\"default\"] { --dt-row-py: var(--spacing-3, 0.75rem); }\n.root[data-density=\"editorial\"] { --dt-row-py: var(--spacing-5, 1.25rem); }\n\n/* Editorial column-header treatment — uppercase, xs scale, tertiary color,\n tracked type (0.08em). Anchored on .root to avoid a bare attribute selector\n (Turbopack pure-selector rule). Overrides the TableHead default styles and\n the sort-button font-weight rule below. */\n.root[data-density=\"editorial\"] th {\n font-size: var(--font-size-xs, 0.6875rem);\n font-weight: var(--font-weight-medium, 500);\n color: var(--text-tertiary, #6b7280);\n letter-spacing: 0.08em;\n text-transform: uppercase;\n}\n\n/* The sort button inherits font from its <th>, but explicitly overrides\n font-weight to semibold — reset to medium for the editorial treatment. */\n.root[data-density=\"editorial\"] .sortButton {\n font-weight: var(--font-weight-medium, 500);\n}\n\n.root td {\n padding-top: var(--dt-row-py, var(--spacing-3, 0.75rem));\n padding-bottom: var(--dt-row-py, var(--spacing-3, 0.75rem));\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-bar row — the thead <tr> that houses the column headers.\n `--data-table-sort-bar-radius` controls the top-corner radius of the first\n and last <th> cells. Defaults to the container's border-radius so the sort\n bar visually follows the table card's rounded top corners.\n\n Set `--data-table-sort-bar-radius: 0` on a parent scope for the\n borderless-admin pattern — straight corners without touching the container\n itself. */\n.sortBar th:first-child {\n border-top-left-radius: var(--data-table-sort-bar-radius, var(--radius-lg, 0.5rem));\n}\n\n.sortBar th:last-child {\n border-top-right-radius: var(--data-table-sort-bar-radius, var(--radius-lg, 0.5rem));\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"
|
|
2367
|
+
"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/* Density axis — vertical row padding step driven by `data-density` on the\n root (or any ancestor). The three tiers map the full density spectrum:\n compact (sub-content lists), default (backward-compatible baseline), and\n editorial (generous card-like rows; high-design admin patterns).\n\n Cells consume `--dt-row-py` via the `.root td` rule below. Horizontal cell\n padding stays at TableCell's `padding: var(--spacing-3)` shorthand cascade\n — we only override the top/bottom longhand properties here, so `default`\n density renders identically to pre-density-axis markup.\n\n Themes can override per-density values via their own selectors. Example:\n [data-theme=\"entr\"] [data-density=\"editorial\"] { --dt-row-py: var(--spacing-6); } */\n.root[data-density=\"compact\"] { --dt-row-py: var(--spacing-2, 0.5rem); }\n.root[data-density=\"default\"] { --dt-row-py: var(--spacing-3, 0.75rem); }\n.root[data-density=\"editorial\"] { --dt-row-py: var(--spacing-5, 1.25rem); }\n\n/* Editorial surface treatment — bakes the editorial-admin baseline values into\n the density axis. The var() reads keep --dt-* hooks working for per-theme\n overrides; the fallbacks resolve to the editorial values when no hook is set.\n Container goes flush (radius 0, no shadow); header and rows use card/page\n surface tiers instead of transparent. */\n.root[data-density=\"editorial\"] .stickyHeader,\n.root[data-density=\"editorial\"] [data-slot=\"table-container\"] {\n border-radius: var(--dt-container-radius, 0);\n box-shadow: var(--dt-container-shadow, none);\n}\n.root[data-density=\"editorial\"] [data-slot=\"table-head\"] {\n background-color: var(--dt-header-bg, var(--surface-card));\n}\n.root[data-density=\"editorial\"] [data-slot=\"table-cell\"] {\n background-color: var(--dt-row-bg, color-mix(in srgb, var(--surface-page) 75%, var(--surface-card) 25%));\n}\n\n/* Editorial column-header treatment — uppercase, 11px, tertiary color,\n tracked type (0.08em). Anchored on .root to avoid a bare attribute selector\n (Turbopack pure-selector rule). Overrides the TableHead default styles and\n the sort-button font-weight rule below. */\n.root[data-density=\"editorial\"] th {\n font-size: var(--dt-header-font-size, 11px);\n font-weight: var(--font-weight-medium, 500);\n color: var(--text-tertiary, #6b7280);\n letter-spacing: 0.08em;\n text-transform: uppercase;\n}\n\n/* Editorial density widens horizontal cell padding (spacing-5 / 20px) to match\n the editorial-admin baseline; vertical stays driven by --dt-row-py. The\n --dt-cell-px hook lets a theme tune it; default equals the editorial value. */\n.root[data-density=\"editorial\"] td,\n.root[data-density=\"editorial\"] th {\n padding-left: var(--dt-cell-px, var(--spacing-5, 1.25rem));\n padding-right: var(--dt-cell-px, var(--spacing-5, 1.25rem));\n}\n\n/* The sort button inherits font from its <th>, but explicitly overrides\n font-weight to semibold — reset to medium for the editorial treatment. */\n.root[data-density=\"editorial\"] .sortButton {\n font-weight: var(--font-weight-medium, 500);\n}\n\n.root td {\n padding-top: var(--dt-row-py, var(--spacing-3, 0.75rem));\n padding-bottom: var(--dt-row-py, var(--spacing-3, 0.75rem));\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-bar row — the thead <tr> that houses the column headers.\n `--data-table-sort-bar-radius` controls the top-corner radius of the first\n and last <th> cells. Defaults to the container's border-radius so the sort\n bar visually follows the table card's rounded top corners.\n\n Set `--data-table-sort-bar-radius: 0` on a parent scope for the\n borderless-admin pattern — straight corners without touching the container\n itself. */\n.sortBar th:first-child {\n border-top-left-radius: var(--data-table-sort-bar-radius, var(--radius-lg, 0.5rem));\n}\n\n.sortBar th:last-child {\n border-top-right-radius: var(--data-table-sort-bar-radius, var(--radius-lg, 0.5rem));\n}\n\n/* Editorial density: the flush admin shell has straight header corners — the\n sort-bar radius defaults to 0 (still overridable via the hook). */\n.root[data-density=\"editorial\"] .sortBar th:first-child {\n border-top-left-radius: var(--data-table-sort-bar-radius, 0);\n}\n\n.root[data-density=\"editorial\"] .sortBar th:last-child {\n border-top-right-radius: var(--data-table-sort-bar-radius, 0);\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"
|
|
2244
2368
|
}
|
|
2245
2369
|
]
|
|
2246
2370
|
},
|
|
@@ -2260,12 +2384,12 @@
|
|
|
2260
2384
|
{
|
|
2261
2385
|
"path": "components/ui/empty-state/empty-state.tsx",
|
|
2262
2386
|
"type": "registry:ui",
|
|
2263
|
-
"content": "import * as React from \"react\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\nimport { cn } from \"../../../lib/utils\"\nimport styles from \"./empty-state.module.css\"\n\nconst emptyStateVariants = cva(styles.base, {\n variants: {\n size: {\n sm: styles.sizeSm,\n md: styles.sizeMd,\n lg: styles.sizeLg,\n },\n tone: {\n default: styles.toneDefault,\n subtle: styles.toneSubtle,\n },\n },\n defaultVariants: {\n size: \"md\",\n tone: \"default\",\n },\n})\n\ntype HeadingElement = \"h2\" | \"h3\" | \"h4\"\n\nexport interface EmptyStateProps\n extends React.HTMLAttributes<HTMLDivElement>,\n VariantProps<typeof emptyStateVariants> {\n /** Optional leading visual — typically a Phosphor icon. */\n icon?: React.ReactNode\n /** Short direct statement of the empty condition. Required. */\n heading: React.ReactNode\n /** Optional 1-2 sentence explanation or guidance. */\n description?: React.ReactNode\n /** Primary CTA slot — typically a Button. */\n action?: React.ReactNode\n /** De-emphasized fallback action — typically a Button. */\n secondaryAction?: React.ReactNode\n /** Heading level for the heading slot. Defaults to `h3`. */\n headingAs?: HeadingElement\n}\n\nconst EmptyState = React.forwardRef<HTMLDivElement, EmptyStateProps>(\n (\n {\n className,\n size,\n tone,\n icon,\n heading,\n description,\n action,\n secondaryAction,\n headingAs = \"h3\",\n ...props\n },\n ref\n ) => {\n const Heading = headingAs as React.ElementType\n const hasBothActions = Boolean(action) && Boolean(secondaryAction)\n const hasAnyAction = Boolean(action) || Boolean(secondaryAction)\n\n return (\n <div\n ref={ref}\n role=\"status\"\n data-slot=\"empty-state\"\n data-tone={tone ?? \"default\"}\n className={cn(emptyStateVariants({ size, tone }), className)}\n {...props}\n >\n {icon ? (\n <div\n data-slot=\"empty-state-icon\"\n className={styles.icon}\n aria-hidden=\"true\"\n >\n {icon}\n </div>\n ) : null}\n <Heading data-slot=\"empty-state-heading\" className={styles.heading}>\n {heading}\n </Heading>\n {description ? (\n <p\n data-slot=\"empty-state-description\"\n className={styles.description}\n >\n {description}\n </p>\n ) : null}\n {hasAnyAction ? (\n <div\n data-slot=\"empty-state-actions\"\n className={styles.actions}\n {...(hasBothActions ? { role: \"group\" } : {})}\n >\n {action ? (\n <div\n data-slot=\"empty-state-action\"\n className={styles.action}\n >\n {action}\n </div>\n ) : null}\n {secondaryAction ? (\n <div\n data-slot=\"empty-state-secondary-action\"\n className={styles.secondaryAction}\n >\n {secondaryAction}\n </div>\n ) : null}\n </div>\n ) : null}\n </div>\n )\n }\n)\nEmptyState.displayName = \"EmptyState\"\n\nexport { EmptyState, emptyStateVariants }\n"
|
|
2387
|
+
"content": "import * as React from \"react\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\nimport { cn } from \"../../../lib/utils\"\nimport styles from \"./empty-state.module.css\"\n\nconst emptyStateVariants = cva(styles.base, {\n variants: {\n size: {\n sm: styles.sizeSm,\n md: styles.sizeMd,\n lg: styles.sizeLg,\n },\n tone: {\n default: styles.toneDefault,\n subtle: styles.toneSubtle,\n },\n /**\n * Surface treatment.\n * - `default` — base placeholder (dashed/borderless per `tone`).\n * - `editorial` — filled card on `--surface-card` with a 64px circular\n * icon chip; an admin-editorial composition. Opt-in; overrides the\n * `tone` surface and `size` icon/heading scale.\n */\n variant: {\n default: undefined,\n editorial: styles.editorial,\n },\n },\n defaultVariants: {\n size: \"md\",\n tone: \"default\",\n variant: \"default\",\n },\n})\n\ntype HeadingElement = \"h2\" | \"h3\" | \"h4\"\n\nexport interface EmptyStateProps\n extends React.HTMLAttributes<HTMLDivElement>,\n VariantProps<typeof emptyStateVariants> {\n /** Optional leading visual — typically a Phosphor icon. */\n icon?: React.ReactNode\n /** Short direct statement of the empty condition. Required. */\n heading: React.ReactNode\n /** Optional 1-2 sentence explanation or guidance. */\n description?: React.ReactNode\n /** Primary CTA slot — typically a Button. */\n action?: React.ReactNode\n /** De-emphasized fallback action — typically a Button. */\n secondaryAction?: React.ReactNode\n /** Heading level for the heading slot. Defaults to `h3`. */\n headingAs?: HeadingElement\n}\n\nconst EmptyState = React.forwardRef<HTMLDivElement, EmptyStateProps>(\n (\n {\n className,\n size,\n tone,\n variant,\n icon,\n heading,\n description,\n action,\n secondaryAction,\n headingAs = \"h3\",\n ...props\n },\n ref\n ) => {\n const Heading = headingAs as React.ElementType\n const hasBothActions = Boolean(action) && Boolean(secondaryAction)\n const hasAnyAction = Boolean(action) || Boolean(secondaryAction)\n\n return (\n <div\n ref={ref}\n role=\"status\"\n data-slot=\"empty-state\"\n data-tone={tone ?? \"default\"}\n data-variant={variant ?? \"default\"}\n className={cn(emptyStateVariants({ size, tone, variant }), className)}\n {...props}\n >\n {icon ? (\n <div\n data-slot=\"empty-state-icon\"\n className={styles.icon}\n aria-hidden=\"true\"\n >\n {icon}\n </div>\n ) : null}\n <Heading data-slot=\"empty-state-heading\" className={styles.heading}>\n {heading}\n </Heading>\n {description ? (\n <p\n data-slot=\"empty-state-description\"\n className={styles.description}\n >\n {description}\n </p>\n ) : null}\n {hasAnyAction ? (\n <div\n data-slot=\"empty-state-actions\"\n className={styles.actions}\n {...(hasBothActions ? { role: \"group\" } : {})}\n >\n {action ? (\n <div\n data-slot=\"empty-state-action\"\n className={styles.action}\n >\n {action}\n </div>\n ) : null}\n {secondaryAction ? (\n <div\n data-slot=\"empty-state-secondary-action\"\n className={styles.secondaryAction}\n >\n {secondaryAction}\n </div>\n ) : null}\n </div>\n ) : null}\n </div>\n )\n }\n)\nEmptyState.displayName = \"EmptyState\"\n\nexport { EmptyState, emptyStateVariants }\n"
|
|
2264
2388
|
},
|
|
2265
2389
|
{
|
|
2266
2390
|
"path": "components/ui/empty-state/empty-state.module.css",
|
|
2267
2391
|
"type": "registry:ui",
|
|
2268
|
-
"content": "/* Empty State base: centered placeholder for empty lists, tables, regions */\n.base {\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n text-align: center;\n width: 100%;\n min-width: 0;\n gap: var(--spacing-2, 0.5rem);\n color: var(--text-secondary, #6b7280);\n border-radius: var(--radius-lg, 0.75rem);\n container-type: inline-size;\n container-name: empty-state;\n}\n\n/* Size variants — padding, gap, icon/heading scale */\n.sizeSm {\n padding: var(--spacing-4, 1rem) var(--spacing-4, 1rem);\n gap: var(--spacing-1, 0.25rem);\n}\n\n.sizeMd {\n padding: var(--spacing-8, 2rem) var(--spacing-5, 1.25rem);\n gap: var(--spacing-2, 0.5rem);\n}\n\n.sizeLg {\n padding: var(--spacing-12, 3rem) var(--spacing-6, 1.5rem);\n gap: var(--spacing-3, 0.75rem);\n}\n\n/* Tone: default — dashed border card on muted surface */\n.toneDefault {\n background-color: var(--surface-muted, #f9fafb);\n border: 1px dashed var(--border-default, #e5e7eb);\n}\n\n/* Tone: subtle — borderless, transparent */\n.toneSubtle {\n background-color: transparent;\n border: 0;\n}\n\n/* Icon slot — leading visual above the heading */\n.icon {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n color: var(--text-tertiary, #6b7280);\n line-height: 1;\n margin-bottom: var(--spacing-1, 0.25rem);\n}\n\n.sizeSm .icon {\n font-size: var(--font-size-xl, 1.25rem);\n}\n\n.sizeSm .icon svg {\n width: var(--spacing-5, 1.25rem);\n height: var(--spacing-5, 1.25rem);\n}\n\n.sizeMd .icon {\n font-size: var(--font-size-3xl, 1.875rem);\n}\n\n.sizeMd .icon svg {\n width: var(--spacing-8, 2rem);\n height: var(--spacing-8, 2rem);\n}\n\n.sizeLg .icon {\n font-size: var(--font-size-4xl, 2.25rem);\n}\n\n.sizeLg .icon svg {\n width: var(--spacing-12, 3rem);\n height: var(--spacing-12, 3rem);\n}\n\n/* Heading — short direct statement */\n.heading {\n margin: 0;\n font-family: var(--font-family-heading, inherit);\n font-weight: var(--font-weight-semibold, 600);\n line-height: var(--line-height-tight, 1.25);\n color: var(--text-primary, #111827);\n letter-spacing: var(--letter-spacing-tight, -0.01em);\n}\n\n.sizeSm .heading {\n font-size: var(--font-size-sm, 0.875rem);\n}\n\n.sizeMd .heading {\n font-size: var(--font-size-base, 1rem);\n}\n\n.sizeLg .heading {\n font-size: var(--font-size-lg, 1.125rem);\n}\n\n/* Description — supporting guidance */\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: 48ch;\n}\n\n.sizeSm .description {\n font-size: var(--font-size-xs, 0.75rem);\n}\n\n.sizeLg .description {\n font-size: var(--font-size-base, 1rem);\n}\n\n/* Action cluster — wraps primary + secondary actions */\n.actions {\n display: flex;\n align-items: center;\n justify-content: center;\n gap: var(--spacing-2, 0.5rem);\n flex-wrap: wrap;\n margin-top: var(--spacing-3, 0.75rem);\n}\n\n.sizeSm .actions {\n margin-top: var(--spacing-2, 0.5rem);\n}\n\n.sizeLg .actions {\n margin-top: var(--spacing-4, 1rem);\n}\n\n.action {\n display: inline-flex;\n}\n\n.secondaryAction {\n display: inline-flex;\n}\n\n/* Container query: stack actions vertically on very narrow regions */\n@container empty-state (max-width: 320px) {\n .actions {\n flex-direction: column;\n align-items: stretch;\n width: 100%;\n }\n\n .action,\n .secondaryAction {\n width: 100%;\n justify-content: center;\n }\n}\n"
|
|
2392
|
+
"content": "/* Empty State base: centered placeholder for empty lists, tables, regions */\n.base {\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n text-align: center;\n width: 100%;\n min-width: 0;\n gap: var(--spacing-2, 0.5rem);\n color: var(--text-secondary, #6b7280);\n border-radius: var(--radius-lg, 0.75rem);\n container-type: inline-size;\n container-name: empty-state;\n}\n\n/* Size variants — padding, gap, icon/heading scale */\n.sizeSm {\n padding: var(--spacing-4, 1rem) var(--spacing-4, 1rem);\n gap: var(--spacing-1, 0.25rem);\n}\n\n.sizeMd {\n padding: var(--spacing-8, 2rem) var(--spacing-5, 1.25rem);\n gap: var(--spacing-2, 0.5rem);\n}\n\n.sizeLg {\n padding: var(--spacing-12, 3rem) var(--spacing-6, 1.5rem);\n gap: var(--spacing-3, 0.75rem);\n}\n\n/* Tone: default — dashed border card on muted surface */\n.toneDefault {\n background-color: var(--surface-muted, #f9fafb);\n border: 1px dashed var(--border-default, #e5e7eb);\n}\n\n/* Tone: subtle — borderless, transparent */\n.toneSubtle {\n background-color: transparent;\n border: 0;\n}\n\n/* Icon slot — leading visual above the heading */\n.icon {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n color: var(--text-tertiary, #6b7280);\n line-height: 1;\n margin-bottom: var(--spacing-1, 0.25rem);\n}\n\n.sizeSm .icon {\n font-size: var(--font-size-xl, 1.25rem);\n}\n\n.sizeSm .icon svg {\n width: var(--spacing-5, 1.25rem);\n height: var(--spacing-5, 1.25rem);\n}\n\n.sizeMd .icon {\n font-size: var(--font-size-3xl, 1.875rem);\n}\n\n.sizeMd .icon svg {\n width: var(--spacing-8, 2rem);\n height: var(--spacing-8, 2rem);\n}\n\n.sizeLg .icon {\n font-size: var(--font-size-4xl, 2.25rem);\n}\n\n.sizeLg .icon svg {\n width: var(--spacing-12, 3rem);\n height: var(--spacing-12, 3rem);\n}\n\n/* Heading — short direct statement */\n.heading {\n margin: 0;\n font-family: var(--font-family-heading, inherit);\n font-weight: var(--font-weight-semibold, 600);\n line-height: var(--line-height-tight, 1.25);\n color: var(--text-primary, #111827);\n letter-spacing: var(--letter-spacing-tight, -0.01em);\n}\n\n.sizeSm .heading {\n font-size: var(--font-size-sm, 0.875rem);\n}\n\n.sizeMd .heading {\n font-size: var(--font-size-base, 1rem);\n}\n\n.sizeLg .heading {\n font-size: var(--font-size-lg, 1.125rem);\n}\n\n/* Description — supporting guidance */\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: 48ch;\n}\n\n.sizeSm .description {\n font-size: var(--font-size-xs, 0.75rem);\n}\n\n.sizeLg .description {\n font-size: var(--font-size-base, 1rem);\n}\n\n/* Action cluster — wraps primary + secondary actions */\n.actions {\n display: flex;\n align-items: center;\n justify-content: center;\n gap: var(--spacing-2, 0.5rem);\n flex-wrap: wrap;\n margin-top: var(--spacing-3, 0.75rem);\n}\n\n.sizeSm .actions {\n margin-top: var(--spacing-2, 0.5rem);\n}\n\n.sizeLg .actions {\n margin-top: var(--spacing-4, 1rem);\n}\n\n.action {\n display: inline-flex;\n}\n\n.secondaryAction {\n display: inline-flex;\n}\n\n/* Container query: stack actions vertically on very narrow regions */\n@container empty-state (max-width: 320px) {\n .actions {\n flex-direction: column;\n align-items: stretch;\n width: 100%;\n }\n\n .action,\n .secondaryAction {\n width: 100%;\n justify-content: center;\n }\n}\n\n/* ─────────────────────────────────────────────────────────────\n * Variant: editorial — filled card with a circular icon chip.\n * Opt-in admin-editorial composition. Self-contained: overrides the\n * `tone` surface and the `size` icon/heading scale. Declared after the\n * size rules so its equal-specificity child overrides win by source order.\n * ───────────────────────────────────────────────────────────── */\n.editorial {\n background-color: var(--surface-card, #ffffff);\n border: 0;\n border-radius: var(--radius-lg, 0.75rem);\n padding: var(--spacing-16, 4rem) var(--spacing-8, 2rem);\n gap: var(--spacing-3, 0.75rem);\n}\n\n/* 64px circular icon chip */\n.editorial .icon {\n width: 64px;\n height: 64px;\n border-radius: var(--radius-full, 999px);\n background-color: var(--surface-subtle, #f5f5f6);\n color: var(--text-tertiary, #6b7280);\n font-size: 28px;\n margin-bottom: var(--spacing-3, 0.75rem);\n}\n\n.editorial .icon svg {\n width: 28px;\n height: 28px;\n}\n\n.editorial .heading {\n font-size: var(--font-size-xl, 1.25rem);\n}\n\n.editorial .description {\n font-size: var(--font-size-sm, 0.875rem);\n color: var(--text-tertiary, #6b7280);\n max-width: 360px;\n}\n\n.editorial .actions {\n margin-top: var(--spacing-3, 0.75rem);\n}\n"
|
|
2269
2393
|
}
|
|
2270
2394
|
]
|
|
2271
2395
|
},
|
|
@@ -2338,12 +2462,12 @@
|
|
|
2338
2462
|
{
|
|
2339
2463
|
"path": "components/ui/page-header/page-header.tsx",
|
|
2340
2464
|
"type": "registry:ui",
|
|
2341
|
-
"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 * Title line-height override. Forwarded as a raw CSS `line-height` value\n * on the `--page-header-title-leading` custom property (a unitless number\n * like `1.05` or any CSS length). When omitted, the title falls back to its\n * default line-height (tight for the standard title, `1` for the marquee\n * scale), so existing call sites render unchanged.\n */\n
|
|
2465
|
+
"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 /**\n * Optional media/identity node rendered to the LEFT of the title block inside\n * the title row (typically an Avatar or identity plate). When present the row\n * forms an identity lockup and the slot top-aligns against the title/description\n * stack (VI-539). Omitting it leaves the row exactly as the default\n * text|actions layout, so existing call sites render unchanged.\n */\n leading?: 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 * Title line-height override. Forwarded as a raw CSS `line-height` value\n * on the `--page-header-title-leading` custom property (a unitless number\n * like `1.05` or any CSS length). When omitted, the title falls back to its\n * default line-height (tight for the standard title, `1` for the marquee\n * scale), so existing call sites render unchanged.\n */\n titleLeading?: string | number\n}\n\nconst PageHeader = React.forwardRef<HTMLElement, PageHeaderProps>(\n (\n {\n className,\n size,\n eyebrow,\n title,\n description,\n breadcrumb,\n leading,\n actions,\n as = \"header\",\n titleAs = \"h1\",\n titleSize,\n titleFamily,\n titleLeading,\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 hasTitleLeading = titleLeading !== undefined && titleLeading !== null\n\n const titleStyle: React.CSSProperties | undefined =\n rawTitleSize || rawTitleFamily || hasTitleLeading\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 ...(hasTitleLeading\n ? ({\n \"--page-header-title-leading\": String(titleLeading),\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\n data-slot=\"page-header-row\"\n data-has-leading={leading ? \"\" : undefined}\n className={styles.row}\n >\n {leading ? (\n <div data-slot=\"page-header-leading\" className={styles.leading}>\n {leading}\n </div>\n ) : null}\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"
|
|
2342
2466
|
},
|
|
2343
2467
|
{
|
|
2344
2468
|
"path": "components/ui/page-header/page-header.module.css",
|
|
2345
2469
|
"type": "registry:ui",
|
|
2346
|
-
"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-leading` is intentionally left unset here so the\n title falls back to its per-rule default line-height (tight for the\n standard title, `1` for the marquee scale); the `leading` prop sets it. */\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(--page-header-title-leading, 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: var(--page-header-title-leading, 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"
|
|
2470
|
+
"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-leading` is intentionally left unset here so the\n title falls back to its per-rule default line-height (tight for the\n standard title, `1` for the marquee scale); the `leading` prop sets it. */\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/* When a leading media/identity slot is present, the row forms an identity\n lockup: the media sits before the text stack a tight gap from it. Scoped to\n `data-has-leading` so the default (text|actions) row is untouched for\n consumers that don't pass `leading`. The expanding text block keeps actions\n pinned right via the inherited `justify-content: space-between`. */\n.row[data-has-leading] {\n align-items: center;\n gap: var(--page-header-leading-gap, var(--spacing-3, 0.75rem));\n}\n\n/* Leading media/identity slot: sits before the text block, vertically\n centered against the title/description stack, never shrinks. (Reverts the\n VI-539 top-align, which assumed a different slot — the blessed admin\n identity lockup centers the media. Superseded per the admin-editorial\n reconcile plan.) */\n.leading {\n display: flex;\n align-items: center;\n flex: 0 0 auto;\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(--page-header-title-leading, 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: var(--page-header-title-leading, 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"
|
|
2347
2471
|
}
|
|
2348
2472
|
]
|
|
2349
2473
|
},
|
|
@@ -2396,6 +2520,30 @@
|
|
|
2396
2520
|
}
|
|
2397
2521
|
]
|
|
2398
2522
|
},
|
|
2523
|
+
{
|
|
2524
|
+
"name": "section-intro",
|
|
2525
|
+
"type": "registry:ui",
|
|
2526
|
+
"description": "Marketing section opener with a mono uppercase eyebrow, display-font heading, and optional lede paragraph. Eyebrow color tracks a live-rewritten CSS custom property for runtime brand-accent binding.",
|
|
2527
|
+
"category": "general",
|
|
2528
|
+
"dependencies": [
|
|
2529
|
+
"@loworbitstudio/visor-core"
|
|
2530
|
+
],
|
|
2531
|
+
"registryDependencies": [
|
|
2532
|
+
"utils"
|
|
2533
|
+
],
|
|
2534
|
+
"files": [
|
|
2535
|
+
{
|
|
2536
|
+
"path": "components/ui/section-intro/section-intro.tsx",
|
|
2537
|
+
"type": "registry:ui",
|
|
2538
|
+
"content": "import * as React from \"react\"\nimport { cn } from \"../../../lib/utils\"\nimport styles from \"./section-intro.module.css\"\n\ntype HeadingElement = \"h1\" | \"h2\" | \"h3\"\n\nexport interface SectionIntroProps\n extends Omit<React.HTMLAttributes<HTMLElement>, \"children\"> {\n /**\n * Short mono uppercase eyebrow rendered above the heading.\n * Reads its color from `--section-intro-eyebrow-color`, which defaults to\n * `--accent-default`. Consumers can override this at any ancestor level so\n * the color tracks a live-rewritten CSS var (e.g. a keyed brand accent).\n */\n eyebrow?: React.ReactNode\n /** Display-font heading — the main marketing statement. */\n heading: React.ReactNode\n /** Optional supporting paragraph rendered beneath the heading. */\n lede?: React.ReactNode\n /**\n * Text alignment for all three slots.\n * @default \"left\"\n */\n align?: \"left\" | \"center\"\n /**\n * Heading level for the `heading` slot.\n * @default \"h2\"\n */\n headingAs?: HeadingElement\n /** Root element tag. Defaults to `header`. */\n as?: \"header\" | \"div\" | \"section\"\n}\n\nconst SectionIntro = React.forwardRef<HTMLElement, SectionIntroProps>(\n (\n {\n className,\n eyebrow,\n heading,\n lede,\n align = \"left\",\n headingAs = \"h2\",\n as = \"header\",\n ...props\n },\n ref\n ) => {\n const Root = as as React.ElementType\n const Heading = headingAs as React.ElementType\n\n return (\n <Root\n ref={ref}\n data-slot=\"section-intro\"\n data-align={align}\n className={cn(styles.root, className)}\n {...props}\n >\n {eyebrow ? (\n <p data-slot=\"section-intro-eyebrow\" className={styles.eyebrow}>\n {eyebrow}\n </p>\n ) : null}\n <Heading\n data-slot=\"section-intro-heading\"\n className={styles.heading}\n >\n {heading}\n </Heading>\n {lede ? (\n <p data-slot=\"section-intro-lede\" className={styles.lede}>\n {lede}\n </p>\n ) : null}\n </Root>\n )\n }\n)\nSectionIntro.displayName = \"SectionIntro\"\n\nexport { SectionIntro }\n"
|
|
2539
|
+
},
|
|
2540
|
+
{
|
|
2541
|
+
"path": "components/ui/section-intro/section-intro.module.css",
|
|
2542
|
+
"type": "registry:ui",
|
|
2543
|
+
"content": "/* SectionIntro — marketing eyebrow + display heading + optional lede\n *\n * The eyebrow color is intentionally driven through a CSS custom property\n * (`--section-intro-eyebrow-color`) so that consumers can override it at\n * any ancestor level and have it track a live-rewritten brand accent var\n * (for example a keyed `--color-acid` that swaps between artist palette entries).\n * Default fallback chain: --section-intro-eyebrow-color → --accent-default\n * → --interactive-primary-bg → #111827 (Tailwind Gray-900).\n *\n * Alignment is controlled by data-align on the root (set by the `align`\n * prop). Both \"left\" and \"center\" are defined here.\n */\n\n.root {\n display: flex;\n flex-direction: column;\n gap: var(--spacing-4, 1rem);\n width: 100%;\n min-width: 0;\n}\n\n/* ── Alignment variants ─────────────────────────────────────────────────── */\n\n.root[data-align=\"left\"] {\n align-items: flex-start;\n text-align: left;\n}\n\n.root[data-align=\"center\"] {\n align-items: center;\n text-align: center;\n}\n\n/* ── Eyebrow ────────────────────────────────────────────────────────────── */\n\n.eyebrow {\n margin: 0;\n font-family: var(--font-mono, ui-monospace, monospace);\n font-size: var(--font-size-xs, 0.6875rem);\n font-weight: var(--font-weight-medium, 500);\n letter-spacing: var(--letter-spacing-widest, 0.2em);\n text-transform: uppercase;\n line-height: var(--line-height-tight, 1.25);\n /*\n * Live-rewriteable eyebrow color. Override --section-intro-eyebrow-color\n * at any ancestor (for example on the <section> that contains this component) to\n * track a keyed brand accent that changes at runtime.\n */\n color: var(\n --section-intro-eyebrow-color,\n var(--accent-default, var(--interactive-primary-bg, #111827))\n );\n}\n\n/* ── Heading ────────────────────────────────────────────────────────────── */\n\n.heading {\n margin: 0;\n font-family: var(--font-display, var(--font-family-heading, inherit));\n font-size: clamp(2.5rem, 6vw, 4rem);\n font-weight: var(--font-weight-medium, 500);\n line-height: var(--line-height-none, 1.05);\n letter-spacing: var(--letter-spacing-tight, -0.02em);\n color: var(--text-primary, #111827);\n text-wrap: balance;\n max-width: 18ch;\n}\n\n.root[data-align=\"center\"] .heading {\n max-width: 22ch;\n}\n\n/* ── Lede ───────────────────────────────────────────────────────────────── */\n\n.lede {\n margin: 0;\n font-size: var(--font-size-base, 1rem);\n line-height: var(--line-height-relaxed, 1.6);\n font-weight: var(--font-weight-light, 300);\n color: var(--text-secondary, #6b7280);\n max-width: 54ch;\n}\n\n.root[data-align=\"center\"] .lede {\n max-width: 60ch;\n}\n"
|
|
2544
|
+
}
|
|
2545
|
+
]
|
|
2546
|
+
},
|
|
2399
2547
|
{
|
|
2400
2548
|
"name": "section-nav",
|
|
2401
2549
|
"type": "registry:ui",
|
|
@@ -2413,12 +2561,12 @@
|
|
|
2413
2561
|
{
|
|
2414
2562
|
"path": "components/ui/section-nav/section-nav.tsx",
|
|
2415
2563
|
"type": "registry:ui",
|
|
2416
|
-
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport { Slot, Slottable } from \"@radix-ui/react-slot\"\nimport type { Icon } from \"@phosphor-icons/react\"\nimport { cn } from \"../../../lib/utils\"\nimport styles from \"./section-nav.module.css\"\n\n// ─── SectionNav (root) ───────────────────────────────────────────────────────\n\nconst SectionNav = React.forwardRef<HTMLElement, React.ComponentProps<\"nav\">>(\n ({ className, \"aria-label\": ariaLabel, ...props }, ref) => (\n <nav\n ref={ref}\n aria-label={ariaLabel ?? \"section\"}\n data-slot=\"section-nav\"\n className={cn(styles.root, className)}\n {...props}\n />\n )\n)\nSectionNav.displayName = \"SectionNav\"\n\n// ─── SectionNavItem ──────────────────────────────────────────────────────────\n\nexport interface SectionNavItemProps extends React.ComponentProps<\"a\"> {\n /**\n * When true, merge the item's chrome onto the immediate child element instead\n * of rendering an `<a>`. Use with `next/link` for client-side navigation:\n * `<SectionNavItem asChild isActive label=\"Members\"><Link href=\"/members\" /></SectionNavItem>`.\n */\n asChild?: boolean\n /** Marks the item as the current section — text-primary, 2px primary underline, primary-tinted count pill. */\n isActive?: boolean\n /** Leading Phosphor icon component (e.g. `UsersIcon`). */\n icon?: Icon\n /** Item label text. */\n label: React.ReactNode\n /** Optional trailing count pill value. `0` is rendered; `undefined`/`null` hides the pill. */\n count?: number\n}\n\nconst SectionNavItem = React.forwardRef<HTMLAnchorElement, SectionNavItemProps>(\n (\n { className, asChild, isActive, icon
|
|
2564
|
+
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport { Slot, Slottable } from \"@radix-ui/react-slot\"\nimport type { Icon } from \"@phosphor-icons/react\"\nimport { cn } from \"../../../lib/utils\"\nimport styles from \"./section-nav.module.css\"\n\n// ─── SectionNav (root) ───────────────────────────────────────────────────────\n\nconst SectionNav = React.forwardRef<HTMLElement, React.ComponentProps<\"nav\">>(\n ({ className, \"aria-label\": ariaLabel, ...props }, ref) => (\n <nav\n ref={ref}\n aria-label={ariaLabel ?? \"section\"}\n data-slot=\"section-nav\"\n className={cn(styles.root, className)}\n {...props}\n />\n )\n)\nSectionNav.displayName = \"SectionNav\"\n\n// ─── SectionNavItem ──────────────────────────────────────────────────────────\n\nexport interface SectionNavItemProps extends React.ComponentProps<\"a\"> {\n /**\n * When true, merge the item's chrome onto the immediate child element instead\n * of rendering an `<a>`. Use with `next/link` for client-side navigation:\n * `<SectionNavItem asChild isActive label=\"Members\"><Link href=\"/members\" /></SectionNavItem>`.\n */\n asChild?: boolean\n /** Marks the item as the current section — text-primary, 2px primary underline, primary-tinted count pill. */\n isActive?: boolean\n /** Blessed-API alias for `isActive` (the admin reference builds pass `active`). */\n active?: boolean\n /**\n * Leading icon, accepted in two forms:\n * - a Phosphor icon **component** (e.g. `icon={UsersIcon}`) — rendered as\n * `<Icon className={styles.icon} weight=\"regular\" />`, the canonical form;\n * - a rendered **element** (e.g. `icon={<Users size={16} weight=\"bold\" />}`) —\n * rendered as-is so its own `size`/`weight`/props are preserved, wrapped in\n * the `styles.icon` slot span.\n */\n icon?: Icon | React.ReactNode\n /** Item label text. */\n label: React.ReactNode\n /** Optional trailing count pill value. `0` is rendered; `undefined`/`null` hides the pill. */\n count?: number\n}\n\nconst SectionNavItem = React.forwardRef<HTMLAnchorElement, SectionNavItemProps>(\n (\n { className, asChild, isActive, active, icon, label, count, children, ...props },\n ref\n ) => {\n const Comp = asChild ? Slot : \"a\"\n const isCurrent = isActive ?? active ?? false\n const showCount = count != null\n\n // `icon` accepts both a Phosphor component (`icon={Users}`) and a rendered\n // element (`icon={<Users size={16} weight=\"bold\" />}`). Branch on\n // React.isValidElement: render an element as-is (preserving its own props),\n // wrapped in the icon slot span; render a component type via the canonical\n // `<Icon className={styles.icon} weight=\"regular\" />` form.\n let iconNode: React.ReactNode = null\n if (React.isValidElement(icon)) {\n iconNode = (\n <span className={styles.icon} aria-hidden=\"true\">\n {icon}\n </span>\n )\n } else if (icon) {\n // A component *type* — a function or a forwardRef/memo object (Phosphor\n // icons are forwardRef objects, so `typeof` is \"object\", not \"function\").\n const IconComp = icon as Icon\n iconNode = (\n <IconComp className={styles.icon} weight=\"regular\" aria-hidden=\"true\" />\n )\n }\n\n const chrome = (\n <>\n {iconNode}\n <span className={styles.label}>{label}</span>\n {showCount && (\n <span\n className={cn(\n styles.count,\n isCurrent ? styles.countActive : styles.countNeutral\n )}\n >\n {count}\n </span>\n )}\n </>\n )\n\n return (\n <Comp\n ref={ref}\n data-slot=\"section-nav-item\"\n data-active={isCurrent || undefined}\n aria-current={isCurrent ? \"page\" : undefined}\n className={cn(styles.item, isCurrent && styles.itemActive, className)}\n {...props}\n >\n {chrome}\n <Slottable>{children}</Slottable>\n </Comp>\n )\n }\n)\nSectionNavItem.displayName = \"SectionNavItem\"\n\nexport { SectionNav, SectionNavItem }\n"
|
|
2417
2565
|
},
|
|
2418
2566
|
{
|
|
2419
2567
|
"path": "components/ui/section-nav/section-nav.module.css",
|
|
2420
2568
|
"type": "registry:ui",
|
|
2421
|
-
"content": "/* SectionNav — link/anchor-based section sub-navigation.\n *\n * A horizontal strip of links: leading Phosphor icon + label + optional\n * trailing count pill. The active item gets text-primary, a static 2px\n * primary underline, and a primary-tinted count pill.\n *\n * Distinct from Tabs: items navigate via href (next/link asChild), there are\n * no content panels, and the underline is static (not an animated indicator).\n *\n * Theme-agnostic: all values reference CSS custom property tokens.\n */\n\n/* Root — the nav strip with a hairline baseline the active underline sits on. */\n.root {\n display: flex;\n align-items: stretch;\n gap: var(--spacing-
|
|
2569
|
+
"content": "/* SectionNav — link/anchor-based section sub-navigation.\n *\n * A horizontal strip of links: leading Phosphor icon + label + optional\n * trailing count pill. The active item gets text-primary, a static 2px\n * primary underline, and a primary-tinted count pill.\n *\n * Distinct from Tabs: items navigate via href (next/link asChild), there are\n * no content panels, and the underline is static (not an animated indicator).\n *\n * Theme-agnostic: all values reference CSS custom property tokens.\n */\n\n/* Root — the nav strip with a hairline baseline the active underline sits on. */\n.root {\n display: flex;\n align-items: stretch;\n gap: var(--spacing-2, 0.5rem);\n border-bottom: var(--stroke-width-thin, 1px) solid\n var(--hairline, var(--border-default, #e5e7eb));\n /* No overflow-x: a scroll container would clip the items' -1px overlap of\n this hairline, shaving the bottom pixel off the active underline. */\n}\n\n/* Item — a single link: icon + label + optional count pill. */\n.item {\n position: relative;\n display: inline-flex;\n align-items: center;\n gap: var(--spacing-2, 0.5rem);\n padding: var(--spacing-3, 0.75rem) var(--spacing-4, 1rem);\n font-size: var(--font-size-sm, 0.875rem);\n font-weight: var(--font-weight-medium, 500);\n line-height: 1;\n white-space: nowrap;\n color: var(--text-tertiary, #9ca3af);\n text-decoration: none;\n cursor: pointer;\n /* Reserve the 2px underline so resting/active items don't shift vertically. */\n border-bottom: 2px solid transparent;\n margin-bottom: calc(-1 * var(--stroke-width-thin, 1px));\n background: transparent;\n transition: color var(--motion-duration-fast, 150ms)\n var(--motion-easing-standard, ease),\n border-color var(--motion-duration-fast, 150ms)\n var(--motion-easing-standard, ease);\n}\n\n.item:hover {\n color: var(--text-primary, #111827);\n}\n\n.item:focus-visible {\n outline: var(--focus-ring-width, 2px) solid var(--border-focus, currentColor);\n outline-offset: var(--focus-ring-offset, 2px);\n border-radius: var(--radius-sm, 0.25rem);\n}\n\n/* Active — text-primary + 2px primary underline. */\n.itemActive {\n color: var(--text-primary, #111827);\n border-bottom-color: var(--primary, #2563eb);\n}\n\n/* Leading icon — tertiary at rest, recolors with the active item. */\n.icon {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n color: var(--text-tertiary, #9ca3af);\n}\n\n.itemActive .icon {\n color: var(--text-primary, #111827);\n}\n\n/* Label text. */\n.label {\n min-width: 0;\n}\n\n/* Trailing count pill. */\n.count {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n border-radius: var(--radius-full, 9999px);\n padding: var(--stroke-width-thin, 1px) calc(var(--spacing-1, 0.25rem) * 1.5);\n font-size: 11px;\n font-weight: var(--font-weight-medium, 500);\n font-variant-numeric: tabular-nums;\n line-height: normal;\n}\n\n/* Neutral (resting) count pill — subtle surface, tertiary text. */\n.countNeutral {\n background-color: var(--surface-subtle, #f3f4f6);\n color: var(--text-tertiary, #9ca3af);\n}\n\n/* Primary-tinted count pill — active item. */\n.countActive {\n background-color: color-mix(in srgb, var(--primary, #2563eb) 22%, transparent);\n color: var(--primary, #2563eb);\n}\n"
|
|
2422
2570
|
}
|
|
2423
2571
|
]
|
|
2424
2572
|
},
|
|
@@ -2463,12 +2611,12 @@
|
|
|
2463
2611
|
{
|
|
2464
2612
|
"path": "components/ui/score-indicator/score-indicator.tsx",
|
|
2465
2613
|
"type": "registry:ui",
|
|
2466
|
-
"content": "import * as React from \"react\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\nimport { WarningCircle, Warning } from \"@phosphor-icons/react\"\nimport { cn } from \"../../../lib/utils\"\nimport styles from \"./score-indicator.module.css\"\n\nconst scoreIndicatorVariants = cva(styles.base, {\n variants: {\n size: {\n sm: styles.sizeSm,\n md: styles.sizeMd,\n lg: styles.sizeLg,\n },\n denominator: {\n none: styles.denominatorNone,\n trailing: styles.denominatorTrailing,\n below: styles.denominatorBelow,\n },\n },\n defaultVariants: {\n size: \"md\",\n denominator: \"trailing\",\n },\n})\n\nconst TONE_CLASS: Record<ResolvedTone, string> = {\n success: styles.toneSuccess,\n warning: styles.toneWarning,\n destructive: styles.toneDestructive,\n info: styles.toneInfo,\n neutral: styles.toneNeutral,\n}\n\nexport type ScoreIndicatorTone =\n | \"auto\"\n | \"success\"\n | \"warning\"\n | \"destructive\"\n | \"info\"\n | \"neutral\"\n\nexport type ResolvedTone = Exclude<ScoreIndicatorTone, \"auto\">\n\nexport interface ScoreIndicatorProps\n extends Omit<React.HTMLAttributes<HTMLSpanElement>, \"children\">,\n VariantProps<typeof scoreIndicatorVariants> {\n /** Current value. */\n value: number\n /** Maximum value the score can reach. @default 100 */\n max?: number\n /** Visual size. @default \"md\" */\n size?: \"sm\" | \"md\" | \"lg\"\n /** Color treatment. @default \"auto\" — derives from value/max ratio */\n tone?: ScoreIndicatorTone\n /** Optional label for accessibility. Defaults to \"{value} out of {max}\". */\n ariaLabel?: string\n /** Where to show the denominator. @default \"trailing\" */\n denominator?: \"none\" | \"trailing\" | \"below\"\n /** Custom format for the displayed value. Defaults to rounded integer. */\n format?: (value: number, max: number) => string\n}\n\nconst SIZE_RING_PX: Record<NonNullable<ScoreIndicatorProps[\"size\"]>, number> = {\n sm: 24,\n md: 36,\n lg: 56,\n}\n\nconst SIZE_STROKE_PX: Record<NonNullable<ScoreIndicatorProps[\"size\"]>, number> = {\n sm: 2.5,\n md: 3.5,\n lg: 5,\n}\n\nconst defaultFormat = (value: number, _max: number): string =>\n String(Math.round(value))\n\nexport function deriveAutoTone(ratio: number): ResolvedTone {\n if (ratio >= 0.85) return \"success\"\n if (ratio >= 0.6) return \"info\"\n if (ratio >= 0.4) return \"warning\"\n return \"destructive\"\n}\n\nconst ScoreIndicator = React.forwardRef<HTMLSpanElement, ScoreIndicatorProps>(\n (\n {\n className,\n value,\n max = 100,\n size = \"md\",\n tone = \"auto\",\n ariaLabel,\n denominator = \"trailing\",\n format,\n ...props\n },\n ref\n ) => {\n const safeMax = max > 0 ? max : 100\n const clamped = Math.min(Math.max(value, 0), safeMax)\n const ratio = clamped / safeMax\n const resolvedTone: ResolvedTone =\n tone === \"auto\" ? deriveAutoTone(ratio) : tone\n\n const ringPx = SIZE_RING_PX[size]\n const strokePx = SIZE_STROKE_PX[size]\n const viewBox = 100\n const center = viewBox / 2\n const radius = center - (strokePx / 2) * (viewBox / ringPx)\n const circumference = 2 * Math.PI * radius\n const dashOffset = circumference * (1 - ratio)\n\n const formatted = (format ?? defaultFormat)(value, safeMax)\n const denominatorText = `/ ${defaultFormat(safeMax, safeMax)}`\n const computedAriaLabel = ariaLabel ?? `${value} out of ${safeMax}`\n\n return (\n <span\n ref={ref}\n data-slot=\"score-indicator\"\n data-size={size}\n data-tone={resolvedTone}\n data-denominator={denominator}\n className={cn(\n scoreIndicatorVariants({ size, denominator }),\n TONE_CLASS[resolvedTone],\n className\n )}\n {...props}\n >\n <span\n data-slot=\"score-indicator-ring\"\n role=\"img\"\n aria-label={computedAriaLabel}\n className={styles.ring}\n style={{ width: ringPx, height: ringPx }}\n >\n <svg\n
|
|
2614
|
+
"content": "import * as React from \"react\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\nimport { WarningCircle, Warning } from \"@phosphor-icons/react\"\nimport { cn } from \"../../../lib/utils\"\nimport styles from \"./score-indicator.module.css\"\n\nconst scoreIndicatorVariants = cva(styles.base, {\n variants: {\n variant: {\n ring: styles.variantRing,\n solid: styles.variantSolid,\n },\n size: {\n sm: styles.sizeSm,\n md: styles.sizeMd,\n lg: styles.sizeLg,\n },\n denominator: {\n none: styles.denominatorNone,\n trailing: styles.denominatorTrailing,\n below: styles.denominatorBelow,\n },\n },\n defaultVariants: {\n variant: \"ring\",\n size: \"md\",\n denominator: \"trailing\",\n },\n})\n\nconst TONE_CLASS: Record<ResolvedTone, string> = {\n success: styles.toneSuccess,\n warning: styles.toneWarning,\n destructive: styles.toneDestructive,\n info: styles.toneInfo,\n neutral: styles.toneNeutral,\n}\n\nexport type ScoreIndicatorTone =\n | \"auto\"\n | \"success\"\n | \"warning\"\n | \"destructive\"\n | \"info\"\n | \"neutral\"\n\nexport type ResolvedTone = Exclude<ScoreIndicatorTone, \"auto\">\n\nexport interface ScoreIndicatorProps\n extends Omit<React.HTMLAttributes<HTMLSpanElement>, \"children\">,\n VariantProps<typeof scoreIndicatorVariants> {\n /** Current value. */\n value: number\n /** Maximum value the score can reach. @default 100 */\n max?: number\n /**\n * Display treatment. `\"ring\"` draws the SVG track + progress arc (default).\n * `\"solid\"` renders a flat filled tinted disc with no ring — the editorial\n * health-score look. @default \"ring\"\n */\n variant?: \"ring\" | \"solid\"\n /** Visual size. @default \"md\" */\n size?: \"sm\" | \"md\" | \"lg\"\n /** Color treatment. @default \"auto\" — derives from value/max ratio */\n tone?: ScoreIndicatorTone\n /** Optional label for accessibility. Defaults to \"{value} out of {max}\". */\n ariaLabel?: string\n /** Where to show the denominator. @default \"trailing\" */\n denominator?: \"none\" | \"trailing\" | \"below\"\n /** Custom format for the displayed value. Defaults to rounded integer. */\n format?: (value: number, max: number) => string\n}\n\nconst SIZE_RING_PX: Record<NonNullable<ScoreIndicatorProps[\"size\"]>, number> = {\n sm: 24,\n md: 36,\n lg: 56,\n}\n\nconst SIZE_STROKE_PX: Record<NonNullable<ScoreIndicatorProps[\"size\"]>, number> = {\n sm: 2.5,\n md: 3.5,\n lg: 5,\n}\n\nconst defaultFormat = (value: number, _max: number): string =>\n String(Math.round(value))\n\nexport function deriveAutoTone(ratio: number): ResolvedTone {\n if (ratio >= 0.85) return \"success\"\n if (ratio >= 0.6) return \"info\"\n if (ratio >= 0.4) return \"warning\"\n return \"destructive\"\n}\n\nconst ScoreIndicator = React.forwardRef<HTMLSpanElement, ScoreIndicatorProps>(\n (\n {\n className,\n value,\n max = 100,\n variant = \"ring\",\n size = \"md\",\n tone = \"auto\",\n ariaLabel,\n denominator = \"trailing\",\n format,\n ...props\n },\n ref\n ) => {\n const safeMax = max > 0 ? max : 100\n const clamped = Math.min(Math.max(value, 0), safeMax)\n const ratio = clamped / safeMax\n const resolvedTone: ResolvedTone =\n tone === \"auto\" ? deriveAutoTone(ratio) : tone\n\n const ringPx = SIZE_RING_PX[size]\n const strokePx = SIZE_STROKE_PX[size]\n const viewBox = 100\n const center = viewBox / 2\n const radius = center - (strokePx / 2) * (viewBox / ringPx)\n const circumference = 2 * Math.PI * radius\n const dashOffset = circumference * (1 - ratio)\n\n const formatted = (format ?? defaultFormat)(value, safeMax)\n const denominatorText = `/ ${defaultFormat(safeMax, safeMax)}`\n const computedAriaLabel = ariaLabel ?? `${value} out of ${safeMax}`\n\n const isSolid = variant === \"solid\"\n\n return (\n <span\n ref={ref}\n data-slot=\"score-indicator\"\n data-variant={variant}\n data-size={size}\n data-tone={resolvedTone}\n data-denominator={denominator}\n className={cn(\n scoreIndicatorVariants({ variant, size, denominator }),\n TONE_CLASS[resolvedTone],\n className\n )}\n {...props}\n >\n <span\n data-slot=\"score-indicator-ring\"\n role=\"img\"\n aria-label={computedAriaLabel}\n className={styles.ring}\n style={{ width: ringPx, height: ringPx }}\n >\n {isSolid ? null : (\n <svg\n className={styles.svg}\n viewBox={`0 0 ${viewBox} ${viewBox}`}\n aria-hidden=\"true\"\n focusable=\"false\"\n >\n <circle\n className={styles.track}\n cx={center}\n cy={center}\n r={radius}\n fill=\"none\"\n strokeWidth={strokePx * (viewBox / ringPx)}\n />\n <circle\n className={styles.indicator}\n cx={center}\n cy={center}\n r={radius}\n fill=\"none\"\n strokeWidth={strokePx * (viewBox / ringPx)}\n strokeDasharray={circumference}\n strokeDashoffset={dashOffset}\n strokeLinecap=\"round\"\n transform={`rotate(-90 ${center} ${center})`}\n />\n </svg>\n )}\n <span data-slot=\"score-indicator-value\" className={styles.value}>\n {formatted}\n </span>\n {!isSolid &&\n (resolvedTone === \"destructive\" || resolvedTone === \"warning\") ? (\n <span\n data-slot=\"score-indicator-icon\"\n className={styles.iconOverlay}\n aria-hidden=\"true\"\n >\n {resolvedTone === \"destructive\" ? (\n <WarningCircle weight=\"fill\" />\n ) : (\n <Warning weight=\"fill\" />\n )}\n </span>\n ) : null}\n </span>\n {denominator !== \"none\" ? (\n <span\n data-slot=\"score-indicator-denominator\"\n className={styles.denominator}\n aria-hidden=\"true\"\n >\n {denominatorText}\n </span>\n ) : null}\n </span>\n )\n }\n)\nScoreIndicator.displayName = \"ScoreIndicator\"\n\nexport { ScoreIndicator, scoreIndicatorVariants }\n"
|
|
2467
2615
|
},
|
|
2468
2616
|
{
|
|
2469
2617
|
"path": "components/ui/score-indicator/score-indicator.module.css",
|
|
2470
2618
|
"type": "registry:ui",
|
|
2471
|
-
"content": "/* Score Indicator\n *\n * Compact circular score visualization for percentage / ratio metrics\n * (health, uptime, engagement, etc.). Renders an SVG track + indicator\n * ring with the value centered inside and an optional denominator label.\n *\n * All colors flow through CSS custom properties so themes (or consumers)\n * can tune per-tone ring + value colors without forking the component.\n */\n\n.base {\n --score-indicator-track-color: color-mix(\n in srgb,\n var(--border-default, #e5e7eb) 100%,\n transparent\n );\n --score-indicator-stroke-color: var(--text-primary, #111827);\n --score-indicator-value-color: var(--text-primary, #111827);\n --score-indicator-center-bg: transparent;\n --score-indicator-icon-color: var(--text-primary, #111827);\n\n display: inline-flex;\n align-items: center;\n gap: var(--spacing-2, 0.5rem);\n color: var(--text-primary, #111827);\n font-variant-numeric: tabular-nums;\n line-height: var(--line-height-tight, 1.1);\n}\n\n/* Below denominator: stack ring on top of the \"/ N\" label */\n.denominatorBelow {\n flex-direction: column;\n align-items: center;\n gap: var(--spacing-1, 0.25rem);\n}\n\n.denominatorNone,\n.denominatorTrailing {\n /* default flex-row layout */\n}\n\n/* Ring container — holds the SVG + centered value text */\n.ring {\n position: relative;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n border-radius: var(--radius-full, 9999px);\n background-color: var(--score-indicator-center-bg);\n flex-shrink: 0;\n}\n\n.svg {\n position: absolute;\n inset: 0;\n width: 100%;\n height: 100%;\n display: block;\n overflow: visible;\n}\n\n/* Track — full ring at low opacity */\n.track {\n stroke: var(--score-indicator-track-color);\n transition: stroke var(--motion-duration-150, 150ms)\n var(--motion-easing-default, ease-in-out);\n}\n\n/* Indicator — arc proportional to value/max */\n.indicator {\n stroke: var(--score-indicator-stroke-color);\n transition:\n stroke-dashoffset var(--motion-duration-500, 500ms)\n var(--motion-easing-default, ease-in-out),\n stroke var(--motion-duration-150, 150ms)\n var(--motion-easing-default, ease-in-out);\n}\n\n/* Centered value text — sits above the SVG */\n.value {\n position: relative;\n z-index: 1;\n font-weight: var(--font-weight-semibold, 600);\n color: var(--score-indicator-value-color);\n line-height: 1;\n}\n\n/* Trailing or below denominator label */\n.denominator {\n color: var(--text-tertiary, #6b7280);\n font-size: var(--font-size-sm, 0.875rem);\n font-weight: var(--font-weight-regular, 400);\n white-space: nowrap;\n}\n\n/* Icon overlay for destructive / warning tones — sits at top-right of ring */\n.iconOverlay {\n position: absolute;\n top: -2px;\n right: -2px;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n color: var(--score-indicator-icon-color);\n background-color: var(--surface-card, #ffffff);\n border-radius: var(--radius-full, 9999px);\n line-height: 0;\n z-index: 2;\n}\n\n/* Size sm — 24px ring, 11px value */\n.sizeSm .value {\n font-size: 11px;\n}\n\n.sizeSm .denominator {\n font-size: var(--font-size-xs, 0.75rem);\n}\n\n.sizeSm .iconOverlay {\n font-size: 10px;\n}\n\n/* Size md — 36px ring, 14px value (default) */\n.sizeMd .value {\n font-size: 14px;\n}\n\n.sizeMd .denominator {\n font-size: var(--font-size-sm, 0.875rem);\n}\n\n.sizeMd .iconOverlay {\n font-size: 12px;\n}\n\n/* Size lg — 56px ring, 20px value */\n.sizeLg .value {\n font-size: 20px;\n}\n\n.sizeLg .denominator {\n font-size: var(--font-size-base, 1rem);\n}\n\n.sizeLg .iconOverlay {\n font-size: 14px;\n}\n\n/* Tone bindings: each maps the local custom properties to semantic theme\n * tokens. Consumers override by setting any of the --score-indicator-* hooks\n * on the wrapper. Each tone tints the center subtly via color-mix over the\n * matching semantic surface so the value sits on a hint of color.\n */\n\n.toneSuccess {\n --score-indicator-stroke-color: var(--text-success, #16a34a);\n --score-indicator-value-color: var(--text-success, #16a34a);\n --score-indicator-icon-color: var(--text-success, #16a34a);\n --score-indicator-center-bg: color-mix(\n in srgb,\n var(--surface-success-subtle, var(--text-success, #16a34a)) 35%,\n transparent\n );\n}\n\n.toneInfo {\n --score-indicator-stroke-color: var(--text-info, #2563eb);\n --score-indicator-value-color: var(--text-info, #2563eb);\n --score-indicator-icon-color: var(--text-info, #2563eb);\n --score-indicator-center-bg: color-mix(\n in srgb,\n var(--surface-info-subtle, var(--text-info, #2563eb)) 35%,\n transparent\n );\n}\n\n.toneWarning {\n --score-indicator-stroke-color: var(--text-warning, #d97706);\n --score-indicator-value-color: var(--text-warning, #d97706);\n --score-indicator-icon-color: var(--text-warning, #d97706);\n --score-indicator-center-bg: color-mix(\n in srgb,\n var(--surface-warning-subtle, var(--text-warning, #d97706)) 35%,\n transparent\n );\n}\n\n.toneDestructive {\n --score-indicator-stroke-color: var(--text-error, #dc2626);\n --score-indicator-value-color: var(--text-error, #dc2626);\n --score-indicator-icon-color: var(--text-error, #dc2626);\n --score-indicator-center-bg: color-mix(\n in srgb,\n var(--surface-error-subtle, var(--text-error, #dc2626)) 35%,\n transparent\n );\n}\n\n.toneNeutral {\n --score-indicator-stroke-color: var(--text-tertiary, #6b7280);\n --score-indicator-value-color: var(--text-primary, #111827);\n --score-indicator-icon-color: var(--text-tertiary, #6b7280);\n --score-indicator-center-bg: transparent;\n}\n\n/* Respect reduced motion — disable transitions */\n@media (prefers-reduced-motion: reduce) {\n .indicator,\n .track {\n transition: none;\n }\n}\n"
|
|
2619
|
+
"content": "/* Score Indicator\n *\n * Compact circular score visualization for percentage / ratio metrics\n * (health, uptime, engagement, etc.). Renders an SVG track + indicator\n * ring with the value centered inside and an optional denominator label.\n *\n * All colors flow through CSS custom properties so themes (or consumers)\n * can tune per-tone ring + value colors without forking the component.\n */\n\n.base {\n --score-indicator-track-color: color-mix(\n in srgb,\n var(--border-default, #e5e7eb) 100%,\n transparent\n );\n --score-indicator-stroke-color: var(--text-primary, #111827);\n --score-indicator-value-color: var(--text-primary, #111827);\n --score-indicator-center-bg: transparent;\n --score-indicator-icon-color: var(--text-primary, #111827);\n\n /* Solid (flat disc) hooks — only consumed by .variantSolid. Defaults are\n * tone-neutral so the ring variant is never affected by their presence;\n * each tone block below overrides the tint + value color for solid mode. */\n --score-indicator-solid-bg: color-mix(\n in srgb,\n var(--text-tertiary, #6b7280) 18%,\n transparent\n );\n --score-indicator-solid-value-color: var(--text-primary, #111827);\n\n display: inline-flex;\n align-items: center;\n gap: var(--spacing-2, 0.5rem);\n color: var(--text-primary, #111827);\n font-variant-numeric: tabular-nums;\n line-height: var(--line-height-tight, 1.1);\n}\n\n/* Below denominator: stack ring on top of the \"/ N\" label */\n.denominatorBelow {\n flex-direction: column;\n align-items: center;\n gap: var(--spacing-1, 0.25rem);\n}\n\n.denominatorNone,\n.denominatorTrailing {\n /* default flex-row layout */\n}\n\n/* Ring variant — default SVG track + arc treatment. No-op marker class. */\n.variantRing {\n /* default rendering */\n}\n\n/* Solid variant — flat filled tinted disc, no SVG ring. The value text sits\n * on a subtle tone tint (the editorial health-score look). */\n.variantSolid .ring {\n background-color: var(--score-indicator-solid-bg);\n}\n\n.variantSolid .value {\n color: var(--score-indicator-solid-value-color);\n}\n\n/* Ring container — holds the SVG + centered value text */\n.ring {\n position: relative;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n border-radius: var(--radius-full, 9999px);\n background-color: var(--score-indicator-center-bg);\n flex-shrink: 0;\n}\n\n.svg {\n position: absolute;\n inset: 0;\n width: 100%;\n height: 100%;\n display: block;\n overflow: visible;\n}\n\n/* Track — full ring at low opacity */\n.track {\n stroke: var(--score-indicator-track-color);\n transition: stroke var(--motion-duration-150, 150ms)\n var(--motion-easing-default, ease-in-out);\n}\n\n/* Indicator — arc proportional to value/max */\n.indicator {\n stroke: var(--score-indicator-stroke-color);\n transition:\n stroke-dashoffset var(--motion-duration-500, 500ms)\n var(--motion-easing-default, ease-in-out),\n stroke var(--motion-duration-150, 150ms)\n var(--motion-easing-default, ease-in-out);\n}\n\n/* Centered value text — sits above the SVG */\n.value {\n position: relative;\n z-index: 1;\n font-weight: var(--font-weight-semibold, 600);\n color: var(--score-indicator-value-color);\n line-height: 1;\n}\n\n/* Trailing or below denominator label */\n.denominator {\n color: var(--text-tertiary, #6b7280);\n font-size: var(--font-size-sm, 0.875rem);\n font-weight: var(--font-weight-regular, 400);\n white-space: nowrap;\n}\n\n/* Icon overlay for destructive / warning tones — sits at top-right of ring */\n.iconOverlay {\n position: absolute;\n top: -2px;\n right: -2px;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n color: var(--score-indicator-icon-color);\n background-color: var(--surface-card, #ffffff);\n border-radius: var(--radius-full, 9999px);\n line-height: 0;\n z-index: 2;\n}\n\n/* Size sm — 24px ring, 11px value */\n.sizeSm .value {\n font-size: 11px;\n}\n\n.sizeSm .denominator {\n font-size: var(--font-size-xs, 0.75rem);\n}\n\n.sizeSm .iconOverlay {\n font-size: 10px;\n}\n\n/* Size md — 36px ring, 14px value (default) */\n.sizeMd .value {\n font-size: 14px;\n}\n\n.sizeMd .denominator {\n font-size: var(--font-size-sm, 0.875rem);\n}\n\n.sizeMd .iconOverlay {\n font-size: 12px;\n}\n\n/* Size lg — 56px ring, 20px value */\n.sizeLg .value {\n font-size: 20px;\n}\n\n.sizeLg .denominator {\n font-size: var(--font-size-base, 1rem);\n}\n\n.sizeLg .iconOverlay {\n font-size: 14px;\n}\n\n/* Tone bindings: each maps the local custom properties to semantic theme\n * tokens. Consumers override by setting any of the --score-indicator-* hooks\n * on the wrapper. Each tone tints the center subtly via color-mix over the\n * matching semantic surface so the value sits on a hint of color.\n */\n\n.toneSuccess {\n --score-indicator-stroke-color: var(--text-success, #16a34a);\n --score-indicator-value-color: var(--text-success, #16a34a);\n --score-indicator-icon-color: var(--text-success, #16a34a);\n --score-indicator-center-bg: color-mix(\n in srgb,\n var(--surface-success-subtle, var(--text-success, #16a34a)) 35%,\n transparent\n );\n --score-indicator-solid-bg: color-mix(\n in srgb,\n var(--success, var(--text-success, #16a34a)) 18%,\n transparent\n );\n --score-indicator-solid-value-color: var(--success, var(--text-success, #16a34a));\n}\n\n.toneInfo {\n --score-indicator-stroke-color: var(--text-info, #2563eb);\n --score-indicator-value-color: var(--text-info, #2563eb);\n --score-indicator-icon-color: var(--text-info, #2563eb);\n --score-indicator-center-bg: color-mix(\n in srgb,\n var(--surface-info-subtle, var(--text-info, #2563eb)) 35%,\n transparent\n );\n --score-indicator-solid-bg: color-mix(\n in srgb,\n var(--info, var(--text-info, #2563eb)) 18%,\n transparent\n );\n --score-indicator-solid-value-color: var(--info, var(--text-info, #2563eb));\n}\n\n.toneWarning {\n --score-indicator-stroke-color: var(--text-warning, #d97706);\n --score-indicator-value-color: var(--text-warning, #d97706);\n --score-indicator-icon-color: var(--text-warning, #d97706);\n --score-indicator-center-bg: color-mix(\n in srgb,\n var(--surface-warning-subtle, var(--text-warning, #d97706)) 35%,\n transparent\n );\n --score-indicator-solid-bg: color-mix(\n in srgb,\n var(--warning, var(--text-warning, #d97706)) 18%,\n transparent\n );\n --score-indicator-solid-value-color: var(--warning, var(--text-warning, #d97706));\n}\n\n.toneDestructive {\n --score-indicator-stroke-color: var(--text-error, #dc2626);\n --score-indicator-value-color: var(--text-error, #dc2626);\n --score-indicator-icon-color: var(--text-error, #dc2626);\n --score-indicator-center-bg: color-mix(\n in srgb,\n var(--surface-error-subtle, var(--text-error, #dc2626)) 35%,\n transparent\n );\n --score-indicator-solid-bg: color-mix(\n in srgb,\n var(--destructive, var(--text-error, #dc2626)) 18%,\n transparent\n );\n --score-indicator-solid-value-color: var(--destructive, var(--text-error, #dc2626));\n}\n\n.toneNeutral {\n --score-indicator-stroke-color: var(--text-tertiary, #6b7280);\n --score-indicator-value-color: var(--text-primary, #111827);\n --score-indicator-icon-color: var(--text-tertiary, #6b7280);\n --score-indicator-center-bg: transparent;\n --score-indicator-solid-bg: color-mix(\n in srgb,\n var(--text-tertiary, #6b7280) 14%,\n transparent\n );\n --score-indicator-solid-value-color: var(--text-primary, #111827);\n}\n\n/* Respect reduced motion — disable transitions */\n@media (prefers-reduced-motion: reduce) {\n .indicator,\n .track {\n transition: none;\n }\n}\n"
|
|
2472
2620
|
}
|
|
2473
2621
|
]
|
|
2474
2622
|
},
|
|
@@ -2791,6 +2939,30 @@
|
|
|
2791
2939
|
}
|
|
2792
2940
|
]
|
|
2793
2941
|
},
|
|
2942
|
+
{
|
|
2943
|
+
"name": "structured-prompt",
|
|
2944
|
+
"type": "registry:ui",
|
|
2945
|
+
"description": "Inline mad-lib fill-in-the-blank card for structured elicitation flows. Compound: StructuredPrompt > StructuredPromptHeader (icon + uppercase eyebrow) | StructuredPromptBody (tall-line-height prose) | StructuredPromptSlot (filled or empty inline chip; button when interactive, span when display-only) | StructuredPromptHint (footer tertiary text).",
|
|
2946
|
+
"category": "form",
|
|
2947
|
+
"dependencies": [
|
|
2948
|
+
"@loworbitstudio/visor-core"
|
|
2949
|
+
],
|
|
2950
|
+
"registryDependencies": [
|
|
2951
|
+
"utils"
|
|
2952
|
+
],
|
|
2953
|
+
"files": [
|
|
2954
|
+
{
|
|
2955
|
+
"path": "components/ui/structured-prompt/structured-prompt.tsx",
|
|
2956
|
+
"type": "registry:ui",
|
|
2957
|
+
"content": "import * as React from \"react\"\nimport { cn } from \"../../../lib/utils\"\nimport styles from \"./structured-prompt.module.css\"\n\n// StructuredPrompt — inline \"mad-lib\" fill-in-the-blank card\n// Compound: StructuredPrompt > StructuredPromptHeader | StructuredPromptBody | StructuredPromptSlot | StructuredPromptHint\n\nconst StructuredPrompt = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(\n ({ className, ...props }, ref) => (\n <div\n ref={ref}\n data-slot=\"structured-prompt\"\n className={cn(styles.root, className)}\n {...props}\n />\n )\n)\nStructuredPrompt.displayName = \"StructuredPrompt\"\n\nexport interface StructuredPromptHeaderProps extends React.HTMLAttributes<HTMLDivElement> {\n /** Phosphor icon rendered before the eyebrow label. */\n icon?: React.ReactNode\n}\n\nconst StructuredPromptHeader = React.forwardRef<HTMLDivElement, StructuredPromptHeaderProps>(\n ({ className, icon, children, ...props }, ref) => (\n <div\n ref={ref}\n data-slot=\"structured-prompt-header\"\n className={cn(styles.header, className)}\n {...props}\n >\n {icon ? (\n <span data-slot=\"structured-prompt-header-icon\" className={styles.headerIcon} aria-hidden=\"true\">\n {icon}\n </span>\n ) : null}\n <span data-slot=\"structured-prompt-header-label\" className={styles.headerLabel}>\n {children}\n </span>\n </div>\n )\n)\nStructuredPromptHeader.displayName = \"StructuredPromptHeader\"\n\nconst StructuredPromptBody = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(\n ({ className, ...props }, ref) => (\n <div\n ref={ref}\n data-slot=\"structured-prompt-body\"\n className={cn(styles.body, className)}\n {...props}\n />\n )\n)\nStructuredPromptBody.displayName = \"StructuredPromptBody\"\n\nexport interface StructuredPromptSlotProps {\n /** When true, renders the filled (primary-tinted) chip treatment. When false/absent, renders the empty (dashed, muted) chip treatment. */\n filled?: boolean\n /** When provided, the slot renders as a <button> with focus ring. Without onClick, it renders as an inline <span>. */\n onClick?: () => void\n children?: React.ReactNode\n className?: string\n}\n\nconst StructuredPromptSlot = React.forwardRef<\n HTMLButtonElement | HTMLSpanElement,\n StructuredPromptSlotProps\n>(({ filled = false, onClick, children, className }, ref) => {\n const slotClass = cn(styles.slot, filled ? styles.slotFilled : styles.slotEmpty, className)\n\n if (onClick) {\n return (\n <button\n ref={ref as React.Ref<HTMLButtonElement>}\n data-slot=\"structured-prompt-slot\"\n data-filled={filled ? \"true\" : \"false\"}\n type=\"button\"\n onClick={onClick}\n className={slotClass}\n >\n {children}\n </button>\n )\n }\n\n return (\n <span\n ref={ref as React.Ref<HTMLSpanElement>}\n data-slot=\"structured-prompt-slot\"\n data-filled={filled ? \"true\" : \"false\"}\n className={slotClass}\n >\n {children}\n </span>\n )\n})\nStructuredPromptSlot.displayName = \"StructuredPromptSlot\"\n\nconst StructuredPromptHint = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(\n ({ className, ...props }, ref) => (\n <div\n ref={ref}\n data-slot=\"structured-prompt-hint\"\n className={cn(styles.hint, className)}\n {...props}\n />\n )\n)\nStructuredPromptHint.displayName = \"StructuredPromptHint\"\n\nexport {\n StructuredPrompt,\n StructuredPromptHeader,\n StructuredPromptBody,\n StructuredPromptSlot,\n StructuredPromptHint,\n}\n"
|
|
2958
|
+
},
|
|
2959
|
+
{
|
|
2960
|
+
"path": "components/ui/structured-prompt/structured-prompt.module.css",
|
|
2961
|
+
"type": "registry:ui",
|
|
2962
|
+
"content": "/* StructuredPrompt\n *\n * Inline \"mad-lib\" fill-in-the-blank card used in brand elicitation and\n * structured-prompt workflows. A card with an eyebrow header, flowing prose\n * at tall line-height, and inline slot chips (filled or empty) embedded in\n * the prose, plus a hint footer.\n */\n\n.root {\n width: 100%;\n border-radius: var(--radius-xl, 1rem);\n background: var(--surface-card, #ffffff);\n border: var(--stroke-width-thin, 1px) solid var(--border-default, #e5e7eb);\n box-shadow: var(--shadow-sm, 0 1px 2px 0 rgba(0, 0, 0, 0.05));\n overflow: hidden;\n}\n\n/* Header bar — small icon + uppercase eyebrow label, hairline bottom border */\n.header {\n display: flex;\n align-items: center;\n gap: var(--spacing-2, 0.5rem);\n padding: var(--spacing-2-5, 0.625rem) var(--spacing-3-5, 0.875rem);\n border-bottom: var(--stroke-width-thin, 1px) solid var(--hairline, #e5e7eb);\n /* 11.5px — no line-height token matches this tall-prose context; intentional */\n font-size: 11.5px;\n font-weight: 700;\n letter-spacing: 0.04em;\n text-transform: uppercase;\n color: var(--text-tertiary, #6b7280);\n}\n\n.headerIcon {\n display: inline-flex;\n align-items: center;\n /* Use accent color for the eyebrow icon, matching the design spec */\n color: var(--surface-accent-default, #3b82f6);\n flex-shrink: 0;\n /* 13px icon — sized to match the 11.5px label rhythm */\n font-size: 13px;\n line-height: 1;\n}\n\n.headerLabel {\n flex: 1;\n min-width: 0;\n}\n\n/* Body — prose container with tall line-height so inline chips breathe */\n.body {\n padding: var(--spacing-3-5, 0.875rem);\n /* 15px — closest to --font-size-base but intentionally smaller for prose density */\n font-size: 15px;\n /* 2.05 line-height — no token matches; intentional value from design spec (elicit-core HiFi prototype) */\n line-height: 2.05;\n color: var(--text-primary, #111827);\n}\n\n/* Slot chip — inline; rendered inside flowing prose text */\n.slot {\n display: inline-flex;\n align-items: center;\n /* min-height 30px — no spacing token; intentional to match chip rhythm */\n min-height: 30px;\n padding: var(--spacing-0-5, 0.125rem) var(--spacing-2-5, 0.625rem);\n margin: 0 var(--spacing-0-5, 0.125rem);\n border-radius: var(--radius-md, 0.5rem);\n border-width: var(--stroke-width-thin, 1px);\n border-style: solid;\n /* 14px — intentionally one step below body prose for visual hierarchy */\n font-size: 14px;\n cursor: default;\n vertical-align: middle;\n line-height: 1.4;\n /* Reset button defaults when rendered as <button> */\n font-family: inherit;\n text-decoration: none;\n -webkit-appearance: none;\n appearance: none;\n}\n\n/* Focus ring — only when rendered as <button> */\n.slot:focus-visible {\n outline: var(--focus-ring-width, 2px) solid var(--border-focus, #3b82f6);\n outline-offset: var(--focus-ring-offset, 2px);\n}\n\n/* Filled slot — accent-subtle bg + accent-default border + text-link color + 600 weight.\n * Uses the same token triplet as Chip's selected state so it adapts correctly in dark mode.\n */\n.slotFilled {\n background: var(--surface-accent-subtle, #eff6ff);\n border-color: var(--surface-accent-default, #3b82f6);\n color: var(--text-link, #2563eb);\n font-weight: var(--font-weight-semibold, 600);\n}\n\n/* Empty slot — dashed border, muted bg, italic, tertiary text */\n.slotEmpty {\n color: var(--text-tertiary, #6b7280);\n font-style: italic;\n font-weight: var(--font-weight-medium, 500);\n background: var(--surface-subtle, #f9fafb);\n border-style: dashed;\n border-color: var(--border-default, #e5e7eb);\n cursor: text;\n}\n\n/* Hint footer — small tertiary text below the body */\n.hint {\n padding: 0 var(--spacing-3-5, 0.875rem) var(--spacing-3, 0.75rem);\n /* 11.5px — matches header eyebrow size; intentional */\n font-size: 11.5px;\n color: var(--text-tertiary, #6b7280);\n}\n\n/* Respect reduced motion */\n@media (prefers-reduced-motion: reduce) {\n .slot {\n transition: none;\n }\n}\n"
|
|
2963
|
+
}
|
|
2964
|
+
]
|
|
2965
|
+
},
|
|
2794
2966
|
{
|
|
2795
2967
|
"name": "use-media-query",
|
|
2796
2968
|
"type": "registry:hook",
|
|
@@ -3590,7 +3762,7 @@
|
|
|
3590
3762
|
{
|
|
3591
3763
|
"path": "blocks/admin-tabbed-editor/admin-tabbed-editor.tsx",
|
|
3592
3764
|
"type": "registry:block",
|
|
3593
|
-
"content": "\"use client\"\n\nimport * as React from \"react\"\n\nimport { cn } from \"../../lib/utils\"\nimport {\n PageHeader,\n type PageHeaderProps,\n} from \"../../components/ui/page-header/page-header\"\nimport {\n Tabs,\n TabsContent,\n TabsList,\n TabsTrigger,\n} from \"../../components/ui/tabs/tabs\"\nimport { Button } from \"../../components/ui/button/button\"\nimport { ConfirmDialog } from \"../../components/ui/confirm-dialog/confirm-dialog\"\nimport styles from \"./admin-tabbed-editor.module.css\"\n\nexport interface AdminTabbedEditorTab {\n /** Stable identifier used as the Tabs value. */\n id: string\n /** Tab trigger label. */\n label: React.ReactNode\n /** Optional leading icon rendered before the label inside the trigger. */\n icon?: React.ReactNode\n /** Panel content rendered when this tab is active. */\n content: React.ReactNode\n /** Optional badge rendered after the label (e.g. unsaved indicator). */\n badge?: React.ReactNode\n /** Disable the tab trigger. */\n disabled?: boolean\n}\n\nexport interface AdminTabbedEditorProps\n extends Omit<\n React.HTMLAttributes<HTMLDivElement>,\n \"title\" | \"content\"\n > {\n // ── Header ──────────────────────────────────────────────────────────────\n /** Editor 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 actions slot rendered on the right side of the header. */\n headerActions?: React.ReactNode\n /**\n * Header vertical rhythm — forwarded to the internal PageHeader's `size`.\n * Omit to keep the PageHeader default (`\"md\"`).\n */\n headerSize?: PageHeaderProps[\"size\"]\n /**\n * Title font-size override — forwarded to the internal PageHeader's\n * `titleSize` (`\"default\" | \"marquee\"` token, or a raw CSS length).\n * Omit to keep the default title scale.\n */\n titleSize?: PageHeaderProps[\"titleSize\"]\n /**\n * Title font-family override — forwarded to the internal PageHeader's\n * `titleFamily` (`\"heading\" | \"display\"` token, or a raw CSS family).\n * Omit to keep the heading family.\n */\n titleFamily?: PageHeaderProps[\"titleFamily\"]\n /**\n * Title line-height override — forwarded to the internal PageHeader's\n * `leading`. Omit to keep the default line-height.\n */\n leading?: PageHeaderProps[\"leading\"]\n\n // ── Tabs ────────────────────────────────────────────────────────────────\n /** Ordered list of tabs. */\n tabs: AdminTabbedEditorTab[]\n /** Controlled active tab id. */\n activeTab?: string\n /** Uncontrolled default active tab id. Defaults to the first tab. */\n defaultActiveTab?: string\n /** Handler called when the active tab changes. */\n onActiveTabChange?: (id: string) => void\n\n // ── Save / cancel actions ───────────────────────────────────────────────\n /** Save handler. Async-aware: a returned Promise puts the save button into a 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\n // ── State ───────────────────────────────────────────────────────────────\n /** If true, tab switching and cancel are intercepted by the unsaved-changes guard. */\n dirty?: boolean\n /** Externally-controlled busy state — overrides internal async pending detection. */\n busy?: boolean\n /** Disable the save button. */\n disabled?: boolean\n /** Middle slot inside the sticky footer — e.g. \"Last saved 2 minutes ago\". */\n footerStatus?: React.ReactNode\n /** Hide the footer entirely. */\n hideFooter?: boolean\n\n // ── Unsaved guard customization ─────────────────────────────────────────\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 tab.\"\n\nconst AdminTabbedEditor = React.forwardRef<\n HTMLDivElement,\n AdminTabbedEditorProps\n>(function AdminTabbedEditor(\n {\n title,\n eyebrow,\n description,\n breadcrumb,\n headerActions,\n headerSize,\n titleSize,\n titleFamily,\n leading,\n tabs,\n activeTab,\n defaultActiveTab,\n onActiveTabChange,\n onSave,\n onCancel,\n saveLabel = \"Save changes\",\n cancelLabel = \"Cancel\",\n dirty = false,\n busy,\n disabled = false,\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 const firstTabId = tabs[0]?.id\n const [internalActive, setInternalActive] = React.useState<string | undefined>(\n defaultActiveTab ?? firstTabId\n )\n const [isPending, setIsPending] = React.useState(false)\n const [pendingTabId, setPendingTabId] = React.useState<string | null>(null)\n const [pendingCancel, setPendingCancel] = React.useState(false)\n\n const isControlled = activeTab !== undefined\n const currentTab = isControlled ? activeTab : internalActive\n const effectiveBusy = busy ?? isPending\n const showUnsavedGuard = pendingTabId !== null || pendingCancel\n\n const commitTabChange = React.useCallback(\n (next: string) => {\n if (!isControlled) {\n setInternalActive(next)\n }\n onActiveTabChange?.(next)\n },\n [isControlled, onActiveTabChange]\n )\n\n const handleTabsValueChange = React.useCallback(\n (next: string) => {\n if (next === currentTab) return\n if (dirty) {\n setPendingTabId(next)\n return\n }\n commitTabChange(next)\n },\n [currentTab, dirty, commitTabChange]\n )\n\n const handleCancelClick = React.useCallback(() => {\n if (effectiveBusy) return\n if (dirty) {\n setPendingCancel(true)\n return\n }\n onCancel?.()\n }, [dirty, effectiveBusy, onCancel])\n\n const handleSaveClick = React.useCallback(async () => {\n if (!onSave) return\n const result = onSave()\n if (result && typeof (result as Promise<void>).then === \"function\") {\n setIsPending(true)\n try {\n await result\n setIsPending(false)\n } catch (err) {\n setIsPending(false)\n throw err\n }\n }\n }, [onSave])\n\n const handleGuardConfirm = React.useCallback(() => {\n if (pendingTabId !== null) {\n const next = pendingTabId\n setPendingTabId(null)\n commitTabChange(next)\n return\n }\n if (pendingCancel) {\n setPendingCancel(false)\n onCancel?.()\n }\n }, [pendingTabId, pendingCancel, commitTabChange, onCancel])\n\n const handleGuardCancel = React.useCallback(() => {\n setPendingTabId(null)\n setPendingCancel(false)\n }, [])\n\n const saveDisabled = disabled || effectiveBusy || !dirty\n\n return (\n <>\n <div\n ref={ref}\n className={cn(styles.root, className)}\n data-slot=\"admin-tabbed-editor\"\n {...rest}\n >\n <PageHeader\n className={styles.header}\n eyebrow={eyebrow}\n title={title}\n description={description}\n breadcrumb={breadcrumb}\n actions={headerActions}\n size={headerSize}\n titleSize={titleSize}\n titleFamily={titleFamily}\n leading={leading}\n />\n\n <Tabs\n className={styles.tabs}\n value={currentTab}\n onValueChange={handleTabsValueChange}\n >\n <TabsList className={styles.tabsList}>\n {tabs.map((tab) => (\n <TabsTrigger\n key={tab.id}\n value={tab.id}\n disabled={tab.disabled}\n className={styles.tabsTrigger}\n data-slot=\"admin-tabbed-editor-trigger\"\n >\n {tab.icon ? (\n <span\n className={styles.triggerIcon}\n data-slot=\"admin-tabbed-editor-trigger-icon\"\n aria-hidden=\"true\"\n >\n {tab.icon}\n </span>\n ) : null}\n <span className={styles.triggerLabel}>{tab.label}</span>\n {tab.badge ? (\n <span\n className={styles.triggerBadge}\n data-slot=\"admin-tabbed-editor-trigger-badge\"\n >\n {tab.badge}\n </span>\n ) : null}\n </TabsTrigger>\n ))}\n </TabsList>\n\n {tabs.map((tab) => (\n <TabsContent\n key={tab.id}\n value={tab.id}\n className={styles.tabsContent}\n data-slot=\"admin-tabbed-editor-content\"\n >\n {tab.content}\n </TabsContent>\n ))}\n </Tabs>\n\n {hideFooter ? null : (\n <div\n className={styles.footer}\n data-slot=\"admin-tabbed-editor-footer\"\n role=\"group\"\n aria-label=\"Editor actions\"\n >\n <div className={styles.footerCancel}>\n <Button\n type=\"button\"\n variant=\"outline\"\n onClick={handleCancelClick}\n disabled={effectiveBusy}\n data-slot=\"admin-tabbed-editor-cancel\"\n >\n {cancelLabel}\n </Button>\n </div>\n {footerStatus ? (\n <div\n className={styles.footerStatus}\n data-slot=\"admin-tabbed-editor-status\"\n >\n {footerStatus}\n </div>\n ) : null}\n <div className={styles.footerSave}>\n <Button\n type=\"button\"\n onClick={handleSaveClick}\n disabled={saveDisabled}\n aria-busy={effectiveBusy || undefined}\n data-slot=\"admin-tabbed-editor-save\"\n >\n {saveLabel}\n </Button>\n </div>\n </div>\n )}\n </div>\n\n <ConfirmDialog\n open={showUnsavedGuard}\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 </>\n )\n})\n\nAdminTabbedEditor.displayName = \"AdminTabbedEditor\"\n\nexport { AdminTabbedEditor }\n"
|
|
3765
|
+
"content": "\"use client\"\n\nimport * as React from \"react\"\n\nimport { cn } from \"../../lib/utils\"\nimport {\n PageHeader,\n type PageHeaderProps,\n} from \"../../components/ui/page-header/page-header\"\nimport {\n Tabs,\n TabsContent,\n TabsList,\n TabsTrigger,\n} from \"../../components/ui/tabs/tabs\"\nimport { Button } from \"../../components/ui/button/button\"\nimport { ConfirmDialog } from \"../../components/ui/confirm-dialog/confirm-dialog\"\nimport styles from \"./admin-tabbed-editor.module.css\"\n\nexport interface AdminTabbedEditorTab {\n /** Stable identifier used as the Tabs value. */\n id: string\n /** Tab trigger label. */\n label: React.ReactNode\n /** Optional leading icon rendered before the label inside the trigger. */\n icon?: React.ReactNode\n /** Panel content rendered when this tab is active. */\n content: React.ReactNode\n /** Optional badge rendered after the label (e.g. unsaved indicator). */\n badge?: React.ReactNode\n /** Disable the tab trigger. */\n disabled?: boolean\n}\n\nexport interface AdminTabbedEditorProps\n extends Omit<\n React.HTMLAttributes<HTMLDivElement>,\n \"title\" | \"content\"\n > {\n // ── Header ──────────────────────────────────────────────────────────────\n /** Editor 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 actions slot rendered on the right side of the header. */\n headerActions?: React.ReactNode\n /**\n * Header vertical rhythm — forwarded to the internal PageHeader's `size`.\n * Omit to keep the PageHeader default (`\"md\"`).\n */\n headerSize?: PageHeaderProps[\"size\"]\n /**\n * Title font-size override — forwarded to the internal PageHeader's\n * `titleSize` (`\"default\" | \"marquee\"` token, or a raw CSS length).\n * Omit to keep the default title scale.\n */\n titleSize?: PageHeaderProps[\"titleSize\"]\n /**\n * Title font-family override — forwarded to the internal PageHeader's\n * `titleFamily` (`\"heading\" | \"display\"` token, or a raw CSS family).\n * Omit to keep the heading family.\n */\n titleFamily?: PageHeaderProps[\"titleFamily\"]\n /**\n * Optional media/identity node (typically an Avatar) rendered to the left of\n * the title — forwarded to the internal PageHeader's `leading` slot. Omit to\n * keep the default text|actions header layout.\n */\n leading?: PageHeaderProps[\"leading\"]\n\n // ── Tabs ────────────────────────────────────────────────────────────────\n /** Ordered list of tabs. */\n tabs: AdminTabbedEditorTab[]\n /** Controlled active tab id. */\n activeTab?: string\n /** Uncontrolled default active tab id. Defaults to the first tab. */\n defaultActiveTab?: string\n /** Handler called when the active tab changes. */\n onActiveTabChange?: (id: string) => void\n\n // ── Save / cancel actions ───────────────────────────────────────────────\n /** Save handler. Async-aware: a returned Promise puts the save button into a 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\n // ── State ───────────────────────────────────────────────────────────────\n /** If true, tab switching and cancel are intercepted by the unsaved-changes guard. */\n dirty?: boolean\n /** Externally-controlled busy state — overrides internal async pending detection. */\n busy?: boolean\n /** Disable the save button. */\n disabled?: boolean\n /** Middle slot inside the sticky footer — e.g. \"Last saved 2 minutes ago\". */\n footerStatus?: React.ReactNode\n /** Hide the footer entirely. */\n hideFooter?: boolean\n\n // ── Unsaved guard customization ─────────────────────────────────────────\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 tab.\"\n\nconst AdminTabbedEditor = React.forwardRef<\n HTMLDivElement,\n AdminTabbedEditorProps\n>(function AdminTabbedEditor(\n {\n title,\n eyebrow,\n description,\n breadcrumb,\n headerActions,\n headerSize,\n titleSize,\n titleFamily,\n leading,\n tabs,\n activeTab,\n defaultActiveTab,\n onActiveTabChange,\n onSave,\n onCancel,\n saveLabel = \"Save changes\",\n cancelLabel = \"Cancel\",\n dirty = false,\n busy,\n disabled = false,\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 const firstTabId = tabs[0]?.id\n const [internalActive, setInternalActive] = React.useState<string | undefined>(\n defaultActiveTab ?? firstTabId\n )\n const [isPending, setIsPending] = React.useState(false)\n const [pendingTabId, setPendingTabId] = React.useState<string | null>(null)\n const [pendingCancel, setPendingCancel] = React.useState(false)\n\n const isControlled = activeTab !== undefined\n const currentTab = isControlled ? activeTab : internalActive\n const effectiveBusy = busy ?? isPending\n const showUnsavedGuard = pendingTabId !== null || pendingCancel\n\n const commitTabChange = React.useCallback(\n (next: string) => {\n if (!isControlled) {\n setInternalActive(next)\n }\n onActiveTabChange?.(next)\n },\n [isControlled, onActiveTabChange]\n )\n\n const handleTabsValueChange = React.useCallback(\n (next: string) => {\n if (next === currentTab) return\n if (dirty) {\n setPendingTabId(next)\n return\n }\n commitTabChange(next)\n },\n [currentTab, dirty, commitTabChange]\n )\n\n const handleCancelClick = React.useCallback(() => {\n if (effectiveBusy) return\n if (dirty) {\n setPendingCancel(true)\n return\n }\n onCancel?.()\n }, [dirty, effectiveBusy, onCancel])\n\n const handleSaveClick = React.useCallback(async () => {\n if (!onSave) return\n const result = onSave()\n if (result && typeof (result as Promise<void>).then === \"function\") {\n setIsPending(true)\n try {\n await result\n setIsPending(false)\n } catch (err) {\n setIsPending(false)\n throw err\n }\n }\n }, [onSave])\n\n const handleGuardConfirm = React.useCallback(() => {\n if (pendingTabId !== null) {\n const next = pendingTabId\n setPendingTabId(null)\n commitTabChange(next)\n return\n }\n if (pendingCancel) {\n setPendingCancel(false)\n onCancel?.()\n }\n }, [pendingTabId, pendingCancel, commitTabChange, onCancel])\n\n const handleGuardCancel = React.useCallback(() => {\n setPendingTabId(null)\n setPendingCancel(false)\n }, [])\n\n const saveDisabled = disabled || effectiveBusy || !dirty\n\n return (\n <>\n <div\n ref={ref}\n className={cn(styles.root, className)}\n data-slot=\"admin-tabbed-editor\"\n {...rest}\n >\n <PageHeader\n className={styles.header}\n eyebrow={eyebrow}\n title={title}\n description={description}\n breadcrumb={breadcrumb}\n actions={headerActions}\n size={headerSize}\n titleSize={titleSize}\n titleFamily={titleFamily}\n leading={leading}\n />\n\n <Tabs\n className={styles.tabs}\n value={currentTab}\n onValueChange={handleTabsValueChange}\n >\n <TabsList className={styles.tabsList}>\n {tabs.map((tab) => (\n <TabsTrigger\n key={tab.id}\n value={tab.id}\n disabled={tab.disabled}\n className={styles.tabsTrigger}\n data-slot=\"admin-tabbed-editor-trigger\"\n >\n {tab.icon ? (\n <span\n className={styles.triggerIcon}\n data-slot=\"admin-tabbed-editor-trigger-icon\"\n aria-hidden=\"true\"\n >\n {tab.icon}\n </span>\n ) : null}\n <span className={styles.triggerLabel}>{tab.label}</span>\n {tab.badge ? (\n <span\n className={styles.triggerBadge}\n data-slot=\"admin-tabbed-editor-trigger-badge\"\n >\n {tab.badge}\n </span>\n ) : null}\n </TabsTrigger>\n ))}\n </TabsList>\n\n {tabs.map((tab) => (\n <TabsContent\n key={tab.id}\n value={tab.id}\n className={styles.tabsContent}\n data-slot=\"admin-tabbed-editor-content\"\n >\n {tab.content}\n </TabsContent>\n ))}\n </Tabs>\n\n {hideFooter ? null : (\n <div\n className={styles.footer}\n data-slot=\"admin-tabbed-editor-footer\"\n role=\"group\"\n aria-label=\"Editor actions\"\n >\n <div className={styles.footerCancel}>\n <Button\n type=\"button\"\n variant=\"outline\"\n onClick={handleCancelClick}\n disabled={effectiveBusy}\n data-slot=\"admin-tabbed-editor-cancel\"\n >\n {cancelLabel}\n </Button>\n </div>\n {footerStatus ? (\n <div\n className={styles.footerStatus}\n data-slot=\"admin-tabbed-editor-status\"\n >\n {footerStatus}\n </div>\n ) : null}\n <div className={styles.footerSave}>\n <Button\n type=\"button\"\n onClick={handleSaveClick}\n disabled={saveDisabled}\n aria-busy={effectiveBusy || undefined}\n data-slot=\"admin-tabbed-editor-save\"\n >\n {saveLabel}\n </Button>\n </div>\n </div>\n )}\n </div>\n\n <ConfirmDialog\n open={showUnsavedGuard}\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 </>\n )\n})\n\nAdminTabbedEditor.displayName = \"AdminTabbedEditor\"\n\nexport { AdminTabbedEditor }\n"
|
|
3594
3766
|
},
|
|
3595
3767
|
{
|
|
3596
3768
|
"path": "blocks/admin-tabbed-editor/admin-tabbed-editor.module.css",
|
|
@@ -4340,6 +4512,149 @@
|
|
|
4340
4512
|
}
|
|
4341
4513
|
]
|
|
4342
4514
|
},
|
|
4515
|
+
{
|
|
4516
|
+
"name": "vignette",
|
|
4517
|
+
"type": "registry:ui",
|
|
4518
|
+
"description": "Fixed, full-viewport radial vignette layer. Decorative atmosphere atom — pointer-events-none, aria-hidden, static. Ported from Blacklight bl-vignette (BL-326). Gradient stops and strength are configurable via CSS custom properties.",
|
|
4519
|
+
"category": "visual-elements",
|
|
4520
|
+
"dependencies": [
|
|
4521
|
+
"@loworbitstudio/visor-core"
|
|
4522
|
+
],
|
|
4523
|
+
"registryDependencies": [
|
|
4524
|
+
"utils"
|
|
4525
|
+
],
|
|
4526
|
+
"files": [
|
|
4527
|
+
{
|
|
4528
|
+
"path": "components/visual/vignette/vignette.tsx",
|
|
4529
|
+
"type": "registry:ui",
|
|
4530
|
+
"content": "import * as React from \"react\"\nimport { cn } from \"../../../lib/utils\"\nimport styles from \"./vignette.module.css\"\n\n// ---------------------------------------------------------------------------\n// VignetteProps\n// ---------------------------------------------------------------------------\n\nexport interface VignetteProps {\n /**\n * CSS `z-index` for the vignette layer.\n * Default matches the Blacklight bl-vignette value (20).\n */\n zIndex?: number\n /**\n * Additional class names applied to the root element.\n */\n className?: string\n /**\n * Additional inline styles. CSS custom properties can be passed here to\n * override gradient stops/strength:\n *\n * - `--vignette-transparent-stop` (default: `52%`) — inner clear stop\n * - `--vignette-color-stop` (default: `100%`) — outer dark stop\n * - `--vignette-color` (default: `rgba(0, 0, 6, 0.5)`) — outer dark color\n * - `--vignette-size-x` (default: `120%`) — horizontal gradient radius\n * - `--vignette-size-y` (default: `90%`) — vertical gradient radius\n * - `--vignette-position` (default: `50% 38%`) — gradient focal point\n */\n style?: React.CSSProperties\n}\n\n// ---------------------------------------------------------------------------\n// Vignette\n// ---------------------------------------------------------------------------\n\n/**\n * Fixed, full-viewport radial vignette layer. Decorative atmosphere atom —\n * pointer-events-none, aria-hidden, static (no motion). Ported from the\n * Blacklight `.bl-vignette` depth system (BL-326).\n *\n * Drop anywhere in your layout tree; it escapes the containing block via\n * `position: fixed` and covers the full viewport.\n */\nconst Vignette = React.forwardRef<HTMLDivElement, VignetteProps>(\n ({ zIndex = 20, className, style }, ref) => {\n return (\n <div\n ref={ref}\n aria-hidden=\"true\"\n className={cn(styles.vignette, className)}\n style={{ zIndex, ...style }}\n />\n )\n },\n)\n\nVignette.displayName = \"Vignette\"\n\nexport { Vignette }\n"
|
|
4531
|
+
},
|
|
4532
|
+
{
|
|
4533
|
+
"path": "components/visual/vignette/vignette.module.css",
|
|
4534
|
+
"type": "registry:ui",
|
|
4535
|
+
"content": ".vignette {\n position: fixed;\n inset: 0;\n pointer-events: none;\n background: radial-gradient(\n var(--vignette-size-x, 120%) var(--vignette-size-y, 90%) at var(--vignette-position, 50% 38%),\n transparent var(--vignette-transparent-stop, 52%),\n var(--vignette-color, rgba(0, 0, 6, 0.5)) var(--vignette-color-stop, 100%)\n );\n}\n"
|
|
4536
|
+
}
|
|
4537
|
+
]
|
|
4538
|
+
},
|
|
4539
|
+
{
|
|
4540
|
+
"name": "ambient-glow",
|
|
4541
|
+
"type": "registry:ui",
|
|
4542
|
+
"description": "Absolutely-positioned drifting radial glow with live CSS-var-driven color. Decorative only — aria-hidden, pointer-events-none. Keyed and gold variants. Ported from the Blacklight marketing depth system (BL-326).",
|
|
4543
|
+
"category": "visual-elements",
|
|
4544
|
+
"dependencies": [
|
|
4545
|
+
"class-variance-authority",
|
|
4546
|
+
"@loworbitstudio/visor-core"
|
|
4547
|
+
],
|
|
4548
|
+
"registryDependencies": [
|
|
4549
|
+
"utils"
|
|
4550
|
+
],
|
|
4551
|
+
"files": [
|
|
4552
|
+
{
|
|
4553
|
+
"path": "components/visual/ambient-glow/ambient-glow.tsx",
|
|
4554
|
+
"type": "registry:ui",
|
|
4555
|
+
"content": "import * as React from \"react\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\nimport { cn } from \"../../../lib/utils\"\nimport styles from \"./ambient-glow.module.css\"\n\nconst ambientGlowVariants = cva(styles.base, {\n variants: {\n variant: {\n /**\n * Keyed — color drawn from `--glow-color`. Set the var on a parent or\n * on the element via `style`. Blacklight-style live page crossfades write\n * the var on `<html>` every rAF; the glow repaint is handled by the CSS\n * engine — no React re-render required.\n */\n keyed: styles.variantKeyed,\n /**\n * Gold — static warm gold glow (#ffbe26 at 7% opacity). Used for\n * premium / Pro surfaces. `--glow-color` is not consumed in this variant.\n */\n gold: styles.variantGold,\n },\n },\n defaultVariants: {\n variant: \"keyed\",\n },\n})\n\nexport interface AmbientGlowProps\n extends React.HTMLAttributes<HTMLDivElement>,\n VariantProps<typeof ambientGlowVariants> {\n /**\n * Override the glow color at render time.\n * Equivalent to setting `--glow-color` via `style` — prefer this prop when\n * the color is known statically. For live runtime rewrites (e.g. hero\n * crossfades), set `--glow-color` on an ancestor element directly via JS\n * to avoid re-renders.\n *\n * Only used by the `keyed` variant.\n */\n glowColor?: string\n}\n\n/**\n * `AmbientGlow` — absolutely-positioned drifting radial glow.\n *\n * Decorative only: `aria-hidden=\"true\"`, `pointer-events: none`.\n * Parent element must have a non-static position (e.g. `position: relative`)\n * to contain the absolute positioning.\n *\n * Color is driven by the CSS custom property `--glow-color`, which defaults\n * to `var(--accent)`. For live runtime color rewrites (e.g. hero crossfades),\n * write `--glow-color` on an ancestor element — the CSS engine repaints\n * without triggering a React re-render.\n *\n * Size and position are controlled by `className` or `style` at the call site.\n *\n * @example Keyed to theme accent, filling the parent\n * ```tsx\n * <div style={{ position: 'relative', width: 400, height: 400 }}>\n * <AmbientGlow style={{ inset: 0 }} />\n * </div>\n * ```\n *\n * @example Gold variant with custom placement\n * ```tsx\n * <div style={{ position: 'relative' }}>\n * <AmbientGlow variant=\"gold\" style={{ inset: '-10% -15%' }} />\n * </div>\n * ```\n *\n * @example Live color override via glowColor prop\n * ```tsx\n * <AmbientGlow glowColor=\"var(--color-acid)\" style={{ inset: 0 }} />\n * ```\n */\nexport function AmbientGlow({\n variant,\n glowColor,\n className,\n style,\n ...props\n}: AmbientGlowProps) {\n const inlineStyle: React.CSSProperties = {\n ...(glowColor ? { [\"--glow-color\" as string]: glowColor } : {}),\n ...style,\n }\n\n return (\n <div\n aria-hidden=\"true\"\n data-slot=\"ambient-glow\"\n data-variant={variant ?? \"keyed\"}\n className={cn(ambientGlowVariants({ variant }), className)}\n style={inlineStyle}\n {...props}\n />\n )\n}\n"
|
|
4556
|
+
},
|
|
4557
|
+
{
|
|
4558
|
+
"path": "components/visual/ambient-glow/ambient-glow.module.css",
|
|
4559
|
+
"type": "registry:ui",
|
|
4560
|
+
"content": "/* AmbientGlow — drifting radial glow decorative primitive (VI-566)\n *\n * Color contract: all color paths run through CSS custom properties resolved\n * at paint time. The consumer sets --glow-color on a parent element (or on\n * the element itself via style). For Blacklight-style live crossfades, write\n * --glow-color on <html> every rAF — the glow tracks without a re-render.\n *\n * Drift: transform-only (translate3d + scale). Will-change: transform tells\n * the compositor to promote to its own layer. Do NOT combine with CSS utility\n * translate classes — the keyframe already owns the transform axis.\n *\n * Size / position: set via className or style at the call site. The component\n * renders absolutely positioned so the parent needs position: relative (or any\n * non-static position) to contain it.\n */\n\n/* Base */\n.base {\n /* Consumer-overridable glow color — defaults to the theme accent. */\n --glow-color: var(--accent, var(--interactive-primary-bg, #2563eb));\n\n position: absolute;\n pointer-events: none;\n will-change: transform;\n animation: ambientGlowDrift 18s ease-in-out infinite alternate;\n}\n\n/* Keyed variant — color-mix against the live --glow-color */\n.variantKeyed {\n background: radial-gradient(\n closest-side,\n color-mix(in srgb, var(--glow-color) 10%, transparent),\n transparent 72%\n );\n}\n\n/* Gold variant — static warm gold; --glow-color is unused for this variant */\n.variantGold {\n background: radial-gradient(\n closest-side,\n rgba(255, 190, 38, 0.07),\n transparent 72%\n );\n}\n\n/* Drift keyframes — transform-only, ~2% translate + 1.07 scale (matches BL-326) */\n@keyframes ambientGlowDrift {\n from {\n transform: translate3d(-2%, -1.5%, 0) scale(1);\n }\n to {\n transform: translate3d(2%, 2%, 0) scale(1.07);\n }\n}\n\n/* Reduced motion — disable drift, keep the glow visible */\n@media (prefers-reduced-motion: reduce) {\n .base {\n animation: none;\n }\n}\n"
|
|
4561
|
+
}
|
|
4562
|
+
]
|
|
4563
|
+
},
|
|
4564
|
+
{
|
|
4565
|
+
"name": "hero-glow",
|
|
4566
|
+
"type": "registry:ui",
|
|
4567
|
+
"description": "Breathing radial glow band for hero media. Color is driven by a live CSS custom property (--glow-color) so the caller can rewrite it every rAF frame without triggering a React re-render.",
|
|
4568
|
+
"category": "visual-elements",
|
|
4569
|
+
"dependencies": [
|
|
4570
|
+
"@loworbitstudio/visor-core"
|
|
4571
|
+
],
|
|
4572
|
+
"registryDependencies": [
|
|
4573
|
+
"utils"
|
|
4574
|
+
],
|
|
4575
|
+
"files": [
|
|
4576
|
+
{
|
|
4577
|
+
"path": "components/visual/hero-glow/hero-glow.tsx",
|
|
4578
|
+
"type": "registry:ui",
|
|
4579
|
+
"content": "import * as React from \"react\"\nimport { cn } from \"../../../lib/utils\"\nimport styles from \"./hero-glow.module.css\"\n\nexport interface HeroGlowProps extends React.HTMLAttributes<HTMLDivElement> {\n /**\n * The glow color. Accepts any CSS color value.\n *\n * This sets the `--glow-color` CSS custom property on the element.\n * The consumer may also set `--glow-color` externally (e.g., via a parent\n * CSS variable or an inline style that changes every rAF frame).\n *\n * When omitted, `--glow-color` must be set by an ancestor for the glow\n * to appear — the fallback is transparent.\n */\n glowColor?: string\n}\n\n/**\n * HeroGlow — breathing radial glow band for hero media.\n *\n * A decorative `position: absolute` element that casts a radial gradient glow\n * outside its parent box. Color is driven by the `--glow-color` CSS custom\n * property so the caller can rewrite it every rAF frame (e.g. Blacklight's\n * live artist-keyed `--color-acid`) without triggering a React re-render.\n *\n * The breathing animation (opacity 0.75 ↔ 1, scale 1 ↔ 1.03, 7s cycle) is\n * disabled when `prefers-reduced-motion: reduce` is active.\n *\n * Usage:\n * The parent element must be `position: relative`. HeroGlow extends beyond\n * the parent's box via negative inset (-6% top, -8% sides, -10% bottom).\n *\n * ```tsx\n * <div style={{ position: 'relative' }}>\n * <HeroGlow glowColor=\"oklch(70% 0.3 145)\" />\n * <img src=\"hero.jpg\" alt=\"Hero\" />\n * </div>\n * ```\n */\nconst HeroGlow = React.forwardRef<HTMLDivElement, HeroGlowProps>(\n ({ className, glowColor, style, ...props }, ref) => {\n const resolvedStyle: React.CSSProperties = {\n ...style,\n ...(glowColor ? { \"--glow-color\": glowColor } as React.CSSProperties : {}),\n }\n\n return (\n <div\n ref={ref}\n data-slot=\"hero-glow\"\n aria-hidden=\"true\"\n className={cn(styles.root, className)}\n style={resolvedStyle}\n {...props}\n />\n )\n }\n)\nHeroGlow.displayName = \"HeroGlow\"\n\nexport { HeroGlow }\n"
|
|
4580
|
+
},
|
|
4581
|
+
{
|
|
4582
|
+
"path": "components/visual/hero-glow/hero-glow.module.css",
|
|
4583
|
+
"type": "registry:ui",
|
|
4584
|
+
"content": "/* HeroGlow: breathing radial glow band for hero media\n *\n * Port of bl-hero-glow (blacklight-website globals-css, BL-326).\n *\n * Color contract:\n * Color is driven exclusively by --glow-color, which the consumer sets\n * inline or resets every rAF frame (ex. keyed to the active artist color).\n * No hex values are baked in. Fallback is transparent so nothing renders\n * when the var is unset — intentional: callers must supply a color.\n *\n * Layout:\n * position: absolute with negative inset so the glow bleeds outside the\n * parent's box. The parent must be position: relative. pointer-events: none\n * ensures the glow never captures clicks.\n *\n * Animation:\n * @keyframes hero-glow-breathe: opacity 0.75 ↔ 1, scale 1 ↔ 1.03, 7s cycle.\n * prefers-reduced-motion: animation is disabled — glow appears at rest state.\n */\n\n/* ── Keyframe ────────────────────────────────────────────────────────────── */\n\n@keyframes hero-glow-breathe {\n 0%,\n 100% {\n opacity: 0.75;\n transform: scale(1);\n }\n 50% {\n opacity: 1;\n transform: scale(1.03);\n }\n}\n\n/* ── Root ────────────────────────────────────────────────────────────────── */\n\n.root {\n position: absolute;\n inset: -6% -8% -10%;\n pointer-events: none;\n background: radial-gradient(\n 70% 60% at 50% 55%,\n color-mix(in srgb, var(--glow-color, transparent) 16%, transparent),\n transparent 72%\n );\n animation: hero-glow-breathe 7s ease-in-out infinite;\n}\n\n/* ── Reduced motion ─────────────────────────────────────────────────────── */\n\n@media (prefers-reduced-motion: reduce) {\n .root {\n animation: none;\n opacity: 0.75;\n transform: scale(1);\n }\n}\n"
|
|
4585
|
+
}
|
|
4586
|
+
]
|
|
4587
|
+
},
|
|
4588
|
+
{
|
|
4589
|
+
"name": "grain-overlay",
|
|
4590
|
+
"type": "registry:ui",
|
|
4591
|
+
"description": "Fixed, full-viewport film-grain noise layer. Ported from Blacklight's bl-grain depth-system primitive. Decorative only — pointer-events-none, aria-hidden. No dependencies beyond visor-core.",
|
|
4592
|
+
"category": "visual-elements",
|
|
4593
|
+
"dependencies": [
|
|
4594
|
+
"@loworbitstudio/visor-core"
|
|
4595
|
+
],
|
|
4596
|
+
"files": [
|
|
4597
|
+
{
|
|
4598
|
+
"path": "components/visual/grain-overlay/grain-overlay.tsx",
|
|
4599
|
+
"type": "registry:ui",
|
|
4600
|
+
"content": "import * as React from \"react\"\nimport styles from \"./grain-overlay.module.css\"\n\nexport interface GrainOverlayProps {\n /** Grain opacity. Default 0.035 matches the Blacklight bl-grain reference. */\n opacity?: number\n /** z-index of the overlay layer. Default 30 sits above page sections but below modals (z-100). */\n zIndex?: number\n}\n\n/**\n * GrainOverlay — fixed, full-viewport film-grain noise layer.\n *\n * Ports Blacklight's `.bl-grain` depth-system primitive (BL-326). Kills the\n * \"flat digital fill\" read of large near-black areas. Fixed positioning means\n * it does not scroll with content — true film behavior. Static grain (no\n * animation) avoids full-viewport repaint every frame.\n *\n * Decorative only: aria-hidden, pointer-events-none. No color channel —\n * monochrome SVG fractalNoise texture tiled at 160×160px.\n */\nexport function GrainOverlay({ opacity = 0.035, zIndex = 30 }: GrainOverlayProps) {\n return (\n <div\n aria-hidden=\"true\"\n className={styles.grain}\n style={{ opacity, zIndex }}\n />\n )\n}\n\nGrainOverlay.displayName = \"GrainOverlay\"\n"
|
|
4601
|
+
},
|
|
4602
|
+
{
|
|
4603
|
+
"path": "components/visual/grain-overlay/grain-overlay.module.css",
|
|
4604
|
+
"type": "registry:ui",
|
|
4605
|
+
"content": "/*\n * GrainOverlay — film-grain noise texture overlay.\n *\n * Ported from Blacklight bl-grain class (BL-326 depth system).\n * Fixed so it does not scroll with content (true film behavior).\n * Decorative only — pointer-events-none, aria-hidden at the call site.\n *\n * Opacity and z-index are intentionally applied via inline style so consumers\n * can override per-instance without a CSS specificity fight.\n */\n\n.grain {\n position: fixed;\n inset: 0;\n pointer-events: none;\n /* SVG fractalNoise texture — base64 to prevent class-regex false positives in the validator */\n background-image: url(\"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNjAiIGhlaWdodD0iMTYwIj48ZmlsdGVyIGlkPSJuIj48ZmVUdXJidWxlbmNlIHR5cGU9ImZyYWN0YWxOb2lzZSIgYmFzZUZyZXF1ZW5jeT0iMC45IiBudW1PY3RhdmVzPSIyIiBzdGl0Y2hUaWxlcz0ic3RpdGNoIi8+PGZlQ29sb3JNYXRyaXggdHlwZT0ic2F0dXJhdGUiIHZhbHVlcz0iMCIvPjwvZmlsdGVyPjxyZWN0IHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbHRlcj0idXJsKCNuKSIvPjwvc3ZnPgo=\");\n background-repeat: repeat;\n background-size: 160px 160px;\n}\n"
|
|
4606
|
+
}
|
|
4607
|
+
]
|
|
4608
|
+
},
|
|
4609
|
+
{
|
|
4610
|
+
"name": "card-lift",
|
|
4611
|
+
"type": "registry:ui",
|
|
4612
|
+
"description": "CSS-only hover-lift wrapper — translateY(-4px) + deep shadow + live-keyed color-mix halo on hover. Port of .bl-card-lift from blacklight-website (BL-326).",
|
|
4613
|
+
"category": "visual-elements",
|
|
4614
|
+
"dependencies": [
|
|
4615
|
+
"@loworbitstudio/visor-core"
|
|
4616
|
+
],
|
|
4617
|
+
"registryDependencies": [
|
|
4618
|
+
"utils"
|
|
4619
|
+
],
|
|
4620
|
+
"files": [
|
|
4621
|
+
{
|
|
4622
|
+
"path": "components/visual/card-lift/card-lift.tsx",
|
|
4623
|
+
"type": "registry:ui",
|
|
4624
|
+
"content": "import * as React from \"react\"\nimport { cn } from \"../../../lib/utils\"\nimport styles from \"./card-lift.module.css\"\n\n// ─── Types ──────────────────────────────────────────────────────────────────\n\nexport interface CardLiftProps extends React.HTMLAttributes<HTMLDivElement> {\n /**\n * CSS color value for the halo on hover. Accepts any CSS color, including\n * CSS custom properties (e.g. `\"var(--color-acid)\"` or `\"#6366f1\"`).\n * Written to `--lift-color` at paint time — the halo tracks live rewrites\n * without re-render.\n *\n * Defaults to `var(--accent, #6366f1)`.\n */\n liftColor?: string\n}\n\n// ─── CardLift ───────────────────────────────────────────────────────────────\n\n/**\n * CardLift — hover lift + live-keyed halo interaction.\n *\n * A thin wrapper div that applies a CSS-only hover effect:\n * - `translateY(-4px)` lift on hover\n * - Deep ambient shadow + a colored halo keyed to `--lift-color`\n * - `prefers-reduced-motion: reduce` collapses transition and transform\n *\n * Port of `.bl-card-lift` from blacklight-website (BL-326).\n *\n * @example\n * // Basic usage — halo defaults to --accent\n * <CardLift>\n * <Card>...</Card>\n * </CardLift>\n *\n * @example\n * // Live-keyed halo: pass any CSS color or custom property reference\n * <CardLift liftColor=\"var(--color-acid)\">\n * <Card>...</Card>\n * </CardLift>\n */\nconst CardLift = React.forwardRef<HTMLDivElement, CardLiftProps>(\n ({ className, liftColor, style, children, ...props }, ref) => {\n const mergedStyle: React.CSSProperties = liftColor\n ? { \"--lift-color\": liftColor, ...style } as React.CSSProperties\n : style ?? {}\n\n return (\n <div\n ref={ref}\n data-slot=\"card-lift\"\n className={cn(styles.cardLift, className)}\n style={mergedStyle}\n {...props}\n >\n {children}\n </div>\n )\n }\n)\nCardLift.displayName = \"CardLift\"\n\nexport { CardLift }\n"
|
|
4625
|
+
},
|
|
4626
|
+
{
|
|
4627
|
+
"path": "components/visual/card-lift/card-lift.module.css",
|
|
4628
|
+
"type": "registry:ui",
|
|
4629
|
+
"content": "/* CardLift — hover lift + live-keyed halo (port of bl-card-lift, BL-326)\n ========================================================================\n Applies a translateY lift, depth shadow, and a color-mix halo on hover.\n The halo color is driven by --lift-color (default: var(--accent, #6366f1)),\n which consumers can rewrite live — the paint-time var tracks any mutation\n without a React re-render. */\n\n/* ── Default: inherit the lift color from the consumer or fall back ────── */\n.cardLift {\n --lift-color: var(--accent, #6366f1);\n\n transition:\n transform 0.3s cubic-bezier(0.22, 1, 0.36, 1),\n box-shadow 0.3s ease,\n border-color 0.3s ease;\n}\n\n/* ── Hover state ─────────────────────────────────────────────────────────── */\n/* Shadow intentionally uses raw rgba() — this is a stage-lighting FX (BL-326)\n whose depth layer is spec'd at exactly rgba(0,0,0,0.8) at 24px/60px spread.\n The halo layer uses color-mix() against a runtime var, which var(--shadow-*)\n tokens cannot express. Both values are intentional; changing them would break\n visual parity with the blacklight-website source. */\n.cardLift:hover {\n transform: translateY(-4px);\n box-shadow:\n 0 24px 60px -20px rgba(0, 0, 0, 0.8),\n 0 0 60px -16px color-mix(in srgb, var(--lift-color) 30%, transparent);\n}\n\n/* ── Reduced motion ──────────────────────────────────────────────────────── */\n@media (prefers-reduced-motion: reduce) {\n .cardLift,\n .cardLift:hover {\n transform: none;\n transition: none;\n }\n}\n"
|
|
4630
|
+
}
|
|
4631
|
+
]
|
|
4632
|
+
},
|
|
4633
|
+
{
|
|
4634
|
+
"name": "browser-frame",
|
|
4635
|
+
"type": "registry:ui",
|
|
4636
|
+
"description": "Browser-chrome mockup frame with traffic-light dots and a URL pill. Wraps arbitrary content for marketing and landing surfaces. Elevation-free — compose with .lit / .lit-soft / .lit-strong for depth.",
|
|
4637
|
+
"category": "visual-elements",
|
|
4638
|
+
"dependencies": [
|
|
4639
|
+
"@loworbitstudio/visor-core",
|
|
4640
|
+
"@phosphor-icons/react"
|
|
4641
|
+
],
|
|
4642
|
+
"registryDependencies": [
|
|
4643
|
+
"utils"
|
|
4644
|
+
],
|
|
4645
|
+
"files": [
|
|
4646
|
+
{
|
|
4647
|
+
"path": "components/visual/browser-frame/browser-frame.tsx",
|
|
4648
|
+
"type": "registry:ui",
|
|
4649
|
+
"content": "import * as React from \"react\"\nimport { LockSimple } from \"@phosphor-icons/react\"\nimport { cn } from \"../../../lib/utils\"\nimport styles from \"./browser-frame.module.css\"\n\nexport interface BrowserFrameProps {\n /** Display URL shown in the chrome bar, e.g. \"sharlese.epk.pro\" */\n url: string\n /** Optional real link target; the URL pill becomes a clickable anchor */\n href?: string\n /** Content rendered inside the frame below the chrome bar */\n children: React.ReactNode\n /** Additional class name applied to the outer wrapper */\n className?: string\n}\n\n/**\n * BrowserFrame — browser-chrome mockup frame.\n *\n * Ports Blacklight's `EpkFrame` component (VI-570). Renders a chrome bar with\n * three traffic-light dots, a fake URL pill with a lock icon, and an optional\n * real link. Wraps arbitrary content below the bar.\n *\n * Elevation is deliberately excluded — compose with `.lit` / `.lit-soft` /\n * `.lit-strong` from `@loworbitstudio/visor-core/utilities` to add depth.\n *\n * Focus ring color is driven by `--browser-frame-focus-color` so Blacklight\n * (and any other consumer) can bind a keyed accent without forking the component.\n */\nexport function BrowserFrame({ url, href, children, className }: BrowserFrameProps) {\n const pill = (\n <span className={styles.pill}>\n <LockSimple\n className={styles.pillIcon}\n aria-hidden=\"true\"\n weight=\"regular\"\n size={10}\n />\n {url}\n </span>\n )\n\n return (\n <div className={cn(styles.frame, className)}>\n <div className={styles.chrome}>\n <div className={styles.dots} aria-hidden=\"true\">\n <span className={styles.dot} />\n <span className={styles.dot} />\n <span className={styles.dot} />\n </div>\n {href ? (\n <a\n href={href}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n aria-label={`Open ${url} (opens in new tab)`}\n className={styles.pillLink}\n >\n {pill}\n </a>\n ) : (\n pill\n )}\n </div>\n {children}\n </div>\n )\n}\n\nBrowserFrame.displayName = \"BrowserFrame\"\n"
|
|
4650
|
+
},
|
|
4651
|
+
{
|
|
4652
|
+
"path": "components/visual/browser-frame/browser-frame.module.css",
|
|
4653
|
+
"type": "registry:ui",
|
|
4654
|
+
"content": "/*\n * BrowserFrame — browser-chrome mockup frame.\n *\n * Ported from Blacklight EpkFrame (VI-570). Elevation is kept OUT so consumers\n * can compose with the lit / lit-soft / lit-strong utilities from visor-core/utilities.\n *\n * --browser-frame-bg: deep near-black background of the frame shell.\n * Override to use a theme surface token when the consumer has one.\n *\n * --browser-frame-focus-color: focus outline color on the URL pill link.\n * Defaults to var(--accent, currentColor) so Blacklight can bind its\n * keyed per-artist accent by setting this on the frame element.\n */\n\n.frame {\n overflow: hidden;\n border-radius: var(--radius-xl, 1rem);\n border: var(--stroke-width-thin) solid var(--hairline-strong, rgb(255 255 255 / var(--opacity-10, 0.10)));\n background-color: var(--browser-frame-bg, #0a0a0b);\n}\n\n/* Chrome bar */\n.chrome {\n position: relative;\n display: flex;\n height: var(--spacing-10, 40px);\n align-items: center;\n justify-content: center;\n border-bottom: var(--stroke-width-thin) solid var(--hairline, rgb(255 255 255 / var(--opacity-5, 0.05)));\n padding-left: var(--spacing-3, 12px);\n padding-right: var(--spacing-3, 12px);\n}\n\n/* Traffic-light dots cluster */\n.dots {\n position: absolute;\n left: var(--spacing-3_5, 14px);\n display: flex;\n align-items: center;\n gap: var(--spacing-1, 4px);\n}\n\n.dot {\n display: block;\n height: 10px;\n width: 10px;\n border-radius: 50%;\n background-color: rgb(255 255 255 / var(--opacity-12, 0.12));\n}\n\n/* URL pill (non-linked) */\n.pill {\n display: inline-flex;\n align-items: center;\n gap: var(--spacing-1, 4px);\n border-radius: var(--radius-md, 0.375rem);\n background-color: rgb(255 255 255 / var(--opacity-5, 0.05));\n padding-left: var(--spacing-3, 12px);\n padding-right: var(--spacing-3, 12px);\n padding-top: var(--spacing-1, 4px);\n padding-bottom: var(--spacing-1, 4px);\n font-size: 0.6875rem; /* 11px — intentional: mockup chrome reads at near-system scale */\n font-weight: var(--font-weight-light, 300);\n letter-spacing: 0.025em;\n color: rgb(255 255 255 / var(--opacity-60, 0.60));\n}\n\n.pillIcon {\n /* Phosphor icon inherits currentColor; the lock stays on the text baseline */\n flex-shrink: 0;\n}\n\n/* URL pill as a link */\n.pillLink {\n text-decoration: none;\n color: inherit;\n transition: color var(--motion-duration-fast, 150ms) var(--motion-easing-standard, ease);\n}\n\n.pillLink:hover .pill {\n color: rgb(255 255 255 / var(--opacity-80, 0.80));\n}\n\n.pillLink:focus-visible {\n outline: var(--focus-ring-width, 2px) solid var(--browser-frame-focus-color, var(--accent, currentColor));\n outline-offset: var(--focus-ring-offset, 2px);\n border-radius: var(--radius-md, 0.375rem);\n}\n"
|
|
4655
|
+
}
|
|
4656
|
+
]
|
|
4657
|
+
},
|
|
4343
4658
|
{
|
|
4344
4659
|
"name": "sphere",
|
|
4345
4660
|
"type": "registry:ui",
|
|
@@ -4417,7 +4732,7 @@
|
|
|
4417
4732
|
{
|
|
4418
4733
|
"path": "components/devtools/source-inspector/visor-component-names.generated.ts",
|
|
4419
4734
|
"type": "registry:devtool",
|
|
4420
|
-
"content": "// THIS FILE IS GENERATED BY scripts/generate-visor-component-names.ts.\n// Do not edit by hand. Re-run `npm run generate:component-names` after\n// adding, removing, or renaming a Visor component.\n//\n// Source of truth: registry/registry-{ui,blocks,deck,visual,devtools}.ts\n// Used by: components/devtools/source-inspector/* (VI-311)\n\nexport const VISOR_COMPONENT_NAMES: ReadonlySet<string> = new Set([\n \"AccessibilitySection\",\n \"AccessibilitySlide\",\n \"AccessibilitySpecimen\",\n \"Accordion\",\n \"AccordionContent\",\n \"AccordionItem\",\n \"AccordionTrigger\",\n \"ActivityFeed\",\n \"ActivityFeedContext\",\n \"ActivityFeedItem\",\n \"ActivityFeedRoot\",\n \"AdminDashboard\",\n \"AdminDetailDrawer\",\n \"AdminListPage\",\n \"AdminListPageInner\",\n \"AdminSettingsPage\",\n \"AdminShell\",\n \"AdminTabbedEditor\",\n \"AdminWizard\",\n \"Alert\",\n \"AlertActions\",\n \"AlertDescription\",\n \"AlertTitle\",\n \"Avatar\",\n \"AvatarFallback\",\n \"AvatarImage\",\n \"AvatarStack\",\n \"Badge\",\n \"Banner\",\n \"BannerAction\",\n \"BannerDescription\",\n \"BannerTitle\",\n \"BentoGrid\",\n \"BentoTile\",\n \"BentoTileBody\",\n \"BentoTileDescription\",\n \"BentoTileFigure\",\n \"BentoTileHeadline\",\n \"BentoTileMedia\",\n \"BentoTileMeta\",\n \"BentoTileTitle\",\n \"Box\",\n \"Breadcrumb\",\n \"BreadcrumbEllipsis\",\n \"BreadcrumbItem\",\n \"BreadcrumbLink\",\n \"BreadcrumbList\",\n \"BreadcrumbPage\",\n \"BreadcrumbSeparator\",\n \"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 \"ColorPicker\",\n \"ColorPickerSurface\",\n \"ColorSwatch\",\n \"ColorSwatchGrid\",\n \"Combobox\",\n \"ComboboxContent\",\n \"ComboboxContext\",\n \"ComboboxEmpty\",\n \"ComboboxGroup\",\n \"ComboboxInput\",\n \"ComboboxItem\",\n \"ComboboxSeparator\",\n \"Command\",\n \"CommandDialog\",\n \"CommandEmpty\",\n \"CommandGroup\",\n \"CommandInput\",\n \"CommandItem\",\n \"CommandList\",\n \"CommandLoading\",\n \"CommandSeparator\",\n \"CommandShortcut\",\n \"ComponentShowcaseContent\",\n \"ComponentShowcaseSection\",\n \"ComponentShowcaseSlide\",\n \"ConceptSlide\",\n \"ConfigurationPanel\",\n \"ConfirmDialog\",\n \"Container\",\n \"ContextMenu\",\n \"ContextMenuCheckboxItem\",\n \"ContextMenuContent\",\n \"ContextMenuGroup\",\n \"ContextMenuItem\",\n \"ContextMenuLabel\",\n \"ContextMenuPortal\",\n \"ContextMenuRadioGroup\",\n \"ContextMenuRadioItem\",\n \"ContextMenuSeparator\",\n \"ContextMenuShortcut\",\n \"ContextMenuSub\",\n \"ContextMenuSubContent\",\n \"ContextMenuSubTrigger\",\n \"ContextMenuTrigger\",\n \"Controls\",\n \"CtaSection\",\n \"DataTable\",\n \"DataTableInner\",\n \"DatePicker\",\n \"DateRangePicker\",\n \"DeckContext\",\n \"DeckFooter\",\n \"DeckLayout\",\n \"DeckProvider\",\n \"DeckRenderer\",\n \"DesignSystemDeck\",\n \"DesignSystemSpecimen\",\n \"Dialog\",\n \"DialogClose\",\n \"DialogContent\",\n \"DialogDescription\",\n \"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 \"ExportMenu\",\n \"FeaturesGrid\",\n \"Field\",\n \"FieldDescription\",\n \"FieldError\",\n \"FieldLabel\",\n \"Fieldset\",\n \"FieldsetLegend\",\n \"FileUpload\",\n \"FilterBar\",\n \"FilterChip\",\n \"FontShowcase\",\n \"FontShowcaseGrid\",\n \"Footer\",\n \"FooterSection\",\n \"Form\",\n \"FormField\",\n \"FormSpecimenSection\",\n \"FormSpecimenSlide\",\n \"FullscreenOverlay\",\n \"FullscreenOverlayContent\",\n \"FullscreenOverlayTrigger\",\n \"Grid\",\n \"Header\",\n \"Heading\",\n \"HeroSection\",\n \"HeroSlide\",\n \"HoverCard\",\n \"HoverCardContent\",\n \"HoverCardTrigger\",\n \"IconGrid\",\n \"IconGridSection\",\n \"IconSizeRow\",\n \"IconsSlide\",\n \"Image\",\n \"InfographicBar\",\n \"Inline\",\n \"Input\",\n \"Kbd\",\n \"KeyValueList\",\n \"Label\",\n \"Landing\",\n \"Lightbox\",\n \"LightboxContent\",\n \"LightboxContext\",\n \"LightboxTrigger\",\n \"LoginForm\",\n \"Marquee\",\n \"MarqueeBandRenderer\",\n \"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 \"PopoverFooter\",\n \"PopoverSelectionItem\",\n \"PopoverSelectionLabel\",\n \"PopoverSelectionList\",\n \"PopoverTrigger\",\n \"PricingSection\",\n \"ProfileMenu\",\n \"Progress\",\n \"PrototypeReview\",\n \"QuickActions\",\n \"RadioGroup\",\n \"RadioGroupItem\",\n \"RadiusScale\",\n \"RadiusSection\",\n \"RadiusSlide\",\n \"RightRailList\",\n \"ScoreIndicator\",\n \"ScrollArea\",\n \"ScrollBar\",\n \"SearchInput\",\n \"SectionHeader\",\n \"Select\",\n \"SelectContent\",\n \"SelectGroup\",\n \"SelectionListContext\",\n \"SelectItem\",\n \"SelectLabel\",\n \"SelectScrollDownButton\",\n \"SelectScrollUpButton\",\n \"SelectSeparator\",\n \"SelectTrigger\",\n \"SelectValue\",\n \"SemanticColorGrid\",\n \"SemanticColorItem\",\n \"SemanticTokensSlide\",\n \"Separator\",\n \"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 \"Stack\",\n \"Stage\",\n \"StatCard\",\n \"StatHero\",\n \"StationSpectrum\",\n \"StatusBadge\",\n \"StatusColorsSlide\",\n \"StatusDot\",\n \"Stepper\",\n \"StepperContext\",\n \"StepperDescription\",\n \"StepperItem\",\n \"StepperSeparator\",\n \"StepperTitle\",\n \"StepperTrigger\",\n \"StepsSection\",\n \"SurfaceRow\",\n \"SurfaceScaleStack\",\n \"SurfaceSection\",\n \"Switch\",\n \"Table\",\n \"TableBody\",\n \"TableCaption\",\n \"TableCell\",\n \"TableFooter\",\n \"TableHead\",\n \"TableHeader\",\n \"TableRow\",\n \"Tabs\",\n \"TabsContent\",\n \"TabsList\",\n \"TabsTrigger\",\n \"TagInput\",\n \"TestimonialAttribution\",\n \"TestimonialSection\",\n \"Text\",\n \"Textarea\",\n \"ThemeArchitectureSlide\",\n \"ThemeColorsSlide\",\n \"ThemeSwitcher\",\n \"Timeline\",\n \"TimelineContent\",\n \"TimelineDescription\",\n \"TimelineIcon\",\n \"TimelineItem\",\n \"TimelineTimestamp\",\n \"TimelineTitle\",\n \"TitleSlide\",\n \"Toaster\",\n \"TOCSlide\",\n \"ToggleButton\",\n \"ToggleDevImpl\",\n \"ToggleGroup\",\n \"ToggleGroupContext\",\n \"ToggleGroupItem\",\n \"Tooltip\",\n \"TooltipContent\",\n \"TooltipProvider\",\n \"TooltipTrigger\",\n \"TypeBodySlide\",\n \"TypeDisplaySlide\",\n \"TypeScaleStack\",\n \"TypeSpecimen\",\n \"TypographySection\",\n \"WorkspaceSwitcher\",\n])\n"
|
|
4735
|
+
"content": "// THIS FILE IS GENERATED BY scripts/generate-visor-component-names.ts.\n// Do not edit by hand. Re-run `npm run generate:component-names` after\n// adding, removing, or renaming a Visor component.\n//\n// Source of truth: registry/registry-{ui,blocks,deck,visual,devtools}.ts\n// Used by: components/devtools/source-inspector/* (VI-311)\n\nexport const VISOR_COMPONENT_NAMES: ReadonlySet<string> = new Set([\n \"AccessibilitySection\",\n \"AccessibilitySlide\",\n \"AccessibilitySpecimen\",\n \"Accordion\",\n \"AccordionContent\",\n \"AccordionItem\",\n \"AccordionTrigger\",\n \"ActivityFeed\",\n \"ActivityFeedContext\",\n \"ActivityFeedItem\",\n \"ActivityFeedRoot\",\n \"AdminDashboard\",\n \"AdminDetailDrawer\",\n \"AdminListPage\",\n \"AdminListPageInner\",\n \"AdminSettingsPage\",\n \"AdminShell\",\n \"AdminTabbedEditor\",\n \"AdminWizard\",\n \"Alert\",\n \"AlertActions\",\n \"AlertDescription\",\n \"AlertTitle\",\n \"Avatar\",\n \"AvatarFallback\",\n \"AvatarImage\",\n \"AvatarStack\",\n \"Badge\",\n \"Banner\",\n \"BannerAction\",\n \"BannerDescription\",\n \"BannerTitle\",\n \"BentoGrid\",\n \"BentoTile\",\n \"BentoTileBody\",\n \"BentoTileDescription\",\n \"BentoTileFigure\",\n \"BentoTileHeadline\",\n \"BentoTileMedia\",\n \"BentoTileMeta\",\n \"BentoTileTitle\",\n \"Box\",\n \"Breadcrumb\",\n \"BreadcrumbEllipsis\",\n \"BreadcrumbItem\",\n \"BreadcrumbLink\",\n \"BreadcrumbList\",\n \"BreadcrumbPage\",\n \"BreadcrumbSeparator\",\n \"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 \"ChallengeCard\",\n \"ChallengeCardAction\",\n \"ChallengeCardActions\",\n \"ChallengeCardBody\",\n \"ChallengeCardGate\",\n \"ChallengeCardHeader\",\n \"ChartContainer\",\n \"ChartContext\",\n \"ChartLegend\",\n \"ChartLegendContent\",\n \"ChartStyle\",\n \"ChartTooltip\",\n \"ChartTooltipContent\",\n \"Checkbox\",\n \"Chip\",\n \"ChipGroup\",\n \"ChipGroupContext\",\n \"ChipGroupItem\",\n \"ChoiceChip\",\n \"ChromeButton\",\n \"ClosingSlide\",\n \"CodeBlock\",\n \"Collapsible\",\n \"CollapsibleContent\",\n \"CollapsibleTrigger\",\n \"ColorBar\",\n \"ColorPaletteSection\",\n \"ColorPicker\",\n \"ColorPickerSurface\",\n \"ColorSwatch\",\n \"ColorSwatchGrid\",\n \"Combobox\",\n \"ComboboxContent\",\n \"ComboboxContext\",\n \"ComboboxEmpty\",\n \"ComboboxGroup\",\n \"ComboboxInput\",\n \"ComboboxItem\",\n \"ComboboxSeparator\",\n \"Command\",\n \"CommandDialog\",\n \"CommandEmpty\",\n \"CommandGroup\",\n \"CommandInput\",\n \"CommandItem\",\n \"CommandList\",\n \"CommandLoading\",\n \"CommandSeparator\",\n \"CommandShortcut\",\n \"ComponentShowcaseContent\",\n \"ComponentShowcaseSection\",\n \"ComponentShowcaseSlide\",\n \"ConceptSlide\",\n \"ConfigurationPanel\",\n \"ConfirmDialog\",\n \"Container\",\n \"ContextMenu\",\n \"ContextMenuCheckboxItem\",\n \"ContextMenuContent\",\n \"ContextMenuGroup\",\n \"ContextMenuItem\",\n \"ContextMenuLabel\",\n \"ContextMenuPortal\",\n \"ContextMenuRadioGroup\",\n \"ContextMenuRadioItem\",\n \"ContextMenuSeparator\",\n \"ContextMenuShortcut\",\n \"ContextMenuSub\",\n \"ContextMenuSubContent\",\n \"ContextMenuSubTrigger\",\n \"ContextMenuTrigger\",\n \"Controls\",\n \"CtaSection\",\n \"DataTable\",\n \"DataTableInner\",\n \"DatePicker\",\n \"DateRangePicker\",\n \"DeckContext\",\n \"DeckFooter\",\n \"DeckLayout\",\n \"DeckProvider\",\n \"DeckRenderer\",\n \"DesignSystemDeck\",\n \"DesignSystemSpecimen\",\n \"Dialog\",\n \"DialogClose\",\n \"DialogContent\",\n \"DialogDescription\",\n \"DialogFooter\",\n \"DialogHeader\",\n \"DialogOverlay\",\n \"DialogPortal\",\n \"DialogTitle\",\n \"DialogTrigger\",\n \"DotNav\",\n \"DropdownMenu\",\n \"DropdownMenuCheckboxItem\",\n \"DropdownMenuContent\",\n \"DropdownMenuGroup\",\n \"DropdownMenuItem\",\n \"DropdownMenuLabel\",\n \"DropdownMenuPortal\",\n \"DropdownMenuRadioGroup\",\n \"DropdownMenuRadioItem\",\n \"DropdownMenuSeparator\",\n \"DropdownMenuShortcut\",\n \"DropdownMenuSub\",\n \"DropdownMenuSubContent\",\n \"DropdownMenuSubTrigger\",\n \"DropdownMenuTrigger\",\n \"ElevationCard\",\n \"ElevationSlide\",\n \"EmptyState\",\n \"ExportMenu\",\n \"FeaturesGrid\",\n \"Field\",\n \"FieldDescription\",\n \"FieldError\",\n \"FieldLabel\",\n \"Fieldset\",\n \"FieldsetLegend\",\n \"FileUpload\",\n \"FilterBar\",\n \"FilterChip\",\n \"FontShowcase\",\n \"FontShowcaseGrid\",\n \"Footer\",\n \"FooterSection\",\n \"Form\",\n \"FormField\",\n \"FormSpecimenSection\",\n \"FormSpecimenSlide\",\n \"FullscreenOverlay\",\n \"FullscreenOverlayContent\",\n \"FullscreenOverlayTrigger\",\n \"Grid\",\n \"Header\",\n \"Heading\",\n \"HeroSection\",\n \"HeroSlide\",\n \"HoverCard\",\n \"HoverCardContent\",\n \"HoverCardTrigger\",\n \"IconGrid\",\n \"IconGridSection\",\n \"IconSizeRow\",\n \"IconsSlide\",\n \"Image\",\n \"InfographicBar\",\n \"Inline\",\n \"Input\",\n \"Kbd\",\n \"KeyValueList\",\n \"Label\",\n \"Landing\",\n \"Lightbox\",\n \"LightboxContent\",\n \"LightboxContext\",\n \"LightboxTrigger\",\n \"LoginForm\",\n \"Marquee\",\n \"MarqueeBandRenderer\",\n \"MatrixCell\",\n \"MatrixTable\",\n \"MatrixTableInner\",\n \"Menubar\",\n \"MenubarCheckboxItem\",\n \"MenubarContent\",\n \"MenubarGroup\",\n \"MenubarItem\",\n \"MenubarLabel\",\n \"MenubarMenu\",\n \"MenubarRadioGroup\",\n \"MenubarRadioItem\",\n \"MenubarSeparator\",\n \"MenubarShortcut\",\n \"MenubarSub\",\n \"MenubarSubContent\",\n \"MenubarSubTrigger\",\n \"MenubarTrigger\",\n \"MotionDuration\",\n \"MotionDurationSection\",\n \"MotionEasing\",\n \"MotionEasingSection\",\n \"MotionSlide\",\n \"NameRoster\",\n \"NameRosterItem\",\n \"Navbar\",\n \"NavbarBrand\",\n \"NavbarContent\",\n \"NavbarItem\",\n \"NavbarLink\",\n \"NavbarToggle\",\n \"NumberInput\",\n \"OpacityBar\",\n \"OpacitySlide\",\n \"OTPInput\",\n \"PageHeader\",\n \"Pagination\",\n \"PaginationContent\",\n \"PaginationEllipsis\",\n \"PaginationItem\",\n \"PaginationLink\",\n \"PaginationNext\",\n \"PaginationPrevious\",\n \"PasswordInput\",\n \"PhoneInput\",\n \"Popover\",\n \"PopoverAnchor\",\n \"PopoverContent\",\n \"PopoverFooter\",\n \"PopoverSelectionItem\",\n \"PopoverSelectionLabel\",\n \"PopoverSelectionList\",\n \"PopoverTrigger\",\n \"PricingSection\",\n \"ProfileMenu\",\n \"Progress\",\n \"PrototypeReview\",\n \"QuickActions\",\n \"RadioGroup\",\n \"RadioGroupItem\",\n \"RadiusScale\",\n \"RadiusSection\",\n \"RadiusSlide\",\n \"RightRailList\",\n \"ScoreIndicator\",\n \"ScrollArea\",\n \"ScrollBar\",\n \"SearchInput\",\n \"SectionHeader\",\n \"SectionIntro\",\n \"SectionNav\",\n \"SectionNavItem\",\n \"Select\",\n \"SelectContent\",\n \"SelectGroup\",\n \"SelectionListContext\",\n \"SelectItem\",\n \"SelectLabel\",\n \"SelectScrollDownButton\",\n \"SelectScrollUpButton\",\n \"SelectSeparator\",\n \"SelectTrigger\",\n \"SelectValue\",\n \"SemanticColorGrid\",\n \"SemanticColorItem\",\n \"SemanticTokensSlide\",\n \"Separator\",\n \"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 \"Spinner\",\n \"Stack\",\n \"Stage\",\n \"StatCard\",\n \"StatHero\",\n \"StationSpectrum\",\n \"StatusBadge\",\n \"StatusColorsSlide\",\n \"StatusDot\",\n \"Stepper\",\n \"StepperContext\",\n \"StepperDescription\",\n \"StepperItem\",\n \"StepperSeparator\",\n \"StepperTitle\",\n \"StepperTrigger\",\n \"StepsSection\",\n \"SurfaceRow\",\n \"SurfaceScaleStack\",\n \"SurfaceSection\",\n \"Switch\",\n \"Table\",\n \"TableBody\",\n \"TableCaption\",\n \"TableCell\",\n \"TableFooter\",\n \"TableHead\",\n \"TableHeader\",\n \"TableRow\",\n \"Tabs\",\n \"TabsContent\",\n \"TabsList\",\n \"TabsTrigger\",\n \"TagInput\",\n \"TestimonialAttribution\",\n \"TestimonialSection\",\n \"Text\",\n \"Textarea\",\n \"ThemeArchitectureSlide\",\n \"ThemeColorsSlide\",\n \"ThemeSwitcher\",\n \"Timeline\",\n \"TimelineContent\",\n \"TimelineDescription\",\n \"TimelineIcon\",\n \"TimelineItem\",\n \"TimelineTimestamp\",\n \"TimelineTitle\",\n \"TitleSlide\",\n \"Toaster\",\n \"TOCSlide\",\n \"ToggleButton\",\n \"ToggleDevImpl\",\n \"ToggleGroup\",\n \"ToggleGroupContext\",\n \"ToggleGroupItem\",\n \"Tooltip\",\n \"TooltipContent\",\n \"TooltipProvider\",\n \"TooltipTrigger\",\n \"TypeBodySlide\",\n \"TypeDisplaySlide\",\n \"TypeScaleStack\",\n \"TypeSpecimen\",\n \"TypographySection\",\n \"WorkspaceSwitcher\",\n])\n"
|
|
4421
4736
|
}
|
|
4422
4737
|
]
|
|
4423
4738
|
},
|