@exxatdesignux/ui 0.5.1 → 0.5.3

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.
Files changed (138) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/consumer-extras/cursor-rules/exxat-data-tables.mdc +8 -6
  3. package/consumer-extras/cursor-rules/exxat-ds-agents.mdc +2 -1
  4. package/consumer-extras/cursor-rules/exxat-hub-supported-views.mdc +54 -0
  5. package/consumer-extras/cursor-rules/exxat-nav-single-active.mdc +31 -0
  6. package/consumer-extras/cursor-skills/exxat-ds-skill/SKILL.md +8 -3
  7. package/consumer-extras/cursor-skills/exxat-ds-skill/references/data-table-pattern.md +15 -5
  8. package/consumer-extras/cursor-skills/exxat-token-economy/SKILL.md +11 -4
  9. package/consumer-extras/handbook/HANDBOOK.md +1 -1
  10. package/consumer-extras/handbook/reference-implementations.md +2 -2
  11. package/consumer-extras/patterns/data-views-pattern.md +6 -0
  12. package/consumer-extras/patterns/hub-supported-views-pattern.md +53 -0
  13. package/dist/components/data-table/filter-text-value-input.js +1 -1
  14. package/dist/components/data-table/filter-text-value-input.js.map +1 -1
  15. package/dist/components/data-table/index.js +16 -12
  16. package/dist/components/data-table/index.js.map +1 -1
  17. package/dist/components/data-table/pagination.js +16 -12
  18. package/dist/components/data-table/pagination.js.map +1 -1
  19. package/dist/components/data-views/data-row-list.js +1 -1
  20. package/dist/components/data-views/data-row-list.js.map +1 -1
  21. package/dist/components/data-views/hub-table.d.ts +8 -4
  22. package/dist/components/data-views/hub-table.js +31 -16
  23. package/dist/components/data-views/hub-table.js.map +1 -1
  24. package/dist/components/data-views/index.d.ts +1 -1
  25. package/dist/components/data-views/index.js +31 -16
  26. package/dist/components/data-views/index.js.map +1 -1
  27. package/dist/components/data-views/list-page-connected-view-body.d.ts +1 -1
  28. package/dist/components/data-views/list-page-connected-view-body.js +1 -0
  29. package/dist/components/data-views/list-page-connected-view-body.js.map +1 -1
  30. package/dist/components/table-properties/column-row.js +1 -1
  31. package/dist/components/table-properties/column-row.js.map +1 -1
  32. package/dist/components/table-properties/drawer-button.js +6 -5
  33. package/dist/components/table-properties/drawer-button.js.map +1 -1
  34. package/dist/components/table-properties/drawer.js +6 -5
  35. package/dist/components/table-properties/drawer.js.map +1 -1
  36. package/dist/components/table-properties/filter-card.js +2 -2
  37. package/dist/components/table-properties/filter-card.js.map +1 -1
  38. package/dist/components/table-properties/index.d.ts +1 -1
  39. package/dist/components/table-properties/index.js +6 -5
  40. package/dist/components/table-properties/index.js.map +1 -1
  41. package/dist/components/table-properties/sort-card.js +1 -1
  42. package/dist/components/table-properties/sort-card.js.map +1 -1
  43. package/dist/components/templates/index.d.ts +1 -1
  44. package/dist/components/templates/index.js +16 -6
  45. package/dist/components/templates/index.js.map +1 -1
  46. package/dist/components/templates/list-page.d.ts +4 -2
  47. package/dist/components/templates/list-page.js +16 -6
  48. package/dist/components/templates/list-page.js.map +1 -1
  49. package/dist/components/ui/banner.d.ts +2 -2
  50. package/dist/components/ui/banner.js +1 -1
  51. package/dist/components/ui/banner.js.map +1 -1
  52. package/dist/components/ui/coach-mark.js +1 -1
  53. package/dist/components/ui/coach-mark.js.map +1 -1
  54. package/dist/components/ui/context-menu.js +1 -1
  55. package/dist/components/ui/context-menu.js.map +1 -1
  56. package/dist/components/ui/date-picker-field.js +1 -1
  57. package/dist/components/ui/date-picker-field.js.map +1 -1
  58. package/dist/components/ui/dropdown-menu.js +2 -2
  59. package/dist/components/ui/dropdown-menu.js.map +1 -1
  60. package/dist/components/ui/export-drawer.js +3 -3
  61. package/dist/components/ui/export-drawer.js.map +1 -1
  62. package/dist/components/ui/hover-card.js +1 -1
  63. package/dist/components/ui/hover-card.js.map +1 -1
  64. package/dist/components/ui/key-metrics.js +6 -6
  65. package/dist/components/ui/key-metrics.js.map +1 -1
  66. package/dist/components/ui/page-header.js +1 -1
  67. package/dist/components/ui/page-header.js.map +1 -1
  68. package/dist/components/ui/popover.js +1 -1
  69. package/dist/components/ui/popover.js.map +1 -1
  70. package/dist/components/ui/select.js +1 -1
  71. package/dist/components/ui/select.js.map +1 -1
  72. package/dist/components/ui/sheet.js +1 -1
  73. package/dist/components/ui/sheet.js.map +1 -1
  74. package/dist/components/ui/sidebar.d.ts +1 -1
  75. package/dist/components/ui/sidebar.js +3 -3
  76. package/dist/components/ui/sidebar.js.map +1 -1
  77. package/dist/components/ui/tip.js +1 -1
  78. package/dist/components/ui/tip.js.map +1 -1
  79. package/dist/components/ui/tooltip.js +1 -1
  80. package/dist/components/ui/tooltip.js.map +1 -1
  81. package/dist/components/ui/view-segmented-control.js +1 -1
  82. package/dist/components/ui/view-segmented-control.js.map +1 -1
  83. package/dist/{data-list-view-registry-CyBoBML4.d.ts → data-list-view-registry-BstmlfQ3.d.ts} +16 -1
  84. package/dist/index.d.ts +2 -1
  85. package/dist/index.js +151 -29
  86. package/dist/index.js.map +1 -1
  87. package/dist/lib/data-list-view-registry.d.ts +1 -1
  88. package/dist/lib/data-list-view-registry.js +17 -1
  89. package/dist/lib/data-list-view-registry.js.map +1 -1
  90. package/dist/lib/data-list-view-surface.d.ts +1 -1
  91. package/dist/lib/data-list-view-surface.js +1 -0
  92. package/dist/lib/data-list-view-surface.js.map +1 -1
  93. package/dist/lib/list-page-table-properties.d.ts +1 -1
  94. package/dist/lib/list-page-table-properties.js +1 -0
  95. package/dist/lib/list-page-table-properties.js.map +1 -1
  96. package/dist/lib/nav-active.d.ts +38 -0
  97. package/dist/lib/nav-active.js +104 -0
  98. package/dist/lib/nav-active.js.map +1 -0
  99. package/package.json +1 -1
  100. package/src/components/data-table/index.tsx +25 -17
  101. package/src/components/data-views/data-row-list.tsx +1 -1
  102. package/src/components/data-views/hub-table.tsx +9 -3
  103. package/src/components/templates/list-page.tsx +9 -3
  104. package/src/components/ui/banner.tsx +0 -2
  105. package/src/components/ui/coach-mark.tsx +1 -2
  106. package/src/components/ui/context-menu.tsx +1 -1
  107. package/src/components/ui/dropdown-menu.tsx +2 -2
  108. package/src/components/ui/hover-card.tsx +1 -1
  109. package/src/components/ui/key-metrics.tsx +4 -4
  110. package/src/components/ui/popover.tsx +1 -1
  111. package/src/components/ui/select.tsx +1 -1
  112. package/src/components/ui/sheet.tsx +1 -1
  113. package/src/components/ui/sidebar.tsx +3 -3
  114. package/src/components/ui/tooltip.tsx +1 -1
  115. package/src/index.ts +1 -0
  116. package/src/lib/data-list-view-registry.ts +31 -0
  117. package/src/lib/nav-active.ts +162 -0
  118. package/template/.claude/skills/exxat-ds-skill/SKILL.md +2 -1
  119. package/template/AGENTS.md +16 -1
  120. package/template/components/columns-client.tsx +3 -2
  121. package/template/components/columns-showcase.tsx +22 -18
  122. package/template/components/exxat-product-logo.tsx +1 -1
  123. package/template/components/library-table.tsx +62 -23
  124. package/template/components/new-library-item-form.tsx +0 -7
  125. package/template/components/product-wordmark.tsx +1 -1
  126. package/template/components/sidebar/app-sidebar.tsx +14 -106
  127. package/template/components/sidebar/secondary-nav.tsx +22 -4
  128. package/template/components/tokens-hub-auxiliary-views.tsx +301 -0
  129. package/template/components/tokens-themes-client.tsx +44 -16
  130. package/template/docs/HANDBOOK.md +1 -1
  131. package/template/docs/data-views-pattern.md +6 -0
  132. package/template/docs/glossary.md +2 -1
  133. package/template/docs/hub-supported-views-pattern.md +53 -0
  134. package/template/docs/reference-implementations.md +2 -2
  135. package/template/lib/full-hub-supported-views.ts +8 -0
  136. package/template/lib/library-supported-views.ts +5 -12
  137. package/template/package.json +11 -0
  138. package/tokens/hooks-index.json +2 -2
