@exxatdesignux/ui 0.2.18 → 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 (140) hide show
  1. package/CHANGELOG.md +15 -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 +21 -6
  7. package/consumer-extras/cursor-skills/exxat-focused-workflow-page/SKILL.md +57 -0
  8. package/consumer-extras/cursor-skills/exxat-primary-nav-secondary-panel/SKILL.md +4 -2
  9. package/consumer-extras/patterns/consumer-app-pattern.md +39 -0
  10. package/consumer-extras/patterns/consumer-upgrade-checklist.md +20 -0
  11. package/consumer-extras/patterns/data-views-pattern.md +40 -3
  12. package/consumer-extras/patterns/focused-workflow-page-pattern.md +84 -0
  13. package/consumer-extras/patterns/shell-surface-elevation-pattern.md +5 -3
  14. package/package.json +2 -1
  15. package/src/components/ui/button-group.tsx +81 -0
  16. package/src/components/ui/button.tsx +4 -4
  17. package/src/globals.css +7 -1858
  18. package/src/theme.css +10 -1126
  19. package/src/tokens/README.md +15 -0
  20. package/src/tokens/base.css +337 -0
  21. package/src/tokens/high-contrast.css +1195 -0
  22. package/src/tokens/layers.css +224 -0
  23. package/src/tokens/tailwind-bridge.css +118 -0
  24. package/src/tokens/themes.css +201 -0
  25. package/template/AGENTS.md +60 -22
  26. package/template/app/(app)/dashboard/loading.tsx +3 -15
  27. package/template/app/(app)/dashboard/page.tsx +2 -14
  28. package/template/app/(app)/data-list/layout.tsx +43 -0
  29. package/template/app/(app)/data-list/page.tsx +2 -2
  30. package/template/app/(app)/examples/focused-workflow/page.tsx +5 -0
  31. package/template/app/(app)/examples/page.tsx +1 -0
  32. package/template/app/(app)/loading.tsx +1 -18
  33. package/template/app/(app)/question-bank/find/page.tsx +2 -1
  34. package/template/app/(app)/question-bank/library/page.tsx +2 -1
  35. package/template/app/(app)/question-bank/list/page.tsx +2 -1
  36. package/template/app/(app)/question-bank/new/page.tsx +15 -23
  37. package/template/app/(app)/question-bank/page.tsx +2 -1
  38. package/template/app/(app)/settings/page.tsx +4 -5
  39. package/template/app/globals.css +7 -1964
  40. package/template/components/app-route-loading.tsx +14 -0
  41. package/template/components/app-sidebar.tsx +70 -55
  42. package/template/components/data-views/index.ts +37 -9
  43. package/template/components/data-views/list-page-calendar-view.tsx +593 -0
  44. package/template/components/data-views/list-page-connected-view-body.tsx +66 -0
  45. package/template/components/data-views/list-page-folder-columns-panel.tsx +345 -0
  46. package/template/components/data-views/list-page-split-hub-chrome.tsx +8 -0
  47. package/template/components/examples/focused-workflow-showcase.tsx +183 -0
  48. package/template/components/list-hub-board-view.tsx +68 -0
  49. package/template/components/list-hub-client.tsx +186 -0
  50. package/template/components/list-hub-list-view.tsx +36 -0
  51. package/template/components/list-hub-panel-activator.tsx +8 -0
  52. package/template/components/list-hub-secondary-nav.tsx +121 -0
  53. package/template/components/list-hub-table.tsx +336 -0
  54. package/template/components/new-question-composer.tsx +6 -24
  55. package/template/components/product-switcher.tsx +3 -2
  56. package/template/components/question-bank-client.tsx +4 -1
  57. package/template/components/question-bank-folder-columns-panel.tsx +104 -0
  58. package/template/components/question-bank-table.tsx +143 -485
  59. package/template/components/secondary-panel/nav-link-rows.tsx +83 -0
  60. package/template/components/secondary-panel.tsx +4 -44
  61. package/template/components/secondary-panels/list-hub-panel.tsx +39 -0
  62. package/template/components/secondary-panels/question-bank-panel.tsx +39 -0
  63. package/template/components/secondary-panels/registry.tsx +15 -0
  64. package/template/components/settings-appearance-card.tsx +3 -2
  65. package/template/components/settings-client.tsx +59 -15
  66. package/template/components/settings-form-row.tsx +9 -4
  67. package/template/components/table-properties/drawer-button.tsx +13 -0
  68. package/template/components/table-properties/drawer.tsx +65 -4
  69. package/template/components/templates/focused-workflow-layouts.tsx +448 -0
  70. package/template/components/templates/focused-workflow-page-template.tsx +69 -0
  71. package/template/components/templates/list-page.tsx +29 -5
  72. package/template/components/templates/nested-secondary-panel-shell.tsx +2 -1
  73. package/template/components/templates/page-loading-shell.tsx +262 -0
  74. package/template/components/ui/button-group.tsx +1 -0
  75. package/template/docs/consumer-app-pattern.md +39 -0
  76. package/template/docs/data-views-pattern.md +40 -3
  77. package/template/docs/drawer-vs-dialog-pattern.md +3 -1
  78. package/template/docs/focused-workflow-page-pattern.md +84 -0
  79. package/template/docs/shell-surface-elevation-pattern.md +5 -3
  80. package/template/lib/command-menu-search-data.ts +11 -27
  81. package/template/lib/data-list-display-options.ts +16 -2
  82. package/template/lib/data-list-view-registry.ts +104 -0
  83. package/template/lib/data-list-view-surface.ts +15 -1
  84. package/template/lib/data-list-view.ts +10 -1
  85. package/template/lib/data-view-dashboard-storage.ts +38 -35
  86. package/template/lib/hub-connected-view-renderers.ts +58 -0
  87. package/template/lib/list-hub-nav.ts +121 -0
  88. package/template/lib/list-hub-supported-views.ts +10 -0
  89. package/template/lib/list-page-table-properties.ts +3 -7
  90. package/template/lib/list-status-badges.ts +4 -97
  91. package/template/lib/mock/list-hub-directory.ts +27 -0
  92. package/template/lib/mock/list-hub-kpi.ts +27 -0
  93. package/template/lib/mock/navigation.tsx +1 -0
  94. package/template/lib/page-loading-variant.ts +40 -0
  95. package/template/lib/question-bank-supported-views.ts +13 -0
  96. package/template/lib/table-state-lifecycle.ts +2 -2
  97. package/template/app/(app)/data-list/[id]/page.tsx +0 -44
  98. package/template/app/(app)/data-list/new/page.tsx +0 -34
  99. package/template/components/compliance-board-view.tsx +0 -142
  100. package/template/components/compliance-client.tsx +0 -92
  101. package/template/components/compliance-list-view.tsx +0 -54
  102. package/template/components/compliance-page-header.tsx +0 -89
  103. package/template/components/compliance-table.tsx +0 -612
  104. package/template/components/data-view-dashboard-charts-compliance.tsx +0 -963
  105. package/template/components/data-view-dashboard-charts-team.tsx +0 -971
  106. package/template/components/data-view-dashboard-charts.tsx +0 -1503
  107. package/template/components/new-placement-back-btn.tsx +0 -28
  108. package/template/components/new-placement-form.tsx +0 -1068
  109. package/template/components/placement-board-card.tsx +0 -262
  110. package/template/components/placement-detail.tsx +0 -438
  111. package/template/components/placements-board-view.tsx +0 -404
  112. package/template/components/placements-client.tsx +0 -252
  113. package/template/components/placements-list-view.tsx +0 -171
  114. package/template/components/placements-page-header.tsx +0 -166
  115. package/template/components/placements-table-cells.test.tsx +0 -22
  116. package/template/components/placements-table-cells.tsx +0 -173
  117. package/template/components/placements-table-columns.tsx +0 -640
  118. package/template/components/placements-table.tsx +0 -1642
  119. package/template/components/rotations-empty-state.tsx +0 -50
  120. package/template/components/rotations-panel-activator.tsx +0 -8
  121. package/template/components/sites-all-client.tsx +0 -154
  122. package/template/components/sites-board-view.tsx +0 -67
  123. package/template/components/sites-list-view.tsx +0 -42
  124. package/template/components/sites-table.tsx +0 -382
  125. package/template/components/team-board-view.tsx +0 -122
  126. package/template/components/team-client.tsx +0 -100
  127. package/template/components/team-list-view.tsx +0 -59
  128. package/template/components/team-page-header.tsx +0 -92
  129. package/template/components/team-table.tsx +0 -693
  130. package/template/lib/data-view-dashboard-placements-layout.ts +0 -215
  131. package/template/lib/mock/compliance-kpi.ts +0 -61
  132. package/template/lib/mock/compliance.ts +0 -146
  133. package/template/lib/mock/placements-kpi.ts +0 -134
  134. package/template/lib/mock/placements.ts +0 -183
  135. package/template/lib/mock/sites-directory.ts +0 -16
  136. package/template/lib/mock/sites-kpi.ts +0 -25
  137. package/template/lib/mock/team-kpi.ts +0 -60
  138. package/template/lib/mock/team.ts +0 -118
  139. package/template/lib/placement-board-card-layout.ts +0 -79
  140. 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>
