@exxatdesignux/ui 0.2.17 → 0.2.19

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 (162) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/consumer-extras/AGENTS.md +76 -0
  3. package/consumer-extras/README.md +5 -1
  4. package/consumer-extras/cursor-skills/exxat-centralized-list-dataset/SKILL.md +14 -3
  5. package/consumer-extras/cursor-skills/exxat-consumer-app/SKILL.md +37 -0
  6. package/consumer-extras/cursor-skills/exxat-ds-skill/SKILL.md +22 -7
  7. package/consumer-extras/cursor-skills/exxat-focused-workflow-page/SKILL.md +57 -0
  8. package/consumer-extras/cursor-skills/exxat-kpi-flat-band/SKILL.md +38 -0
  9. package/consumer-extras/cursor-skills/exxat-primary-nav-secondary-panel/SKILL.md +10 -3
  10. package/consumer-extras/patterns/consumer-app-pattern.md +39 -0
  11. package/consumer-extras/patterns/consumer-upgrade-checklist.md +20 -0
  12. package/consumer-extras/patterns/data-views-pattern.md +42 -3
  13. package/consumer-extras/patterns/focused-workflow-page-pattern.md +84 -0
  14. package/consumer-extras/patterns/kpi-flat-band-pattern.md +57 -0
  15. package/consumer-extras/patterns/shell-surface-elevation-pattern.md +54 -0
  16. package/package.json +2 -1
  17. package/src/components/ui/button-group.tsx +81 -0
  18. package/src/components/ui/button.tsx +4 -4
  19. package/src/components/ui/sidebar.tsx +2 -2
  20. package/src/globals.css +7 -1807
  21. package/src/theme.css +10 -1126
  22. package/src/tokens/README.md +15 -0
  23. package/src/tokens/base.css +337 -0
  24. package/src/tokens/high-contrast.css +1195 -0
  25. package/src/tokens/layers.css +224 -0
  26. package/src/tokens/tailwind-bridge.css +118 -0
  27. package/src/tokens/themes.css +201 -0
  28. package/template/.cursor/rules/exxat-kbd-shortcuts.mdc +1 -1
  29. package/template/AGENTS.md +66 -21
  30. package/template/app/(app)/dashboard/loading.tsx +3 -15
  31. package/template/app/(app)/dashboard/page.tsx +2 -14
  32. package/template/app/(app)/data-list/layout.tsx +43 -0
  33. package/template/app/(app)/data-list/page.tsx +2 -2
  34. package/template/app/(app)/error.tsx +22 -6
  35. package/template/app/(app)/examples/focused-workflow/page.tsx +5 -0
  36. package/template/app/(app)/examples/page.tsx +1 -0
  37. package/template/app/(app)/layout.tsx +13 -6
  38. package/template/app/(app)/loading.tsx +1 -18
  39. package/template/app/(app)/question-bank/find/page.tsx +2 -1
  40. package/template/app/(app)/question-bank/library/page.tsx +2 -1
  41. package/template/app/(app)/question-bank/list/page.tsx +2 -1
  42. package/template/app/(app)/question-bank/new/page.tsx +15 -23
  43. package/template/app/(app)/question-bank/page.tsx +2 -1
  44. package/template/app/(app)/settings/page.tsx +4 -5
  45. package/template/app/global-error.tsx +63 -0
  46. package/template/app/globals.css +7 -1934
  47. package/template/app/layout.tsx +2 -0
  48. package/template/components/app-route-loading.tsx +14 -0
  49. package/template/components/app-sidebar.tsx +71 -55
  50. package/template/components/data-table/index.tsx +31 -67
  51. package/template/components/data-table/use-table-state.ts +33 -6
  52. package/template/components/data-views/index.ts +37 -9
  53. package/template/components/data-views/list-page-calendar-view.tsx +593 -0
  54. package/template/components/data-views/list-page-connected-view-body.tsx +66 -0
  55. package/template/components/data-views/list-page-folder-columns-panel.tsx +345 -0
  56. package/template/components/data-views/list-page-split-hub-chrome.tsx +8 -0
  57. package/template/components/dev-chunk-load-recovery.tsx +41 -0
  58. package/template/components/examples/focused-workflow-showcase.tsx +183 -0
  59. package/template/components/exxat-product-logo.tsx +2 -6
  60. package/template/components/key-metrics.tsx +54 -22
  61. package/template/components/list-hub-board-view.tsx +68 -0
  62. package/template/components/list-hub-client.tsx +186 -0
  63. package/template/components/list-hub-list-view.tsx +36 -0
  64. package/template/components/list-hub-panel-activator.tsx +8 -0
  65. package/template/components/list-hub-secondary-nav.tsx +121 -0
  66. package/template/components/list-hub-table.tsx +336 -0
  67. package/template/components/new-question-composer.tsx +6 -24
  68. package/template/components/product-switcher.tsx +5 -5
  69. package/template/components/product-wordmark.tsx +4 -7
  70. package/template/components/question-bank-client.tsx +4 -1
  71. package/template/components/question-bank-folder-columns-panel.tsx +104 -0
  72. package/template/components/question-bank-hub-client.tsx +2 -5
  73. package/template/components/question-bank-table.tsx +155 -509
  74. package/template/components/secondary-panel/nav-link-rows.tsx +83 -0
  75. package/template/components/secondary-panel.tsx +4 -44
  76. package/template/components/secondary-panels/list-hub-panel.tsx +39 -0
  77. package/template/components/secondary-panels/question-bank-panel.tsx +39 -0
  78. package/template/components/secondary-panels/registry.tsx +15 -0
  79. package/template/components/settings-appearance-card.tsx +3 -2
  80. package/template/components/settings-client.tsx +59 -15
  81. package/template/components/settings-form-row.tsx +9 -4
  82. package/template/components/sidebar-shell.tsx +2 -1
  83. package/template/components/table-properties/drawer-button.tsx +51 -20
  84. package/template/components/table-properties/drawer.tsx +81 -17
  85. package/template/components/templates/focused-workflow-layouts.tsx +448 -0
  86. package/template/components/templates/focused-workflow-page-template.tsx +69 -0
  87. package/template/components/templates/list-page.tsx +40 -13
  88. package/template/components/templates/nested-secondary-panel-shell.tsx +3 -2
  89. package/template/components/templates/page-loading-shell.tsx +262 -0
  90. package/template/components/ui/button-group.tsx +1 -0
  91. package/template/contexts/product-context.tsx +21 -2
  92. package/template/docs/consumer-app-pattern.md +39 -0
  93. package/template/docs/data-views-pattern.md +42 -3
  94. package/template/docs/drawer-vs-dialog-pattern.md +3 -1
  95. package/template/docs/focused-workflow-page-pattern.md +84 -0
  96. package/template/docs/kpi-flat-band-pattern.md +57 -0
  97. package/template/docs/kpi-strip-max-four-pattern.md +1 -0
  98. package/template/docs/shell-surface-elevation-pattern.md +54 -0
  99. package/template/lib/chunk-load-error.ts +13 -0
  100. package/template/lib/command-menu-search-data.ts +11 -27
  101. package/template/lib/conditional-rule-match.ts +87 -22
  102. package/template/lib/data-list-display-options.ts +16 -2
  103. package/template/lib/data-list-view-registry.ts +104 -0
  104. package/template/lib/data-list-view-surface.ts +15 -1
  105. package/template/lib/data-list-view.ts +16 -1
  106. package/template/lib/data-view-dashboard-storage.ts +38 -35
  107. package/template/lib/hub-connected-view-renderers.ts +58 -0
  108. package/template/lib/list-hub-nav.ts +121 -0
  109. package/template/lib/list-hub-supported-views.ts +10 -0
  110. package/template/lib/list-page-table-properties.ts +3 -7
  111. package/template/lib/list-status-badges.ts +4 -97
  112. package/template/lib/mock/list-hub-directory.ts +27 -0
  113. package/template/lib/mock/list-hub-kpi.ts +27 -0
  114. package/template/lib/mock/navigation.tsx +1 -0
  115. package/template/lib/page-loading-variant.ts +40 -0
  116. package/template/lib/question-bank-supported-views.ts +13 -0
  117. package/template/lib/sidebar-state-cookie.ts +9 -0
  118. package/template/lib/table-state-lifecycle.ts +60 -13
  119. package/template/app/(app)/data-list/[id]/page.tsx +0 -44
  120. package/template/app/(app)/data-list/new/page.tsx +0 -34
  121. package/template/components/compliance-board-view.tsx +0 -142
  122. package/template/components/compliance-client.tsx +0 -92
  123. package/template/components/compliance-list-view.tsx +0 -54
  124. package/template/components/compliance-page-header.tsx +0 -89
  125. package/template/components/compliance-table.tsx +0 -632
  126. package/template/components/data-view-dashboard-charts-compliance.tsx +0 -963
  127. package/template/components/data-view-dashboard-charts-team.tsx +0 -971
  128. package/template/components/data-view-dashboard-charts.tsx +0 -1503
  129. package/template/components/new-placement-back-btn.tsx +0 -28
  130. package/template/components/new-placement-form.tsx +0 -1068
  131. package/template/components/placement-board-card.tsx +0 -262
  132. package/template/components/placement-detail.tsx +0 -438
  133. package/template/components/placements-board-view.tsx +0 -404
  134. package/template/components/placements-client.tsx +0 -252
  135. package/template/components/placements-list-view.tsx +0 -171
  136. package/template/components/placements-page-header.tsx +0 -166
  137. package/template/components/placements-table-cells.test.tsx +0 -22
  138. package/template/components/placements-table-cells.tsx +0 -173
  139. package/template/components/placements-table-columns.tsx +0 -640
  140. package/template/components/placements-table.tsx +0 -1675
  141. package/template/components/rotations-empty-state.tsx +0 -50
  142. package/template/components/rotations-panel-activator.tsx +0 -8
  143. package/template/components/sites-all-client.tsx +0 -154
  144. package/template/components/sites-board-view.tsx +0 -67
  145. package/template/components/sites-list-view.tsx +0 -42
  146. package/template/components/sites-table.tsx +0 -402
  147. package/template/components/team-board-view.tsx +0 -122
  148. package/template/components/team-client.tsx +0 -100
  149. package/template/components/team-list-view.tsx +0 -59
  150. package/template/components/team-page-header.tsx +0 -92
  151. package/template/components/team-table.tsx +0 -714
  152. package/template/lib/data-view-dashboard-placements-layout.ts +0 -215
  153. package/template/lib/mock/compliance-kpi.ts +0 -61
  154. package/template/lib/mock/compliance.ts +0 -146
  155. package/template/lib/mock/placements-kpi.ts +0 -134
  156. package/template/lib/mock/placements.ts +0 -183
  157. package/template/lib/mock/sites-directory.ts +0 -16
  158. package/template/lib/mock/sites-kpi.ts +0 -25
  159. package/template/lib/mock/team-kpi.ts +0 -60
  160. package/template/lib/mock/team.ts +0 -118
  161. package/template/lib/placement-board-card-layout.ts +0 -79
  162. package/template/lib/placement-lifecycle.ts +0 -5
