@loworbitstudio/visor 0.7.0 → 0.9.1
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/README.md +99 -0
- package/dist/CHANGELOG.json +37 -1
- package/dist/index.js +1365 -300
- package/dist/registry.json +183 -2
- package/dist/visor-manifest.json +741 -5
- package/package.json +2 -2
package/dist/registry.json
CHANGED
|
@@ -402,6 +402,32 @@
|
|
|
402
402
|
}
|
|
403
403
|
]
|
|
404
404
|
},
|
|
405
|
+
{
|
|
406
|
+
"name": "chip",
|
|
407
|
+
"type": "registry:ui",
|
|
408
|
+
"description": "A chip family — Chip (display), ChoiceChip (radio-style single-select), and FilterChip (toggle-style multi-select) — modeled on Flutter Material's Chip API.",
|
|
409
|
+
"category": "form",
|
|
410
|
+
"dependencies": [
|
|
411
|
+
"class-variance-authority",
|
|
412
|
+
"@phosphor-icons/react",
|
|
413
|
+
"@loworbitstudio/visor-core"
|
|
414
|
+
],
|
|
415
|
+
"registryDependencies": [
|
|
416
|
+
"utils"
|
|
417
|
+
],
|
|
418
|
+
"files": [
|
|
419
|
+
{
|
|
420
|
+
"path": "components/ui/chip/chip.tsx",
|
|
421
|
+
"type": "registry:ui",
|
|
422
|
+
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport { XIcon } from \"@phosphor-icons/react\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\nimport { cn } from \"../../../lib/utils\"\nimport styles from \"./chip.module.css\"\n\n/* ─── Chip (base) ────────────────────────────────────────────────────── */\n\nconst chipVariants = cva(styles.base, {\n variants: {\n variant: {\n default: styles.variantDefault,\n outlined: styles.variantOutlined,\n },\n size: {\n sm: styles.sizeSm,\n md: styles.sizeMd,\n lg: styles.sizeLg,\n },\n },\n defaultVariants: {\n variant: \"default\",\n size: \"md\",\n },\n})\n\nexport interface ChipProps\n extends React.HTMLAttributes<HTMLDivElement>,\n VariantProps<typeof chipVariants> {\n /** Optional avatar/icon rendered before the label. */\n avatar?: React.ReactNode\n /** Optional leading icon rendered before the label (after avatar). */\n leadingIcon?: React.ReactNode\n /** Label content. Renders as children if omitted. */\n label?: React.ReactNode\n /** Custom delete icon. Defaults to XIcon. */\n deleteIcon?: React.ReactNode\n /** Called when the delete button is activated. When provided the delete button is shown. */\n onDeleted?: () => void\n /** Aria label for the delete button. Defaults to \"Remove\". */\n deleteLabel?: string\n}\n\nconst Chip = React.forwardRef<HTMLDivElement, ChipProps>(\n (\n {\n className,\n variant,\n size,\n avatar,\n leadingIcon,\n label,\n children,\n deleteIcon,\n onDeleted,\n deleteLabel = \"Remove\",\n ...props\n },\n ref,\n ) => {\n const content = label ?? children\n\n return (\n <div\n ref={ref}\n data-slot=\"chip\"\n data-variant={variant ?? \"default\"}\n data-size={size ?? \"md\"}\n className={cn(chipVariants({ variant, size }), className)}\n {...props}\n >\n {avatar ? (\n <span className={styles.avatar} aria-hidden=\"true\">\n {avatar}\n </span>\n ) : null}\n {leadingIcon ? (\n <span className={styles.leadingIcon} aria-hidden=\"true\">\n {leadingIcon}\n </span>\n ) : null}\n <span className={styles.label}>{content}</span>\n {onDeleted ? (\n <button\n type=\"button\"\n aria-label={deleteLabel}\n className={styles.deleteButton}\n onClick={(e) => {\n e.stopPropagation()\n onDeleted()\n }}\n tabIndex={0}\n >\n {deleteIcon ?? (\n <XIcon\n size={12}\n weight=\"bold\"\n aria-hidden=\"true\"\n className={styles.deleteIcon}\n />\n )}\n </button>\n ) : null}\n </div>\n )\n },\n)\nChip.displayName = \"Chip\"\n\n/* ─── ChoiceChip (radio-style single-select) ─────────────────────────── */\n\nexport interface ChoiceChipProps\n extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, \"onClick\">,\n VariantProps<typeof chipVariants> {\n /** Whether this chip is currently selected. */\n selected?: boolean\n /** Optional avatar/icon rendered before the label. */\n avatar?: React.ReactNode\n /** Optional leading icon rendered before the label. */\n leadingIcon?: React.ReactNode\n /** Label text or content. */\n label?: React.ReactNode\n /** Called when the chip is pressed. */\n onPressed?: () => void\n /** Value used when in a ChipGroup. */\n value?: string\n}\n\nconst ChoiceChip = React.forwardRef<HTMLButtonElement, ChoiceChipProps>(\n (\n {\n className,\n variant,\n size,\n selected = false,\n avatar,\n leadingIcon,\n label,\n children,\n onPressed,\n value,\n disabled,\n ...props\n },\n ref,\n ) => {\n const content = label ?? children\n\n return (\n <button\n ref={ref}\n type=\"button\"\n role=\"radio\"\n aria-checked={selected}\n data-slot=\"choice-chip\"\n data-variant={variant ?? \"default\"}\n data-size={size ?? \"md\"}\n data-selected={selected ? \"true\" : \"false\"}\n data-value={value}\n disabled={disabled}\n className={cn(\n chipVariants({ variant, size }),\n styles.interactive,\n selected && styles.selected,\n className,\n )}\n onClick={onPressed}\n {...props}\n >\n {avatar ? (\n <span className={styles.avatar} aria-hidden=\"true\">\n {avatar}\n </span>\n ) : null}\n {leadingIcon ? (\n <span className={styles.leadingIcon} aria-hidden=\"true\">\n {leadingIcon}\n </span>\n ) : null}\n <span className={styles.label}>{content}</span>\n </button>\n )\n },\n)\nChoiceChip.displayName = \"ChoiceChip\"\n\n/* ─── FilterChip (multi-select toggle-style) ─────────────────────────── */\n\nexport interface FilterChipProps\n extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, \"onClick\">,\n VariantProps<typeof chipVariants> {\n /** Whether this chip is currently active/selected. */\n selected?: boolean\n /** Optional leading icon. */\n leadingIcon?: React.ReactNode\n /** Label text or content. */\n label?: React.ReactNode\n /** Called when the chip is toggled. */\n onPressed?: () => void\n /** Value used when in a ChipGroup. */\n value?: string\n}\n\nconst FilterChip = React.forwardRef<HTMLButtonElement, FilterChipProps>(\n (\n {\n className,\n variant,\n size,\n selected = false,\n leadingIcon,\n label,\n children,\n onPressed,\n value,\n disabled,\n ...props\n },\n ref,\n ) => {\n const content = label ?? children\n\n return (\n <button\n ref={ref}\n type=\"button\"\n role=\"checkbox\"\n aria-checked={selected}\n data-slot=\"filter-chip\"\n data-variant={variant ?? \"default\"}\n data-size={size ?? \"md\"}\n data-selected={selected ? \"true\" : \"false\"}\n data-value={value}\n disabled={disabled}\n className={cn(\n chipVariants({ variant, size }),\n styles.interactive,\n selected && styles.selected,\n className,\n )}\n onClick={onPressed}\n {...props}\n >\n {leadingIcon ? (\n <span className={styles.leadingIcon} aria-hidden=\"true\">\n {leadingIcon}\n </span>\n ) : null}\n <span className={styles.label}>{content}</span>\n </button>\n )\n },\n)\nFilterChip.displayName = \"FilterChip\"\n\nexport { Chip, chipVariants, ChoiceChip, FilterChip }\n"
|
|
423
|
+
},
|
|
424
|
+
{
|
|
425
|
+
"path": "components/ui/chip/chip.module.css",
|
|
426
|
+
"type": "registry:ui",
|
|
427
|
+
"content": "/* Chip base */\n.base {\n display: inline-flex;\n align-items: center;\n gap: var(--spacing-1, 0.25rem);\n width: fit-content;\n flex-shrink: 0;\n border-radius: var(--radius-full, 9999px);\n border: var(--stroke-width-thin, 1px) solid var(--border-default, #e5e7eb);\n background-color: var(--surface-card, #ffffff);\n color: var(--text-primary, #111827);\n font-family: var(--font-body, inherit);\n font-weight: var(--font-weight-medium, 500);\n white-space: nowrap;\n user-select: none;\n transition:\n background-color var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out),\n color var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out),\n border-color var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out);\n}\n\n/* Variant: default (filled/tonal surface) */\n.variantDefault {\n background-color: var(--surface-muted, #f3f4f6);\n border-color: transparent;\n color: var(--text-primary, #111827);\n}\n\n/* Variant: outlined */\n.variantOutlined {\n background-color: transparent;\n border-color: var(--border-default, #e5e7eb);\n color: var(--text-primary, #111827);\n}\n\n/* Sizes */\n.sizeSm {\n height: 1.5rem;\n padding: 0 var(--spacing-2, 0.5rem);\n font-size: var(--font-size-xs, 0.75rem);\n gap: var(--spacing-1, 0.25rem);\n}\n\n.sizeMd {\n height: 2rem;\n padding: 0 var(--spacing-3, 0.75rem);\n font-size: var(--font-size-sm, 0.875rem);\n gap: var(--spacing-1, 0.25rem);\n}\n\n.sizeLg {\n height: 2.5rem;\n padding: 0 var(--spacing-4, 1rem);\n font-size: var(--font-size-base, 1rem);\n gap: var(--spacing-2, 0.5rem);\n}\n\n/* Sub-parts */\n.avatar {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n}\n\n.leadingIcon {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n}\n\n.label {\n flex: 1 1 auto;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.deleteButton {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n background: transparent;\n border: none;\n padding: 0;\n cursor: pointer;\n color: var(--text-secondary, #6b7280);\n border-radius: var(--radius-full, 9999px);\n transition: color var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out),\n background-color var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out);\n width: 1rem;\n height: 1rem;\n}\n\n@media (hover: hover) {\n .deleteButton:hover {\n color: var(--text-primary, #111827);\n background-color: color-mix(in srgb, var(--surface-interactive-hover, #f3f4f6) 60%, transparent);\n }\n}\n\n.deleteButton:focus-visible {\n outline: var(--focus-ring-width, 2px) solid var(--border-focus, #6b7280);\n outline-offset: var(--focus-ring-offset, 1px);\n}\n\n.deleteIcon {\n display: block;\n}\n\n/* Interactive chips (ChoiceChip, FilterChip) */\n.interactive {\n cursor: pointer;\n border: var(--stroke-width-thin, 1px) solid transparent;\n}\n\nbutton.base,\n.interactive {\n /* Inherit base layout; add button-specific resets */\n text-align: left;\n appearance: none;\n -webkit-appearance: none;\n}\n\n.interactive:disabled {\n cursor: not-allowed;\n opacity: var(--opacity-40, 0.4);\n pointer-events: none;\n}\n\n.interactive:focus-visible {\n outline: var(--focus-ring-width, 2px) solid var(--border-focus, #6b7280);\n outline-offset: var(--focus-ring-offset, 2px);\n}\n\n@media (hover: hover) {\n .interactive:hover:not(:disabled):not([data-selected=\"true\"]) {\n background-color: var(--surface-interactive-hover, #f3f4f6);\n border-color: var(--border-default, #e5e7eb);\n }\n}\n\n.interactive:active:not(:disabled) {\n transform: translateY(1px);\n transition: none;\n}\n\n/* Selected state */\n.selected {\n background-color: var(--surface-accent-subtle, #eff6ff);\n border-color: var(--surface-accent-default, #3b82f6);\n color: var(--text-link, #2563eb);\n}\n\n@media (hover: hover) {\n .interactive.selected:hover:not(:disabled) {\n background-color: color-mix(in srgb, var(--surface-accent-subtle, #eff6ff) 80%, var(--surface-interactive-hover, #f3f4f6) 20%);\n }\n}\n"
|
|
428
|
+
}
|
|
429
|
+
]
|
|
430
|
+
},
|
|
405
431
|
{
|
|
406
432
|
"name": "avatar",
|
|
407
433
|
"type": "registry:ui",
|
|
@@ -808,6 +834,40 @@
|
|
|
808
834
|
}
|
|
809
835
|
]
|
|
810
836
|
},
|
|
837
|
+
{
|
|
838
|
+
"name": "bento-grid",
|
|
839
|
+
"type": "registry:ui",
|
|
840
|
+
"description": "An asymmetric tile grid primitive with full/half span variants, per-tile aspect ratios, and contain/cover media fit modes. Designed for portfolio, case-study, and showcase layouts.",
|
|
841
|
+
"category": "data-display",
|
|
842
|
+
"dependencies": [
|
|
843
|
+
"@loworbitstudio/visor-core"
|
|
844
|
+
],
|
|
845
|
+
"registryDependencies": [
|
|
846
|
+
"utils"
|
|
847
|
+
],
|
|
848
|
+
"files": [
|
|
849
|
+
{
|
|
850
|
+
"path": "components/ui/bento-grid/bento-grid.tsx",
|
|
851
|
+
"type": "registry:ui",
|
|
852
|
+
"content": "import * as React from \"react\"\nimport { cn } from \"../../../lib/utils\"\nimport styles from \"./bento-grid.module.css\"\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface ResponsiveValue<T> {\n base: T\n sm?: T\n md?: T\n lg?: T\n xl?: T\n}\n\nexport type BentoSpan = \"full\" | \"half\" | number\n\nexport type BentoAspect = \"21/9\" | \"2/1\" | \"4/3\" | \"1/1\" | (string & {})\n\nexport type BentoFit = \"cover\" | \"contain\"\n\nexport type BentoLayout = \"stacked\" | \"overlay\"\n\n// ---------------------------------------------------------------------------\n// BentoGrid\n// ---------------------------------------------------------------------------\n\nexport interface BentoGridProps extends React.HTMLAttributes<HTMLDivElement> {\n /**\n * Number of columns. Accepts a plain number or a responsive breakpoint map.\n * Defaults to 2 columns.\n */\n cols?: number | ResponsiveValue<number>\n /**\n * Gap between tiles as a spacing token suffix (e.g. \"md\" → `--spacing-md`).\n * Defaults to \"4\" which resolves to `--spacing-4`.\n */\n gap?: string\n /**\n * When true, tiles fade + rise from 24px on viewport entry. Children are\n * staggered by their DOM order — left-to-right on a row, row-by-row down\n * the grid. Respects `prefers-reduced-motion`. Defaults to false.\n */\n reveal?: boolean\n /**\n * Per-tile reveal delay in milliseconds. Each tile's actual delay is\n * `revealStepMs × index`. Defaults to 110ms.\n */\n revealStepMs?: number\n /**\n * IntersectionObserver threshold for the entrance trigger. Defaults to 0.2.\n */\n revealThreshold?: number\n}\n\nconst BentoGrid = React.forwardRef<HTMLDivElement, BentoGridProps>(\n (\n {\n className,\n cols = 2,\n gap = \"4\",\n style,\n reveal = false,\n revealStepMs = 110,\n revealThreshold = 0.2,\n children,\n ...props\n },\n forwardedRef\n ) => {\n const innerRef = React.useRef<HTMLDivElement | null>(null)\n const [inView, setInView] = React.useState(false)\n\n const setRef = React.useCallback(\n (node: HTMLDivElement | null) => {\n innerRef.current = node\n if (typeof forwardedRef === \"function\") {\n forwardedRef(node)\n } else if (forwardedRef) {\n forwardedRef.current = node\n }\n },\n [forwardedRef]\n )\n\n React.useEffect(() => {\n if (!reveal) return\n const element = innerRef.current\n if (!element || typeof IntersectionObserver === \"undefined\") return\n\n const observer = new IntersectionObserver(\n ([entry]) => {\n if (entry.isIntersecting) {\n setInView(true)\n observer.disconnect()\n }\n },\n { threshold: revealThreshold }\n )\n observer.observe(element)\n return () => observer.disconnect()\n }, [reveal, revealThreshold])\n\n const colCount = typeof cols === \"number\" ? cols : cols.base\n const smCols = typeof cols === \"object\" ? cols.sm : undefined\n const mdCols = typeof cols === \"object\" ? cols.md : undefined\n const lgCols = typeof cols === \"object\" ? cols.lg : undefined\n const xlCols = typeof cols === \"object\" ? cols.xl : undefined\n\n const cssVars: React.CSSProperties & Record<string, string | number> = {\n \"--bento-cols\": colCount,\n \"--bento-gap\": `var(--spacing-${gap}, 1rem)`,\n ...(smCols !== undefined ? { \"--bento-cols-sm\": smCols } : {}),\n ...(mdCols !== undefined ? { \"--bento-cols-md\": mdCols } : {}),\n ...(lgCols !== undefined ? { \"--bento-cols-lg\": lgCols } : {}),\n ...(xlCols !== undefined ? { \"--bento-cols-xl\": xlCols } : {}),\n ...(reveal ? { \"--bento-reveal-step\": `${revealStepMs}ms` } : {}),\n ...style,\n }\n\n // When reveal is on, walk children and inject a per-tile --reveal-index so\n // the cascade stagger reads in DOM order.\n const decoratedChildren = reveal\n ? React.Children.map(children, (child, idx) => {\n if (!React.isValidElement(child)) return child\n const childProps = child.props as React.HTMLAttributes<HTMLElement>\n return React.cloneElement(child, {\n style: {\n ...(childProps.style ?? {}),\n \"--reveal-index\": idx,\n },\n } as Partial<React.HTMLAttributes<HTMLElement>>)\n })\n : children\n\n return (\n <div\n ref={setRef}\n data-slot=\"bento-grid\"\n data-reveal={reveal ? \"true\" : undefined}\n data-revealed={reveal && inView ? \"true\" : undefined}\n className={cn(styles.bentoGrid, className)}\n style={cssVars}\n {...props}\n >\n {decoratedChildren}\n </div>\n )\n }\n)\nBentoGrid.displayName = \"BentoGrid\"\n\n// ---------------------------------------------------------------------------\n// BentoTile\n// ---------------------------------------------------------------------------\n\nexport interface BentoTileProps extends React.HTMLAttributes<HTMLElement> {\n /**\n * Visual layout of the tile.\n * - \"stacked\" (default): media renders on top with its own aspect ratio,\n * body is a sibling block below in document flow. Tile height = media + body.\n * - \"overlay\": the tile carries the aspect ratio, media fills it absolutely,\n * body floats over the lower portion.\n * @default \"stacked\"\n */\n layout?: BentoLayout\n /**\n * Column span: \"full\" (1/-1), \"half\" (1 col), or a numeric span count.\n * Defaults to \"half\".\n */\n span?: BentoSpan\n /**\n * Aspect ratio for the tile (e.g. \"21/9\", \"2/1\", \"4/3\", \"1/1\").\n * Applied to the media in \"stacked\" mode, and to the tile root in \"overlay\" mode.\n */\n aspect?: BentoAspect\n /**\n * Media fit mode. \"cover\" fills the tile; \"contain\" fits media inside\n * a surface-card background plate.\n * Defaults to \"cover\".\n */\n fit?: BentoFit\n /**\n * When provided, renders the tile root as an `<a>` element.\n */\n href?: string\n /**\n * Link target (e.g. \"_blank\"). Only relevant when `href` is set.\n */\n target?: string\n /**\n * Link rel attribute. Only relevant when `href` is set.\n */\n rel?: string\n}\n\nconst BentoTile = React.forwardRef<HTMLElement, BentoTileProps>(\n (\n {\n className,\n layout = \"stacked\",\n span = \"half\",\n aspect,\n fit = \"cover\",\n href,\n target,\n rel,\n style,\n children,\n ...props\n },\n ref\n ) => {\n const spanStyle: React.CSSProperties & Record<string, string | number> = {\n ...(aspect ? { \"--bento-tile-aspect\": aspect.replace(\"/\", \" / \") } : {}),\n ...style,\n }\n\n const spanClass = cn(\n styles.bentoTile,\n layout === \"stacked\" ? styles.bentoTileStacked : styles.bentoTileOverlay,\n span === \"full\" && styles.bentoTileFull,\n span === \"half\" && styles.bentoTileHalf,\n typeof span === \"number\" && styles.bentoTileNumeric,\n fit === \"contain\" && styles.bentoTileContain,\n className\n )\n\n const spanDataAttr =\n typeof span === \"number\" ? { \"--bento-tile-span\": span } : {}\n\n const combinedStyle = { ...spanStyle, ...spanDataAttr }\n\n if (href) {\n return (\n <a\n ref={ref as React.Ref<HTMLAnchorElement>}\n data-slot=\"bento-tile\"\n data-span={span}\n data-fit={fit}\n data-layout={layout}\n href={href}\n target={target}\n rel={rel ?? (target === \"_blank\" ? \"noopener noreferrer\" : undefined)}\n className={spanClass}\n style={combinedStyle}\n {...(props as React.AnchorHTMLAttributes<HTMLAnchorElement>)}\n >\n {children}\n </a>\n )\n }\n\n return (\n <article\n ref={ref as React.Ref<HTMLElement>}\n data-slot=\"bento-tile\"\n data-span={span}\n data-fit={fit}\n data-layout={layout}\n className={spanClass}\n style={combinedStyle}\n {...props}\n >\n {children}\n </article>\n )\n }\n)\nBentoTile.displayName = \"BentoTile\"\n\n// ---------------------------------------------------------------------------\n// BentoTileMedia\n// ---------------------------------------------------------------------------\n\nexport interface BentoTileMediaProps\n extends React.ImgHTMLAttributes<HTMLImageElement> {\n src: string\n alt: string\n loading?: \"lazy\" | \"eager\"\n}\n\nconst BentoTileMedia = React.forwardRef<HTMLImageElement, BentoTileMediaProps>(\n ({ className, loading = \"lazy\", alt, ...props }, ref) => {\n return (\n <img\n ref={ref}\n data-slot=\"bento-tile-media\"\n className={cn(styles.bentoTileMedia, className)}\n loading={loading}\n alt={alt}\n {...props}\n />\n )\n }\n)\nBentoTileMedia.displayName = \"BentoTileMedia\"\n\n// ---------------------------------------------------------------------------\n// BentoTileBody\n// ---------------------------------------------------------------------------\n\nexport type BentoTileBodyProps = React.HTMLAttributes<HTMLDivElement>\n\nconst BentoTileBody = React.forwardRef<HTMLDivElement, BentoTileBodyProps>(\n ({ className, ...props }, ref) => {\n return (\n <div\n ref={ref}\n data-slot=\"bento-tile-body\"\n className={cn(styles.bentoTileBody, className)}\n {...props}\n />\n )\n }\n)\nBentoTileBody.displayName = \"BentoTileBody\"\n\n// ---------------------------------------------------------------------------\n// BentoTileMeta — eyebrow row (e.g. \"WEB · LOW ORBIT · 2026\")\n// ---------------------------------------------------------------------------\n\nexport interface BentoTileMetaProps extends React.HTMLAttributes<HTMLDivElement> {\n /**\n * Shorthand: provide an array of strings, each rendered as a span with a\n * `·` separator between siblings. When omitted, children are used as-is.\n */\n items?: React.ReactNode[]\n}\n\nconst BentoTileMeta = React.forwardRef<HTMLDivElement, BentoTileMetaProps>(\n ({ className, items, children, ...props }, ref) => {\n const content = items\n ? items.map((item, idx) => <span key={idx}>{item}</span>)\n : children\n return (\n <div\n ref={ref}\n data-slot=\"bento-tile-meta\"\n className={cn(styles.bentoTileMeta, className)}\n {...props}\n >\n {content}\n </div>\n )\n }\n)\nBentoTileMeta.displayName = \"BentoTileMeta\"\n\n// ---------------------------------------------------------------------------\n// BentoTileTitle — large display heading with optional hover arrow\n// ---------------------------------------------------------------------------\n\nexport interface BentoTileTitleProps\n extends React.HTMLAttributes<HTMLHeadingElement> {\n /** Heading level. @default \"h3\" */\n as?: \"h2\" | \"h3\" | \"h4\"\n /**\n * Render a ↗ arrow after the title that nudges on tile hover. Useful when\n * the tile is a link.\n * @default false\n */\n showArrow?: boolean\n}\n\nconst BentoTileTitle = React.forwardRef<HTMLHeadingElement, BentoTileTitleProps>(\n ({ className, as: Tag = \"h3\", showArrow = false, children, ...props }, ref) => {\n return (\n <Tag\n ref={ref}\n data-slot=\"bento-tile-title\"\n className={cn(styles.bentoTileTitle, className)}\n {...props}\n >\n {children}\n {showArrow ? (\n <span className={styles.bentoTileTitleArrow} aria-hidden=\"true\">\n {\"↗\"}\n </span>\n ) : null}\n </Tag>\n )\n }\n)\nBentoTileTitle.displayName = \"BentoTileTitle\"\n\n// ---------------------------------------------------------------------------\n// BentoTileFigure — non-image figure slot (charts, large numbers, custom SVG)\n// ---------------------------------------------------------------------------\n//\n// Drop-in replacement for BentoTileMedia when the tile's hero element is not\n// a photographic image — typical use cases: data charts, large statistic\n// numbers, illustrated SVGs, or any composed JSX. Inherits the same hover\n// scale behavior as BentoTileMedia (suppressed in fit=\"contain\" mode) and the\n// same layout-mode positioning (relative in stacked, absolute in overlay).\n\nexport type BentoTileFigureProps = React.HTMLAttributes<HTMLDivElement>\n\nconst BentoTileFigure = React.forwardRef<HTMLDivElement, BentoTileFigureProps>(\n ({ className, ...props }, ref) => {\n return (\n <div\n ref={ref}\n data-slot=\"bento-tile-figure\"\n className={cn(styles.bentoTileMedia, styles.bentoTileFigure, className)}\n {...props}\n />\n )\n }\n)\nBentoTileFigure.displayName = \"BentoTileFigure\"\n\n// ---------------------------------------------------------------------------\n// BentoTileHeadline — large display heading for headline-only tiles\n// ---------------------------------------------------------------------------\n//\n// Use BentoTileHeadline (not BentoTileTitle) when the tile's primary content\n// is the headline itself — manifesto pages, big-idea moments, statement\n// tiles. Scales from 2rem → 3.5rem and pairs naturally with BentoTileMeta or\n// BentoTileDescription as supporting copy.\n\nexport interface BentoTileHeadlineProps\n extends React.HTMLAttributes<HTMLHeadingElement> {\n /** Heading level. @default \"h2\" */\n as?: \"h1\" | \"h2\" | \"h3\"\n}\n\nconst BentoTileHeadline = React.forwardRef<\n HTMLHeadingElement,\n BentoTileHeadlineProps\n>(({ className, as: Tag = \"h2\", ...props }, ref) => {\n return (\n <Tag\n ref={ref}\n data-slot=\"bento-tile-headline\"\n className={cn(styles.bentoTileHeadline, className)}\n {...props}\n />\n )\n})\nBentoTileHeadline.displayName = \"BentoTileHeadline\"\n\n// ---------------------------------------------------------------------------\n// BentoTileDescription — muted body text under the title\n// ---------------------------------------------------------------------------\n//\n// Renders as <div> rather than <p> so MDX-authored children (which MDX\n// auto-wraps in <p>) don't create invalid <p>-in-<p> nesting and hydration\n// errors. The semantic loss is minor — the body content is still readable\n// prose, just hosted in a styled container.\n\nexport type BentoTileDescriptionProps =\n React.HTMLAttributes<HTMLDivElement>\n\nconst BentoTileDescription = React.forwardRef<\n HTMLDivElement,\n BentoTileDescriptionProps\n>(({ className, ...props }, ref) => {\n return (\n <div\n ref={ref}\n data-slot=\"bento-tile-description\"\n className={cn(styles.bentoTileDescription, className)}\n {...props}\n />\n )\n})\nBentoTileDescription.displayName = \"BentoTileDescription\"\n\n// ---------------------------------------------------------------------------\n// Exports\n// ---------------------------------------------------------------------------\n\nexport {\n BentoGrid,\n BentoTile,\n BentoTileMedia,\n BentoTileFigure,\n BentoTileBody,\n BentoTileMeta,\n BentoTileTitle,\n BentoTileHeadline,\n BentoTileDescription,\n}\n"
|
|
853
|
+
},
|
|
854
|
+
{
|
|
855
|
+
"path": "components/ui/bento-grid/bento-grid.module.css",
|
|
856
|
+
"type": "registry:ui",
|
|
857
|
+
"content": "/* ============================================================\n BentoGrid — asymmetric tile grid layout component\n ============================================================ */\n\n/* ---------------------------------------------------------------------------\n Grid container\n --------------------------------------------------------------------------- */\n\n.bentoGrid {\n display: grid;\n grid-template-columns: repeat(var(--bento-cols, 2), 1fr);\n gap: var(--bento-gap, var(--spacing-4, 1rem));\n /* Tiles hug their content vertically — they do NOT stretch to share row\n height. Matches the strata-v1 prototype's work-grid alignment. */\n align-items: start;\n}\n\n/* Responsive breakpoints — only apply when the CSS variable is set */\n@media (min-width: 640px) {\n .bentoGrid {\n grid-template-columns: repeat(\n var(--bento-cols-sm, var(--bento-cols, 2)),\n 1fr\n );\n }\n}\n\n@media (min-width: 768px) {\n .bentoGrid {\n grid-template-columns: repeat(\n var(--bento-cols-md, var(--bento-cols-sm, var(--bento-cols, 2))),\n 1fr\n );\n }\n}\n\n@media (min-width: 1024px) {\n .bentoGrid {\n grid-template-columns: repeat(\n var(--bento-cols-lg, var(--bento-cols-md, var(--bento-cols-sm, var(--bento-cols, 2)))),\n 1fr\n );\n }\n}\n\n@media (min-width: 1280px) {\n .bentoGrid {\n grid-template-columns: repeat(\n var(--bento-cols-xl, var(--bento-cols-lg, var(--bento-cols-md, var(--bento-cols-sm, var(--bento-cols, 2))))),\n 1fr\n );\n }\n}\n\n/* ---------------------------------------------------------------------------\n Tile base\n --------------------------------------------------------------------------- */\n\n.bentoTile {\n /* Customization slots — defaults match the strata-v1 marketing prototype.\n Override per-grid via a parent token-override scope or per-tile via style. */\n --bento-tile-body-padding-y: var(--spacing-3, 0.75rem); /* 12px — tight vertical rhythm */\n --bento-tile-body-padding-x: clamp(var(--spacing-3, 0.75rem), 1.6vw, var(--spacing-5, 1.25rem));\n --bento-tile-body-gap: var(--spacing-2, 0.5rem); /* 8px between meta / title / description */\n --bento-tile-radius: var(--radius-lg, 0.75rem);\n --bento-tile-media-scale-hover: 1.04;\n /* Default is no lift — hover affordance is the media scale + shadow bump.\n Consumers can opt in by overriding this slot (for example -3px). */\n --bento-tile-lift-hover: 0px;\n\n position: relative;\n display: flex;\n flex-direction: column;\n overflow: hidden;\n isolation: isolate;\n border-radius: var(--bento-tile-radius);\n background-color: var(--surface-card, #ffffff);\n color: var(--text-primary, #111827);\n box-shadow: var(--shadow-sm);\n /* Aspect ratio lives on the layout modifier — overlay applies it to the tile root, stacked applies it to the media. */\n transition:\n box-shadow var(--motion-duration-normal, 200ms) var(--motion-easing-default, ease),\n transform var(--motion-duration-normal, 200ms) var(--motion-easing-default, ease);\n}\n\n/* Prose reset — when the tile renders inside a fumadocs prose (or any\n Tailwind-Typography) container, default img/p/h3 margins of ~2em swamp\n the tile's own spacing. Reset every child element we render to margin: 0;\n the tile's own padding + gap tokens become the only source of spacing. */\n.bentoTile :where(img),\n.bentoTile :where(p),\n.bentoTile :where(h2),\n.bentoTile :where(h3),\n.bentoTile :where(h4),\n.bentoTile :where(ul),\n.bentoTile :where(ol) {\n margin: 0;\n}\n\n/* Stacked (default): media on top with its own aspect ratio, body as a sibling block below. Total tile height = media height + body height. */\n.bentoTileStacked {\n /* No aspect-ratio on the tile root — height comes from media + body. */\n}\n\n/* Overlay: the tile root carries the aspect ratio. Media fills it absolutely; body floats over the lower portion. */\n.bentoTileOverlay {\n aspect-ratio: var(--bento-tile-aspect, auto);\n}\n\n/* Clickable tiles — anchor root */\na.bentoTile {\n text-decoration: none;\n cursor: pointer;\n}\n\n/* The hover affordance is the image scale (on .bentoTileMedia) and the\n box-shadow bump here. Tile lift is opt-in via the --bento-tile-lift-hover\n slot, default 0 — text and eyebrow do NOT shift by default. */\na.bentoTile:hover {\n box-shadow: var(--shadow-md);\n transform: translateY(var(--bento-tile-lift-hover, 0px));\n}\n\na.bentoTile:focus-visible {\n outline: var(--focus-ring-width, 2px) solid var(--border-focus, #6366f1);\n outline-offset: var(--focus-ring-offset, 2px);\n}\n\n/* ---------------------------------------------------------------------------\n Span variants\n --------------------------------------------------------------------------- */\n\n.bentoTileFull {\n grid-column: 1 / -1;\n}\n\n.bentoTileHalf {\n grid-column: span 1;\n}\n\n.bentoTileNumeric {\n grid-column: span var(--bento-tile-span, 1);\n}\n\n/* Collapse all spans to single column on narrow viewports */\n@media (max-width: 639px) {\n .bentoTileFull,\n .bentoTileHalf,\n .bentoTileNumeric {\n grid-column: 1 / -1;\n }\n}\n\n/* ---------------------------------------------------------------------------\n Fit variants\n --------------------------------------------------------------------------- */\n\n.bentoTileContain {\n background-color: var(--surface-card, #ffffff);\n}\n\n/* ---------------------------------------------------------------------------\n Media — layout-scoped positioning + Ken-Burns-style hover zoom\n --------------------------------------------------------------------------- */\n\n.bentoTileMedia {\n display: block; /* Drop inline baseline space — avoids a hairline gap above/below the image. */\n width: 100%;\n object-fit: cover; /* default; overridden by contain modifier below */\n /* Slow ease-out zoom on tile hover — applied here so any tile (anchor or article) gets the effect. */\n transition: transform var(--motion-duration-slow, 600ms)\n var(--motion-easing-enter, cubic-bezier(0.19, 1, 0.22, 1));\n transform-origin: center center;\n}\n\n.bentoTile:hover .bentoTileMedia {\n transform: scale(var(--bento-tile-media-scale-hover));\n}\n\n/* Stacked: media is a normal block sized by its own aspect ratio. */\n.bentoTileStacked .bentoTileMedia {\n position: relative;\n aspect-ratio: var(--bento-tile-aspect, 16 / 10);\n height: auto;\n}\n\n/* Overlay: media absolutely fills the tile (which carries the aspect ratio). */\n.bentoTileOverlay .bentoTileMedia {\n position: absolute;\n inset: 0;\n height: 100%;\n}\n\n/* Contain mode in stacked layout — the media slot drops its aspect-ratio\n enforcement so the image's natural dimensions dictate the slot height.\n No letterboxing; the body sits directly under the image. */\n.bentoTileStacked.bentoTileContain .bentoTileMedia {\n aspect-ratio: auto;\n height: auto;\n object-fit: initial;\n}\n\n/* Contain in overlay mode keeps the tile's aspect ratio (the tile is the\n container), so we still need object-fit: contain to letterbox inside. */\n.bentoTileOverlay.bentoTileContain .bentoTileMedia {\n object-fit: contain;\n}\n\n/* ---------------------------------------------------------------------------\n Tile body — layout-scoped positioning\n --------------------------------------------------------------------------- */\n\n.bentoTileBody {\n display: grid;\n gap: var(--bento-tile-body-gap);\n padding-block: var(--bento-tile-body-padding-y);\n padding-inline: var(--bento-tile-body-padding-x);\n align-content: start;\n}\n\n/* Stacked: body sits below the media in normal flow at its natural height —\n no flex-grow, so the tile hugs its content. */\n.bentoTileStacked .bentoTileBody {\n position: static;\n}\n\n/* Overlay: body floats on top, pushed to bottom of the tile. A subtle\n bottom-anchored gradient keeps text legible against any image. */\n.bentoTileOverlay .bentoTileBody {\n position: relative;\n z-index: 1;\n margin-top: auto;\n background: linear-gradient(\n to top,\n var(--overlay-bg) 0%,\n var(--overlay-bg) 60%,\n transparent 100%\n );\n}\n\n/* ---------------------------------------------------------------------------\n Body content typography: meta (eyebrow), title, description\n --------------------------------------------------------------------------- */\n\n.bentoTileMeta {\n display: flex;\n flex-wrap: wrap;\n gap: var(--spacing-3, 0.75rem);\n font-size: 0.6875rem; /* 11px — intentional tracking-display size */\n font-weight: var(--font-weight-medium, 500);\n /* Tight line-height so when items wrap to a second line, the wrapped row sits close to the first instead of leaving a body-text-sized gap. */\n line-height: var(--line-height-tight, 1.25);\n letter-spacing: 0.18em;\n text-transform: uppercase;\n color: var(--text-tertiary, #6b7280);\n font-variant-numeric: tabular-nums;\n /* When wrapping, give each row a small vertical breathing space. */\n row-gap: var(--spacing-1, 0.25rem);\n}\n\n.bentoTileMeta > * + *::before {\n content: \"·\";\n margin-right: var(--spacing-3, 0.75rem);\n margin-left: calc(var(--spacing-2, 0.5rem) * -1);\n color: var(--text-tertiary, #6b7280);\n opacity: 0.6;\n}\n\n.bentoTileTitle {\n margin: 0;\n font-size: clamp(1.125rem, 1.6vw, 1.5rem);\n font-weight: var(--font-weight-bold, 700);\n line-height: 1.1;\n letter-spacing: -0.02em;\n color: var(--text-primary, #111827);\n display: inline-flex;\n align-items: center;\n gap: var(--spacing-2, 0.5rem);\n}\n\n/* Headline tile variant — for tiles whose primary content IS the headline (manifesto, big-idea moment, statement). Bigger than .bentoTileTitle. */\n.bentoTileHeadline {\n margin: 0;\n font-size: clamp(2rem, 3.6vw, 3.5rem);\n font-weight: var(--font-weight-bold, 700);\n line-height: 1;\n letter-spacing: -0.03em;\n color: var(--text-primary, #111827);\n}\n\n/* Figure tile slot — non-image hero (chart, large number, custom SVG). Inherits .bentoTileMedia's hover-scale + layout-mode positioning; adds a flex centering pass so any child sits centered in the slot by default. */\n.bentoTileFigure {\n display: flex;\n align-items: center;\n justify-content: center;\n background-color: var(--surface-subtle, var(--surface-card, #ffffff));\n color: var(--text-primary, #111827);\n}\n\n.bentoTileTitleArrow {\n opacity: 0.45;\n transition:\n opacity var(--motion-duration-normal, 200ms) var(--motion-easing-default, ease),\n transform var(--motion-duration-normal, 200ms) var(--motion-easing-default, ease);\n}\n\n.bentoTile:hover .bentoTileTitleArrow {\n opacity: 1;\n transform: translate(4px, -4px);\n}\n\n.bentoTileDescription {\n margin: 0;\n color: var(--text-secondary, #6b7280);\n /* One notch down from the previous 0.9375rem — sits on the visor `sm` token. */\n font-size: var(--font-size-sm, 0.875rem);\n font-weight: var(--font-weight-normal, 400);\n line-height: 1.5;\n max-width: 56ch;\n}\n\n/* ---------------------------------------------------------------------------\n Reveal cascade — fade + 24px rise on viewport entry, staggered by DOM order.\n --------------------------------------------------------------------------- */\n\n.bentoGrid[data-reveal] > .bentoTile {\n opacity: 0;\n transform: translate3d(0, 24px, 0);\n transition:\n opacity var(--motion-duration-slow, 800ms) var(--motion-easing-enter, cubic-bezier(0.19, 1, 0.22, 1)),\n transform var(--motion-duration-slow, 800ms) var(--motion-easing-enter, cubic-bezier(0.19, 1, 0.22, 1));\n transition-delay: calc(var(--reveal-index, 0) * var(--bento-reveal-step, 110ms));\n}\n\n.bentoGrid[data-reveal][data-revealed] > .bentoTile {\n opacity: 1;\n transform: translate3d(0, 0, 0);\n}\n\n/* ---------------------------------------------------------------------------\n Reduced motion — disable hover zoom, arrow nudge, and reveal cascade.\n --------------------------------------------------------------------------- */\n\n@media (prefers-reduced-motion: reduce) {\n .bentoTileMedia,\n .bentoTileTitleArrow,\n .bentoTile,\n a.bentoTile {\n transition: none;\n }\n\n .bentoTile:hover .bentoTileMedia,\n .bentoTile:hover .bentoTileTitleArrow,\n a.bentoTile:hover {\n transform: none;\n }\n\n .bentoGrid[data-reveal] > .bentoTile {\n opacity: 1;\n transform: none;\n transition: none;\n }\n}\n"
|
|
858
|
+
},
|
|
859
|
+
{
|
|
860
|
+
"path": "components/ui/bento-grid/bento-grid.module.css.d.ts",
|
|
861
|
+
"type": "registry:ui",
|
|
862
|
+
"content": "declare const styles: {\n readonly bentoGrid: string\n readonly bentoTile: string\n readonly bentoTileStacked: string\n readonly bentoTileOverlay: string\n readonly bentoTileFull: string\n readonly bentoTileHalf: string\n readonly bentoTileNumeric: string\n readonly bentoTileContain: string\n readonly bentoTileMedia: string\n readonly bentoTileFigure: string\n readonly bentoTileBody: string\n readonly bentoTileMeta: string\n readonly bentoTileTitle: string\n readonly bentoTileTitleArrow: string\n readonly bentoTileHeadline: string\n readonly bentoTileDescription: string\n}\n\nexport default styles\n"
|
|
863
|
+
},
|
|
864
|
+
{
|
|
865
|
+
"path": "components/ui/bento-grid/bento-grid.visor.yaml",
|
|
866
|
+
"type": "registry:ui",
|
|
867
|
+
"content": "name: BentoGrid\ndescription: An asymmetric tile grid primitive with full/half span variants, per-tile aspect ratios, and contain/cover media fit modes. Designed for portfolio, case-study, and showcase layouts.\nextends: HTMLDivElement\ncategory: data-display\n\nwhen_to_use:\n - Asymmetric portfolio or case-study grids where tiles have different visual weights\n - Hero-style layouts mixing full-width feature tiles with paired half-width tiles\n - Showcase grids with varied aspect ratios (21:9, 2:1, 4:3) and image fit modes\n - Marketing pages using Apple-style \"bento\" layouts\n - Any grid where tiles intentionally have different sizes or spans\n\nwhen_not_to_use:\n - Symmetric uniform grids where all tiles are the same size (use card-grid instead)\n - Simple two-column content layouts (use CSS grid or flexbox directly)\n - List-style data displays (use Table or DataTable)\n - Pinterest-style masonry layouts where tile heights vary based on content\n\nwhy: >\n BentoGrid fills the gap between card-grid (uniform tile sizes) and fully custom CSS grids.\n It provides a structured API for asymmetric layouts while keeping consumer code readable —\n span=\"full\" for hero tiles, span=\"half\" for paired tiles, aspect per tile for mixed ratios.\n All tiles collapse to single-column on narrow viewports automatically. The compound primitive\n pattern (Grid + Tile + TileMedia + TileBody) mirrors the Card pattern for consistency.\n\nprops:\n - name: cols\n type: \"number | { base: number; sm?: number; md?: number; lg?: number; xl?: number }\"\n default: \"2\"\n description: \"Number of grid columns. Accepts a plain number or a responsive breakpoint map.\"\n - name: gap\n type: string\n default: '\"4\"'\n description: \"Gap between tiles as a spacing token suffix. E.g. '4' resolves to var(--spacing-4).\"\n - name: reveal\n type: boolean\n default: 'false'\n description: \"Fade + rise each tile on viewport entry, staggered by DOM order. Suppressed under prefers-reduced-motion.\"\n - name: revealStepMs\n type: number\n default: '110'\n description: \"Per-tile delay in milliseconds. Each tile's actual delay is revealStepMs × index.\"\n - name: revealThreshold\n type: number\n default: '0.2'\n description: \"IntersectionObserver threshold for the entrance trigger.\"\n\nsub_components:\n - name: BentoTile\n description: Individual grid tile. Renders as <article> by default, or <a> when href is provided.\n props:\n - name: layout\n type: '\"stacked\" | \"overlay\"'\n default: '\"stacked\"'\n description: \"Visual layout. stacked (default): media on top with its own aspect ratio, body below as a sibling. overlay: media fills the tile (which carries the aspect ratio), body floats over the lower portion.\"\n - name: span\n type: '\"full\" | \"half\" | number'\n default: '\"half\"'\n description: \"Column span: full (1/-1), half (1 col), or a numeric span count.\"\n - name: aspect\n type: '\"21/9\" | \"2/1\" | \"4/3\" | \"1/1\" | string'\n description: \"Aspect ratio. In stacked mode it shapes the media; in overlay mode it shapes the entire tile root.\"\n - name: fit\n type: '\"cover\" | \"contain\"'\n default: '\"cover\"'\n description: \"Media fit mode. contain adds a surface-card background plate for logos/portraits.\"\n - name: href\n type: string\n description: \"When provided, renders the tile root as an anchor element.\"\n - name: target\n type: string\n description: \"Link target (e.g. _blank). Only relevant when href is set.\"\n - name: rel\n type: string\n description: \"Link rel attribute. Defaults to 'noopener noreferrer' when target is _blank.\"\n - name: BentoTileMedia\n description: Image element. Zooms 1.04× on tile hover (suppressed in fit=\"contain\" mode).\n props:\n - name: src\n type: string\n required: true\n description: \"Image source URL.\"\n - name: alt\n type: string\n required: true\n description: \"Image alt text for accessibility.\"\n - name: loading\n type: '\"lazy\" | \"eager\"'\n default: '\"lazy\"'\n description: \"Native image loading strategy.\"\n - name: BentoTileBody\n description: Body container. Stretches to fill the tile in stacked mode so tiles in the same row share height.\n - name: BentoTileMeta\n description: Eyebrow row — uppercase, tracked, with · separators between items. Accepts an `items` array shorthand or arbitrary children.\n props:\n - name: items\n type: ReactNode[]\n description: \"Array of items rendered as spans with `·` separators between siblings. When omitted, children are used as-is.\"\n - name: BentoTileTitle\n description: Display heading with optional hover arrow that nudges on tile hover.\n props:\n - name: as\n type: '\"h2\" | \"h3\" | \"h4\"'\n default: '\"h3\"'\n description: \"Heading element.\"\n - name: showArrow\n type: boolean\n default: 'false'\n description: \"Render a ↗ arrow after the title; nudges on tile hover.\"\n - name: BentoTileDescription\n description: Muted body text with max-width 56ch for comfortable reading. Renders as <div> (not <p>) so MDX-authored multi-line children don't create invalid <p>-in-<p> nesting.\n - name: BentoTileFigure\n description: Non-image hero slot — drop-in replacement for BentoTileMedia when the tile's hero is a chart, large number, or custom JSX. Inherits the same hover scale + layout-mode positioning as the media slot.\n - name: BentoTileHeadline\n description: Larger display heading (clamp 2rem → 3.5rem) for tiles where the heading is the primary content (manifesto / statement / headline tiles).\n props:\n - name: as\n type: '\"h1\" | \"h2\" | \"h3\"'\n default: '\"h2\"'\n description: \"Heading element.\"\n\ncustomization:\n description: >\n Each tile (.bentoTile) exposes the following CSS custom properties.\n Override per-tile via style, or per-grid via a parent token-override scope.\n properties:\n - name: --bento-tile-radius\n default: var(--radius-lg)\n description: Tile corner radius.\n - name: --bento-tile-body-padding\n default: clamp(1rem, 2vw, 1.75rem)\n description: Body padding (responsive clamp).\n - name: --bento-tile-body-gap\n default: var(--spacing-3, 0.75rem)\n description: Gap between meta / title / description.\n - name: --bento-tile-media-scale-hover\n default: '1.04'\n description: Image scale on tile hover. Set to 1 to disable.\n - name: --bento-tile-lift-hover\n default: '0px'\n description: Optional vertical translate on anchor-tile hover. Default is 0 — the canonical hover affordance is the image scale and box-shadow bump. Consumers can opt in to a tile lift by overriding this slot (e.g. -3px).\n\ndependencies:\n - \"@loworbitstudio/visor-core\"\n\npreview_url: \"https://visor.design/docs/components/data-display/bento-grid\"\n\nexample: |\n <BentoGrid cols={{ base: 1, md: 2 }} gap=\"4\">\n <BentoTile span=\"full\" aspect=\"21/9\">\n <BentoTileMedia src=\"/projects/knowmentum.jpg\" alt=\"Knowmentum project hero\" />\n <BentoTileBody>\n <h3>Knowmentum</h3>\n <p>Knowledge platform for high-performers.</p>\n </BentoTileBody>\n </BentoTile>\n\n <BentoTile span=\"half\" aspect=\"2/1\" href=\"https://animal.nyc/\" target=\"_blank\">\n <BentoTileMedia src=\"/projects/animal-nyc.jpg\" alt=\"Animal NYC\" />\n <BentoTileBody>\n <h3>Animal NYC</h3>\n </BentoTileBody>\n </BentoTile>\n\n <BentoTile span=\"half\" aspect=\"2/1\" href=\"https://animal.la/\" target=\"_blank\">\n <BentoTileMedia src=\"/projects/animal-la.jpg\" alt=\"Animal LA\" />\n <BentoTileBody>\n <h3>Animal LA</h3>\n </BentoTileBody>\n </BentoTile>\n\n <BentoTile span=\"half\" aspect=\"4/3\" fit=\"contain\">\n <BentoTileMedia src=\"/logos/client-logo.png\" alt=\"Client logo\" />\n <BentoTileBody>\n <p>Natural-fit logo tile</p>\n </BentoTileBody>\n </BentoTile>\n </BentoGrid>\n"
|
|
868
|
+
}
|
|
869
|
+
]
|
|
870
|
+
},
|
|
811
871
|
{
|
|
812
872
|
"name": "banner",
|
|
813
873
|
"type": "registry:ui",
|
|
@@ -1377,6 +1437,30 @@
|
|
|
1377
1437
|
}
|
|
1378
1438
|
]
|
|
1379
1439
|
},
|
|
1440
|
+
{
|
|
1441
|
+
"name": "marquee",
|
|
1442
|
+
"type": "registry:ui",
|
|
1443
|
+
"description": "A multi-band counter-flow infinite-scroll primitive for continuous animated content strips. Pure CSS animation with pause-on-hover and prefers-reduced-motion support.",
|
|
1444
|
+
"category": "data-display",
|
|
1445
|
+
"dependencies": [
|
|
1446
|
+
"@loworbitstudio/visor-core"
|
|
1447
|
+
],
|
|
1448
|
+
"registryDependencies": [
|
|
1449
|
+
"utils"
|
|
1450
|
+
],
|
|
1451
|
+
"files": [
|
|
1452
|
+
{
|
|
1453
|
+
"path": "components/ui/marquee/marquee.tsx",
|
|
1454
|
+
"type": "registry:ui",
|
|
1455
|
+
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport { cn } from \"../../../lib/utils\"\nimport styles from \"./marquee.module.css\"\n\nexport interface MarqueeBand {\n /** Items to display in this band. Rendered twice internally for seamless loop. */\n items: React.ReactNode[]\n /** Scroll direction. @default \"left\" */\n direction?: \"left\" | \"right\"\n /** Duration of one full scroll cycle in seconds. @default 40 */\n durationSec?: number\n /** Separator rendered between items. String or ReactNode. */\n separator?: React.ReactNode | string\n}\n\nexport interface MarqueeProps extends React.HTMLAttributes<HTMLDivElement> {\n /**\n * Multi-band configuration. When provided, `items`, `durationSec`, and\n * `separator` at the top level are ignored.\n */\n bands?: MarqueeBand[]\n /**\n * Single-band shorthand: items to display.\n * Used when `bands` is not provided.\n */\n items?: React.ReactNode[]\n /** Duration of one full scroll cycle in seconds. @default 40 */\n durationSec?: number\n /** Separator rendered between items. String or ReactNode. @default undefined */\n separator?: React.ReactNode | string\n /**\n * When true, scrolling pauses on hover.\n * @default true\n */\n pauseOnHover?: boolean\n /** Gap between items. Accepts any CSS gap value. @default \"var(--spacing-6, 1.5rem)\" */\n gap?: React.CSSProperties[\"gap\"]\n /** Custom render function for each item. */\n renderItem?: (item: React.ReactNode, index: number) => React.ReactNode\n /** Custom render function for the separator. */\n renderSeparator?: (separator: React.ReactNode | string, index: number) => React.ReactNode\n}\n\n// ── Internal band renderer ──────────────────────────────────────────────────\n\ninterface BandProps {\n band: MarqueeBand\n pauseOnHover: boolean\n gap: React.CSSProperties[\"gap\"]\n renderItem?: MarqueeProps[\"renderItem\"]\n renderSeparator?: MarqueeProps[\"renderSeparator\"]\n bandIndex: number\n}\n\nfunction MarqueeBandRenderer({\n band,\n pauseOnHover,\n gap,\n renderItem,\n renderSeparator,\n bandIndex,\n}: BandProps) {\n const { items, direction = \"left\", durationSec = 40, separator } = band\n\n const animationDirection = direction === \"right\" ? \"reverse\" : \"normal\"\n\n const trackStyle: React.CSSProperties = {\n \"--marquee-duration\": `${durationSec}s`,\n \"--marquee-gap\": gap,\n animationDirection,\n } as React.CSSProperties\n\n function renderTrackItems(keyPrefix: string) {\n return items.map((item, idx) => {\n const itemNode = renderItem ? renderItem(item, idx) : item\n const hasSeparator = separator !== undefined && separator !== null && separator !== \"\"\n const separatorNode = hasSeparator\n ? renderSeparator\n ? renderSeparator(separator, idx)\n : <span className={styles.separator} aria-hidden=\"true\">{separator}</span>\n : null\n\n return (\n <React.Fragment key={`${keyPrefix}-${idx}`}>\n <span className={styles.item}>{itemNode}</span>\n {hasSeparator && separatorNode}\n </React.Fragment>\n )\n })\n }\n\n return (\n <div\n className={cn(styles.band, pauseOnHover && styles.pauseOnHover)}\n data-slot=\"marquee-band\"\n data-band-index={bandIndex}\n data-direction={direction}\n >\n {/* Track contains items twice for seamless loop — aria-hidden on the whole track */}\n <div\n className={styles.track}\n style={trackStyle}\n aria-hidden=\"true\"\n >\n {/* First copy */}\n <div className={styles.set}>\n {renderTrackItems(\"a\")}\n </div>\n {/* Second copy — creates the seamless loop illusion */}\n <div className={styles.set} aria-hidden=\"true\">\n {renderTrackItems(\"b\")}\n </div>\n </div>\n </div>\n )\n}\n\n// ── Marquee component ────────────────────────────────────────────────────────\n\n/**\n * Marquee — a multi-band counter-flow infinite-scroll primitive.\n *\n * Renders one or more bands of items in a continuous animated scroll loop.\n * Per-band direction allows counter-flow patterns (e.g., \"trusted by\" strips).\n *\n * Animation is pure CSS keyframes. Direction reverses via `animation-direction: reverse`.\n * Pause-on-hover is applied via `:hover → animation-play-state: paused`.\n * prefers-reduced-motion: animation is disabled instantly.\n */\nconst Marquee = React.forwardRef<HTMLDivElement, MarqueeProps>(\n (\n {\n className,\n bands,\n items,\n durationSec = 40,\n separator,\n pauseOnHover = true,\n gap = \"var(--spacing-6, 1.5rem)\",\n renderItem,\n renderSeparator,\n ...props\n },\n ref\n ) => {\n // Normalise to band array for unified rendering\n const resolvedBands: MarqueeBand[] = bands ?? [\n {\n items: items ?? [],\n direction: \"left\",\n durationSec,\n separator,\n },\n ]\n\n // When an aria-label is provided, add role=\"region\" so the landmark is valid.\n // A plain <div> with aria-label violates aria-prohibited-attr.\n const role = props[\"aria-label\"] || props[\"aria-labelledby\"] ? \"region\" : undefined\n\n return (\n <div\n ref={ref}\n data-slot=\"marquee\"\n role={role}\n className={cn(styles.root, className)}\n {...props}\n >\n {resolvedBands.map((band, idx) => (\n <MarqueeBandRenderer\n key={idx}\n band={band}\n pauseOnHover={pauseOnHover}\n gap={gap}\n renderItem={renderItem}\n renderSeparator={renderSeparator}\n bandIndex={idx}\n />\n ))}\n </div>\n )\n }\n)\nMarquee.displayName = \"Marquee\"\n\nexport { Marquee }\n"
|
|
1456
|
+
},
|
|
1457
|
+
{
|
|
1458
|
+
"path": "components/ui/marquee/marquee.module.css",
|
|
1459
|
+
"type": "registry:ui",
|
|
1460
|
+
"content": "/* Marquee: multi-band counter-flow infinite-scroll primitive\n *\n * Layout strategy:\n * Each band is a masked overflow container. Inside sits a `track` which\n * contains two identical `set` elements laid out side-by-side. The\n * animation moves the track left by 50% (= one set width), then loops.\n *\n * Animation strategy:\n * @keyframes marquee-scroll: translateX(0) → translateX(-50%)\n * Direction reverses (right→left) via animation-direction: reverse.\n * Duration and gap are injected as CSS custom properties per-band.\n * pause-on-hover: .pauseOnHover:hover .track { animation-play-state: paused }\n *\n * prefers-reduced-motion:\n * Animation is disabled — items appear statically with no motion.\n */\n\n/* ── Keyframe ────────────────────────────────────────────────────────────── */\n\n@keyframes marquee-scroll {\n from {\n transform: translateX(0);\n }\n to {\n transform: translateX(-50%);\n }\n}\n\n/* ── Root ────────────────────────────────────────────────────────────────── */\n\n.root {\n --marquee-gap: var(--spacing-6, 1.5rem);\n --marquee-duration: 40s;\n --marquee-fade-width: var(--spacing-12, 3rem);\n\n /* Item typography slots — override on the root to scale up for marketing\n surfaces. Defaults resolve to the body-text size used by ticker strips. */\n --marquee-item-color: var(--text-primary, #111827);\n --marquee-item-font-size: var(--font-size-sm, 0.875rem);\n --marquee-item-font-weight: var(--font-weight-medium, 500);\n --marquee-item-line-height: var(--line-height-normal, 1.5);\n --marquee-item-letter-spacing: normal;\n\n /* Separator slots */\n --marquee-separator-color: var(--text-tertiary, #9ca3af);\n --marquee-separator-font-size: var(--marquee-item-font-size);\n\n display: flex;\n flex-direction: column;\n gap: var(--spacing-4, 1rem);\n width: 100%;\n overflow: hidden;\n}\n\n/* ── Band ────────────────────────────────────────────────────────────────── */\n\n.band {\n position: relative;\n width: 100%;\n overflow: hidden;\n /* Fade edges for polished infinite-scroll effect */\n -webkit-mask-image: linear-gradient(\n to right,\n transparent 0,\n black var(--marquee-fade-width),\n black calc(100% - var(--marquee-fade-width)),\n transparent 100%\n );\n mask-image: linear-gradient(\n to right,\n transparent 0,\n black var(--marquee-fade-width),\n black calc(100% - var(--marquee-fade-width)),\n transparent 100%\n );\n}\n\n/* ── Track ────────────────────────────────────────────────────────────────── */\n\n/*\n * The track contains two `.set` elements (first copy + second copy) sitting\n * flush against each other. Each .set carries a trailing margin equal to the\n * inter-item gap, so the total track width is exactly 2 × (set + gap). The\n * translateX(-50%) wrap then lands at the exact start of the second set —\n * seamless. Putting a `gap` on .track itself would land the wrap mid-gap and\n * cause a visible jump every cycle.\n */\n.track {\n display: flex;\n flex-direction: row;\n align-items: center;\n width: max-content;\n animation: marquee-scroll var(--marquee-duration, 40s) var(--motion-easing-linear, linear) infinite;\n will-change: transform;\n}\n\n/* ── Item set ─────────────────────────────────────────────────────────────── */\n\n.set {\n display: flex;\n flex-direction: row;\n align-items: center;\n gap: var(--marquee-gap);\n flex-shrink: 0;\n /* Trailing gap that bridges this set's last item to the next set's first item — required for a seamless -50% wrap. See `.track` comment above. */\n margin-right: var(--marquee-gap);\n}\n\n/* ── Item ─────────────────────────────────────────────────────────────────── */\n\n.item {\n display: inline-flex;\n align-items: center;\n flex-shrink: 0;\n color: var(--marquee-item-color);\n font-size: var(--marquee-item-font-size);\n font-weight: var(--marquee-item-font-weight);\n letter-spacing: var(--marquee-item-letter-spacing);\n /* line-height: normal (not tight) so descenders (g, y, p, q, j) clear the band's overflow boundary at marketing-display font sizes */\n line-height: var(--marquee-item-line-height);\n white-space: nowrap;\n}\n\n/* ── Separator ─────────────────────────────────────────────────────────────── */\n\n.separator {\n display: inline-flex;\n align-items: center;\n flex-shrink: 0;\n color: var(--marquee-separator-color);\n font-size: var(--marquee-separator-font-size);\n line-height: var(--marquee-item-line-height);\n user-select: none;\n}\n\n/* ── Pause on hover ─────────────────────────────────────────────────────── */\n\n.pauseOnHover:hover .track {\n animation-play-state: paused;\n}\n\n/* ── Reduced motion ─────────────────────────────────────────────────────── */\n\n@media (prefers-reduced-motion: reduce) {\n .track {\n /* Disable animation entirely — items appear statically */\n animation: none;\n /* Show only the first set; overflow hidden on band clips the rest */\n transform: none;\n }\n\n /* Remove the fade mask when static to avoid clipping visible items */\n .band {\n -webkit-mask-image: none;\n mask-image: none;\n overflow: auto;\n }\n\n /* Wrap items for readability in static/accessible mode */\n .track {\n flex-wrap: wrap;\n }\n\n .set:last-child {\n /* Hide the duplicate set in reduced-motion mode */\n display: none;\n }\n}\n"
|
|
1461
|
+
}
|
|
1462
|
+
]
|
|
1463
|
+
},
|
|
1380
1464
|
{
|
|
1381
1465
|
"name": "fieldset",
|
|
1382
1466
|
"type": "registry:ui",
|
|
@@ -1490,6 +1574,30 @@
|
|
|
1490
1574
|
}
|
|
1491
1575
|
]
|
|
1492
1576
|
},
|
|
1577
|
+
{
|
|
1578
|
+
"name": "name-roster",
|
|
1579
|
+
"type": "registry:ui",
|
|
1580
|
+
"description": "A column-flow alphabetical list of named items with a dot-prefix indicator and a highlighted-row variant for featured or owned entries.",
|
|
1581
|
+
"category": "data-display",
|
|
1582
|
+
"dependencies": [
|
|
1583
|
+
"@loworbitstudio/visor-core"
|
|
1584
|
+
],
|
|
1585
|
+
"registryDependencies": [
|
|
1586
|
+
"utils"
|
|
1587
|
+
],
|
|
1588
|
+
"files": [
|
|
1589
|
+
{
|
|
1590
|
+
"path": "components/ui/name-roster/name-roster.tsx",
|
|
1591
|
+
"type": "registry:ui",
|
|
1592
|
+
"content": "import * as React from \"react\"\nimport { cn } from \"../../../lib/utils\"\nimport styles from \"./name-roster.module.css\"\n\n// ------------------------------------------------------------------ Types\n\n/** A single item in the items-shorthand prop */\nexport interface NameRosterItemData {\n name: string\n highlighted?: boolean\n}\n\n/**\n * Responsive column value — accepts a plain number or a breakpoint map.\n * Breakpoints: base, sm (640px), md (768px), lg (1024px), xl (1280px).\n */\nexport type ResponsiveValue<T> =\n | T\n | {\n base?: T\n sm?: T\n md?: T\n lg?: T\n xl?: T\n }\n\n// ------------------------------------------------------------------ NameRosterItem\n\nexport interface NameRosterItemProps extends React.LiHTMLAttributes<HTMLLIElement> {\n /** Render this item with the highlighted dot color and type treatment. */\n highlighted?: boolean\n}\n\nconst NameRosterItem = React.forwardRef<HTMLLIElement, NameRosterItemProps>(\n ({ className, highlighted = false, ...props }, ref) => {\n return (\n <li\n ref={ref}\n data-slot=\"name-roster-item\"\n data-highlighted={highlighted ? \"true\" : undefined}\n className={cn(styles.item, highlighted && styles.itemHighlighted, className)}\n {...props}\n />\n )\n }\n)\nNameRosterItem.displayName = \"NameRosterItem\"\n\n// ------------------------------------------------------------------ NameRoster\n\nexport interface NameRosterProps extends React.HTMLAttributes<HTMLUListElement | HTMLOListElement> {\n /**\n * Number of CSS columns to display.\n * Pass a number for a fixed column count, or a breakpoint map for responsive layout.\n * Defaults to 1.\n */\n columns?: ResponsiveValue<number>\n /**\n * Sort order of items.\n * - \"alpha\" — sort children / items array alphabetically via localeCompare\n * - \"none\" — render in insertion order (default)\n */\n sort?: \"alpha\" | \"none\"\n /**\n * Shorthand: provide an array of items instead of JSX children.\n * When both items and children are provided, items takes precedence.\n */\n items?: NameRosterItemData[]\n /**\n * Show the dot prefix indicator on each item. Defaults to true.\n */\n dot?: boolean\n /**\n * HTML list element to render. Defaults to \"ul\".\n */\n as?: \"ul\" | \"ol\"\n}\n\n/** Build an inline style object that sets CSS custom properties for each breakpoint. */\nfunction buildColumnStyle(columns: ResponsiveValue<number>): React.CSSProperties {\n if (typeof columns === \"number\") {\n return { \"--roster-columns\": columns } as React.CSSProperties\n }\n\n const props: Record<string, number> = {}\n if (columns.base !== undefined) props[\"--roster-columns\"] = columns.base\n if (columns.sm !== undefined) props[\"--roster-columns-sm\"] = columns.sm\n if (columns.md !== undefined) props[\"--roster-columns-md\"] = columns.md\n if (columns.lg !== undefined) props[\"--roster-columns-lg\"] = columns.lg\n if (columns.xl !== undefined) props[\"--roster-columns-xl\"] = columns.xl\n return props as React.CSSProperties\n}\n\nconst NameRoster = React.forwardRef<\n HTMLUListElement | HTMLOListElement,\n NameRosterProps\n>(\n (\n {\n className,\n style,\n columns = 1,\n sort = \"none\",\n items,\n dot = true,\n as: Tag = \"ul\",\n children,\n ...props\n },\n ref\n ) => {\n const columnStyle = buildColumnStyle(columns)\n\n let content: React.ReactNode\n\n if (items !== undefined) {\n // items-shorthand mode\n let sorted = [...items]\n if (sort === \"alpha\") {\n sorted = sorted.sort((a, b) => a.name.localeCompare(b.name))\n }\n content = sorted.map((item) => (\n <NameRosterItem key={item.name} highlighted={item.highlighted}>\n {item.name}\n </NameRosterItem>\n ))\n } else {\n // children mode — sort if requested\n if (sort === \"alpha\") {\n const childArray = React.Children.toArray(children)\n childArray.sort((a, b) => {\n const getLabel = (node: React.ReactNode): string => {\n if (!React.isValidElement(node)) return \"\"\n const p = node.props as Record<string, unknown>\n return typeof p.children === \"string\" ? p.children : \"\"\n }\n return getLabel(a).localeCompare(getLabel(b))\n })\n content = childArray\n } else {\n content = children\n }\n }\n\n return (\n <Tag\n ref={ref as React.Ref<HTMLUListElement & HTMLOListElement>}\n data-slot=\"name-roster\"\n data-dot={dot ? \"true\" : \"false\"}\n className={cn(styles.roster, !dot && styles.rosterNoDot, className)}\n style={{ ...columnStyle, ...style }}\n {...props}\n >\n {content}\n </Tag>\n )\n }\n)\nNameRoster.displayName = \"NameRoster\"\n\nexport { NameRoster, NameRosterItem }\n"
|
|
1593
|
+
},
|
|
1594
|
+
{
|
|
1595
|
+
"path": "components/ui/name-roster/name-roster.module.css",
|
|
1596
|
+
"type": "registry:ui",
|
|
1597
|
+
"content": "/* NameRoster — column-flow alphabetical name list */\n\n/* ---- Container ---- */\n.roster {\n /* Customization slots — defaults resolve to current visual output so existing consumers see no change. Override these in a token-override scope to retheme the roster (typography, hover color, dot, transform). */\n\n /* Typography */\n --roster-item-font-size: var(--font-size-sm, 0.875rem);\n --roster-item-font-weight: var(--font-weight-normal, 400);\n --roster-item-letter-spacing: normal;\n --roster-item-line-height: var(--line-height-normal, 1.5);\n\n /* Colors */\n --roster-item-color: var(--text-primary, #111827);\n --roster-item-color-hover: var(--text-secondary, #6b7280);\n --roster-item-color-highlighted: var(--text-primary, #111827);\n\n /* Dot */\n --roster-dot-size: 0.375rem; /* 6px — decorative, not a spacing token */\n --roster-dot-color: var(--surface-accent-strong, #111827);\n --roster-dot-color-hover: var(--roster-dot-color);\n --roster-dot-color-highlighted: var(--surface-accent-default, #6b7280);\n --roster-dot-glow-hover: none;\n\n /* Item transform on hover (off by default; consumers can opt in) */\n --roster-item-hover-transform: none;\n\n /* Highlighted item weight */\n --roster-item-font-weight-highlighted: var(--font-weight-medium, 500);\n\n list-style: none;\n margin: 0;\n padding: 0;\n\n /* Default: single column */\n column-count: var(--roster-columns, 1);\n column-fill: balance;\n column-gap: var(--spacing-8, 2rem);\n}\n\n/* Responsive column breakpoints — set via CSS custom properties from the component */\n@media (min-width: 640px) {\n .roster {\n column-count: var(--roster-columns-sm, var(--roster-columns, 1));\n }\n}\n\n@media (min-width: 768px) {\n .roster {\n column-count: var(--roster-columns-md, var(--roster-columns-sm, var(--roster-columns, 1)));\n }\n}\n\n@media (min-width: 1024px) {\n .roster {\n column-count: var(--roster-columns-lg, var(--roster-columns-md, var(--roster-columns-sm, var(--roster-columns, 1))));\n }\n}\n\n@media (min-width: 1280px) {\n .roster {\n column-count: var(--roster-columns-xl, var(--roster-columns-lg, var(--roster-columns-md, var(--roster-columns-sm, var(--roster-columns, 1)))));\n }\n}\n\n/* ---- Item ---- */\n.item {\n display: flex;\n align-items: center;\n gap: var(--spacing-2, 0.5rem);\n padding-block: var(--spacing-1, 0.25rem);\n break-inside: avoid;\n\n font-size: var(--roster-item-font-size);\n font-weight: var(--roster-item-font-weight);\n letter-spacing: var(--roster-item-letter-spacing);\n color: var(--roster-item-color);\n line-height: var(--roster-item-line-height);\n\n cursor: default;\n transition:\n color var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out),\n transform var(--motion-duration-300, 300ms) var(--motion-easing-default, ease-in-out);\n}\n\n/* Dot indicator — ::before pseudo-element */\n.item::before {\n content: \"\";\n flex-shrink: 0;\n display: block;\n width: var(--roster-dot-size);\n height: var(--roster-dot-size);\n border-radius: var(--radius-full, 9999px);\n background-color: var(--roster-dot-color);\n transition:\n background-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/* Hover type + dot treatment — consumers override the relevant --roster-* slots to retheme */\n.item:hover {\n color: var(--roster-item-color-hover);\n transform: var(--roster-item-hover-transform);\n}\n\n.item:hover::before {\n background-color: var(--roster-dot-color-hover);\n box-shadow: var(--roster-dot-glow-hover);\n}\n\n/* Anchor children inherit item color and drop the underline — the dot is the only visual affordance the roster needs. Consumers wanting underlines can re-add them via global CSS. */\n.item a {\n color: inherit;\n text-decoration: none;\n}\n\n.item a:hover {\n color: inherit;\n}\n\n/* ---- No-dot modifier ---- */\n.rosterNoDot .item::before {\n display: none;\n}\n\n/* ---- Highlighted variant ---- */\n.itemHighlighted {\n font-weight: var(--roster-item-font-weight-highlighted);\n color: var(--roster-item-color-highlighted);\n}\n\n.itemHighlighted::before {\n background-color: var(--roster-dot-color-highlighted);\n}\n"
|
|
1598
|
+
}
|
|
1599
|
+
]
|
|
1600
|
+
},
|
|
1493
1601
|
{
|
|
1494
1602
|
"name": "number-input",
|
|
1495
1603
|
"type": "registry:ui",
|
|
@@ -1865,7 +1973,7 @@
|
|
|
1865
1973
|
{
|
|
1866
1974
|
"path": "components/ui/font-showcase/font-showcase.module.css",
|
|
1867
1975
|
"type": "registry:ui",
|
|
1868
|
-
"content": "/* ─── FontShowcase Card ──────────────────────────────────────────────────── */\n\n.card {\n display: flex;\n flex-direction: column;\n gap: var(--spacing-4, 1rem);\n padding: var(--spacing-6, 1.5rem);\n border: 1px solid var(--border-default, #e5e7eb);\n border-radius: var(--radius-xl, 0.75rem);\n background: var(--surface-card, #ffffff);\n transition: box-shadow var(--motion-duration-fast, 150ms) var(--motion-easing-default, ease);\n}\n\n@media (hover: hover) {\n .card:hover {\n box-shadow: var(--shadow-md, 0 4px 6px -1px rgba(0, 0, 0, 0.1));\n }\n}\n\n/* ─── Header ─────────────────────────────────────────────────────────────── */\n\n.header {\n display: flex;\n align-items: flex-start;\n justify-content: space-between;\n gap: var(--spacing-3, 0.75rem);\n}\n\n.meta {\n display: flex;\n flex-direction: column;\n gap: var(--spacing-1, 0.25rem);\n}\n\n.familyName {\n font-size: var(--font-size-lg, 1.125rem);\n font-weight: 600;\n color: var(--text-primary, #111827);\n line-height: 1.25;\n}\n\n.token {\n font-family: var(--font-mono, monospace);\n font-size: var(--font-size-xs, 0.625rem);\n line-height: 1;\n color: var(--text-
|
|
1976
|
+
"content": "/* ─── FontShowcase Card ──────────────────────────────────────────────────── */\n\n.card {\n display: flex;\n flex-direction: column;\n gap: var(--spacing-4, 1rem);\n padding: var(--spacing-6, 1.5rem);\n border: 1px solid var(--border-default, #e5e7eb);\n border-radius: var(--radius-xl, 0.75rem);\n background: var(--surface-card, #ffffff);\n transition: box-shadow var(--motion-duration-fast, 150ms) var(--motion-easing-default, ease);\n}\n\n@media (hover: hover) {\n .card:hover {\n box-shadow: var(--shadow-md, 0 4px 6px -1px rgba(0, 0, 0, 0.1));\n }\n}\n\n/* ─── Header ─────────────────────────────────────────────────────────────── */\n\n.header {\n display: flex;\n align-items: flex-start;\n justify-content: space-between;\n gap: var(--spacing-3, 0.75rem);\n}\n\n.meta {\n display: flex;\n flex-direction: column;\n gap: var(--spacing-1, 0.25rem);\n}\n\n.familyName {\n font-size: var(--font-size-lg, 1.125rem);\n font-weight: 600;\n color: var(--text-primary, #111827);\n line-height: 1.25;\n}\n\n.token {\n font-family: var(--font-mono, monospace);\n font-size: var(--font-size-xs, 0.625rem);\n line-height: 1;\n color: var(--text-secondary, #4b5563);\n background: var(--surface-muted, #f3f4f6);\n padding: var(--spacing-1, 0.25rem) var(--spacing-2, 0.5rem);\n border-radius: var(--radius-sm, 0.25rem);\n display: inline-block;\n margin-top: var(--spacing-1, 0.25rem);\n width: fit-content;\n}\n\n/* ─── Hero ────────────────────────────────────────────────────────────────── */\n\n.hero {\n font-size: 4.5rem;\n line-height: 1;\n color: var(--text-primary, #111827);\n letter-spacing: -0.02em;\n user-select: none;\n opacity: 0.9;\n}\n\n/* ─── Divider ─────────────────────────────────────────────────────────────── */\n\n.divider {\n border: none;\n border-top: 1px solid var(--border-muted, #f3f4f6);\n}\n\n/* ─── Weight Specimens ────────────────────────────────────────────────────── */\n\n.weights {\n display: flex;\n flex-direction: column;\n gap: var(--spacing-3, 0.75rem);\n}\n\n.weightRow {\n display: flex;\n align-items: baseline;\n gap: var(--spacing-4, 1rem);\n}\n\n.weightSample {\n font-size: var(--font-size-xl, 1.25rem);\n line-height: 1.3;\n color: var(--text-primary, #111827);\n flex: 1;\n min-width: 0;\n}\n\n.weightMeta {\n display: flex;\n flex-direction: column;\n align-items: flex-end;\n flex-shrink: 0;\n gap: var(--spacing-1, 0.25rem);\n}\n\n.weightValue {\n font-family: var(--font-mono, monospace);\n font-size: var(--font-size-xs, 0.625rem);\n line-height: 1;\n color: var(--text-secondary, #4b5563);\n}\n\n/* ─── Grid ────────────────────────────────────────────────────────────────── */\n\n.grid {\n display: grid;\n grid-template-columns: repeat(auto-fill, minmax(22.5rem, 1fr));\n gap: var(--spacing-6, 1.5rem);\n}\n"
|
|
1869
1977
|
}
|
|
1870
1978
|
]
|
|
1871
1979
|
},
|
|
@@ -2133,6 +2241,30 @@
|
|
|
2133
2241
|
}
|
|
2134
2242
|
]
|
|
2135
2243
|
},
|
|
2244
|
+
{
|
|
2245
|
+
"name": "stat-hero",
|
|
2246
|
+
"type": "registry:ui",
|
|
2247
|
+
"description": "Hero-scale animated metric banner with a 35/65 grid layout, large headline value, and an animated trendline (CSS stroke-dasharray). Respects prefers-reduced-motion and updates trendline color live via theme class swap.",
|
|
2248
|
+
"category": "admin",
|
|
2249
|
+
"dependencies": [
|
|
2250
|
+
"@loworbitstudio/visor-core"
|
|
2251
|
+
],
|
|
2252
|
+
"registryDependencies": [
|
|
2253
|
+
"utils"
|
|
2254
|
+
],
|
|
2255
|
+
"files": [
|
|
2256
|
+
{
|
|
2257
|
+
"path": "components/ui/stat-hero/stat-hero.tsx",
|
|
2258
|
+
"type": "registry:ui",
|
|
2259
|
+
"content": "import * as React from \"react\"\nimport { cn } from \"../../../lib/utils\"\nimport styles from \"./stat-hero.module.css\"\n\nexport type StatHeroDeltaDirection = \"up\" | \"down\" | \"flat\"\n\nexport interface StatHeroDelta {\n /** Display value, e.g. \"+12.4%\" or \"-$2.1K\". */\n value: React.ReactNode\n /** Semantic direction of the change. Drives color and glyph. */\n direction: StatHeroDeltaDirection\n}\n\nexport interface StatHeroProps extends React.HTMLAttributes<HTMLElement> {\n /** Small uppercase label describing the metric, e.g. \"Monthly Recurring Revenue\". */\n label: React.ReactNode\n /** Hero-scale metric value, e.g. \"$1,240,000\". */\n value: React.ReactNode\n /** Optional change indicator shown below the value. */\n delta?: StatHeroDelta\n /** Array of numeric data points driving the animated trendline (min 2). */\n values: number[]\n /** Optional caption rendered beneath the delta row. */\n caption?: React.ReactNode\n}\n\nconst DELTA_GLYPH: Record<StatHeroDeltaDirection, string> = {\n up: \"↑\",\n down: \"↓\",\n flat: \"→\",\n}\n\nconst DELTA_WORD: Record<StatHeroDeltaDirection, string> = {\n up: \"up\",\n down: \"down\",\n flat: \"flat\",\n}\n\n/**\n * Compute normalized SVG polyline points from a number array.\n * Returns null if values.length < 2.\n */\nfunction computePoints(\n values: number[],\n width: number,\n height: number\n): string | null {\n if (values.length < 2) return null\n const min = Math.min(...values)\n const max = Math.max(...values)\n const range = max - min || 1\n const stepX = width / (values.length - 1)\n return values\n .map((v, i) => {\n const x = i * stepX\n // Pad 4px top and bottom so the stroke doesn't clip at the SVG edge\n const y = 4 + ((max - v) / range) * (height - 8)\n return `${x.toFixed(1)},${y.toFixed(1)}`\n })\n .join(\" \")\n}\n\nconst StatHero = React.forwardRef<HTMLElement, StatHeroProps>(\n (\n {\n className,\n label,\n value,\n delta,\n values,\n caption,\n ...props\n },\n ref\n ) => {\n const SVG_WIDTH = 600\n const SVG_HEIGHT = 120\n const points = computePoints(values, SVG_WIDTH, SVG_HEIGHT)\n\n return (\n <article\n ref={ref}\n data-slot=\"stat-hero\"\n className={cn(styles.base, className)}\n {...props}\n >\n {/* Left column: label, value, delta, caption */}\n <div data-slot=\"stat-hero-body\" className={styles.body}>\n <p data-slot=\"stat-hero-label\" className={styles.label}>\n {label}\n </p>\n\n <p data-slot=\"stat-hero-value\" className={styles.value}>\n {value}\n </p>\n\n {delta ? (\n <div\n data-slot=\"stat-hero-delta\"\n data-direction={delta.direction}\n className={styles.delta}\n >\n <span className={styles.deltaGlyph} aria-hidden=\"true\">\n {DELTA_GLYPH[delta.direction]}\n </span>\n <span className={styles.deltaValue}>{delta.value}</span>\n <span className={styles.srOnly}>\n {DELTA_WORD[delta.direction]}\n </span>\n </div>\n ) : null}\n\n {caption ? (\n <p data-slot=\"stat-hero-caption\" className={styles.caption}>\n {caption}\n </p>\n ) : null}\n </div>\n\n {/* Right column: animated trendline SVG */}\n <div\n data-slot=\"stat-hero-chart\"\n className={styles.chart}\n aria-hidden=\"true\"\n >\n {points ? (\n <svg\n viewBox={`0 0 ${SVG_WIDTH} ${SVG_HEIGHT}`}\n preserveAspectRatio=\"none\"\n className={styles.svg}\n >\n <polyline\n className={styles.trendline}\n points={points}\n fill=\"none\"\n stroke=\"currentColor\"\n strokeLinejoin=\"round\"\n strokeLinecap=\"round\"\n />\n </svg>\n ) : null}\n </div>\n </article>\n )\n }\n)\nStatHero.displayName = \"StatHero\"\n\nexport { StatHero }\n"
|
|
2260
|
+
},
|
|
2261
|
+
{
|
|
2262
|
+
"path": "components/ui/stat-hero/stat-hero.module.css",
|
|
2263
|
+
"type": "registry:ui",
|
|
2264
|
+
"content": "/* Stat Hero: hero-scale animated metric banner\n *\n * Layout strategy: full-width banner with a 35/65 CSS grid split.\n * The left column holds the metric copy (label, value, delta, caption);\n * the right column holds a full-height animated trendline SVG.\n *\n * The trendline draw animation uses stroke-dasharray + stroke-dashoffset\n * so no JS animation loop is required. The SVG polyline uses\n * stroke=\"currentColor\" and color: var(--interactive-primary-bg) on the\n * chart region so the trendline color updates live when the theme class\n * swaps on :root.\n *\n * prefers-reduced-motion: reduce — animation is disabled; the trendline\n * is drawn at its full length instantly (stroke-dashoffset: 0, no transition).\n */\n\n.base {\n --stat-hero-value-size: 6rem; /* ~96px — overridable per consumer */\n\n display: grid;\n grid-template-columns: 35fr 65fr;\n gap: var(--spacing-8, 2rem);\n align-items: center;\n\n padding: var(--spacing-8, 2rem);\n background-color: var(--surface-card, #ffffff);\n border: 1px solid var(--border-default, #e5e7eb);\n border-radius: var(--radius-lg, 0.75rem);\n color: var(--text-primary, #111827);\n box-shadow: var(--shadow-sm, 0 1px 2px 0 rgb(0 0 0 / 0.05));\n overflow: hidden;\n}\n\n/* ── Left column: label, value, delta, caption ─────────────────────────── */\n\n.body {\n display: flex;\n flex-direction: column;\n gap: var(--spacing-3, 0.75rem);\n min-width: 0;\n}\n\n.label {\n margin: 0;\n font-size: var(--font-size-xs, 0.75rem);\n font-weight: var(--font-weight-semibold, 600);\n letter-spacing: var(--letter-spacing-wide, 0.05em);\n text-transform: uppercase;\n color: var(--text-tertiary, #6b7280);\n line-height: var(--line-height-tight, 1.25);\n}\n\n.value {\n margin: 0;\n font-family: var(--font-family-heading, inherit);\n font-size: var(--stat-hero-value-size, 6rem);\n font-weight: var(--font-weight-semibold, 600);\n line-height: 1;\n letter-spacing: var(--letter-spacing-tight, -0.02em);\n color: var(--text-primary, #111827);\n font-variant-numeric: tabular-nums;\n}\n\n/* Delta row — change indicator */\n.delta {\n display: inline-flex;\n align-items: center;\n gap: var(--spacing-1, 0.25rem);\n font-size: var(--font-size-lg, 1.125rem);\n font-weight: var(--font-weight-medium, 500);\n line-height: var(--line-height-tight, 1.25);\n color: var(--text-secondary, #6b7280);\n}\n\n.delta[data-direction=\"up\"] {\n color: var(--text-success, #15803d);\n}\n\n.delta[data-direction=\"down\"] {\n color: var(--text-danger, #b91c1c);\n}\n\n.delta[data-direction=\"flat\"] {\n color: var(--text-tertiary, #6b7280);\n}\n\n.deltaGlyph {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n font-size: 1em;\n line-height: 1;\n}\n\n.deltaValue {\n font-variant-numeric: tabular-nums;\n}\n\n.caption {\n margin: 0;\n font-size: var(--font-size-sm, 0.875rem);\n color: var(--text-tertiary, #6b7280);\n line-height: var(--line-height-normal, 1.5);\n}\n\n/* Visually hidden but announced to screen readers */\n.srOnly {\n position: absolute;\n width: 1px;\n height: 1px;\n padding: 0;\n margin: -1px;\n overflow: hidden;\n clip: rect(0, 0, 0, 0);\n white-space: nowrap;\n border: 0;\n}\n\n/* ── Right column: animated trendline ──────────────────────────────────── */\n\n.chart {\n /* Drive trendline color via currentColor so theme swaps propagate. */\n color: var(--interactive-primary-bg, #2563eb);\n height: 100%;\n min-height: var(--spacing-20, 5rem);\n display: flex;\n align-items: stretch;\n}\n\n.svg {\n width: 100%;\n height: 100%;\n display: block;\n min-height: var(--spacing-20, 5rem);\n}\n\n/* Trendline draw animation:\n * stroke-dasharray is set to a large value that covers any polyline length.\n * stroke-dashoffset starts at that value (hidden) and transitions to 0\n * (fully drawn). Uses the slowest available motion token for a dramatic\n * flagship reveal effect.\n */\n.trendline {\n stroke-width: var(--stroke-width-medium, 2);\n stroke-dasharray: 2000;\n stroke-dashoffset: 2000;\n animation: draw-trendline var(--motion-duration-800, 800ms)\n var(--motion-easing-ease-out, cubic-bezier(0, 0, 0.2, 1)) forwards;\n}\n\n@keyframes draw-trendline {\n to {\n stroke-dashoffset: 0;\n }\n}\n\n/* Respect reduced motion — disable animation, show at full length instantly */\n@media (prefers-reduced-motion: reduce) {\n .trendline {\n animation: none;\n stroke-dashoffset: 0;\n }\n}\n"
|
|
2265
|
+
}
|
|
2266
|
+
]
|
|
2267
|
+
},
|
|
2136
2268
|
{
|
|
2137
2269
|
"name": "status-badge",
|
|
2138
2270
|
"type": "registry:ui",
|
|
@@ -2159,6 +2291,30 @@
|
|
|
2159
2291
|
}
|
|
2160
2292
|
]
|
|
2161
2293
|
},
|
|
2294
|
+
{
|
|
2295
|
+
"name": "station-spectrum",
|
|
2296
|
+
"type": "registry:ui",
|
|
2297
|
+
"description": "Animated N-station progress diagram with a hairline rail that draws on scroll-entry and dots that illuminate sequentially via CSS transition delays. Designed for 'process / phases / pipeline' marketing diagrams.",
|
|
2298
|
+
"category": "visual-elements",
|
|
2299
|
+
"dependencies": [
|
|
2300
|
+
"@loworbitstudio/visor-core"
|
|
2301
|
+
],
|
|
2302
|
+
"registryDependencies": [
|
|
2303
|
+
"utils"
|
|
2304
|
+
],
|
|
2305
|
+
"files": [
|
|
2306
|
+
{
|
|
2307
|
+
"path": "components/ui/station-spectrum/station-spectrum.tsx",
|
|
2308
|
+
"type": "registry:ui",
|
|
2309
|
+
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport { cn } from \"../../../lib/utils\"\nimport styles from \"./station-spectrum.module.css\"\n\nexport interface Station {\n /** Numeric label displayed above the dot, e.g. \"01\". */\n num: string\n /** Station title displayed below the rail. */\n title: string\n /** Optional description text shown beneath the title. */\n description?: string\n}\n\nexport interface StationSpectrumProps extends React.HTMLAttributes<HTMLElement> {\n /** Array of stations to render. Minimum 2 recommended. */\n stations: Station[]\n /**\n * Visual density of the diagram.\n * `compact` reduces horizontal spacing for tighter layouts.\n * @default \"default\"\n */\n density?: \"compact\" | \"default\"\n /**\n * When true, the animation is active (rail draws, dots illuminate).\n * Pass explicitly to control the trigger from outside the component.\n * Ignored when `autoTrigger` is true.\n */\n inView?: boolean\n /**\n * When true (default), the component auto-wires an IntersectionObserver\n * to trigger the animation when the element enters the viewport.\n * Set to false to control the trigger manually via the `inView` prop.\n * @default true\n */\n autoTrigger?: boolean\n}\n\n/**\n * StationSpectrum — animated N-station progress diagram.\n *\n * Layout is two stacked bands sharing the same N-column grid:\n * - `.dotsBand` holds the per-station num + dot pair and the animated rail.\n * The rail is positioned `bottom: dot-size/2` from the band so it threads\n * through the dots' vertical centers regardless of font metrics.\n * - `.labelsBand` is the canonical `<ol>` exposing titles and descriptions\n * to assistive tech (the dotsBand is decorative, marked aria-hidden).\n *\n * Animation is triggered by the `inView` class on the root:\n * - `autoTrigger=true` (default): wired via IntersectionObserver\n * - `autoTrigger=false`: driven by the `inView` prop\n *\n * prefers-reduced-motion: animation is disabled; rail and dots appear\n * at their final state instantly.\n */\nconst StationSpectrum = React.forwardRef<HTMLElement, StationSpectrumProps>(\n (\n {\n className,\n stations,\n density = \"default\",\n inView,\n autoTrigger = true,\n ...props\n },\n forwardedRef\n ) => {\n const innerRef = React.useRef<HTMLElement | null>(null)\n const [autoInView, setAutoInView] = React.useState(false)\n\n const setRef = React.useCallback(\n (node: HTMLElement | null) => {\n innerRef.current = node\n if (typeof forwardedRef === \"function\") {\n forwardedRef(node)\n } else if (forwardedRef) {\n forwardedRef.current = node\n }\n },\n [forwardedRef]\n )\n\n React.useEffect(() => {\n if (!autoTrigger) return\n const element = innerRef.current\n if (!element || typeof IntersectionObserver === \"undefined\") return\n\n const observer = new IntersectionObserver(\n ([entry]) => {\n if (entry.isIntersecting) {\n setAutoInView(true)\n observer.disconnect()\n }\n },\n { threshold: 0.3 }\n )\n\n observer.observe(element)\n return () => observer.disconnect()\n }, [autoTrigger])\n\n const isActive = autoTrigger ? autoInView : (inView ?? false)\n\n const { style: userStyle, ...restProps } = props\n\n return (\n <section\n ref={setRef}\n data-slot=\"station-spectrum\"\n data-density={density}\n className={cn(styles.base, isActive && styles.inView, className)}\n style={{\n ...(userStyle ?? {}),\n \"--station-count\": stations.length,\n } as React.CSSProperties}\n {...restProps}\n >\n {/* Decorative band: nums, dots, and the animated rail. */}\n <div className={styles.dotsBand} aria-hidden=\"true\">\n <div className={styles.dotsRow}>\n {stations.map((station, idx) => (\n <div\n key={station.num}\n className={styles.dotCell}\n style={{ \"--idx\": idx } as React.CSSProperties}\n >\n <span className={styles.num}>{station.num}</span>\n <span className={styles.dot} />\n </div>\n ))}\n </div>\n <div className={styles.rail} aria-hidden=\"true\">\n <div className={styles.railLine} />\n </div>\n </div>\n\n {/* Accessible ordered list — titles and descriptions. */}\n <ol className={styles.labelsBand} aria-label=\"Process stages\">\n {stations.map((station, idx) => (\n <li\n key={station.num}\n className={styles.labelCell}\n style={{ \"--idx\": idx } as React.CSSProperties}\n >\n <span className={styles.title}>{station.title}</span>\n {station.description ? (\n <span className={styles.description}>{station.description}</span>\n ) : null}\n </li>\n ))}\n </ol>\n </section>\n )\n }\n)\nStationSpectrum.displayName = \"StationSpectrum\"\n\nexport { StationSpectrum }\n"
|
|
2310
|
+
},
|
|
2311
|
+
{
|
|
2312
|
+
"path": "components/ui/station-spectrum/station-spectrum.module.css",
|
|
2313
|
+
"type": "registry:ui",
|
|
2314
|
+
"content": "/* StationSpectrum: animated N-station progress diagram\n *\n * Layout strategy:\n * Two stacked bands share the same N-column grid:\n * - `.dotsBand` contains the num+dot pairs and the rail. The rail is\n * positioned `bottom: dot-size/2` from the band, so the line threads\n * through the vertical centers of the dots without depending on font\n * metrics or magic offsets.\n * - `.labelsBand` is the accessible <ol> with titles and descriptions.\n *\n * Animation strategy:\n * Rail: transform: scaleX(0) → scaleX(1) with transform-origin: left.\n * Dots: background-color transitions staggered via `--idx` * delay-step.\n * Triggered by `.inView` on the root.\n *\n * prefers-reduced-motion:\n * All transitions suppressed; rail and dots render in their final state.\n */\n\n/* ── Root ──────────────────────────────────────────────────────────────── */\n\n.base {\n --station-spectrum-dot-size: var(--spacing-3, 0.75rem); /* 12px */\n --station-spectrum-dot-size-compact: var(--spacing-2, 0.5rem); /* 8px */\n --station-spectrum-rail-thickness: var(--stroke-width-thin, 1px);\n --station-spectrum-rail-color: var(--border-default, #e5e7eb);\n --station-spectrum-dot-color-off: var(--surface-card, #ffffff);\n --station-spectrum-dot-color-on: var(--interactive-primary-bg, #2563eb);\n --station-spectrum-dot-border-color: var(--border-default, #e5e7eb);\n --station-spectrum-dot-border-color-on: var(--interactive-primary-bg, #2563eb);\n --station-spectrum-num-color: var(--text-tertiary, #6b7280);\n --station-spectrum-title-color: var(--text-primary, #111827);\n --station-spectrum-desc-color: var(--text-secondary, #6b7280);\n\n /* Rail animation timing */\n --rail-duration: var(--motion-duration-slow, 500ms);\n --rail-easing: var(--motion-easing-enter, cubic-bezier(0, 0, 0.2, 1));\n --dot-delay-step: 80ms; /* per-dot stagger: intentional constant */\n --rail-delay: 200ms; /* head-start for rail before dots begin */\n\n position: relative;\n width: 100%;\n}\n\n/* ── Dots band (num + dot + rail) ──────────────────────────────────────── */\n\n.dotsBand {\n position: relative;\n width: 100%;\n}\n\n.dotsRow {\n display: grid;\n grid-template-columns: repeat(var(--station-count, 1), 1fr);\n position: relative;\n z-index: 1; /* above the rail */\n}\n\n.dotCell {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: var(--spacing-2, 0.5rem);\n /* The last dotCell drops its right padding so its dot aligns flex-start\n inside an equal-width column — the rail's right offset (computed from\n station count) takes care of meeting the last dot's center. */\n}\n\n.num {\n font-size: var(--font-size-xs, 0.75rem);\n font-weight: var(--font-weight-semibold, 600);\n letter-spacing: var(--letter-spacing-wide, 0.05em);\n color: var(--station-spectrum-num-color);\n line-height: var(--line-height-tight, 1.25);\n font-variant-numeric: tabular-nums;\n transition: color var(--motion-duration-normal, 200ms) var(--motion-easing-default, ease)\n calc(var(--idx, 0) * var(--dot-delay-step) + var(--rail-delay));\n}\n\n.dot {\n width: var(--station-spectrum-dot-size);\n height: var(--station-spectrum-dot-size);\n border-radius: 50%;\n background-color: var(--station-spectrum-dot-color-off);\n border: var(--station-spectrum-rail-thickness) solid var(--station-spectrum-dot-border-color);\n transition:\n background-color var(--motion-duration-normal, 200ms) var(--motion-easing-default, ease)\n calc(var(--idx, 0) * var(--dot-delay-step) + var(--rail-delay)),\n border-color var(--motion-duration-normal, 200ms) var(--motion-easing-default, ease)\n calc(var(--idx, 0) * var(--dot-delay-step) + var(--rail-delay));\n}\n\n/* ── Rail ──────────────────────────────────────────────────────────────── */\n/*\n * The rail threads through the vertical center of the dots. Because the\n * dot sits at the bottom of each .dotCell (after the num row), and the\n * .dotsBand wraps only the dots row, positioning the rail by `bottom`\n * with an offset of `dot-size/2` puts the line exactly at the dots'\n * vertical center — no font-metric-dependent math required.\n *\n * Horizontally: dots are flex-start within equal-width columns, so the\n * first dot's center is `dot-size/2` from the band's left edge and the\n * last dot's center is `100% / N - dot-size/2` inset from the right edge.\n */\n\n.rail {\n position: absolute;\n left: calc(var(--station-spectrum-dot-size) / 2);\n right: calc(100% / var(--station-count, 1) - var(--station-spectrum-dot-size) / 2);\n bottom: calc(var(--station-spectrum-dot-size) / 2);\n /* Subtract half the rail's own thickness so the LINE's center (not its\n top edge) lands at the dots' vertical center. */\n margin-bottom: calc(var(--station-spectrum-rail-thickness) / -2);\n height: var(--station-spectrum-rail-thickness);\n pointer-events: none;\n z-index: 0;\n}\n\n.railLine {\n width: 100%;\n height: 100%;\n background-color: var(--station-spectrum-rail-color);\n transform-origin: left center;\n transform: scaleX(0);\n transition: transform var(--rail-duration) var(--rail-easing);\n}\n\n/* ── Labels band (titles + descriptions) ───────────────────────────────── */\n\n.labelsBand {\n display: grid;\n grid-template-columns: repeat(var(--station-count, 1), 1fr);\n list-style: none;\n margin: 0;\n padding: 0;\n margin-top: var(--spacing-3, 0.75rem);\n}\n\n.labelCell {\n display: flex;\n flex-direction: column;\n gap: var(--spacing-1, 0.25rem);\n padding-right: var(--spacing-4, 1rem);\n}\n\n.labelCell:last-child {\n padding-right: 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(--station-spectrum-title-color);\n line-height: var(--line-height-tight, 1.25);\n}\n\n.description {\n display: block;\n font-size: var(--font-size-xs, 0.75rem);\n color: var(--station-spectrum-desc-color);\n line-height: var(--line-height-normal, 1.5);\n}\n\n/* ── inView state — animation active ──────────────────────────────────── */\n\n.inView .railLine {\n transform: scaleX(1);\n}\n\n.inView .dot {\n background-color: var(--station-spectrum-dot-color-on);\n border-color: var(--station-spectrum-dot-border-color-on);\n}\n\n.inView .num {\n color: var(--interactive-primary-bg, #2563eb);\n}\n\n/* ── Density variant: compact ─────────────────────────────────────────── */\n\n.base[data-density=\"compact\"] {\n --station-spectrum-dot-size: var(--station-spectrum-dot-size-compact);\n}\n\n.base[data-density=\"compact\"] .labelCell {\n padding-right: var(--spacing-2, 0.5rem);\n}\n\n.base[data-density=\"compact\"] .title {\n font-size: var(--font-size-xs, 0.75rem);\n}\n\n.base[data-density=\"compact\"] .description {\n display: none; /* descriptions hidden in compact mode */\n}\n\n/* ── Reduced motion ────────────────────────────────────────────────────── */\n\n@media (prefers-reduced-motion: reduce) {\n .railLine {\n transition: none;\n transform: scaleX(1);\n }\n\n .dot,\n .num {\n transition: none;\n }\n\n .dot {\n background-color: var(--station-spectrum-dot-color-on);\n border-color: var(--station-spectrum-dot-border-color-on);\n }\n\n .num {\n color: var(--interactive-primary-bg, #2563eb);\n }\n}\n"
|
|
2315
|
+
}
|
|
2316
|
+
]
|
|
2317
|
+
},
|
|
2162
2318
|
{
|
|
2163
2319
|
"name": "theme-switcher",
|
|
2164
2320
|
"type": "registry:ui",
|
|
@@ -3425,6 +3581,31 @@
|
|
|
3425
3581
|
}
|
|
3426
3582
|
]
|
|
3427
3583
|
},
|
|
3584
|
+
{
|
|
3585
|
+
"name": "chip-group",
|
|
3586
|
+
"type": "registry:block",
|
|
3587
|
+
"description": "A composable container managing selection state for ChoiceChip (single-select) and FilterChip (multi-select) chips. Mirrors Flutter Material's chip selection model with type=\"single\" (radio) and type=\"multiple\" (checkbox) modes.",
|
|
3588
|
+
"category": "form",
|
|
3589
|
+
"dependencies": [
|
|
3590
|
+
"@loworbitstudio/visor-core"
|
|
3591
|
+
],
|
|
3592
|
+
"registryDependencies": [
|
|
3593
|
+
"utils",
|
|
3594
|
+
"chip"
|
|
3595
|
+
],
|
|
3596
|
+
"files": [
|
|
3597
|
+
{
|
|
3598
|
+
"path": "blocks/chip-group/chip-group.tsx",
|
|
3599
|
+
"type": "registry:block",
|
|
3600
|
+
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport { cn } from \"../../lib/utils\"\nimport styles from \"./chip-group.module.css\"\n\n/* ─── Context ────────────────────────────────────────────────────────── */\n\ntype ChipGroupContextValue = {\n type: \"single\" | \"multiple\"\n value: string[]\n onValueChange: (value: string[]) => void\n}\n\nconst ChipGroupContext = React.createContext<ChipGroupContextValue | null>(null)\n\nexport function useChipGroup() {\n return React.useContext(ChipGroupContext)\n}\n\n/* ─── ChipGroup ──────────────────────────────────────────────────────── */\n\nexport interface ChipGroupProps {\n /**\n * \"single\" — acts like a radio group: selecting one deselects the others.\n * \"multiple\" — acts like a checkbox group: each chip toggles independently.\n */\n type: \"single\" | \"multiple\"\n /**\n * Controlled value. For \"single\", at most one string. For \"multiple\", any\n * number of strings. Pass an empty array for \"no selection\".\n */\n value?: string[]\n /**\n * Default value for uncontrolled usage.\n */\n defaultValue?: string[]\n /**\n * Fires with the new value array after any selection change.\n */\n onValueChange?: (value: string[]) => void\n /** Layout direction. Defaults to \"horizontal\". */\n direction?: \"horizontal\" | \"vertical\"\n /** Extra class forwarded to the root element. */\n className?: string\n children: React.ReactNode\n /** Accessible label describing the group's purpose. */\n \"aria-label\"?: string\n /** Points to a labelling element when aria-label is insufficient. */\n \"aria-labelledby\"?: string\n}\n\nconst ChipGroup = React.forwardRef<HTMLDivElement, ChipGroupProps>(\n (\n {\n type,\n value: controlledValue,\n defaultValue,\n onValueChange,\n direction = \"horizontal\",\n className,\n children,\n \"aria-label\": ariaLabel,\n \"aria-labelledby\": ariaLabelledBy,\n },\n ref,\n ) => {\n const isControlled = controlledValue !== undefined\n const [internalValue, setInternalValue] = React.useState<string[]>(\n defaultValue ?? [],\n )\n\n const value = isControlled ? controlledValue : internalValue\n\n const handleValueChange = React.useCallback(\n (newValue: string[]) => {\n if (!isControlled) {\n setInternalValue(newValue)\n }\n onValueChange?.(newValue)\n },\n [isControlled, onValueChange],\n )\n\n const contextValue = React.useMemo(\n () => ({ type, value, onValueChange: handleValueChange }),\n [type, value, handleValueChange],\n )\n\n return (\n <ChipGroupContext.Provider value={contextValue}>\n <div\n ref={ref}\n role=\"group\"\n data-slot=\"chip-group\"\n data-type={type}\n data-direction={direction}\n aria-label={ariaLabel}\n aria-labelledby={ariaLabelledBy}\n className={cn(\n styles.root,\n direction === \"vertical\" && styles.vertical,\n className,\n )}\n >\n {children}\n </div>\n </ChipGroupContext.Provider>\n )\n },\n)\nChipGroup.displayName = \"ChipGroup\"\n\n/* ─── ChipGroupItem ──────────────────────────────────────────────────── */\n\nexport interface ChipGroupItemProps {\n /**\n * The value this item represents. Must be unique within the group.\n */\n value: string\n /** Whether this item is individually disabled. */\n disabled?: boolean\n className?: string\n children: React.ReactNode\n}\n\n/**\n * ChipGroupItem wraps any chip (ChoiceChip or FilterChip) in the group context.\n * It injects `selected`, `onPressed`, and `value` props automatically.\n */\nconst ChipGroupItem = React.forwardRef<HTMLElement, ChipGroupItemProps>(\n ({ value, disabled, className, children }, ref) => {\n const ctx = React.useContext(ChipGroupContext)\n const isSelected = ctx ? ctx.value.includes(value) : false\n\n const handlePress = React.useCallback(() => {\n if (!ctx || disabled) return\n if (ctx.type === \"single\") {\n ctx.onValueChange([value])\n } else {\n const next = ctx.value.includes(value)\n ? ctx.value.filter((v) => v !== value)\n : [...ctx.value, value]\n ctx.onValueChange(next)\n }\n }, [ctx, value, disabled])\n\n // Clone the child chip injecting the managed props\n const child = React.Children.only(children) as React.ReactElement<\n Record<string, unknown>\n >\n\n return React.cloneElement(child, {\n ref,\n value,\n selected: isSelected,\n onPressed: handlePress,\n disabled: disabled ?? child.props.disabled,\n className: cn(child.props.className as string | undefined, className),\n })\n },\n)\nChipGroupItem.displayName = \"ChipGroupItem\"\n\nexport { ChipGroup, ChipGroupItem }\n"
|
|
3601
|
+
},
|
|
3602
|
+
{
|
|
3603
|
+
"path": "blocks/chip-group/chip-group.module.css",
|
|
3604
|
+
"type": "registry:block",
|
|
3605
|
+
"content": "/* ChipGroup root */\n.root {\n display: flex;\n flex-direction: row;\n flex-wrap: wrap;\n align-items: center;\n gap: var(--spacing-2, 0.5rem);\n}\n\n.vertical {\n flex-direction: column;\n align-items: flex-start;\n}\n"
|
|
3606
|
+
}
|
|
3607
|
+
]
|
|
3608
|
+
},
|
|
3428
3609
|
{
|
|
3429
3610
|
"name": "cta-section",
|
|
3430
3611
|
"type": "registry:block",
|
|
@@ -3643,7 +3824,7 @@
|
|
|
3643
3824
|
{
|
|
3644
3825
|
"path": "components/devtools/source-inspector/visor-component-names.generated.ts",
|
|
3645
3826
|
"type": "registry:devtool",
|
|
3646
|
-
"content": "// THIS FILE IS GENERATED BY scripts/generate-visor-component-names.ts.\n// Do not edit by hand. Re-run `npm run generate:component-names` after\n// adding, removing, or renaming a Visor component.\n//\n// Source of truth: registry/registry-{ui,blocks,deck,visual,devtools}.ts\n// Used by: components/devtools/source-inspector/* (VI-311)\n\nexport const VISOR_COMPONENT_NAMES: ReadonlySet<string> = new Set([\n \"AccessibilitySection\",\n \"AccessibilitySlide\",\n \"AccessibilitySpecimen\",\n \"Accordion\",\n \"AccordionContent\",\n \"AccordionItem\",\n \"AccordionTrigger\",\n \"ActivityFeed\",\n \"ActivityFeedContext\",\n \"ActivityFeedItem\",\n \"ActivityFeedRoot\",\n \"AdminDashboard\",\n \"AdminDetailDrawer\",\n \"AdminListPage\",\n \"AdminListPageInner\",\n \"AdminSettingsPage\",\n \"AdminShell\",\n \"AdminTabbedEditor\",\n \"AdminWizard\",\n \"Alert\",\n \"AlertDescription\",\n \"AlertTitle\",\n \"Avatar\",\n \"AvatarFallback\",\n \"AvatarImage\",\n \"Badge\",\n \"Banner\",\n \"BannerAction\",\n \"BannerDescription\",\n \"BannerTitle\",\n \"Breadcrumb\",\n \"BreadcrumbEllipsis\",\n \"BreadcrumbItem\",\n \"BreadcrumbLink\",\n \"BreadcrumbList\",\n \"BreadcrumbPage\",\n \"BreadcrumbSeparator\",\n \"BulkActionBar\",\n \"Button\",\n \"ButtonSpecimenSection\",\n \"ButtonSpecimenSlide\",\n \"Calendar\",\n \"Card\",\n \"CardContent\",\n \"CardDescription\",\n \"CardFooter\",\n \"CardGrid\",\n \"CardHeader\",\n \"CardTitle\",\n \"Carousel\",\n \"CarouselContent\",\n \"CarouselContext\",\n \"CarouselGallery\",\n \"CarouselItem\",\n \"CarouselNext\",\n \"CarouselPrevious\",\n \"ChartContainer\",\n \"ChartContext\",\n \"ChartLegend\",\n \"ChartLegendContent\",\n \"ChartStyle\",\n \"ChartTooltip\",\n \"ChartTooltipContent\",\n \"Checkbox\",\n \"ClosingSlide\",\n \"CodeBlock\",\n \"Collapsible\",\n \"CollapsibleContent\",\n \"CollapsibleTrigger\",\n \"ColorBar\",\n \"ColorPaletteSection\",\n \"ColorSwatch\",\n \"ColorSwatchGrid\",\n \"Combobox\",\n \"ComboboxContent\",\n \"ComboboxContext\",\n \"ComboboxEmpty\",\n \"ComboboxGroup\",\n \"ComboboxInput\",\n \"ComboboxItem\",\n \"ComboboxSeparator\",\n \"Command\",\n \"CommandDialog\",\n \"CommandEmpty\",\n \"CommandGroup\",\n \"CommandInput\",\n \"CommandItem\",\n \"CommandList\",\n \"CommandLoading\",\n \"CommandSeparator\",\n \"CommandShortcut\",\n \"ComponentShowcaseContent\",\n \"ComponentShowcaseSection\",\n \"ComponentShowcaseSlide\",\n \"ConceptSlide\",\n \"ConfigurationPanel\",\n \"ConfirmDialog\",\n \"ContextMenu\",\n \"ContextMenuCheckboxItem\",\n \"ContextMenuContent\",\n \"ContextMenuGroup\",\n \"ContextMenuItem\",\n \"ContextMenuLabel\",\n \"ContextMenuPortal\",\n \"ContextMenuRadioGroup\",\n \"ContextMenuRadioItem\",\n \"ContextMenuSeparator\",\n \"ContextMenuShortcut\",\n \"ContextMenuSub\",\n \"ContextMenuSubContent\",\n \"ContextMenuSubTrigger\",\n \"ContextMenuTrigger\",\n \"CtaSection\",\n \"DataTable\",\n \"DataTableInner\",\n \"DatePicker\",\n \"DateRangePicker\",\n \"DeckContext\",\n \"DeckFooter\",\n \"DeckLayout\",\n \"DeckProvider\",\n \"DeckRenderer\",\n \"DesignSystemDeck\",\n \"DesignSystemSpecimen\",\n \"Dialog\",\n \"DialogClose\",\n \"DialogContent\",\n \"DialogDescription\",\n \"DialogHeader\",\n \"DialogOverlay\",\n \"DialogPortal\",\n \"DialogTitle\",\n \"DialogTrigger\",\n \"DotNav\",\n \"DropdownMenu\",\n \"DropdownMenuCheckboxItem\",\n \"DropdownMenuContent\",\n \"DropdownMenuGroup\",\n \"DropdownMenuItem\",\n \"DropdownMenuLabel\",\n \"DropdownMenuPortal\",\n \"DropdownMenuRadioGroup\",\n \"DropdownMenuRadioItem\",\n \"DropdownMenuSeparator\",\n \"DropdownMenuShortcut\",\n \"DropdownMenuSub\",\n \"DropdownMenuSubContent\",\n \"DropdownMenuSubTrigger\",\n \"DropdownMenuTrigger\",\n \"ElevationCard\",\n \"ElevationSlide\",\n \"EmptyState\",\n \"FeaturesGrid\",\n \"Field\",\n \"FieldDescription\",\n \"FieldError\",\n \"FieldLabel\",\n \"Fieldset\",\n \"FieldsetLegend\",\n \"FileUpload\",\n \"FilterBar\",\n \"FontShowcase\",\n \"FontShowcaseGrid\",\n \"FooterSection\",\n \"Form\",\n \"FormField\",\n \"FormSpecimenSection\",\n \"FormSpecimenSlide\",\n \"FullscreenOverlay\",\n \"FullscreenOverlayContent\",\n \"FullscreenOverlayTrigger\",\n \"Heading\",\n \"HeroSection\",\n \"HeroSlide\",\n \"HoverCard\",\n \"HoverCardContent\",\n \"HoverCardTrigger\",\n \"IconGrid\",\n \"IconGridSection\",\n \"IconSizeRow\",\n \"IconsSlide\",\n \"Image\",\n \"Input\",\n \"Kbd\",\n \"Label\",\n \"Lightbox\",\n \"LightboxContent\",\n \"LightboxContext\",\n \"LightboxTrigger\",\n \"LoginForm\",\n \"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 \"Navbar\",\n \"NavbarBrand\",\n \"NavbarContent\",\n \"NavbarItem\",\n \"NavbarLink\",\n \"NavbarToggle\",\n \"NumberInput\",\n \"OpacityBar\",\n \"OpacitySlide\",\n \"OTPInput\",\n \"PageHeader\",\n \"Pagination\",\n \"PaginationContent\",\n \"PaginationEllipsis\",\n \"PaginationItem\",\n \"PaginationLink\",\n \"PaginationNext\",\n \"PaginationPrevious\",\n \"PasswordInput\",\n \"PhoneInput\",\n \"Popover\",\n \"PopoverAnchor\",\n \"PopoverContent\",\n \"PopoverTrigger\",\n \"PricingSection\",\n \"Progress\",\n \"RadioGroup\",\n \"RadioGroupItem\",\n \"RadiusScale\",\n \"RadiusSection\",\n \"RadiusSlide\",\n \"ScrollArea\",\n \"ScrollBar\",\n \"SearchInput\",\n \"Select\",\n \"SelectContent\",\n \"SelectGroup\",\n \"SelectItem\",\n \"SelectLabel\",\n \"SelectScrollDownButton\",\n \"SelectScrollUpButton\",\n \"SelectSeparator\",\n \"SelectTrigger\",\n \"SelectValue\",\n \"SemanticColorGrid\",\n \"SemanticColorItem\",\n \"SemanticTokensSlide\",\n \"Separator\",\n \"ShadowSection\",\n \"Sheet\",\n \"SheetClose\",\n \"SheetContent\",\n \"SheetDescription\",\n \"SheetFooter\",\n \"SheetHeader\",\n \"SheetOverlay\",\n \"SheetPortal\",\n \"SheetTitle\",\n \"SheetTrigger\",\n \"Sidebar\",\n \"SidebarContent\",\n \"SidebarContext\",\n \"SidebarFooter\",\n \"SidebarGroup\",\n \"SidebarGroupAction\",\n \"SidebarGroupContent\",\n \"SidebarGroupLabel\",\n \"SidebarHeader\",\n \"SidebarInset\",\n \"SidebarMenu\",\n \"SidebarMenuAction\",\n \"SidebarMenuBadge\",\n \"SidebarMenuButton\",\n \"SidebarMenuItem\",\n \"SidebarMenuSub\",\n \"SidebarMenuSubButton\",\n \"SidebarMenuSubItem\",\n \"SidebarProvider\",\n \"SidebarRail\",\n \"SidebarSeparator\",\n \"SidebarTrigger\",\n \"Skeleton\",\n \"Slide\",\n \"SlideHeader\",\n \"Slider\",\n \"SliderControl\",\n \"SlideThemeContext\",\n \"SlideThemeProvider\",\n \"SourceInspector\",\n \"SourceInspectorContext\",\n \"SourceInspectorDevImpl\",\n \"SourceInspectorProvider\",\n \"SourceInspectorRunner\",\n \"SourceInspectorToggle\",\n \"SpacingScale\",\n \"SpacingSection\",\n \"SpacingSlide\",\n \"Sphere\",\n \"SpherePlayground\",\n \"StatCard\",\n \"StatusBadge\",\n \"StatusColorsSlide\",\n \"Stepper\",\n \"StepperContext\",\n \"StepperDescription\",\n \"StepperItem\",\n \"StepperSeparator\",\n \"StepperTitle\",\n \"StepperTrigger\",\n \"StepsSection\",\n \"SurfaceRow\",\n \"SurfaceSection\",\n \"Switch\",\n \"Table\",\n \"TableBody\",\n \"TableCaption\",\n \"TableCell\",\n \"TableFooter\",\n \"TableHead\",\n \"TableHeader\",\n \"TableRow\",\n \"Tabs\",\n \"TabsContent\",\n \"TabsList\",\n \"TabsTrigger\",\n \"TagInput\",\n \"TestimonialAttribution\",\n \"TestimonialSection\",\n \"Text\",\n \"Textarea\",\n \"ThemeArchitectureSlide\",\n \"ThemeColorsSlide\",\n \"ThemeSwitcher\",\n \"Timeline\",\n \"TimelineContent\",\n \"TimelineDescription\",\n \"TimelineIcon\",\n \"TimelineItem\",\n \"TimelineTimestamp\",\n \"TimelineTitle\",\n \"TitleSlide\",\n \"Toaster\",\n \"TOCSlide\",\n \"ToggleButton\",\n \"ToggleDevImpl\",\n \"ToggleGroup\",\n \"ToggleGroupContext\",\n \"ToggleGroupItem\",\n \"Tooltip\",\n \"TooltipContent\",\n \"TooltipProvider\",\n \"TooltipTrigger\",\n \"TypeBodySlide\",\n \"TypeDisplaySlide\",\n \"TypeSpecimen\",\n \"TypographySection\",\n])\n"
|
|
3827
|
+
"content": "// THIS FILE IS GENERATED BY scripts/generate-visor-component-names.ts.\n// Do not edit by hand. Re-run `npm run generate:component-names` after\n// adding, removing, or renaming a Visor component.\n//\n// Source of truth: registry/registry-{ui,blocks,deck,visual,devtools}.ts\n// Used by: components/devtools/source-inspector/* (VI-311)\n\nexport const VISOR_COMPONENT_NAMES: ReadonlySet<string> = new Set([\n \"AccessibilitySection\",\n \"AccessibilitySlide\",\n \"AccessibilitySpecimen\",\n \"Accordion\",\n \"AccordionContent\",\n \"AccordionItem\",\n \"AccordionTrigger\",\n \"ActivityFeed\",\n \"ActivityFeedContext\",\n \"ActivityFeedItem\",\n \"ActivityFeedRoot\",\n \"AdminDashboard\",\n \"AdminDetailDrawer\",\n \"AdminListPage\",\n \"AdminListPageInner\",\n \"AdminSettingsPage\",\n \"AdminShell\",\n \"AdminTabbedEditor\",\n \"AdminWizard\",\n \"Alert\",\n \"AlertDescription\",\n \"AlertTitle\",\n \"Avatar\",\n \"AvatarFallback\",\n \"AvatarImage\",\n \"Badge\",\n \"Banner\",\n \"BannerAction\",\n \"BannerDescription\",\n \"BannerTitle\",\n \"BentoGrid\",\n \"BentoTile\",\n \"BentoTileBody\",\n \"BentoTileDescription\",\n \"BentoTileFigure\",\n \"BentoTileHeadline\",\n \"BentoTileMedia\",\n \"BentoTileMeta\",\n \"BentoTileTitle\",\n \"Breadcrumb\",\n \"BreadcrumbEllipsis\",\n \"BreadcrumbItem\",\n \"BreadcrumbLink\",\n \"BreadcrumbList\",\n \"BreadcrumbPage\",\n \"BreadcrumbSeparator\",\n \"BulkActionBar\",\n \"Button\",\n \"ButtonSpecimenSection\",\n \"ButtonSpecimenSlide\",\n \"Calendar\",\n \"Card\",\n \"CardContent\",\n \"CardDescription\",\n \"CardFooter\",\n \"CardGrid\",\n \"CardHeader\",\n \"CardTitle\",\n \"Carousel\",\n \"CarouselContent\",\n \"CarouselContext\",\n \"CarouselGallery\",\n \"CarouselItem\",\n \"CarouselNext\",\n \"CarouselPrevious\",\n \"ChartContainer\",\n \"ChartContext\",\n \"ChartLegend\",\n \"ChartLegendContent\",\n \"ChartStyle\",\n \"ChartTooltip\",\n \"ChartTooltipContent\",\n \"Checkbox\",\n \"Chip\",\n \"ChipGroup\",\n \"ChipGroupContext\",\n \"ChipGroupItem\",\n \"ChoiceChip\",\n \"ClosingSlide\",\n \"CodeBlock\",\n \"Collapsible\",\n \"CollapsibleContent\",\n \"CollapsibleTrigger\",\n \"ColorBar\",\n \"ColorPaletteSection\",\n \"ColorSwatch\",\n \"ColorSwatchGrid\",\n \"Combobox\",\n \"ComboboxContent\",\n \"ComboboxContext\",\n \"ComboboxEmpty\",\n \"ComboboxGroup\",\n \"ComboboxInput\",\n \"ComboboxItem\",\n \"ComboboxSeparator\",\n \"Command\",\n \"CommandDialog\",\n \"CommandEmpty\",\n \"CommandGroup\",\n \"CommandInput\",\n \"CommandItem\",\n \"CommandList\",\n \"CommandLoading\",\n \"CommandSeparator\",\n \"CommandShortcut\",\n \"ComponentShowcaseContent\",\n \"ComponentShowcaseSection\",\n \"ComponentShowcaseSlide\",\n \"ConceptSlide\",\n \"ConfigurationPanel\",\n \"ConfirmDialog\",\n \"ContextMenu\",\n \"ContextMenuCheckboxItem\",\n \"ContextMenuContent\",\n \"ContextMenuGroup\",\n \"ContextMenuItem\",\n \"ContextMenuLabel\",\n \"ContextMenuPortal\",\n \"ContextMenuRadioGroup\",\n \"ContextMenuRadioItem\",\n \"ContextMenuSeparator\",\n \"ContextMenuShortcut\",\n \"ContextMenuSub\",\n \"ContextMenuSubContent\",\n \"ContextMenuSubTrigger\",\n \"ContextMenuTrigger\",\n \"CtaSection\",\n \"DataTable\",\n \"DataTableInner\",\n \"DatePicker\",\n \"DateRangePicker\",\n \"DeckContext\",\n \"DeckFooter\",\n \"DeckLayout\",\n \"DeckProvider\",\n \"DeckRenderer\",\n \"DesignSystemDeck\",\n \"DesignSystemSpecimen\",\n \"Dialog\",\n \"DialogClose\",\n \"DialogContent\",\n \"DialogDescription\",\n \"DialogHeader\",\n \"DialogOverlay\",\n \"DialogPortal\",\n \"DialogTitle\",\n \"DialogTrigger\",\n \"DotNav\",\n \"DropdownMenu\",\n \"DropdownMenuCheckboxItem\",\n \"DropdownMenuContent\",\n \"DropdownMenuGroup\",\n \"DropdownMenuItem\",\n \"DropdownMenuLabel\",\n \"DropdownMenuPortal\",\n \"DropdownMenuRadioGroup\",\n \"DropdownMenuRadioItem\",\n \"DropdownMenuSeparator\",\n \"DropdownMenuShortcut\",\n \"DropdownMenuSub\",\n \"DropdownMenuSubContent\",\n \"DropdownMenuSubTrigger\",\n \"DropdownMenuTrigger\",\n \"ElevationCard\",\n \"ElevationSlide\",\n \"EmptyState\",\n \"FeaturesGrid\",\n \"Field\",\n \"FieldDescription\",\n \"FieldError\",\n \"FieldLabel\",\n \"Fieldset\",\n \"FieldsetLegend\",\n \"FileUpload\",\n \"FilterBar\",\n \"FilterChip\",\n \"FontShowcase\",\n \"FontShowcaseGrid\",\n \"FooterSection\",\n \"Form\",\n \"FormField\",\n \"FormSpecimenSection\",\n \"FormSpecimenSlide\",\n \"FullscreenOverlay\",\n \"FullscreenOverlayContent\",\n \"FullscreenOverlayTrigger\",\n \"Heading\",\n \"HeroSection\",\n \"HeroSlide\",\n \"HoverCard\",\n \"HoverCardContent\",\n \"HoverCardTrigger\",\n \"IconGrid\",\n \"IconGridSection\",\n \"IconSizeRow\",\n \"IconsSlide\",\n \"Image\",\n \"Input\",\n \"Kbd\",\n \"Label\",\n \"Lightbox\",\n \"LightboxContent\",\n \"LightboxContext\",\n \"LightboxTrigger\",\n \"LoginForm\",\n \"Marquee\",\n \"MarqueeBandRenderer\",\n \"Menubar\",\n \"MenubarCheckboxItem\",\n \"MenubarContent\",\n \"MenubarGroup\",\n \"MenubarItem\",\n \"MenubarLabel\",\n \"MenubarMenu\",\n \"MenubarRadioGroup\",\n \"MenubarRadioItem\",\n \"MenubarSeparator\",\n \"MenubarShortcut\",\n \"MenubarSub\",\n \"MenubarSubContent\",\n \"MenubarSubTrigger\",\n \"MenubarTrigger\",\n \"MotionDuration\",\n \"MotionDurationSection\",\n \"MotionEasing\",\n \"MotionEasingSection\",\n \"MotionSlide\",\n \"NameRoster\",\n \"NameRosterItem\",\n \"Navbar\",\n \"NavbarBrand\",\n \"NavbarContent\",\n \"NavbarItem\",\n \"NavbarLink\",\n \"NavbarToggle\",\n \"NumberInput\",\n \"OpacityBar\",\n \"OpacitySlide\",\n \"OTPInput\",\n \"PageHeader\",\n \"Pagination\",\n \"PaginationContent\",\n \"PaginationEllipsis\",\n \"PaginationItem\",\n \"PaginationLink\",\n \"PaginationNext\",\n \"PaginationPrevious\",\n \"PasswordInput\",\n \"PhoneInput\",\n \"Popover\",\n \"PopoverAnchor\",\n \"PopoverContent\",\n \"PopoverTrigger\",\n \"PricingSection\",\n \"Progress\",\n \"RadioGroup\",\n \"RadioGroupItem\",\n \"RadiusScale\",\n \"RadiusSection\",\n \"RadiusSlide\",\n \"ScrollArea\",\n \"ScrollBar\",\n \"SearchInput\",\n \"Select\",\n \"SelectContent\",\n \"SelectGroup\",\n \"SelectItem\",\n \"SelectLabel\",\n \"SelectScrollDownButton\",\n \"SelectScrollUpButton\",\n \"SelectSeparator\",\n \"SelectTrigger\",\n \"SelectValue\",\n \"SemanticColorGrid\",\n \"SemanticColorItem\",\n \"SemanticTokensSlide\",\n \"Separator\",\n \"ShadowSection\",\n \"Sheet\",\n \"SheetClose\",\n \"SheetContent\",\n \"SheetDescription\",\n \"SheetFooter\",\n \"SheetHeader\",\n \"SheetOverlay\",\n \"SheetPortal\",\n \"SheetTitle\",\n \"SheetTrigger\",\n \"Sidebar\",\n \"SidebarContent\",\n \"SidebarContext\",\n \"SidebarFooter\",\n \"SidebarGroup\",\n \"SidebarGroupAction\",\n \"SidebarGroupContent\",\n \"SidebarGroupLabel\",\n \"SidebarHeader\",\n \"SidebarInset\",\n \"SidebarMenu\",\n \"SidebarMenuAction\",\n \"SidebarMenuBadge\",\n \"SidebarMenuButton\",\n \"SidebarMenuItem\",\n \"SidebarMenuSub\",\n \"SidebarMenuSubButton\",\n \"SidebarMenuSubItem\",\n \"SidebarProvider\",\n \"SidebarRail\",\n \"SidebarSeparator\",\n \"SidebarTrigger\",\n \"Skeleton\",\n \"Slide\",\n \"SlideHeader\",\n \"Slider\",\n \"SliderControl\",\n \"SlideThemeContext\",\n \"SlideThemeProvider\",\n \"SourceInspector\",\n \"SourceInspectorContext\",\n \"SourceInspectorDevImpl\",\n \"SourceInspectorProvider\",\n \"SourceInspectorRunner\",\n \"SourceInspectorToggle\",\n \"SpacingScale\",\n \"SpacingSection\",\n \"SpacingSlide\",\n \"Sphere\",\n \"SpherePlayground\",\n \"StatCard\",\n \"StatHero\",\n \"StationSpectrum\",\n \"StatusBadge\",\n \"StatusColorsSlide\",\n \"Stepper\",\n \"StepperContext\",\n \"StepperDescription\",\n \"StepperItem\",\n \"StepperSeparator\",\n \"StepperTitle\",\n \"StepperTrigger\",\n \"StepsSection\",\n \"SurfaceRow\",\n \"SurfaceSection\",\n \"Switch\",\n \"Table\",\n \"TableBody\",\n \"TableCaption\",\n \"TableCell\",\n \"TableFooter\",\n \"TableHead\",\n \"TableHeader\",\n \"TableRow\",\n \"Tabs\",\n \"TabsContent\",\n \"TabsList\",\n \"TabsTrigger\",\n \"TagInput\",\n \"TestimonialAttribution\",\n \"TestimonialSection\",\n \"Text\",\n \"Textarea\",\n \"ThemeArchitectureSlide\",\n \"ThemeColorsSlide\",\n \"ThemeSwitcher\",\n \"Timeline\",\n \"TimelineContent\",\n \"TimelineDescription\",\n \"TimelineIcon\",\n \"TimelineItem\",\n \"TimelineTimestamp\",\n \"TimelineTitle\",\n \"TitleSlide\",\n \"Toaster\",\n \"TOCSlide\",\n \"ToggleButton\",\n \"ToggleDevImpl\",\n \"ToggleGroup\",\n \"ToggleGroupContext\",\n \"ToggleGroupItem\",\n \"Tooltip\",\n \"TooltipContent\",\n \"TooltipProvider\",\n \"TooltipTrigger\",\n \"TypeBodySlide\",\n \"TypeDisplaySlide\",\n \"TypeSpecimen\",\n \"TypographySection\",\n \"WorkspaceSwitcher\",\n])\n"
|
|
3647
3828
|
}
|
|
3648
3829
|
]
|
|
3649
3830
|
},
|