@@ -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,
@@ -169,6 +173,14 @@ export function TablePropertiesDrawerButton({
169
173
  onPaginationChange,
170
174
  }
171
175
 
176
+ const viewTypeOptions = React.useMemo(
177
+ () =>
178
+ supportedViewTypes != null
179
+ ? dataListViewSelectionTilesForHub(supportedViewTypes)
180
+ : undefined,
181
+ [supportedViewTypes],
182
+ )
183
+
172
184
  return (
173
185
  <>
174
186
  {extraActions}
@@ -241,6 +253,7 @@ export function TablePropertiesDrawerButton({
241
253
  onDisplayOptionsChange={patch => ruleHandlersRef.current.onDisplayOptionsChange(patch)}
242
254
  currentView={currentView}
243
255
  onViewChange={onViewChange}
256
+ viewTypeOptions={viewTypeOptions}
244
257
  boardGroupByColumnOptions={boardGroupByColumnOptions}
245
258
  renderFilterOptionValue={renderFilterOptionValue}
246
259
  />
@@ -26,7 +26,10 @@ import {
26
26
  SelectTrigger,
27
27
  SelectValue,
28
28
  } from "@/components/ui/select"
29
- import type { DataListDisplayOptions } from "@/lib/data-list-display-options"
29
+ import {
30
+ CALENDAR_MAIN_VIEW_TILES,
31
+ type DataListDisplayOptions,
32
+ } from "@/lib/data-list-display-options"
30
33
  import { Tip } from "@/components/ui/tip"
31
34
  import { ToggleSwitch } from "@/components/ui/toggle-switch"
32
35
  import { Button } from "@/components/ui/button"
@@ -95,6 +98,11 @@ export interface TablePropertiesDrawerProps {
95
98
  // View type
96
99
  currentView?: DataListViewType
97
100
  onViewChange?: (view: DataListViewType) => void
101
+ /**
102
+ * View-type tiles in Properties — defaults to all `DATA_LIST_VIEW_TILES`.
103
+ * Pass hub-filtered options (same set as `ListPageTemplate` `supportedViewTypes`).
104
+ */
105
+ viewTypeOptions?: readonly { value: DataListViewType; label: string; icon: string }[]
98
106
  /** Lifecycle context (e.g. tab filter) — shown in the drawer header */
99
107
  lifecycleTabLabel?: string
100
108
  /**
@@ -161,6 +169,7 @@ export function TablePropertiesDrawer({
161
169
  filterFields = FILTER_FIELDS,
162
170
  currentView,
163
171
  onViewChange,
172
+ viewTypeOptions = DATA_LIST_VIEW_TILES,
164
173
  lifecycleTabLabel,
165
174
  fieldDefinitions,
166
175
  resolveColumnLabel: resolveColumnLabelProp,
@@ -200,6 +209,7 @@ export function TablePropertiesDrawer({
200
209
 
201
210
  const viewSurface = currentView ?? "table"
202
211
  const isBoardView = viewSurface === "board"
212
+ const isCalendarView = viewSurface === "calendar"
203
213
  const boardGroupByLabel =
204
214
  boardGroupByColumnOptions?.find(o => o.key === displayOptions.boardGroupByColumnKey)?.label
205
215
  const viewDisplayLabel = dataListViewLabel(viewSurface)
@@ -222,10 +232,16 @@ export function TablePropertiesDrawer({
222
232
  if (viewSurface === "dashboard") {
223
233
  return "Charts · KPI metrics"
224
234
  }
235
+ if (viewSurface === "calendar") {
236
+ return [
237
+ displayOptions.showCalendarSummaryPanel ? "Summary panel" : "No summary",
238
+ displayOptions.calendarMainView === "week" ? "Week layout" : "Month layout",
239
+ ].join(" · ")
240
+ }
225
241
  return [showGridlines ? "Gridlines" : null, pagination ? "Paginated" : null].filter(Boolean).join(" · ") || "Default"
226
242
  })()
227
243
  const viewDisplayIcon =
228
- DATA_LIST_VIEW_TILES.find(t => t.value === viewSurface)?.icon ?? "fa-table"
244
+ viewTypeOptions.find(t => t.value === viewSurface)?.icon ?? "fa-table"
229
245
 
230
246
  // ── Sort drag-and-drop ────────────────────────────────────────────────────
231
247
  const sortDrag = useDraggableList(sortRules, r => r.id, onSortRulesChange)
@@ -287,7 +303,7 @@ export function TablePropertiesDrawer({
287
303
  <div className="px-4 pb-3">
288
304
  <SelectionTileGrid<DataListViewType>
289
305
  sectionLabel="View type"
290
- options={DATA_LIST_VIEW_TILES}
306
+ options={viewTypeOptions}
291
307
  columns={4}
292
308
  value={currentView}
293
309
  onValueChange={onViewChange}
@@ -438,6 +454,12 @@ export function TablePropertiesDrawer({
438
454
  </p>
439
455
  ) : null}
440
456
 
457
+ {isCalendarView ? (
458
+ <p className="text-xs text-muted-foreground leading-relaxed">
459
+ {dataListViewLabel("calendar")} uses the same filtered rows as table and board. Scroll the main calendar vertically to move between months; use the summary panel for a mini month picker and event list.
460
+ </p>
461
+ ) : null}
462
+
441
463
  {isBoardView && boardGroupByColumnOptions && boardGroupByColumnOptions.length > 1 ? (
442
464
  <div className="flex items-center justify-between gap-3 py-2">
443
465
  <div className="flex items-center gap-2.5 min-w-0 flex-1">
@@ -513,7 +535,8 @@ export function TablePropertiesDrawer({
513
535
  <div
514
536
  className={cn(
515
537
  "space-y-3",
516
- (viewSurface === "board" || viewSurface === "table") && "border-t border-border pt-4",
538
+ (viewSurface === "board" || viewSurface === "table" || isCalendarView) &&
539
+ "border-t border-border pt-4",
517
540
  )}
518
541
  >
519
542
  <p className="text-xs font-semibold text-muted-foreground uppercase tracking-wider">Display options</p>
@@ -621,6 +644,44 @@ export function TablePropertiesDrawer({
621
644
  </>
622
645
  )}
623
646
 
647
+ {isCalendarView && (
648
+ <>
649
+ <div className="flex items-center justify-between gap-2 py-2">
650
+ <div className="flex items-center gap-2.5 min-w-0 flex-1">
651
+ <span className="inline-flex items-center justify-center size-9 rounded-lg bg-secondary border border-border shrink-0">
652
+ <i className="fa-light fa-sidebar text-[15px] text-secondary-foreground" aria-hidden="true" />
653
+ </span>
654
+ <div className="min-w-0">
655
+ <p className="text-sm font-medium text-foreground leading-tight">Summary panel</p>
656
+ <p className="text-xs text-muted-foreground mt-0.5">
657
+ Mini month, event list, and layout on the left.
658
+ </p>
659
+ </div>
660
+ </div>
661
+ <ToggleSwitch
662
+ id="toggle-calendar-summary"
663
+ checked={displayOptions.showCalendarSummaryPanel}
664
+ onChange={v => onDisplayOptionsChange({ showCalendarSummaryPanel: v })}
665
+ />
666
+ </div>
667
+ <div className="pt-2">
668
+ <SelectionTileGrid
669
+ sectionLabel="Main calendar layout"
670
+ options={CALENDAR_MAIN_VIEW_TILES.map(t => ({
671
+ value: t.value,
672
+ label: t.label,
673
+ icon: t.icon,
674
+ }))}
675
+ columns={2}
676
+ value={displayOptions.calendarMainView}
677
+ onValueChange={v => onDisplayOptionsChange({ calendarMainView: v })}
678
+ interaction="button"
679
+ idPrefix="props-calendar-main-view"
680
+ />
681
+ </div>
682
+ </>
683
+ )}
684
+
624
685
  {(viewSurface === "table" || viewSurface === "list") && (
625
686
  <div className="flex items-center justify-between gap-2 py-2">
626
687
  <div className="flex items-center gap-2.5 min-w-0 flex-1">