@@ -0,0 +1,83 @@
1
+ "use client"
2
+
3
+ import Link from "next/link"
4
+ import { Tip } from "@/components/ui/tip"
5
+ import { cn } from "@/lib/utils"
6
+
7
+ export function SecondaryPanelNavRow({
8
+ href,
9
+ active,
10
+ iconClass,
11
+ label,
12
+ onClick,
13
+ }: {
14
+ href: string
15
+ active: boolean
16
+ iconClass: string
17
+ label: string
18
+ onClick?: () => void
19
+ }) {
20
+ return (
21
+ <li className="min-w-0">
22
+ <Tip label={label} side="right">
23
+ <Link
24
+ href={href}
25
+ scroll={false}
26
+ onClick={() => onClick?.()}
27
+ aria-current={active ? "page" : undefined}
28
+ className={cn(
29
+ "flex w-full min-w-0 items-center gap-2 rounded-md px-2 py-1.5 text-left text-sm transition-colors",
30
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset",
31
+ active
32
+ ? "bg-sidebar-accent font-medium text-sidebar-accent-foreground"
33
+ : "text-sidebar-foreground hover:bg-sidebar-accent/50",
34
+ )}
35
+ >
36
+ <span className="size-4 shrink-0 text-center text-[13px] leading-none" aria-hidden>
37
+ <i className={cn(active ? "fa-solid" : "fa-light", iconClass)} aria-hidden />
38
+ </span>
39
+ <span className="min-w-0 flex-1 truncate">{label}</span>
40
+ </Link>
41
+ </Tip>
42
+ </li>
43
+ )
44
+ }
45
+
46
+ /** Icon-rail row — matches primary sidebar collapsed hit target (`size-9`). */
47
+ export function SecondaryPanelIconNavRow({
48
+ href,
49
+ active,
50
+ iconClass,
51
+ label,
52
+ onClick,
53
+ }: {
54
+ href: string
55
+ active: boolean
56
+ iconClass: string
57
+ label: string
58
+ onClick?: () => void
59
+ }) {
60
+ return (
61
+ <li className="flex w-full justify-center" role="none">
62
+ <Tip label={label} side="right">
63
+ <Link
64
+ href={href}
65
+ scroll={false}
66
+ onClick={() => onClick?.()}
67
+ aria-current={active ? "page" : undefined}
68
+ className={cn(
69
+ "flex size-9 shrink-0 items-center justify-center rounded-md transition-colors",
70
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset",
71
+ active
72
+ ? "bg-sidebar-accent text-sidebar-accent-foreground"
73
+ : "text-sidebar-foreground hover:bg-sidebar-accent/50",
74
+ )}
75
+ >
76
+ <span className="text-center text-[15px] leading-none" aria-hidden>
77
+ <i className={cn(active ? "fa-solid" : "fa-light", iconClass)} aria-hidden />
78
+ </span>
79
+ </Link>
80
+ </Tip>
81
+ </li>
82
+ )
83
+ }
@@ -4,16 +4,14 @@
4
4
  * SecondaryPanel — nested rail between the primary icon sidebar and content.