@@ -56,8 +56,6 @@ const VARIANT_CONFIG = {
56
56
  },
57
57
  }
58
58
 
59
- type BannerVariant = keyof typeof VARIANT_CONFIG
60
-
61
59
  // ─────────────────────────────────────────────────────────────────────────────
62
60
  // SystemBanner — inline at the top of the main content area
63
61
  // ─────────────────────────────────────────────────────────────────────────────
@@ -25,7 +25,6 @@ import * as React from "react"
25
25
  import { createPortal } from "react-dom"
26
26
  import { Popover as PopoverPrimitive } from "radix-ui"
27
27
  import { cva, type VariantProps } from "class-variance-authority"
28
- import { Button } from "./button"
29
28
  import { cn } from "../../lib/utils"
30
29
  import type { CoachMarkState } from "../../hooks/use-coach-mark"
31
30
 
@@ -258,7 +257,7 @@ export function CoachMark({
258
257
  "data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
259
258
  "data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95",
260
259
  "data-[side=bottom]:slide-in-from-top-2 data-[side=top]:slide-in-from-bottom-2",
261
- "data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2",
260
+ "data-[side=left]:slide-in-from-end-2 data-[side=right]:slide-in-from-start-2",
262
261
  className
263
262
  )}
264
263
  >
@@ -126,7 +126,7 @@ function ContextMenuContent({
126
126
  <ContextMenuPrimitive.Content
127
127
  data-slot="context-menu-content"
128
128
  className={cn(
129
- "z-50 max-h-(--radix-context-menu-content-available-height) origin-(--radix-context-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-lg bg-popover p-1 text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
129
+ "z-50 max-h-(--radix-context-menu-content-available-height) origin-(--radix-context-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-lg bg-popover p-1 text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-end-2 data-[side=right]:slide-in-from-start-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
130
130
  DROPDOWN_MENU_CONTENT_SURFACE_CLASS,
131
131
  className,
132
132
  )}
@@ -47,7 +47,7 @@ function DropdownMenuContent({
47
47
  sideOffset={sideOffset}
48
48
  align={align}
49
49
  className={cn(
50
- "z-50 max-h-(--radix-dropdown-menu-content-available-height) origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-lg bg-popover p-1 text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:overflow-hidden data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
50
+ "z-50 max-h-(--radix-dropdown-menu-content-available-height) origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-lg bg-popover p-1 text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-end-2 data-[side=right]:slide-in-from-start-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:overflow-hidden data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
51
51
  DROPDOWN_MENU_CONTENT_SURFACE_CLASS,
52
52
  className,
53
53
  )}
@@ -403,7 +403,7 @@ function DropdownMenuSubContent({
403
403
  <DropdownMenuPrimitive.SubContent
404
404
  data-slot="dropdown-menu-sub-content"
405
405
  className={cn(
406
- "z-50 origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-lg bg-popover p-1 text-popover-foreground shadow-lg ring-1 ring-foreground/10 duration-100 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
406
+ "z-50 origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-lg bg-popover p-1 text-popover-foreground shadow-lg ring-1 ring-foreground/10 duration-100 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-end-2 data-[side=right]:slide-in-from-start-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
407
407
  DROPDOWN_MENU_CONTENT_SURFACE_CLASS,
408
408
  className,
409
409
  )}
@@ -54,7 +54,7 @@ function HoverCardContent({
54
54
  align={align}
55
55
  sideOffset={sideOffset}
56
56
  className={cn(
57
- "z-50 w-64 origin-(--radix-hover-card-content-transform-origin) rounded-lg bg-popover p-4 text-sm text-popover-foreground shadow-md ring-1 ring-foreground/10 outline-none data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
57
+ "z-50 w-64 origin-(--radix-hover-card-content-transform-origin) rounded-lg bg-popover p-4 text-sm text-popover-foreground shadow-md ring-1 ring-foreground/10 outline-none data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-end-2 data-[side=right]:slide-in-from-start-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
58
58
  className,
59
59
  )}
60
60
  {...props}
@@ -261,7 +261,7 @@ function flatMetricsHairlineClass(
261
261
  const childBorder = "[&>*]:border-[color:var(--key-metrics-flat-divider)]"
262
262
 
263
263
  if (itemCount === 2) {
264
- return cn("gap-0", childBorder, "[&>*:first-child]:border-r")
264
+ return cn("gap-0", childBorder, "[&>*:first-child]:border-e")
265
265
  }
266
266
 
267
267
  if (itemCount === 4) {
@@ -272,15 +272,15 @@ function flatMetricsHairlineClass(
272
272
  "gap-0",
273
273
  childBorder,
274
274
  /* Wide strip (matches `@[30rem]:grid-cols-4`) — verticals between all tiles, no horizontal */
275
- "[&>*:not(:last-child)]:border-r",
275
+ "[&>*:not(:last-child)]:border-e",
276
276
  /* Narrow strip (`@[18rem]`–`@[30rem]` 2×2) */
277
277
  `${narrow2x2}:[&>*:not(:last-child)]:border-e-0`,
278
- `${narrow2x2}:[&>*:nth-child(odd)]:border-r`,
278
+ `${narrow2x2}:[&>*:nth-child(odd)]:border-e`,
279
279
  `${narrow2x2}:[&>*:not(:nth-last-child(-n+2))]:border-b`,
280
280
  )
281
281
  }
282
282
 
283
- return cn("gap-0", childBorder, "[&>*:not(:last-child)]:border-r")
283
+ return cn("gap-0", childBorder, "[&>*:not(:last-child)]:border-e")
284
284
  }
285
285
 
286
286
  function metricsRowColumnsClass(rowLength: number, metricsHalfWidthLayout: boolean): string {
@@ -34,7 +34,7 @@ function PopoverContent({
34
34
  "data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
35
35
  "data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95",
36
36
  "data-[side=bottom]:slide-in-from-top-2 data-[side=top]:slide-in-from-bottom-2",
37
- "data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2",
37
+ "data-[side=left]:slide-in-from-end-2 data-[side=right]:slide-in-from-start-2",
38
38
  className
39
39
  )}
40
40
  {...props}
@@ -68,7 +68,7 @@ function SelectContent({
68
68
  <SelectPrimitive.Content
69
69
  data-slot="select-content"
70
70
  data-align-trigger={position === "item-aligned"}
71
- className={cn("relative z-50 max-h-(--radix-select-content-available-height) min-w-36 origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-lg bg-popover text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 data-[align-trigger=true]:animate-none data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95", position ==="popper"&&"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 rtl:data-[side=left]:translate-x-1 data-[side=right]:translate-x-1 rtl:data-[side=right]:-translate-x-1 data-[side=top]:-translate-y-1", className )}
71
+ className={cn("relative z-50 max-h-(--radix-select-content-available-height) min-w-36 origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-lg bg-popover text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 data-[align-trigger=true]:animate-none data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-end-2 data-[side=right]:slide-in-from-start-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95", position ==="popper"&&"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 rtl:data-[side=left]:translate-x-1 data-[side=right]:translate-x-1 rtl:data-[side=right]:-translate-x-1 data-[side=top]:-translate-y-1", className )}
72
72
  position={position}
73
73
  align={align}
74
74
  {...props}
@@ -63,7 +63,7 @@ function SheetContent({
63
63
  data-slot="sheet-content"
64
64
  data-side={side}
65
65
  className={cn(
66
- "fixed z-50 flex flex-col gap-4 bg-background bg-clip-padding text-sm shadow-lg outline-none duration-300 ease-out data-[side=bottom]:inset-x-0 data-[side=bottom]:bottom-0 data-[side=bottom]:h-auto data-[side=bottom]:border-t data-[side=left]:inset-y-0 data-[side=left]:left-0 data-[side=left]:h-full data-[side=left]:w-3/4 data-[side=left]:border-e data-[side=right]:inset-y-0 data-[side=right]:right-0 data-[side=right]:h-full data-[side=right]:w-3/4 data-[side=right]:border-s data-[side=top]:inset-x-0 data-[side=top]:top-0 data-[side=top]:h-auto data-[side=top]:border-b data-[side=left]:sm:max-w-sm data-[side=right]:sm:max-w-sm data-open:animate-in data-open:fade-in-0 data-[side=bottom]:data-open:slide-in-from-bottom-6 data-[side=left]:data-open:slide-in-from-left-6 data-[side=right]:data-open:slide-in-from-right-6 data-[side=top]:data-open:slide-in-from-top-6 data-closed:animate-out data-closed:fade-out-0 data-[side=bottom]:data-closed:slide-out-to-bottom-6 data-[side=left]:data-closed:slide-out-to-left-6 data-[side=right]:data-closed:slide-out-to-right-6 data-[side=top]:data-closed:slide-out-to-top-6",
66
+ "fixed z-50 flex flex-col gap-4 bg-background bg-clip-padding text-sm shadow-lg outline-none duration-300 ease-out data-[side=bottom]:inset-x-0 data-[side=bottom]:bottom-0 data-[side=bottom]:h-auto data-[side=bottom]:border-t data-[side=left]:inset-y-0 data-[side=left]:start-0 data-[side=left]:h-full data-[side=left]:w-3/4 data-[side=left]:border-e data-[side=right]:inset-y-0 data-[side=right]:end-0 data-[side=right]:h-full data-[side=right]:w-3/4 data-[side=right]:border-s data-[side=top]:inset-x-0 data-[side=top]:top-0 data-[side=top]:h-auto data-[side=top]:border-b data-[side=left]:sm:max-w-sm data-[side=right]:sm:max-w-sm data-open:animate-in data-open:fade-in-0 data-[side=bottom]:data-open:slide-in-from-bottom-6 data-[side=left]:data-open:slide-in-from-start-6 data-[side=right]:data-open:slide-in-from-end-6 data-[side=top]:data-open:slide-in-from-top-6 data-closed:animate-out data-closed:fade-out-0 data-[side=bottom]:data-closed:slide-out-to-bottom-6 data-[side=left]:data-closed:slide-out-to-start-6 data-[side=right]:data-closed:slide-out-to-end-6 data-[side=top]:data-closed:slide-out-to-top-6",
67
67
  className
68
68
  )}
69
69
  {...props}
@@ -121,7 +121,7 @@ function SidebarProvider({
121
121
  // `SidebarAutoCollapse` route) must NOT trigger a re-reconcile that reads the
122
122
  // saved cookie back as "expanded" and snaps the rail open again.
123
123
  // The functional setter avoids closing over a stale `open` value.
124
- // eslint-disable-next-line react-hooks/exhaustive-deps
124
+ // Deps are `[openProp]` only — not `open` — so incidental collapses do not re-read the cookie.
125
125
  React.useLayoutEffect(() => {
126
126
  if (typeof window === "undefined") return
127
127
  if (typeof document !== "undefined" && document.cookie.includes(`${SIDEBAR_COOKIE_LEGACY_NAME}=`)) {
@@ -132,7 +132,7 @@ function SidebarProvider({
132
132
  const fromCookie = readSidebarStateCookie()
133
133
  if (fromCookie === undefined) return
134
134
  _setOpen((prev) => (prev === fromCookie ? prev : fromCookie))
135
- }, [])
135
+ }, [openProp])
136
136
 
137
137
  const setOpen = React.useCallback(
138
138
  (
@@ -246,7 +246,7 @@ function Sidebar({
246
246
  collapsible = "offcanvas",
247
247
  className,
248
248
  children,
249
- dir,
249
+ dir: _dir,
250
250
  ...props
251
251
  }: React.ComponentProps<"div"> & {
252
252
  side?: "left" | "right"
@@ -50,7 +50,7 @@ function TooltipContent({
50
50
  data-slot="tooltip-content"
51
51
  sideOffset={sideOffset}
52
52
  className={cn(
53
- "z-50 inline-flex w-fit max-w-xs origin-(--radix-tooltip-content-transform-origin) items-center gap-1.5 rounded-md bg-foreground px-3 py-1.5 text-xs text-background has-data-[slot=kbd]:pe-1.5 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 **:data-[slot=kbd]:relative **:data-[slot=kbd]:isolate **:data-[slot=kbd]:z-50 **:data-[slot=kbd]:rounded-sm data-[state=delayed-open]:animate-in data-[state=delayed-open]:fade-in-0 data-[state=delayed-open]:zoom-in-95 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
53
+ "z-50 inline-flex w-fit max-w-xs origin-(--radix-tooltip-content-transform-origin) items-center gap-1.5 rounded-md bg-foreground px-3 py-1.5 text-xs text-background has-data-[slot=kbd]:pe-1.5 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-end-2 data-[side=right]:slide-in-from-start-2 data-[side=top]:slide-in-from-bottom-2 **:data-[slot=kbd]:relative **:data-[slot=kbd]:isolate **:data-[slot=kbd]:z-50 **:data-[slot=kbd]:rounded-sm data-[state=delayed-open]:animate-in data-[state=delayed-open]:fade-in-0 data-[state=delayed-open]:zoom-in-95 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
54
54
  className
55
55
  )}
56
56
  {...props}
package/src/index.ts CHANGED
@@ -46,6 +46,7 @@ export {
46
46
  export * from "./lib/list-page-table-properties"
47
47
  export * from "./lib/data-list-view"
48
48
  export * from "./lib/data-list-view-registry"
49
+ export * from "./lib/nav-active"
49
50
  export * from "./lib/data-list-view-surface"
50
51
  export * from "./lib/data-list-display-options"
51
52
 
@@ -6,6 +6,7 @@
6
6
  * `getDataListViewRenderKind` + `ListPageConnectedViewBody` (never a dashboard fallback).
7
7
  *
8
8
  * @see `docs/data-views-pattern.md` — "View registry and connected bodies"
9
+ * @see `.cursor/rules/exxat-hub-supported-views.mdc` — default hubs use **`FULL_HUB_SUPPORTED_VIEWS`** (seven views)
9
10
  */
10
11
 
11
12
  import {
@@ -47,6 +48,36 @@ const BY_VALUE = new Map<DataListViewType, DataListViewDefinition>(
47
48
 
48
49
  export const DATA_LIST_VIEW_REGISTRY: readonly DataListViewDefinition[] = DEFINITIONS
49
50
 
51
+ /**
52
+ * Default view allowlist for **primary list hubs** (Placements / Team / Students-style pages).
53
+ * Pair with `ListPageTemplate` + `HubTable` when the hub implements table, list, board,
54
+ * and dashboard renderers. Omit `supportedViewTypes` on both components to use this default.
55
+ */
56
+ export const PRIMARY_HUB_SUPPORTED_VIEWS = [
57
+ "table",
58
+ "list",
59
+ "board",
60
+ "dashboard",
61
+ ] as const satisfies readonly DataListViewType[]
62
+
63
+ /**
64
+ * Default allowlist for **list-page hubs** in this design system (Library / Column types /
65
+ * Tokens, etc.). Matches the All questions hub: table, list, board, dashboard, folder,
66
+ * panel, and tree-panel. Pair with renderers for each kind on `HubTable` + `ListPageTemplate`.
67
+ */
68
+ export const FULL_HUB_SUPPORTED_VIEWS = [
69
+ "table",
70
+ "list",
71
+ "board",
72
+ "dashboard",
73
+ "folder",
74
+ "panel",
75
+ "tree-panel",
76
+ ] as const satisfies readonly DataListViewType[]
77
+
78
+ /** Every registered view type (includes folder, panel, calendar, tree-panel). */
79
+ export const ALL_DATA_LIST_VIEW_TYPES = DATA_LIST_VIEW_REGISTRY.map(d => d.value)
80
+
50
81
  export function dataListViewDefinition(view: DataListViewType): DataListViewDefinition {
51
82
  const def = BY_VALUE.get(view)
52
83
  if (!def) {
@@ -0,0 +1,162 @@
1
+ /**
2
+ * Sidebar / secondary-nav active-state helpers.
3
+ *
4
+ * Only one nav target should be "selected" at a time. Prefix matching
5
+ * (`/dashboard` active on `/dashboard/students`) is correct only when no
6
+ * more-specific nav href also matches the current pathname.
7
+ */
8
+
9
+ export function normalizePathname(pathname: string): string {
10
+ if (pathname.length > 1 && pathname.endsWith("/")) return pathname.slice(0, -1)
11
+ return pathname
12
+ }
13
+
14
+ /** Path segment before `#` (if any). */
15
+ export function navUrlPath(url: string): string {
16
+ const hash = url.indexOf("#")
17
+ return hash >= 0 ? url.slice(0, hash) : url
18
+ }
19
+
20
+ /** Hash fragment after `#`, or `null` when the href has no fragment. */
21
+ export function navUrlFragment(url: string): string | null {
22
+ const i = url.indexOf("#")
23
+ return i >= 0 ? url.slice(i + 1) : null
24
+ }
25
+
26
+ export function normalizedLocationHash(locationHash: string): string {
27
+ if (!locationHash) return ""
28
+ return locationHash.startsWith("#") ? locationHash.slice(1) : locationHash
29
+ }
30
+
31
+ /** Path → hash fragments claimed by *another* nav item at the same path. */
32
+ export function buildNavHashClaims(
33
+ urls: readonly string[],
34
+ ): ReadonlyMap<string, ReadonlySet<string>> {
35
+ const map = new Map<string, Set<string>>()
36
+ for (const url of urls) {
37
+ const p = navUrlPath(url)
38
+ const f = navUrlFragment(url)
39
+ if (!p || f === null) continue
40
+ let set = map.get(p)
41
+ if (!set) {
42
+ set = new Set<string>()
43
+ map.set(p, set)
44
+ }
45
+ set.add(f)
46
+ }
47
+ return map
48
+ }
49
+
50
+ function navHasMoreSpecificHashMatch(
51
+ pathname: string,
52
+ locationHash: string,
53
+ hashClaimsByPath: ReadonlyMap<string, ReadonlySet<string>>,
54
+ ): boolean {
55
+ const claims = hashClaimsByPath.get(pathname)
56
+ if (!claims) return false
57
+ const h = normalizedLocationHash(locationHash)
58
+ if (h === "") return false
59
+ return claims.has(h)
60
+ }
61
+
62
+ function pathnameMatchesNavPath(
63
+ pathname: string,
64
+ pathOnly: string,
65
+ locationHash: string,
66
+ hashClaimsByPath: ReadonlyMap<string, ReadonlySet<string>>,
67
+ ): boolean {
68
+ const norm = normalizePathname(pathname)
69
+ const h = normalizedLocationHash(locationHash)
70
+
71
+ if (pathOnly === "/") {
72
+ if (norm !== "/" || h !== "") return false
73
+ return !navHasMoreSpecificHashMatch("/", locationHash, hashClaimsByPath)
74
+ }
75
+
76
+ if (norm === pathOnly) {
77
+ return !navHasMoreSpecificHashMatch(pathOnly, locationHash, hashClaimsByPath)
78
+ }
79
+
80
+ if (pathOnly === "/library") {
81
+ return norm.startsWith("/library/")
82
+ }
83
+ if (pathOnly.startsWith("/library/")) {
84
+ return norm === pathOnly
85
+ }
86
+
87
+ return norm.startsWith(`${pathOnly}/`)
88
+ }
89
+
90
+ /**
91
+ * Longest matching nav path for `pathname` among `candidateUrls` (path + prefix rules).
92
+ * Returns the winning href string, or `null`.
93
+ */
94
+ export function resolveActiveNavHref(
95
+ pathname: string,
96
+ candidateUrls: readonly string[],
97
+ options?: {
98
+ locationHash?: string
99
+ hashClaimsByPath?: ReadonlyMap<string, ReadonlySet<string>>
100
+ },
101
+ ): string | null {
102
+ const norm = normalizePathname(pathname)
103
+ const hashClaims = options?.hashClaimsByPath ?? buildNavHashClaims(candidateUrls)
104
+ const locationHash = options?.locationHash ?? ""
105
+
106
+ let bestHref: string | null = null
107
+ let bestLen = -1
108
+
109
+ for (const url of candidateUrls) {
110
+ const pathOnly = navUrlPath(url)
111
+ const frag = navUrlFragment(url)
112
+ if (!pathOnly || pathOnly === "#") continue
113
+
114
+ let matches = false
115
+ if (frag !== null) {
116
+ const h = normalizedLocationHash(locationHash)
117
+ if (pathOnly === "/") matches = norm === "/" && h === frag
118
+ else if (pathOnly === "/library") matches = norm.startsWith("/library/") && h === frag
119
+ else if (pathOnly.startsWith("/library/")) matches = norm === pathOnly && h === frag
120
+ else matches = norm === pathOnly && h === frag
121
+ } else {
122
+ matches = pathnameMatchesNavPath(norm, pathOnly, locationHash, hashClaims)
123
+ }
124
+
125
+ if (matches && pathOnly.length > bestLen) {
126
+ bestHref = url
127
+ bestLen = pathOnly.length
128
+ }
129
+ }
130
+
131
+ return bestHref
132
+ }
133
+
134
+ /** True when `url` is the single best match for `pathname` among all nav hrefs. */
135
+ export function isNavHrefActive(
136
+ pathname: string,
137
+ url: string,
138
+ allNavUrls: readonly string[],
139
+ options?: {
140
+ locationHash?: string
141
+ hashClaimsByPath?: ReadonlyMap<string, ReadonlySet<string>>
142
+ },
143
+ ): boolean {
144
+ const active = resolveActiveNavHref(pathname, allNavUrls, options)
145
+ if (!active) return false
146
+ return navUrlPath(active) === navUrlPath(url) && navUrlFragment(active) === navUrlFragment(url)
147
+ }
148
+
149
+ /** Collect every `url` from a nav tree (primary rows + children). */
150
+ export function collectNavUrls(
151
+ items: ReadonlyArray<{ url?: string; children?: ReadonlyArray<{ url?: string; children?: unknown }> }>,
152
+ ): string[] {
153
+ const out: string[] = []
154
+ const walk = (list: ReadonlyArray<{ url?: string; children?: ReadonlyArray<{ url?: string; children?: unknown }> }>) => {
155
+ for (const it of list) {
156
+ if (typeof it.url === "string" && it.url.length > 0 && it.url !== "#") out.push(it.url)
157
+ if (Array.isArray(it.children)) walk(it.children)
158
+ }
159
+ }
160
+ walk(items)
161
+ return out
162
+ }
@@ -112,7 +112,8 @@ ListPageTemplate
112
112
  ```
113
113
 
114
114
  **Reference implementations:**
115
- - `components/columns-showcase.tsx` + `components/columns-client.tsx` — smallest single-view hub (start here)
115
+ - `components/library-table.tsx` + `library-client.tsx` — canonical seven-view hub (start here)
116
+ - `components/columns-showcase.tsx` — cell catalog via `LibraryTable` (custom `columnDefs`, same Add view as Library)
116
117
  - `components/tokens-themes-client.tsx` + `components/tokens-secondary-nav.tsx` — hub with secondary panel + URL-driven scope + built-in pagination chrome
117
118
  - `components/library-table.tsx` + `components/library-hub-client.tsx` — full multi-view hub (table, board, dashboard)
118
119
 
@@ -81,7 +81,7 @@ If two documents conflict, prefer the **more specific** rule for the file type,
81
81
 
82
82
  **MUST:** If the main surface is a **`DataTable`** (or equivalent data grid), wrap it in **`ListPageTemplate`** so the **views toolbar** exists (tabs, add view, per-tab settings). Do **not** place `DataTable` only under `PageHeader` without the tab shell.
83
83
 
84
- **Reference implementations:** `components/library-hub-client.tsx` (full hub), `components/columns-showcase.tsx` (single-view catalog), `components/tokens-themes-client.tsx` (hub + secondary panel).
84
+ **Reference implementations:** `components/library-client.tsx` + `components/library-table.tsx` (canonical seven-view hub), `components/columns-showcase.tsx` (cell catalog via `LibraryTable`), `components/tokens-themes-client.tsx` + `components/tokens-hub-auxiliary-views.tsx`.
85
85
 
86
86
  **Rationale:** Consistent navigation, saved views, per-tab view type (table / list / board / dashboard), export at template level.
87
87
 
@@ -99,6 +99,20 @@ If two documents conflict, prefer the **more specific** rule for the file type,
99
99
 
100
100
  **Details:** `docs/data-views-pattern.md` (mock data, connected views, dashboard view).
101
101
 
102
+ ### 4.1.1 Add view parity (`supportedViewTypes`)
103
+
104
+ **MUST:** Every **`ListPageTemplate`** hub that mounts **`HubTable`** uses the same **seven** Add view options as Library (All questions) unless a narrower list is **documented** in `lib/<entity>-supported-views.ts`:
105
+
106
+ - Registry constant: **`FULL_HUB_SUPPORTED_VIEWS`** (`@/lib/data-list-view-registry` or `@/lib/full-hub-supported-views.ts`).
107
+ - Pass the **same** allowlist to **`ListPageTemplate`** and **`HubTable`** (or omit on both for the default).
108
+ - Implement a **real renderer** for each allowed view (list = **`ListPageBoardCard`** via `renderListRow` — copy **`library-table.tsx`**).
109
+ - **`LibraryItem`** catalogs (Column types): use **`LibraryTable`** with `columnDefs` + `folders` — do not trim to four views or placeholder list rows.
110
+ - **Tokens:** **`tokens-hub-auxiliary-views.tsx`** + **`FULL_HUB_SUPPORTED_VIEWS`**.
111
+
112
+ **MUST NOT:** `supportedViewTypes={["table"]}`, bare two-line `renderListRow`, or `PRIMARY_HUB_SUPPORTED_VIEWS` without documented product exception.
113
+
114
+ **Binding rule:** `.cursor/rules/exxat-hub-supported-views.mdc`. **Pattern doc:** `docs/hub-supported-views-pattern.md`.
115
+
102
116
  ### 4.2 `TablePropertiesDrawer` and the active view
103
117
 
104
118
  **MUST:** Any page that uses **`ListPageTemplate`** with **`tab.viewType`** (table / list / board / dashboard) and renders **`TablePropertiesDrawer`** **MUST** pass:
@@ -634,6 +648,7 @@ Copy and complete when implementing or reviewing:
634
648
  - [ ] **Primary hub + large data:** Same composition as `PlacementsClient` / `TeamClient` (template + metrics when applicable).
635
649
  - [ ] **All view tabs:** List/board/dashboard use **`tableState.rows`**; dashboard view uses **`KeyMetrics`** + shared KPI helpers — no “not wired” placeholders or duplicate metric cards.
636
650
  - [ ] **Properties drawer:** **`TablePropertiesDrawer`** receives **`currentView`** and **`onViewChange`** from **`renderContent`** / **`updateTab`** + **`dataListViewIcon`** (§4.2) — not table-only copy on Board/List/Dashboard.
651
+ - [ ] **Add view parity:** **`FULL_HUB_SUPPORTED_VIEWS`** on **`ListPageTemplate`** + **`HubTable`** (in sync); every allowed view has a renderer; list uses **`ListPageBoardCard`** — **`.cursor/rules/exxat-hub-supported-views.mdc`**, **`docs/hub-supported-views-pattern.md`**.
637
652
  - [ ] **Data view dashboard (Placements / Team / Compliance):** Charts use **`ChartFigure`** + **`ChartDataTable`**; **Edit layout** on toolbar; **`activeBar` / `activeShape`** keyboard styling from **`lib/chart-keyboard-selection`** — not opacity-only **`Cell`** hacks (§4.3).
638
653
  - [ ] **Dashboard layout persistence:** **`lib/data-view-dashboard-storage`** (or **`saveDashboardLayout`** / **`loadDashboardLayout`** on Placements); **`mergeDashboardLayout`** on load — no new ad-hoc storage keys for the same layout (§4.3).
639
654
  - [ ] **⌘K palette (§7.1):** If adding or changing **`dataGroups`**, map rows in **`lib/command-menu-search-data.ts`** (not `command-menu.tsx`); use **`searchOnly`** on bulky groups; keep **`docs/command-menu-pattern.md`** aligned.
@@ -7,7 +7,7 @@
7
7
  * (`PrimaryPageTemplate` + `ListPageTemplate`):
8
8
  * - `header` : `PageHeader` with title + one-line subtitle describing the demo.
9
9
  * - `metrics` : `KeyMetrics` `variant="flat"` — patterns, pinned, sortable, demo rows.
10
- * - tabs : single `table` view tab (one demo table — no list / board variants).
10
+ * - tabs : default `table` tab; Add view offers list / board / dashboard (same as Library).
11
11
  * - `renderContent` : the `<ColumnsShowcase />` DataTable surface.
12
12
  *
13
13
  * Cell patterns are exercised inside `columns-showcase.tsx` so the rendered
@@ -33,6 +33,7 @@ import {
33
33
  COLUMNS_SHOWCASE_PATTERN_COUNT,
34
34
  COLUMNS_SHOWCASE_PINNED_COUNT,
35
35
  COLUMNS_SHOWCASE_SORTABLE_COUNT,
36
+ COLUMNS_SUPPORTED_VIEWS,
36
37
  } from "@/components/columns-showcase"
37
38
 
38
39
  const COLUMNS_DEFAULT_TABS: ViewTab[] = [
@@ -127,8 +128,8 @@ export function ColumnsClient() {
127
128
  onTabsChange={setTabs}
128
129
  activeTabId={activeTabId}
129
130
  onActiveTabChange={setActiveTabId}
130
- supportedViewTypes={["table"]}
131
131
  getTabCount={getTabCount}
132
+ supportedViewTypes={COLUMNS_SUPPORTED_VIEWS}
132
133
  header={
133
134
  <PageHeader
134
135
  title="Column types"
@@ -48,7 +48,6 @@
48
48
 
49
49
  import * as React from "react"
50
50
  import {
51
- HubTable,
52
51
  AttachmentCountCell,
53
52
  BooleanToggleCell,
54
53
  CurrencyCell,
@@ -66,6 +65,9 @@ import {
66
65
  type RowActionDef,
67
66
  } from "@/components/data-views"
68
67
  import type { DataListViewType } from "@/lib/data-list-view"
68
+ import { FULL_HUB_SUPPORTED_VIEWS } from "@/lib/data-list-view-registry"
69
+ import { LibraryTable } from "@/components/library-table"
70
+ import { DEFAULT_LIBRARY_FOLDERS, type LibraryFolder } from "@/lib/mock/library-folders"
69
71
  import { AvatarInitials } from "@/components/ui/avatar"
70
72
  import { cn } from "@/lib/utils"
71
73
  import {
@@ -477,7 +479,8 @@ export const COLUMNS_SHOWCASE_PATTERN_COUNT = 18
477
479
  export const COLUMNS_SHOWCASE_PINNED_COUNT = 3 // select + name + actions
478
480
  export const COLUMNS_SHOWCASE_SORTABLE_COUNT = 11 // name, owner, type, level, rating, progress, cost, count, files, lastActivityAt, updatedAt
479
481
 
480
- const COLUMNS_SUPPORTED_VIEWS: readonly DataListViewType[] = ["table"] as const
482
+ /** Same seven views as Library / All questions (Add view + Properties). */
483
+ export const COLUMNS_SUPPORTED_VIEWS = FULL_HUB_SUPPORTED_VIEWS
481
484
 
482
485
  export interface ColumnsShowcaseProps {
483
486
  /** Active view from `ListPageTemplate.renderContent`. */
@@ -493,6 +496,9 @@ export interface ColumnsShowcaseProps {
493
496
  */
494
497
  export function ColumnsShowcase({ view, onViewChange }: ColumnsShowcaseProps) {
495
498
  const [rows, setRows] = React.useState<LibraryItem[]>(() => buildRows())
499
+ const [folders, setFolders] = React.useState<LibraryFolder[]>(() =>
500
+ DEFAULT_LIBRARY_FOLDERS.map(f => ({ ...f })),
501
+ )
496
502
  const [pagination, setPagination] = React.useState(false)
497
503
 
498
504
  const toggleFavorite = React.useCallback((row: LibraryItem) => {
@@ -514,28 +520,26 @@ export function ColumnsShowcase({ view, onViewChange }: ColumnsShowcaseProps) {
514
520
  const columns = useColumns(toggleFavorite, togglePublished)
515
521
 
516
522
  return (
517
- <HubTable<LibraryItem>
518
- rows={rows}
519
- columns={columns}
523
+ <LibraryTable
524
+ items={rows}
525
+ onItemsChange={setRows}
526
+ folders={folders}
527
+ onFoldersChange={setFolders}
520
528
  view={view}
521
529
  onViewChange={onViewChange}
522
- supportedViewTypes={COLUMNS_SUPPORTED_VIEWS}
523
- hubLabel="Column types"
524
- lifecycleTabLabel="Column types"
525
- searchAriaLabel="Search columns showcase"
526
- getRowId={(r) => r.id}
527
- getRowSelectionLabel={(r) => r.stem}
528
- defaultSort={{ key: "stem", dir: "asc" }}
530
+ columnDefs={columns}
531
+ hubLabels={{
532
+ hubLabel: "Column types",
533
+ lifecycleTabLabel: "Column types",
534
+ searchAriaLabel: "Search columns showcase",
535
+ listAriaLabel: "Column types",
536
+ defaultSort: { key: "stem", dir: "asc" },
537
+ }}
529
538
  pagination={pagination}
530
539
  onPaginationChange={setPagination}
531
540
  paginationInitialPageSize={5}
532
541
  paginationPageSizeOptions={[5, 10, 25]}
533
- emptyState={
534
- <p className="text-sm text-muted-foreground">
535
- No rows match your filters.
536
- </p>
537
- }
538
- renderers={{}}
542
+ showBulkActions={false}
539
543
  />
540
544
  )
541
545
  }
@@ -186,7 +186,7 @@ function ExxatLogoBase({
186
186
  export function ExxatProductLogo({
187
187
  product,
188
188
  className,
189
- variant = "default",
189
+ variant: _variant = "default",
190
190
  }: ExxatProductLogoProps) {
191
191
  const customProductBrand = useAppStore(s => s.customProductBrand)
192
192
  const productBrandColors = useAppStore(s => s.productBrandColors)