@loworbitstudio/visor 1.11.1 → 1.12.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 +49 -1
- package/dist/registry.json +206 -6
- package/dist/visor-manifest.json +655 -3
- package/package.json +1 -1
package/dist/registry.json
CHANGED
|
@@ -522,12 +522,12 @@
|
|
|
522
522
|
{
|
|
523
523
|
"path": "components/ui/skeleton/skeleton.tsx",
|
|
524
524
|
"type": "registry:ui",
|
|
525
|
-
"content": "import * as React from \"react\"\nimport { cn } from \"../../../lib/utils\"\nimport styles from \"./skeleton.module.css\"\n\nconst Skeleton = React.forwardRef<\n HTMLDivElement,\n React.HTMLAttributes<HTMLDivElement>\n>(({ className, ...props }, ref) => {\n return (\n <div\n ref={ref}\n data-slot=\"skeleton\"\n className={cn(styles.skeleton, className)}\n {...props}\n />\n )\n})\nSkeleton.displayName = \"Skeleton\"\n\nexport { Skeleton }\n"
|
|
525
|
+
"content": "import * as React from \"react\"\nimport { cn } from \"../../../lib/utils\"\nimport styles from \"./skeleton.module.css\"\n\nconst Skeleton = React.forwardRef<\n HTMLDivElement,\n React.HTMLAttributes<HTMLDivElement>\n>(({ className, ...props }, ref) => {\n return (\n <div\n ref={ref}\n data-slot=\"skeleton\"\n className={cn(styles.skeleton, className)}\n {...props}\n />\n )\n})\nSkeleton.displayName = \"Skeleton\"\n\n// ---------------------------------------------------------------------------\n// SkeletonList — list-row loading placeholder\n//\n// Renders `count` rows, each with an avatar circle, two text lines, and an\n// optional badge pill. Mirrors the list-row anatomy from the VI-584 spec.\n// All shapes are `.skeleton` instances so they share the shimmer animation.\n// ---------------------------------------------------------------------------\nexport interface SkeletonListProps extends React.HTMLAttributes<HTMLDivElement> {\n /** Number of placeholder rows to render. Defaults to 3. */\n count?: number\n}\n\nconst SkeletonList = React.forwardRef<HTMLDivElement, SkeletonListProps>(\n ({ count = 3, className, ...props }, ref) => {\n return (\n <div\n ref={ref}\n role=\"status\"\n aria-label=\"Loading list\"\n data-slot=\"skeleton-list\"\n className={cn(styles.skeletonList, className)}\n {...props}\n >\n {Array.from({ length: count }, (_, i) => (\n <div key={i} className={styles.skeletonListRow}>\n {/* Avatar circle */}\n <div\n className={cn(styles.skeleton, styles.skeletonAvatar)}\n aria-hidden=\"true\"\n />\n {/* Text column */}\n <div className={styles.skeletonListText}>\n <div\n className={cn(styles.skeleton, styles.skeletonLineHeading, styles.skeletonW40)}\n aria-hidden=\"true\"\n />\n <div\n className={cn(styles.skeleton, styles.skeletonLineBody, styles.skeletonW70)}\n aria-hidden=\"true\"\n />\n </div>\n {/* Badge pill */}\n <div\n className={cn(styles.skeleton, styles.skeletonBadge)}\n aria-hidden=\"true\"\n />\n </div>\n ))}\n </div>\n )\n }\n)\nSkeletonList.displayName = \"SkeletonList\"\n\n// ---------------------------------------------------------------------------\n// SkeletonTable — table-row loading placeholder\n//\n// Renders `rows` rows with `columns` cells each. Useful for data tables where\n// the row height and column count mirror the real table structure.\n// ---------------------------------------------------------------------------\nexport interface SkeletonTableProps extends React.HTMLAttributes<HTMLDivElement> {\n /** Number of table rows to render. Defaults to 4. */\n rows?: number\n /** Number of columns per row. Defaults to 4. */\n columns?: number\n}\n\nconst SkeletonTable = React.forwardRef<HTMLDivElement, SkeletonTableProps>(\n ({ rows = 4, columns = 4, className, ...props }, ref) => {\n return (\n <div\n ref={ref}\n role=\"status\"\n aria-label=\"Loading table\"\n data-slot=\"skeleton-table\"\n className={cn(styles.skeletonTable, className)}\n {...props}\n >\n {Array.from({ length: rows }, (_, ri) => (\n <div key={ri} className={styles.skeletonTableRow}>\n {Array.from({ length: columns }, (_, ci) => {\n // Vary widths across cells so rows look organic, not uniform\n const widthClass =\n ci === 0\n ? styles.skeletonW60\n : ci === columns - 1\n ? styles.skeletonW40\n : styles.skeletonW80\n return (\n <div key={ci} className={styles.skeletonTableCell}>\n <div\n className={cn(styles.skeleton, styles.skeletonLineBody, widthClass)}\n aria-hidden=\"true\"\n />\n </div>\n )\n })}\n </div>\n ))}\n </div>\n )\n }\n)\nSkeletonTable.displayName = \"SkeletonTable\"\n\n// ---------------------------------------------------------------------------\n// SkeletonDetail — detail / profile-view loading placeholder\n//\n// Renders a large avatar block, a heading-height line, and a block of body\n// text lines. Useful for detail panels, profile pages, and sidebars.\n// ---------------------------------------------------------------------------\nexport interface SkeletonDetailProps extends React.HTMLAttributes<HTMLDivElement> {\n /** Number of body text lines below the heading. Defaults to 3. */\n lines?: number\n}\n\nconst SkeletonDetail = React.forwardRef<HTMLDivElement, SkeletonDetailProps>(\n ({ lines = 3, className, ...props }, ref) => {\n // Alternate widths for body lines: 80%, 70%, 60%, repeat\n const lineWidths = [styles.skeletonW80, styles.skeletonW70, styles.skeletonW60]\n\n return (\n <div\n ref={ref}\n role=\"status\"\n aria-label=\"Loading detail\"\n data-slot=\"skeleton-detail\"\n className={cn(styles.skeletonDetail, className)}\n {...props}\n >\n {/* Large avatar / thumbnail */}\n <div\n className={cn(styles.skeleton, styles.skeletonAvatarLg)}\n aria-hidden=\"true\"\n />\n {/* Content block */}\n <div className={styles.skeletonDetailContent}>\n {/* Heading line */}\n <div\n className={cn(styles.skeleton, styles.skeletonLineH1, styles.skeletonW60)}\n aria-hidden=\"true\"\n />\n {/* Body lines */}\n {Array.from({ length: lines }, (_, i) => (\n <div\n key={i}\n className={cn(\n styles.skeleton,\n styles.skeletonLineBody,\n lineWidths[i % lineWidths.length]\n )}\n aria-hidden=\"true\"\n />\n ))}\n </div>\n </div>\n )\n }\n)\nSkeletonDetail.displayName = \"SkeletonDetail\"\n\nexport { Skeleton, SkeletonList, SkeletonTable, SkeletonDetail }\n"
|
|
526
526
|
},
|
|
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 /* 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"
|
|
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\n/* ── Content-shape-aware primitives (VI-584) ──────────────────────────────\n Height / radius / width helpers used internally by SkeletonList,\n SkeletonTable, and SkeletonDetail. All dimensions trace to tokens or are\n documented as intentional pixel values that mirror spec anatomy. */\n\n/* Bar heights */\n.skeletonLineH1 { height: 24px; border-radius: var(--radius-md, 0.375rem); }\n.skeletonLineHeading { height: 18px; border-radius: var(--radius-md, 0.375rem); }\n.skeletonLineBody { height: 14px; border-radius: var(--radius-sm, 0.25rem); }\n\n/* Avatar / thumbnail */\n.skeletonAvatar {\n width: 40px;\n height: 40px;\n border-radius: var(--radius-full, 9999px);\n flex-shrink: 0;\n}\n\n/* Larger avatar for detail views */\n.skeletonAvatarLg {\n width: 64px;\n height: 64px;\n border-radius: var(--radius-full, 9999px);\n flex-shrink: 0;\n}\n\n/* Badge pill used in list rows */\n.skeletonBadge {\n width: 72px;\n height: 22px;\n border-radius: var(--radius-full, 9999px);\n flex-shrink: 0;\n}\n\n/* Width utilities — fractional widths for organic line-length variety */\n.skeletonW40 { width: 40%; }\n.skeletonW60 { width: 60%; }\n.skeletonW70 { width: 70%; }\n.skeletonW80 { width: 80%; }\n.skeletonWFull { width: 100%; }\n\n/* ── SkeletonList layout ─── */\n.skeletonList {\n display: flex;\n flex-direction: column;\n}\n\n.skeletonListRow {\n display: flex;\n align-items: center;\n gap: var(--spacing-3, 0.75rem);\n padding: var(--spacing-3, 0.75rem) 0;\n border-bottom: 1px solid var(--border-default, #e5e7eb);\n}\n\n/* Remove bottom border on last row */\n.skeletonListRow:last-child {\n border-bottom: none;\n}\n\n.skeletonListText {\n display: flex;\n flex-direction: column;\n gap: var(--spacing-2, 0.5rem);\n flex: 1;\n}\n\n/* ── SkeletonTable layout ─── */\n.skeletonTable {\n display: flex;\n flex-direction: column;\n gap: 0;\n}\n\n.skeletonTableRow {\n display: flex;\n align-items: center;\n gap: var(--spacing-4, 1rem);\n padding: var(--spacing-3, 0.75rem) 0;\n border-bottom: 1px solid var(--border-default, #e5e7eb);\n}\n\n.skeletonTableRow:last-child {\n border-bottom: none;\n}\n\n.skeletonTableCell {\n flex: 1;\n}\n\n/* ── SkeletonDetail layout ─── */\n.skeletonDetail {\n display: flex;\n align-items: flex-start;\n gap: var(--spacing-4, 1rem);\n}\n\n.skeletonDetailContent {\n display: flex;\n flex-direction: column;\n gap: var(--spacing-2, 0.5rem);\n flex: 1;\n}\n"
|
|
531
531
|
}
|
|
532
532
|
]
|
|
533
533
|
},
|
|
@@ -556,6 +556,30 @@
|
|
|
556
556
|
}
|
|
557
557
|
]
|
|
558
558
|
},
|
|
559
|
+
{
|
|
560
|
+
"name": "slow-network-bar",
|
|
561
|
+
"type": "registry:ui",
|
|
562
|
+
"description": "A 4px indeterminate progress bar that appears after a configurable threshold (default 3 s) to signal slow or stalled network requests. Includes useSlowRequest hook for automatic threshold management. Indeterminate animation only — no fake percentages.",
|
|
563
|
+
"category": "feedback",
|
|
564
|
+
"dependencies": [
|
|
565
|
+
"@loworbitstudio/visor-core"
|
|
566
|
+
],
|
|
567
|
+
"registryDependencies": [
|
|
568
|
+
"utils"
|
|
569
|
+
],
|
|
570
|
+
"files": [
|
|
571
|
+
{
|
|
572
|
+
"path": "components/ui/slow-network-bar/slow-network-bar.tsx",
|
|
573
|
+
"type": "registry:ui",
|
|
574
|
+
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport { cn } from \"../../../lib/utils\"\nimport styles from \"./slow-network-bar.module.css\"\n\nexport type SlowNetworkBarState = \"hidden\" | \"visible\" | \"resolving\"\n\nexport interface SlowNetworkBarProps\n extends Omit<React.HTMLAttributes<HTMLDivElement>, \"children\"> {\n /**\n * Visibility state of the slow-network bar.\n *\n * - `\"hidden\"` — Not rendered; fast requests never trigger this.\n * - `\"visible\"` — Indeterminate animation active; request is still pending past the threshold.\n * - `\"resolving\"` — Bar completes a full sweep then fades out (400ms).\n *\n * @default \"hidden\"\n */\n state?: SlowNetworkBarState\n /**\n * Accessible label for the progress bar. Announced to screen readers when the\n * bar becomes visible.\n * @default \"Loading, please wait…\"\n */\n label?: string\n}\n\n/**\n * SlowNetworkBar\n *\n * A 4px progress bar pinned to the top of a content zone that appears only when\n * a request has been pending longer than a threshold (~3 s). Uses an indeterminate\n * animation because the remaining time is unknown — showing false percentage\n * progress creates user anxiety.\n *\n * Pairs with `useSlowRequest` for automatic threshold detection, or can be driven\n * manually via the `state` prop.\n *\n * Composition rules:\n * - Never shown alongside a Skeleton — choose one per loading zone.\n * - On error, set state to `\"hidden\"` immediately; let the error pattern take over.\n * - Does not block interaction; user can cancel or navigate away while bar is visible.\n */\nconst SlowNetworkBar = React.forwardRef<HTMLDivElement, SlowNetworkBarProps>(\n (\n {\n className,\n state = \"hidden\",\n label = \"Loading, please wait…\",\n ...props\n },\n ref\n ) => {\n return (\n <div\n ref={ref}\n data-slot=\"slow-network-bar\"\n data-state={state}\n role=\"progressbar\"\n aria-label={label}\n aria-valuemin={0}\n aria-valuemax={100}\n aria-busy={state !== \"hidden\"}\n className={cn(\n styles.wrap,\n state === \"visible\" && styles.wrapVisible,\n state === \"resolving\" && styles.wrapResolving,\n className\n )}\n {...props}\n >\n <div\n data-slot=\"slow-network-bar-fill\"\n className={cn(\n styles.fill,\n state === \"visible\" && styles.fillIndeterminate,\n state === \"resolving\" && styles.fillResolving\n )}\n />\n </div>\n )\n }\n)\nSlowNetworkBar.displayName = \"SlowNetworkBar\"\n\n/**\n * useSlowRequest\n *\n * Hook that drives SlowNetworkBar state automatically. Starts a timer on mount;\n * if the request is still pending after `threshold` ms, the bar becomes visible.\n * Cleans up on unmount (component cancelled / navigated away).\n *\n * @param threshold — Milliseconds before the bar appears. Default 3000ms.\n * @returns `{ state, trigger, resolve, reset }` — call `trigger()` when the\n * request starts, `resolve()` when it completes (success or error), `reset()`\n * to return to hidden immediately.\n *\n * @example\n * ```tsx\n * const { state, trigger, resolve } = useSlowRequest(3000);\n *\n * const handleExport = async () => {\n * trigger();\n * try {\n * await exportReport();\n * } finally {\n * resolve();\n * }\n * };\n *\n * return (\n * <>\n * <SlowNetworkBar state={state} />\n * <button onClick={handleExport}>Export</button>\n * </>\n * );\n * ```\n */\nfunction useSlowRequest(threshold = 3000) {\n const [state, setState] = React.useState<SlowNetworkBarState>(\"hidden\")\n const thresholdRef = React.useRef<ReturnType<typeof setTimeout> | null>(null)\n const resolveTimerRef = React.useRef<ReturnType<typeof setTimeout> | null>(null)\n\n const clearTimers = React.useCallback(() => {\n if (thresholdRef.current !== null) {\n clearTimeout(thresholdRef.current)\n thresholdRef.current = null\n }\n if (resolveTimerRef.current !== null) {\n clearTimeout(resolveTimerRef.current)\n resolveTimerRef.current = null\n }\n }, [])\n\n const trigger = React.useCallback(() => {\n clearTimers()\n setState(\"hidden\")\n thresholdRef.current = setTimeout(() => {\n setState(\"visible\")\n }, threshold)\n }, [threshold, clearTimers])\n\n const resolve = React.useCallback(() => {\n clearTimers()\n setState((prev) => {\n if (prev === \"visible\") {\n // Complete the sweep animation then fade out\n resolveTimerRef.current = setTimeout(() => {\n setState(\"hidden\")\n }, 800)\n return \"resolving\"\n }\n // Fast path: request resolved before threshold — never show bar\n return \"hidden\"\n })\n }, [clearTimers])\n\n const reset = React.useCallback(() => {\n clearTimers()\n setState(\"hidden\")\n }, [clearTimers])\n\n // Cleanup on unmount\n React.useEffect(() => {\n return () => clearTimers()\n }, [clearTimers])\n\n return { state, trigger, resolve, reset }\n}\n\nexport { SlowNetworkBar, useSlowRequest }\n"
|
|
575
|
+
},
|
|
576
|
+
{
|
|
577
|
+
"path": "components/ui/slow-network-bar/slow-network-bar.module.css",
|
|
578
|
+
"type": "registry:ui",
|
|
579
|
+
"content": "/*\n * SlowNetworkBar\n *\n * 4px progress bar pinned below the navigation bar. Appears only after a\n * configurable threshold (default 3s) to prevent flicker on fast connections.\n * Uses an indeterminate animation — never percentage-based, because the remaining\n * time is unknown and fake progress creates anxiety.\n *\n * Token references:\n * Fill gradient: --primary (leading) → --accent (mid) → --primary (trailing)\n * Track: transparent (no background rail — bar floats above content)\n * Motion: --motion-duration-300 (fade-in), --motion-easing-ease-out (entrance),\n * --motion-easing-ease-in (exit), --motion-duration-1500 (sweep loop)\n */\n\n/* ── Keyframes ── */\n\n@keyframes slow-network-indeterminate {\n 0% {\n left: -60%;\n width: 60%;\n }\n 50% {\n left: 20%;\n width: 80%;\n }\n 100% {\n left: 110%;\n width: 60%;\n }\n}\n\n@keyframes slow-network-fade-out {\n from {\n opacity: 1;\n }\n to {\n opacity: 0;\n }\n}\n\n/* ── Wrapper ── */\n\n.wrap {\n /* 4px height per spec */\n height: 4px;\n width: 100%;\n position: relative;\n overflow: hidden;\n background: transparent;\n\n /* hidden by default — fast requests never see this */\n opacity: 0;\n pointer-events: none;\n}\n\n/* visible state — fade in over 300ms (ease-out entrance) */\n.wrap.wrapVisible {\n opacity: 1;\n transition: opacity var(--motion-duration-300, 300ms) var(--motion-easing-ease-out, cubic-bezier(0, 0, 0.2, 1));\n pointer-events: none;\n}\n\n/* resolving state — sweep completes, then fade out */\n.wrap.wrapResolving {\n opacity: 1;\n animation: slow-network-fade-out\n var(--motion-duration-300, 300ms)\n var(--motion-easing-ease-in, cubic-bezier(0.4, 0, 1, 1))\n 400ms\n forwards;\n pointer-events: none;\n}\n\n/* ── Fill bar ── */\n\n.fill {\n position: absolute;\n top: 0;\n height: 100%;\n /* Default — invisible (hidden state) */\n width: 0;\n background: transparent;\n}\n\n/* indeterminate sweep animation — looping, left-to-right */\n.fill.fillIndeterminate {\n background: linear-gradient(\n 90deg,\n var(--primary, #111827),\n var(--accent, #111827),\n var(--primary, #111827)\n );\n background-size: 200% 100%;\n animation: slow-network-indeterminate var(--motion-duration-1500, 1500ms) var(--motion-easing-ease-in-out, cubic-bezier(0.4, 0, 0.2, 1)) infinite;\n left: -60%;\n width: 60%;\n}\n\n/* resolving — expand to full width, then the wrapper fades out */\n.fill.fillResolving {\n background: var(--primary, #111827);\n left: 0;\n width: 100%;\n transition: width var(--motion-duration-300, 300ms) var(--motion-easing-ease-out, cubic-bezier(0, 0, 0.2, 1));\n}\n\n/* ── Reduced motion ── */\n\n@media (prefers-reduced-motion: reduce) {\n .wrap.wrapVisible {\n transition: none;\n }\n\n .wrap.wrapResolving {\n animation: none;\n opacity: 0;\n }\n\n .fill.fillIndeterminate {\n animation: none;\n left: 0;\n width: 100%;\n opacity: var(--opacity-40, 0.4);\n }\n\n .fill.fillResolving {\n transition: none;\n }\n}\n"
|
|
580
|
+
}
|
|
581
|
+
]
|
|
582
|
+
},
|
|
559
583
|
{
|
|
560
584
|
"name": "tooltip",
|
|
561
585
|
"type": "registry:ui",
|
|
@@ -631,6 +655,32 @@
|
|
|
631
655
|
}
|
|
632
656
|
]
|
|
633
657
|
},
|
|
658
|
+
{
|
|
659
|
+
"name": "coherence-check",
|
|
660
|
+
"type": "registry:ui",
|
|
661
|
+
"description": "Check group with pass/warn/fail rows for displaying coherence audit results — group header, state icon circle, title, description with inline code support, and ghost Fix action button.",
|
|
662
|
+
"category": "feedback",
|
|
663
|
+
"dependencies": [
|
|
664
|
+
"class-variance-authority",
|
|
665
|
+
"@phosphor-icons/react",
|
|
666
|
+
"@loworbitstudio/visor-core"
|
|
667
|
+
],
|
|
668
|
+
"registryDependencies": [
|
|
669
|
+
"utils"
|
|
670
|
+
],
|
|
671
|
+
"files": [
|
|
672
|
+
{
|
|
673
|
+
"path": "components/ui/coherence-check/coherence-check.tsx",
|
|
674
|
+
"type": "registry:ui",
|
|
675
|
+
"content": "import * as React from \"react\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\nimport { Check, Warning, X } from \"@phosphor-icons/react\"\nimport { cn } from \"../../../lib/utils\"\nimport styles from \"./coherence-check.module.css\"\n\n// ─── CheckGroup ─────────────────────────────────────────────────────────────\n\nexport interface CheckGroupProps extends React.HTMLAttributes<HTMLDivElement> {\n /** Group header label (rendered uppercase). */\n heading: string\n}\n\nconst CheckGroup = React.forwardRef<HTMLDivElement, CheckGroupProps>(\n ({ className, heading, children, ...props }, ref) => {\n return (\n <div\n ref={ref}\n data-slot=\"check-group\"\n className={cn(styles.group, className)}\n {...props}\n >\n <p className={styles.groupHeading}>{heading}</p>\n {children}\n </div>\n )\n }\n)\nCheckGroup.displayName = \"CheckGroup\"\n\n// ─── CheckRow variants ───────────────────────────────────────────────────────\n\nconst checkRowVariants = cva(styles.row, {\n variants: {\n state: {\n pass: styles.statePass,\n warn: styles.stateWarn,\n fail: styles.stateFail,\n },\n },\n defaultVariants: {\n state: \"pass\",\n },\n})\n\n// ─── Icon lookup ─────────────────────────────────────────────────────────────\n\nconst STATE_ICON = {\n pass: Check,\n warn: Warning,\n fail: X,\n} as const\n\n// ─── CheckRow ────────────────────────────────────────────────────────────────\n\nexport interface CheckRowProps\n extends Omit<React.HTMLAttributes<HTMLDivElement>, \"title\">,\n VariantProps<typeof checkRowVariants> {\n /** Check title / finding label. */\n title: string\n /**\n * Supporting description. Supports inline `<code>` elements for token names.\n * Pass a React node to use `<code>` inline; pass a string for plain text.\n */\n description?: React.ReactNode\n /**\n * Label for the ghost Fix action button. When provided, the button renders\n * right-aligned. When omitted, no button is shown (e.g. pass rows).\n */\n fixLabel?: string\n /** Callback invoked when the Fix action button is clicked. */\n onFix?: () => void\n}\n\nconst CheckRow = React.forwardRef<HTMLDivElement, CheckRowProps>(\n (\n {\n className,\n state,\n title,\n description,\n fixLabel,\n onFix,\n ...props\n },\n ref\n ) => {\n const resolvedState = state ?? \"pass\"\n const Icon = STATE_ICON[resolvedState]\n\n return (\n <div\n ref={ref}\n data-slot=\"check-row\"\n data-state={resolvedState}\n className={cn(checkRowVariants({ state }), className)}\n {...props}\n >\n <span\n data-slot=\"check-row-icon\"\n className={styles.icon}\n aria-hidden=\"true\"\n >\n <Icon weight=\"bold\" />\n </span>\n <div data-slot=\"check-row-body\" className={styles.body}>\n <strong className={styles.rowTitle}>{title}</strong>\n {description != null && (\n <p className={styles.rowDescription}>{description}</p>\n )}\n </div>\n {fixLabel != null && (\n <button\n type=\"button\"\n data-slot=\"check-row-fix\"\n className={styles.fixButton}\n onClick={onFix}\n >\n {fixLabel}\n </button>\n )}\n </div>\n )\n }\n)\nCheckRow.displayName = \"CheckRow\"\n\nexport { CheckGroup, CheckRow, checkRowVariants }\n"
|
|
676
|
+
},
|
|
677
|
+
{
|
|
678
|
+
"path": "components/ui/coherence-check/coherence-check.module.css",
|
|
679
|
+
"type": "registry:ui",
|
|
680
|
+
"content": "/* ─── CheckGroup ─────────────────────────────────────────────────────────── */\n\n.group {\n margin-bottom: var(--spacing-5, 1.25rem);\n}\n\n/* Uppercase group header */\n.groupHeading {\n margin: 0 0 var(--spacing-2, 0.5rem);\n padding: 0 calc(var(--spacing-1, 0.25rem) / 2);\n font-size: var(--font-size-xs, 0.75rem);\n font-weight: var(--font-weight-extrabold, 800);\n letter-spacing: 0.06em;\n text-transform: uppercase;\n color: var(--text-tertiary, #9ca3af);\n line-height: 1.4;\n}\n\n/* ─── CheckRow ───────────────────────────────────────────────────────────── */\n\n.row {\n display: flex;\n align-items: flex-start;\n gap: var(--spacing-3, 0.75rem);\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) var(--spacing-4, 1rem);\n margin-bottom: var(--spacing-2, 0.5rem);\n box-shadow: var(--shadow-sm);\n}\n\n/* Last row in a group has no bottom margin */\n.row:last-child {\n margin-bottom: 0;\n}\n\n/* ─── State icon circle ─────────────────────────────────────────────────── */\n\n.icon {\n flex: none;\n width: var(--spacing-6, 1.5rem);\n height: var(--spacing-6, 1.5rem);\n border-radius: var(--radius-full, 9999px);\n display: grid;\n place-items: center;\n margin-top: calc(var(--spacing-1, 0.25rem) / 4);\n color: var(--text-inverse, #ffffff);\n}\n\n.icon svg {\n width: var(--spacing-4, 1rem);\n height: var(--spacing-4, 1rem);\n}\n\n/* State — icon background tints */\n.statePass .icon {\n background: var(--success, #22c55e);\n}\n\n.stateWarn .icon {\n background: var(--warning, #f59e0b);\n}\n\n.stateFail .icon {\n background: var(--destructive, #ef4444);\n}\n\n/* ─── Body (title + description) ─────────────────────────────────────────── */\n\n.body {\n flex: 1;\n min-width: 0;\n}\n\n.rowTitle {\n display: block;\n font-size: var(--font-size-sm, 0.875rem);\n font-weight: var(--font-weight-bold, 700);\n color: var(--text-primary, #111827);\n line-height: 1.4;\n}\n\n.rowDescription {\n margin: calc(var(--spacing-1, 0.25rem) * 0.75) 0 0;\n font-size: var(--font-size-xs, 0.75rem);\n color: var(--text-secondary, #6b7280);\n line-height: 1.5;\n}\n\n/* Inline code token support within description */\n.rowDescription code {\n font-family: var(--font-mono, ui-monospace, monospace);\n font-size: var(--font-size-xs, 0.75rem);\n background: var(--surface-muted, #f3f4f6);\n padding: var(--stroke-width-thin, 1px) var(--spacing-1, 0.25rem);\n border-radius: var(--radius-sm, 0.125rem);\n}\n\n/* ─── Fix action button (ghost) ──────────────────────────────────────────── */\n\n.fixButton {\n flex: none;\n align-self: center;\n height: var(--spacing-8, 2rem);\n padding: 0 var(--spacing-3, 0.75rem);\n border-radius: var(--radius-md, 0.25rem);\n font-family: inherit;\n font-size: var(--font-size-xs, 0.75rem);\n font-weight: var(--font-weight-bold, 700);\n border: var(--stroke-width-thin, 1px) solid var(--border-default, #e5e7eb);\n background: transparent;\n color: var(--text-primary, #111827);\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 white-space: nowrap;\n}\n\n.fixButton:hover {\n background: var(--surface-muted, #f3f4f6);\n border-color: var(--border-strong, #d1d5db);\n}\n\n.fixButton:focus-visible {\n outline: var(--focus-ring-width, 2px) solid var(--border-focus, currentColor);\n outline-offset: var(--focus-ring-offset, 2px);\n}\n"
|
|
681
|
+
}
|
|
682
|
+
]
|
|
683
|
+
},
|
|
634
684
|
{
|
|
635
685
|
"name": "progress",
|
|
636
686
|
"type": "registry:ui",
|
|
@@ -1020,7 +1070,7 @@
|
|
|
1020
1070
|
{
|
|
1021
1071
|
"path": "components/ui/toast/toast.module.css",
|
|
1022
1072
|
"type": "registry:ui",
|
|
1023
|
-
"content": "/* Toaster container */\n.toaster {\n /* Container styling if needed */\n}\n\n/* Individual toast — :global() selectors nested under .toaster for CSS Modules purity.\n Sonner renders toasts as children of its container element. */\n.toaster :global([data-sonner-toast]) {\n font-family: var(--font-family-sans, system-ui, sans-serif);\n background-color: var(--surface-popover, var(--surface-card, #ffffff));\n color: var(--text-primary, #111827);\n border-radius: var(--radius-lg, 0.5rem);\n box-shadow: var(--shadow-lg);\n padding: var(--spacing-3, 0.75rem) var(--spacing-4, 1rem);\n gap: var(--spacing-2, 0.5rem);\n /* Override Sonner's default 400ms transitions with Visor motion tokens */\n transition:\n transform var(--motion-duration-normal, 200ms) var(--motion-easing-enter, ease-out),\n opacity var(--motion-duration-normal, 200ms) var(--motion-easing-enter, ease-out),\n height var(--motion-duration-normal, 200ms) !important;\n}\n\n/* Faster exit animation for dismissed toasts */\n.toaster :global([data-sonner-toast][data-removed=\"true\"]) {\n transition:\n transform var(--motion-duration-150, 150ms) var(--motion-easing-exit, ease-in),\n opacity var(--motion-duration-150, 150ms) var(--motion-easing-exit, ease-in) !important;\n}\n\n/* Toast title */\n.toaster :global([data-sonner-toast] [data-title]) {\n font-weight: var(--font-weight-semibold, 600);\n font-size: var(--font-size-sm, 0.875rem);\n color: var(--text-primary, #111827);\n}\n\n/* Toast description */\n.toaster :global([data-sonner-toast] [data-description]) {\n font-size: var(--font-size-sm, 0.875rem);\n color: var(--text-secondary, #6b7280);\n}\n\n/* Type-specific colors — subtle bg fill + left accent border */\n.toaster :global([data-sonner-toast][data-type=\"success\"]) {\n background-color: var(--surface-
|
|
1073
|
+
"content": "/* Toaster container */\n.toaster {\n /* Container styling if needed */\n}\n\n/* Individual toast — :global() selectors nested under .toaster for CSS Modules purity.\n Sonner renders toasts as children of its container element. */\n.toaster :global([data-sonner-toast]) {\n font-family: var(--font-family-sans, system-ui, sans-serif);\n background-color: var(--surface-popover, var(--surface-card, #ffffff));\n color: var(--text-primary, #111827);\n border-radius: var(--radius-lg, 0.5rem);\n box-shadow: var(--shadow-lg);\n padding: var(--spacing-3, 0.75rem) var(--spacing-4, 1rem);\n gap: var(--spacing-2, 0.5rem);\n /* Override Sonner's default 400ms transitions with Visor motion tokens */\n transition:\n transform var(--motion-duration-normal, 200ms) var(--motion-easing-enter, ease-out),\n opacity var(--motion-duration-normal, 200ms) var(--motion-easing-enter, ease-out),\n height var(--motion-duration-normal, 200ms) !important;\n}\n\n/* Faster exit animation for dismissed toasts */\n.toaster :global([data-sonner-toast][data-removed=\"true\"]) {\n transition:\n transform var(--motion-duration-150, 150ms) var(--motion-easing-exit, ease-in),\n opacity var(--motion-duration-150, 150ms) var(--motion-easing-exit, ease-in) !important;\n}\n\n/* Toast title */\n.toaster :global([data-sonner-toast] [data-title]) {\n font-weight: var(--font-weight-semibold, 600);\n font-size: var(--font-size-sm, 0.875rem);\n color: var(--text-primary, #111827);\n}\n\n/* Toast description */\n.toaster :global([data-sonner-toast] [data-description]) {\n font-size: var(--font-size-sm, 0.875rem);\n color: var(--text-secondary, #6b7280);\n}\n\n/* Type-specific colors — subtle bg fill + left accent border */\n\n/* Success: Borealis-spec dark inverse surface (VI-589).\n Dark bg + light text signals a completed, affirmative action.\n Falls back to a high-contrast dark surface on themes that lack\n the --surface-inverse token (for example, custom minimal themes). */\n.toaster :global([data-sonner-toast][data-type=\"success\"]) {\n background-color: var(--surface-inverse, var(--text-primary, #111827));\n color: var(--text-inverse, #f9fafb);\n border-left: none;\n}\n\n/* Success title — use inverse text so it reads on the dark bg */\n.toaster :global([data-sonner-toast][data-type=\"success\"] [data-title]) {\n color: var(--text-inverse, #f9fafb);\n font-weight: var(--font-weight-semibold, 700);\n}\n\n/* Success description — slightly muted inverse text */\n.toaster :global([data-sonner-toast][data-type=\"success\"] [data-description]) {\n color: var(--text-inverse-secondary, #d1d5db);\n}\n\n/* Success action button — accent colour reads on dark surface */\n.toaster :global([data-sonner-toast][data-type=\"success\"] [data-button]) {\n background-color: var(--primary, #111827);\n color: var(--primary-text, #ffffff);\n border: var(--stroke-width-thin, 1px) solid color-mix(in srgb, var(--primary, #111827) 60%, transparent);\n}\n\n/* Success close button — muted inverse */\n.toaster :global([data-sonner-toast][data-type=\"success\"] [data-close-button]) {\n background-color: transparent;\n border-color: color-mix(in srgb, var(--text-inverse, #f9fafb) 20%, transparent);\n color: var(--text-inverse-secondary, #d1d5db);\n}\n\n.toaster :global([data-sonner-toast][data-type=\"error\"]) {\n background-color: var(--surface-error-subtle, #fef2f2);\n border-left: 3px solid var(--border-error, #ef4444);\n}\n\n.toaster :global([data-sonner-toast][data-type=\"warning\"]) {\n background-color: var(--surface-warning-subtle, #fffbeb);\n border-left: 3px solid var(--border-warning, #f59e0b);\n}\n\n.toaster :global([data-sonner-toast][data-type=\"info\"]) {\n background-color: var(--surface-info-subtle, #f0f9ff);\n border-left: 3px solid var(--border-info, #0ea5e9);\n}\n\n/* Action button */\n.toaster :global([data-sonner-toast] [data-button]) {\n background-color: var(--interactive-primary-bg, var(--primary, #111827));\n color: var(--interactive-primary-text, #ffffff);\n border-radius: var(--radius-md, 0.375rem);\n font-size: var(--font-size-xs, 0.75rem);\n font-weight: var(--font-weight-medium, 500);\n padding: var(--spacing-1, 0.25rem) var(--spacing-2, 0.5rem);\n}\n\n/* Cancel button */\n.toaster :global([data-sonner-toast] [data-cancel]) {\n background-color: transparent;\n color: var(--text-secondary, #6b7280);\n font-size: var(--font-size-xs, 0.75rem);\n}\n\n/* Close button */\n.toaster :global([data-sonner-toast] [data-close-button]) {\n background-color: var(--surface-card, #ffffff);\n border: 1px solid var(--border-default, #e5e7eb);\n color: var(--text-secondary, #6b7280);\n}\n\n/* Dark mode support — :where() keeps specificity low so type-specific selectors above win */\n:global(:where(.dark)) .toaster :global([data-sonner-toast]),\n:global(:where([data-theme=\"dark\"])) .toaster :global([data-sonner-toast]) {\n background-color: var(--surface-card, #1f2937);\n color: var(--text-primary, #f9fafb);\n}\n\n/* Class-based styles applied via Sonner's classNames option */\n.toast {\n /* Specificity boost layer — matches :global styles above */\n}\n\n.title {\n font-weight: var(--font-weight-semibold, 600);\n}\n\n.description {\n color: var(--text-secondary, #6b7280);\n}\n\n.actionButton {\n background-color: var(--interactive-primary-bg, var(--primary, #111827));\n color: var(--interactive-primary-text, #ffffff);\n border-radius: var(--radius-md, 0.375rem);\n font-size: var(--font-size-xs, 0.75rem);\n font-weight: var(--font-weight-medium, 500);\n padding: var(--spacing-1, 0.25rem) var(--spacing-2, 0.5rem);\n}\n\n.cancelButton {\n background-color: transparent;\n color: var(--text-secondary, #6b7280);\n font-size: var(--font-size-xs, 0.75rem);\n}\n\n.closeButton {\n background-color: var(--surface-card, #ffffff);\n border: 1px solid var(--border-default, #e5e7eb);\n color: var(--text-secondary, #6b7280);\n}\n"
|
|
1024
1074
|
}
|
|
1025
1075
|
]
|
|
1026
1076
|
},
|
|
@@ -1517,6 +1567,32 @@
|
|
|
1517
1567
|
}
|
|
1518
1568
|
]
|
|
1519
1569
|
},
|
|
1570
|
+
{
|
|
1571
|
+
"name": "session-timeout",
|
|
1572
|
+
"type": "registry:ui",
|
|
1573
|
+
"description": "Non-dismissible full-screen overlay for auth session expiry. Renders at portal root, blurs app content, and presents a centered card with a Sign in CTA. ESC key and backdrop click are inert. Async onSignIn handler shows spinner in redirecting state.",
|
|
1574
|
+
"category": "overlay",
|
|
1575
|
+
"dependencies": [
|
|
1576
|
+
"@radix-ui/react-dialog",
|
|
1577
|
+
"@phosphor-icons/react",
|
|
1578
|
+
"@loworbitstudio/visor-core"
|
|
1579
|
+
],
|
|
1580
|
+
"registryDependencies": [
|
|
1581
|
+
"utils"
|
|
1582
|
+
],
|
|
1583
|
+
"files": [
|
|
1584
|
+
{
|
|
1585
|
+
"path": "components/ui/session-timeout/session-timeout.tsx",
|
|
1586
|
+
"type": "registry:ui",
|
|
1587
|
+
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport * as DialogPrimitive from \"@radix-ui/react-dialog\"\nimport { Lock } from \"@phosphor-icons/react\"\nimport { cn } from \"../../../lib/utils\"\nimport styles from \"./session-timeout.module.css\"\n\nexport interface SessionTimeoutProps {\n /**\n * Whether the session timeout overlay is visible.\n * Typically driven by an auth event listener (JWT expiry, 401, Supabase SIGNED_OUT).\n */\n open?: boolean\n /**\n * Called when the user clicks the primary \"Sign in\" button.\n * Returning a Promise puts the overlay into the `redirecting` state automatically.\n * @default undefined\n */\n onSignIn?: () => void | Promise<void>\n /**\n * Called when the user clicks the optional \"Return home\" ghost link.\n * If omitted, the link is not rendered.\n * @default undefined\n */\n onReturnHome?: () => void\n /**\n * Label for the primary CTA. Defaults to \"Sign in\".\n * @default \"Sign in\"\n */\n signInLabel?: React.ReactNode\n /**\n * Label for the optional ghost link. Defaults to \"Return home\".\n * @default \"Return home\"\n */\n returnHomeLabel?: React.ReactNode\n /**\n * Additional class name applied to the card surface.\n * @default undefined\n */\n className?: string\n}\n\n/**\n * SessionTimeout renders a non-dismissible full-screen overlay when an auth\n * session expires. The overlay covers the entire viewport via a React portal,\n * blurs the app content beneath, and presents a centered card with a primary\n * Sign in CTA and an optional Return home escape hatch.\n *\n * - ESC key and backdrop click are inert — the user must explicitly act.\n * - Clicking \"Sign in\" transitions to the `redirecting` state (spinner +\n * disabled CTA) while the async `onSignIn` handler runs.\n * - The current path should be stored in a `redirect` query param before\n * navigating to /login (handled by the `onSignIn` prop).\n */\nconst SessionTimeout = React.forwardRef<\n React.ComponentRef<typeof DialogPrimitive.Content>,\n SessionTimeoutProps\n>(\n (\n {\n open = false,\n onSignIn,\n onReturnHome,\n signInLabel = \"Sign in\",\n returnHomeLabel = \"Return home\",\n className,\n },\n ref\n ) => {\n const [isRedirecting, setIsRedirecting] = React.useState(false)\n\n // Reset redirecting state whenever the overlay closes (e.g. open flips to false)\n React.useEffect(() => {\n if (!open) {\n setIsRedirecting(false)\n }\n }, [open])\n\n const handleSignIn = React.useCallback(async () => {\n if (!onSignIn) return\n const result = onSignIn()\n if (result && typeof (result as Promise<void>).then === \"function\") {\n setIsRedirecting(true)\n try {\n await result\n } finally {\n setIsRedirecting(false)\n }\n }\n }, [onSignIn])\n\n return (\n <DialogPrimitive.Root open={open} data-slot=\"session-timeout-root\">\n <DialogPrimitive.Portal>\n <DialogPrimitive.Overlay\n data-slot=\"session-timeout-overlay\"\n className={styles.overlay}\n />\n <DialogPrimitive.Content\n ref={ref}\n data-slot=\"session-timeout\"\n data-state={isRedirecting ? \"redirecting\" : \"expired\"}\n className={styles.content}\n aria-labelledby=\"session-timeout-title\"\n aria-describedby=\"session-timeout-description\"\n /* Non-dismissible: prevent ESC and backdrop click from closing */\n onEscapeKeyDown={(e) => e.preventDefault()}\n onPointerDownOutside={(e) => e.preventDefault()}\n onInteractOutside={(e) => e.preventDefault()}\n >\n {/* Visually hidden title for screen readers */}\n <DialogPrimitive.Title className={styles.srOnly}>\n Session expired\n </DialogPrimitive.Title>\n <DialogPrimitive.Description className={styles.srOnly}>\n Your session has expired. Sign in again to continue.\n </DialogPrimitive.Description>\n\n <div\n role=\"alertdialog\"\n aria-modal=\"true\"\n aria-labelledby=\"session-timeout-title\"\n aria-describedby=\"session-timeout-description\"\n className={cn(styles.card, className)}\n >\n {/* Lock icon */}\n <div className={styles.iconWrap} aria-hidden=\"true\">\n <Lock\n size={28}\n weight=\"bold\"\n className={styles.icon}\n />\n </div>\n\n <h2\n id=\"session-timeout-title\"\n className={styles.headline}\n >\n Your session has expired\n </h2>\n\n <p\n id=\"session-timeout-description\"\n className={styles.copy}\n >\n Sign in again to continue where you left off.\n </p>\n\n <button\n type=\"button\"\n className={styles.signinBtn}\n onClick={handleSignIn}\n disabled={isRedirecting}\n aria-busy={isRedirecting || undefined}\n data-slot=\"session-timeout-signin\"\n >\n {isRedirecting && (\n <span\n className={styles.signinSpinner}\n aria-hidden=\"true\"\n />\n )}\n <span>\n {isRedirecting ? \"Signing in…\" : signInLabel}\n </span>\n </button>\n\n {onReturnHome && (\n <button\n type=\"button\"\n className={styles.returnHomeBtn}\n onClick={onReturnHome}\n disabled={isRedirecting}\n data-slot=\"session-timeout-return-home\"\n >\n {returnHomeLabel}\n </button>\n )}\n </div>\n </DialogPrimitive.Content>\n </DialogPrimitive.Portal>\n </DialogPrimitive.Root>\n )\n }\n)\nSessionTimeout.displayName = \"SessionTimeout\"\n\nexport { SessionTimeout }\n"
|
|
1588
|
+
},
|
|
1589
|
+
{
|
|
1590
|
+
"path": "components/ui/session-timeout/session-timeout.module.css",
|
|
1591
|
+
"type": "registry:ui",
|
|
1592
|
+
"content": "/* Session Timeout Overlay */\n\n/* Scrim — semi-transparent backdrop with blur, covers the full viewport */\n.overlay {\n position: fixed;\n inset: 0;\n z-index: 50;\n background-color: var(--overlay-bg, rgba(0, 0, 0, var(--opacity-80, 0.8)));\n backdrop-filter: blur(2px);\n -webkit-backdrop-filter: blur(2px);\n}\n\n.overlay[data-state=\"open\"] {\n animation: overlayIn var(--motion-duration-normal, 200ms) var(--motion-easing-enter, ease-out);\n}\n\n.overlay[data-state=\"closed\"] {\n animation: overlayFadeOut var(--motion-duration-150, 150ms) var(--motion-easing-exit, ease-in);\n}\n\n/* Content shell — centers the card in the viewport */\n.content {\n position: fixed;\n inset: 0;\n z-index: 51;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: var(--spacing-6, 1.5rem);\n outline: none;\n /* No background on content shell — scrim is on .overlay */\n}\n\n/* Card surface */\n.card {\n background-color: var(--surface-card, #ffffff);\n border-radius: var(--radius-xl, 1rem);\n padding: var(--spacing-10, 2.5rem) var(--spacing-9, 2.25rem);\n text-align: center;\n max-width: 340px;\n width: 100%;\n box-shadow: var(--shadow-lg);\n border: var(--stroke-width-thin, 1px) solid var(--border-default, #e5e7eb);\n}\n\n.card[data-state=\"open\"],\n.content[data-state=\"open\"] .card {\n animation: cardIn var(--motion-duration-normal, 200ms) var(--motion-easing-enter, ease-out);\n}\n\n/* Lock icon wrap — circular tinted plate */\n.iconWrap {\n width: 4rem;\n height: 4rem;\n border-radius: var(--radius-full, 9999px);\n background-color: color-mix(in srgb, var(--primary, #2563eb) var(--opacity-10, 10%), transparent);\n display: flex;\n align-items: center;\n justify-content: center;\n margin: 0 auto var(--spacing-6, 1.5rem);\n}\n\n/* Lock icon — uses primary color */\n.icon {\n color: var(--primary, #2563eb);\n}\n\n/* Headline */\n.headline {\n font-size: var(--font-size-xl, 1.25rem);\n font-weight: var(--font-weight-bold, 700);\n color: var(--text-primary, #111827);\n line-height: 1.25;\n margin-bottom: var(--spacing-2, 0.5rem);\n margin-top: 0;\n}\n\n/* Body copy */\n.copy {\n font-size: var(--font-size-sm, 0.875rem);\n color: var(--text-secondary, #4b5563);\n line-height: 1.6;\n margin-bottom: var(--spacing-7, 1.75rem);\n margin-top: 0;\n}\n\n/* Primary CTA — Sign in */\n.signinBtn {\n display: flex;\n align-items: center;\n justify-content: center;\n gap: var(--spacing-2, 0.5rem);\n width: 100%;\n padding: var(--spacing-3, 0.75rem) var(--spacing-7, 1.75rem);\n border-radius: var(--radius-md, 0.375rem);\n border: none;\n background-color: var(--interactive-primary-bg, var(--primary, #2563eb));\n color: var(--interactive-primary-text, #ffffff);\n font-size: var(--font-size-sm, 0.875rem);\n font-weight: var(--font-weight-semibold, 600);\n cursor: pointer;\n transition:\n background-color var(--motion-duration-fast, 100ms) var(--motion-easing-default, ease),\n opacity var(--motion-duration-fast, 100ms) var(--motion-easing-default, ease);\n}\n\n@media (hover: hover) {\n .signinBtn:hover:not(:disabled) {\n background-color: var(--interactive-primary-bg-hover, var(--primary, #1d4ed8));\n }\n}\n\n.signinBtn: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.signinBtn:disabled {\n opacity: var(--opacity-60, 0.6);\n cursor: not-allowed;\n}\n\n/* Spinner — inline ring spinner for the primary CTA's redirecting state */\n.signinSpinner {\n display: inline-block;\n flex-shrink: 0;\n width: 0.875rem;\n height: 0.875rem;\n border-radius: var(--radius-full, 9999px);\n border-style: solid;\n border-width: var(--stroke-width-regular, 1.5px);\n border-color: color-mix(in srgb, var(--text-inverse, #ffffff) var(--opacity-40, 40%), transparent);\n border-top-color: var(--text-inverse, #ffffff);\n animation: spin var(--motion-duration-1500, 1500ms) var(--motion-easing-linear, linear) infinite;\n}\n\n/* Return home ghost link */\n.returnHomeBtn {\n display: block;\n width: 100%;\n margin-top: var(--spacing-3, 0.75rem);\n padding: var(--spacing-1, 0.25rem);\n background: none;\n border: none;\n font-size: var(--font-size-sm, 0.875rem);\n font-weight: var(--font-weight-medium, 500);\n color: var(--text-tertiary, #9ca3af);\n text-align: center;\n cursor: pointer;\n transition: color var(--motion-duration-fast, 100ms) var(--motion-easing-default, ease);\n}\n\n@media (hover: hover) {\n .returnHomeBtn:hover:not(:disabled) {\n color: var(--text-secondary, #4b5563);\n }\n}\n\n.returnHomeBtn:focus-visible {\n outline: var(--focus-ring-width, 2px) solid var(--border-focus, #3b82f6);\n outline-offset: var(--focus-ring-offset, 2px);\n border-radius: var(--radius-sm, 0.125rem);\n}\n\n.returnHomeBtn:disabled {\n opacity: var(--opacity-40, 0.4);\n cursor: not-allowed;\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 */\n@keyframes overlayIn {\n from { opacity: 0; }\n to { opacity: 1; }\n}\n\n@keyframes overlayFadeOut {\n from { opacity: 1; }\n to { opacity: 0; }\n}\n\n@keyframes cardIn {\n from {\n opacity: 0;\n transform: translateY(var(--spacing-4, 1rem)) scale(0.97);\n }\n to {\n opacity: 1;\n transform: translateY(0) scale(1);\n }\n}\n\n@keyframes spin {\n to { transform: rotate(360deg); }\n}\n"
|
|
1593
|
+
}
|
|
1594
|
+
]
|
|
1595
|
+
},
|
|
1520
1596
|
{
|
|
1521
1597
|
"name": "file-upload",
|
|
1522
1598
|
"type": "registry:ui",
|
|
@@ -1678,6 +1754,30 @@
|
|
|
1678
1754
|
}
|
|
1679
1755
|
]
|
|
1680
1756
|
},
|
|
1757
|
+
{
|
|
1758
|
+
"name": "form-error",
|
|
1759
|
+
"type": "registry:ui",
|
|
1760
|
+
"description": "Form-level submission error banner — appears inside a form card when submission is blocked by field validation failures. Pairs with Field, FieldError, and Input[aria-invalid] to deliver the complete Borealis form validation pattern.",
|
|
1761
|
+
"category": "form",
|
|
1762
|
+
"dependencies": [
|
|
1763
|
+
"@loworbitstudio/visor-core"
|
|
1764
|
+
],
|
|
1765
|
+
"registryDependencies": [
|
|
1766
|
+
"utils"
|
|
1767
|
+
],
|
|
1768
|
+
"files": [
|
|
1769
|
+
{
|
|
1770
|
+
"path": "components/ui/form-error/form-error.tsx",
|
|
1771
|
+
"type": "registry:ui",
|
|
1772
|
+
"content": "import * as React from \"react\"\nimport { cn } from \"../../../lib/utils\"\nimport styles from \"./form-error.module.css\"\n\n/* ─── FormError ────────────────────────────────────────────────────────\n *\n * Submission-level error banner — appears inside a form card when the\n * server or client-side submit validation fails. Informs the user that\n * one or more fields need attention before the form can be submitted.\n *\n * Anatomy:\n * - Left-border accent (destructive) — the primary error signal\n * - Optional leading icon slot (aria-hidden, decorative)\n * - FormErrorTitle — direct statement of the failure\n * - FormErrorDescription — optional count or guidance copy\n *\n * Token references:\n * - Border: --border-error (2px left accent)\n * - Surface: color-mix(--destructive 8%, --surface-card)\n * - Icon color: --destructive\n * - Title color: --text-primary (stays readable — color + border signal error)\n * - Body color: --text-secondary\n */\nexport interface FormErrorProps extends React.HTMLAttributes<HTMLDivElement> {\n /**\n * Optional leading icon (e.g. a Phosphor `WarningCircle`). Rendered\n * at the destructive color — `aria-hidden`, so the `role=\"alert\"` on\n * the container carries the semantic weight.\n */\n icon?: React.ReactNode\n}\n\nconst FormError = React.forwardRef<HTMLDivElement, FormErrorProps>(\n ({ className, icon, children, ...props }, ref) => {\n return (\n <div\n ref={ref}\n role=\"alert\"\n data-slot=\"form-error\"\n className={cn(styles.base, className)}\n {...props}\n >\n {icon && (\n <span\n className={styles.icon}\n data-slot=\"form-error-icon\"\n aria-hidden=\"true\"\n >\n {icon}\n </span>\n )}\n <div className={styles.content} data-slot=\"form-error-content\">\n {children}\n </div>\n </div>\n )\n }\n)\nFormError.displayName = \"FormError\"\n\n/* ─── FormErrorTitle ─────────────────────────────────────────────────── */\n\nexport type FormErrorTitleProps = React.HTMLAttributes<HTMLParagraphElement>\n\nconst FormErrorTitle = React.forwardRef<HTMLParagraphElement, FormErrorTitleProps>(\n ({ className, ...props }, ref) => {\n return (\n <p\n ref={ref}\n data-slot=\"form-error-title\"\n className={cn(styles.title, className)}\n {...props}\n />\n )\n }\n)\nFormErrorTitle.displayName = \"FormErrorTitle\"\n\n/* ─── FormErrorDescription ───────────────────────────────────────────── */\n\nexport type FormErrorDescriptionProps = React.HTMLAttributes<HTMLParagraphElement>\n\nconst FormErrorDescription = React.forwardRef<\n HTMLParagraphElement,\n FormErrorDescriptionProps\n>(({ className, ...props }, ref) => {\n return (\n <p\n ref={ref}\n data-slot=\"form-error-description\"\n className={cn(styles.description, className)}\n {...props}\n />\n )\n})\nFormErrorDescription.displayName = \"FormErrorDescription\"\n\nexport { FormError, FormErrorTitle, FormErrorDescription }\n"
|
|
1773
|
+
},
|
|
1774
|
+
{
|
|
1775
|
+
"path": "components/ui/form-error/form-error.module.css",
|
|
1776
|
+
"type": "registry:ui",
|
|
1777
|
+
"content": "/* FormError — form-level submission error banner.\n *\n * Appears inside a form card when submission is blocked by one or more\n * validation errors. Left-border accent on a destructive-tinted surface\n * communicates severity without alarming red text everywhere.\n *\n * All values are design tokens — no hardcoded hex or magic numbers.\n */\n\n.base {\n display: flex;\n align-items: flex-start;\n gap: var(--spacing-3, 0.75rem);\n padding: var(--spacing-3, 0.75rem) var(--spacing-4, 1rem);\n border-radius: 0 var(--radius-md, 0.375rem) var(--radius-md, 0.375rem) 0;\n /* Destructive-tinted surface — softened so it doesn't overwhelm */\n background: color-mix(in srgb, var(--destructive, #ef4444) 8%, var(--surface-card, #ffffff));\n /* Left accent border — 3px solid, the primary error signal */\n border-left: 3px solid var(--border-error, var(--destructive, #ef4444));\n}\n\n/* Leading icon slot */\n.icon {\n flex-shrink: 0;\n /* 1px optical nudge to vertically align icon with the title cap-height */\n margin-top: calc(var(--spacing-1, 0.25rem) / 4);\n display: inline-flex;\n align-items: center;\n justify-content: center;\n color: var(--destructive, #ef4444);\n font-size: 1.125rem;\n line-height: 1;\n}\n\n/* Content column — title + optional description */\n.content {\n min-width: 0;\n display: flex;\n flex-direction: column;\n gap: var(--spacing-1, 0.25rem);\n}\n\n/* Title: --text-primary (stays readable — color + border carry the error signal) */\n.title {\n margin: 0;\n font-size: var(--font-size-sm, 0.875rem);\n font-weight: var(--font-weight-semibold, 600);\n color: var(--text-primary, #111827);\n line-height: 1.4;\n}\n\n/* Description: secondary, lighter */\n.description {\n margin: 0;\n font-size: var(--font-size-sm, 0.875rem);\n font-weight: var(--font-weight-normal, 400);\n color: var(--text-secondary, #6b7280);\n line-height: 1.5;\n}\n"
|
|
1778
|
+
}
|
|
1779
|
+
]
|
|
1780
|
+
},
|
|
1681
1781
|
{
|
|
1682
1782
|
"name": "search-input",
|
|
1683
1783
|
"type": "registry:ui",
|
|
@@ -1779,6 +1879,29 @@
|
|
|
1779
1879
|
}
|
|
1780
1880
|
]
|
|
1781
1881
|
},
|
|
1882
|
+
{
|
|
1883
|
+
"name": "offline-banner",
|
|
1884
|
+
"type": "registry:ui",
|
|
1885
|
+
"description": "App-shell banner for network connectivity loss. Sticky below the nav bar with offline, reconnecting, and restored states. Includes a useNetworkStatus hook that listens to browser online/offline events.",
|
|
1886
|
+
"category": "feedback",
|
|
1887
|
+
"dependencies": [
|
|
1888
|
+
"@phosphor-icons/react",
|
|
1889
|
+
"@loworbitstudio/visor-core"
|
|
1890
|
+
],
|
|
1891
|
+
"registryDependencies": [],
|
|
1892
|
+
"files": [
|
|
1893
|
+
{
|
|
1894
|
+
"path": "components/ui/offline-banner/offline-banner.tsx",
|
|
1895
|
+
"type": "registry:ui",
|
|
1896
|
+
"content": "'use client';\n\nimport * as React from \"react\"\nimport { WifiSlashIcon, ArrowsClockwiseIcon, CheckCircleIcon } from \"@phosphor-icons/react\"\nimport styles from \"./offline-banner.module.css\"\n\n// ─── Network status types ────────────────────────────────────────────────────\n\nexport type NetworkState = \"online\" | \"offline\" | \"reconnecting\" | \"restored\"\n\n// ─── useNetworkStatus hook ───────────────────────────────────────────────────\n\nexport interface UseNetworkStatusOptions {\n /** Called when manual retry check completes (resolve to report online/offline result). */\n onRetry?: () => Promise<boolean>\n /** Duration (ms) to show the \"restored\" state before auto-dismissing. Default: 1500. */\n restoredDisplayDuration?: number\n}\n\nexport interface UseNetworkStatusReturn {\n networkState: NetworkState\n retry: () => void\n}\n\n/**\n * Hook that tracks navigator.onLine and drives the offline banner state machine.\n *\n * States:\n * online → No banner shown (initial state when online)\n * offline → Network lost; banner visible with \"You're offline\" + Retry button\n * reconnecting → Retry pressed; spinner shown, Retry hidden\n * restored → Back online; brief \"Back online\" confirmation, auto-dismisses after 1.5s\n */\nexport function useNetworkStatus(options: UseNetworkStatusOptions = {}): UseNetworkStatusReturn {\n const { onRetry, restoredDisplayDuration = 1500 } = options\n\n const [networkState, setNetworkState] = React.useState<NetworkState>(\n // SSR-safe: default to \"online\" on server; browser will correct on mount\n typeof navigator !== \"undefined\" && !navigator.onLine ? \"offline\" : \"online\"\n )\n\n // Listen to browser online/offline events\n React.useEffect(() => {\n function handleOnline() {\n setNetworkState(\"restored\")\n }\n\n function handleOffline() {\n setNetworkState(\"offline\")\n }\n\n window.addEventListener(\"online\", handleOnline)\n window.addEventListener(\"offline\", handleOffline)\n\n // Sync initial state on mount (in case it changed between server render and mount)\n if (!navigator.onLine) {\n setNetworkState(\"offline\")\n }\n\n return () => {\n window.removeEventListener(\"online\", handleOnline)\n window.removeEventListener(\"offline\", handleOffline)\n }\n }, [])\n\n // Auto-dismiss \"restored\" state after display duration\n React.useEffect(() => {\n if (networkState !== \"restored\") return\n\n const timer = setTimeout(() => {\n setNetworkState(\"online\")\n }, restoredDisplayDuration)\n\n return () => clearTimeout(timer)\n }, [networkState, restoredDisplayDuration])\n\n const retry = React.useCallback(async () => {\n setNetworkState(\"reconnecting\")\n\n if (onRetry) {\n try {\n const isOnline = await onRetry()\n setNetworkState(isOnline ? \"restored\" : \"offline\")\n } catch {\n setNetworkState(\"offline\")\n }\n } else {\n // Default: check navigator.onLine\n if (navigator.onLine) {\n setNetworkState(\"restored\")\n } else {\n // Brief pause to show reconnecting state, then revert to offline\n await new Promise<void>((resolve) => setTimeout(resolve, 800))\n setNetworkState(\"offline\")\n }\n }\n }, [onRetry])\n\n return { networkState, retry }\n}\n\n// ─── OfflineBanner component ─────────────────────────────────────────────────\n\nexport interface OfflineBannerProps {\n /** Current network state. Use with `useNetworkStatus()` or control manually. */\n networkState: NetworkState\n /** Called when the user presses \"Retry\". */\n onRetry?: () => void\n /** Additional className for the banner root. */\n className?: string\n /** Custom label for the offline state. Default: \"You're offline\". */\n offlineLabel?: string\n /** Custom label for the reconnecting state. Default: \"Reconnecting…\". */\n reconnectingLabel?: string\n /** Custom label for the restored state. Default: \"Back online\". */\n restoredLabel?: string\n /** Custom label for the retry button. Default: \"Retry\". */\n retryLabel?: string\n}\n\n/**\n * OfflineBanner — full-width sticky banner for network connectivity loss.\n *\n * Composes the existing Banner pattern: dark surface, inverse text, accent icon.\n * Mounts below the navigation bar using sticky positioning — does not overlay content.\n *\n * Usage:\n * const { networkState, retry } = useNetworkStatus();\n * <OfflineBanner networkState={networkState} onRetry={retry} />\n */\nexport const OfflineBanner = React.forwardRef<HTMLDivElement, OfflineBannerProps>(\n function OfflineBanner(\n {\n networkState,\n onRetry,\n className,\n offlineLabel = \"You're offline\",\n reconnectingLabel = \"Reconnecting…\",\n restoredLabel = \"Back online\",\n retryLabel = \"Retry\",\n },\n ref\n ) {\n // Hidden when online\n if (networkState === \"online\") return null\n\n const isOffline = networkState === \"offline\"\n const isReconnecting = networkState === \"reconnecting\"\n const isRestored = networkState === \"restored\"\n\n const bannerClass = [\n styles.banner,\n isRestored ? styles.bannerRestored : styles.bannerOffline,\n className,\n ]\n .filter(Boolean)\n .join(\" \")\n\n return (\n <div\n ref={ref}\n data-slot=\"offline-banner\"\n data-state={networkState}\n role=\"status\"\n aria-live=\"polite\"\n aria-label={\n isRestored\n ? restoredLabel\n : isReconnecting\n ? reconnectingLabel\n : offlineLabel\n }\n className={bannerClass}\n >\n {/* Left: icon or spinner */}\n <span className={styles.iconSlot} aria-hidden=\"true\">\n {isRestored ? (\n <CheckCircleIcon weight=\"fill\" className={styles.iconRestored} />\n ) : isReconnecting ? (\n <span className={styles.spinner} />\n ) : (\n <WifiSlashIcon weight=\"bold\" className={styles.iconOffline} />\n )}\n </span>\n\n {/* Center: status label */}\n <span className={styles.label}>\n {isRestored\n ? restoredLabel\n : isReconnecting\n ? reconnectingLabel\n : offlineLabel}\n </span>\n\n {/* Right: retry button (offline state only) */}\n {isOffline && onRetry && (\n <button\n type=\"button\"\n className={styles.retryBtn}\n onClick={onRetry}\n aria-label={`${retryLabel} — check network connection`}\n >\n <ArrowsClockwiseIcon weight=\"bold\" className={styles.retryIcon} aria-hidden=\"true\" />\n {retryLabel}\n </button>\n )}\n </div>\n )\n }\n)\nOfflineBanner.displayName = \"OfflineBanner\"\n"
|
|
1897
|
+
},
|
|
1898
|
+
{
|
|
1899
|
+
"path": "components/ui/offline-banner/offline-banner.module.css",
|
|
1900
|
+
"type": "registry:ui",
|
|
1901
|
+
"content": "/* OfflineBanner\n * Full-width sticky banner pinned below the navigation bar.\n * Dark surface (--surface-overlay) with inverse text — authoritative, not alarming.\n * Accent icon (--accent) reads against the dark background.\n *\n * Three rendered states share the same base layout:\n * offline → dark bg, wifi-off icon, \"You're offline\" + Retry button\n * reconnecting → dark bg, spinner replacing icon, \"Reconnecting…\", no Retry\n * restored → success-tinted bg, check icon, \"Back online\" (auto-dismisses)\n */\n\n/* ── Slide-in entrance animation ─────────────────────────────────────────── */\n/* Resting state is opacity:1 (SSR-safe — never gated on JS mount) */\n@keyframes offlineBannerEnter {\n from {\n transform: translateY(-100%);\n opacity: 0;\n }\n to {\n transform: translateY(0);\n opacity: 1;\n }\n}\n\n/* ── Shared banner base ──────────────────────────────────────────────────── */\n.banner {\n width: 100%;\n position: sticky;\n top: 0;\n z-index: 40;\n display: flex;\n align-items: center;\n gap: var(--spacing-2, 0.5rem);\n padding: 0 var(--spacing-4, 1rem);\n height: 44px;\n animation: offlineBannerEnter var(--motion-duration-normal, 200ms) var(--motion-easing-enter, ease-out);\n}\n\n/* ── Offline / reconnecting: dark inverse surface ────────────────────────── */\n.bannerOffline {\n background-color: var(--surface-overlay, #111827);\n border-bottom: var(--stroke-width-thin, 1px) solid\n color-mix(in srgb, var(--accent, #0ea5e9) 20%, transparent);\n color: var(--text-inverse, #ffffff);\n}\n\n/* ── Restored: success-tinted surface ───────────────────────────────────── */\n.bannerRestored {\n background-color: var(--surface-success-subtle, #f0fdf4);\n border-bottom: var(--stroke-width-thin, 1px) solid var(--border-success, #22c55e);\n color: var(--text-success, #16a34a);\n}\n\n/* ── Icon slot ───────────────────────────────────────────────────────────── */\n.iconSlot {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n width: 1.125rem;\n height: 1.125rem;\n}\n\n.iconOffline {\n width: 1.125rem;\n height: 1.125rem;\n color: var(--accent, #0ea5e9);\n}\n\n.iconRestored {\n width: 1.125rem;\n height: 1.125rem;\n color: var(--text-success, #16a34a);\n}\n\n/* ── Spinner (reconnecting state) ────────────────────────────────────────── */\n@keyframes offlineBannerSpin {\n to {\n transform: rotate(360deg);\n }\n}\n\n.spinner {\n display: block;\n width: 1rem;\n height: 1rem;\n border: var(--stroke-width-regular, 2px) solid\n color-mix(in srgb, var(--accent, #0ea5e9) 30%, transparent);\n border-top-color: var(--accent, #0ea5e9);\n border-radius: var(--radius-full, 9999px);\n animation: offlineBannerSpin var(--motion-duration-800, 800ms) var(--motion-easing-linear, linear) infinite;\n flex-shrink: 0;\n}\n\n/* ── Status label ────────────────────────────────────────────────────────── */\n.label {\n flex: 1;\n font-size: var(--font-size-sm, 0.875rem);\n font-weight: var(--font-weight-medium, 500);\n line-height: 1;\n /* Inherits color from .bannerOffline or .bannerRestored */\n}\n\n/* ── Retry button ────────────────────────────────────────────────────────── */\n.retryBtn {\n display: inline-flex;\n align-items: center;\n gap: var(--spacing-1, 0.25rem);\n margin-left: auto;\n padding: var(--spacing-1, 0.25rem) var(--spacing-2, 0.5rem);\n border: none;\n border-radius: var(--radius-sm, 0.25rem);\n background: transparent;\n font-size: var(--font-size-sm, 0.875rem);\n font-weight: var(--font-weight-semibold, 600);\n color: var(--accent, #0ea5e9);\n cursor: pointer;\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}\n\n.retryBtn:hover {\n background-color: color-mix(in srgb, var(--accent, #0ea5e9) 10%, transparent);\n}\n\n.retryBtn: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.retryIcon {\n width: 0.875rem;\n height: 0.875rem;\n}\n"
|
|
1902
|
+
}
|
|
1903
|
+
]
|
|
1904
|
+
},
|
|
1782
1905
|
{
|
|
1783
1906
|
"name": "number-input",
|
|
1784
1907
|
"type": "registry:ui",
|
|
@@ -1958,6 +2081,32 @@
|
|
|
1958
2081
|
}
|
|
1959
2082
|
]
|
|
1960
2083
|
},
|
|
2084
|
+
{
|
|
2085
|
+
"name": "editable-block",
|
|
2086
|
+
"type": "registry:ui",
|
|
2087
|
+
"description": "Canvas brand block — a card tile with an uppercase label header, value body, hover-revealed edit icon, and editing state with inline input, save button, and AI action slot. Used for free-edit canvas boards where each attribute is independently editable.",
|
|
2088
|
+
"category": "general",
|
|
2089
|
+
"dependencies": [
|
|
2090
|
+
"@phosphor-icons/react",
|
|
2091
|
+
"class-variance-authority",
|
|
2092
|
+
"@loworbitstudio/visor-core"
|
|
2093
|
+
],
|
|
2094
|
+
"registryDependencies": [
|
|
2095
|
+
"utils"
|
|
2096
|
+
],
|
|
2097
|
+
"files": [
|
|
2098
|
+
{
|
|
2099
|
+
"path": "components/ui/editable-block/editable-block.tsx",
|
|
2100
|
+
"type": "registry:ui",
|
|
2101
|
+
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport { PencilSimple, Check, Sparkle } from \"@phosphor-icons/react\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\nimport { cn } from \"../../../lib/utils\"\nimport styles from \"./editable-block.module.css\"\n\n/* ─── CVA ────────────────────────────────────────────────────────────────── */\n\nconst editableBlockVariants = cva(styles.root, {\n variants: {\n state: {\n default: styles.stateDefault,\n editing: styles.stateEditing,\n done: styles.stateDone,\n },\n },\n defaultVariants: {\n state: \"default\",\n },\n})\n\n/* ─── Props ───────────────────────────────────────────────────────────────── */\n\nexport interface EditableBlockProps\n extends Omit<React.HTMLAttributes<HTMLDivElement>, \"onSave\"> {\n /** Block label — rendered uppercase above the value. */\n label: string\n /** The current value to display. */\n value: string\n /** Whether the block is in a \"done\" / confirmed state (shows a check in the header). */\n done?: boolean\n /**\n * Called when the user saves an edited value.\n * Receives the new string. Parent is responsible for updating `value`.\n */\n onSave?: (value: string) => void\n /**\n * Label for the AI action button shown in editing state.\n * Pass `null` or omit to suppress the AI action.\n * @default \"Ask AI to pressure-test\"\n */\n aiActionLabel?: string | null\n /** Called when the AI action button is clicked. */\n onAiAction?: () => void\n /**\n * Initial edit mode. Useful for controlled callers that open the block pre-edited.\n * @default false\n */\n defaultEditing?: boolean\n}\n\n/* ─── Component ───────────────────────────────────────────────────────────── */\n\nconst EditableBlock = React.forwardRef<HTMLDivElement, EditableBlockProps>(\n (\n {\n className,\n label,\n value,\n done = false,\n onSave,\n aiActionLabel = \"Ask AI to pressure-test\",\n onAiAction,\n defaultEditing = false,\n ...props\n },\n ref\n ) => {\n const [editing, setEditing] = React.useState(defaultEditing)\n const [draft, setDraft] = React.useState(value)\n const inputRef = React.useRef<HTMLInputElement>(null)\n\n // Sync draft when value prop changes externally (e.g. after save)\n React.useEffect(() => {\n if (!editing) setDraft(value)\n }, [value, editing])\n\n // Focus input on edit open\n React.useEffect(() => {\n if (editing) {\n inputRef.current?.focus()\n inputRef.current?.select()\n }\n }, [editing])\n\n const handleEditClick = (e: React.MouseEvent) => {\n e.stopPropagation()\n setDraft(value)\n setEditing(true)\n }\n\n const handleSave = () => {\n setEditing(false)\n onSave?.(draft.trim() || value)\n }\n\n const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {\n if (e.key === \"Enter\") {\n e.preventDefault()\n handleSave()\n } else if (e.key === \"Escape\") {\n e.preventDefault()\n setEditing(false)\n setDraft(value)\n }\n }\n\n const currentState = editing ? \"editing\" : done ? \"done\" : \"default\"\n\n return (\n <div\n ref={ref}\n data-slot=\"editable-block\"\n data-state={currentState}\n className={cn(editableBlockVariants({ state: currentState }), className)}\n onClick={!editing ? handleEditClick : undefined}\n {...props}\n >\n {/* Header row: label + done check */}\n <div className={styles.header}>\n <span className={styles.label}>{label}</span>\n {done && !editing && (\n <span className={styles.doneCheck} aria-label=\"Done\">\n <Check weight=\"bold\" />\n </span>\n )}\n </div>\n\n {/* Value body */}\n {!editing && (\n <div className={styles.value}>{value}</div>\n )}\n\n {/* Hover-revealed edit icon (view state only) */}\n {!editing && (\n <button\n type=\"button\"\n className={styles.editIcon}\n aria-label={`Edit ${label}`}\n tabIndex={0}\n onClick={handleEditClick}\n >\n <PencilSimple />\n </button>\n )}\n\n {/* Editing state: inline input + save button */}\n {editing && (\n <>\n <div className={styles.editRow} data-slot=\"editable-block-edit-row\">\n <input\n ref={inputRef}\n type=\"text\"\n className={styles.editInput}\n value={draft}\n onChange={(e) => setDraft(e.target.value)}\n onKeyDown={handleKeyDown}\n aria-label={`Edit ${label}`}\n />\n <button\n type=\"button\"\n className={styles.saveButton}\n aria-label=\"Save\"\n onClick={handleSave}\n >\n <Check weight=\"bold\" />\n </button>\n </div>\n\n {/* AI action slot */}\n {aiActionLabel != null && (\n <button\n type=\"button\"\n className={styles.aiAction}\n data-slot=\"editable-block-ai-action\"\n onClick={onAiAction}\n >\n <Sparkle />\n {aiActionLabel}\n </button>\n )}\n </>\n )}\n </div>\n )\n }\n)\nEditableBlock.displayName = \"EditableBlock\"\n\n/* ─── Exports ─────────────────────────────────────────────────────────────── */\n\nexport { EditableBlock, editableBlockVariants }\nexport type { VariantProps as EditableBlockVariantProps }\n"
|
|
2102
|
+
},
|
|
2103
|
+
{
|
|
2104
|
+
"path": "components/ui/editable-block/editable-block.module.css",
|
|
2105
|
+
"type": "registry:ui",
|
|
2106
|
+
"content": "/* EditableBlock\n *\n * Canvas brand block — a card tile with an uppercase label header (+ done\n * check), a value body, a hover-revealed edit icon, and an editing state\n * (focus ring, inline input + save button, AI action slot).\n *\n * Visual source: docs/design/brand-workbench/journey (HTML) L274-285\n * (prototype selectors: block, bk, bv, ed, editrow, aibtn)\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 tile ─────────────────────────────────────────────────────────── */\n\n.root {\n background: var(--surface-card, #ffffff);\n border: var(--stroke-width-thin, 1px) solid var(--hairline, rgba(0,0,0,0.06));\n border-radius: var(--radius-lg, 0.75rem);\n padding: var(--spacing-3_5, 0.875rem); /* prototype: 14px */\n box-shadow: var(--shadow-sm, 0 1px 2px 0 rgb(0 0 0 / 0.05));\n min-height: 6rem; /* prototype: 96px */\n position: relative;\n cursor: pointer;\n display: flex;\n flex-direction: column;\n transition:\n border-color 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/* ─── State: default (hover) ────────────────────────────────────────────── */\n\n.stateDefault:hover {\n border-color: var(--border-default, #e5e7eb);\n}\n\n/* Reveal edit icon on hover — use CSS opacity so resting state is still\n in the DOM (avoids SSR/hydration race on mount). */\n.stateDefault:hover .editIcon {\n opacity: var(--opacity-80, 0.8);\n}\n\n/* ─── State: done ───────────────────────────────────────────────────────── */\n\n.stateDone:hover {\n border-color: var(--border-default, #e5e7eb);\n}\n\n.stateDone:hover .editIcon {\n opacity: var(--opacity-80, 0.8);\n}\n\n/* ─── State: editing ────────────────────────────────────────────────────── */\n\n.stateEditing {\n /* Focus-ring shadow — exception per token-rules Rule 2 (focus ring variant) */\n border-color: var(--primary, #111827);\n box-shadow: 0 0 0 var(--spacing-1, 0.25rem) var(--interactive-primary-soft, color-mix(in srgb, var(--interactive-primary-bg, #111827) 12%, transparent));\n cursor: default;\n}\n\n.stateEditing .label {\n color: var(--primary, #111827);\n}\n\n/* ─── Header row ────────────────────────────────────────────────────────── */\n\n.header {\n display: flex;\n align-items: center;\n gap: var(--spacing-1_5, 0.375rem); /* prototype: 7px ≈ 1.5 × 4px */\n margin-bottom: var(--spacing-2, 0.5rem); /* prototype: 8px */\n}\n\n/* ─── Label ─────────────────────────────────────────────────────────────── */\n\n.label {\n font-size: var(--font-size-2xs, 0.65625rem); /* prototype: 10.5px */\n font-weight: 800;\n letter-spacing: 0.07em;\n text-transform: uppercase;\n color: var(--text-tertiary, #6b7280);\n transition: color var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out);\n}\n\n/* ─── Done check icon ───────────────────────────────────────────────────── */\n\n.doneCheck {\n display: inline-flex;\n align-items: center;\n color: var(--success, #22c55e);\n}\n\n.doneCheck > svg {\n width: var(--spacing-3, 0.75rem); /* prototype: 12px */\n height: var(--spacing-3, 0.75rem);\n stroke-width: 3;\n}\n\n/* ─── Value body ────────────────────────────────────────────────────────── */\n\n.value {\n font-size: var(--font-size-sm, 0.84375rem); /* prototype: 13.5px ≈ sm */\n color: var(--text-primary, #111827);\n line-height: var(--line-height-snug, 1.45);\n flex: 1;\n}\n\n/* ─── Hover-revealed edit icon ──────────────────────────────────────────── */\n\n.editIcon {\n position: absolute;\n top: var(--spacing-2_5, 0.6875rem); /* prototype: 11px */\n right: var(--spacing-2_5, 0.6875rem);\n display: inline-flex;\n align-items: center;\n justify-content: center;\n color: var(--text-tertiary, #6b7280);\n /* Resting state opacity=0 via CSS (visible on hover via .stateDefault:hover rule).\n Must remain in DOM at rest for accessibility & SSR hydration safety. */\n opacity: 0;\n background: none;\n border: none;\n padding: 0;\n cursor: pointer;\n transition: opacity var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out);\n}\n\n.editIcon:focus-visible {\n opacity: var(--opacity-80, 0.8);\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.editIcon > svg {\n width: var(--spacing-3_5, 0.875rem); /* prototype: 14px */\n height: var(--spacing-3_5, 0.875rem);\n}\n\n/* ─── Edit row (input + save button) ────────────────────────────────────── */\n\n.editRow {\n display: flex;\n gap: var(--spacing-1_5, 0.375rem); /* prototype: 7px */\n margin-top: var(--spacing-2_5, 0.625rem); /* prototype: 10px */\n}\n\n/* Inline edit input */\n.editInput {\n flex: 1;\n height: var(--spacing-8, 2rem); /* prototype: 32px */\n border-radius: var(--radius-md, 0.5rem);\n background: var(--surface-subtle, #f9fafb);\n border: var(--stroke-width-thin, 1px) solid\n color-mix(in srgb, var(--primary, #111827) 30%, transparent);\n font-family: inherit;\n font-size: var(--font-size-sm, 0.84375rem); /* prototype: 13px */\n font-weight: 600;\n color: var(--text-primary, #111827);\n padding: 0 var(--spacing-2_5, 0.625rem); /* prototype: 0 10px */\n outline: none;\n}\n\n.editInput:focus-visible {\n outline: var(--focus-ring-width, 2px) solid var(--primary, #111827);\n outline-offset: var(--focus-ring-offset, 2px);\n}\n\n/* Save button (checkmark) */\n.saveButton {\n width: var(--spacing-8, 2rem); /* prototype: 32px */\n height: var(--spacing-8, 2rem);\n border-radius: var(--radius-md, 0.5rem);\n background: var(--primary, #111827);\n color: var(--primary-text, #ffffff);\n display: grid;\n place-items: center;\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}\n\n.saveButton:hover {\n background: color-mix(in srgb, var(--primary, #111827) 88%, white);\n}\n\n.saveButton:focus-visible {\n outline: var(--focus-ring-width, 2px) solid var(--primary, #111827);\n outline-offset: var(--focus-ring-offset, 2px);\n}\n\n.saveButton > svg {\n width: var(--spacing-3_5, 0.9375rem); /* prototype: 15px */\n height: var(--spacing-3_5, 0.9375rem);\n stroke-width: 2.4;\n}\n\n/* ─── AI action button ───────────────────────────────────────────────────── */\n\n.aiAction {\n display: inline-flex;\n align-items: center;\n gap: var(--spacing-1_5, 0.375rem); /* prototype: 6px */\n margin-top: var(--spacing-2, 0.5rem); /* prototype: 9px ≈ spacing-2 */\n font-family: inherit;\n font-size: var(--font-size-xs, 0.71875rem); /* prototype: 11.5px */\n font-weight: 700;\n color: var(--primary, #111827);\n background: none;\n border: none;\n cursor: pointer;\n padding: 0;\n transition: opacity var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out);\n}\n\n.aiAction:hover {\n opacity: var(--opacity-80, 0.8);\n}\n\n.aiAction: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.aiAction > svg {\n width: var(--spacing-3, 0.8125rem); /* prototype: 13px */\n height: var(--spacing-3, 0.8125rem);\n}\n\n/* ─── Reduced motion ─────────────────────────────────────────────────────── */\n\n@media (prefers-reduced-motion: reduce) {\n .root,\n .label,\n .editIcon,\n .saveButton,\n .aiAction {\n transition: none;\n }\n}\n"
|
|
2107
|
+
}
|
|
2108
|
+
]
|
|
2109
|
+
},
|
|
1961
2110
|
{
|
|
1962
2111
|
"name": "elevation-card",
|
|
1963
2112
|
"type": "registry:ui",
|
|
@@ -2410,12 +2559,12 @@
|
|
|
2410
2559
|
{
|
|
2411
2560
|
"path": "components/ui/empty-state/empty-state.tsx",
|
|
2412
2561
|
"type": "registry:ui",
|
|
2413
|
-
"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
|
|
2562
|
+
"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 * Semantic intent — clarifies the cause of the empty state and styles\n * the icon chip accordingly (Borealis spec §5).\n * - `first-use` — no items yet; accent-tinted chip; encourages a creation CTA.\n * - `zero-results` — filter or search returned nothing; accent-tinted chip;\n * encourages a clear-filter action.\n * - `no-access` — permission or feature gate; neutral chip with lock icon.\n */\n intent: {\n \"first-use\": styles.intentFirstUse,\n \"zero-results\": styles.intentZeroResults,\n \"no-access\": styles.intentNoAccess,\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 /**\n * When true (or when `intent` is set), wraps the icon in a circular chip\n * whose color is controlled by the `intent` variant. Ignored if no `icon` is\n * provided.\n */\n iconWrap?: boolean\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 intent,\n icon,\n iconWrap,\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 // Auto-activate icon wrap when intent is set, unless explicitly disabled\n const shouldWrapIcon = icon != null && (iconWrap ?? intent != null)\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 {...(intent ? { \"data-intent\": intent } : {})}\n className={cn(emptyStateVariants({ size, tone, variant, intent }), className)}\n {...props}\n >\n {icon ? (\n shouldWrapIcon ? (\n <div\n data-slot=\"empty-state-icon\"\n className={styles.iconWrap}\n aria-hidden=\"true\"\n >\n {icon}\n </div>\n ) : (\n <div\n data-slot=\"empty-state-icon\"\n className={styles.icon}\n aria-hidden=\"true\"\n >\n {icon}\n </div>\n )\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"
|
|
2414
2563
|
},
|
|
2415
2564
|
{
|
|
2416
2565
|
"path": "components/ui/empty-state/empty-state.module.css",
|
|
2417
2566
|
"type": "registry:ui",
|
|
2418
|
-
"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"
|
|
2567
|
+
"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/* Icon wrap — circular chip behind the icon; activated by intent variants */\n.iconWrap {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n border-radius: var(--radius-full, 9999px);\n width: 72px;\n height: 72px;\n margin-bottom: var(--spacing-3, 0.75rem);\n background-color: var(--surface-subtle, #f5f5f6);\n color: var(--text-tertiary, #6b7280);\n}\n\n.iconWrap svg {\n width: 32px;\n height: 32px;\n}\n\n/* Intent: first-use — accent-tinted chip, primary action CTA encouraged */\n.intentFirstUse .iconWrap {\n background-color: var(--surface-accent-subtle, #eff6ff);\n color: var(--surface-accent-default, #3b82f6);\n}\n\n/* Intent: zero-results — accent-tinted chip, clear-filter secondary action */\n.intentZeroResults .iconWrap {\n background-color: var(--surface-accent-subtle, #eff6ff);\n color: var(--surface-accent-default, #3b82f6);\n}\n\n/* Intent: no-access — neutral chip, lock icon */\n.intentNoAccess .iconWrap {\n background-color: var(--surface-subtle, #f5f5f6);\n color: var(--text-secondary, #6b7280);\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"
|
|
2419
2568
|
}
|
|
2420
2569
|
]
|
|
2421
2570
|
},
|
|
@@ -2443,6 +2592,32 @@
|
|
|
2443
2592
|
}
|
|
2444
2593
|
]
|
|
2445
2594
|
},
|
|
2595
|
+
{
|
|
2596
|
+
"name": "success-feedback",
|
|
2597
|
+
"type": "registry:ui",
|
|
2598
|
+
"description": "App-wide success/transition feedback pattern. useSuccessToast() fires an auto-dismissing Sonner toast with Borealis-spec defaults (4s, optional undo action). SuccessLiveRegion provides an explicit aria-live polite node for screen readers.",
|
|
2599
|
+
"category": "feedback",
|
|
2600
|
+
"dependencies": [
|
|
2601
|
+
"sonner",
|
|
2602
|
+
"@loworbitstudio/visor-core"
|
|
2603
|
+
],
|
|
2604
|
+
"registryDependencies": [
|
|
2605
|
+
"utils",
|
|
2606
|
+
"toast"
|
|
2607
|
+
],
|
|
2608
|
+
"files": [
|
|
2609
|
+
{
|
|
2610
|
+
"path": "components/ui/success-feedback/success-feedback.tsx",
|
|
2611
|
+
"type": "registry:ui",
|
|
2612
|
+
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport { toast } from \"../toast/toast\"\nimport styles from \"./success-feedback.module.css\"\n\n/* ──────────────────────────────────────────────────────────────────────────\n Success Feedback Pattern (VI-589)\n\n Borealis-recommended wrapper for the app-wide success / transition\n feedback pattern. Builds on the existing Toast primitive (Sonner-backed).\n\n Key behaviours per Borealis spec (06-success-toast.html):\n - Auto-dismiss after 4s (configurable, min 3s, max 8s)\n - Optional \"Undo\" / \"View\" action\n - Deduplication within 500ms window via toast id\n - Explicit a11y live region (`role=status aria-live=polite`) for cases\n where the Toaster is not in the portal\n ────────────────────────────────────────────────────────────────────────── */\n\nexport interface SuccessToastOptions {\n /** Supporting sub-copy beneath the title (optional). */\n description?: string\n /**\n * Optional action affordance rendered as a link-style button.\n * Mapped to Sonner's `action` option.\n */\n action?: {\n label: string\n onClick: () => void\n }\n /**\n * Auto-dismiss duration in milliseconds.\n * Clamped to [3000, 8000] per Borealis spec.\n * Defaults to 4000ms.\n */\n duration?: number\n /**\n * Explicit toast id for deduplication.\n * Sonner merges calls with the same id within its window.\n */\n id?: string | number\n}\n\n/**\n * `useSuccessToast` — imperative hook for the success-feedback pattern.\n *\n * Returns a stable `showSuccess(title, options?)` function that fires\n * a Sonner success toast pre-configured with the Borealis spec defaults:\n * 4s auto-dismiss, polite live region, optional undo action.\n *\n * @example\n * const { showSuccess } = useSuccessToast();\n * await saveProject();\n * showSuccess(\"Project saved\", { description: \"All changes have been saved.\" });\n *\n * // With undo action\n * showSuccess(\"Item deleted\", {\n * action: { label: \"Undo\", onClick: handleUndo },\n * });\n */\nexport function useSuccessToast() {\n const showSuccess = React.useCallback(\n (title: string, options: SuccessToastOptions = {}) => {\n const { description, action, duration = 4000, id } = options\n const clampedDuration = Math.min(8000, Math.max(3000, duration))\n\n toast.success(title, {\n description,\n duration: clampedDuration,\n action: action\n ? { label: action.label, onClick: action.onClick }\n : undefined,\n id,\n })\n },\n []\n )\n\n return { showSuccess }\n}\n\n/* ──────────────────────────────────────────────────────────────────────────\n SuccessLiveRegion\n\n A visually-hidden `role=status aria-live=polite` node that surfaces\n success announcements to assistive technology when the Toaster portal\n is not present (e.g. server-rendered shells, testing environments).\n\n Usage: mount once in the app root alongside `<Toaster />`.\n\n The `message` prop should be set to the latest success title so screen\n readers re-announce on every update. Reset to empty string after\n announcement if needed.\n ────────────────────────────────────────────────────────────────────────── */\n\nexport interface SuccessLiveRegionProps extends React.HTMLAttributes<HTMLDivElement> {\n /** The success message to announce. Updates trigger a screen-reader announcement. */\n message?: string\n}\n\nconst SuccessLiveRegion = React.forwardRef<HTMLDivElement, SuccessLiveRegionProps>(\n ({ message = \"\", className, ...props }, ref) => {\n return (\n <div\n ref={ref}\n role=\"status\"\n aria-live=\"polite\"\n aria-atomic=\"true\"\n data-slot=\"success-live-region\"\n className={`${styles.liveRegion}${className ? ` ${className}` : \"\"}`}\n {...props}\n >\n {message}\n </div>\n )\n }\n)\nSuccessLiveRegion.displayName = \"SuccessLiveRegion\"\n\nexport { SuccessLiveRegion }\n"
|
|
2613
|
+
},
|
|
2614
|
+
{
|
|
2615
|
+
"path": "components/ui/success-feedback/success-feedback.module.css",
|
|
2616
|
+
"type": "registry:ui",
|
|
2617
|
+
"content": "/* SuccessLiveRegion — visually hidden, screen-reader only.\n Uses the standard SR-only technique: clip to a 1×1 pixel box,\n position absolute so it does not affect layout flow. */\n.liveRegion {\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"
|
|
2618
|
+
}
|
|
2619
|
+
]
|
|
2620
|
+
},
|
|
2446
2621
|
{
|
|
2447
2622
|
"name": "filter-bar",
|
|
2448
2623
|
"type": "registry:ui",
|
|
@@ -3013,6 +3188,31 @@
|
|
|
3013
3188
|
}
|
|
3014
3189
|
]
|
|
3015
3190
|
},
|
|
3191
|
+
{
|
|
3192
|
+
"name": "conflict-banner",
|
|
3193
|
+
"type": "registry:ui",
|
|
3194
|
+
"description": "Inline conflict detection and optimistic-rollback UI for concurrent edit scenarios. Surfaces a 409/ETag conflict inline within the affected record with Keep my version / Load latest actions and an optional collapsible diff view. Pairs with the useOptimisticMutation hook for a full pending → conflict → resolving → resolved state machine.",
|
|
3195
|
+
"category": "feedback",
|
|
3196
|
+
"dependencies": [
|
|
3197
|
+
"@phosphor-icons/react",
|
|
3198
|
+
"@loworbitstudio/visor-core"
|
|
3199
|
+
],
|
|
3200
|
+
"registryDependencies": [
|
|
3201
|
+
"utils"
|
|
3202
|
+
],
|
|
3203
|
+
"files": [
|
|
3204
|
+
{
|
|
3205
|
+
"path": "components/ui/conflict-banner/conflict-banner.tsx",
|
|
3206
|
+
"type": "registry:ui",
|
|
3207
|
+
"content": "'use client';\n\nimport * as React from \"react\"\nimport { Warning } from \"@phosphor-icons/react\"\nimport { cn } from \"../../../lib/utils\"\nimport styles from \"./conflict-banner.module.css\"\n\n// ── State machine types ────────────────────────────────────────────────────\n\nexport type ConflictState =\n | \"pending\"\n | \"conflict\"\n | \"resolving\"\n | \"resolved-local\"\n | \"resolved-remote\"\n\nexport interface ConflictDiff {\n field: string\n yours: string\n theirs: string\n}\n\n// ── ConflictBanner ─────────────────────────────────────────────────────────\n\nexport interface ConflictBannerProps\n extends Omit<React.HTMLAttributes<HTMLDivElement>, \"onSelect\"> {\n /** Current state of the optimistic mutation. */\n state?: ConflictState\n /** Descriptive copy — who else edited this entity. */\n description?: string\n /** Structured diff rows shown under \"See what changed\". */\n diffs?: ConflictDiff[]\n /** Called when user chooses \"Keep my version\". */\n onKeepMine?: () => void\n /** Called when user chooses \"Load latest\". */\n onLoadLatest?: () => void\n}\n\nconst ConflictBanner = React.forwardRef<HTMLDivElement, ConflictBannerProps>(\n (\n {\n className,\n state = \"conflict\",\n description = \"This record was updated by someone else.\",\n diffs = [],\n onKeepMine,\n onLoadLatest,\n ...props\n },\n ref\n ) => {\n const [diffOpen, setDiffOpen] = React.useState(false)\n\n const isVisible = state === \"conflict\" || state === \"resolving\"\n const isResolving = state === \"resolving\"\n\n if (!isVisible) return null\n\n return (\n <div\n ref={ref}\n data-slot=\"conflict-banner\"\n data-state={state}\n role=\"alert\"\n aria-live=\"assertive\"\n className={cn(styles.banner, className)}\n {...props}\n >\n {/* Header row */}\n <div className={styles.header}>\n <Warning\n className={styles.icon}\n size={18}\n weight=\"fill\"\n aria-hidden\n />\n <div className={styles.content}>\n <div className={styles.title}>\n This record was updated by someone else\n </div>\n {description && (\n <div className={styles.description}>{description}</div>\n )}\n </div>\n </div>\n\n {/* Action buttons */}\n <div className={styles.actions}>\n <button\n type=\"button\"\n className={cn(styles.btn, styles.btnKeep)}\n onClick={onKeepMine}\n disabled={isResolving}\n aria-busy={isResolving}\n >\n {isResolving && <span className={styles.spinner} aria-hidden />}\n {isResolving ? \"Saving…\" : \"Keep my version\"}\n </button>\n <button\n type=\"button\"\n className={cn(styles.btn, styles.btnLoad)}\n onClick={onLoadLatest}\n disabled={isResolving}\n >\n Load latest\n </button>\n </div>\n\n {/* Optional diff view */}\n {diffs.length > 0 && (\n <>\n <button\n type=\"button\"\n className={styles.diffToggle}\n onClick={() => setDiffOpen((o) => !o)}\n aria-expanded={diffOpen}\n aria-controls=\"conflict-banner-diff\"\n >\n {diffOpen ? \"Hide changes\" : \"See what changed\"}\n </button>\n {diffOpen && (\n <div\n id=\"conflict-banner-diff\"\n className={styles.diff}\n role=\"region\"\n aria-label=\"Conflict diff\"\n >\n {diffs.map((d) => (\n <div key={d.field} className={styles.diffRow}>\n <span className={styles.diffField}>{d.field} — </span>\n <span className={styles.diffYours}>\n Yours: “{d.yours}”\n </span>\n {\" · \"}\n <span className={styles.diffTheirs}>\n Theirs: “{d.theirs}”\n </span>\n </div>\n ))}\n </div>\n )}\n </>\n )}\n </div>\n )\n }\n)\nConflictBanner.displayName = \"ConflictBanner\"\n\n// ── useOptimisticMutation ──────────────────────────────────────────────────\n\nexport type MutationStatus =\n | \"idle\"\n | \"pending\"\n | \"conflict\"\n | \"resolving\"\n | \"resolved-local\"\n | \"resolved-remote\"\n\nexport interface UseOptimisticMutationOptions<T> {\n /** Apply the optimistic value immediately before the async call resolves. */\n onOptimisticApply?: (value: T) => void\n /** Revert to the previous value on conflict or error. */\n onRollback?: (previousValue: T) => void\n /** Load the latest remote value after choosing \"Load latest\". */\n onLoadLatest?: () => Promise<T>\n /** Submit the user's version when choosing \"Keep my version\". */\n onKeepMine?: (value: T) => Promise<void>\n}\n\nexport interface UseOptimisticMutationReturn<T> {\n status: MutationStatus\n conflictState: ConflictState\n /** The value currently displayed (optimistic or remote). */\n currentValue: T | undefined\n /** Call with the mutation function — returns a conflict result on 409. */\n mutate: (\n value: T,\n fn: (value: T) => Promise<void>\n ) => Promise<void>\n /** Resolve by keeping the user's version. */\n keepMine: () => Promise<void>\n /** Resolve by loading the latest remote state. */\n loadLatest: () => Promise<void>\n /** Reset to idle state. */\n reset: () => void\n}\n\nexport function useOptimisticMutation<T>(\n initialValue: T,\n options: UseOptimisticMutationOptions<T> = {}\n): UseOptimisticMutationReturn<T> {\n const { onOptimisticApply, onRollback, onLoadLatest, onKeepMine } = options\n\n const [status, setStatus] = React.useState<MutationStatus>(\"idle\")\n const [currentValue, setCurrentValue] = React.useState<T>(initialValue)\n const previousValueRef = React.useRef<T>(initialValue)\n const pendingValueRef = React.useRef<T>(initialValue)\n\n const conflictState = React.useMemo<ConflictState>(() => {\n switch (status) {\n case \"idle\":\n return \"pending\"\n case \"pending\":\n return \"pending\"\n case \"conflict\":\n return \"conflict\"\n case \"resolving\":\n return \"resolving\"\n case \"resolved-local\":\n return \"resolved-local\"\n case \"resolved-remote\":\n return \"resolved-remote\"\n default:\n return \"pending\"\n }\n }, [status])\n\n const mutate = React.useCallback(\n async (value: T, fn: (value: T) => Promise<void>) => {\n previousValueRef.current = currentValue\n pendingValueRef.current = value\n\n // Optimistic apply\n setCurrentValue(value)\n setStatus(\"pending\")\n onOptimisticApply?.(value)\n\n try {\n await fn(value)\n setStatus(\"idle\")\n } catch {\n // On any error, treat as conflict and rollback\n setCurrentValue(previousValueRef.current)\n onRollback?.(previousValueRef.current)\n setStatus(\"conflict\")\n }\n },\n [currentValue, onOptimisticApply, onRollback]\n )\n\n const keepMine = React.useCallback(async () => {\n setStatus(\"resolving\")\n try {\n if (onKeepMine) {\n await onKeepMine(pendingValueRef.current)\n }\n setCurrentValue(pendingValueRef.current)\n setStatus(\"resolved-local\")\n } catch {\n setStatus(\"conflict\")\n }\n }, [onKeepMine])\n\n const loadLatest = React.useCallback(async () => {\n setStatus(\"resolving\")\n try {\n if (onLoadLatest) {\n const latest = await onLoadLatest()\n setCurrentValue(latest)\n }\n setStatus(\"resolved-remote\")\n } catch {\n setStatus(\"conflict\")\n }\n }, [onLoadLatest])\n\n const reset = React.useCallback(() => {\n setStatus(\"idle\")\n }, [])\n\n return {\n status,\n conflictState,\n currentValue,\n mutate,\n keepMine,\n loadLatest,\n reset,\n }\n}\n\nexport { ConflictBanner }\n"
|
|
3208
|
+
},
|
|
3209
|
+
{
|
|
3210
|
+
"path": "components/ui/conflict-banner/conflict-banner.module.css",
|
|
3211
|
+
"type": "registry:ui",
|
|
3212
|
+
"content": "/* ConflictBanner — inline conflict detection for optimistic mutations */\n\n@keyframes bannerIn {\n from {\n opacity: 0;\n transform: translateY(calc(var(--spacing-2, 0.5rem) * -1));\n }\n to {\n opacity: 1;\n transform: translateY(0);\n }\n}\n\n@keyframes spin {\n to {\n transform: rotate(360deg);\n }\n}\n\n.banner {\n background-color: var(--surface-warning-subtle, rgba(255, 178, 23, 0.08));\n border-left: var(--stroke-width-medium, 3px) solid var(--border-warning, #f59e0b);\n border-radius: 0 var(--radius-md, 0.375rem) var(--radius-md, 0.375rem) 0;\n padding: var(--spacing-4, 1rem) var(--spacing-4, 1rem);\n animation: bannerIn var(--motion-duration-fast, 200ms) var(--motion-easing-enter, ease-out);\n}\n\n/* Header row: icon + content */\n.header {\n display: flex;\n align-items: flex-start;\n gap: var(--spacing-3, 0.75rem);\n margin-bottom: var(--spacing-3, 0.75rem);\n}\n\n.icon {\n color: var(--text-warning, #b45309);\n flex-shrink: 0;\n}\n\n.content {\n flex: 1;\n min-width: 0;\n}\n\n.title {\n font-size: var(--font-size-sm, 0.875rem);\n font-weight: var(--font-weight-semibold, 600);\n color: var(--text-primary, #111827);\n line-height: 1.5;\n}\n\n.description {\n font-size: var(--font-size-sm, 0.875rem);\n color: var(--text-secondary, #4b5563);\n margin-top: var(--spacing-1, 0.25rem);\n line-height: 1.5;\n}\n\n/* Action buttons */\n.actions {\n display: flex;\n gap: var(--spacing-2, 0.5rem);\n flex-wrap: wrap;\n}\n\n.btn {\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 font-weight: var(--font-weight-semibold, 600);\n padding: var(--spacing-2, 0.5rem) var(--spacing-3, 0.75rem);\n border-radius: var(--radius-md, 0.375rem);\n border: var(--stroke-width-thin, 1px) solid var(--border-default, #e5e7eb);\n background: transparent;\n cursor: pointer;\n transition:\n background-color var(--motion-duration-fast, 150ms) var(--motion-easing-base, ease),\n border-color var(--motion-duration-fast, 150ms) var(--motion-easing-base, ease);\n}\n\n.btn:disabled {\n opacity: var(--opacity-50, 0.5);\n cursor: not-allowed;\n}\n\n/* Keep my version — primary intent */\n.btnKeep {\n color: var(--text-warning, #b45309);\n border-color: color-mix(in srgb, var(--border-warning, #f59e0b) 40%, transparent);\n}\n\n.btnKeep:hover:not(:disabled) {\n background-color: var(--surface-warning-subtle, rgba(255, 178, 23, 0.06));\n border-color: var(--border-warning, #f59e0b);\n}\n\n/* Load latest — ghost intent */\n.btnLoad {\n color: var(--text-secondary, #4b5563);\n border-color: var(--border-default, #e5e7eb);\n}\n\n.btnLoad:hover:not(:disabled) {\n background-color: var(--surface-subtle, #f9fafb);\n}\n\n/* Spinner for resolving state */\n.spinner {\n display: inline-block;\n width: 0.8125rem;\n height: 0.8125rem;\n border: var(--stroke-width-thin, 1px) solid color-mix(in srgb, var(--border-warning, #f59e0b) 30%, transparent);\n border-top-color: var(--text-warning, #b45309);\n border-radius: 50%;\n animation: spin var(--motion-duration-800, 800ms) var(--motion-easing-linear, linear) infinite;\n flex-shrink: 0;\n}\n\n/* Diff toggle */\n.diffToggle {\n display: inline-block;\n font-size: var(--font-size-xs, 0.75rem);\n font-weight: var(--font-weight-medium, 500);\n color: var(--text-tertiary, #9ca3af);\n background: none;\n border: none;\n cursor: pointer;\n margin-top: var(--spacing-3, 0.75rem);\n text-decoration: underline;\n text-underline-offset: 2px;\n padding: 0;\n}\n\n.diffToggle:hover {\n color: var(--text-secondary, #4b5563);\n}\n\n/* Diff panel */\n.diff {\n margin-top: var(--spacing-3, 0.75rem);\n background-color: var(--surface-subtle, #f9fafb);\n border-radius: var(--radius-md, 0.375rem);\n padding: var(--spacing-2, 0.5rem) var(--spacing-3, 0.75rem);\n}\n\n.diffRow {\n font-size: var(--font-size-xs, 0.75rem);\n line-height: 1.8;\n}\n\n.diffField {\n font-weight: var(--font-weight-semibold, 600);\n color: var(--text-secondary, #4b5563);\n}\n\n.diffYours {\n color: var(--text-warning, #b45309);\n}\n\n.diffTheirs {\n color: var(--text-error, #b91c1c);\n}\n"
|
|
3213
|
+
}
|
|
3214
|
+
]
|
|
3215
|
+
},
|
|
3016
3216
|
{
|
|
3017
3217
|
"name": "use-media-query",
|
|
3018
3218
|
"type": "registry:hook",
|
|
@@ -4782,7 +4982,7 @@
|
|
|
4782
4982
|
{
|
|
4783
4983
|
"path": "components/devtools/source-inspector/visor-component-names.generated.ts",
|
|
4784
4984
|
"type": "registry:devtool",
|
|
4785
|
-
"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"
|
|
4985
|
+
"content": "// THIS FILE IS GENERATED BY scripts/generate-visor-component-names.ts.\n// Do not edit by hand. Re-run `npm run generate:component-names` after\n// adding, removing, or renaming a Visor component.\n//\n// Source of truth: registry/registry-{ui,blocks,deck,visual,devtools}.ts\n// Used by: components/devtools/source-inspector/* (VI-311)\n\nexport const VISOR_COMPONENT_NAMES: ReadonlySet<string> = new Set([\n \"AccessibilitySection\",\n \"AccessibilitySlide\",\n \"AccessibilitySpecimen\",\n \"Accordion\",\n \"AccordionContent\",\n \"AccordionItem\",\n \"AccordionTrigger\",\n \"ActivityFeed\",\n \"ActivityFeedContext\",\n \"ActivityFeedItem\",\n \"ActivityFeedRoot\",\n \"AdminDashboard\",\n \"AdminDetailDrawer\",\n \"AdminListPage\",\n \"AdminListPageInner\",\n \"AdminSettingsPage\",\n \"AdminShell\",\n \"AdminTabbedEditor\",\n \"AdminWizard\",\n \"Alert\",\n \"AlertActions\",\n \"AlertDescription\",\n \"AlertTitle\",\n \"AmbientGlow\",\n \"Avatar\",\n \"AvatarFallback\",\n \"AvatarImage\",\n \"AvatarStack\",\n \"Badge\",\n \"Banner\",\n \"BannerAction\",\n \"BannerDescription\",\n \"BannerTitle\",\n \"BentoGrid\",\n \"BentoTile\",\n \"BentoTileBody\",\n \"BentoTileDescription\",\n \"BentoTileFigure\",\n \"BentoTileHeadline\",\n \"BentoTileMedia\",\n \"BentoTileMeta\",\n \"BentoTileTitle\",\n \"Box\",\n \"Breadcrumb\",\n \"BreadcrumbEllipsis\",\n \"BreadcrumbItem\",\n \"BreadcrumbLink\",\n \"BreadcrumbList\",\n \"BreadcrumbPage\",\n \"BreadcrumbSeparator\",\n \"BrowserFrame\",\n \"BulkActionBar\",\n \"Button\",\n \"ButtonSpecimenSection\",\n \"ButtonSpecimenSlide\",\n \"Calendar\",\n \"Card\",\n \"CardContent\",\n \"CardDescription\",\n \"CardFooter\",\n \"CardGrid\",\n \"CardHeader\",\n \"CardLift\",\n \"CardTitle\",\n \"Carousel\",\n \"CarouselContent\",\n \"CarouselContext\",\n \"CarouselGallery\",\n \"CarouselItem\",\n \"CarouselNext\",\n \"CarouselPrevious\",\n \"ChallengeCard\",\n \"ChallengeCardAction\",\n \"ChallengeCardActions\",\n \"ChallengeCardBody\",\n \"ChallengeCardGate\",\n \"ChallengeCardHeader\",\n \"ChartContainer\",\n \"ChartContext\",\n \"ChartLegend\",\n \"ChartLegendContent\",\n \"ChartStyle\",\n \"ChartTooltip\",\n \"ChartTooltipContent\",\n \"Checkbox\",\n \"CheckGroup\",\n \"CheckRow\",\n \"Chip\",\n \"ChipGroup\",\n \"ChipGroupContext\",\n \"ChipGroupItem\",\n \"ChoiceChip\",\n \"ChromeButton\",\n \"ClosingSlide\",\n \"CodeBlock\",\n \"Collapsible\",\n \"CollapsibleContent\",\n \"CollapsibleTrigger\",\n \"ColorBar\",\n \"ColorPaletteSection\",\n \"ColorPicker\",\n \"ColorPickerSurface\",\n \"ColorSwatch\",\n \"ColorSwatchGrid\",\n \"Combobox\",\n \"ComboboxContent\",\n \"ComboboxContext\",\n \"ComboboxEmpty\",\n \"ComboboxGroup\",\n \"ComboboxInput\",\n \"ComboboxItem\",\n \"ComboboxSeparator\",\n \"Command\",\n \"CommandDialog\",\n \"CommandEmpty\",\n \"CommandGroup\",\n \"CommandInput\",\n \"CommandItem\",\n \"CommandList\",\n \"CommandLoading\",\n \"CommandSeparator\",\n \"CommandShortcut\",\n \"ComponentShowcaseContent\",\n \"ComponentShowcaseSection\",\n \"ComponentShowcaseSlide\",\n \"Composer\",\n \"ComposerContext\",\n \"ComposerField\",\n \"ComposerSend\",\n \"ComposerSpacer\",\n \"ComposerToolbar\",\n \"ComposerToolButton\",\n \"ConceptSlide\",\n \"ConfigurationPanel\",\n \"ConfirmDialog\",\n \"ConflictBanner\",\n \"Container\",\n \"ContextMenu\",\n \"ContextMenuCheckboxItem\",\n \"ContextMenuContent\",\n \"ContextMenuGroup\",\n \"ContextMenuItem\",\n \"ContextMenuLabel\",\n \"ContextMenuPortal\",\n \"ContextMenuRadioGroup\",\n \"ContextMenuRadioItem\",\n \"ContextMenuSeparator\",\n \"ContextMenuShortcut\",\n \"ContextMenuSub\",\n \"ContextMenuSubContent\",\n \"ContextMenuSubTrigger\",\n \"ContextMenuTrigger\",\n \"Controls\",\n \"CtaSection\",\n \"DataTable\",\n \"DataTableInner\",\n \"DatePicker\",\n \"DateRangePicker\",\n \"DeckContext\",\n \"DeckFooter\",\n \"DeckLayout\",\n \"DeckProvider\",\n \"DeckRenderer\",\n \"DesignSystemDeck\",\n \"DesignSystemSpecimen\",\n \"Dialog\",\n \"DialogClose\",\n \"DialogContent\",\n \"DialogDescription\",\n \"DialogFooter\",\n \"DialogHeader\",\n \"DialogOverlay\",\n \"DialogPortal\",\n \"DialogTitle\",\n \"DialogTrigger\",\n \"DotNav\",\n \"DropdownMenu\",\n \"DropdownMenuCheckboxItem\",\n \"DropdownMenuContent\",\n \"DropdownMenuGroup\",\n \"DropdownMenuItem\",\n \"DropdownMenuLabel\",\n \"DropdownMenuPortal\",\n \"DropdownMenuRadioGroup\",\n \"DropdownMenuRadioItem\",\n \"DropdownMenuSeparator\",\n \"DropdownMenuShortcut\",\n \"DropdownMenuSub\",\n \"DropdownMenuSubContent\",\n \"DropdownMenuSubTrigger\",\n \"DropdownMenuTrigger\",\n \"EditableBlock\",\n \"ElevationCard\",\n \"ElevationSlide\",\n \"EmptyState\",\n \"ErrorPlacard\",\n \"ExportMenu\",\n \"FeaturesGrid\",\n \"Field\",\n \"FieldDescription\",\n \"FieldError\",\n \"FieldLabel\",\n \"Fieldset\",\n \"FieldsetLegend\",\n \"FileUpload\",\n \"FilterBar\",\n \"FilterChip\",\n \"FontShowcase\",\n \"FontShowcaseGrid\",\n \"Footer\",\n \"FooterSection\",\n \"Form\",\n \"FormError\",\n \"FormErrorDescription\",\n \"FormErrorTitle\",\n \"FormField\",\n \"FormSpecimenSection\",\n \"FormSpecimenSlide\",\n \"FullscreenOverlay\",\n \"FullscreenOverlayContent\",\n \"FullscreenOverlayTrigger\",\n \"GrainOverlay\",\n \"Grid\",\n \"Header\",\n \"Heading\",\n \"HeroGlow\",\n \"HeroSection\",\n \"HeroSlide\",\n \"HoverCard\",\n \"HoverCardContent\",\n \"HoverCardTrigger\",\n \"IconGrid\",\n \"IconGridSection\",\n \"IconSizeRow\",\n \"IconsSlide\",\n \"Image\",\n \"InfographicBar\",\n \"Inline\",\n \"Input\",\n \"Kbd\",\n \"KeyValueList\",\n \"Label\",\n \"Landing\",\n \"Lightbox\",\n \"LightboxContent\",\n \"LightboxContext\",\n \"LightboxTrigger\",\n \"LoginForm\",\n \"Marquee\",\n \"MarqueeBandRenderer\",\n \"MatrixCell\",\n \"MatrixTable\",\n \"MatrixTableInner\",\n \"Menubar\",\n \"MenubarCheckboxItem\",\n \"MenubarContent\",\n \"MenubarGroup\",\n \"MenubarItem\",\n \"MenubarLabel\",\n \"MenubarMenu\",\n \"MenubarRadioGroup\",\n \"MenubarRadioItem\",\n \"MenubarSeparator\",\n \"MenubarShortcut\",\n \"MenubarSub\",\n \"MenubarSubContent\",\n \"MenubarSubTrigger\",\n \"MenubarTrigger\",\n \"MotionDuration\",\n \"MotionDurationSection\",\n \"MotionEasing\",\n \"MotionEasingSection\",\n \"MotionSlide\",\n \"NameRoster\",\n \"NameRosterItem\",\n \"Navbar\",\n \"NavbarBrand\",\n \"NavbarContent\",\n \"NavbarItem\",\n \"NavbarLink\",\n \"NavbarToggle\",\n \"NumberInput\",\n \"OfflineBanner\",\n \"OpacityBar\",\n \"OpacitySlide\",\n \"OTPInput\",\n \"PageHeader\",\n \"Pagination\",\n \"PaginationContent\",\n \"PaginationEllipsis\",\n \"PaginationItem\",\n \"PaginationLink\",\n \"PaginationNext\",\n \"PaginationPrevious\",\n \"PasswordInput\",\n \"PhoneInput\",\n \"Popover\",\n \"PopoverAnchor\",\n \"PopoverContent\",\n \"PopoverFooter\",\n \"PopoverSelectionItem\",\n \"PopoverSelectionLabel\",\n \"PopoverSelectionList\",\n \"PopoverTrigger\",\n \"PricingSection\",\n \"ProfileMenu\",\n \"Progress\",\n \"PrototypeReview\",\n \"QuickActions\",\n \"RadioGroup\",\n \"RadioGroupItem\",\n \"RadiusScale\",\n \"RadiusSection\",\n \"RadiusSlide\",\n \"RightRailList\",\n \"ScoreIndicator\",\n \"ScrollArea\",\n \"ScrollBar\",\n \"SearchInput\",\n \"SectionHeader\",\n \"SectionIntro\",\n \"SectionNav\",\n \"SectionNavItem\",\n \"SegmentedProgress\",\n \"Select\",\n \"SelectContent\",\n \"SelectGroup\",\n \"SelectionListContext\",\n \"SelectItem\",\n \"SelectLabel\",\n \"SelectScrollDownButton\",\n \"SelectScrollUpButton\",\n \"SelectSeparator\",\n \"SelectTrigger\",\n \"SelectValue\",\n \"SemanticColorGrid\",\n \"SemanticColorItem\",\n \"SemanticTokensSlide\",\n \"Separator\",\n \"SessionTimeout\",\n \"ShadowSection\",\n \"Sheet\",\n \"SheetClose\",\n \"SheetContent\",\n \"SheetDescription\",\n \"SheetFooter\",\n \"SheetHeader\",\n \"SheetOverlay\",\n \"SheetPortal\",\n \"SheetTitle\",\n \"SheetTrigger\",\n \"Sidebar\",\n \"SidebarContent\",\n \"SidebarContext\",\n \"SidebarFooter\",\n \"SidebarGroup\",\n \"SidebarGroupAction\",\n \"SidebarGroupContent\",\n \"SidebarGroupLabel\",\n \"SidebarHeader\",\n \"SidebarInset\",\n \"SidebarMenu\",\n \"SidebarMenuAction\",\n \"SidebarMenuBadge\",\n \"SidebarMenuButton\",\n \"SidebarMenuItem\",\n \"SidebarMenuSub\",\n \"SidebarMenuSubButton\",\n \"SidebarMenuSubItem\",\n \"SidebarProvider\",\n \"SidebarRail\",\n \"SidebarSeparator\",\n \"SidebarTrigger\",\n \"Skeleton\",\n \"SkeletonDetail\",\n \"SkeletonList\",\n \"SkeletonTable\",\n \"Slide\",\n \"SlideHeader\",\n \"Slider\",\n \"SliderControl\",\n \"SlideThemeContext\",\n \"SlideThemeProvider\",\n \"SlowNetworkBar\",\n \"SourceInspector\",\n \"SourceInspectorContext\",\n \"SourceInspectorDevImpl\",\n \"SourceInspectorProvider\",\n \"SourceInspectorRunner\",\n \"SourceInspectorToggle\",\n \"SpacingScale\",\n \"SpacingSection\",\n \"SpacingSlide\",\n \"Sparkline\",\n \"SpecimenCard\",\n \"SpecimenCardFooter\",\n \"Sphere\",\n \"SpherePlayground\",\n \"Spinner\",\n \"Stack\",\n \"Stage\",\n \"StatCard\",\n \"StatHero\",\n \"StationSpectrum\",\n \"StatusBadge\",\n \"StatusColorsSlide\",\n \"StatusDot\",\n \"Stepper\",\n \"StepperContext\",\n \"StepperDescription\",\n \"StepperItem\",\n \"StepperSeparator\",\n \"StepperTitle\",\n \"StepperTrigger\",\n \"StepsSection\",\n \"StructuredPrompt\",\n \"StructuredPromptBody\",\n \"StructuredPromptHeader\",\n \"StructuredPromptHint\",\n \"StructuredPromptSlot\",\n \"SuccessLiveRegion\",\n \"SurfaceRow\",\n \"SurfaceScaleStack\",\n \"SurfaceSection\",\n \"Switch\",\n \"Table\",\n \"TableBody\",\n \"TableCaption\",\n \"TableCell\",\n \"TableFooter\",\n \"TableHead\",\n \"TableHeader\",\n \"TableRow\",\n \"Tabs\",\n \"TabsContent\",\n \"TabsList\",\n \"TabsTrigger\",\n \"TagInput\",\n \"TestimonialAttribution\",\n \"TestimonialSection\",\n \"Text\",\n \"Textarea\",\n \"ThemeArchitectureSlide\",\n \"ThemeColorsSlide\",\n \"ThemeSwitcher\",\n \"Timeline\",\n \"TimelineContent\",\n \"TimelineDescription\",\n \"TimelineIcon\",\n \"TimelineItem\",\n \"TimelineTimestamp\",\n \"TimelineTitle\",\n \"TitleSlide\",\n \"ToastCard\",\n \"ToastCardStack\",\n \"Toaster\",\n \"TOCSlide\",\n \"ToggleButton\",\n \"ToggleDevImpl\",\n \"ToggleGroup\",\n \"ToggleGroupContext\",\n \"ToggleGroupItem\",\n \"Tooltip\",\n \"TooltipContent\",\n \"TooltipProvider\",\n \"TooltipTrigger\",\n \"TypeBodySlide\",\n \"TypeDisplaySlide\",\n \"TypeScaleStack\",\n \"TypeSpecimen\",\n \"TypographySection\",\n \"Vignette\",\n \"WorkspaceSwitcher\",\n])\n"
|
|
4786
4986
|
}
|
|
4787
4987
|
]
|
|
4788
4988
|
},
|