@loworbitstudio/visor 1.16.0 → 1.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "version": "0.4.0",
3
- "generated_at": "2026-07-07T00:00:53.434Z",
3
+ "generated_at": "2026-07-08T04:58:15.600Z",
4
4
  "components": {
5
5
  "accessibility-specimen": {
6
6
  "changeType": "current",
package/dist/index.js CHANGED
@@ -35,7 +35,7 @@ function loadManifest() {
35
35
  function findItem(registry, name) {
36
36
  return registry.items.find((item) => item.name === name);
37
37
  }
38
- function resolveTransitiveDeps(registry, names, onWarning) {
38
+ function resolveTransitiveDeps(registry, names, onWarning, includeSuggested = false) {
39
39
  const resolved = /* @__PURE__ */ new Map();
40
40
  const queue = names.map((n) => ({
41
41
  name: n,
@@ -49,10 +49,11 @@ function resolveTransitiveDeps(registry, names, onWarning) {
49
49
  throw new Error(`Registry item "${name}" not found.`);
50
50
  }
51
51
  resolved.set(name, item);
52
- if (item.registryDependencies) {
52
+ const walkDeps = includeSuggested ? [...item.registryDependencies ?? [], ...item.suggestedDependencies ?? []] : item.registryDependencies;
53
+ if (walkDeps) {
53
54
  const childAncestors = new Set(ancestors);
54
55
  childAncestors.add(name);
55
- for (const dep of item.registryDependencies) {
56
+ for (const dep of walkDeps) {
56
57
  if (childAncestors.has(dep)) {
57
58
  onWarning?.(`Circular registry dependency: ${name} \u2192 ${dep}`);
58
59
  } else if (!resolved.has(dep)) {
@@ -63,6 +64,17 @@ function resolveTransitiveDeps(registry, names, onWarning) {
63
64
  }
64
65
  return Array.from(resolved.values());
65
66
  }
67
+ function collectSuggestedDeps(registry, rootNames, resolvedNames) {
68
+ const suggested = /* @__PURE__ */ new Set();
69
+ for (const name of rootNames) {
70
+ const item = findItem(registry, name);
71
+ if (!item?.suggestedDependencies) continue;
72
+ for (const dep of item.suggestedDependencies) {
73
+ if (!resolvedNames.has(dep)) suggested.add(dep);
74
+ }
75
+ }
76
+ return Array.from(suggested).sort();
77
+ }
66
78
  function collectDependencies(items) {
67
79
  const deps = /* @__PURE__ */ new Set();
68
80
  const devDeps = /* @__PURE__ */ new Set();
@@ -2169,6 +2181,7 @@ function runFlutterPubGet(cwd, bin) {
2169
2181
  function addCommand(components, cwd, options = {}) {
2170
2182
  const json = options.json ?? false;
2171
2183
  const dryRun = options.dryRun ?? false;
2184
+ const withSuggested = options.withSuggested ?? false;
2172
2185
  const target = options.target ?? "react";
2173
2186
  const prefix = dryRun ? "[dry-run] " : "";
2174
2187
  let autoInitialized = false;
@@ -2318,9 +2331,14 @@ function addCommand(components, cwd, options = {}) {
2318
2331
  const circularWarnings = [];
2319
2332
  let items;
2320
2333
  try {
2321
- items = resolveTransitiveDeps(targetRegistry, canonicalNames, (msg) => {
2322
- circularWarnings.push(msg);
2323
- });
2334
+ items = resolveTransitiveDeps(
2335
+ targetRegistry,
2336
+ canonicalNames,
2337
+ (msg) => {
2338
+ circularWarnings.push(msg);
2339
+ },
2340
+ withSuggested
2341
+ );
2324
2342
  } catch (error) {
2325
2343
  if (json) {
2326
2344
  const message = error instanceof Error ? error.message : String(error);
@@ -2334,6 +2352,8 @@ function addCommand(components, cwd, options = {}) {
2334
2352
  logger.warn(warning);
2335
2353
  }
2336
2354
  }
2355
+ const resolvedNames = new Set(items.map((i) => i.name));
2356
+ const suggestedAvailable = withSuggested ? [] : collectSuggestedDeps(targetRegistry, canonicalNames, resolvedNames);
2337
2357
  if (!json) {
2338
2358
  logger.info(
2339
2359
  `Resolving ${itemNames.length} item(s) \u2192 ${items.length} total (with dependencies)`
@@ -2501,6 +2521,7 @@ function addCommand(components, cwd, options = {}) {
2501
2521
  autoInitialized,
2502
2522
  requested: itemNames,
2503
2523
  resolved: items.map((i) => i.name),
2524
+ ...suggestedAvailable.length > 0 ? { suggested: suggestedAvailable } : {},
2504
2525
  files: { written: writtenFiles, skipped: skippedFiles },
2505
2526
  dependencies: { installed: installedDeps, failed: failedDeps },
2506
2527
  warnings
@@ -2511,6 +2532,17 @@ function addCommand(components, cwd, options = {}) {
2511
2532
  );
2512
2533
  process.exit(failedDeps.length > 0 ? 1 : 0);
2513
2534
  }
2535
+ if (suggestedAvailable.length > 0) {
2536
+ logger.blank();
2537
+ logger.info(
2538
+ `Suggested slot-fill components (not installed): ${suggestedAvailable.join(", ")}`
2539
+ );
2540
+ logger.info(
2541
+ ` These fill the block's slots but aren't required to render it. Add with:`
2542
+ );
2543
+ logger.info(` npx visor add ${canonicalNames.join(" ")} --block --with-suggested`);
2544
+ logger.info(` or: npx visor add ${suggestedAvailable.join(" ")}`);
2545
+ }
2514
2546
  if (failedDeps.length > 0) {
2515
2547
  process.exit(1);
2516
2548
  }
@@ -8154,9 +8186,9 @@ program.command("init").description("Initialize Visor \u2014 with --template nex
8154
8186
  program.command("list").description("List all available registry items").option("--json", "output structured JSON (for AI agents)").option("--category <name>", "filter items by category").action((options) => {
8155
8187
  listCommand(process.cwd(), options);
8156
8188
  });
8157
- program.command("add").description("Add components, hooks, blocks, or utilities to your project").argument("[items...]", "names of registry items to add").option("--overwrite", "overwrite existing files", false).option("--category <name>", "install all items from a category").option("--block", "install blocks instead of components").option("--target <platform>", "target platform: react (default) or flutter", "react").option("--dry-run", "preview what would be added without writing files").option("--json", "output structured JSON (for AI agents)").action((items, options) => {
8189
+ program.command("add").description("Add components, hooks, blocks, or utilities to your project").argument("[items...]", "names of registry items to add").option("--overwrite", "overwrite existing files", false).option("--category <name>", "install all items from a category").option("--block", "install blocks instead of components").option("--with-suggested", "also install a block's suggested slot-fill components (e.g. breadcrumb, dropdown-menu, sidebar for admin-shell)").option("--target <platform>", "target platform: react (default) or flutter", "react").option("--dry-run", "preview what would be added without writing files").option("--json", "output structured JSON (for AI agents)").action((items, options) => {
8158
8190
  const target = options.target === "flutter" ? "flutter" : "react";
8159
- addCommand(items, process.cwd(), { overwrite: options.overwrite, category: options.category, block: options.block, target, dryRun: options.dryRun, json: options.json });
8191
+ addCommand(items, process.cwd(), { overwrite: options.overwrite, category: options.category, block: options.block, withSuggested: options.withSuggested, target, dryRun: options.dryRun, json: options.json });
8160
8192
  });
8161
8193
  program.command("diff").description(
8162
8194
  "Show differences between local files and the registry"
@@ -903,7 +903,7 @@
903
903
  {
904
904
  "path": "components/ui/sidebar/sidebar.tsx",
905
905
  "type": "registry:ui",
906
- "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\nimport { SidebarIcon } from \"@phosphor-icons/react\"\nimport { cn } from \"../../../lib/utils\"\nimport styles from \"./sidebar.module.css\"\n\nconst SIDEBAR_COOKIE_NAME = \"sidebar_state\"\nconst SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7\nconst SIDEBAR_WIDTH = \"16rem\"\nconst SIDEBAR_WIDTH_MOBILE = \"18rem\"\nconst SIDEBAR_WIDTH_ICON = \"3rem\"\nconst SIDEBAR_KEYBOARD_SHORTCUT = \"b\"\n\ntype SidebarContextProps = {\n state: \"expanded\" | \"collapsed\"\n open: boolean\n setOpen: (open: boolean) => void\n openMobile: boolean\n setOpenMobile: (open: boolean) => void\n isMobile: boolean\n toggleSidebar: () => void\n}\n\nconst SidebarContext = React.createContext<SidebarContextProps | null>(null)\n\nfunction useSidebar() {\n const context = React.useContext(SidebarContext)\n if (!context) {\n throw new Error(\"useSidebar must be used within a SidebarProvider.\")\n }\n return context\n}\n\nexport interface SidebarProviderProps extends React.ComponentProps<\"div\"> {\n defaultOpen?: boolean\n open?: boolean\n onOpenChange?: (open: boolean) => void\n}\n\nconst SidebarProvider = React.forwardRef<HTMLDivElement, SidebarProviderProps>(\n (\n {\n defaultOpen = true,\n open: openProp,\n onOpenChange: setOpenProp,\n className,\n style,\n children,\n ...props\n },\n ref\n ) => {\n const [isMobile, setIsMobile] = React.useState(false)\n const [openMobile, setOpenMobile] = React.useState(false)\n const [_open, _setOpen] = React.useState(defaultOpen)\n const open = openProp ?? _open\n\n React.useEffect(() => {\n const mql = window.matchMedia(\"(max-width: 768px)\")\n const handleChange = (e: MediaQueryListEvent) => setIsMobile(e.matches)\n setIsMobile(mql.matches)\n mql.addEventListener(\"change\", handleChange)\n return () => mql.removeEventListener(\"change\", handleChange)\n }, [])\n\n const setOpen = React.useCallback(\n (value: boolean | ((value: boolean) => boolean)) => {\n const openState = typeof value === \"function\" ? value(open) : value\n if (setOpenProp) {\n setOpenProp(openState)\n } else {\n _setOpen(openState)\n }\n document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`\n },\n [setOpenProp, open]\n )\n\n const toggleSidebar = React.useCallback(() => {\n return isMobile\n ? setOpenMobile((prev) => !prev)\n : setOpen((prev) => !prev)\n }, [isMobile, setOpen, setOpenMobile])\n\n React.useEffect(() => {\n const handleKeyDown = (event: KeyboardEvent) => {\n if (event.key === SIDEBAR_KEYBOARD_SHORTCUT && (event.metaKey || event.ctrlKey)) {\n event.preventDefault()\n toggleSidebar()\n }\n }\n window.addEventListener(\"keydown\", handleKeyDown)\n return () => window.removeEventListener(\"keydown\", handleKeyDown)\n }, [toggleSidebar])\n\n const state = open ? \"expanded\" : \"collapsed\"\n\n const contextValue = React.useMemo<SidebarContextProps>(\n () => ({\n state,\n open,\n setOpen,\n isMobile,\n openMobile,\n setOpenMobile,\n toggleSidebar,\n }),\n [state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar]\n )\n\n return (\n <SidebarContext.Provider value={contextValue}>\n <div\n ref={ref}\n data-slot=\"sidebar-wrapper\"\n style={\n {\n \"--sidebar-width\": SIDEBAR_WIDTH,\n \"--sidebar-width-mobile\": SIDEBAR_WIDTH_MOBILE,\n \"--sidebar-width-icon\": SIDEBAR_WIDTH_ICON,\n ...style,\n } as React.CSSProperties\n }\n className={cn(styles.wrapper, className)}\n {...props}\n >\n {children}\n </div>\n </SidebarContext.Provider>\n )\n }\n)\nSidebarProvider.displayName = \"SidebarProvider\"\n\nexport interface SidebarProps extends React.ComponentProps<\"div\"> {\n side?: \"left\" | \"right\"\n variant?: \"sidebar\" | \"floating\" | \"inset\"\n collapsible?: \"offcanvas\" | \"icon\" | \"none\"\n}\n\nconst Sidebar = React.forwardRef<HTMLDivElement, SidebarProps>(\n (\n {\n side = \"left\",\n variant = \"sidebar\",\n collapsible = \"offcanvas\",\n className,\n children,\n ...props\n },\n ref\n ) => {\n const { state } = useSidebar()\n\n if (collapsible === \"none\") {\n return (\n <div\n ref={ref}\n data-slot=\"sidebar\"\n className={cn(styles.sidebarStatic, className)}\n {...props}\n >\n {children}\n </div>\n )\n }\n\n return (\n <div\n ref={ref}\n className={cn(styles.sidebarContainer, className)}\n data-state={state}\n data-collapsible={state === \"collapsed\" ? collapsible : \"\"}\n data-variant={variant}\n data-side={side}\n data-slot=\"sidebar\"\n {...props}\n >\n <div\n data-slot=\"sidebar-gap\"\n className={styles.sidebarGap}\n />\n <div\n data-slot=\"sidebar-inner\"\n data-side={side}\n className={styles.sidebarInner}\n >\n <div\n data-sidebar=\"sidebar\"\n className={styles.sidebarContent}\n >\n {children}\n </div>\n </div>\n </div>\n )\n }\n)\nSidebar.displayName = \"Sidebar\"\n\nconst SidebarTrigger = React.forwardRef<\n HTMLButtonElement,\n React.ComponentProps<\"button\">\n>(({ className, onClick, children, ...props }, ref) => {\n const { toggleSidebar } = useSidebar()\n\n return (\n <button\n ref={ref}\n data-sidebar=\"trigger\"\n data-slot=\"sidebar-trigger\"\n className={cn(styles.trigger, className)}\n onClick={(event) => {\n onClick?.(event)\n toggleSidebar()\n }}\n {...props}\n >\n {children ?? (\n <>\n <SidebarIcon />\n <span className={styles.srOnly}>Toggle Sidebar</span>\n </>\n )}\n </button>\n )\n})\nSidebarTrigger.displayName = \"SidebarTrigger\"\n\nconst SidebarRail = React.forwardRef<HTMLButtonElement, React.ComponentProps<\"button\">>(\n ({ className, ...props }, ref) => {\n const { toggleSidebar } = useSidebar()\n\n return (\n <button\n ref={ref}\n data-sidebar=\"rail\"\n data-slot=\"sidebar-rail\"\n aria-label=\"Toggle Sidebar\"\n tabIndex={-1}\n onClick={toggleSidebar}\n title=\"Toggle Sidebar\"\n className={cn(styles.rail, className)}\n {...props}\n />\n )\n }\n)\nSidebarRail.displayName = \"SidebarRail\"\n\nconst SidebarInset = React.forwardRef<HTMLElement, React.ComponentProps<\"main\">>(\n ({ className, ...props }, ref) => (\n <main\n ref={ref}\n data-slot=\"sidebar-inset\"\n className={cn(styles.inset, className)}\n {...props}\n />\n )\n)\nSidebarInset.displayName = \"SidebarInset\"\n\nconst SidebarHeader = React.forwardRef<HTMLDivElement, React.ComponentProps<\"div\">>(\n ({ className, ...props }, ref) => (\n <div\n ref={ref}\n data-slot=\"sidebar-header\"\n data-sidebar=\"header\"\n className={cn(styles.header, className)}\n {...props}\n />\n )\n)\nSidebarHeader.displayName = \"SidebarHeader\"\n\nconst SidebarFooter = React.forwardRef<HTMLDivElement, React.ComponentProps<\"div\">>(\n ({ className, ...props }, ref) => (\n <div\n ref={ref}\n data-slot=\"sidebar-footer\"\n data-sidebar=\"footer\"\n className={cn(styles.footer, className)}\n {...props}\n />\n )\n)\nSidebarFooter.displayName = \"SidebarFooter\"\n\nconst SidebarSeparator = React.forwardRef<HTMLHRElement, React.ComponentProps<\"hr\">>(\n ({ className, ...props }, ref) => (\n <hr\n ref={ref}\n data-slot=\"sidebar-separator\"\n data-sidebar=\"separator\"\n className={cn(styles.separator, className)}\n {...props}\n />\n )\n)\nSidebarSeparator.displayName = \"SidebarSeparator\"\n\nconst SidebarContent = React.forwardRef<HTMLDivElement, React.ComponentProps<\"div\">>(\n ({ className, ...props }, ref) => (\n <div\n ref={ref}\n data-slot=\"sidebar-content\"\n data-sidebar=\"content\"\n className={cn(styles.contentArea, className)}\n {...props}\n />\n )\n)\nSidebarContent.displayName = \"SidebarContent\"\n\nconst SidebarGroup = React.forwardRef<HTMLDivElement, React.ComponentProps<\"div\">>(\n ({ className, ...props }, ref) => (\n <div\n ref={ref}\n data-slot=\"sidebar-group\"\n data-sidebar=\"group\"\n className={cn(styles.group, className)}\n {...props}\n />\n )\n)\nSidebarGroup.displayName = \"SidebarGroup\"\n\nconst SidebarGroupLabel = React.forwardRef<HTMLDivElement, React.ComponentProps<\"div\">>(\n ({ className, ...props }, ref) => (\n <div\n ref={ref}\n data-slot=\"sidebar-group-label\"\n data-sidebar=\"group-label\"\n className={cn(styles.groupLabel, className)}\n {...props}\n />\n )\n)\nSidebarGroupLabel.displayName = \"SidebarGroupLabel\"\n\nconst SidebarGroupAction = React.forwardRef<HTMLButtonElement, React.ComponentProps<\"button\">>(\n ({ className, ...props }, ref) => (\n <button\n ref={ref}\n data-slot=\"sidebar-group-action\"\n data-sidebar=\"group-action\"\n className={cn(styles.groupAction, className)}\n {...props}\n />\n )\n)\nSidebarGroupAction.displayName = \"SidebarGroupAction\"\n\nconst SidebarGroupContent = React.forwardRef<HTMLDivElement, React.ComponentProps<\"div\">>(\n ({ className, ...props }, ref) => (\n <div\n ref={ref}\n data-slot=\"sidebar-group-content\"\n data-sidebar=\"group-content\"\n className={cn(styles.groupContent, className)}\n {...props}\n />\n )\n)\nSidebarGroupContent.displayName = \"SidebarGroupContent\"\n\nconst SidebarMenu = React.forwardRef<HTMLUListElement, React.ComponentProps<\"ul\">>(\n ({ className, ...props }, ref) => (\n <ul\n ref={ref}\n data-slot=\"sidebar-menu\"\n data-sidebar=\"menu\"\n className={cn(styles.menu, className)}\n {...props}\n />\n )\n)\nSidebarMenu.displayName = \"SidebarMenu\"\n\nconst SidebarMenuItem = React.forwardRef<HTMLLIElement, React.ComponentProps<\"li\">>(\n ({ className, ...props }, ref) => (\n <li\n ref={ref}\n data-slot=\"sidebar-menu-item\"\n data-sidebar=\"menu-item\"\n className={cn(styles.menuItem, className)}\n {...props}\n />\n )\n)\nSidebarMenuItem.displayName = \"SidebarMenuItem\"\n\nconst sidebarMenuButtonVariants = cva(styles.menuButton, {\n variants: {\n variant: {\n default: styles.menuButtonDefault,\n outline: styles.menuButtonOutline,\n },\n size: {\n default: styles.menuButtonSizeDefault,\n sm: styles.menuButtonSizeSm,\n lg: styles.menuButtonSizeLg,\n },\n },\n defaultVariants: {\n variant: \"default\",\n size: \"default\",\n },\n})\n\nexport interface SidebarMenuButtonProps\n extends React.ComponentProps<\"button\">,\n VariantProps<typeof sidebarMenuButtonVariants> {\n asChild?: boolean\n isActive?: boolean\n tooltip?: string\n}\n\nconst SidebarMenuButton = React.forwardRef<HTMLButtonElement, SidebarMenuButtonProps>(\n ({ className, variant = \"default\", size = \"default\", isActive, children, ...props }, ref) => (\n <button\n ref={ref}\n data-slot=\"sidebar-menu-button\"\n data-sidebar=\"menu-button\"\n data-size={size}\n data-active={isActive || undefined}\n className={cn(sidebarMenuButtonVariants({ variant, size }), className)}\n {...props}\n >\n {children}\n </button>\n )\n)\nSidebarMenuButton.displayName = \"SidebarMenuButton\"\n\nconst SidebarMenuAction = React.forwardRef<\n HTMLButtonElement,\n React.ComponentProps<\"button\"> & { showOnHover?: boolean }\n>(({ className, showOnHover, ...props }, ref) => (\n <button\n ref={ref}\n data-slot=\"sidebar-menu-action\"\n data-sidebar=\"menu-action\"\n className={cn(\n styles.menuAction,\n showOnHover && styles.menuActionHover,\n className\n )}\n {...props}\n />\n))\nSidebarMenuAction.displayName = \"SidebarMenuAction\"\n\nconst SidebarMenuBadge = React.forwardRef<HTMLDivElement, React.ComponentProps<\"div\">>(\n ({ className, ...props }, ref) => (\n <div\n ref={ref}\n data-slot=\"sidebar-menu-badge\"\n data-sidebar=\"menu-badge\"\n className={cn(styles.menuBadge, className)}\n {...props}\n />\n )\n)\nSidebarMenuBadge.displayName = \"SidebarMenuBadge\"\n\nconst SidebarMenuSub = React.forwardRef<HTMLUListElement, React.ComponentProps<\"ul\">>(\n ({ className, ...props }, ref) => (\n <ul\n ref={ref}\n data-slot=\"sidebar-menu-sub\"\n data-sidebar=\"menu-sub\"\n className={cn(styles.menuSub, className)}\n {...props}\n />\n )\n)\nSidebarMenuSub.displayName = \"SidebarMenuSub\"\n\nconst SidebarMenuSubItem = React.forwardRef<HTMLLIElement, React.ComponentProps<\"li\">>(\n ({ className, ...props }, ref) => (\n <li\n ref={ref}\n data-slot=\"sidebar-menu-sub-item\"\n data-sidebar=\"menu-sub-item\"\n className={cn(styles.menuSubItem, className)}\n {...props}\n />\n )\n)\nSidebarMenuSubItem.displayName = \"SidebarMenuSubItem\"\n\nexport interface SidebarMenuSubButtonProps extends React.ComponentProps<\"a\"> {\n asChild?: boolean\n size?: \"sm\" | \"md\"\n isActive?: boolean\n}\n\nconst SidebarMenuSubButton = React.forwardRef<HTMLAnchorElement, SidebarMenuSubButtonProps>(\n ({ className, size = \"md\", isActive, ...props }, ref) => (\n <a\n ref={ref}\n data-slot=\"sidebar-menu-sub-button\"\n data-sidebar=\"menu-sub-button\"\n data-size={size}\n data-active={isActive || undefined}\n className={cn(\n styles.menuSubButton,\n size === \"sm\" && styles.menuSubButtonSm,\n className\n )}\n {...props}\n />\n )\n)\nSidebarMenuSubButton.displayName = \"SidebarMenuSubButton\"\n\nexport {\n Sidebar,\n SidebarContent,\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 useSidebar,\n sidebarMenuButtonVariants,\n}\n"
906
+ "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\nimport { SidebarIcon } from \"@phosphor-icons/react\"\nimport { cn } from \"../../../lib/utils\"\nimport styles from \"./sidebar.module.css\"\n\nconst SIDEBAR_COOKIE_NAME = \"sidebar_state\"\nconst SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7\nconst SIDEBAR_WIDTH = \"16rem\"\nconst SIDEBAR_WIDTH_MOBILE = \"18rem\"\nconst SIDEBAR_WIDTH_ICON = \"3rem\"\nconst SIDEBAR_KEYBOARD_SHORTCUT = \"b\"\nconst SIDEBAR_MOBILE_MEDIA_QUERY = \"(max-width: 768px)\"\n\n// `isMobile` is derived from a `matchMedia` subscription via\n// `useSyncExternalStore` (below) instead of a `useState` + `useEffect` pair.\n// This keeps SSR safe — the server snapshot is always `false` — while avoiding\n// a synchronous `setState` inside an effect body, which the React 19 compiler\n// lint (`react-hooks/set-state-in-effect`) flags as a cascading-render hazard.\nfunction subscribeIsMobile(callback: () => void) {\n const mql = window.matchMedia(SIDEBAR_MOBILE_MEDIA_QUERY)\n mql.addEventListener(\"change\", callback)\n return () => mql.removeEventListener(\"change\", callback)\n}\n\nfunction getIsMobileSnapshot() {\n return window.matchMedia(SIDEBAR_MOBILE_MEDIA_QUERY).matches\n}\n\nfunction getIsMobileServerSnapshot() {\n return false\n}\n\ntype SidebarContextProps = {\n state: \"expanded\" | \"collapsed\"\n open: boolean\n setOpen: (open: boolean) => void\n openMobile: boolean\n setOpenMobile: (open: boolean) => void\n isMobile: boolean\n toggleSidebar: () => void\n}\n\nconst SidebarContext = React.createContext<SidebarContextProps | null>(null)\n\nfunction useSidebar() {\n const context = React.useContext(SidebarContext)\n if (!context) {\n throw new Error(\"useSidebar must be used within a SidebarProvider.\")\n }\n return context\n}\n\nexport interface SidebarProviderProps extends React.ComponentProps<\"div\"> {\n defaultOpen?: boolean\n open?: boolean\n onOpenChange?: (open: boolean) => void\n}\n\nconst SidebarProvider = React.forwardRef<HTMLDivElement, SidebarProviderProps>(\n (\n {\n defaultOpen = true,\n open: openProp,\n onOpenChange: setOpenProp,\n className,\n style,\n children,\n ...props\n },\n ref\n ) => {\n const isMobile = React.useSyncExternalStore(\n subscribeIsMobile,\n getIsMobileSnapshot,\n getIsMobileServerSnapshot\n )\n const [openMobile, setOpenMobile] = React.useState(false)\n const [_open, _setOpen] = React.useState(defaultOpen)\n const open = openProp ?? _open\n\n const setOpen = React.useCallback(\n (value: boolean | ((value: boolean) => boolean)) => {\n const openState = typeof value === \"function\" ? value(open) : value\n if (setOpenProp) {\n setOpenProp(openState)\n } else {\n _setOpen(openState)\n }\n document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`\n },\n [setOpenProp, open]\n )\n\n const toggleSidebar = React.useCallback(() => {\n return isMobile\n ? setOpenMobile((prev) => !prev)\n : setOpen((prev) => !prev)\n }, [isMobile, setOpen, setOpenMobile])\n\n React.useEffect(() => {\n const handleKeyDown = (event: KeyboardEvent) => {\n if (event.key === SIDEBAR_KEYBOARD_SHORTCUT && (event.metaKey || event.ctrlKey)) {\n event.preventDefault()\n toggleSidebar()\n }\n }\n window.addEventListener(\"keydown\", handleKeyDown)\n return () => window.removeEventListener(\"keydown\", handleKeyDown)\n }, [toggleSidebar])\n\n const state = open ? \"expanded\" : \"collapsed\"\n\n const contextValue = React.useMemo<SidebarContextProps>(\n () => ({\n state,\n open,\n setOpen,\n isMobile,\n openMobile,\n setOpenMobile,\n toggleSidebar,\n }),\n [state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar]\n )\n\n return (\n <SidebarContext.Provider value={contextValue}>\n <div\n ref={ref}\n data-slot=\"sidebar-wrapper\"\n style={\n {\n \"--sidebar-width\": SIDEBAR_WIDTH,\n \"--sidebar-width-mobile\": SIDEBAR_WIDTH_MOBILE,\n \"--sidebar-width-icon\": SIDEBAR_WIDTH_ICON,\n ...style,\n } as React.CSSProperties\n }\n className={cn(styles.wrapper, className)}\n {...props}\n >\n {children}\n </div>\n </SidebarContext.Provider>\n )\n }\n)\nSidebarProvider.displayName = \"SidebarProvider\"\n\nexport interface SidebarProps extends React.ComponentProps<\"div\"> {\n side?: \"left\" | \"right\"\n variant?: \"sidebar\" | \"floating\" | \"inset\"\n collapsible?: \"offcanvas\" | \"icon\" | \"none\"\n}\n\nconst Sidebar = React.forwardRef<HTMLDivElement, SidebarProps>(\n (\n {\n side = \"left\",\n variant = \"sidebar\",\n collapsible = \"offcanvas\",\n className,\n children,\n ...props\n },\n ref\n ) => {\n const { state } = useSidebar()\n\n if (collapsible === \"none\") {\n return (\n <div\n ref={ref}\n data-slot=\"sidebar\"\n className={cn(styles.sidebarStatic, className)}\n {...props}\n >\n {children}\n </div>\n )\n }\n\n return (\n <div\n ref={ref}\n className={cn(styles.sidebarContainer, className)}\n data-state={state}\n data-collapsible={state === \"collapsed\" ? collapsible : \"\"}\n data-variant={variant}\n data-side={side}\n data-slot=\"sidebar\"\n {...props}\n >\n <div\n data-slot=\"sidebar-gap\"\n className={styles.sidebarGap}\n />\n <div\n data-slot=\"sidebar-inner\"\n data-side={side}\n className={styles.sidebarInner}\n >\n <div\n data-sidebar=\"sidebar\"\n className={styles.sidebarContent}\n >\n {children}\n </div>\n </div>\n </div>\n )\n }\n)\nSidebar.displayName = \"Sidebar\"\n\nconst SidebarTrigger = React.forwardRef<\n HTMLButtonElement,\n React.ComponentProps<\"button\">\n>(({ className, onClick, children, ...props }, ref) => {\n const { toggleSidebar } = useSidebar()\n\n return (\n <button\n ref={ref}\n data-sidebar=\"trigger\"\n data-slot=\"sidebar-trigger\"\n className={cn(styles.trigger, className)}\n onClick={(event) => {\n onClick?.(event)\n toggleSidebar()\n }}\n {...props}\n >\n {children ?? (\n <>\n <SidebarIcon />\n <span className={styles.srOnly}>Toggle Sidebar</span>\n </>\n )}\n </button>\n )\n})\nSidebarTrigger.displayName = \"SidebarTrigger\"\n\nconst SidebarRail = React.forwardRef<HTMLButtonElement, React.ComponentProps<\"button\">>(\n ({ className, ...props }, ref) => {\n const { toggleSidebar } = useSidebar()\n\n return (\n <button\n ref={ref}\n data-sidebar=\"rail\"\n data-slot=\"sidebar-rail\"\n aria-label=\"Toggle Sidebar\"\n tabIndex={-1}\n onClick={toggleSidebar}\n title=\"Toggle Sidebar\"\n className={cn(styles.rail, className)}\n {...props}\n />\n )\n }\n)\nSidebarRail.displayName = \"SidebarRail\"\n\nconst SidebarInset = React.forwardRef<HTMLElement, React.ComponentProps<\"main\">>(\n ({ className, ...props }, ref) => (\n <main\n ref={ref}\n data-slot=\"sidebar-inset\"\n className={cn(styles.inset, className)}\n {...props}\n />\n )\n)\nSidebarInset.displayName = \"SidebarInset\"\n\nconst SidebarHeader = React.forwardRef<HTMLDivElement, React.ComponentProps<\"div\">>(\n ({ className, ...props }, ref) => (\n <div\n ref={ref}\n data-slot=\"sidebar-header\"\n data-sidebar=\"header\"\n className={cn(styles.header, className)}\n {...props}\n />\n )\n)\nSidebarHeader.displayName = \"SidebarHeader\"\n\nconst SidebarFooter = React.forwardRef<HTMLDivElement, React.ComponentProps<\"div\">>(\n ({ className, ...props }, ref) => (\n <div\n ref={ref}\n data-slot=\"sidebar-footer\"\n data-sidebar=\"footer\"\n className={cn(styles.footer, className)}\n {...props}\n />\n )\n)\nSidebarFooter.displayName = \"SidebarFooter\"\n\nconst SidebarSeparator = React.forwardRef<HTMLHRElement, React.ComponentProps<\"hr\">>(\n ({ className, ...props }, ref) => (\n <hr\n ref={ref}\n data-slot=\"sidebar-separator\"\n data-sidebar=\"separator\"\n className={cn(styles.separator, className)}\n {...props}\n />\n )\n)\nSidebarSeparator.displayName = \"SidebarSeparator\"\n\nconst SidebarContent = React.forwardRef<HTMLDivElement, React.ComponentProps<\"div\">>(\n ({ className, ...props }, ref) => (\n <div\n ref={ref}\n data-slot=\"sidebar-content\"\n data-sidebar=\"content\"\n className={cn(styles.contentArea, className)}\n {...props}\n />\n )\n)\nSidebarContent.displayName = \"SidebarContent\"\n\nconst SidebarGroup = React.forwardRef<HTMLDivElement, React.ComponentProps<\"div\">>(\n ({ className, ...props }, ref) => (\n <div\n ref={ref}\n data-slot=\"sidebar-group\"\n data-sidebar=\"group\"\n className={cn(styles.group, className)}\n {...props}\n />\n )\n)\nSidebarGroup.displayName = \"SidebarGroup\"\n\nconst SidebarGroupLabel = React.forwardRef<HTMLDivElement, React.ComponentProps<\"div\">>(\n ({ className, ...props }, ref) => (\n <div\n ref={ref}\n data-slot=\"sidebar-group-label\"\n data-sidebar=\"group-label\"\n className={cn(styles.groupLabel, className)}\n {...props}\n />\n )\n)\nSidebarGroupLabel.displayName = \"SidebarGroupLabel\"\n\nconst SidebarGroupAction = React.forwardRef<HTMLButtonElement, React.ComponentProps<\"button\">>(\n ({ className, ...props }, ref) => (\n <button\n ref={ref}\n data-slot=\"sidebar-group-action\"\n data-sidebar=\"group-action\"\n className={cn(styles.groupAction, className)}\n {...props}\n />\n )\n)\nSidebarGroupAction.displayName = \"SidebarGroupAction\"\n\nconst SidebarGroupContent = React.forwardRef<HTMLDivElement, React.ComponentProps<\"div\">>(\n ({ className, ...props }, ref) => (\n <div\n ref={ref}\n data-slot=\"sidebar-group-content\"\n data-sidebar=\"group-content\"\n className={cn(styles.groupContent, className)}\n {...props}\n />\n )\n)\nSidebarGroupContent.displayName = \"SidebarGroupContent\"\n\nconst SidebarMenu = React.forwardRef<HTMLUListElement, React.ComponentProps<\"ul\">>(\n ({ className, ...props }, ref) => (\n <ul\n ref={ref}\n data-slot=\"sidebar-menu\"\n data-sidebar=\"menu\"\n className={cn(styles.menu, className)}\n {...props}\n />\n )\n)\nSidebarMenu.displayName = \"SidebarMenu\"\n\nconst SidebarMenuItem = React.forwardRef<HTMLLIElement, React.ComponentProps<\"li\">>(\n ({ className, ...props }, ref) => (\n <li\n ref={ref}\n data-slot=\"sidebar-menu-item\"\n data-sidebar=\"menu-item\"\n className={cn(styles.menuItem, className)}\n {...props}\n />\n )\n)\nSidebarMenuItem.displayName = \"SidebarMenuItem\"\n\nconst sidebarMenuButtonVariants = cva(styles.menuButton, {\n variants: {\n variant: {\n default: styles.menuButtonDefault,\n outline: styles.menuButtonOutline,\n },\n size: {\n default: styles.menuButtonSizeDefault,\n sm: styles.menuButtonSizeSm,\n lg: styles.menuButtonSizeLg,\n },\n },\n defaultVariants: {\n variant: \"default\",\n size: \"default\",\n },\n})\n\nexport interface SidebarMenuButtonProps\n extends React.ComponentProps<\"button\">,\n VariantProps<typeof sidebarMenuButtonVariants> {\n asChild?: boolean\n isActive?: boolean\n tooltip?: string\n}\n\nconst SidebarMenuButton = React.forwardRef<HTMLButtonElement, SidebarMenuButtonProps>(\n ({ className, variant = \"default\", size = \"default\", isActive, children, ...props }, ref) => (\n <button\n ref={ref}\n data-slot=\"sidebar-menu-button\"\n data-sidebar=\"menu-button\"\n data-size={size}\n data-active={isActive || undefined}\n className={cn(sidebarMenuButtonVariants({ variant, size }), className)}\n {...props}\n >\n {children}\n </button>\n )\n)\nSidebarMenuButton.displayName = \"SidebarMenuButton\"\n\nconst SidebarMenuAction = React.forwardRef<\n HTMLButtonElement,\n React.ComponentProps<\"button\"> & { showOnHover?: boolean }\n>(({ className, showOnHover, ...props }, ref) => (\n <button\n ref={ref}\n data-slot=\"sidebar-menu-action\"\n data-sidebar=\"menu-action\"\n className={cn(\n styles.menuAction,\n showOnHover && styles.menuActionHover,\n className\n )}\n {...props}\n />\n))\nSidebarMenuAction.displayName = \"SidebarMenuAction\"\n\nconst SidebarMenuBadge = React.forwardRef<HTMLDivElement, React.ComponentProps<\"div\">>(\n ({ className, ...props }, ref) => (\n <div\n ref={ref}\n data-slot=\"sidebar-menu-badge\"\n data-sidebar=\"menu-badge\"\n className={cn(styles.menuBadge, className)}\n {...props}\n />\n )\n)\nSidebarMenuBadge.displayName = \"SidebarMenuBadge\"\n\nconst SidebarMenuSub = React.forwardRef<HTMLUListElement, React.ComponentProps<\"ul\">>(\n ({ className, ...props }, ref) => (\n <ul\n ref={ref}\n data-slot=\"sidebar-menu-sub\"\n data-sidebar=\"menu-sub\"\n className={cn(styles.menuSub, className)}\n {...props}\n />\n )\n)\nSidebarMenuSub.displayName = \"SidebarMenuSub\"\n\nconst SidebarMenuSubItem = React.forwardRef<HTMLLIElement, React.ComponentProps<\"li\">>(\n ({ className, ...props }, ref) => (\n <li\n ref={ref}\n data-slot=\"sidebar-menu-sub-item\"\n data-sidebar=\"menu-sub-item\"\n className={cn(styles.menuSubItem, className)}\n {...props}\n />\n )\n)\nSidebarMenuSubItem.displayName = \"SidebarMenuSubItem\"\n\nexport interface SidebarMenuSubButtonProps extends React.ComponentProps<\"a\"> {\n asChild?: boolean\n size?: \"sm\" | \"md\"\n isActive?: boolean\n}\n\nconst SidebarMenuSubButton = React.forwardRef<HTMLAnchorElement, SidebarMenuSubButtonProps>(\n ({ className, size = \"md\", isActive, ...props }, ref) => (\n <a\n ref={ref}\n data-slot=\"sidebar-menu-sub-button\"\n data-sidebar=\"menu-sub-button\"\n data-size={size}\n data-active={isActive || undefined}\n className={cn(\n styles.menuSubButton,\n size === \"sm\" && styles.menuSubButtonSm,\n className\n )}\n {...props}\n />\n )\n)\nSidebarMenuSubButton.displayName = \"SidebarMenuSubButton\"\n\nexport {\n Sidebar,\n SidebarContent,\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 useSidebar,\n sidebarMenuButtonVariants,\n}\n"
907
907
  },
908
908
  {
909
909
  "path": "components/ui/sidebar/sidebar.module.css",
@@ -2447,7 +2447,7 @@
2447
2447
  {
2448
2448
  "path": "components/ui/bulk-action-bar/bulk-action-bar.tsx",
2449
2449
  "type": "registry:ui",
2450
- "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { X } from \"@phosphor-icons/react\"\nimport { cn } from \"../../../lib/utils\"\nimport { Button } from \"../button/button\"\nimport styles from \"./bulk-action-bar.module.css\"\n\nexport interface BulkActionBarProps\n extends React.HTMLAttributes<HTMLDivElement> {\n /** Number of selected items. Bar renders only when count > 0. */\n count: number\n /** Action buttons cluster — typically one or more `<Button>` instances. */\n children: React.ReactNode\n\n /** Render inline (non-sticky) instead of fixed to the viewport bottom. */\n inline?: boolean\n /** Render flat: no shadow, no radius, border-top only. Use inside table cards or panels. */\n flat?: boolean\n /** Selection label renderer. Defaults to `(n) => `${n} selected``. */\n label?: (count: number) => React.ReactNode\n /** Aria-label and tooltip for the dismiss button. Defaults to \"Clear selection\". */\n clearLabel?: React.ReactNode\n\n /** Fired by the Escape key and the dismiss button. */\n onClear?: () => void\n /** Show the dismiss (X) button. Defaults to `true`. */\n dismissible?: boolean\n /** Auto-focus the first action button on mount. Defaults to `true`. */\n autoFocus?: boolean\n}\n\nconst defaultLabel = (count: number) => `${count} selected`\n\nconst BulkActionBar = React.forwardRef<HTMLDivElement, BulkActionBarProps>(\n (\n {\n className,\n count,\n children,\n inline = false,\n flat = false,\n label = defaultLabel,\n clearLabel = \"Clear selection\",\n onClear,\n dismissible = true,\n autoFocus = true,\n ...props\n },\n ref\n ) => {\n const actionsRef = React.useRef<HTMLDivElement>(null)\n\n // Escape-to-clear — attached only when `onClear` is provided.\n React.useEffect(() => {\n if (!onClear) return\n const handleKeyDown = (event: KeyboardEvent) => {\n if (event.key === \"Escape\") {\n onClear()\n }\n }\n document.addEventListener(\"keydown\", handleKeyDown)\n return () => {\n document.removeEventListener(\"keydown\", handleKeyDown)\n }\n }, [onClear])\n\n // Auto-focus first action button on mount (not on every count change).\n // eslint-disable-next-line react-hooks/exhaustive-deps\n React.useEffect(() => {\n if (!autoFocus) return\n const root = actionsRef.current\n if (!root) return\n const firstButton = root.querySelector<HTMLButtonElement>(\n \"button:not([disabled])\"\n )\n firstButton?.focus()\n }, [])\n\n if (count <= 0) return null\n\n const showDismiss = dismissible && typeof onClear === \"function\"\n\n return (\n <div\n ref={ref}\n role=\"toolbar\"\n aria-label=\"Bulk actions\"\n data-slot=\"bulk-action-bar\"\n data-inline={inline ? \"true\" : undefined}\n data-flat={flat ? \"true\" : undefined}\n className={cn(\n styles.base,\n inline ? styles.inline : styles.sticky,\n flat && styles.flat,\n className\n )}\n {...props}\n >\n <span\n data-slot=\"bulk-action-bar-count\"\n className={styles.count}\n aria-live=\"polite\"\n aria-atomic=\"true\"\n >\n {label(count)}\n </span>\n\n <div\n ref={actionsRef}\n data-slot=\"bulk-action-bar-actions\"\n className={styles.actions}\n >\n {children}\n </div>\n\n {showDismiss ? (\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n onClick={onClear}\n data-slot=\"bulk-action-bar-dismiss\"\n className={styles.dismiss}\n aria-label={\n typeof clearLabel === \"string\" ? clearLabel : \"Clear selection\"\n }\n >\n <X aria-hidden=\"true\" />\n </Button>\n ) : null}\n </div>\n )\n }\n)\nBulkActionBar.displayName = \"BulkActionBar\"\n\nexport { BulkActionBar }\n"
2450
+ "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { X } from \"@phosphor-icons/react\"\nimport { cn } from \"../../../lib/utils\"\nimport { Button } from \"../button/button\"\nimport styles from \"./bulk-action-bar.module.css\"\n\nexport interface BulkActionBarProps\n extends React.HTMLAttributes<HTMLDivElement> {\n /** Number of selected items. Bar renders only when count > 0. */\n count: number\n /** Action buttons cluster — typically one or more `<Button>` instances. */\n children: React.ReactNode\n\n /** Render inline (non-sticky) instead of fixed to the viewport bottom. */\n inline?: boolean\n /** Render flat: no shadow, no radius, border-top only. Use inside table cards or panels. */\n flat?: boolean\n /** Selection label renderer. Defaults to `(n) => `${n} selected``. */\n label?: (count: number) => React.ReactNode\n /** Aria-label and tooltip for the dismiss button. Defaults to \"Clear selection\". */\n clearLabel?: React.ReactNode\n\n /** Fired by the Escape key and the dismiss button. */\n onClear?: () => void\n /** Show the dismiss (X) button. Defaults to `true`. */\n dismissible?: boolean\n /** Auto-focus the first action button on mount. Defaults to `true`. */\n autoFocus?: boolean\n}\n\nconst defaultLabel = (count: number) => `${count} selected`\n\nconst BulkActionBar = React.forwardRef<HTMLDivElement, BulkActionBarProps>(\n (\n {\n className,\n count,\n children,\n inline = false,\n flat = false,\n label = defaultLabel,\n clearLabel = \"Clear selection\",\n onClear,\n dismissible = true,\n autoFocus = true,\n ...props\n },\n ref\n ) => {\n const actionsRef = React.useRef<HTMLDivElement>(null)\n\n // Escape-to-clear — attached only when `onClear` is provided.\n React.useEffect(() => {\n if (!onClear) return\n const handleKeyDown = (event: KeyboardEvent) => {\n if (event.key === \"Escape\") {\n onClear()\n }\n }\n document.addEventListener(\"keydown\", handleKeyDown)\n return () => {\n document.removeEventListener(\"keydown\", handleKeyDown)\n }\n }, [onClear])\n\n // Auto-focus first action button on mount (not on every count change).\n React.useEffect(() => {\n if (!autoFocus) return\n const root = actionsRef.current\n if (!root) return\n const firstButton = root.querySelector<HTMLButtonElement>(\n \"button:not([disabled])\"\n )\n firstButton?.focus()\n // Mount-only by design; `autoFocus` is intentionally omitted so a later\n // prop flip does not steal focus. The React 19 compiler lint reports the\n // missing dep on the deps-array line, so the narrow disable sits here\n // rather than above the hook. See VI-602.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [])\n\n if (count <= 0) return null\n\n const showDismiss = dismissible && typeof onClear === \"function\"\n\n return (\n <div\n ref={ref}\n role=\"toolbar\"\n aria-label=\"Bulk actions\"\n data-slot=\"bulk-action-bar\"\n data-inline={inline ? \"true\" : undefined}\n data-flat={flat ? \"true\" : undefined}\n className={cn(\n styles.base,\n inline ? styles.inline : styles.sticky,\n flat && styles.flat,\n className\n )}\n {...props}\n >\n <span\n data-slot=\"bulk-action-bar-count\"\n className={styles.count}\n aria-live=\"polite\"\n aria-atomic=\"true\"\n >\n {label(count)}\n </span>\n\n <div\n ref={actionsRef}\n data-slot=\"bulk-action-bar-actions\"\n className={styles.actions}\n >\n {children}\n </div>\n\n {showDismiss ? (\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n onClick={onClear}\n data-slot=\"bulk-action-bar-dismiss\"\n className={styles.dismiss}\n aria-label={\n typeof clearLabel === \"string\" ? clearLabel : \"Clear selection\"\n }\n >\n <X aria-hidden=\"true\" />\n </Button>\n ) : null}\n </div>\n )\n }\n)\nBulkActionBar.displayName = \"BulkActionBar\"\n\nexport { BulkActionBar }\n"
2451
2451
  },
2452
2452
  {
2453
2453
  "path": "components/ui/bulk-action-bar/bulk-action-bar.module.css",
@@ -2534,7 +2534,7 @@
2534
2534
  {
2535
2535
  "path": "components/ui/data-table/data-table.tsx",
2536
2536
  "type": "registry:ui",
2537
- "content": "\"use client\"\n\nimport * as React from \"react\"\nimport {\n flexRender,\n getCoreRowModel,\n getFilteredRowModel,\n getPaginationRowModel,\n getSortedRowModel,\n useReactTable,\n type ColumnDef,\n type OnChangeFn,\n type PaginationState,\n type RowSelectionState,\n type SortingState,\n type Table as TanstackTable,\n} from \"@tanstack/react-table\"\nimport {\n CaretDownIcon,\n CaretLeftIcon,\n CaretRightIcon,\n CaretUpIcon,\n CaretUpDownIcon,\n} from \"@phosphor-icons/react\"\n\nimport { cn } from \"../../../lib/utils\"\nimport {\n Table,\n TableBody,\n TableCell,\n TableHead,\n TableHeader,\n TableRow,\n} from \"../table/table\"\nimport { Button } from \"../button/button\"\nimport { Checkbox } from \"../checkbox/checkbox\"\nimport { Skeleton } from \"../skeleton/skeleton\"\nimport { EmptyState } from \"../empty-state/empty-state\"\nimport styles from \"./data-table.module.css\"\n\nexport type {\n ColumnDef,\n SortingState,\n RowSelectionState,\n PaginationState,\n OnChangeFn,\n}\n\nexport interface DataTableGroupRow {\n kind: \"group\"\n id: string\n label: string\n count?: number\n}\n\nexport interface DataTableDataRow<TData> {\n kind: \"data\"\n id: string\n row: TData\n}\n\nexport type DataTableRow<TData> = DataTableGroupRow | DataTableDataRow<TData>\n\n/**\n * Semantic per-row tone keys. Map to subtle background tints via CSS — see\n * `data-table.module.css`. Mirrors the tone vocabulary used by `StatusBadge`\n * / `StatusDot` so a row tagged \"live\" reads as one signal with a \"live\"\n * badge inside the row.\n */\nexport type DataTableRowTone =\n | \"live\"\n | \"warn\"\n | \"scheduled\"\n | \"sold\"\n | \"draft\"\n | \"danger\"\n | \"info\"\n\nexport interface DataTableProps<TData, TValue = unknown>\n extends Omit<React.HTMLAttributes<HTMLDivElement>, \"onChange\"> {\n columns: ColumnDef<TData, TValue>[]\n data?: TData[]\n\n // Mixed render order with group-head separators. When provided, the caller\n // owns sort/grouping/windowing — sort UI and pagination footer are\n // suppressed. Group rows are excluded from selection state.\n rows?: DataTableRow<TData>[]\n groupRowRenderer?: (group: DataTableGroupRow) => React.ReactNode\n\n /**\n * Map each data row to a semantic tone for a subtle background tint. When\n * the callback returns `undefined`, the row renders on the default surface.\n * Tones resolve to Visor surface tokens at the CSS layer — see\n * `data-table.module.css`.\n */\n rowTone?: (row: TData) => DataTableRowTone | undefined\n\n /**\n * When supplied, every data row becomes a keyboard-activatable target:\n * `role=\"button\"`, `tabIndex={0}`, click + Enter/Space dispatch the\n * handler, and a `data-clickable=\"true\"` attribute drives the hover/focus\n * affordance. The injected selection checkbox cell stops propagation, so\n * clicking it does not trigger `onRowClick`.\n */\n onRowClick?: (row: TData) => void\n\n // Sorting\n sorting?: SortingState\n onSortingChange?: OnChangeFn<SortingState>\n defaultSorting?: SortingState\n\n // Pagination\n pagination?: PaginationState\n onPaginationChange?: OnChangeFn<PaginationState>\n pageSize?: number\n pageSizeOptions?: number[]\n\n // Selection\n enableRowSelection?: boolean\n rowSelection?: RowSelectionState\n onRowSelectionChange?: OnChangeFn<RowSelectionState>\n getRowId?: (row: TData, index: number) => string\n\n // Global filter\n globalFilter?: string\n onGlobalFilterChange?: (value: string) => void\n\n // States\n loading?: boolean\n emptyState?: React.ReactNode\n\n /**\n * Opt-in per-column skeleton shapes for the loading state (VI-516). When\n * supplied, each skeleton row's cell contents are produced by this render\n * prop instead of the default uniform full-width bars — letting the caller\n * mirror the real row's silhouette (logo plates, badge pills, two-line id\n * stacks) for a calmer load. Return the cell *contents*; DataTable still owns\n * the `<tr>`/`<td>` structure, key, and `data-slot`. `colIndex` walks the\n * resolved column list (the injected selection column, when present, is\n * index 0). Return `null`/`undefined` to leave a cell empty.\n *\n * Default (prop omitted) is unchanged: every cell renders a full-width\n * `Skeleton` bar, exactly as today. Pair with the `Skeleton` shape classes\n * from `skeleton.module.css` (`shapeLogo`, `shapePill`, `shapeCircle`).\n */\n loadingSkeletonCell?: (args: {\n rowIndex: number\n colIndex: number\n columnId: string\n }) => React.ReactNode\n\n // Layout\n stickyHeader?: boolean\n\n /**\n * Vertical row padding step. Maps to a `data-density` attribute on the root\n * which drives the `--dt-row-py` custom property the table's `<td>` cells\n * consume. Themes can override per-density values without forking the\n * component — see `data-table.module.css`.\n *\n * - `\"compact\"` — 8px (sub-content density: long lists, narrow viewports)\n * - `\"default\"` — 12px (current behaviour; no visual regression for existing\n * consumers)\n * - `\"editorial\"` — 20px (generous; each row reads as a card; high-design\n * admin patterns); column headers also render uppercase, `--font-size-xs`,\n * `--text-tertiary`, `letter-spacing: 0.08em`\n */\n density?: \"compact\" | \"default\" | \"editorial\"\n}\n\nconst DEFAULT_PAGE_SIZE_OPTIONS = [10, 25, 50, 100]\n\nfunction DataTableInner<TData, TValue = unknown>(\n props: DataTableProps<TData, TValue>,\n ref: React.ForwardedRef<HTMLDivElement>\n) {\n const {\n columns: userColumns,\n data,\n rows,\n groupRowRenderer,\n sorting: controlledSorting,\n onSortingChange,\n defaultSorting,\n pagination: controlledPagination,\n onPaginationChange,\n pageSize = 10,\n pageSizeOptions = DEFAULT_PAGE_SIZE_OPTIONS,\n enableRowSelection = false,\n rowSelection: controlledRowSelection,\n onRowSelectionChange,\n getRowId,\n globalFilter: controlledGlobalFilter,\n onGlobalFilterChange,\n loading = false,\n emptyState,\n loadingSkeletonCell,\n stickyHeader = false,\n rowTone,\n onRowClick,\n density = \"default\",\n className,\n ...rest\n } = props\n\n // When rows is provided, the caller owns sort/grouping/windowing. We bypass\n // TanStack pagination and column sort UI, but keep the table instance for\n // selection state and cell rendering on data rows.\n const hasRows = rows != null\n const dataItems = React.useMemo(() => {\n if (hasRows) {\n return rows!.flatMap((item) =>\n item.kind === \"data\" ? [item.row] : []\n )\n }\n return data ?? []\n }, [hasRows, rows, data])\n\n const internalGetRowId = React.useMemo(() => {\n if (getRowId) return getRowId\n if (hasRows) {\n const ids = rows!.flatMap((item) =>\n item.kind === \"data\" ? [item.id] : []\n )\n return (_row: TData, index: number) => ids[index] ?? String(index)\n }\n return undefined\n }, [getRowId, hasRows, rows])\n\n // Uncontrolled sorting state\n const [internalSorting, setInternalSorting] = React.useState<SortingState>(\n defaultSorting ?? []\n )\n const sortingIsControlled = controlledSorting !== undefined\n const sorting = sortingIsControlled ? controlledSorting : internalSorting\n const handleSortingChange: OnChangeFn<SortingState> = (updater) => {\n if (!sortingIsControlled) {\n setInternalSorting((prev) =>\n typeof updater === \"function\" ? updater(prev) : updater\n )\n }\n onSortingChange?.(updater)\n }\n\n // Uncontrolled pagination state\n const [internalPagination, setInternalPagination] =\n React.useState<PaginationState>({ pageIndex: 0, pageSize })\n const paginationIsControlled = controlledPagination !== undefined\n const pagination = paginationIsControlled\n ? controlledPagination\n : internalPagination\n const handlePaginationChange: OnChangeFn<PaginationState> = (updater) => {\n if (!paginationIsControlled) {\n setInternalPagination((prev) =>\n typeof updater === \"function\" ? updater(prev) : updater\n )\n }\n onPaginationChange?.(updater)\n }\n\n // Uncontrolled selection state\n const [internalRowSelection, setInternalRowSelection] =\n React.useState<RowSelectionState>({})\n const selectionIsControlled = controlledRowSelection !== undefined\n const rowSelection = selectionIsControlled\n ? controlledRowSelection\n : internalRowSelection\n const handleRowSelectionChange: OnChangeFn<RowSelectionState> = (updater) => {\n if (!selectionIsControlled) {\n setInternalRowSelection((prev) =>\n typeof updater === \"function\" ? updater(prev) : updater\n )\n }\n onRowSelectionChange?.(updater)\n }\n\n // Global filter\n const [internalGlobalFilter, setInternalGlobalFilter] = React.useState(\"\")\n const globalFilterIsControlled = controlledGlobalFilter !== undefined\n const globalFilter = globalFilterIsControlled\n ? controlledGlobalFilter\n : internalGlobalFilter\n const handleGlobalFilterChange = (value: unknown) => {\n const next = typeof value === \"function\" ? (value as (p: string) => string)(globalFilter) : (value as string)\n if (!globalFilterIsControlled) {\n setInternalGlobalFilter(next ?? \"\")\n }\n onGlobalFilterChange?.(next ?? \"\")\n }\n\n // Inject a selection column when enabled\n const columns = React.useMemo<ColumnDef<TData, TValue>[]>(() => {\n if (!enableRowSelection) return userColumns\n const selectionColumn: ColumnDef<TData, TValue> = {\n id: \"__select\",\n enableSorting: false,\n size: 40,\n header: ({ table }) => (\n <Checkbox\n aria-label=\"Select all rows\"\n checked={\n table.getIsAllPageRowsSelected()\n ? true\n : table.getIsSomePageRowsSelected()\n ? \"indeterminate\"\n : false\n }\n onCheckedChange={(value) =>\n table.toggleAllPageRowsSelected(value === true)\n }\n />\n ),\n cell: ({ row }) => (\n // Stop click/keydown from bubbling to the parent <tr>, otherwise\n // toggling the checkbox would also fire `onRowClick`. The wrapper\n // is presentational — focus and ARIA continue to live on the\n // underlying Checkbox.\n <div\n data-slot=\"data-table-selection-cell\"\n onClick={(e) => e.stopPropagation()}\n onKeyDown={(e) => {\n if (e.key === \"Enter\" || e.key === \" \") e.stopPropagation()\n }}\n >\n <Checkbox\n aria-label=\"Select row\"\n checked={row.getIsSelected()}\n disabled={!row.getCanSelect()}\n onCheckedChange={(value) => row.toggleSelected(value === true)}\n />\n </div>\n ),\n }\n return [selectionColumn, ...userColumns]\n }, [enableRowSelection, userColumns])\n\n const table: TanstackTable<TData> = useReactTable<TData>({\n data: dataItems,\n columns,\n state: {\n sorting,\n pagination,\n rowSelection,\n globalFilter,\n },\n enableRowSelection,\n getRowId: internalGetRowId,\n onSortingChange: handleSortingChange,\n onPaginationChange: handlePaginationChange,\n onRowSelectionChange: handleRowSelectionChange,\n onGlobalFilterChange: handleGlobalFilterChange,\n getCoreRowModel: getCoreRowModel(),\n getSortedRowModel: getSortedRowModel(),\n getPaginationRowModel: getPaginationRowModel(),\n getFilteredRowModel: getFilteredRowModel(),\n })\n\n const totalRows = table.getFilteredRowModel().rows.length\n const pageRows = table.getRowModel().rows\n const colCount = columns.length\n const pageIndex = table.getState().pagination.pageIndex\n const currentPageSize = table.getState().pagination.pageSize\n const pageCount = table.getPageCount()\n const firstRow = totalRows === 0 ? 0 : pageIndex * currentPageSize + 1\n const lastRow = Math.min((pageIndex + 1) * currentPageSize, totalRows)\n\n const isEmpty = !loading && dataItems.length === 0 && !hasRows\n const defaultEmpty = <EmptyState heading=\"No results\" tone=\"subtle\" />\n\n const defaultGroupRowContent = (group: DataTableGroupRow) => (\n <span data-slot=\"data-table-group-label\" className={styles.groupLabel}>\n {group.label}\n {group.count != null && (\n <span className={styles.groupCount}>{group.count}</span>\n )}\n </span>\n )\n\n // Build the per-data-row props (tone, clickable affordance, keyboard\n // activation). Shared between the `rows`-driven path and the standard\n // pageRows path so the two stay in lockstep.\n //\n // Note on role=\"button\": axe flags nested-interactive when a `<tr>` carries\n // `role=\"button\"` and also contains an interactive control (the selection\n // checkbox cell). When selection is enabled, the row stays semantically a\n // table row — click + keyboard activation still work via the explicit\n // handlers, but the role override is dropped to keep `<tr>` semantics and\n // satisfy WCAG nested-interactive. When selection is off, the row is a\n // pure click target and `role=\"button\"` is safe.\n const getDataRowProps = (rowData: TData) => {\n const tone = rowTone?.(rowData)\n const clickable = onRowClick != null\n const handleClick = clickable\n ? () => onRowClick!(rowData)\n : undefined\n const handleKeyDown = clickable\n ? (e: React.KeyboardEvent<HTMLTableRowElement>) => {\n if (e.key === \"Enter\" || e.key === \" \") {\n e.preventDefault()\n onRowClick!(rowData)\n }\n }\n : undefined\n const useButtonRole = clickable && !enableRowSelection\n return {\n className: styles.dataRow,\n \"data-tone\": tone,\n \"data-clickable\": clickable ? \"true\" : undefined,\n role: useButtonRole ? (\"button\" as const) : undefined,\n tabIndex: clickable ? 0 : undefined,\n onClick: handleClick,\n onKeyDown: handleKeyDown,\n }\n }\n\n return (\n <div\n ref={ref}\n data-slot=\"data-table\"\n data-density={density}\n className={cn(styles.root, className)}\n {...rest}\n >\n <Table>\n <TableHeader\n className={cn(stickyHeader && styles.stickyHeader)}\n data-sticky={stickyHeader || undefined}\n >\n {table.getHeaderGroups().map((headerGroup) => (\n <TableRow key={headerGroup.id} className={styles.sortBar}>\n {headerGroup.headers.map((header) => {\n const canSort = header.column.getCanSort() && !hasRows\n const sortDir = header.column.getIsSorted()\n const ariaSort: React.AriaAttributes[\"aria-sort\"] = hasRows\n ? undefined\n : sortDir === \"asc\"\n ? \"ascending\"\n : sortDir === \"desc\"\n ? \"descending\"\n : canSort\n ? \"none\"\n : undefined\n const headerContent = header.isPlaceholder\n ? null\n : flexRender(\n header.column.columnDef.header,\n header.getContext()\n )\n const columnLabel =\n typeof header.column.columnDef.header === \"string\"\n ? (header.column.columnDef.header as string)\n : header.column.id\n const nextSortStateLabel =\n sortDir === \"asc\"\n ? \"descending\"\n : sortDir === \"desc\"\n ? \"unsorted\"\n : \"ascending\"\n return (\n <TableHead\n key={header.id}\n aria-sort={ariaSort}\n style={{\n width:\n header.column.id === \"__select\" ? \"40px\" : undefined,\n }}\n >\n {canSort ? (\n <button\n type=\"button\"\n className={styles.sortButton}\n onClick={header.column.getToggleSortingHandler()}\n aria-label={`${columnLabel}, sort ${nextSortStateLabel}`}\n >\n <span className={styles.sortLabel}>\n {headerContent}\n </span>\n <span className={styles.sortIcon} aria-hidden=\"true\">\n {sortDir === \"asc\" ? (\n <CaretUpIcon weight=\"bold\" />\n ) : sortDir === \"desc\" ? (\n <CaretDownIcon weight=\"bold\" />\n ) : (\n <CaretUpDownIcon weight=\"bold\" />\n )}\n </span>\n </button>\n ) : (\n headerContent\n )}\n </TableHead>\n )\n })}\n </TableRow>\n ))}\n </TableHeader>\n <TableBody>\n {loading ? (\n Array.from({ length: currentPageSize }).map((_, rowIdx) => (\n <TableRow key={`skeleton-${rowIdx}`} data-slot=\"data-table-skeleton-row\">\n {columns.map((col, colIdx) => (\n <TableCell key={`skeleton-${rowIdx}-${colIdx}`}>\n {loadingSkeletonCell ? (\n loadingSkeletonCell({\n rowIndex: rowIdx,\n colIndex: colIdx,\n columnId: col.id ?? String(colIdx),\n })\n ) : (\n <Skeleton className={styles.skeletonCell} />\n )}\n </TableCell>\n ))}\n </TableRow>\n ))\n ) : isEmpty ? (\n <TableRow data-slot=\"data-table-empty-row\">\n <TableCell colSpan={colCount} className={styles.emptyCell}>\n {emptyState ?? defaultEmpty}\n </TableCell>\n </TableRow>\n ) : hasRows ? (\n rows!.map((item) => {\n if (item.kind === \"group\") {\n return (\n <TableRow\n key={`group-${item.id}`}\n data-slot=\"data-table-group-row\"\n className={styles.groupRow}\n >\n <TableCell\n colSpan={colCount}\n className={styles.groupCell}\n >\n {groupRowRenderer\n ? groupRowRenderer(item)\n : defaultGroupRowContent(item)}\n </TableCell>\n </TableRow>\n )\n }\n const tsRow = table.getRow(item.id)\n const rowProps = getDataRowProps(item.row)\n return (\n <TableRow\n key={item.id}\n data-state={tsRow.getIsSelected() ? \"selected\" : undefined}\n {...rowProps}\n >\n {tsRow.getVisibleCells().map((cell) => (\n <TableCell key={cell.id}>\n {flexRender(\n cell.column.columnDef.cell,\n cell.getContext()\n )}\n </TableCell>\n ))}\n </TableRow>\n )\n })\n ) : pageRows.length === 0 ? (\n <TableRow data-slot=\"data-table-empty-row\">\n <TableCell colSpan={colCount} className={styles.emptyCell}>\n {emptyState ?? defaultEmpty}\n </TableCell>\n </TableRow>\n ) : (\n pageRows.map((row) => {\n const rowProps = getDataRowProps(row.original)\n return (\n <TableRow\n key={row.id}\n data-state={row.getIsSelected() ? \"selected\" : undefined}\n {...rowProps}\n >\n {row.getVisibleCells().map((cell) => (\n <TableCell key={cell.id}>\n {flexRender(\n cell.column.columnDef.cell,\n cell.getContext()\n )}\n </TableCell>\n ))}\n </TableRow>\n )\n })\n )}\n </TableBody>\n </Table>\n\n {!hasRows && (\n <div className={styles.footer} data-slot=\"data-table-footer\">\n <div className={styles.footerInfo} aria-live=\"polite\">\n {totalRows === 0\n ? \"No results\"\n : `Showing ${firstRow} to ${lastRow} of ${totalRows}`}\n </div>\n <div className={styles.footerControls}>\n <label className={styles.pageSizeLabel}>\n <span className={styles.pageSizeLabelText}>Rows per page</span>\n <select\n className={styles.pageSizeSelect}\n value={currentPageSize}\n onChange={(e) => table.setPageSize(Number(e.target.value))}\n aria-label=\"Rows per page\"\n >\n {pageSizeOptions.map((opt) => (\n <option key={opt} value={opt}>\n {opt}\n </option>\n ))}\n </select>\n </label>\n <div className={styles.pageNav}>\n <span className={styles.pageCounter}>\n Page {pageCount === 0 ? 0 : pageIndex + 1} of {pageCount}\n </span>\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n onClick={() => table.previousPage()}\n disabled={!table.getCanPreviousPage()}\n aria-label=\"Previous page\"\n >\n <CaretLeftIcon weight=\"bold\" aria-hidden=\"true\" />\n </Button>\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n onClick={() => table.nextPage()}\n disabled={!table.getCanNextPage()}\n aria-label=\"Next page\"\n >\n <CaretRightIcon weight=\"bold\" aria-hidden=\"true\" />\n </Button>\n </div>\n </div>\n </div>\n )}\n </div>\n )\n}\n\n// forwardRef with generics — preserve TData through the cast\nconst DataTable = React.forwardRef(DataTableInner) as <\n TData,\n TValue = unknown,\n>(\n props: DataTableProps<TData, TValue> & {\n ref?: React.ForwardedRef<HTMLDivElement>\n }\n) => ReturnType<typeof DataTableInner>\n\n;(DataTable as unknown as { displayName: string }).displayName = \"DataTable\"\n\nexport { DataTable }\n"
2537
+ "content": "\"use client\"\n\nimport * as React from \"react\"\nimport {\n flexRender,\n getCoreRowModel,\n getFilteredRowModel,\n getPaginationRowModel,\n getSortedRowModel,\n useReactTable,\n type ColumnDef,\n type OnChangeFn,\n type PaginationState,\n type RowSelectionState,\n type SortingState,\n type Table as TanstackTable,\n} from \"@tanstack/react-table\"\nimport {\n CaretDownIcon,\n CaretLeftIcon,\n CaretRightIcon,\n CaretUpIcon,\n CaretUpDownIcon,\n} from \"@phosphor-icons/react\"\n\nimport { cn } from \"../../../lib/utils\"\nimport {\n Table,\n TableBody,\n TableCell,\n TableHead,\n TableHeader,\n TableRow,\n} from \"../table/table\"\nimport { Button } from \"../button/button\"\nimport { Checkbox } from \"../checkbox/checkbox\"\nimport { Skeleton } from \"../skeleton/skeleton\"\nimport { EmptyState } from \"../empty-state/empty-state\"\nimport styles from \"./data-table.module.css\"\n\nexport type {\n ColumnDef,\n SortingState,\n RowSelectionState,\n PaginationState,\n OnChangeFn,\n}\n\nexport interface DataTableGroupRow {\n kind: \"group\"\n id: string\n label: string\n count?: number\n}\n\nexport interface DataTableDataRow<TData> {\n kind: \"data\"\n id: string\n row: TData\n}\n\nexport type DataTableRow<TData> = DataTableGroupRow | DataTableDataRow<TData>\n\n/**\n * Semantic per-row tone keys. Map to subtle background tints via CSS — see\n * `data-table.module.css`. Mirrors the tone vocabulary used by `StatusBadge`\n * / `StatusDot` so a row tagged \"live\" reads as one signal with a \"live\"\n * badge inside the row.\n */\nexport type DataTableRowTone =\n | \"live\"\n | \"warn\"\n | \"scheduled\"\n | \"sold\"\n | \"draft\"\n | \"danger\"\n | \"info\"\n\nexport interface DataTableProps<TData, TValue = unknown>\n extends Omit<React.HTMLAttributes<HTMLDivElement>, \"onChange\"> {\n columns: ColumnDef<TData, TValue>[]\n data?: TData[]\n\n // Mixed render order with group-head separators. When provided, the caller\n // owns sort/grouping/windowing — sort UI and pagination footer are\n // suppressed. Group rows are excluded from selection state.\n rows?: DataTableRow<TData>[]\n groupRowRenderer?: (group: DataTableGroupRow) => React.ReactNode\n\n /**\n * Map each data row to a semantic tone for a subtle background tint. When\n * the callback returns `undefined`, the row renders on the default surface.\n * Tones resolve to Visor surface tokens at the CSS layer — see\n * `data-table.module.css`.\n */\n rowTone?: (row: TData) => DataTableRowTone | undefined\n\n /**\n * When supplied, every data row becomes a keyboard-activatable target:\n * `role=\"button\"`, `tabIndex={0}`, click + Enter/Space dispatch the\n * handler, and a `data-clickable=\"true\"` attribute drives the hover/focus\n * affordance. The injected selection checkbox cell stops propagation, so\n * clicking it does not trigger `onRowClick`.\n */\n onRowClick?: (row: TData) => void\n\n // Sorting\n sorting?: SortingState\n onSortingChange?: OnChangeFn<SortingState>\n defaultSorting?: SortingState\n\n // Pagination\n pagination?: PaginationState\n onPaginationChange?: OnChangeFn<PaginationState>\n pageSize?: number\n pageSizeOptions?: number[]\n\n // Selection\n enableRowSelection?: boolean\n rowSelection?: RowSelectionState\n onRowSelectionChange?: OnChangeFn<RowSelectionState>\n getRowId?: (row: TData, index: number) => string\n\n // Global filter\n globalFilter?: string\n onGlobalFilterChange?: (value: string) => void\n\n // States\n loading?: boolean\n emptyState?: React.ReactNode\n\n /**\n * Opt-in per-column skeleton shapes for the loading state (VI-516). When\n * supplied, each skeleton row's cell contents are produced by this render\n * prop instead of the default uniform full-width bars — letting the caller\n * mirror the real row's silhouette (logo plates, badge pills, two-line id\n * stacks) for a calmer load. Return the cell *contents*; DataTable still owns\n * the `<tr>`/`<td>` structure, key, and `data-slot`. `colIndex` walks the\n * resolved column list (the injected selection column, when present, is\n * index 0). Return `null`/`undefined` to leave a cell empty.\n *\n * Default (prop omitted) is unchanged: every cell renders a full-width\n * `Skeleton` bar, exactly as today. Pair with the `Skeleton` shape classes\n * from `skeleton.module.css` (`shapeLogo`, `shapePill`, `shapeCircle`).\n */\n loadingSkeletonCell?: (args: {\n rowIndex: number\n colIndex: number\n columnId: string\n }) => React.ReactNode\n\n // Layout\n stickyHeader?: boolean\n\n /**\n * Vertical row padding step. Maps to a `data-density` attribute on the root\n * which drives the `--dt-row-py` custom property the table's `<td>` cells\n * consume. Themes can override per-density values without forking the\n * component — see `data-table.module.css`.\n *\n * - `\"compact\"` — 8px (sub-content density: long lists, narrow viewports)\n * - `\"default\"` — 12px (current behaviour; no visual regression for existing\n * consumers)\n * - `\"editorial\"` — 20px (generous; each row reads as a card; high-design\n * admin patterns); column headers also render uppercase, `--font-size-xs`,\n * `--text-tertiary`, `letter-spacing: 0.08em`\n */\n density?: \"compact\" | \"default\" | \"editorial\"\n}\n\nconst DEFAULT_PAGE_SIZE_OPTIONS = [10, 25, 50, 100]\n\nfunction DataTableInner<TData, TValue = unknown>(\n props: DataTableProps<TData, TValue>,\n ref: React.ForwardedRef<HTMLDivElement>\n) {\n const {\n columns: userColumns,\n data,\n rows,\n groupRowRenderer,\n sorting: controlledSorting,\n onSortingChange,\n defaultSorting,\n pagination: controlledPagination,\n onPaginationChange,\n pageSize = 10,\n pageSizeOptions = DEFAULT_PAGE_SIZE_OPTIONS,\n enableRowSelection = false,\n rowSelection: controlledRowSelection,\n onRowSelectionChange,\n getRowId,\n globalFilter: controlledGlobalFilter,\n onGlobalFilterChange,\n loading = false,\n emptyState,\n loadingSkeletonCell,\n stickyHeader = false,\n rowTone,\n onRowClick,\n density = \"default\",\n className,\n ...rest\n } = props\n\n // When rows is provided, the caller owns sort/grouping/windowing. We bypass\n // TanStack pagination and column sort UI, but keep the table instance for\n // selection state and cell rendering on data rows.\n const hasRows = rows != null\n const dataItems = React.useMemo(() => {\n if (hasRows) {\n return rows!.flatMap((item) =>\n item.kind === \"data\" ? [item.row] : []\n )\n }\n return data ?? []\n }, [hasRows, rows, data])\n\n const internalGetRowId = React.useMemo(() => {\n if (getRowId) return getRowId\n if (hasRows) {\n const ids = rows!.flatMap((item) =>\n item.kind === \"data\" ? [item.id] : []\n )\n return (_row: TData, index: number) => ids[index] ?? String(index)\n }\n return undefined\n }, [getRowId, hasRows, rows])\n\n // Uncontrolled sorting state\n const [internalSorting, setInternalSorting] = React.useState<SortingState>(\n defaultSorting ?? []\n )\n const sortingIsControlled = controlledSorting !== undefined\n const sorting = sortingIsControlled ? controlledSorting : internalSorting\n const handleSortingChange: OnChangeFn<SortingState> = (updater) => {\n if (!sortingIsControlled) {\n setInternalSorting((prev) =>\n typeof updater === \"function\" ? updater(prev) : updater\n )\n }\n onSortingChange?.(updater)\n }\n\n // Uncontrolled pagination state\n const [internalPagination, setInternalPagination] =\n React.useState<PaginationState>({ pageIndex: 0, pageSize })\n const paginationIsControlled = controlledPagination !== undefined\n const pagination = paginationIsControlled\n ? controlledPagination\n : internalPagination\n const handlePaginationChange: OnChangeFn<PaginationState> = (updater) => {\n if (!paginationIsControlled) {\n setInternalPagination((prev) =>\n typeof updater === \"function\" ? updater(prev) : updater\n )\n }\n onPaginationChange?.(updater)\n }\n\n // Uncontrolled selection state\n const [internalRowSelection, setInternalRowSelection] =\n React.useState<RowSelectionState>({})\n const selectionIsControlled = controlledRowSelection !== undefined\n const rowSelection = selectionIsControlled\n ? controlledRowSelection\n : internalRowSelection\n const handleRowSelectionChange: OnChangeFn<RowSelectionState> = (updater) => {\n if (!selectionIsControlled) {\n setInternalRowSelection((prev) =>\n typeof updater === \"function\" ? updater(prev) : updater\n )\n }\n onRowSelectionChange?.(updater)\n }\n\n // Global filter\n const [internalGlobalFilter, setInternalGlobalFilter] = React.useState(\"\")\n const globalFilterIsControlled = controlledGlobalFilter !== undefined\n const globalFilter = globalFilterIsControlled\n ? controlledGlobalFilter\n : internalGlobalFilter\n const handleGlobalFilterChange = (value: unknown) => {\n const next = typeof value === \"function\" ? (value as (p: string) => string)(globalFilter) : (value as string)\n if (!globalFilterIsControlled) {\n setInternalGlobalFilter(next ?? \"\")\n }\n onGlobalFilterChange?.(next ?? \"\")\n }\n\n // Inject a selection column when enabled\n const columns = React.useMemo<ColumnDef<TData, TValue>[]>(() => {\n if (!enableRowSelection) return userColumns\n const selectionColumn: ColumnDef<TData, TValue> = {\n id: \"__select\",\n enableSorting: false,\n size: 40,\n header: ({ table }) => (\n <Checkbox\n aria-label=\"Select all rows\"\n checked={\n table.getIsAllPageRowsSelected()\n ? true\n : table.getIsSomePageRowsSelected()\n ? \"indeterminate\"\n : false\n }\n onCheckedChange={(value) =>\n table.toggleAllPageRowsSelected(value === true)\n }\n />\n ),\n cell: ({ row }) => (\n // Stop click/keydown from bubbling to the parent <tr>, otherwise\n // toggling the checkbox would also fire `onRowClick`. The wrapper\n // is presentational — focus and ARIA continue to live on the\n // underlying Checkbox.\n <div\n data-slot=\"data-table-selection-cell\"\n onClick={(e) => e.stopPropagation()}\n onKeyDown={(e) => {\n if (e.key === \"Enter\" || e.key === \" \") e.stopPropagation()\n }}\n >\n <Checkbox\n aria-label=\"Select row\"\n checked={row.getIsSelected()}\n disabled={!row.getCanSelect()}\n onCheckedChange={(value) => row.toggleSelected(value === true)}\n />\n </div>\n ),\n }\n return [selectionColumn, ...userColumns]\n }, [enableRowSelection, userColumns])\n\n // NOTE: `useReactTable()` returns non-memoizable row models/handlers, so a\n // consumer on the React 19 compiler ruleset (eslint-plugin-react-hooks v6)\n // sees a non-blocking `react-hooks/incompatible-library` warning here. This\n // is inherent to TanStack Table and cannot be refactored away. It is\n // intentionally NOT suppressed in Visor source — Visor's own eslint\n // (react-hooks v5) doesn't load that rule, so a disable directive for it\n // errors here. See VI-602.\n const table: TanstackTable<TData> = useReactTable<TData>({\n data: dataItems,\n columns,\n state: {\n sorting,\n pagination,\n rowSelection,\n globalFilter,\n },\n enableRowSelection,\n getRowId: internalGetRowId,\n onSortingChange: handleSortingChange,\n onPaginationChange: handlePaginationChange,\n onRowSelectionChange: handleRowSelectionChange,\n onGlobalFilterChange: handleGlobalFilterChange,\n getCoreRowModel: getCoreRowModel(),\n getSortedRowModel: getSortedRowModel(),\n getPaginationRowModel: getPaginationRowModel(),\n getFilteredRowModel: getFilteredRowModel(),\n })\n\n const totalRows = table.getFilteredRowModel().rows.length\n const pageRows = table.getRowModel().rows\n const colCount = columns.length\n const pageIndex = table.getState().pagination.pageIndex\n const currentPageSize = table.getState().pagination.pageSize\n const pageCount = table.getPageCount()\n const firstRow = totalRows === 0 ? 0 : pageIndex * currentPageSize + 1\n const lastRow = Math.min((pageIndex + 1) * currentPageSize, totalRows)\n\n const isEmpty = !loading && dataItems.length === 0 && !hasRows\n const defaultEmpty = <EmptyState heading=\"No results\" tone=\"subtle\" />\n\n const defaultGroupRowContent = (group: DataTableGroupRow) => (\n <span data-slot=\"data-table-group-label\" className={styles.groupLabel}>\n {group.label}\n {group.count != null && (\n <span className={styles.groupCount}>{group.count}</span>\n )}\n </span>\n )\n\n // Build the per-data-row props (tone, clickable affordance, keyboard\n // activation). Shared between the `rows`-driven path and the standard\n // pageRows path so the two stay in lockstep.\n //\n // Note on role=\"button\": axe flags nested-interactive when a `<tr>` carries\n // `role=\"button\"` and also contains an interactive control (the selection\n // checkbox cell). When selection is enabled, the row stays semantically a\n // table row — click + keyboard activation still work via the explicit\n // handlers, but the role override is dropped to keep `<tr>` semantics and\n // satisfy WCAG nested-interactive. When selection is off, the row is a\n // pure click target and `role=\"button\"` is safe.\n const getDataRowProps = (rowData: TData) => {\n const tone = rowTone?.(rowData)\n const clickable = onRowClick != null\n const handleClick = clickable\n ? () => onRowClick!(rowData)\n : undefined\n const handleKeyDown = clickable\n ? (e: React.KeyboardEvent<HTMLTableRowElement>) => {\n if (e.key === \"Enter\" || e.key === \" \") {\n e.preventDefault()\n onRowClick!(rowData)\n }\n }\n : undefined\n const useButtonRole = clickable && !enableRowSelection\n return {\n className: styles.dataRow,\n \"data-tone\": tone,\n \"data-clickable\": clickable ? \"true\" : undefined,\n role: useButtonRole ? (\"button\" as const) : undefined,\n tabIndex: clickable ? 0 : undefined,\n onClick: handleClick,\n onKeyDown: handleKeyDown,\n }\n }\n\n return (\n <div\n ref={ref}\n data-slot=\"data-table\"\n data-density={density}\n className={cn(styles.root, className)}\n {...rest}\n >\n <Table>\n <TableHeader\n className={cn(stickyHeader && styles.stickyHeader)}\n data-sticky={stickyHeader || undefined}\n >\n {table.getHeaderGroups().map((headerGroup) => (\n <TableRow key={headerGroup.id} className={styles.sortBar}>\n {headerGroup.headers.map((header) => {\n const canSort = header.column.getCanSort() && !hasRows\n const sortDir = header.column.getIsSorted()\n const ariaSort: React.AriaAttributes[\"aria-sort\"] = hasRows\n ? undefined\n : sortDir === \"asc\"\n ? \"ascending\"\n : sortDir === \"desc\"\n ? \"descending\"\n : canSort\n ? \"none\"\n : undefined\n const headerContent = header.isPlaceholder\n ? null\n : flexRender(\n header.column.columnDef.header,\n header.getContext()\n )\n const columnLabel =\n typeof header.column.columnDef.header === \"string\"\n ? (header.column.columnDef.header as string)\n : header.column.id\n const nextSortStateLabel =\n sortDir === \"asc\"\n ? \"descending\"\n : sortDir === \"desc\"\n ? \"unsorted\"\n : \"ascending\"\n return (\n <TableHead\n key={header.id}\n aria-sort={ariaSort}\n style={{\n width:\n header.column.id === \"__select\" ? \"40px\" : undefined,\n }}\n >\n {canSort ? (\n <button\n type=\"button\"\n className={styles.sortButton}\n onClick={header.column.getToggleSortingHandler()}\n aria-label={`${columnLabel}, sort ${nextSortStateLabel}`}\n >\n <span className={styles.sortLabel}>\n {headerContent}\n </span>\n <span className={styles.sortIcon} aria-hidden=\"true\">\n {sortDir === \"asc\" ? (\n <CaretUpIcon weight=\"bold\" />\n ) : sortDir === \"desc\" ? (\n <CaretDownIcon weight=\"bold\" />\n ) : (\n <CaretUpDownIcon weight=\"bold\" />\n )}\n </span>\n </button>\n ) : (\n headerContent\n )}\n </TableHead>\n )\n })}\n </TableRow>\n ))}\n </TableHeader>\n <TableBody>\n {loading ? (\n Array.from({ length: currentPageSize }).map((_, rowIdx) => (\n <TableRow key={`skeleton-${rowIdx}`} data-slot=\"data-table-skeleton-row\">\n {columns.map((col, colIdx) => (\n <TableCell key={`skeleton-${rowIdx}-${colIdx}`}>\n {loadingSkeletonCell ? (\n loadingSkeletonCell({\n rowIndex: rowIdx,\n colIndex: colIdx,\n columnId: col.id ?? String(colIdx),\n })\n ) : (\n <Skeleton className={styles.skeletonCell} />\n )}\n </TableCell>\n ))}\n </TableRow>\n ))\n ) : isEmpty ? (\n <TableRow data-slot=\"data-table-empty-row\">\n <TableCell colSpan={colCount} className={styles.emptyCell}>\n {emptyState ?? defaultEmpty}\n </TableCell>\n </TableRow>\n ) : hasRows ? (\n rows!.map((item) => {\n if (item.kind === \"group\") {\n return (\n <TableRow\n key={`group-${item.id}`}\n data-slot=\"data-table-group-row\"\n className={styles.groupRow}\n >\n <TableCell\n colSpan={colCount}\n className={styles.groupCell}\n >\n {groupRowRenderer\n ? groupRowRenderer(item)\n : defaultGroupRowContent(item)}\n </TableCell>\n </TableRow>\n )\n }\n const tsRow = table.getRow(item.id)\n const rowProps = getDataRowProps(item.row)\n return (\n <TableRow\n key={item.id}\n data-state={tsRow.getIsSelected() ? \"selected\" : undefined}\n {...rowProps}\n >\n {tsRow.getVisibleCells().map((cell) => (\n <TableCell key={cell.id}>\n {flexRender(\n cell.column.columnDef.cell,\n cell.getContext()\n )}\n </TableCell>\n ))}\n </TableRow>\n )\n })\n ) : pageRows.length === 0 ? (\n <TableRow data-slot=\"data-table-empty-row\">\n <TableCell colSpan={colCount} className={styles.emptyCell}>\n {emptyState ?? defaultEmpty}\n </TableCell>\n </TableRow>\n ) : (\n pageRows.map((row) => {\n const rowProps = getDataRowProps(row.original)\n return (\n <TableRow\n key={row.id}\n data-state={row.getIsSelected() ? \"selected\" : undefined}\n {...rowProps}\n >\n {row.getVisibleCells().map((cell) => (\n <TableCell key={cell.id}>\n {flexRender(\n cell.column.columnDef.cell,\n cell.getContext()\n )}\n </TableCell>\n ))}\n </TableRow>\n )\n })\n )}\n </TableBody>\n </Table>\n\n {!hasRows && (\n <div className={styles.footer} data-slot=\"data-table-footer\">\n <div className={styles.footerInfo} aria-live=\"polite\">\n {totalRows === 0\n ? \"No results\"\n : `Showing ${firstRow} to ${lastRow} of ${totalRows}`}\n </div>\n <div className={styles.footerControls}>\n <label className={styles.pageSizeLabel}>\n <span className={styles.pageSizeLabelText}>Rows per page</span>\n <select\n className={styles.pageSizeSelect}\n value={currentPageSize}\n onChange={(e) => table.setPageSize(Number(e.target.value))}\n aria-label=\"Rows per page\"\n >\n {pageSizeOptions.map((opt) => (\n <option key={opt} value={opt}>\n {opt}\n </option>\n ))}\n </select>\n </label>\n <div className={styles.pageNav}>\n <span className={styles.pageCounter}>\n Page {pageCount === 0 ? 0 : pageIndex + 1} of {pageCount}\n </span>\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n onClick={() => table.previousPage()}\n disabled={!table.getCanPreviousPage()}\n aria-label=\"Previous page\"\n >\n <CaretLeftIcon weight=\"bold\" aria-hidden=\"true\" />\n </Button>\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n onClick={() => table.nextPage()}\n disabled={!table.getCanNextPage()}\n aria-label=\"Next page\"\n >\n <CaretRightIcon weight=\"bold\" aria-hidden=\"true\" />\n </Button>\n </div>\n </div>\n </div>\n )}\n </div>\n )\n}\n\n// forwardRef with generics — preserve TData through the cast\nconst DataTable = React.forwardRef(DataTableInner) as <\n TData,\n TValue = unknown,\n>(\n props: DataTableProps<TData, TValue> & {\n ref?: React.ForwardedRef<HTMLDivElement>\n }\n) => ReturnType<typeof DataTableInner>\n\n;(DataTable as unknown as { displayName: string }).displayName = \"DataTable\"\n\nexport { DataTable }\n"
2538
2538
  },
2539
2539
  {
2540
2540
  "path": "components/ui/data-table/data-table.module.css",
@@ -2936,7 +2936,7 @@
2936
2936
  {
2937
2937
  "path": "components/ui/status-badge/status-badge.tsx",
2938
2938
  "type": "registry:ui",
2939
- "content": "import * as React from \"react\"\nimport { cn } from \"../../../lib/utils\"\nimport { Badge } from \"../badge/badge\"\nimport type { BadgeProps } from \"../badge/badge\"\nimport styles from \"./status-badge.module.css\"\n\nexport type StatusBadgeStatus =\n | \"healthy\"\n | \"degraded\"\n | \"down\"\n | \"failed\"\n | \"running\"\n | \"pending\"\n | \"queued\"\n | \"idle\"\n | \"complete\"\n | \"live\"\n | \"warn\"\n | \"scheduled\"\n | \"sold\"\n | \"draft\"\n | \"prospect\"\n | \"pitched\"\n | \"contracted\"\n | \"active\"\n | \"paused\"\n | \"completed\"\n | \"archived\"\n\nexport type StatusBadgeTone = \"subtle\" | \"filled\"\n\nexport interface StatusBadgeProps\n extends Omit<React.HTMLAttributes<HTMLSpanElement>, \"children\"> {\n /** Semantic admin status. Drives both color and default label. */\n status: StatusBadgeStatus\n /** Visible text. Defaults to the capitalized status key. */\n label?: React.ReactNode\n /** Which Badge variant family to use. Defaults to \"subtle\". */\n tone?: StatusBadgeTone\n /** Render the leading indicator dot. Defaults to true. */\n indicator?: boolean\n /** Animate the indicator dot with a soft pulse. Defaults to false. */\n pulse?: boolean\n}\n\n/**\n * Default human-readable labels for each status. Exported so consumers can\n * localize or override without reimplementing the map.\n */\nexport const statusBadgeLabels: Record<StatusBadgeStatus, string> = {\n healthy: \"Healthy\",\n degraded: \"Degraded\",\n down: \"Down\",\n failed: \"Failed\",\n running: \"Running\",\n pending: \"Pending\",\n queued: \"Queued\",\n idle: \"Idle\",\n complete: \"Complete\",\n live: \"Live\",\n warn: \"Warn\",\n scheduled: \"Scheduled\",\n sold: \"Sold\",\n draft: \"Draft\",\n prospect: \"Prospect\",\n pitched: \"Pitched\",\n contracted: \"Contracted\",\n active: \"Active\",\n paused: \"Paused\",\n completed: \"Completed\",\n archived: \"Archived\",\n}\n\ntype BadgeVariant = NonNullable<BadgeProps[\"variant\"]>\n\n/**\n * Color group keyed on the semantic status. Used to pick the indicator dot\n * class and the underlying Badge variant.\n */\ntype StatusColorGroup =\n | \"success\"\n | \"warning\"\n | \"destructive\"\n | \"info\"\n | \"neutral\"\n\nconst STATUS_COLOR_GROUP: Record<StatusBadgeStatus, StatusColorGroup> = {\n healthy: \"success\",\n complete: \"success\",\n degraded: \"warning\",\n pending: \"warning\",\n down: \"destructive\",\n failed: \"destructive\",\n running: \"info\",\n queued: \"neutral\",\n idle: \"neutral\",\n // Admin-ui event tones — map to existing semantic groups.\n // live: active/in-progress positive event → success accent\n // warn: needs attention but not failing → warning\n // scheduled: upcoming/planned, visually grouped with draft → neutral\n // sold: positive completed outcome → success\n // draft: unpublished/muted → neutral\n live: \"success\",\n warn: \"warning\",\n scheduled: \"neutral\",\n sold: \"success\",\n draft: \"neutral\",\n // CRM / pipeline stages (VI-492) — bind to existing semantic groups.\n // No new tokens: stages that share a meaning share a color.\n // prospect: new informational lead → info\n // pitched: awaiting response, needs attention → warning\n // contracted / active / completed: positive, in-good-standing → success\n // paused: temporarily on hold, needs attention → warning\n // archived: closed/muted, grouped with draft → neutral\n prospect: \"info\",\n pitched: \"warning\",\n contracted: \"success\",\n active: \"success\",\n paused: \"warning\",\n completed: \"success\",\n archived: \"neutral\",\n}\n\nconst SUBTLE_VARIANT: Record<StatusColorGroup, BadgeVariant> = {\n success: \"success\",\n warning: \"warning\",\n destructive: \"destructive\",\n info: \"info\",\n neutral: \"neutral\",\n}\n\nconst FILLED_VARIANT: Record<StatusColorGroup, BadgeVariant> = {\n success: \"filled-success\",\n warning: \"filled-warning\",\n destructive: \"filled-destructive\",\n info: \"filled-info\",\n neutral: \"filled-neutral\",\n}\n\nconst INDICATOR_CLASS: Record<StatusColorGroup, string> = {\n success: styles.indicatorSuccess,\n warning: styles.indicatorWarning,\n destructive: styles.indicatorDestructive,\n info: styles.indicatorInfo,\n neutral: styles.indicatorNeutral,\n}\n\nconst StatusBadge = React.forwardRef<HTMLSpanElement, StatusBadgeProps>(\n (\n {\n className,\n status,\n label,\n tone = \"subtle\",\n indicator = true,\n pulse = false,\n ...props\n },\n ref\n ) => {\n const group = STATUS_COLOR_GROUP[status]\n const variant: BadgeVariant =\n tone === \"filled\" ? FILLED_VARIANT[group] : SUBTLE_VARIANT[group]\n const visibleLabel = label ?? statusBadgeLabels[status]\n\n return (\n <Badge\n ref={ref}\n variant={variant}\n data-slot=\"status-badge\"\n data-status={status}\n data-tone={tone}\n className={cn(className)}\n {...props}\n >\n {indicator ? (\n <span\n data-slot=\"status-badge-indicator\"\n aria-hidden=\"true\"\n className={cn(\n styles.indicator,\n INDICATOR_CLASS[group],\n pulse && styles.pulse\n )}\n />\n ) : null}\n <span className={styles.srOnly}>Status: </span>\n <span data-slot=\"status-badge-label\">{visibleLabel}</span>\n </Badge>\n )\n }\n)\nStatusBadge.displayName = \"StatusBadge\"\n\nexport { StatusBadge }\n"
2939
+ "content": "import * as React from \"react\"\nimport { cn } from \"../../../lib/utils\"\nimport { Badge } from \"../badge/badge\"\nimport type { BadgeProps } from \"../badge/badge\"\nimport styles from \"./status-badge.module.css\"\n\nexport type StatusBadgeStatus =\n | \"healthy\"\n | \"degraded\"\n | \"down\"\n | \"failed\"\n | \"running\"\n | \"pending\"\n | \"queued\"\n | \"idle\"\n | \"complete\"\n | \"live\"\n | \"warn\"\n | \"scheduled\"\n | \"sold\"\n | \"draft\"\n | \"prospect\"\n | \"pitched\"\n | \"contracted\"\n | \"active\"\n | \"paused\"\n | \"completed\"\n | \"archived\"\n\nexport type StatusBadgeTone = \"subtle\" | \"filled\"\n\nexport interface StatusBadgeProps\n extends Omit<React.HTMLAttributes<HTMLSpanElement>, \"children\"> {\n /** Semantic admin status. Drives both color and default label. */\n status: StatusBadgeStatus\n /** Visible text. Defaults to the capitalized status key. */\n label?: React.ReactNode\n /** Which Badge variant family to use. Defaults to \"subtle\". */\n tone?: StatusBadgeTone\n /** Render the leading indicator dot. Defaults to true. */\n indicator?: boolean\n /** Animate the indicator dot with a soft pulse. Defaults to false. */\n pulse?: boolean\n}\n\n/**\n * Default human-readable labels for each status. Exported so consumers can\n * localize or override without reimplementing the map.\n */\nexport const statusBadgeLabels: Record<StatusBadgeStatus, string> = {\n healthy: \"Healthy\",\n degraded: \"Degraded\",\n down: \"Down\",\n failed: \"Failed\",\n running: \"Running\",\n pending: \"Pending\",\n queued: \"Queued\",\n idle: \"Idle\",\n complete: \"Complete\",\n live: \"Live\",\n warn: \"Warn\",\n scheduled: \"Scheduled\",\n sold: \"Sold\",\n draft: \"Draft\",\n prospect: \"Prospect\",\n pitched: \"Pitched\",\n contracted: \"Contracted\",\n active: \"Active\",\n paused: \"Paused\",\n completed: \"Completed\",\n archived: \"Archived\",\n}\n\ntype BadgeVariant = NonNullable<BadgeProps[\"variant\"]>\n\n/**\n * Color group keyed on the semantic status. Used to pick the indicator dot\n * class and the underlying Badge variant.\n */\ntype StatusColorGroup =\n | \"success\"\n | \"warning\"\n | \"destructive\"\n | \"info\"\n | \"neutral\"\n\nconst STATUS_COLOR_GROUP: Record<StatusBadgeStatus, StatusColorGroup> = {\n healthy: \"success\",\n complete: \"success\",\n degraded: \"warning\",\n pending: \"warning\",\n down: \"destructive\",\n failed: \"destructive\",\n running: \"info\",\n queued: \"neutral\",\n idle: \"neutral\",\n // Admin-ui event tones — map to existing semantic groups.\n // live: active/in-progress positive event → success accent\n // warn: needs attention but not failing → warning\n // scheduled: upcoming/committed, distinct from draft's muted grey info\n // sold: positive completed outcome → success\n // draft: unpublished/muted → neutral\n live: \"success\",\n warn: \"warning\",\n scheduled: \"info\",\n sold: \"success\",\n draft: \"neutral\",\n // CRM / pipeline stages (VI-492) — bind to existing semantic groups.\n // No new tokens: stages that share a meaning share a color.\n // prospect: new informational lead → info\n // pitched: awaiting response, needs attention → warning\n // contracted / active / completed: positive, in-good-standing → success\n // paused: temporarily on hold, needs attention → warning\n // archived: closed/muted, grouped with draft → neutral\n prospect: \"info\",\n pitched: \"warning\",\n contracted: \"success\",\n active: \"success\",\n paused: \"warning\",\n completed: \"success\",\n archived: \"neutral\",\n}\n\nconst SUBTLE_VARIANT: Record<StatusColorGroup, BadgeVariant> = {\n success: \"success\",\n warning: \"warning\",\n destructive: \"destructive\",\n info: \"info\",\n neutral: \"neutral\",\n}\n\nconst FILLED_VARIANT: Record<StatusColorGroup, BadgeVariant> = {\n success: \"filled-success\",\n warning: \"filled-warning\",\n destructive: \"filled-destructive\",\n info: \"filled-info\",\n neutral: \"filled-neutral\",\n}\n\nconst INDICATOR_CLASS: Record<StatusColorGroup, string> = {\n success: styles.indicatorSuccess,\n warning: styles.indicatorWarning,\n destructive: styles.indicatorDestructive,\n info: styles.indicatorInfo,\n neutral: styles.indicatorNeutral,\n}\n\nconst StatusBadge = React.forwardRef<HTMLSpanElement, StatusBadgeProps>(\n (\n {\n className,\n status,\n label,\n tone = \"subtle\",\n indicator = true,\n pulse = false,\n ...props\n },\n ref\n ) => {\n const group = STATUS_COLOR_GROUP[status]\n const variant: BadgeVariant =\n tone === \"filled\" ? FILLED_VARIANT[group] : SUBTLE_VARIANT[group]\n const visibleLabel = label ?? statusBadgeLabels[status]\n\n return (\n <Badge\n ref={ref}\n variant={variant}\n data-slot=\"status-badge\"\n data-status={status}\n data-tone={tone}\n className={cn(className)}\n {...props}\n >\n {indicator ? (\n <span\n data-slot=\"status-badge-indicator\"\n aria-hidden=\"true\"\n className={cn(\n styles.indicator,\n INDICATOR_CLASS[group],\n pulse && styles.pulse\n )}\n />\n ) : null}\n <span className={styles.srOnly}>Status: </span>\n <span data-slot=\"status-badge-label\">{visibleLabel}</span>\n </Badge>\n )\n }\n)\nStatusBadge.displayName = \"StatusBadge\"\n\nexport { StatusBadge }\n"
2940
2940
  },
2941
2941
  {
2942
2942
  "path": "components/ui/status-badge/status-badge.module.css",
@@ -3858,7 +3858,9 @@
3858
3858
  "@loworbitstudio/visor-core"
3859
3859
  ],
3860
3860
  "registryDependencies": [
3861
- "utils",
3861
+ "utils"
3862
+ ],
3863
+ "suggestedDependencies": [
3862
3864
  "breadcrumb",
3863
3865
  "dropdown-menu",
3864
3866
  "sidebar"
@@ -3934,6 +3936,34 @@
3934
3936
  }
3935
3937
  ]
3936
3938
  },
3939
+ {
3940
+ "name": "admin-detail",
3941
+ "type": "registry:block",
3942
+ "description": "Full-page, read-oriented detail RECORD for the admin-shell main column. Composes an identity header (media + title + StatusBadge + actions), N key-value sections built on KeyValueList, an optional sensitive/reveal panel gated behind a Switch, and optional sub-list slots for ledgers or history. The full-page sibling to admin-detail-drawer.",
3943
+ "category": "admin",
3944
+ "dependencies": [
3945
+ "@loworbitstudio/visor-core"
3946
+ ],
3947
+ "registryDependencies": [
3948
+ "utils",
3949
+ "key-value-list",
3950
+ "status-badge",
3951
+ "switch",
3952
+ "separator"
3953
+ ],
3954
+ "files": [
3955
+ {
3956
+ "path": "blocks/admin-detail/admin-detail.tsx",
3957
+ "type": "registry:block",
3958
+ "content": "\"use client\"\n\nimport * as React from \"react\"\n\nimport { cn } from \"../../lib/utils\"\nimport {\n KeyValueList,\n type KeyValueItem,\n} from \"../../components/ui/key-value-list/key-value-list\"\nimport {\n StatusBadge,\n statusBadgeLabels,\n type StatusBadgeStatus,\n} from \"../../components/ui/status-badge/status-badge\"\nimport { Switch } from \"../../components/ui/switch/switch\"\nimport { Separator } from \"../../components/ui/separator/separator\"\nimport styles from \"./admin-detail.module.css\"\n\n// ─── Shared KeyValueList passthrough ─────────────────────────────────────────\n\n/** KeyValueList configuration shared by record sections and the sensitive panel. */\ninterface KeyValueConfig {\n /** Label/value pairs rendered via the composed `KeyValueList`. */\n items?: KeyValueItem[]\n /** Grid column count forwarded to `KeyValueList`. Defaults to 2. */\n columns?: 1 | 2 | 3 | 4\n /** Label placement forwarded to `KeyValueList`. Defaults to `stacked`. */\n orientation?: \"horizontal\" | \"stacked\"\n /** Row density forwarded to `KeyValueList`. Defaults to `editorial`. */\n density?: \"compact\" | \"default\" | \"editorial\"\n}\n\n// ─── Types ───────────────────────────────────────────────────────────────────\n\nexport interface AdminDetailSection extends KeyValueConfig {\n /** Stable identifier — becomes the section's DOM `id` anchor. */\n id?: string\n /** Small uppercase label rendered above the section title. */\n eyebrow?: React.ReactNode\n /** Section heading. */\n title?: React.ReactNode\n /** Supporting copy rendered below the section title. */\n description?: React.ReactNode\n /** Right-aligned action slot for the section header (edit link, menu, etc.). */\n actions?: React.ReactNode\n /**\n * Arbitrary sub-list content rendered below the key-value pairs — invoice\n * ledger rows, booking history, or any bespoke table. Renders after `items`.\n */\n content?: React.ReactNode\n}\n\nexport interface AdminDetailSensitivePanel extends KeyValueConfig {\n /** Stable identifier — becomes the panel's DOM `id` anchor. */\n id?: string\n /** Small uppercase label rendered above the panel title. */\n eyebrow?: React.ReactNode\n /** Panel heading — e.g. \"Tax & Banking\". */\n title?: React.ReactNode\n /** Supporting copy rendered below the panel title. */\n description?: React.ReactNode\n /** Extra content revealed alongside `items` when the panel is unlocked. */\n content?: React.ReactNode\n /** Label paired with the reveal switch. Defaults to \"Reveal\". */\n revealLabel?: React.ReactNode\n /** Note shown while the panel is hidden. Defaults to \"Hidden for privacy.\" */\n hiddenNote?: React.ReactNode\n /** Controlled reveal state. Omit for uncontrolled behavior. */\n revealed?: boolean\n /** Reveal-state change handler (fires in both controlled and uncontrolled modes). */\n onRevealedChange?: (revealed: boolean) => void\n /** Initial reveal state when uncontrolled. Defaults to false. */\n defaultRevealed?: boolean\n}\n\nexport interface AdminDetailProps\n extends Omit<React.HTMLAttributes<HTMLDivElement>, \"title\"> {\n // ── Identity header ───────────────────────────────────────────────────────\n /** Small uppercase label rendered above the record title. */\n eyebrow?: React.ReactNode\n /** Record title — the primary identity of the page. */\n title: React.ReactNode\n /** Supporting line beneath the title (email, handle, category, etc.). */\n subtitle?: React.ReactNode\n /** Leading media slot — Avatar, logo plate, or icon. */\n media?: React.ReactNode\n /**\n * Record status. A `StatusBadgeStatus` string renders a composed\n * `StatusBadge`; any other node renders as-is.\n */\n status?: StatusBadgeStatus | React.ReactNode\n /** Breadcrumb node rendered above the identity row. */\n breadcrumb?: React.ReactNode\n /** Right-aligned header action slot (edit, archive, overflow menu). */\n actions?: React.ReactNode\n /** Replace the default identity header entirely with custom chrome. */\n header?: React.ReactNode\n /** Suppress the hairline divider beneath the identity header. */\n hideHeaderDivider?: boolean\n\n // ── Body ──────────────────────────────────────────────────────────────────\n /** Key-value record sections, each composing a `KeyValueList`. */\n sections?: AdminDetailSection[]\n /** Optional sensitive/reveal panel gated behind a reveal switch. */\n sensitive?: AdminDetailSensitivePanel\n /** Arbitrary trailing content appended after the sections. */\n children?: React.ReactNode\n\n // ── Layout ────────────────────────────────────────────────────────────────\n /** Max-width of the record column. Number is treated as pixels. */\n maxWidth?: number | string\n}\n\n// ─── Helpers ─────────────────────────────────────────────────────────────────\n\nfunction isStatusBadgeStatus(\n value: React.ReactNode\n): value is StatusBadgeStatus {\n return typeof value === \"string\" && value in statusBadgeLabels\n}\n\nfunction resolveSize(\n value: number | string | undefined,\n fallback: string\n): string {\n if (value == null) return fallback\n return typeof value === \"number\" ? `${value}px` : value\n}\n\n// ─── Section renderer ────────────────────────────────────────────────────────\n\nfunction SectionHeader({\n eyebrow,\n title,\n description,\n actions,\n}: {\n eyebrow?: React.ReactNode\n title?: React.ReactNode\n description?: React.ReactNode\n actions?: React.ReactNode\n}) {\n if (!eyebrow && !title && !description && !actions) return null\n return (\n <div className={styles.sectionHeader} data-slot=\"admin-detail-section-header\">\n <div className={styles.sectionHeaderText}>\n {eyebrow ? (\n <p className={styles.sectionEyebrow}>{eyebrow}</p>\n ) : null}\n {title ? <h2 className={styles.sectionTitle}>{title}</h2> : null}\n {description ? (\n <p className={styles.sectionDescription}>{description}</p>\n ) : null}\n </div>\n {actions ? (\n <div className={styles.sectionActions}>{actions}</div>\n ) : null}\n </div>\n )\n}\n\nfunction RecordSection({ section }: { section: AdminDetailSection }) {\n const hasItems = section.items != null && section.items.length > 0\n return (\n <section\n id={section.id}\n className={styles.section}\n data-slot=\"admin-detail-section\"\n >\n <SectionHeader\n eyebrow={section.eyebrow}\n title={section.title}\n description={section.description}\n actions={section.actions}\n />\n {hasItems ? (\n <KeyValueList\n items={section.items!}\n columns={section.columns ?? 2}\n orientation={section.orientation ?? \"stacked\"}\n density={section.density ?? \"editorial\"}\n />\n ) : null}\n {section.content}\n </section>\n )\n}\n\n// ─── Sensitive panel ─────────────────────────────────────────────────────────\n\nfunction SensitivePanel({ panel }: { panel: AdminDetailSensitivePanel }) {\n const isControlled = panel.revealed !== undefined\n const [internalRevealed, setInternalRevealed] = React.useState(\n panel.defaultRevealed ?? false\n )\n const revealed = isControlled\n ? (panel.revealed as boolean)\n : internalRevealed\n const switchId = React.useId()\n const contentId = React.useId()\n\n const handleChange = React.useCallback(\n (next: boolean) => {\n if (!isControlled) setInternalRevealed(next)\n panel.onRevealedChange?.(next)\n },\n [isControlled, panel]\n )\n\n const hasItems = panel.items != null && panel.items.length > 0\n const revealLabel = panel.revealLabel ?? \"Reveal\"\n const hiddenNote = panel.hiddenNote ?? \"Hidden for privacy.\"\n\n return (\n <section\n id={panel.id}\n className={styles.sensitive}\n data-slot=\"admin-detail-sensitive\"\n data-revealed={revealed ? \"\" : undefined}\n >\n <div className={styles.sensitiveHeader}>\n <div className={styles.sectionHeaderText}>\n {panel.eyebrow ? (\n <p className={styles.sectionEyebrow}>{panel.eyebrow}</p>\n ) : null}\n {panel.title ? (\n <h2 className={styles.sectionTitle}>{panel.title}</h2>\n ) : null}\n {panel.description ? (\n <p className={styles.sectionDescription}>{panel.description}</p>\n ) : null}\n </div>\n <div\n className={styles.revealControl}\n data-slot=\"admin-detail-reveal\"\n >\n <label htmlFor={switchId} className={styles.revealLabel}>\n {revealLabel}\n </label>\n <Switch\n id={switchId}\n checked={revealed}\n onCheckedChange={handleChange}\n aria-controls={contentId}\n />\n </div>\n </div>\n\n <div\n id={contentId}\n className={styles.sensitiveBody}\n data-slot=\"admin-detail-sensitive-body\"\n >\n {revealed ? (\n <>\n {hasItems ? (\n <KeyValueList\n items={panel.items!}\n columns={panel.columns ?? 2}\n orientation={panel.orientation ?? \"stacked\"}\n density={panel.density ?? \"editorial\"}\n />\n ) : null}\n {panel.content}\n </>\n ) : (\n <p className={styles.sensitiveNote}>{hiddenNote}</p>\n )}\n </div>\n </section>\n )\n}\n\n// ─── AdminDetail ─────────────────────────────────────────────────────────────\n\nconst AdminDetail = React.forwardRef<HTMLDivElement, AdminDetailProps>(\n function AdminDetail(\n {\n eyebrow,\n title,\n subtitle,\n media,\n status,\n breadcrumb,\n actions,\n header,\n hideHeaderDivider = false,\n sections,\n sensitive,\n children,\n maxWidth,\n className,\n style,\n ...rest\n },\n ref\n ) {\n const resolvedStatus = isStatusBadgeStatus(status) ? (\n <StatusBadge status={status} />\n ) : (\n status\n )\n\n const rootStyle = {\n ...style,\n [\"--admin-detail-max-width\" as string]: resolveSize(maxWidth, \"none\"),\n } as React.CSSProperties\n\n // Collect body regions so dividers interleave cleanly.\n const regions: React.ReactNode[] = []\n sections?.forEach((section, i) => {\n regions.push(\n <RecordSection key={section.id ?? `section-${i}`} section={section} />\n )\n })\n if (sensitive) {\n regions.push(\n <SensitivePanel key={sensitive.id ?? \"sensitive\"} panel={sensitive} />\n )\n }\n if (children != null) {\n regions.push(\n <div\n key=\"extra\"\n className={styles.section}\n data-slot=\"admin-detail-extra\"\n >\n {children}\n </div>\n )\n }\n\n return (\n <div\n ref={ref}\n className={cn(styles.root, className)}\n style={rootStyle}\n data-slot=\"admin-detail\"\n {...rest}\n >\n {header ?? (\n <header\n className={cn(\n styles.identity,\n !hideHeaderDivider && styles.identityDivided\n )}\n data-slot=\"admin-detail-header\"\n >\n {breadcrumb ? (\n <div\n className={styles.breadcrumb}\n data-slot=\"admin-detail-breadcrumb\"\n >\n {breadcrumb}\n </div>\n ) : null}\n <div className={styles.identityRow}>\n {media ? (\n <div\n className={styles.media}\n data-slot=\"admin-detail-media\"\n >\n {media}\n </div>\n ) : null}\n <div className={styles.identityText}>\n {eyebrow ? (\n <p className={styles.eyebrow}>{eyebrow}</p>\n ) : null}\n <div className={styles.titleRow}>\n <h1\n className={styles.title}\n data-slot=\"admin-detail-title\"\n >\n {title}\n </h1>\n {resolvedStatus ? (\n <span\n className={styles.status}\n data-slot=\"admin-detail-status\"\n >\n {resolvedStatus}\n </span>\n ) : null}\n </div>\n {subtitle ? (\n <p className={styles.subtitle}>{subtitle}</p>\n ) : null}\n </div>\n {actions ? (\n <div\n className={styles.actions}\n data-slot=\"admin-detail-actions\"\n >\n {actions}\n </div>\n ) : null}\n </div>\n </header>\n )}\n\n {regions.length > 0 ? (\n <div className={styles.body} data-slot=\"admin-detail-body\">\n {regions.map((node, i) => (\n <React.Fragment key={i}>\n {i > 0 ? (\n <Separator className={styles.divider} decorative />\n ) : null}\n {node}\n </React.Fragment>\n ))}\n </div>\n ) : null}\n </div>\n )\n }\n)\n\nAdminDetail.displayName = \"AdminDetail\"\n\nexport { AdminDetail }\n"
3959
+ },
3960
+ {
3961
+ "path": "blocks/admin-detail/admin-detail.module.css",
3962
+ "type": "registry:block",
3963
+ "content": "/* Admin Detail\n * Full-page, read-oriented detail RECORD for the admin-shell main column.\n * Vertical stack: identity header → N key-value sections (composed\n * KeyValueList) → optional sensitive/reveal panel → optional sub-list slots,\n * separated by hairline dividers. The natural sibling to admin-list-page and\n * admin-detail-drawer — a page, never a drawer.\n */\n\n.root {\n container-type: inline-size;\n display: flex;\n flex-direction: column;\n width: 100%;\n max-width: var(--admin-detail-max-width, none);\n gap: var(--spacing-8, 2rem);\n color: var(--text-primary, #111827);\n}\n\n/* ── Identity header ──────────────────────────────────────────────── */\n.identity {\n display: flex;\n flex-direction: column;\n gap: var(--spacing-4, 1rem);\n}\n\n.identityDivided {\n padding-bottom: var(--spacing-8, 2rem);\n border-bottom: var(--stroke-width-thin, 1px) solid\n var(--border-muted, #e5e7eb);\n}\n\n.breadcrumb {\n min-width: 0;\n}\n\n.identityRow {\n display: flex;\n flex-direction: row;\n align-items: flex-start;\n gap: var(--spacing-4, 1rem);\n}\n\n.media {\n flex: 0 0 auto;\n display: flex;\n align-items: center;\n}\n\n.identityText {\n flex: 1 1 auto;\n min-width: 0;\n display: flex;\n flex-direction: column;\n gap: var(--spacing-2, 0.5rem);\n}\n\n.eyebrow {\n margin: 0;\n font-size: var(--font-size-xs, 0.6875rem);\n font-weight: var(--font-weight-medium, 500);\n letter-spacing: 0.12em;\n text-transform: uppercase;\n color: var(--text-tertiary, #6b7280);\n line-height: 1.4;\n}\n\n.titleRow {\n display: flex;\n flex-direction: row;\n align-items: center;\n flex-wrap: wrap;\n gap: var(--spacing-3, 0.75rem);\n min-width: 0;\n}\n\n.title {\n margin: 0;\n font-size: var(--font-size-2xl, 1.5rem);\n font-weight: var(--font-weight-semibold, 600);\n line-height: 1.2;\n color: var(--text-primary, #111827);\n}\n\n.status {\n flex: 0 0 auto;\n display: inline-flex;\n align-items: center;\n}\n\n.subtitle {\n margin: 0;\n font-size: var(--font-size-sm, 0.875rem);\n color: var(--text-secondary, #6b7280);\n line-height: 1.5;\n}\n\n.actions {\n flex: 0 0 auto;\n display: flex;\n flex-direction: row;\n align-items: center;\n gap: var(--spacing-2, 0.5rem);\n}\n\n/* ── Body / sections ──────────────────────────────────────────────── */\n.body {\n display: flex;\n flex-direction: column;\n gap: var(--spacing-8, 2rem);\n min-width: 0;\n}\n\n.section {\n display: flex;\n flex-direction: column;\n gap: var(--spacing-5, 1.25rem);\n min-width: 0;\n}\n\n.sectionHeader {\n display: flex;\n flex-direction: row;\n align-items: flex-start;\n justify-content: space-between;\n gap: var(--spacing-4, 1rem);\n min-width: 0;\n}\n\n.sectionHeaderText {\n flex: 1 1 auto;\n min-width: 0;\n display: flex;\n flex-direction: column;\n gap: var(--spacing-1, 0.25rem);\n}\n\n.sectionEyebrow {\n margin: 0;\n font-size: var(--font-size-xs, 0.6875rem);\n font-weight: var(--font-weight-medium, 500);\n letter-spacing: 0.12em;\n text-transform: uppercase;\n color: var(--text-tertiary, #6b7280);\n line-height: 1.4;\n}\n\n.sectionTitle {\n margin: 0;\n font-size: var(--font-size-lg, 1.125rem);\n font-weight: var(--font-weight-semibold, 600);\n line-height: 1.3;\n color: var(--text-primary, #111827);\n}\n\n.sectionDescription {\n margin: 0;\n font-size: var(--font-size-sm, 0.875rem);\n color: var(--text-secondary, #6b7280);\n line-height: 1.5;\n}\n\n.sectionActions {\n flex: 0 0 auto;\n display: flex;\n flex-direction: row;\n align-items: center;\n gap: var(--spacing-2, 0.5rem);\n}\n\n/* Divider between body regions. */\n.divider {\n margin: 0;\n}\n\n/* ── Sensitive / reveal panel ─────────────────────────────────────── */\n.sensitive {\n display: flex;\n flex-direction: column;\n gap: var(--spacing-4, 1rem);\n padding: var(--spacing-5, 1.25rem);\n border: var(--stroke-width-thin, 1px) solid var(--border-muted, #e5e7eb);\n border-radius: var(--radius-lg, 0.75rem);\n background-color: var(--surface-subtle, #f9fafb);\n}\n\n.sensitiveHeader {\n display: flex;\n flex-direction: row;\n align-items: flex-start;\n justify-content: space-between;\n gap: var(--spacing-4, 1rem);\n min-width: 0;\n}\n\n.revealControl {\n flex: 0 0 auto;\n display: flex;\n flex-direction: row;\n align-items: center;\n gap: var(--spacing-2, 0.5rem);\n}\n\n.revealLabel {\n font-size: var(--font-size-sm, 0.875rem);\n font-weight: var(--font-weight-medium, 500);\n color: var(--text-secondary, #6b7280);\n cursor: pointer;\n user-select: none;\n}\n\n.sensitiveBody {\n min-width: 0;\n}\n\n.sensitiveNote {\n margin: 0;\n font-size: var(--font-size-sm, 0.875rem);\n font-style: italic;\n color: var(--text-tertiary, #6b7280);\n}\n\n/* ── Responsive ───────────────────────────────────────────────────── */\n@container (max-width: 640px) {\n .identityRow {\n flex-direction: column;\n }\n\n .actions {\n width: 100%;\n }\n\n .sectionHeader,\n .sensitiveHeader {\n flex-direction: column;\n }\n}\n"
3964
+ }
3965
+ ]
3966
+ },
3937
3967
  {
3938
3968
  "name": "admin-detail-drawer",
3939
3969
  "type": "registry:block",
@@ -4103,6 +4133,31 @@
4103
4133
  }
4104
4134
  ]
4105
4135
  },
4136
+ {
4137
+ "name": "month-calendar",
4138
+ "type": "registry:block",
4139
+ "description": "Theme-portable month event-grid (scheduler) block: a 6×7 day-cell grid under a localized weekday header, with prev/next month navigation, a view-mode segmented control (Month / Week / Day), and per-cell event chips. Each chip carries a status-dot slot (success / warning / danger / info) and an optional series tint (1–5) keyed to the theme's chart ramp. Days outside the displayed month are dimmed; optional today and selected-day highlighting. Fully token-driven and theme-agnostic.",
4140
+ "category": "data-display",
4141
+ "dependencies": [
4142
+ "@loworbitstudio/visor-core",
4143
+ "@phosphor-icons/react"
4144
+ ],
4145
+ "registryDependencies": [
4146
+ "utils"
4147
+ ],
4148
+ "files": [
4149
+ {
4150
+ "path": "blocks/month-calendar/month-calendar.tsx",
4151
+ "type": "registry:block",
4152
+ "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { CaretLeft, CaretRight } from \"@phosphor-icons/react\"\n\nimport { cn } from \"../../lib/utils\"\nimport styles from \"./month-calendar.module.css\"\n\n/**\n * Status tone driving the leading dot on an event chip. Each tone binds to a\n * Visor semantic status token so the dot adopts the active theme's palette.\n */\nexport type MonthCalendarStatus =\n | \"default\"\n | \"success\"\n | \"warning\"\n | \"danger\"\n | \"info\"\n\n/** Series tint index — keyed to the theme's five-stop chart ramp. */\nexport type MonthCalendarSeries = 1 | 2 | 3 | 4 | 5\n\nexport interface MonthCalendarEvent {\n /** Stable key for the event. */\n id: string\n /**\n * The day this event lands in. Only the calendar date (year/month/day) is\n * used for placement — any time component is ignored.\n */\n date: Date\n /** Chip label. */\n title: string\n /**\n * Status tone for the leading dot. Omit for a neutral dot. Binds to the\n * `--surface-{success,warning,error,info}-default` semantic tokens.\n */\n status?: MonthCalendarStatus\n /**\n * Series tint (1–5). Events sharing an index render with the same background\n * tint and accent bar, keyed to the theme's chart color ramp — the standard\n * way to color-code a recurring series or a resource lane.\n */\n series?: MonthCalendarSeries\n}\n\nexport interface MonthCalendarViewOption {\n /** Machine value emitted via `onViewChange`. */\n value: string\n /** Human label rendered in the segment. */\n label: React.ReactNode\n}\n\nexport interface MonthCalendarProps\n extends Omit<React.HTMLAttributes<HTMLDivElement>, \"onSelect\"> {\n /** Any date within the month to display. Controlled. */\n month: Date\n /**\n * Fired with the first day of the previous / next month when the month-nav\n * arrows are used.\n */\n onMonthChange?: (month: Date) => void\n /** Events to place into day cells. */\n events?: MonthCalendarEvent[]\n /**\n * The day the grid marks as \"today\". Omit to mark none — the block never\n * reads the system clock itself, keeping server and client render identical\n * (no hydration mismatch). Pass `new Date()` from a client boundary to opt in.\n */\n today?: Date\n /** Selected day, highlighted distinctly from today. */\n selectedDate?: Date\n /** Fired when a day cell is activated (only when provided — cells are inert otherwise). */\n onSelectDate?: (date: Date) => void\n /** Fired when an event chip is activated (only when provided — chips are inert otherwise). */\n onEventSelect?: (event: MonthCalendarEvent) => void\n /** First column of the week: 0 = Sunday (default), 1 = Monday. */\n weekStartsOn?: 0 | 1\n /** Max chips shown per day before collapsing to a \"+N more\" row. Default 3. */\n maxChipsPerDay?: number\n /** BCP-47 locale for the month title and weekday headers. Default `\"en-US\"`. */\n locale?: string\n /** View-mode options for the segmented control. Default Month / Week / Day. */\n viewOptions?: MonthCalendarViewOption[]\n /** Active view value (controlled). Falls back to internal state when omitted. */\n view?: string\n /** Fired when a view-mode segment is chosen. */\n onViewChange?: (view: string) => void\n}\n\nconst DEFAULT_VIEW_OPTIONS: MonthCalendarViewOption[] = [\n { value: \"month\", label: \"Month\" },\n { value: \"week\", label: \"Week\" },\n { value: \"day\", label: \"Day\" },\n]\n\nconst DAYS_IN_GRID = 42\n// 2023-01-01 was a Sunday — a fixed anchor for deriving localized weekday names.\nconst WEEKDAY_ANCHOR_YEAR = 2023\n\nfunction firstOfMonth(date: Date): Date {\n return new Date(date.getFullYear(), date.getMonth(), 1)\n}\n\nfunction addMonths(date: Date, delta: number): Date {\n return new Date(date.getFullYear(), date.getMonth() + delta, 1)\n}\n\nfunction isSameDay(a: Date | undefined, b: Date): boolean {\n return (\n a !== undefined &&\n a.getFullYear() === b.getFullYear() &&\n a.getMonth() === b.getMonth() &&\n a.getDate() === b.getDate()\n )\n}\n\nfunction dayKey(date: Date): string {\n return `${date.getFullYear()}-${date.getMonth()}-${date.getDate()}`\n}\n\nconst MonthCalendar = React.forwardRef<HTMLDivElement, MonthCalendarProps>(\n function MonthCalendar(\n {\n month,\n onMonthChange,\n events = [],\n today,\n selectedDate,\n onSelectDate,\n onEventSelect,\n weekStartsOn = 0,\n maxChipsPerDay = 3,\n locale = \"en-US\",\n viewOptions = DEFAULT_VIEW_OPTIONS,\n view,\n onViewChange,\n className,\n ...rest\n },\n ref\n ) {\n const [internalView, setInternalView] = React.useState<string>(\n () => view ?? viewOptions[0]?.value ?? \"month\"\n )\n const activeView = view ?? internalView\n\n const handleViewSelect = React.useCallback(\n (next: string) => {\n if (view === undefined) setInternalView(next)\n onViewChange?.(next)\n },\n [view, onViewChange]\n )\n\n const monthLabel = React.useMemo(\n () =>\n new Intl.DateTimeFormat(locale, {\n month: \"long\",\n year: \"numeric\",\n }).format(month),\n [locale, month]\n )\n\n const weekdayLabels = React.useMemo(() => {\n const formatter = new Intl.DateTimeFormat(locale, { weekday: \"short\" })\n return Array.from({ length: 7 }, (_, i) =>\n formatter.format(\n new Date(WEEKDAY_ANCHOR_YEAR, 0, 1 + ((weekStartsOn + i) % 7))\n )\n )\n }, [locale, weekStartsOn])\n\n const cells = React.useMemo(() => {\n const first = firstOfMonth(month)\n const offset = (first.getDay() - weekStartsOn + 7) % 7\n return Array.from({ length: DAYS_IN_GRID }, (_, i) =>\n new Date(first.getFullYear(), first.getMonth(), 1 - offset + i)\n )\n }, [month, weekStartsOn])\n\n const eventsByDay = React.useMemo(() => {\n const map = new Map<string, MonthCalendarEvent[]>()\n for (const event of events) {\n const key = dayKey(event.date)\n const bucket = map.get(key)\n if (bucket) bucket.push(event)\n else map.set(key, [event])\n }\n return map\n }, [events])\n\n const dayLabelFormatter = React.useMemo(\n () =>\n new Intl.DateTimeFormat(locale, {\n month: \"long\",\n day: \"numeric\",\n year: \"numeric\",\n }),\n [locale]\n )\n\n const displayedMonth = month.getMonth()\n\n return (\n <div\n ref={ref}\n className={cn(styles.root, className)}\n data-slot=\"month-calendar\"\n {...rest}\n >\n <div className={styles.header} data-slot=\"month-calendar-header\">\n <div className={styles.nav} data-slot=\"month-calendar-nav\">\n <button\n type=\"button\"\n className={styles.navButton}\n onClick={() => onMonthChange?.(addMonths(month, -1))}\n disabled={!onMonthChange}\n aria-label=\"Previous month\"\n data-slot=\"month-calendar-prev\"\n >\n <CaretLeft weight=\"bold\" aria-hidden />\n </button>\n <h2 className={styles.monthLabel} data-slot=\"month-calendar-label\">\n {monthLabel}\n </h2>\n <button\n type=\"button\"\n className={styles.navButton}\n onClick={() => onMonthChange?.(addMonths(month, 1))}\n disabled={!onMonthChange}\n aria-label=\"Next month\"\n data-slot=\"month-calendar-next\"\n >\n <CaretRight weight=\"bold\" aria-hidden />\n </button>\n </div>\n\n {viewOptions.length > 0 ? (\n <div\n className={styles.segmented}\n role=\"group\"\n aria-label=\"Calendar view\"\n data-slot=\"month-calendar-view\"\n >\n {viewOptions.map((option) => (\n <button\n key={option.value}\n type=\"button\"\n className={styles.segment}\n data-active={option.value === activeView ? \"true\" : undefined}\n aria-pressed={option.value === activeView}\n onClick={() => handleViewSelect(option.value)}\n >\n {option.label}\n </button>\n ))}\n </div>\n ) : null}\n </div>\n\n <div className={styles.weekdays} data-slot=\"month-calendar-weekdays\">\n {weekdayLabels.map((label, i) => (\n <div key={i} className={styles.weekday} aria-hidden=\"true\">\n {label}\n </div>\n ))}\n </div>\n\n <div className={styles.grid} data-slot=\"month-calendar-grid\">\n {cells.map((cell) => {\n const dayEvents = eventsByDay.get(dayKey(cell)) ?? []\n const visible = dayEvents.slice(0, maxChipsPerDay)\n const overflow = dayEvents.length - visible.length\n const outside = cell.getMonth() !== displayedMonth\n const isToday = isSameDay(today, cell)\n const isSelected = isSameDay(selectedDate, cell)\n const dayLabel = dayLabelFormatter.format(cell)\n const cellLabel =\n dayEvents.length > 0\n ? `${dayLabel}, ${dayEvents.length} event${dayEvents.length === 1 ? \"\" : \"s\"}`\n : dayLabel\n\n const DayTag: React.ElementType = onSelectDate ? \"button\" : \"div\"\n\n return (\n <div\n key={dayKey(cell)}\n className={styles.cell}\n data-slot=\"month-calendar-day\"\n data-outside={outside ? \"true\" : undefined}\n data-today={isToday ? \"true\" : undefined}\n data-selected={isSelected ? \"true\" : undefined}\n >\n <DayTag\n className={styles.dayHead}\n {...(onSelectDate\n ? {\n type: \"button\" as const,\n onClick: () => onSelectDate(cell),\n \"aria-label\": cellLabel,\n \"aria-pressed\": isSelected,\n }\n : {})}\n data-slot=\"month-calendar-day-head\"\n >\n <span className={styles.dayNumber}>{cell.getDate()}</span>\n </DayTag>\n\n {dayEvents.length > 0 ? (\n <div\n className={styles.events}\n data-slot=\"month-calendar-day-events\"\n >\n {visible.map((event) => {\n const ChipTag: React.ElementType = onEventSelect\n ? \"button\"\n : \"div\"\n return (\n <ChipTag\n key={event.id}\n className={styles.chip}\n data-status={event.status ?? \"default\"}\n data-series={event.series}\n data-slot=\"month-calendar-event\"\n {...(onEventSelect\n ? {\n type: \"button\" as const,\n onClick: () => onEventSelect(event),\n }\n : {})}\n >\n <span\n className={styles.dot}\n data-status={event.status ?? \"default\"}\n aria-hidden=\"true\"\n />\n <span className={styles.chipLabel}>{event.title}</span>\n </ChipTag>\n )\n })}\n {overflow > 0 ? (\n <div className={styles.overflow}>+{overflow} more</div>\n ) : null}\n </div>\n ) : null}\n </div>\n )\n })}\n </div>\n </div>\n )\n }\n)\n\nMonthCalendar.displayName = \"MonthCalendar\"\n\nexport { MonthCalendar }\n"
4153
+ },
4154
+ {
4155
+ "path": "blocks/month-calendar/month-calendar.module.css",
4156
+ "type": "registry:block",
4157
+ "content": "/* Month Calendar\n * A theme-portable month event-grid (scheduler) block: a 6×7 day-cell grid\n * under a weekday header, with month navigation, a view-mode segmented control,\n * and per-cell event chips carrying a status dot and an optional series tint.\n *\n * Every color, size, spacing, stroke, radius, and motion value binds to a Visor\n * token so the block adopts the active theme without modification. Selectors are\n * anchored on a local class to satisfy the docs bundler's pure-selector rule.\n */\n\n.root {\n display: flex;\n flex-direction: column;\n background: var(--surface-card);\n border: var(--stroke-width-thin) solid var(--border-muted);\n border-radius: var(--radius-lg);\n overflow: hidden;\n color: var(--text-primary);\n}\n\n/* ── Header: month nav + view segmented control ── */\n.header {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n justify-content: space-between;\n gap: var(--spacing-3, 0.75rem);\n padding: var(--spacing-4, 1rem);\n}\n\n.nav {\n display: flex;\n align-items: center;\n gap: var(--spacing-2, 0.5rem);\n}\n\n.navButton {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n /* 32px — icon-button sizing; sizing tokens deferred (Rule 5 exception). */\n width: 2rem;\n height: 2rem;\n padding: 0;\n color: var(--text-secondary);\n background: transparent;\n border: var(--stroke-width-thin) solid transparent;\n border-radius: var(--radius-md);\n cursor: pointer;\n}\n\n.navButton:hover:not(:disabled) {\n background: var(--surface-interactive-hover);\n color: var(--text-primary);\n}\n\n.navButton:disabled {\n opacity: var(--opacity-40);\n cursor: default;\n}\n\n.navButton:focus-visible {\n outline: var(--focus-ring-width) solid var(--border-focus);\n outline-offset: var(--focus-ring-offset);\n}\n\n.monthLabel {\n margin: 0;\n min-width: 10ch;\n text-align: center;\n font-size: var(--font-size-lg, 1.125rem);\n font-weight: var(--font-weight-semibold, 600);\n color: var(--text-primary);\n}\n\n/* ── View-mode segmented control ── */\n.segmented {\n display: inline-flex;\n align-items: center;\n gap: var(--stroke-width-thin);\n padding: var(--spacing-1, 0.25rem);\n background: var(--surface-subtle);\n border: var(--stroke-width-thin) solid var(--border-muted);\n border-radius: var(--radius-md);\n}\n\n.segment {\n appearance: none;\n padding: var(--spacing-1, 0.25rem) var(--spacing-3, 0.75rem);\n font-size: var(--font-size-sm, 0.875rem);\n font-weight: var(--font-weight-medium, 500);\n color: var(--text-secondary);\n background: transparent;\n border: none;\n border-radius: var(--radius-sm);\n cursor: pointer;\n transition: color var(--motion-duration-fast, 100ms)\n var(--motion-easing-default, ease-in-out),\n background-color var(--motion-duration-fast, 100ms)\n var(--motion-easing-default, ease-in-out);\n}\n\n.segment:hover:not([data-active]) {\n color: var(--text-primary);\n}\n\n.segment[data-active] {\n color: var(--text-primary);\n background: var(--surface-card);\n box-shadow: var(--shadow-xs);\n}\n\n.segment:focus-visible {\n outline: var(--focus-ring-width) solid var(--border-focus);\n outline-offset: var(--focus-ring-offset);\n}\n\n/* ── Weekday header row ── */\n.weekdays {\n display: grid;\n grid-template-columns: repeat(7, minmax(0, 1fr));\n border-top: var(--stroke-width-thin) solid var(--border-muted);\n background: var(--surface-subtle);\n}\n\n.weekday {\n padding: var(--spacing-2, 0.5rem);\n text-align: center;\n font-size: var(--font-size-xs, 0.75rem);\n font-weight: var(--font-weight-medium, 500);\n letter-spacing: 0.04em;\n text-transform: uppercase;\n color: var(--text-tertiary);\n}\n\n/* ── 6×7 day grid ── */\n/* The hairline background shows through the 1px gaps to draw the grid lines. */\n.grid {\n display: grid;\n grid-template-columns: repeat(7, minmax(0, 1fr));\n grid-auto-rows: minmax(0, 1fr);\n gap: var(--stroke-width-thin);\n background: var(--border-muted);\n border-top: var(--stroke-width-thin) solid var(--border-muted);\n}\n\n.cell {\n display: flex;\n flex-direction: column;\n gap: var(--spacing-1, 0.25rem);\n /* 104px — day-cell min height; sizing tokens deferred (Rule 5 exception). */\n min-height: 6.5rem;\n padding: var(--spacing-1, 0.25rem);\n background: var(--surface-card);\n}\n\n.cell[data-outside] {\n background: var(--surface-subtle);\n}\n\n.cell[data-outside] .dayNumber {\n color: var(--text-disabled);\n}\n\n.cell[data-selected] {\n background: var(--interactive-primary-soft);\n}\n\n/* ── Day number head (button when the day is selectable) ── */\n.dayHead {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n align-self: flex-start;\n /* 24px — day-number badge sizing; sizing tokens deferred (Rule 5 exception). */\n min-width: 1.5rem;\n height: 1.5rem;\n padding: 0 var(--spacing-1, 0.25rem);\n font: inherit;\n color: inherit;\n background: transparent;\n border: none;\n border-radius: var(--radius-full);\n cursor: default;\n}\n\n.dayHead:is(button) {\n cursor: pointer;\n}\n\n.dayHead:is(button):hover {\n background: var(--surface-interactive-hover);\n}\n\n.dayHead:focus-visible {\n outline: var(--focus-ring-width) solid var(--border-focus);\n outline-offset: var(--focus-ring-offset);\n}\n\n.dayNumber {\n font-size: var(--font-size-sm, 0.875rem);\n font-weight: var(--font-weight-medium, 500);\n color: var(--text-secondary);\n font-variant-numeric: tabular-nums;\n}\n\n.cell[data-today] .dayHead {\n background: var(--interactive-primary-bg);\n}\n\n.cell[data-today] .dayNumber {\n color: var(--interactive-primary-text);\n font-weight: var(--font-weight-semibold, 600);\n}\n\n/* ── Event chips ── */\n.events {\n display: flex;\n flex-direction: column;\n gap: var(--spacing-1, 0.25rem);\n min-width: 0;\n}\n\n.chip {\n display: flex;\n align-items: center;\n gap: var(--spacing-1, 0.25rem);\n width: 100%;\n padding: var(--spacing-1, 0.25rem) var(--spacing-2, 0.5rem);\n font-size: var(--font-size-xs, 0.75rem);\n font-weight: var(--font-weight-medium, 500);\n text-align: start;\n color: var(--text-primary);\n background: var(--surface-subtle);\n border: none;\n border-radius: var(--radius-sm);\n cursor: default;\n}\n\n.chip:is(button) {\n cursor: pointer;\n}\n\n.chip:is(button):hover {\n background: var(--surface-interactive-hover);\n}\n\n.chip:focus-visible {\n outline: var(--focus-ring-width) solid var(--border-focus);\n outline-offset: var(--focus-ring-offset);\n}\n\n/* Series tint — background wash + leading accent bar keyed to the chart ramp. */\n.chip[data-series=\"1\"] {\n --mc-series: var(--chart-1);\n}\n.chip[data-series=\"2\"] {\n --mc-series: var(--chart-2);\n}\n.chip[data-series=\"3\"] {\n --mc-series: var(--chart-3);\n}\n.chip[data-series=\"4\"] {\n --mc-series: var(--chart-4);\n}\n.chip[data-series=\"5\"] {\n --mc-series: var(--chart-5);\n}\n\n.chip[data-series] {\n background: color-mix(in srgb, var(--mc-series) 16%, transparent);\n box-shadow: inset var(--stroke-width-medium) 0 0 0 var(--mc-series);\n}\n\n.chip[data-series]:is(button):hover {\n background: color-mix(in srgb, var(--mc-series) 28%, transparent);\n}\n\n.chipLabel {\n flex: 1;\n min-width: 0;\n overflow: hidden;\n white-space: nowrap;\n text-overflow: ellipsis;\n}\n\n/* ── Status dot ── */\n.dot {\n flex-shrink: 0;\n /* 8px dot — bound to the spacing scale. */\n width: var(--spacing-2, 0.5rem);\n height: var(--spacing-2, 0.5rem);\n border-radius: var(--radius-full);\n background: var(--text-tertiary);\n}\n\n.dot[data-status=\"success\"] {\n background: var(--surface-success-default);\n}\n.dot[data-status=\"warning\"] {\n background: var(--surface-warning-default);\n}\n.dot[data-status=\"danger\"] {\n background: var(--surface-error-default);\n}\n.dot[data-status=\"info\"] {\n background: var(--surface-info-default);\n}\n\n/* ── Overflow row ── */\n.overflow {\n padding: 0 var(--spacing-2, 0.5rem);\n font-size: var(--font-size-xs, 0.75rem);\n font-weight: var(--font-weight-medium, 500);\n color: var(--text-tertiary);\n}\n\n@media (prefers-reduced-motion: reduce) {\n .segment {\n transition: none;\n }\n}\n"
4158
+ }
4159
+ ]
4160
+ },
4106
4161
  {
4107
4162
  "name": "workspace-switcher",
4108
4163
  "type": "registry:block",
@@ -4982,7 +5037,7 @@
4982
5037
  {
4983
5038
  "path": "components/devtools/source-inspector/visor-component-names.generated.ts",
4984
5039
  "type": "registry:devtool",
4985
- "content": "// THIS FILE IS GENERATED BY scripts/generate-visor-component-names.ts.\n// Do not edit by hand. Re-run `npm run generate:component-names` after\n// adding, removing, or renaming a Visor component.\n//\n// Source of truth: registry/registry-{ui,blocks,deck,visual,devtools}.ts\n// Used by: components/devtools/source-inspector/* (VI-311)\n\nexport const VISOR_COMPONENT_NAMES: ReadonlySet<string> = new Set([\n \"AccessibilitySection\",\n \"AccessibilitySlide\",\n \"AccessibilitySpecimen\",\n \"Accordion\",\n \"AccordionContent\",\n \"AccordionItem\",\n \"AccordionTrigger\",\n \"ActivityFeed\",\n \"ActivityFeedContext\",\n \"ActivityFeedItem\",\n \"ActivityFeedRoot\",\n \"AdminDashboard\",\n \"AdminDetailDrawer\",\n \"AdminListPage\",\n \"AdminListPageInner\",\n \"AdminSettingsPage\",\n \"AdminShell\",\n \"AdminTabbedEditor\",\n \"AdminWizard\",\n \"Alert\",\n \"AlertActions\",\n \"AlertDescription\",\n \"AlertTitle\",\n \"AmbientGlow\",\n \"Avatar\",\n \"AvatarFallback\",\n \"AvatarImage\",\n \"AvatarStack\",\n \"Badge\",\n \"Banner\",\n \"BannerAction\",\n \"BannerDescription\",\n \"BannerTitle\",\n \"BentoGrid\",\n \"BentoTile\",\n \"BentoTileBody\",\n \"BentoTileDescription\",\n \"BentoTileFigure\",\n \"BentoTileHeadline\",\n \"BentoTileMedia\",\n \"BentoTileMeta\",\n \"BentoTileTitle\",\n \"Box\",\n \"Breadcrumb\",\n \"BreadcrumbEllipsis\",\n \"BreadcrumbItem\",\n \"BreadcrumbLink\",\n \"BreadcrumbList\",\n \"BreadcrumbPage\",\n \"BreadcrumbSeparator\",\n \"BrowserFrame\",\n \"BulkActionBar\",\n \"Button\",\n \"ButtonSpecimenSection\",\n \"ButtonSpecimenSlide\",\n \"Calendar\",\n \"Card\",\n \"CardContent\",\n \"CardDescription\",\n \"CardFooter\",\n \"CardGrid\",\n \"CardHeader\",\n \"CardLift\",\n \"CardTitle\",\n \"Carousel\",\n \"CarouselContent\",\n \"CarouselContext\",\n \"CarouselGallery\",\n \"CarouselItem\",\n \"CarouselNext\",\n \"CarouselPrevious\",\n \"ChallengeCard\",\n \"ChallengeCardAction\",\n \"ChallengeCardActions\",\n \"ChallengeCardBody\",\n \"ChallengeCardGate\",\n \"ChallengeCardHeader\",\n \"ChartContainer\",\n \"ChartContext\",\n \"ChartLegend\",\n \"ChartLegendContent\",\n \"ChartStyle\",\n \"ChartTooltip\",\n \"ChartTooltipContent\",\n \"Checkbox\",\n \"CheckGroup\",\n \"CheckRow\",\n \"Chip\",\n \"ChipGroup\",\n \"ChipGroupContext\",\n \"ChipGroupItem\",\n \"ChoiceChip\",\n \"ChromeButton\",\n \"ClosingSlide\",\n \"CodeBlock\",\n \"Collapsible\",\n \"CollapsibleContent\",\n \"CollapsibleTrigger\",\n \"ColorBar\",\n \"ColorPaletteSection\",\n \"ColorPicker\",\n \"ColorPickerSurface\",\n \"ColorSwatch\",\n \"ColorSwatchGrid\",\n \"Combobox\",\n \"ComboboxContent\",\n \"ComboboxContext\",\n \"ComboboxEmpty\",\n \"ComboboxGroup\",\n \"ComboboxInput\",\n \"ComboboxItem\",\n \"ComboboxSeparator\",\n \"Command\",\n \"CommandDialog\",\n \"CommandEmpty\",\n \"CommandGroup\",\n \"CommandInput\",\n \"CommandItem\",\n \"CommandList\",\n \"CommandLoading\",\n \"CommandSeparator\",\n \"CommandShortcut\",\n \"ComponentShowcaseContent\",\n \"ComponentShowcaseSection\",\n \"ComponentShowcaseSlide\",\n \"Composer\",\n \"ComposerContext\",\n \"ComposerField\",\n \"ComposerSend\",\n \"ComposerSpacer\",\n \"ComposerToolbar\",\n \"ComposerToolButton\",\n \"ConceptSlide\",\n \"ConfigurationPanel\",\n \"ConfirmDialog\",\n \"ConflictBanner\",\n \"Container\",\n \"ContextMenu\",\n \"ContextMenuCheckboxItem\",\n \"ContextMenuContent\",\n \"ContextMenuGroup\",\n \"ContextMenuItem\",\n \"ContextMenuLabel\",\n \"ContextMenuPortal\",\n \"ContextMenuRadioGroup\",\n \"ContextMenuRadioItem\",\n \"ContextMenuSeparator\",\n \"ContextMenuShortcut\",\n \"ContextMenuSub\",\n \"ContextMenuSubContent\",\n \"ContextMenuSubTrigger\",\n \"ContextMenuTrigger\",\n \"Controls\",\n \"CtaSection\",\n \"DataTable\",\n \"DataTableInner\",\n \"DatePicker\",\n \"DateRangePicker\",\n \"DeckContext\",\n \"DeckFooter\",\n \"DeckLayout\",\n \"DeckProvider\",\n \"DeckRenderer\",\n \"DesignSystemDeck\",\n \"DesignSystemSpecimen\",\n \"Dialog\",\n \"DialogClose\",\n \"DialogContent\",\n \"DialogDescription\",\n \"DialogFooter\",\n \"DialogHeader\",\n \"DialogOverlay\",\n \"DialogPortal\",\n \"DialogTitle\",\n \"DialogTrigger\",\n \"DotNav\",\n \"DropdownMenu\",\n \"DropdownMenuCheckboxItem\",\n \"DropdownMenuContent\",\n \"DropdownMenuGroup\",\n \"DropdownMenuItem\",\n \"DropdownMenuLabel\",\n \"DropdownMenuPortal\",\n \"DropdownMenuRadioGroup\",\n \"DropdownMenuRadioItem\",\n \"DropdownMenuSeparator\",\n \"DropdownMenuShortcut\",\n \"DropdownMenuSub\",\n \"DropdownMenuSubContent\",\n \"DropdownMenuSubTrigger\",\n \"DropdownMenuTrigger\",\n \"EditableBlock\",\n \"ElevationCard\",\n \"ElevationSlide\",\n \"EmptyState\",\n \"ErrorPlacard\",\n \"ExportMenu\",\n \"FeaturesGrid\",\n \"Field\",\n \"FieldDescription\",\n \"FieldError\",\n \"FieldLabel\",\n \"Fieldset\",\n \"FieldsetLegend\",\n \"FileUpload\",\n \"FilterBar\",\n \"FilterChip\",\n \"FontShowcase\",\n \"FontShowcaseGrid\",\n \"Footer\",\n \"FooterSection\",\n \"Form\",\n \"FormError\",\n \"FormErrorDescription\",\n \"FormErrorTitle\",\n \"FormField\",\n \"FormSpecimenSection\",\n \"FormSpecimenSlide\",\n \"FullscreenOverlay\",\n \"FullscreenOverlayContent\",\n \"FullscreenOverlayTrigger\",\n \"GrainOverlay\",\n \"Grid\",\n \"Header\",\n \"Heading\",\n \"HeroGlow\",\n \"HeroSection\",\n \"HeroSlide\",\n \"HoverCard\",\n \"HoverCardContent\",\n \"HoverCardTrigger\",\n \"IconGrid\",\n \"IconGridSection\",\n \"IconSizeRow\",\n \"IconsSlide\",\n \"Image\",\n \"InfographicBar\",\n \"Inline\",\n \"Input\",\n \"Kbd\",\n \"KeyValueList\",\n \"Label\",\n \"Landing\",\n \"Lightbox\",\n \"LightboxContent\",\n \"LightboxContext\",\n \"LightboxTrigger\",\n \"LoginForm\",\n \"Marquee\",\n \"MarqueeBandRenderer\",\n \"MatrixCell\",\n \"MatrixTable\",\n \"MatrixTableInner\",\n \"Menubar\",\n \"MenubarCheckboxItem\",\n \"MenubarContent\",\n \"MenubarGroup\",\n \"MenubarItem\",\n \"MenubarLabel\",\n \"MenubarMenu\",\n \"MenubarRadioGroup\",\n \"MenubarRadioItem\",\n \"MenubarSeparator\",\n \"MenubarShortcut\",\n \"MenubarSub\",\n \"MenubarSubContent\",\n \"MenubarSubTrigger\",\n \"MenubarTrigger\",\n \"MotionDuration\",\n \"MotionDurationSection\",\n \"MotionEasing\",\n \"MotionEasingSection\",\n \"MotionSlide\",\n \"NameRoster\",\n \"NameRosterItem\",\n \"Navbar\",\n \"NavbarBrand\",\n \"NavbarContent\",\n \"NavbarItem\",\n \"NavbarLink\",\n \"NavbarToggle\",\n \"NumberInput\",\n \"OfflineBanner\",\n \"OpacityBar\",\n \"OpacitySlide\",\n \"OTPInput\",\n \"PageHeader\",\n \"Pagination\",\n \"PaginationContent\",\n \"PaginationEllipsis\",\n \"PaginationItem\",\n \"PaginationLink\",\n \"PaginationNext\",\n \"PaginationPrevious\",\n \"PasswordInput\",\n \"PhoneInput\",\n \"Popover\",\n \"PopoverAnchor\",\n \"PopoverContent\",\n \"PopoverFooter\",\n \"PopoverSelectionItem\",\n \"PopoverSelectionLabel\",\n \"PopoverSelectionList\",\n \"PopoverTrigger\",\n \"PricingSection\",\n \"ProfileMenu\",\n \"Progress\",\n \"PrototypeReview\",\n \"QuickActions\",\n \"RadioGroup\",\n \"RadioGroupItem\",\n \"RadiusScale\",\n \"RadiusSection\",\n \"RadiusSlide\",\n \"RightRailList\",\n \"ScoreIndicator\",\n \"ScrollArea\",\n \"ScrollBar\",\n \"SearchInput\",\n \"SectionHeader\",\n \"SectionIntro\",\n \"SectionNav\",\n \"SectionNavItem\",\n \"SegmentedProgress\",\n \"Select\",\n \"SelectContent\",\n \"SelectGroup\",\n \"SelectionListContext\",\n \"SelectItem\",\n \"SelectLabel\",\n \"SelectScrollDownButton\",\n \"SelectScrollUpButton\",\n \"SelectSeparator\",\n \"SelectTrigger\",\n \"SelectValue\",\n \"SemanticColorGrid\",\n \"SemanticColorItem\",\n \"SemanticTokensSlide\",\n \"Separator\",\n \"SessionTimeout\",\n \"ShadowSection\",\n \"Sheet\",\n \"SheetClose\",\n \"SheetContent\",\n \"SheetDescription\",\n \"SheetFooter\",\n \"SheetHeader\",\n \"SheetOverlay\",\n \"SheetPortal\",\n \"SheetTitle\",\n \"SheetTrigger\",\n \"Sidebar\",\n \"SidebarContent\",\n \"SidebarContext\",\n \"SidebarFooter\",\n \"SidebarGroup\",\n \"SidebarGroupAction\",\n \"SidebarGroupContent\",\n \"SidebarGroupLabel\",\n \"SidebarHeader\",\n \"SidebarInset\",\n \"SidebarMenu\",\n \"SidebarMenuAction\",\n \"SidebarMenuBadge\",\n \"SidebarMenuButton\",\n \"SidebarMenuItem\",\n \"SidebarMenuSub\",\n \"SidebarMenuSubButton\",\n \"SidebarMenuSubItem\",\n \"SidebarProvider\",\n \"SidebarRail\",\n \"SidebarSeparator\",\n \"SidebarTrigger\",\n \"Skeleton\",\n \"SkeletonDetail\",\n \"SkeletonList\",\n \"SkeletonTable\",\n \"Slide\",\n \"SlideHeader\",\n \"Slider\",\n \"SliderControl\",\n \"SlideThemeContext\",\n \"SlideThemeProvider\",\n \"SlowNetworkBar\",\n \"SourceInspector\",\n \"SourceInspectorContext\",\n \"SourceInspectorDevImpl\",\n \"SourceInspectorProvider\",\n \"SourceInspectorRunner\",\n \"SourceInspectorToggle\",\n \"SpacingScale\",\n \"SpacingSection\",\n \"SpacingSlide\",\n \"Sparkline\",\n \"SpecimenCard\",\n \"SpecimenCardFooter\",\n \"Sphere\",\n \"SpherePlayground\",\n \"Spinner\",\n \"Stack\",\n \"Stage\",\n \"StatCard\",\n \"StatHero\",\n \"StationSpectrum\",\n \"StatusBadge\",\n \"StatusColorsSlide\",\n \"StatusDot\",\n \"Stepper\",\n \"StepperContext\",\n \"StepperDescription\",\n \"StepperItem\",\n \"StepperSeparator\",\n \"StepperTitle\",\n \"StepperTrigger\",\n \"StepsSection\",\n \"StructuredPrompt\",\n \"StructuredPromptBody\",\n \"StructuredPromptHeader\",\n \"StructuredPromptHint\",\n \"StructuredPromptSlot\",\n \"SuccessLiveRegion\",\n \"SurfaceRow\",\n \"SurfaceScaleStack\",\n \"SurfaceSection\",\n \"Switch\",\n \"Table\",\n \"TableBody\",\n \"TableCaption\",\n \"TableCell\",\n \"TableFooter\",\n \"TableHead\",\n \"TableHeader\",\n \"TableRow\",\n \"Tabs\",\n \"TabsContent\",\n \"TabsList\",\n \"TabsTrigger\",\n \"TagInput\",\n \"TestimonialAttribution\",\n \"TestimonialSection\",\n \"Text\",\n \"Textarea\",\n \"ThemeArchitectureSlide\",\n \"ThemeColorsSlide\",\n \"ThemeSwitcher\",\n \"Timeline\",\n \"TimelineContent\",\n \"TimelineDescription\",\n \"TimelineIcon\",\n \"TimelineItem\",\n \"TimelineTimestamp\",\n \"TimelineTitle\",\n \"TitleSlide\",\n \"ToastCard\",\n \"ToastCardStack\",\n \"Toaster\",\n \"TOCSlide\",\n \"ToggleButton\",\n \"ToggleDevImpl\",\n \"ToggleGroup\",\n \"ToggleGroupContext\",\n \"ToggleGroupItem\",\n \"Tooltip\",\n \"TooltipContent\",\n \"TooltipProvider\",\n \"TooltipTrigger\",\n \"TypeBodySlide\",\n \"TypeDisplaySlide\",\n \"TypeScaleStack\",\n \"TypeSpecimen\",\n \"TypographySection\",\n \"Vignette\",\n \"WorkspaceSwitcher\",\n])\n"
5040
+ "content": "// THIS FILE IS GENERATED BY scripts/generate-visor-component-names.ts.\n// Do not edit by hand. Re-run `npm run generate:component-names` after\n// adding, removing, or renaming a Visor component.\n//\n// Source of truth: registry/registry-{ui,blocks,deck,visual,devtools}.ts\n// Used by: components/devtools/source-inspector/* (VI-311)\n\nexport const VISOR_COMPONENT_NAMES: ReadonlySet<string> = new Set([\n \"AccessibilitySection\",\n \"AccessibilitySlide\",\n \"AccessibilitySpecimen\",\n \"Accordion\",\n \"AccordionContent\",\n \"AccordionItem\",\n \"AccordionTrigger\",\n \"ActivityFeed\",\n \"ActivityFeedContext\",\n \"ActivityFeedItem\",\n \"ActivityFeedRoot\",\n \"AdminDashboard\",\n \"AdminDetail\",\n \"AdminDetailDrawer\",\n \"AdminListPage\",\n \"AdminListPageInner\",\n \"AdminSettingsPage\",\n \"AdminShell\",\n \"AdminTabbedEditor\",\n \"AdminWizard\",\n \"Alert\",\n \"AlertActions\",\n \"AlertDescription\",\n \"AlertTitle\",\n \"AmbientGlow\",\n \"Avatar\",\n \"AvatarFallback\",\n \"AvatarImage\",\n \"AvatarStack\",\n \"Badge\",\n \"Banner\",\n \"BannerAction\",\n \"BannerDescription\",\n \"BannerTitle\",\n \"BentoGrid\",\n \"BentoTile\",\n \"BentoTileBody\",\n \"BentoTileDescription\",\n \"BentoTileFigure\",\n \"BentoTileHeadline\",\n \"BentoTileMedia\",\n \"BentoTileMeta\",\n \"BentoTileTitle\",\n \"Box\",\n \"Breadcrumb\",\n \"BreadcrumbEllipsis\",\n \"BreadcrumbItem\",\n \"BreadcrumbLink\",\n \"BreadcrumbList\",\n \"BreadcrumbPage\",\n \"BreadcrumbSeparator\",\n \"BrowserFrame\",\n \"BulkActionBar\",\n \"Button\",\n \"ButtonSpecimenSection\",\n \"ButtonSpecimenSlide\",\n \"Calendar\",\n \"Card\",\n \"CardContent\",\n \"CardDescription\",\n \"CardFooter\",\n \"CardGrid\",\n \"CardHeader\",\n \"CardLift\",\n \"CardTitle\",\n \"Carousel\",\n \"CarouselContent\",\n \"CarouselContext\",\n \"CarouselGallery\",\n \"CarouselItem\",\n \"CarouselNext\",\n \"CarouselPrevious\",\n \"ChallengeCard\",\n \"ChallengeCardAction\",\n \"ChallengeCardActions\",\n \"ChallengeCardBody\",\n \"ChallengeCardGate\",\n \"ChallengeCardHeader\",\n \"ChartContainer\",\n \"ChartContext\",\n \"ChartLegend\",\n \"ChartLegendContent\",\n \"ChartStyle\",\n \"ChartTooltip\",\n \"ChartTooltipContent\",\n \"Checkbox\",\n \"CheckGroup\",\n \"CheckRow\",\n \"Chip\",\n \"ChipGroup\",\n \"ChipGroupContext\",\n \"ChipGroupItem\",\n \"ChoiceChip\",\n \"ChromeButton\",\n \"ClosingSlide\",\n \"CodeBlock\",\n \"Collapsible\",\n \"CollapsibleContent\",\n \"CollapsibleTrigger\",\n \"ColorBar\",\n \"ColorPaletteSection\",\n \"ColorPicker\",\n \"ColorPickerSurface\",\n \"ColorSwatch\",\n \"ColorSwatchGrid\",\n \"Combobox\",\n \"ComboboxContent\",\n \"ComboboxContext\",\n \"ComboboxEmpty\",\n \"ComboboxGroup\",\n \"ComboboxInput\",\n \"ComboboxItem\",\n \"ComboboxSeparator\",\n \"Command\",\n \"CommandDialog\",\n \"CommandEmpty\",\n \"CommandGroup\",\n \"CommandInput\",\n \"CommandItem\",\n \"CommandList\",\n \"CommandLoading\",\n \"CommandSeparator\",\n \"CommandShortcut\",\n \"ComponentShowcaseContent\",\n \"ComponentShowcaseSection\",\n \"ComponentShowcaseSlide\",\n \"Composer\",\n \"ComposerContext\",\n \"ComposerField\",\n \"ComposerSend\",\n \"ComposerSpacer\",\n \"ComposerToolbar\",\n \"ComposerToolButton\",\n \"ConceptSlide\",\n \"ConfigurationPanel\",\n \"ConfirmDialog\",\n \"ConflictBanner\",\n \"Container\",\n \"ContextMenu\",\n \"ContextMenuCheckboxItem\",\n \"ContextMenuContent\",\n \"ContextMenuGroup\",\n \"ContextMenuItem\",\n \"ContextMenuLabel\",\n \"ContextMenuPortal\",\n \"ContextMenuRadioGroup\",\n \"ContextMenuRadioItem\",\n \"ContextMenuSeparator\",\n \"ContextMenuShortcut\",\n \"ContextMenuSub\",\n \"ContextMenuSubContent\",\n \"ContextMenuSubTrigger\",\n \"ContextMenuTrigger\",\n \"Controls\",\n \"CtaSection\",\n \"DataTable\",\n \"DataTableInner\",\n \"DatePicker\",\n \"DateRangePicker\",\n \"DeckContext\",\n \"DeckFooter\",\n \"DeckLayout\",\n \"DeckProvider\",\n \"DeckRenderer\",\n \"DesignSystemDeck\",\n \"DesignSystemSpecimen\",\n \"Dialog\",\n \"DialogClose\",\n \"DialogContent\",\n \"DialogDescription\",\n \"DialogFooter\",\n \"DialogHeader\",\n \"DialogOverlay\",\n \"DialogPortal\",\n \"DialogTitle\",\n \"DialogTrigger\",\n \"DotNav\",\n \"DropdownMenu\",\n \"DropdownMenuCheckboxItem\",\n \"DropdownMenuContent\",\n \"DropdownMenuGroup\",\n \"DropdownMenuItem\",\n \"DropdownMenuLabel\",\n \"DropdownMenuPortal\",\n \"DropdownMenuRadioGroup\",\n \"DropdownMenuRadioItem\",\n \"DropdownMenuSeparator\",\n \"DropdownMenuShortcut\",\n \"DropdownMenuSub\",\n \"DropdownMenuSubContent\",\n \"DropdownMenuSubTrigger\",\n \"DropdownMenuTrigger\",\n \"EditableBlock\",\n \"ElevationCard\",\n \"ElevationSlide\",\n \"EmptyState\",\n \"ErrorPlacard\",\n \"ExportMenu\",\n \"FeaturesGrid\",\n \"Field\",\n \"FieldDescription\",\n \"FieldError\",\n \"FieldLabel\",\n \"Fieldset\",\n \"FieldsetLegend\",\n \"FileUpload\",\n \"FilterBar\",\n \"FilterChip\",\n \"FontShowcase\",\n \"FontShowcaseGrid\",\n \"Footer\",\n \"FooterSection\",\n \"Form\",\n \"FormError\",\n \"FormErrorDescription\",\n \"FormErrorTitle\",\n \"FormField\",\n \"FormSpecimenSection\",\n \"FormSpecimenSlide\",\n \"FullscreenOverlay\",\n \"FullscreenOverlayContent\",\n \"FullscreenOverlayTrigger\",\n \"GrainOverlay\",\n \"Grid\",\n \"Header\",\n \"Heading\",\n \"HeroGlow\",\n \"HeroSection\",\n \"HeroSlide\",\n \"HoverCard\",\n \"HoverCardContent\",\n \"HoverCardTrigger\",\n \"IconGrid\",\n \"IconGridSection\",\n \"IconSizeRow\",\n \"IconsSlide\",\n \"Image\",\n \"InfographicBar\",\n \"Inline\",\n \"Input\",\n \"Kbd\",\n \"KeyValueList\",\n \"Label\",\n \"Landing\",\n \"Lightbox\",\n \"LightboxContent\",\n \"LightboxContext\",\n \"LightboxTrigger\",\n \"LoginForm\",\n \"Marquee\",\n \"MarqueeBandRenderer\",\n \"MatrixCell\",\n \"MatrixTable\",\n \"MatrixTableInner\",\n \"Menubar\",\n \"MenubarCheckboxItem\",\n \"MenubarContent\",\n \"MenubarGroup\",\n \"MenubarItem\",\n \"MenubarLabel\",\n \"MenubarMenu\",\n \"MenubarRadioGroup\",\n \"MenubarRadioItem\",\n \"MenubarSeparator\",\n \"MenubarShortcut\",\n \"MenubarSub\",\n \"MenubarSubContent\",\n \"MenubarSubTrigger\",\n \"MenubarTrigger\",\n \"MonthCalendar\",\n \"MotionDuration\",\n \"MotionDurationSection\",\n \"MotionEasing\",\n \"MotionEasingSection\",\n \"MotionSlide\",\n \"NameRoster\",\n \"NameRosterItem\",\n \"Navbar\",\n \"NavbarBrand\",\n \"NavbarContent\",\n \"NavbarItem\",\n \"NavbarLink\",\n \"NavbarToggle\",\n \"NumberInput\",\n \"OfflineBanner\",\n \"OpacityBar\",\n \"OpacitySlide\",\n \"OTPInput\",\n \"PageHeader\",\n \"Pagination\",\n \"PaginationContent\",\n \"PaginationEllipsis\",\n \"PaginationItem\",\n \"PaginationLink\",\n \"PaginationNext\",\n \"PaginationPrevious\",\n \"PasswordInput\",\n \"PhoneInput\",\n \"Popover\",\n \"PopoverAnchor\",\n \"PopoverContent\",\n \"PopoverFooter\",\n \"PopoverSelectionItem\",\n \"PopoverSelectionLabel\",\n \"PopoverSelectionList\",\n \"PopoverTrigger\",\n \"PricingSection\",\n \"ProfileMenu\",\n \"Progress\",\n \"PrototypeReview\",\n \"QuickActions\",\n \"RadioGroup\",\n \"RadioGroupItem\",\n \"RadiusScale\",\n \"RadiusSection\",\n \"RadiusSlide\",\n \"RecordSection\",\n \"RightRailList\",\n \"ScoreIndicator\",\n \"ScrollArea\",\n \"ScrollBar\",\n \"SearchInput\",\n \"SectionHeader\",\n \"SectionIntro\",\n \"SectionNav\",\n \"SectionNavItem\",\n \"SegmentedProgress\",\n \"Select\",\n \"SelectContent\",\n \"SelectGroup\",\n \"SelectionListContext\",\n \"SelectItem\",\n \"SelectLabel\",\n \"SelectScrollDownButton\",\n \"SelectScrollUpButton\",\n \"SelectSeparator\",\n \"SelectTrigger\",\n \"SelectValue\",\n \"SemanticColorGrid\",\n \"SemanticColorItem\",\n \"SemanticTokensSlide\",\n \"SensitivePanel\",\n \"Separator\",\n \"SessionTimeout\",\n \"ShadowSection\",\n \"Sheet\",\n \"SheetClose\",\n \"SheetContent\",\n \"SheetDescription\",\n \"SheetFooter\",\n \"SheetHeader\",\n \"SheetOverlay\",\n \"SheetPortal\",\n \"SheetTitle\",\n \"SheetTrigger\",\n \"Sidebar\",\n \"SidebarContent\",\n \"SidebarContext\",\n \"SidebarFooter\",\n \"SidebarGroup\",\n \"SidebarGroupAction\",\n \"SidebarGroupContent\",\n \"SidebarGroupLabel\",\n \"SidebarHeader\",\n \"SidebarInset\",\n \"SidebarMenu\",\n \"SidebarMenuAction\",\n \"SidebarMenuBadge\",\n \"SidebarMenuButton\",\n \"SidebarMenuItem\",\n \"SidebarMenuSub\",\n \"SidebarMenuSubButton\",\n \"SidebarMenuSubItem\",\n \"SidebarProvider\",\n \"SidebarRail\",\n \"SidebarSeparator\",\n \"SidebarTrigger\",\n \"Skeleton\",\n \"SkeletonDetail\",\n \"SkeletonList\",\n \"SkeletonTable\",\n \"Slide\",\n \"SlideHeader\",\n \"Slider\",\n \"SliderControl\",\n \"SlideThemeContext\",\n \"SlideThemeProvider\",\n \"SlowNetworkBar\",\n \"SourceInspector\",\n \"SourceInspectorContext\",\n \"SourceInspectorDevImpl\",\n \"SourceInspectorProvider\",\n \"SourceInspectorRunner\",\n \"SourceInspectorToggle\",\n \"SpacingScale\",\n \"SpacingSection\",\n \"SpacingSlide\",\n \"Sparkline\",\n \"SpecimenCard\",\n \"SpecimenCardFooter\",\n \"Sphere\",\n \"SpherePlayground\",\n \"Spinner\",\n \"Stack\",\n \"Stage\",\n \"StatCard\",\n \"StatHero\",\n \"StationSpectrum\",\n \"StatusBadge\",\n \"StatusColorsSlide\",\n \"StatusDot\",\n \"Stepper\",\n \"StepperContext\",\n \"StepperDescription\",\n \"StepperItem\",\n \"StepperSeparator\",\n \"StepperTitle\",\n \"StepperTrigger\",\n \"StepsSection\",\n \"StructuredPrompt\",\n \"StructuredPromptBody\",\n \"StructuredPromptHeader\",\n \"StructuredPromptHint\",\n \"StructuredPromptSlot\",\n \"SuccessLiveRegion\",\n \"SurfaceRow\",\n \"SurfaceScaleStack\",\n \"SurfaceSection\",\n \"Switch\",\n \"Table\",\n \"TableBody\",\n \"TableCaption\",\n \"TableCell\",\n \"TableFooter\",\n \"TableHead\",\n \"TableHeader\",\n \"TableRow\",\n \"Tabs\",\n \"TabsContent\",\n \"TabsList\",\n \"TabsTrigger\",\n \"TagInput\",\n \"TestimonialAttribution\",\n \"TestimonialSection\",\n \"Text\",\n \"Textarea\",\n \"ThemeArchitectureSlide\",\n \"ThemeColorsSlide\",\n \"ThemeSwitcher\",\n \"Timeline\",\n \"TimelineContent\",\n \"TimelineDescription\",\n \"TimelineIcon\",\n \"TimelineItem\",\n \"TimelineTimestamp\",\n \"TimelineTitle\",\n \"TitleSlide\",\n \"ToastCard\",\n \"ToastCardStack\",\n \"Toaster\",\n \"TOCSlide\",\n \"ToggleButton\",\n \"ToggleDevImpl\",\n \"ToggleGroup\",\n \"ToggleGroupContext\",\n \"ToggleGroupItem\",\n \"Tooltip\",\n \"TooltipContent\",\n \"TooltipProvider\",\n \"TooltipTrigger\",\n \"TypeBodySlide\",\n \"TypeDisplaySlide\",\n \"TypeScaleStack\",\n \"TypeSpecimen\",\n \"TypographySection\",\n \"Vignette\",\n \"WorkspaceSwitcher\",\n])\n"
4986
5041
  }
4987
5042
  ]
4988
5043
  },
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "version": "0.4.0",
3
- "generated_at": "2026-07-07T00:00:53.428Z",
3
+ "generated_at": "2026-07-08T04:58:15.594Z",
4
4
  "components": {
5
5
  "accessibility-specimen": {
6
6
  "category": "specimen",
@@ -10816,6 +10816,28 @@
10816
10816
  "Settings forms (use admin-settings-page)"
10817
10817
  ]
10818
10818
  },
10819
+ "admin-detail": {
10820
+ "category": "admin",
10821
+ "description": "Full-page, read-oriented detail RECORD for the admin-shell main column. Composes an identity header (media + title + StatusBadge + actions), N key-value sections built on KeyValueList, an optional sensitive/reveal panel gated behind a Switch, and optional sub-list slots for ledgers or history — separated by hairline dividers. The full-page sibling to admin-detail-drawer (a drawer) and admin-list-page.",
10822
+ "components_used": [
10823
+ "key-value-list",
10824
+ "status-badge",
10825
+ "switch",
10826
+ "separator"
10827
+ ],
10828
+ "when_to_use": [
10829
+ "Read-oriented single-record detail pages rendered in the admin-shell main column",
10830
+ "Records with an identity header plus grouped key-value facts (profile, account, metadata)",
10831
+ "Detail pages that expose sensitive data (tax IDs, banking, W-9) behind a reveal control",
10832
+ "Records with sub-ledgers or history tables beneath the key-value sections"
10833
+ ],
10834
+ "when_not_to_use": [
10835
+ "Inline editing of a record from a list without leaving the page (use admin-detail-drawer)",
10836
+ "Full-page multi-section editors with save/cancel (use admin-settings-page)",
10837
+ "Multi-step create/edit flows (use admin-wizard)",
10838
+ "Tabular lists of many records (use admin-list-page)"
10839
+ ]
10840
+ },
10819
10841
  "admin-detail-drawer": {
10820
10842
  "category": "admin",
10821
10843
  "description": "Right-side slide-out panel for viewing or editing a single record. Composes Sheet with a sticky save/cancel footer, async save handler with pending state, an unsaved-changes guard powered by ConfirmDialog, sm/md/lg/xl width variants, and optional header customization via hideHeader and customHeader props.",
@@ -11171,6 +11193,21 @@
11171
11193
  "Registration forms (extend with additional fields)"
11172
11194
  ]
11173
11195
  },
11196
+ "month-calendar": {
11197
+ "category": "data-display",
11198
+ "description": "A theme-portable month event-grid (scheduler) block. Renders a 6×7 day-cell grid under a localized weekday header, with prev/next month navigation, a view-mode segmented control (Month / Week / Day), and per-cell event chips. Each chip carries a status-dot slot (success / warning / danger / info) and an optional series tint (1–5) keyed to the theme's chart ramp for color-coding a recurring series or resource lane. Days outside the displayed month are dimmed; optional today and selected-day highlighting. Fully token-driven — every color, spacing, stroke, radius, shadow, opacity, and motion value binds to a Visor token, so the grid adopts the active theme without modification.",
11199
+ "components_used": [],
11200
+ "when_to_use": [
11201
+ "Admin or booking surfaces that show events-in-a-month (the most common scheduling view)",
11202
+ "Color-coding recurring series or resource lanes via the per-chip series tint",
11203
+ "Status-at-a-glance calendars where each event needs a success / warning / danger / info dot"
11204
+ ],
11205
+ "when_not_to_use": [
11206
+ "Picking a single date or date range (use calendar, date-picker, or date-range-picker)",
11207
+ "Time-slot / day-timeline scheduling with hour rows (use a dedicated day/week scheduler)",
11208
+ "Dense agenda lists with no month context (use a table or activity-feed)"
11209
+ ]
11210
+ },
11174
11211
  "pricing-section": {
11175
11212
  "category": "marketing",
11176
11213
  "description": "A responsive pricing tier grid with feature lists, highlighted plan, and per-tier CTAs.",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@loworbitstudio/visor",
3
- "version": "1.16.0",
3
+ "version": "1.17.0",
4
4
  "description": "CLI for the Visor design system — add components, hooks, and utilities to your project.",
5
5
  "type": "module",
6
6
  "bin": {