@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.
- package/CHANGELOG.md +30 -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 +22 -7
- package/consumer-extras/cursor-skills/exxat-focused-workflow-page/SKILL.md +57 -0
- package/consumer-extras/cursor-skills/exxat-kpi-flat-band/SKILL.md +38 -0
- package/consumer-extras/cursor-skills/exxat-primary-nav-secondary-panel/SKILL.md +10 -3
- 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 +42 -3
- package/consumer-extras/patterns/focused-workflow-page-pattern.md +84 -0
- package/consumer-extras/patterns/kpi-flat-band-pattern.md +57 -0
- package/consumer-extras/patterns/shell-surface-elevation-pattern.md +54 -0
- package/package.json +2 -1
- package/src/components/ui/button-group.tsx +81 -0
- package/src/components/ui/button.tsx +4 -4
- package/src/components/ui/sidebar.tsx +2 -2
- package/src/globals.css +7 -1807
- 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/.cursor/rules/exxat-kbd-shortcuts.mdc +1 -1
- package/template/AGENTS.md +66 -21
- 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)/error.tsx +22 -6
- package/template/app/(app)/examples/focused-workflow/page.tsx +5 -0
- package/template/app/(app)/examples/page.tsx +1 -0
- package/template/app/(app)/layout.tsx +13 -6
- 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/global-error.tsx +63 -0
- package/template/app/globals.css +7 -1934
- package/template/app/layout.tsx +2 -0
- package/template/components/app-route-loading.tsx +14 -0
- package/template/components/app-sidebar.tsx +71 -55
- package/template/components/data-table/index.tsx +31 -67
- package/template/components/data-table/use-table-state.ts +33 -6
- 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/dev-chunk-load-recovery.tsx +41 -0
- package/template/components/examples/focused-workflow-showcase.tsx +183 -0
- package/template/components/exxat-product-logo.tsx +2 -6
- package/template/components/key-metrics.tsx +54 -22
- 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 +5 -5
- package/template/components/product-wordmark.tsx +4 -7
- 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-hub-client.tsx +2 -5
- package/template/components/question-bank-table.tsx +155 -509
- 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/sidebar-shell.tsx +2 -1
- package/template/components/table-properties/drawer-button.tsx +51 -20
- package/template/components/table-properties/drawer.tsx +81 -17
- 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 +40 -13
- package/template/components/templates/nested-secondary-panel-shell.tsx +3 -2
- package/template/components/templates/page-loading-shell.tsx +262 -0
- package/template/components/ui/button-group.tsx +1 -0
- package/template/contexts/product-context.tsx +21 -2
- package/template/docs/consumer-app-pattern.md +39 -0
- package/template/docs/data-views-pattern.md +42 -3
- package/template/docs/drawer-vs-dialog-pattern.md +3 -1
- package/template/docs/focused-workflow-page-pattern.md +84 -0
- package/template/docs/kpi-flat-band-pattern.md +57 -0
- package/template/docs/kpi-strip-max-four-pattern.md +1 -0
- package/template/docs/shell-surface-elevation-pattern.md +54 -0
- package/template/lib/chunk-load-error.ts +13 -0
- package/template/lib/command-menu-search-data.ts +11 -27
- package/template/lib/conditional-rule-match.ts +87 -22
- 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 +16 -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/sidebar-state-cookie.ts +9 -0
- package/template/lib/table-state-lifecycle.ts +60 -13
- 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 -632
- 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 -1675
- 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 -402
- 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 -714
- 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>
|
|
@@ -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={
|
|
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
|
/>
|