5
5
  * Full width shows hub scope nav; **compact** matches the primary sidebar icon rail (`w-12`).
6
6
  *
7
- * Chrome uses {@link NestedSecondaryPanelShell}. Question bank body stays in
8
- * `question-bank-secondary-nav.tsx` (domain-specific), not duplicated here.
7
+ * Chrome uses {@link NestedSecondaryPanelShell}. Panel bodies live in
8
+ * `components/secondary-panels/` + {@link SECONDARY_PANELS}; hub nav stays domain-specific.
9
9
  */
10
10
 
11
11
  import * as React from "react"
12
12
  import { useSidebar } from "@/components/ui/sidebar"
13
- import { Tip } from "@/components/ui/tip"
14
- import { Button } from "@/components/ui/button"
15
- import { QuestionBankSecondaryNav } from "@/components/question-bank-secondary-nav"
16
13
  import { NestedSecondaryPanelShell } from "@/components/templates/nested-secondary-panel-shell"
14
+ import { SECONDARY_PANELS } from "@/components/secondary-panels/registry"
17
15
  import { useSidebarReflowZoom } from "@/hooks/use-sidebar-reflow-zoom"
18
16
  import type { QuestionBankItem } from "@/lib/mock/question-bank"
19
17
  import type { QuestionBankFolder } from "@/lib/mock/question-bank-folders"
