@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.
- package/CHANGELOG.md +15 -0
- package/consumer-extras/AGENTS.md +76 -0
- package/consumer-extras/README.md +5 -1
- package/consumer-extras/cursor-skills/exxat-centralized-list-dataset/SKILL.md +14 -3
- package/consumer-extras/cursor-skills/exxat-consumer-app/SKILL.md +37 -0
- package/consumer-extras/cursor-skills/exxat-ds-skill/SKILL.md +21 -6
- package/consumer-extras/cursor-skills/exxat-focused-workflow-page/SKILL.md +57 -0
- package/consumer-extras/cursor-skills/exxat-primary-nav-secondary-panel/SKILL.md +4 -2
- package/consumer-extras/patterns/consumer-app-pattern.md +39 -0
- package/consumer-extras/patterns/consumer-upgrade-checklist.md +20 -0
- package/consumer-extras/patterns/data-views-pattern.md +40 -3
- package/consumer-extras/patterns/focused-workflow-page-pattern.md +84 -0
- package/consumer-extras/patterns/shell-surface-elevation-pattern.md +5 -3
- package/package.json +2 -1
- package/src/components/ui/button-group.tsx +81 -0
- package/src/components/ui/button.tsx +4 -4
- package/src/globals.css +7 -1858
- package/src/theme.css +10 -1126
- package/src/tokens/README.md +15 -0
- package/src/tokens/base.css +337 -0
- package/src/tokens/high-contrast.css +1195 -0
- package/src/tokens/layers.css +224 -0
- package/src/tokens/tailwind-bridge.css +118 -0
- package/src/tokens/themes.css +201 -0
- package/template/AGENTS.md +60 -22
- package/template/app/(app)/dashboard/loading.tsx +3 -15
- package/template/app/(app)/dashboard/page.tsx +2 -14
- package/template/app/(app)/data-list/layout.tsx +43 -0
- package/template/app/(app)/data-list/page.tsx +2 -2
- package/template/app/(app)/examples/focused-workflow/page.tsx +5 -0
- package/template/app/(app)/examples/page.tsx +1 -0
- package/template/app/(app)/loading.tsx +1 -18
- package/template/app/(app)/question-bank/find/page.tsx +2 -1
- package/template/app/(app)/question-bank/library/page.tsx +2 -1
- package/template/app/(app)/question-bank/list/page.tsx +2 -1
- package/template/app/(app)/question-bank/new/page.tsx +15 -23
- package/template/app/(app)/question-bank/page.tsx +2 -1
- package/template/app/(app)/settings/page.tsx +4 -5
- package/template/app/globals.css +7 -1964
- package/template/components/app-route-loading.tsx +14 -0
- package/template/components/app-sidebar.tsx +70 -55
- package/template/components/data-views/index.ts +37 -9
- package/template/components/data-views/list-page-calendar-view.tsx +593 -0
- package/template/components/data-views/list-page-connected-view-body.tsx +66 -0
- package/template/components/data-views/list-page-folder-columns-panel.tsx +345 -0
- package/template/components/data-views/list-page-split-hub-chrome.tsx +8 -0
- package/template/components/examples/focused-workflow-showcase.tsx +183 -0
- package/template/components/list-hub-board-view.tsx +68 -0
- package/template/components/list-hub-client.tsx +186 -0
- package/template/components/list-hub-list-view.tsx +36 -0
- package/template/components/list-hub-panel-activator.tsx +8 -0
- package/template/components/list-hub-secondary-nav.tsx +121 -0
- package/template/components/list-hub-table.tsx +336 -0
- package/template/components/new-question-composer.tsx +6 -24
- package/template/components/product-switcher.tsx +3 -2
- package/template/components/question-bank-client.tsx +4 -1
- package/template/components/question-bank-folder-columns-panel.tsx +104 -0
- package/template/components/question-bank-table.tsx +143 -485
- package/template/components/secondary-panel/nav-link-rows.tsx +83 -0
- package/template/components/secondary-panel.tsx +4 -44
- package/template/components/secondary-panels/list-hub-panel.tsx +39 -0
- package/template/components/secondary-panels/question-bank-panel.tsx +39 -0
- package/template/components/secondary-panels/registry.tsx +15 -0
- package/template/components/settings-appearance-card.tsx +3 -2
- package/template/components/settings-client.tsx +59 -15
- package/template/components/settings-form-row.tsx +9 -4
- package/template/components/table-properties/drawer-button.tsx +13 -0
- package/template/components/table-properties/drawer.tsx +65 -4
- package/template/components/templates/focused-workflow-layouts.tsx +448 -0
- package/template/components/templates/focused-workflow-page-template.tsx +69 -0
- package/template/components/templates/list-page.tsx +29 -5
- package/template/components/templates/nested-secondary-panel-shell.tsx +2 -1
- package/template/components/templates/page-loading-shell.tsx +262 -0
- package/template/components/ui/button-group.tsx +1 -0
- package/template/docs/consumer-app-pattern.md +39 -0
- package/template/docs/data-views-pattern.md +40 -3
- package/template/docs/drawer-vs-dialog-pattern.md +3 -1
- package/template/docs/focused-workflow-page-pattern.md +84 -0
- package/template/docs/shell-surface-elevation-pattern.md +5 -3
- package/template/lib/command-menu-search-data.ts +11 -27
- package/template/lib/data-list-display-options.ts +16 -2
- package/template/lib/data-list-view-registry.ts +104 -0
- package/template/lib/data-list-view-surface.ts +15 -1
- package/template/lib/data-list-view.ts +10 -1
- package/template/lib/data-view-dashboard-storage.ts +38 -35
- package/template/lib/hub-connected-view-renderers.ts +58 -0
- package/template/lib/list-hub-nav.ts +121 -0
- package/template/lib/list-hub-supported-views.ts +10 -0
- package/template/lib/list-page-table-properties.ts +3 -7
- package/template/lib/list-status-badges.ts +4 -97
- package/template/lib/mock/list-hub-directory.ts +27 -0
- package/template/lib/mock/list-hub-kpi.ts +27 -0
- package/template/lib/mock/navigation.tsx +1 -0
- package/template/lib/page-loading-variant.ts +40 -0
- package/template/lib/question-bank-supported-views.ts +13 -0
- package/template/lib/table-state-lifecycle.ts +2 -2
- package/template/app/(app)/data-list/[id]/page.tsx +0 -44
- package/template/app/(app)/data-list/new/page.tsx +0 -34
- package/template/components/compliance-board-view.tsx +0 -142
- package/template/components/compliance-client.tsx +0 -92
- package/template/components/compliance-list-view.tsx +0 -54
- package/template/components/compliance-page-header.tsx +0 -89
- package/template/components/compliance-table.tsx +0 -612
- package/template/components/data-view-dashboard-charts-compliance.tsx +0 -963
- package/template/components/data-view-dashboard-charts-team.tsx +0 -971
- package/template/components/data-view-dashboard-charts.tsx +0 -1503
- package/template/components/new-placement-back-btn.tsx +0 -28
- package/template/components/new-placement-form.tsx +0 -1068
- package/template/components/placement-board-card.tsx +0 -262
- package/template/components/placement-detail.tsx +0 -438
- package/template/components/placements-board-view.tsx +0 -404
- package/template/components/placements-client.tsx +0 -252
- package/template/components/placements-list-view.tsx +0 -171
- package/template/components/placements-page-header.tsx +0 -166
- package/template/components/placements-table-cells.test.tsx +0 -22
- package/template/components/placements-table-cells.tsx +0 -173
- package/template/components/placements-table-columns.tsx +0 -640
- package/template/components/placements-table.tsx +0 -1642
- package/template/components/rotations-empty-state.tsx +0 -50
- package/template/components/rotations-panel-activator.tsx +0 -8
- package/template/components/sites-all-client.tsx +0 -154
- package/template/components/sites-board-view.tsx +0 -67
- package/template/components/sites-list-view.tsx +0 -42
- package/template/components/sites-table.tsx +0 -382
- package/template/components/team-board-view.tsx +0 -122
- package/template/components/team-client.tsx +0 -100
- package/template/components/team-list-view.tsx +0 -59
- package/template/components/team-page-header.tsx +0 -92
- package/template/components/team-table.tsx +0 -693
- package/template/lib/data-view-dashboard-placements-layout.ts +0 -215
- package/template/lib/mock/compliance-kpi.ts +0 -61
- package/template/lib/mock/compliance.ts +0 -146
- package/template/lib/mock/placements-kpi.ts +0 -134
- package/template/lib/mock/placements.ts +0 -183
- package/template/lib/mock/sites-directory.ts +0 -16
- package/template/lib/mock/sites-kpi.ts +0 -25
- package/template/lib/mock/team-kpi.ts +0 -60
- package/template/lib/mock/team.ts +0 -118
- package/template/lib/placement-board-card-layout.ts +0 -79
- 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}.
|
|
8
|
-
* `
|
|
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 ?
|
|
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
|
-
|
|
521
|
+
<>
|
|
522
522
|
<header className="mb-8 space-y-1">
|
|
523
523
|
<h2 className="text-lg font-semibold text-foreground">Appearance & 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
|
-
|
|
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
|
-
<
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
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
|
-
</
|
|
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
|
-
*
|
|
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
|
|
27
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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={
|
|
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") &&
|
|
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">
|