@@ -166,47 +164,9 @@ export function SecondaryPanelProvider({ children }: { children: React.ReactNode
166
164
  // SecondaryPanel — the actual rendered panel
167
165
  // ─────────────────────────────────────────────────────────────────────────────
168
166
 
169
- function QuestionBankPanel() {
170
- const { collapseActiveSecondaryPanel, secondaryPanelCompact } = useSecondaryPanel()
171
-
172
- if (secondaryPanelCompact) {
173
- return <QuestionBankSecondaryNav />
174
- }
175
-
176
- return (
177
- <>
178
- <div className="flex items-center justify-between gap-2 px-4 pt-4 pb-2">
179
- <h2
180
- className="text-xl font-semibold leading-tight text-sidebar-foreground"
181
- style={{ fontFamily: "var(--font-heading)" }}
182
- >
183
- Library
184
- </h2>
185
- <Tip label="Collapse to icons" side="bottom">
186
- <Button
187
- type="button"
188
- size="icon"
189
- variant="ghost"
190
- onClick={() => collapseActiveSecondaryPanel()}
191
- aria-label="Collapse to icons"
192
- >
193
- <i className="fa-light fa-angles-left" aria-hidden="true" />
194
- </Button>
195
- </Tip>
196
- </div>
197
- <QuestionBankSecondaryNav />
198
- </>
199
- )
200
- }
201
-
202
- /** Register panel components by id when a route opts into `secondaryPanel` in nav. */
203
- const PANELS: Record<string, React.FC> = {
204
- "question-bank": QuestionBankPanel,
205
- }
206
-
207
167
  export function SecondaryPanel() {
208
168
  const { activePanel, secondaryPanelCompact } = useSecondaryPanel()
209
- const PanelContent = activePanel ? PANELS[activePanel] : null
169
+ const PanelContent = activePanel ? SECONDARY_PANELS[activePanel] : null
210
170
 
211
171
  return (
212
172
  <NestedSecondaryPanelShell open={Boolean(activePanel)} compact={secondaryPanelCompact}>
@@ -0,0 +1,39 @@
1
+ "use client"
2
+
3
+ import { Button } from "@/components/ui/button"
4
+ import { Tip } from "@/components/ui/tip"
5
+ import { ListHubSecondaryNav } from "@/components/list-hub-secondary-nav"
6
+ import { useSecondaryPanel } from "@/components/secondary-panel"
7
+
8
+ export function ListHubPanel() {
9
+ const { collapseActiveSecondaryPanel, secondaryPanelCompact } = useSecondaryPanel()
10
+
11
+ if (secondaryPanelCompact) {
12
+ return <ListHubSecondaryNav />
13
+ }
14
+
15
+ return (
16
+ <>
17
+ <div className="flex items-center justify-between gap-2 px-4 pt-4 pb-2">
18
+ <h2
19
+ className="text-xl font-semibold leading-tight text-sidebar-foreground"
20
+ style={{ fontFamily: "var(--font-heading)" }}
21
+ >
22
+ Directory
23
+ </h2>
24
+ <Tip label="Collapse to icons" side="bottom">
25
+ <Button
26
+ type="button"
27
+ size="icon"
28
+ variant="ghost"
29
+ onClick={() => collapseActiveSecondaryPanel()}
30
+ aria-label="Collapse to icons"
31
+ >
32
+ <i className="fa-light fa-angles-left" aria-hidden="true" />
33
+ </Button>
34
+ </Tip>
35
+ </div>
36
+ <ListHubSecondaryNav />
37
+ </>
38
+ )
39
+ }
@@ -0,0 +1,39 @@
1
+ "use client"
2
+
3
+ import { Button } from "@/components/ui/button"
4
+ import { Tip } from "@/components/ui/tip"
5
+ import { QuestionBankSecondaryNav } from "@/components/question-bank-secondary-nav"
6
+ import { useSecondaryPanel } from "@/components/secondary-panel"
7
+
8
+ export function QuestionBankPanel() {
9
+ const { collapseActiveSecondaryPanel, secondaryPanelCompact } = useSecondaryPanel()
10
+
11
+ if (secondaryPanelCompact) {
12
+ return <QuestionBankSecondaryNav />
13
+ }
14
+
15
+ return (
16
+ <>
17
+ <div className="flex items-center justify-between gap-2 px-4 pt-4 pb-2">
18
+ <h2
19
+ className="text-xl font-semibold leading-tight text-sidebar-foreground"
20
+ style={{ fontFamily: "var(--font-heading)" }}
21
+ >
22
+ Library
23
+ </h2>
24
+ <Tip label="Collapse to icons" side="bottom">
25
+ <Button
26
+ type="button"
27
+ size="icon"
28
+ variant="ghost"
29
+ onClick={() => collapseActiveSecondaryPanel()}
30
+ aria-label="Collapse to icons"
31
+ >
32
+ <i className="fa-light fa-angles-left" aria-hidden="true" />
33
+ </Button>
34
+ </Tip>
35
+ </div>
36
+ <QuestionBankSecondaryNav />
37
+ </>
38
+ )
39
+ }
@@ -0,0 +1,15 @@
1
+ import type * as React from "react"
2
+
3
+ import { ListHubPanel } from "@/components/secondary-panels/list-hub-panel"
4
+ import { QuestionBankPanel } from "@/components/secondary-panels/question-bank-panel"
5
+
6
+ /**
7
+ * Secondary panel bodies keyed by `NavLinkItem.secondaryPanel` / `useAutoPanel(panelId)`.
8
+ * Add a new entry here + nav `secondaryPanel` id + route layout — do not extend Question bank files.
9
+ */
10
+ export const SECONDARY_PANELS: Record<string, React.FC> = {
11
+ "question-bank": QuestionBankPanel,
12
+ "list-hub": ListHubPanel,
13
+ }
14
+
15
+ export type SecondaryPanelId = keyof typeof SECONDARY_PANELS
@@ -518,7 +518,7 @@ export function SettingsAppearanceCard() {
518
518
  )
519
519
 
520
520
  return (
521
- <section id="appearance" className="scroll-mt-20">
521
+ <>
522
522
  <header className="mb-8 space-y-1">
523
523
  <h2 className="text-lg font-semibold text-foreground">Appearance &amp; display</h2>
524
524
  <p className="text-sm text-muted-foreground">Saved in this browser.</p>
@@ -529,6 +529,7 @@ export function SettingsAppearanceCard() {
529
529
  ) : (
530
530
  <FieldGroup className="gap-8">
531
531
  <SettingsFormRow
532
+ layout="stacked"
532
533
  label="Products"
533
534
  description="Recolour the brand mark + wordmark for each product. Switch the active product from the sidebar."
534
535
  >
@@ -862,6 +863,6 @@ export function SettingsAppearanceCard() {
862
863
  </DialogFooter>
863
864
  </DialogContent>
864
865
  </Dialog>
865
- </section>
866
+ </>
866
867
  )
867
868
  }
@@ -39,6 +39,19 @@ import { SettingsAppearanceCard } from "@/components/settings-appearance-card"
39
39
  import { SettingsFormRow } from "@/components/settings-form-row"
40
40
  import { FieldGroup } from "@/components/ui/field"
41
41
  import { FilterTextValueInput } from "@/components/data-table/filter-text-value-input"
42
+ import {
43
+ FocusedWorkflowSidebarSections,
44
+ type FocusedWorkflowSidebarSection,
45
+ } from "@/components/templates/focused-workflow-layouts"
46
+ import { PageHeader } from "@/components/page-header"
47
+
48
+ const SETTINGS_SECTIONS: readonly FocusedWorkflowSidebarSection[] = [
49
+ { id: "account", label: "Account" },
50
+ { id: "appearance", label: "Appearance" },
51
+ { id: "input-formats", label: "Input formats" },
52
+ { id: "banner", label: "System banner" },
53
+ { id: "tours", label: "Guided tours" },
54
+ ]
42
55
 
43
56
  const SYSTEM_BANNER_VARIANTS: SystemBannerVariant[] = [
44
57
  "info",
@@ -363,8 +376,32 @@ function buildFlowStatuses() {
363
376
  }))
364
377
  }
365
378
 
379
+ function sectionIdFromHash(hash: string): string {
380
+ const id = hash.startsWith("#") ? hash.slice(1) : hash
381
+ if (id && SETTINGS_SECTIONS.some(s => s.id === id)) return id
382
+ return SETTINGS_SECTIONS[0]!.id
383
+ }
384
+
366
385
  export function SettingsClient() {
367
386
  const router = useRouter()
387
+ const [activeSection, setActiveSection] = React.useState(() =>
388
+ typeof window !== "undefined" ? sectionIdFromHash(window.location.hash) : SETTINGS_SECTIONS[0]!.id,
389
+ )
390
+
391
+ React.useEffect(() => {
392
+ const syncFromHash = () => {
393
+ const id = sectionIdFromHash(window.location.hash)
394
+ setActiveSection(id)
395
+ if (window.location.hash) {
396
+ requestAnimationFrame(() => {
397
+ document.getElementById(id)?.scrollIntoView({ behavior: "smooth", block: "start" })
398
+ })
399
+ }
400
+ }
401
+ syncFromHash()
402
+ window.addEventListener("hashchange", syncFromHash)
403
+ return () => window.removeEventListener("hashchange", syncFromHash)
404
+ }, [])
368
405
 
369
406
  const [demoPhone, setDemoPhone] = React.useState("")
370
407
  const [demoZip, setDemoZip] = React.useState("")
@@ -403,20 +440,27 @@ export function SettingsClient() {
403
440
  }
404
441
 
405
442
  return (
406
- <div className="flex w-full min-w-0 flex-col">
407
- <div>
408
- <h1
409
- className="text-2xl font-semibold tracking-tight leading-tight text-foreground"
410
- style={{ fontFamily: "var(--font-heading)" }}
411
- >
412
- Settings
413
- </h1>
414
- <p className="mt-1 text-sm leading-relaxed text-muted-foreground">
415
- Preferences and tools for this workspace. Display options apply on this device and are stored in your browser.
416
- </p>
417
- </div>
418
-
419
- <div className="mt-10 flex flex-col gap-20">
443
+ <FocusedWorkflowSidebarSections
444
+ sections={SETTINGS_SECTIONS}
445
+ activeSectionId={activeSection}
446
+ onSectionSelect={id => {
447
+ setActiveSection(id)
448
+ const nextHash = `#${id}`
449
+ if (window.location.hash !== nextHash) {
450
+ window.history.replaceState(null, "", `${window.location.pathname}${window.location.search}${nextHash}`)
451
+ }
452
+ document.getElementById(id)?.scrollIntoView({ behavior: "smooth", block: "start" })
453
+ }}
454
+ navLabel="Settings sections"
455
+ header={
456
+ <PageHeader
457
+ title="Settings"
458
+ subtitle="Preferences and tools for this workspace. Display options apply on this device and are stored in your browser."
459
+ className="px-0 pt-0 pb-0 lg:px-0"
460
+ />
461
+ }
462
+ >
463
+ <div className="flex flex-col gap-20">
420
464
  <section id="account" className="scroll-mt-20">
421
465
  <header className="mb-6 space-y-1">
422
466
  <h2 className="text-lg font-semibold text-foreground">Account</h2>
@@ -534,6 +578,6 @@ export function SettingsClient() {
534
578
  </div>
535
579
  </section>
536
580
  </div>
537
- </div>
581
+ </FocusedWorkflowSidebarSections>
538
582
  )
539
583
  }
@@ -5,7 +5,7 @@ import { Label } from "@/components/ui/label"
5
5
  import { cn } from "@/lib/utils"
6
6
 
7
7
  /**
8
- * Two-column settings row: label + helper on the left, controls on the right.
8
+ * Settings row — inline (label left, control right on lg+) or stacked (label above).
9
9
  */
10
10
  export function SettingsFormRow({
11
11
  label,
@@ -13,22 +13,27 @@ export function SettingsFormRow({
13
13
  htmlFor,
14
14
  children,
15
15
  className,
16
+ layout = "inline",
16
17
  }: {
17
18
  label: string
18
19
  description?: string
19
20
  htmlFor?: string
20
21
  children: React.ReactNode
21
22
  className?: string
23
+ /** `stacked` — label block above full-width controls (wide panels, product lists). */
24
+ layout?: "inline" | "stacked"
22
25
  }) {
23
26
  return (
24
27
  <div
25
28
  className={cn(
26
- "grid grid-cols-1 gap-3 sm:gap-4 lg:grid-cols-[minmax(0,220px)_minmax(0,1fr)] lg:gap-10 lg:items-start",
27
- "border-b border-border/70 pb-8 last:border-0 last:pb-0",
29
+ "grid grid-cols-1 gap-3 sm:gap-4 border-b border-border/70 pb-8 last:border-0 last:pb-0",
30
+ layout === "inline" &&
31
+ "lg:grid-cols-[minmax(0,220px)_minmax(0,1fr)] lg:gap-10 lg:items-start",
32
+ layout === "stacked" && "gap-4",
28
33
  className,
29
34
  )}
30
35
  >
31
- <div className="space-y-1 lg:pt-1 text-start">
36
+ <div className="space-y-1 text-start lg:pt-1">
32
37
  <Label htmlFor={htmlFor} className="text-sm font-medium text-foreground">
33
38
  {label}
34
39
  </Label>
@@ -3,7 +3,8 @@
3
3
  /**
4
4
  * SidebarShell — SidebarProvider with layout-aware widths.
5
5
  * Desktop expanded/collapsed is persisted in the `sidebar_state` cookie by `@exxatdesignux/ui`
6
- * `SidebarProvider` (read on mount + write on toggle).
6
+ * `SidebarProvider` (read on mount + write on toggle). `(app)/layout` passes
7
+ * `defaultOpen` from the same cookie on the server so SSR matches the first client paint.
7
8
  */
8
9
 
9
10
  import * as React from "react"
@@ -23,6 +23,7 @@ import { TablePropertiesDrawer } from "./drawer"
23
23
  import type { ActiveFilter, ConditionalRule, FilterFieldDef, SortRule } from "./types"
24
24
  import type { RowHeight } from "@/lib/row-height"
25
25
  import type { DataListViewType } from "@/lib/data-list-view"
26
+ import { dataListViewSelectionTilesForHub } from "@/lib/data-list-view-registry"
26
27
  import type { DataListDisplayOptions } from "@/lib/data-list-display-options"
27
28
 
28
29
  // ─────────────────────────────────────────────────────────────────────────────
@@ -96,6 +97,8 @@ export interface TablePropertiesDrawerButtonProps {
96
97
  /** View type shown in the drawer header tile grid. */
97
98
  currentView?: DataListViewType
98
99
  onViewChange?: (v: DataListViewType) => void
100
+ /** When set, Properties view tiles match `ListPageTemplate` `supportedViewTypes`. */
101
+ supportedViewTypes?: readonly DataListViewType[]
99
102
  /** Shown below the "Properties" title in the drawer header (e.g. "Team", "Compliance"). */
100
103
  lifecycleTabLabel?: string
101
104
  /**
@@ -129,6 +132,7 @@ export function TablePropertiesDrawerButton({
129
132
  onPaginationChange,
130
133
  currentView,
131
134
  onViewChange,
135
+ supportedViewTypes,
132
136
  lifecycleTabLabel,
133
137
  boardGroupByColumnOptions,
134
138
  extraActions,
@@ -151,6 +155,32 @@ export function TablePropertiesDrawerButton({
151
155
  sortKey,
152
156
  } = state
153
157
 
158
+ // Sheet is portaled; keep latest handlers so sort/filter/conditional edits are not lost.
159
+ const stateRef = React.useRef(state)
160
+ stateRef.current = state
161
+ const ruleHandlersRef = React.useRef({
162
+ onAddConditionalRule,
163
+ onRemoveConditionalRule,
164
+ onUpdateConditionalRule,
165
+ onDisplayOptionsChange,
166
+ onPaginationChange,
167
+ })
168
+ ruleHandlersRef.current = {
169
+ onAddConditionalRule,
170
+ onRemoveConditionalRule,
171
+ onUpdateConditionalRule,
172
+ onDisplayOptionsChange,
173
+ onPaginationChange,
174
+ }
175
+
176
+ const viewTypeOptions = React.useMemo(
177
+ () =>
178
+ supportedViewTypes != null
179
+ ? dataListViewSelectionTilesForHub(supportedViewTypes)
180
+ : undefined,
181
+ [supportedViewTypes],
182
+ )
183
+
154
184
  return (
155
185
  <>
156
186
  {extraActions}
@@ -185,44 +215,45 @@ export function TablePropertiesDrawerButton({
185
215
  rowHeight={rowHeight}
186
216
  onRowHeightChange={setRowHeight}
187
217
  pagination={pagination}
188
- onPaginationChange={onPaginationChange ?? (() => {})}
218
+ onPaginationChange={v => ruleHandlersRef.current.onPaginationChange?.(v)}
189
219
  activeFilters={activeFilters}
190
- onAddFilter={fieldKey => addFilter(fieldKey, true)}
191
- onUpdateFilter={updateFilter}
192
- onRemoveFilter={removeFilter}
193
- getFilterConnector={getConnector}
194
- onToggleFilterConnector={toggleConnector}
220
+ onAddFilter={fieldKey => stateRef.current.addFilter(fieldKey, true)}
221
+ onUpdateFilter={(id, patch) => stateRef.current.updateFilter(id, patch)}
222
+ onRemoveFilter={id => stateRef.current.removeFilter(id)}
223
+ getFilterConnector={leftId => stateRef.current.getConnector(leftId)}
224
+ onToggleFilterConnector={leftId => stateRef.current.toggleConnector(leftId)}
195
225
  filterBarVisible={filterBarVisible}
196
- onFilterBarVisibleChange={setFilterBarVisible}
226
+ onFilterBarVisibleChange={v => stateRef.current.setFilterBarVisible(v)}
197
227
  drawerExpandedFilters={drawerExpandedFilters}
198
- onDrawerExpandedFiltersChange={setDrawerExpandedFilters}
228
+ onDrawerExpandedFiltersChange={stateRef.current.setDrawerExpandedFilters}
199
229
  totalRows={totalRows}
200
230
  filteredRows={rows.length}
201
231
  sortRules={sortRules}
202
- onSortRulesChange={setSortRules}
203
- onAddSortRule={addSortRule}
204
- onRemoveSortRule={removeSortRule}
205
- onToggleSortDir={toggleSortDir}
232
+ onSortRulesChange={rules => stateRef.current.setSortRules(rules)}
233
+ onAddSortRule={fieldKey => stateRef.current.addSortRule(fieldKey)}
234
+ onRemoveSortRule={id => stateRef.current.removeSortRule(id)}
235
+ onToggleSortDir={id => stateRef.current.toggleSortDir(id)}
206
236
  colOrder={colOrder}
207
- onColOrderChange={setColOrder}
237
+ onColOrderChange={order => stateRef.current.setColOrder(order)}
208
238
  hiddenCols={hiddenCols}
209
- onToggleColVisibility={toggleColVisibility}
210
- onMoveCol={moveCol}
239
+ onToggleColVisibility={key => stateRef.current.toggleColVisibility(key)}
240
+ onMoveCol={(key, dir) => stateRef.current.moveCol(key, dir)}
211
241
  groupBy={groupBy}
212
- onGroupByChange={setGroupBy}
242
+ onGroupByChange={key => stateRef.current.setGroupBy(key)}
213
243
  primarySortKey={sortKey}
214
244
  conditionalRules={conditionalRules}
215
- onAddConditionalRule={onAddConditionalRule}
216
- onRemoveConditionalRule={onRemoveConditionalRule}
217
- onUpdateConditionalRule={onUpdateConditionalRule}
245
+ onAddConditionalRule={rule => ruleHandlersRef.current.onAddConditionalRule(rule)}
246
+ onRemoveConditionalRule={id => ruleHandlersRef.current.onRemoveConditionalRule(id)}
247
+ onUpdateConditionalRule={(id, patch) => ruleHandlersRef.current.onUpdateConditionalRule(id, patch)}
218
248
  filterFields={filterFields}
219
249
  lifecycleTabLabel={lifecycleTabLabel}
220
250
  fieldDefinitions={fieldDefinitions}
221
251
  resolveColumnLabel={resolveColumnLabel}
222
252
  displayOptions={displayOptions}
223
- onDisplayOptionsChange={onDisplayOptionsChange}
253
+ onDisplayOptionsChange={patch => ruleHandlersRef.current.onDisplayOptionsChange(patch)}
224
254
  currentView={currentView}
225
255
  onViewChange={onViewChange}
256
+ viewTypeOptions={viewTypeOptions}
226
257
  boardGroupByColumnOptions={boardGroupByColumnOptions}
227
258
  renderFilterOptionValue={renderFilterOptionValue}
228
259
  />