@exxatdesignux/ui 0.0.6 → 0.0.7
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/bin/init.mjs +29 -0
- package/package.json +7 -2
- package/template/.nvmrc +1 -0
- package/template/.prettierignore +7 -0
- package/template/.prettierrc +11 -0
- package/template/AGENTS.md +485 -0
- package/template/Logo/Exxat_Prism.svg +39 -0
- package/template/Logo/Exxat_one.svg +36 -0
- package/template/README.md +58 -0
- package/template/app/(app)/compliance/page.tsx +10 -0
- package/template/app/(app)/dashboard/loading.tsx +18 -0
- package/template/app/(app)/dashboard/page.tsx +36 -0
- package/template/app/(app)/data-list/[id]/page.tsx +28 -0
- package/template/app/(app)/data-list/new/page.tsx +31 -0
- package/template/app/(app)/data-list/page.tsx +10 -0
- package/template/app/(app)/error.tsx +43 -0
- package/template/app/(app)/help/page.tsx +34 -0
- package/template/app/(app)/layout.tsx +54 -0
- package/template/app/(app)/loading.tsx +18 -0
- package/template/app/(app)/question-bank/page.tsx +10 -0
- package/template/app/(app)/rotations/page.tsx +15 -0
- package/template/app/(app)/settings/page.tsx +17 -0
- package/template/app/(app)/sites/all/page.tsx +13 -0
- package/template/app/(app)/team/page.tsx +10 -0
- package/template/app/favicon.ico +0 -0
- package/template/app/globals.css +1811 -0
- package/template/app/layout.tsx +95 -0
- package/template/app/page.tsx +9 -0
- package/template/components/.gitkeep +0 -0
- package/template/components/app-sidebar-dynamic.tsx +15 -0
- package/template/components/app-sidebar.tsx +901 -0
- package/template/components/ask-leo-composer.tsx +216 -0
- package/template/components/ask-leo-sidebar.tsx +509 -0
- package/template/components/chart-area-interactive.tsx +293 -0
- package/template/components/charts-overview.tsx +2321 -0
- package/template/components/command-menu-01.tsx +133 -0
- package/template/components/command-menu-02.tsx +386 -0
- package/template/components/command-menu.tsx +182 -0
- package/template/components/compliance-board-view.tsx +134 -0
- package/template/components/compliance-client.tsx +92 -0
- package/template/components/compliance-list-view.tsx +59 -0
- package/template/components/compliance-page-header.tsx +89 -0
- package/template/components/compliance-table.tsx +525 -0
- package/template/components/dashboard-onboarding-gallery.tsx +13 -0
- package/template/components/dashboard-onboarding.tsx +21 -0
- package/template/components/dashboard-promo-banner.tsx +67 -0
- package/template/components/dashboard-quota-progress-card.tsx +369 -0
- package/template/components/dashboard-report-charts.tsx +69 -0
- package/template/components/dashboard-section-heading.tsx +68 -0
- package/template/components/dashboard-tabs.tsx +598 -0
- package/template/components/data-list-client.tsx +239 -0
- package/template/components/data-list-table-cells.test.tsx +22 -0
- package/template/components/data-list-table-cells.tsx +173 -0
- package/template/components/data-list-table.tsx +879 -0
- package/template/components/data-table/filter-date-calendar.tsx +38 -0
- package/template/components/data-table/filter-text-value-input.tsx +77 -0
- package/template/components/data-table/index.tsx +1612 -0
- package/template/components/data-table/pagination.tsx +256 -0
- package/template/components/data-table/types.ts +91 -0
- package/template/components/data-table/use-table-state.ts +566 -0
- package/template/components/data-view-dashboard-charts-compliance.tsx +960 -0
- package/template/components/data-view-dashboard-charts-team.tsx +968 -0
- package/template/components/data-view-dashboard-charts.tsx +1668 -0
- package/template/components/data-views/board-card-primitives.tsx +93 -0
- package/template/components/data-views/index.ts +41 -0
- package/template/components/data-views/list-page-board-card.tsx +192 -0
- package/template/components/data-views/list-page-board-template.tsx +122 -0
- package/template/components/data-views/placement-board-card.tsx +262 -0
- package/template/components/export-drawer.tsx +375 -0
- package/template/components/exxat-product-logo.tsx +453 -0
- package/template/components/form-layout-01.tsx +131 -0
- package/template/components/getting-started.tsx +625 -0
- package/template/components/key-metrics.tsx +920 -0
- package/template/components/leo-insight-indicator.tsx +364 -0
- package/template/components/leo-typing-dots.tsx +121 -0
- package/template/components/list-hub-status-badge.tsx +51 -0
- package/template/components/list-page-dashboard-charts.tsx +18 -0
- package/template/components/nav-documents.tsx +89 -0
- package/template/components/nav-main.tsx +58 -0
- package/template/components/nav-secondary.tsx +64 -0
- package/template/components/nav-user.tsx +190 -0
- package/template/components/new-placement-back-btn.tsx +28 -0
- package/template/components/new-placement-form.tsx +1066 -0
- package/template/components/onboarding/index.ts +4 -0
- package/template/components/onboarding/onboarding-01.tsx +7 -0
- package/template/components/onboarding/onboarding-02.tsx +7 -0
- package/template/components/onboarding/onboarding-03.tsx +7 -0
- package/template/components/onboarding/onboarding-04.tsx +7 -0
- package/template/components/page-header.tsx +57 -0
- package/template/components/placement-detail.tsx +438 -0
- package/template/components/placements-board-view.tsx +404 -0
- package/template/components/placements-list-view.tsx +285 -0
- package/template/components/placements-page-header.tsx +160 -0
- package/template/components/placements-table-columns.tsx +639 -0
- package/template/components/product-switcher.tsx +116 -0
- package/template/components/question-bank-board-view.tsx +205 -0
- package/template/components/question-bank-client.tsx +77 -0
- package/template/components/question-bank-list-view.tsx +59 -0
- package/template/components/question-bank-page-header.tsx +89 -0
- package/template/components/question-bank-table.tsx +586 -0
- package/template/components/rotations-empty-state.tsx +47 -0
- package/template/components/rotations-panel-activator.tsx +8 -0
- package/template/components/secondary-nav.tsx +394 -0
- package/template/components/secondary-panel.tsx +239 -0
- package/template/components/section-cards.tsx +106 -0
- package/template/components/settings-appearance-card.tsx +424 -0
- package/template/components/settings-client.tsx +537 -0
- package/template/components/settings-form-row.tsx +42 -0
- package/template/components/sidebar-auto-collapse.tsx +23 -0
- package/template/components/sidebar-auto-open.tsx +18 -0
- package/template/components/sidebar-shell.tsx +37 -0
- package/template/components/site-header.tsx +93 -0
- package/template/components/sites-all-client.tsx +154 -0
- package/template/components/sites-board-view.tsx +67 -0
- package/template/components/sites-list-view.tsx +47 -0
- package/template/components/sites-table.tsx +312 -0
- package/template/components/system-banner-slot.tsx +66 -0
- package/template/components/table-properties/column-row.tsx +90 -0
- package/template/components/table-properties/draggable-list.ts +49 -0
- package/template/components/table-properties/drawer-button.tsx +231 -0
- package/template/components/table-properties/drawer.tsx +1102 -0
- package/template/components/table-properties/filter-card.tsx +251 -0
- package/template/components/table-properties/index.ts +22 -0
- package/template/components/table-properties/sort-card.tsx +59 -0
- package/template/components/table-properties/types.ts +124 -0
- package/template/components/task-list-panel.tsx +98 -0
- package/template/components/task-priority-badge.tsx +28 -0
- package/template/components/team-board-view.tsx +114 -0
- package/template/components/team-client.tsx +93 -0
- package/template/components/team-list-view.tsx +62 -0
- package/template/components/team-page-header.tsx +92 -0
- package/template/components/team-table.tsx +525 -0
- package/template/components/templates/list-page.tsx +576 -0
- package/template/components/templates/primary-page-template.tsx +56 -0
- package/template/components/theme-color-sync.tsx +32 -0
- package/template/components/theme-provider.tsx +71 -0
- package/template/components/tinted-icon-disc.tsx +53 -0
- package/template/components/ui/ai-thinking-surface.tsx +121 -0
- package/template/components/ui/avatar.tsx +1 -0
- package/template/components/ui/badge.tsx +1 -0
- package/template/components/ui/banner.tsx +1 -0
- package/template/components/ui/breadcrumb.tsx +1 -0
- package/template/components/ui/button.tsx +1 -0
- package/template/components/ui/calendar.tsx +1 -0
- package/template/components/ui/card.tsx +1 -0
- package/template/components/ui/chart.tsx +1 -0
- package/template/components/ui/checkbox.tsx +1 -0
- package/template/components/ui/coach-mark.tsx +1 -0
- package/template/components/ui/collapsible.tsx +1 -0
- package/template/components/ui/command.tsx +1 -0
- package/template/components/ui/date-picker-field.tsx +1 -0
- package/template/components/ui/dialog.tsx +1 -0
- package/template/components/ui/dot-pattern.tsx +159 -0
- package/template/components/ui/drag-handle-grip.tsx +1 -0
- package/template/components/ui/drawer.tsx +1 -0
- package/template/components/ui/dropdown-menu.tsx +1 -0
- package/template/components/ui/field.tsx +1 -0
- package/template/components/ui/form.tsx +1 -0
- package/template/components/ui/input-group.tsx +1 -0
- package/template/components/ui/input-mask.tsx +1 -0
- package/template/components/ui/input.tsx +1 -0
- package/template/components/ui/kbd.tsx +1 -0
- package/template/components/ui/label.tsx +1 -0
- package/template/components/ui/leo-icon.tsx +726 -0
- package/template/components/ui/payment-card-fields.tsx +1 -0
- package/template/components/ui/popover.tsx +1 -0
- package/template/components/ui/radio-group.tsx +1 -0
- package/template/components/ui/select.tsx +1 -0
- package/template/components/ui/selection-tile-grid.tsx +1 -0
- package/template/components/ui/separator.tsx +1 -0
- package/template/components/ui/sheet.tsx +1 -0
- package/template/components/ui/sidebar.tsx +1 -0
- package/template/components/ui/skeleton.tsx +1 -0
- package/template/components/ui/sonner.tsx +1 -0
- package/template/components/ui/status-badge.tsx +1 -0
- package/template/components/ui/table.tsx +1 -0
- package/template/components/ui/tabs.tsx +1 -0
- package/template/components/ui/textarea.tsx +1 -0
- package/template/components/ui/tip.tsx +1 -0
- package/template/components/ui/toggle-group.tsx +1 -0
- package/template/components/ui/toggle-switch.tsx +1 -0
- package/template/components/ui/toggle.tsx +1 -0
- package/template/components/ui/tooltip.tsx +1 -0
- package/template/components/ui/view-segmented-control.tsx +1 -0
- package/template/components.json +27 -0
- package/template/contexts/chart-variant-context.tsx +35 -0
- package/template/contexts/command-menu-context.tsx +28 -0
- package/template/contexts/dashboard-view-context.tsx +35 -0
- package/template/contexts/product-context.tsx +38 -0
- package/template/contexts/system-banner-context.tsx +127 -0
- package/template/docs/command-menu-pattern.md +45 -0
- package/template/docs/data-views-pattern.md +160 -0
- package/template/ecosystem.config.cjs +20 -0
- package/template/eslint.config.mjs +18 -0
- package/template/fontawesome-subset.manifest.json +190 -0
- package/template/hooks/.gitkeep +0 -0
- package/template/hooks/use-app-theme.ts +1 -0
- package/template/hooks/use-coach-mark.ts +1 -0
- package/template/hooks/use-mobile.ts +1 -0
- package/template/hooks/use-mod-key-label.ts +1 -0
- package/template/lib/.gitkeep +0 -0
- package/template/lib/ask-leo-route-context.ts +133 -0
- package/template/lib/chart-keyboard-selection.test.ts +20 -0
- package/template/lib/chart-keyboard-selection.ts +17 -0
- package/template/lib/chart-line-dash.ts +16 -0
- package/template/lib/coach-mark-registry.ts +68 -0
- package/template/lib/command-menu-config.ts +127 -0
- package/template/lib/command-menu-search-data.ts +44 -0
- package/template/lib/conditional-rule-match.ts +32 -0
- package/template/lib/dashboard-customize-coach-mark.ts +18 -0
- package/template/lib/dashboard-layout-merge.ts +63 -0
- package/template/lib/data-list-display-options.ts +35 -0
- package/template/lib/data-list-persistence.ts +280 -0
- package/template/lib/data-list-view-surface.ts +58 -0
- package/template/lib/data-list-view.ts +29 -0
- package/template/lib/data-view-dashboard-storage.ts +101 -0
- package/template/lib/date-filter.ts +8 -0
- package/template/lib/dev-log.test.ts +28 -0
- package/template/lib/dev-log.ts +8 -0
- package/template/lib/editable-target.ts +10 -0
- package/template/lib/floating-sheet-panel.ts +72 -0
- package/template/lib/initials-from-name.ts +7 -0
- package/template/lib/list-page-table-properties.ts +52 -0
- package/template/lib/list-status-badges.ts +168 -0
- package/template/lib/logo-dev.ts +12 -0
- package/template/lib/mock/compliance-kpi.ts +61 -0
- package/template/lib/mock/compliance.ts +146 -0
- package/template/lib/mock/dashboard.ts +105 -0
- package/template/lib/mock/navigation.tsx +231 -0
- package/template/lib/mock/placements-kpi.ts +134 -0
- package/template/lib/mock/placements.ts +183 -0
- package/template/lib/mock/question-bank-kpi.ts +61 -0
- package/template/lib/mock/question-bank.ts +142 -0
- package/template/lib/mock/sites-directory.ts +16 -0
- package/template/lib/mock/sites-kpi.ts +25 -0
- package/template/lib/mock/team-kpi.ts +60 -0
- package/template/lib/mock/team.ts +118 -0
- package/template/lib/motion-ui.ts +17 -0
- package/template/lib/placement-board-card-layout.ts +79 -0
- package/template/lib/placement-lifecycle.ts +5 -0
- package/template/lib/row-height.ts +10 -0
- package/template/lib/stock-portrait.ts +11 -0
- package/template/lib/utils.test.ts +13 -0
- package/template/lib/utils.ts +1 -0
- package/template/next.config.mjs +15 -0
- package/template/package.json +83 -0
- package/template/postcss.config.mjs +8 -0
- package/template/public/.gitkeep +0 -0
- package/template/public/Illustration/Rotation.svg +74 -0
- package/template/public/avatars/user.svg +11 -0
- package/template/public/favicon/favicon.ico +0 -0
- package/template/public/favicon.ico +0 -0
- package/template/public/logos/exxat-one.svg +36 -0
- package/template/public/logos/exxat-prism.svg +39 -0
- package/template/public/mock-schools/emory.svg +4 -0
- package/template/public/mock-schools/rush.svg +4 -0
- package/template/scripts/fontawesome-subset-audit.mjs +190 -0
- package/template/scripts/pm2-startup-macos.sh +13 -0
- package/template/skills-lock.json +10 -0
- package/template/stores/app-store.ts +33 -0
- package/template/tests/setup.ts +1 -0
- package/template/tsconfig.json +35 -0
- package/template/types/react-payment-inputs.d.ts +19 -0
- package/template/vitest.config.ts +18 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "../../../../packages/ui/src/components/ui/payment-card-fields"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "../../../../packages/ui/src/components/ui/popover"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "../../../../packages/ui/src/components/ui/radio-group"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "../../../../packages/ui/src/components/ui/select"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "../../../../packages/ui/src/components/ui/selection-tile-grid"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "../../../../packages/ui/src/components/ui/separator"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "../../../../packages/ui/src/components/ui/sheet"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "../../../../packages/ui/src/components/ui/sidebar"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "../../../../packages/ui/src/components/ui/skeleton"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "../../../../packages/ui/src/components/ui/sonner"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "../../../../packages/ui/src/components/ui/status-badge"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "../../../../packages/ui/src/components/ui/table"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "../../../../packages/ui/src/components/ui/tabs"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "../../../../packages/ui/src/components/ui/textarea"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "../../../../packages/ui/src/components/ui/tip"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "../../../../packages/ui/src/components/ui/toggle-group"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "../../../../packages/ui/src/components/ui/toggle-switch"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "../../../../packages/ui/src/components/ui/toggle"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "../../../../packages/ui/src/components/ui/tooltip"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "../../../../packages/ui/src/components/ui/view-segmented-control"
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema.json",
|
|
3
|
+
"style": "radix-nova",
|
|
4
|
+
"rsc": true,
|
|
5
|
+
"tsx": true,
|
|
6
|
+
"tailwind": {
|
|
7
|
+
"config": "",
|
|
8
|
+
"css": "app/globals.css",
|
|
9
|
+
"baseColor": "mist",
|
|
10
|
+
"cssVariables": true,
|
|
11
|
+
"prefix": ""
|
|
12
|
+
},
|
|
13
|
+
"iconLibrary": "lucide",
|
|
14
|
+
"rtl": true,
|
|
15
|
+
"menuColor": "default",
|
|
16
|
+
"menuAccent": "subtle",
|
|
17
|
+
"aliases": {
|
|
18
|
+
"components": "@/components",
|
|
19
|
+
"utils": "@/lib/utils",
|
|
20
|
+
"ui": "@/components/ui",
|
|
21
|
+
"lib": "@/lib",
|
|
22
|
+
"hooks": "@/hooks"
|
|
23
|
+
},
|
|
24
|
+
"registries": {
|
|
25
|
+
"@blocks-so": "https://blocks.so/r/{name}.json"
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ChartVariantContext
|
|
5
|
+
* Shared state for the active chart card style on the dashboard.
|
|
6
|
+
* Consumed by NavUser (sidebar profile menu) and DashboardTabs (page content).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import * as React from "react"
|
|
10
|
+
|
|
11
|
+
export type ChartVariant = "normal" | "tabs" | "selector" | "metrics-tabs" | "kpi-chart"
|
|
12
|
+
|
|
13
|
+
interface ChartVariantContextValue {
|
|
14
|
+
chartVariant: ChartVariant
|
|
15
|
+
setChartVariant: (v: ChartVariant) => void
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const ChartVariantContext = React.createContext<ChartVariantContextValue>({
|
|
19
|
+
chartVariant: "normal",
|
|
20
|
+
setChartVariant: () => {},
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
export function ChartVariantProvider({ children }: { children: React.ReactNode }) {
|
|
24
|
+
const [chartVariant, setChartVariant] = React.useState<ChartVariant>("kpi-chart")
|
|
25
|
+
const value = React.useMemo(() => ({ chartVariant, setChartVariant }), [chartVariant])
|
|
26
|
+
return (
|
|
27
|
+
<ChartVariantContext.Provider value={value}>
|
|
28
|
+
{children}
|
|
29
|
+
</ChartVariantContext.Provider>
|
|
30
|
+
)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function useChartVariant() {
|
|
34
|
+
return React.useContext(ChartVariantContext)
|
|
35
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import type { CommandMenuConfig } from "@/lib/command-menu-config"
|
|
5
|
+
|
|
6
|
+
const CommandMenuContext = React.createContext<CommandMenuConfig | null>(null)
|
|
7
|
+
|
|
8
|
+
export function CommandMenuProvider({
|
|
9
|
+
value,
|
|
10
|
+
children,
|
|
11
|
+
}: {
|
|
12
|
+
value: CommandMenuConfig
|
|
13
|
+
children: React.ReactNode
|
|
14
|
+
}) {
|
|
15
|
+
return (
|
|
16
|
+
<CommandMenuContext.Provider value={value}>{children}</CommandMenuContext.Provider>
|
|
17
|
+
)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function useCommandMenuConfig(): CommandMenuConfig {
|
|
21
|
+
const ctx = React.useContext(CommandMenuContext)
|
|
22
|
+
if (!ctx) {
|
|
23
|
+
throw new Error(
|
|
24
|
+
"useCommandMenuConfig must be used within CommandMenuProvider (wrap the app shell and pass command menu config).",
|
|
25
|
+
)
|
|
26
|
+
}
|
|
27
|
+
return ctx
|
|
28
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* DashboardViewContext
|
|
5
|
+
* Shared state for the active dashboard view (report | simple | mix).
|
|
6
|
+
* Consumed by NavUser (sidebar profile menu) and DashboardTabs (page content).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import * as React from "react"
|
|
10
|
+
|
|
11
|
+
export type DashboardView = "report" | "simple" | "mix"
|
|
12
|
+
|
|
13
|
+
interface DashboardViewContextValue {
|
|
14
|
+
activeView: DashboardView
|
|
15
|
+
setActiveView: (v: DashboardView) => void
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const DashboardViewContext = React.createContext<DashboardViewContextValue>({
|
|
19
|
+
activeView: "report",
|
|
20
|
+
setActiveView: () => {},
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
export function useDashboardView() {
|
|
24
|
+
return React.useContext(DashboardViewContext)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function DashboardViewProvider({ children }: { children: React.ReactNode }) {
|
|
28
|
+
const [activeView, setActiveView] = React.useState<DashboardView>("report")
|
|
29
|
+
const value = React.useMemo(() => ({ activeView, setActiveView }), [activeView])
|
|
30
|
+
return (
|
|
31
|
+
<DashboardViewContext.Provider value={value}>
|
|
32
|
+
{children}
|
|
33
|
+
</DashboardViewContext.Provider>
|
|
34
|
+
)
|
|
35
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ProductProvider — rehydrates the Zustand app store from localStorage after
|
|
5
|
+
* mount, then keeps the <html> theme class in sync with the active product.
|
|
6
|
+
*
|
|
7
|
+
* `useProduct()` is a thin convenience hook over useAppStore so all existing
|
|
8
|
+
* call-sites continue to work without any changes.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import * as React from "react"
|
|
12
|
+
import { useAppStore, type Product } from "@/stores/app-store"
|
|
13
|
+
|
|
14
|
+
export type { Product }
|
|
15
|
+
|
|
16
|
+
export function useProduct() {
|
|
17
|
+
const product = useAppStore(s => s.product)
|
|
18
|
+
const setProduct = useAppStore(s => s.setProduct)
|
|
19
|
+
return { product, setProduct }
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function ProductProvider({ children }: { children: React.ReactNode }) {
|
|
23
|
+
const product = useAppStore(s => s.product)
|
|
24
|
+
|
|
25
|
+
// Rehydrate from localStorage once — keeps SSR render matching server output.
|
|
26
|
+
React.useEffect(() => {
|
|
27
|
+
void useAppStore.persist.rehydrate()
|
|
28
|
+
}, [])
|
|
29
|
+
|
|
30
|
+
// Sync theme class to <html> whenever product changes.
|
|
31
|
+
React.useEffect(() => {
|
|
32
|
+
const html = document.documentElement
|
|
33
|
+
html.classList.remove("theme-one", "theme-prism")
|
|
34
|
+
html.classList.add(product === "exxat-one" ? "theme-one" : "theme-prism")
|
|
35
|
+
}, [product])
|
|
36
|
+
|
|
37
|
+
return <>{children}</>
|
|
38
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* SystemBannerContext — the top-of-app system banner is now user-configurable
|
|
5
|
+
* from Settings. We persist the config in `localStorage` so a product owner
|
|
6
|
+
* (or a demo) can preview "maintenance notice", "new feature", "promo", etc.
|
|
7
|
+
* without a redeploy. In production this would be backed by an admin API, but
|
|
8
|
+
* the storage shape + provider surface already match what that API would need.
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* - `<SystemBannerProvider>` wraps the app in (app)/layout.tsx.
|
|
12
|
+
* - Layout reads the config via `useSystemBanner()` and renders `<SystemBanner>` only when enabled.
|
|
13
|
+
* - Settings page calls `updateConfig()` / `setEnabled()` to change it.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import * as React from "react"
|
|
17
|
+
|
|
18
|
+
export type SystemBannerVariant = "info" | "warning" | "error" | "success" | "promo"
|
|
19
|
+
|
|
20
|
+
export type SystemBannerEmphasis = "prominent" | "subtle"
|
|
21
|
+
|
|
22
|
+
export interface SystemBannerConfig {
|
|
23
|
+
enabled: boolean
|
|
24
|
+
variant: SystemBannerVariant
|
|
25
|
+
emphasis: SystemBannerEmphasis
|
|
26
|
+
title: string
|
|
27
|
+
message: string
|
|
28
|
+
actionLabel?: string
|
|
29
|
+
actionHref?: string
|
|
30
|
+
dismissible: boolean
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const DEFAULT_SYSTEM_BANNER_CONFIG: SystemBannerConfig = {
|
|
34
|
+
enabled: true,
|
|
35
|
+
variant: "promo",
|
|
36
|
+
emphasis: "prominent",
|
|
37
|
+
title: "Exxat One Premium",
|
|
38
|
+
message: "Unlock advanced analytics, AI insights, and priority support.",
|
|
39
|
+
actionLabel: "Learn more",
|
|
40
|
+
actionHref: "#",
|
|
41
|
+
dismissible: true,
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const STORAGE_KEY = "exxat:system-banner-config"
|
|
45
|
+
|
|
46
|
+
interface SystemBannerContextValue {
|
|
47
|
+
config: SystemBannerConfig
|
|
48
|
+
updateConfig: (patch: Partial<SystemBannerConfig>) => void
|
|
49
|
+
setEnabled: (enabled: boolean) => void
|
|
50
|
+
/** Apply an arbitrary config snapshot wholesale (used by Settings "Apply" button). */
|
|
51
|
+
applyConfig: (next: SystemBannerConfig) => void
|
|
52
|
+
/** Restore the shipped default. */
|
|
53
|
+
reset: () => void
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const SystemBannerContext = React.createContext<SystemBannerContextValue>({
|
|
57
|
+
config: DEFAULT_SYSTEM_BANNER_CONFIG,
|
|
58
|
+
updateConfig: () => {},
|
|
59
|
+
setEnabled: () => {},
|
|
60
|
+
applyConfig: () => {},
|
|
61
|
+
reset: () => {},
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
function readStored(): SystemBannerConfig {
|
|
65
|
+
if (typeof window === "undefined") return DEFAULT_SYSTEM_BANNER_CONFIG
|
|
66
|
+
try {
|
|
67
|
+
const raw = window.localStorage.getItem(STORAGE_KEY)
|
|
68
|
+
if (!raw) return DEFAULT_SYSTEM_BANNER_CONFIG
|
|
69
|
+
const parsed = JSON.parse(raw) as Partial<SystemBannerConfig>
|
|
70
|
+
// Merge so newly-added fields in the default keep working for old payloads.
|
|
71
|
+
return { ...DEFAULT_SYSTEM_BANNER_CONFIG, ...parsed }
|
|
72
|
+
} catch {
|
|
73
|
+
return DEFAULT_SYSTEM_BANNER_CONFIG
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function SystemBannerProvider({ children }: { children: React.ReactNode }) {
|
|
78
|
+
// Start from default on SSR; hydrate from storage on mount so server and client markup match.
|
|
79
|
+
const [config, setConfig] = React.useState<SystemBannerConfig>(DEFAULT_SYSTEM_BANNER_CONFIG)
|
|
80
|
+
const [hydrated, setHydrated] = React.useState(false)
|
|
81
|
+
|
|
82
|
+
React.useEffect(() => {
|
|
83
|
+
setConfig(readStored())
|
|
84
|
+
setHydrated(true)
|
|
85
|
+
}, [])
|
|
86
|
+
|
|
87
|
+
// Persist whenever config changes post-hydration.
|
|
88
|
+
React.useEffect(() => {
|
|
89
|
+
if (!hydrated) return
|
|
90
|
+
try {
|
|
91
|
+
window.localStorage.setItem(STORAGE_KEY, JSON.stringify(config))
|
|
92
|
+
} catch {
|
|
93
|
+
/* storage full / disabled — silently ignore, the UI still works for the session. */
|
|
94
|
+
}
|
|
95
|
+
}, [config, hydrated])
|
|
96
|
+
|
|
97
|
+
// Cross-tab sync — if you change the banner in one tab, others follow.
|
|
98
|
+
React.useEffect(() => {
|
|
99
|
+
function onStorage(e: StorageEvent) {
|
|
100
|
+
if (e.key !== STORAGE_KEY || !e.newValue) return
|
|
101
|
+
try {
|
|
102
|
+
setConfig({ ...DEFAULT_SYSTEM_BANNER_CONFIG, ...JSON.parse(e.newValue) })
|
|
103
|
+
} catch {
|
|
104
|
+
/* ignore malformed payloads */
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
window.addEventListener("storage", onStorage)
|
|
108
|
+
return () => window.removeEventListener("storage", onStorage)
|
|
109
|
+
}, [])
|
|
110
|
+
|
|
111
|
+
const value = React.useMemo<SystemBannerContextValue>(
|
|
112
|
+
() => ({
|
|
113
|
+
config,
|
|
114
|
+
updateConfig: (patch) => setConfig((prev) => ({ ...prev, ...patch })),
|
|
115
|
+
setEnabled: (enabled) => setConfig((prev) => ({ ...prev, enabled })),
|
|
116
|
+
applyConfig: (next) => setConfig(next),
|
|
117
|
+
reset: () => setConfig(DEFAULT_SYSTEM_BANNER_CONFIG),
|
|
118
|
+
}),
|
|
119
|
+
[config],
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
return <SystemBannerContext.Provider value={value}>{children}</SystemBannerContext.Provider>
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function useSystemBanner() {
|
|
126
|
+
return React.useContext(SystemBannerContext)
|
|
127
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Global command palette (⌘K)
|
|
2
|
+
|
|
3
|
+
**Code:** `components/command-menu.tsx`, config **`buildCommandMenuConfig()`** in **`lib/command-menu-config.ts`**, wired in **`app/(app)/layout.tsx`** via **`CommandMenuProvider`**. Searchable row data is built in **`lib/command-menu-search-data.ts`** (not inside the shell component).
|
|
4
|
+
|
|
5
|
+
## Role
|
|
6
|
+
|
|
7
|
+
The command menu is the app’s **global search and AI entry**—one place to jump to routes, library pages, patterns, and AI starters. It is **not** a duplicate of the left sidebar; it **indexes** product surfaces for fast discovery.
|
|
8
|
+
|
|
9
|
+
## User model
|
|
10
|
+
|
|
11
|
+
| Need | Where it lives |
|
|
12
|
+
|------|----------------|
|
|
13
|
+
| **Find a page, component, or pattern** | Palette — filterable list, **Enter** to navigate. |
|
|
14
|
+
| **Quick AI / natural language** | Prefer **short answers or “research” snippets inside the palette** when the product can return compact, citeable results without leaving the flow. |
|
|
15
|
+
| **Long or complex answers** | **Ask Leo** right sidebar — multi-step reasoning, long-form help, or anything that does not fit a small results surface. |
|
|
16
|
+
|
|
17
|
+
**Design rule:** Treat the palette as **progressive disclosure**—lightweight results first in-menu; **escalate** to Ask Leo when the answer is too large or needs a dedicated conversation surface.
|
|
18
|
+
|
|
19
|
+
## Keyboard
|
|
20
|
+
|
|
21
|
+
- **⌘K / Ctrl+K** — open / toggle the palette (global listener in `CommandMenu`).
|
|
22
|
+
- **Ask Leo** remains **⌘⌥K / Ctrl+Alt+K** (see `.cursor/rules/exxat-kbd-shortcuts.mdc`) so it does not collide with table search where both exist.
|
|
23
|
+
|
|
24
|
+
## Configuration
|
|
25
|
+
|
|
26
|
+
- **Static groups** (AI suggestions, Navigation, Components, Patterns) live in **`lib/command-menu-config.ts`**.
|
|
27
|
+
- Optional **`dataGroups`** are passed to **`buildCommandMenuConfig({ dataGroups })`** from the app shell. **Implementation:** **`getCommandMenuSearchDataGroups()`** in **`lib/command-menu-search-data.ts`** maps mock/API rows (e.g. placements with student names) to **`CommandMenuItem`** rows with **`label`**, **`href`**, and **`keywords`** for cmdk filtering. The layout uses **`buildCommandMenuConfig({ dataGroups: getCommandMenuSearchDataGroups() })`**. Keep domain mapping **outside** `command-menu.tsx` so data sources can change without editing the shell.
|
|
28
|
+
- **`searchOnly` on `CommandMenuGroup`:** cmdk shows **every** mounted item when the search box is empty. For large indexes (hundreds of placements), set **`searchOnly: true`** on that group so **`command-menu.tsx`** does not render it until the user has typed a non-empty query. Static groups (AI, Navigation, …) stay visible on open; the data group appears once the user searches.
|
|
29
|
+
|
|
30
|
+
## Files (quick reference)
|
|
31
|
+
|
|
32
|
+
| Piece | Location |
|
|
33
|
+
|-------|----------|
|
|
34
|
+
| Palette UI | `components/command-menu.tsx` |
|
|
35
|
+
| Static groups + `buildCommandMenuConfig` | `lib/command-menu-config.ts` (`CommandMenuGroup.searchOnly`) |
|
|
36
|
+
| Row → menu items (placements, etc.) | `lib/command-menu-search-data.ts` |
|
|
37
|
+
| Provider + `dataGroups` | `app/(app)/layout.tsx` |
|
|
38
|
+
|
|
39
|
+
## Sidebar
|
|
40
|
+
|
|
41
|
+
**“Search or ask Leo”** in the sidebar opens the same palette; shortcuts and copy should stay aligned with **`AGENTS.md` §7.1**.
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
Keep this document aligned with **`exxat-ds/AGENTS.md` §7.1** when behavior or copy changes.
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# Data views pattern (table, list, board)
|
|
2
|
+
|
|
3
|
+
> **Canonical rules for agents (MUST/MUST NOT, checklists):** [`AGENTS.md`](../AGENTS.md) in this package (including **§8 Accessibility**). This file is the long-form narrative; keep both aligned when patterns change.
|
|
4
|
+
|
|
5
|
+
This document describes how list pages combine **views**, **toolbar** behavior, **filters**, **properties**, and **persistence** in this codebase.
|
|
6
|
+
|
|
7
|
+
## Reuse existing components (required)
|
|
8
|
+
|
|
9
|
+
**Prefer composing what already exists** over rebuilding one-off tabs, search, filters, or property panels. The **Placements** flow is the reference implementation; new list/table/board pages should wire the same building blocks with new data and column definitions.
|
|
10
|
+
|
|
11
|
+
| Need | Reuse | Where Placements uses it |
|
|
12
|
+
| --- | --- | --- |
|
|
13
|
+
| **View tabs** (table / list / board, lifecycle filters) | `ListPageTemplate` (`ViewTab`, `renderContent`, optional metrics + export) | `components/data-list-client.tsx` + `components/templates/list-page.tsx` |
|
|
14
|
+
| **Table shell** (search, filter bar, sort, grouping, columns, pagination) | `DataTable`, `DataTableToolbar`, `useTableState` | `components/data-table/`, `components/data-list-table.tsx` |
|
|
15
|
+
| **Properties drawer** (display, columns, filters, sort, view type tiles) | `TablePropertiesDrawer` from `@/components/table-properties` | `DrawerToolbar` / list–board shells in `data-list-table.tsx` |
|
|
16
|
+
| **Board / list** | `PlacementsBoardView`, `PlacementsListView` + same `useTableState` | `DataListTable` |
|
|
17
|
+
| **Page header** (primary CTA + More ⋯ + export) | `PlacementsPageHeader` or `TeamPageHeader` | `components/placements-page-header.tsx`, `components/team-page-header.tsx` |
|
|
18
|
+
| **Team page (primary template)** | `TeamClient` = `ListPageTemplate` + `KeyMetrics` + `TeamPageHeader` + `TeamTable` (same composition as `DataListClient`) | `components/team-client.tsx`, `lib/mock/team-kpi.ts` |
|
|
19
|
+
| **Team roster** | `TeamTable` — `DataTable` + `useTableState` + `TablePropertiesDrawer`; list/board/dashboard read **`tableState.rows`** | `components/team-table.tsx` |
|
|
20
|
+
| **Dashboard view (list tab)** | **`KeyMetrics`** (`variant="flat"` or `"card"`) — same KPI system as the template metrics strip; **do not** add ad-hoc `Card` grids for entity summaries | `TeamTable` dashboard branch, `lib/mock/team-kpi.ts` |
|
|
21
|
+
| **Export** | `ExportDrawer` | `ListPageTemplate` export props; `DataListClient` |
|
|
22
|
+
|
|
23
|
+
**Rules:** (1) Import and compose these components; pass **props** and **column defs** for your entity. (2) If something is missing, **extend the shared component** under `components/` (e.g. a new optional slot on `DataTableToolbar`) rather than copying markup into a single page. (3) Card-only or lightweight pages may use a smaller **Properties** sheet only when there is **no** table—otherwise use `TablePropertiesDrawer` with `DataTable` (see Team).
|
|
24
|
+
|
|
25
|
+
### Table pages must use view tabs
|
|
26
|
+
|
|
27
|
+
If the page uses a **`DataTable`** (or equivalent data grid) as the main surface, it **must** sit under **`ListPageTemplate`** so users get the **views toolbar** (tabs, add view, per-tab settings). The default `ViewTab` entries should include at least one tab whose `viewType` is **`table`** when the primary experience is tabular data. **Do not** render a `DataTable` alone under only `PageHeader` + body.
|
|
28
|
+
|
|
29
|
+
- **Reference:** `DataListClient` (Placements) and `TeamClient` (Team).
|
|
30
|
+
- **Rationale:** Consistent navigation, saved views, per-tab view type (table / list / board), and export at the template level.
|
|
31
|
+
|
|
32
|
+
## Architecture
|
|
33
|
+
|
|
34
|
+
- **Page shell** — `ListPageTemplate` owns the views toolbar (tabs), optional metrics, and export drawer. Content for the active tab is rendered via `renderContent`.
|
|
35
|
+
- **Per–lifecycle (or category) data** — `DataListTable` swaps **columns** and **row sets** based on `lifecycleTabId` (e.g. All / Upcoming / Ongoing / Completed). Each lifecycle tab gets its **own** `DataListTable` instance (`key={tab.filterId}`) so `useTableState` resets correctly when columns change.
|
|
36
|
+
- **Shared table state** — `useTableState` holds sort, search, filters, grouping, column order/visibility/pins/widths, row height, gridlines, and pagination flags. **Table**, **list**, **board**, and **dashboard** all read the same state so switching view type keeps behavior aligned.
|
|
37
|
+
|
|
38
|
+
## Mock data and connected views
|
|
39
|
+
|
|
40
|
+
1. **Put entity rows in `lib/mock/<entity>.ts`** — Export a typed array (e.g. `TeamMember[]`, `Placement[]`) and reuse it from the page client and from KPI helpers.
|
|
41
|
+
2. **KPI / summary helpers** — Add `lib/mock/<entity>-kpi.ts` (or next to the mock file) with pure functions **`entityKpiMetrics(rows: T[])`** and **`entityKpiInsight(rows: T[])`** returning `MetricItem[]` and `MetricInsight` from `@/components/key-metrics`. Drive **both** the template **`metrics`** slot and the **dashboard view** from the **same helpers**, passing **`tableState.rows`** in the table component so filters/search apply everywhere.
|
|
42
|
+
3. **Single table component** — One component (e.g. `TeamTable`) receives **`members`** (full mock) + **`view`**. It calls **`useTableState(members, columns, …)`** once. Branch on `view`:
|
|
43
|
+
- **`table`** → `DataTable` with that state.
|
|
44
|
+
- **`list` / `board`** → `DataTableToolbar` + list/board UI with **`tableState.rows`**.
|
|
45
|
+
- **`dashboard`** → `DataTableToolbar` + **`KeyMetrics`** (and/or other **existing** dashboard building blocks — `ChartsOverview` only when charts are product-appropriate) fed by **`teamKpiMetrics(tableState.rows)`** / **`teamKpiInsight(tableState.rows)`** — **not** bespoke `Card` grids duplicating KPIs.
|
|
46
|
+
4. **Client wiring** — `renderContent` always renders the table component with **`view={tab.viewType}`** and **`key={tab.id}`** (not `viewType`) so switching views does not remount state. **Do not** show “not wired” placeholders for list/board/dashboard when the table stack supports those views.
|
|
47
|
+
5. **Full-route dashboards** — The **`/dashboard`** page uses **`DashboardTabs`**, **`KeyMetrics`**, and **`ChartsOverview`** with `lib/mock/dashboard.ts`. List-page **dashboard view** is a **narrower** slice: reuse **`KeyMetrics`** (+ entity KPI helpers) first; add charts only when they match the entity and reuse **`ChartsOverview`** patterns from `components/charts-overview.tsx`.
|
|
48
|
+
|
|
49
|
+
## Table vs list vs board vs dashboard
|
|
50
|
+
|
|
51
|
+
| Concern | Table | List | Board | Dashboard (view tab) |
|
|
52
|
+
| --- | --- | --- | --- | --- |
|
|
53
|
+
| Primary surface | `DataTable` | `PlacementsListView` / entity list | `PlacementsBoardView` / entity board | **`KeyMetrics`** (+ optional charts via shared dashboard components) |
|
|
54
|
+
| Data source | `useTableState` | **`tableState.rows`** | **`tableState.rows`** | **`tableState.rows`** via KPI helpers |
|
|
55
|
+
| Column headers / labels | `showColumnLabels` | Same source columns, list layout | Phase columns + optional board column menu | N/A (metrics from same columns/filters) |
|
|
56
|
+
| Row click / navigation | From `DataListTable` | From list shell | Card `onOpen` | N/A |
|
|
57
|
+
| Pagination | Optional `PaginationBar` + `CountSyncer` | Same pattern | N/A (board uses phase columns) | N/A |
|
|
58
|
+
|
|
59
|
+
## Toolbar and properties
|
|
60
|
+
|
|
61
|
+
- **Search** — Global quick search lives in table state (`search` / `showToolbarSearch` from display options). Board phase columns can add local search in the board column header.
|
|
62
|
+
- **Filters** — Built from `ColumnDef.filter` and `filterFields` passed to `TablePropertiesDrawer`. Connectors between filters (`and` / `or`) are part of table state.
|
|
63
|
+
- **Sort / group** — Sort rules and `groupBy` are in table state; board menu proxies the same actions when `boardColumnMenu` is wired.
|
|
64
|
+
- **Properties drawer** — `TablePropertiesDrawer` is **generic**: supply `filterFields`, `fieldDefinitionsForDrawer`, `resolveColumnLabel`, `activeFilters`, sort/column handlers, `displayOptions`, and optional view-type tiles. Domain-specific defaults (e.g. `FILTER_FIELDS` in `types.ts`) are optional; new pages can pass their own definitions.
|
|
65
|
+
|
|
66
|
+
**Import:** `@/components/table-properties` re-exports the drawer and types.
|
|
67
|
+
|
|
68
|
+
## Board UI reuse
|
|
69
|
+
|
|
70
|
+
**Handbook:** **`AGENTS.md` §4.4** — board card shell, badge row, shared status maps, and MUST/MUST NOT. **Cursor:** **`.cursor/rules/exxat-board-cards.mdc`**, skill **`.cursor/skills/exxat-board-cards/SKILL.md`**.
|
|
71
|
+
|
|
72
|
+
- **Card shell** — `components/data-views/list-page-board-card.tsx` — **`ListPageBoardCard`**, **`ListPageBoardCardHeader`**, **`ListPageBoardCardTitleRow`**, **`ListPageBoardCardAvatar`**, **`ListPageBoardCardBadgeRow`**, **`ListPageBoardCardBody`**, **`ListPageBoardCardSecondary`**. All product board cards on list hubs **should** use this shell (same **`Card` `size="sm"`** pattern as Placements).
|
|
73
|
+
- **Primitives** — `components/data-views/board-card-primitives.tsx` — `BoardCardIconRow`, `BoardCardTwoLineBlock` (optional `line2`), `BoardNewCardPlaceholder`, `lineClampClass`.
|
|
74
|
+
- **Status (Team & Compliance)** — `lib/list-status-badges.ts` — single source for label strings + badge `className` tails for **table, list, and board**. Do **not** pair with `uppercase` on the Badge (labels are sentence case, aligned with Placements `BoardStatusBadge`).
|
|
75
|
+
- **Owner initials** — `lib/initials-from-name.ts` when mock rows have a display name but no `initials` field.
|
|
76
|
+
- **Shared column shell** — `components/data-views/list-page-board-template.tsx` — `ListPageBoardTemplate` + `ListPageBoardColumnDef<T>`: define columns with `filter` predicates, `renderCard`, `getRowKey`. Used by **Team** and **Compliance** boards; new hubs should start here before custom chrome.
|
|
77
|
+
- **Placement card** — `components/data-views/placement-board-card.tsx` — `BoardPlacementCard` composes **`ListPageBoardCard`** parts with `ColumnDef<Placement>` and lifecycle layout helpers from `lib/placement-board-card-layout.ts`. Placements keeps richer column headers (search, menus); still uses the same primitives.
|
|
78
|
+
- New entities should add their own card component that composes **`ListPageBoardCard`** + primitives rather than duplicating column scroll/layout or ad-hoc card chrome.
|
|
79
|
+
|
|
80
|
+
## Dashboard view (list pages)
|
|
81
|
+
|
|
82
|
+
- **Reuse the dashboard chart bundle** — `components/dashboard-report-charts.tsx` — `DashboardReportCharts`: flat `KeyMetrics` + middle chart section + period comparison `KeyMetrics` card. **`ChartsOverview`** (placement-themed demo gallery) is the default middle section for `/dashboard` and **Placements**. **Team** passes **`chartsSection={<TeamDashboardChartsSection members={tableState.rows} />}`** so charts reflect roster data, not placements. List hubs use `ListPageDashboardCharts` (`metricsSingleRow`). Chart **style** can follow `ChartVariantProvider` when using `ChartsOverview`.
|
|
83
|
+
- **Data tab canvas charts** (`data-view-dashboard-charts*.tsx`) share **`ChartFigure`**, **`ChartCard`**, and **`ChartDataTable`** with `charts-overview.tsx`. **Layout** for Placements / Team / Compliance is stored in one place: **`lib/data-view-dashboard-storage.ts`** (see `AGENTS.md` §4.3). **Keyboard-selected bars/slices** must use **`lib/chart-keyboard-selection.ts`** (`activeBar` / `activeShape`) so behavior matches the gallery — not opacity-only `Cell` dimming.
|
|
84
|
+
|
|
85
|
+
## Persistence
|
|
86
|
+
|
|
87
|
+
- **Page-level** (`lib/data-list-persistence.ts`, key `exxat-ds:data-list:page:v1`): `displayOptions`, `showMetrics`, `tabs`, `activeTabId`. Loaded in `DataListClient` with `useLayoutEffect`, saved debounced on change. `ListPageTemplate` runs in **controlled** mode when `tabs` / `onTabsChange` / `activeTabId` / `onActiveTabChange` are passed.
|
|
88
|
+
- **Per–lifecycle tab** (key `exxat-ds:data-list:lifecycle:v1:<filterId>`): sort, search, filters, group by, column order/hidden/widths/pins/wrap, column menu search map, row height, gridlines, filter bar visibility, search popover, conditional rules, pagination toggle and page/size. Hydrated in `DataListTable` with `useLayoutEffect`; saved debounced when those fields change.
|
|
89
|
+
|
|
90
|
+
To add a new page: copy the `DataListClient` pattern (controlled `ListPageTemplate` + storage key namespace) or call `schedulePageSave` / `scheduleLifecycleSave` with your own keys.
|
|
91
|
+
|
|
92
|
+
## Rules of thumb
|
|
93
|
+
|
|
94
|
+
1. **One `useTableState` per logical table** — Remount with `key` when the column set or entity context changes.
|
|
95
|
+
2. **Don’t fork filter/sort UX** — Reuse `TablePropertiesDrawer` and `DataTableToolbar` patterns so accessibility and behavior stay consistent.
|
|
96
|
+
3. **Boards derive from the same columns** — `boardColumns` / `hiddenColKeys` should reflect table `displayCols` so “hide column” and “properties” stay in sync.
|
|
97
|
+
4. **Persist stable JSON** — Version objects with `v: 1` and keep keys namespaced to allow migrations later.
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
## Required UX for dense lists (10+ items)
|
|
102
|
+
|
|
103
|
+
When a page shows a **list**, **table**, or **card grid** with **more than 10 items**, it must expose:
|
|
104
|
+
|
|
105
|
+
| Capability | Table / list / board | Card-only pages (no `DataTable`) |
|
|
106
|
+
| --- | --- | --- |
|
|
107
|
+
| **Search** | Toolbar search + column quick-search where applicable | At least one search control filtering the visible set |
|
|
108
|
+
| **Filter** | Filter bar + `TablePropertiesDrawer` filters | Filters in toolbar or in Properties, as appropriate to the content |
|
|
109
|
+
| **Sorting** | Column sort / sort rules in drawer | User-visible sort (e.g. name, date, role) |
|
|
110
|
+
| **Properties** | `TablePropertiesDrawer` (columns, display, filters, sort) | A **Properties** entry point (sheet or drawer) for view options — same *role* as table properties, even if the UI is simpler |
|
|
111
|
+
|
|
112
|
+
Below the threshold, these may be omitted unless the page is a primary data hub (see below).
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
## Data pages: primary CTA + More + Export
|
|
117
|
+
|
|
118
|
+
If the page **has exportable data** (rows, members, placements, etc.), follow the **Placements** header pattern:
|
|
119
|
+
|
|
120
|
+
1. **Primary action** — One default (filled) button for the main task (e.g. **New placement**, **Invite member**). Do **not** use `variant="outline"` for that primary action.
|
|
121
|
+
2. **More (⋯)** — An outline **icon** button opening a menu that includes **Export** (and other overflow actions). Wire **Export** to `ExportDrawer` (or equivalent).
|
|
122
|
+
3. **Subtitle** — Prefer a short line with **count + freshness** (e.g. `24 records · Last updated now`), matching `PlacementsPageHeader`.
|
|
123
|
+
|
|
124
|
+
Reference: `components/placements-page-header.tsx`, `components/team-page-header.tsx`, `components/team-client.tsx`.
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
## Page vs drawer (actions and auxiliary views)
|
|
129
|
+
|
|
130
|
+
**When to use a drawer or sheet:** The user needs **the surrounding page** (list, hub, or parent task) to stay **visible for context** *and* they need a **quick view**, **quick actions**, or a **short auxiliary step** — e.g. table properties, export, a brief row summary, or “change one setting and dismiss.”
|
|
131
|
+
|
|
132
|
+
**When to use a new page (route):** The flow is **primary**, **long-form**, **multi-step**, or should have its **own URL** / bookmark / history **without** the parent page behind it — e.g. full create/edit, wizards, or detail that *is* the task.
|
|
133
|
+
|
|
134
|
+
**Rule of thumb:** **Context + quick** → **drawer**; **otherwise** → **new page**.
|
|
135
|
+
|
|
136
|
+
Canonical rules: **`AGENTS.md` §6.4**, root **`.cursor/rules/exxat-page-vs-drawer.mdc`**.
|
|
137
|
+
|
|
138
|
+
---
|
|
139
|
+
|
|
140
|
+
## Primary pages with large datasets
|
|
141
|
+
|
|
142
|
+
When a route is a **primary** destination in nav (main hub for an entity) **and** the dataset is **large** or **highly interactive**:
|
|
143
|
+
|
|
144
|
+
- Use the **primary page template**: `ListPageTemplate` (tabs, metrics strip, export drawer at template level) + data client pattern as in **`DataListClient`** / **`DataListTable`** — not a minimal `PageHeader`-only layout.
|
|
145
|
+
- Smaller satellite pages may use **`PageHeader` + section content**; once the dataset grows past the dense-list threshold, add the toolbar rules above and consider promoting to the full template if the page becomes a main hub.
|
|
146
|
+
|
|
147
|
+
---
|
|
148
|
+
|
|
149
|
+
## Summary checklist
|
|
150
|
+
|
|
151
|
+
- [ ] **Reuse** — Tabs, search, filters, and property UI come from **`ListPageTemplate`**, **`DataTable` / `useTableState`**, **`TablePropertiesDrawer`**, and related Placements modules—not ad-hoc duplicates.
|
|
152
|
+
- [ ] **Table + tabs** — Any **`DataTable`** is wrapped in **`ListPageTemplate`** (view tabs), not only `PageHeader` + body.
|
|
153
|
+
- [ ] **>10 items** → search, filter, sort, properties (per surface type above).
|
|
154
|
+
- [ ] **Has data to export** → **More** menu with **Export** + shared `ExportDrawer` pattern.
|
|
155
|
+
- [ ] **Primary + large / main hub** → `ListPageTemplate`-style shell where applicable.
|
|
156
|
+
- [ ] **Page vs drawer (§6.4)** — Quick actions with parent **context** → drawer/sheet; primary or long flows → **new route**.
|
|
157
|
+
- [ ] **Primary button** → `Button` default variant (`size="lg"` for parity with Placements), not outline.
|
|
158
|
+
- [ ] **Dashboard view tab** → `KeyMetrics` + shared KPI helpers from **`tableState.rows`**; no duplicate one-off metric cards.
|
|
159
|
+
- [ ] **Data view charts** → `ChartFigure` + `chart-keyboard-selection`; layout persistence via **`data-view-dashboard-storage`** (see `AGENTS.md` §4.3).
|
|
160
|
+
- [ ] **Mock + KPIs** → Entity rows in **`lib/mock/`**; **`entityKpiMetrics` / `entityKpiInsight`** consumed by template metrics and dashboard view.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PM2 — keep `next dev` running after the terminal closes; restarts on crash.
|
|
3
|
+
* Start: `nvm use && npm run dev:daemon`
|
|
4
|
+
* @see README.md — Development (daemon)
|
|
5
|
+
*/
|
|
6
|
+
module.exports = {
|
|
7
|
+
apps: [
|
|
8
|
+
{
|
|
9
|
+
name: "exxat-ds",
|
|
10
|
+
script: "npm",
|
|
11
|
+
args: "run dev",
|
|
12
|
+
cwd: __dirname,
|
|
13
|
+
interpreter: "none",
|
|
14
|
+
autorestart: true,
|
|
15
|
+
max_restarts: 30,
|
|
16
|
+
min_uptime: "4s",
|
|
17
|
+
exp_backoff_restart_delay: 2000,
|
|
18
|
+
},
|
|
19
|
+
],
|
|
20
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { defineConfig, globalIgnores } from "eslint/config";
|
|
2
|
+
import nextVitals from "eslint-config-next/core-web-vitals";
|
|
3
|
+
import nextTs from "eslint-config-next/typescript";
|
|
4
|
+
|
|
5
|
+
const eslintConfig = defineConfig([
|
|
6
|
+
...nextVitals,
|
|
7
|
+
...nextTs,
|
|
8
|
+
// Override default ignores of eslint-config-next.
|
|
9
|
+
globalIgnores([
|
|
10
|
+
// Default ignores of eslint-config-next:
|
|
11
|
+
".next/**",
|
|
12
|
+
"out/**",
|
|
13
|
+
"build/**",
|
|
14
|
+
"next-env.d.ts",
|
|
15
|
+
]),
|
|
16
|
+
]);
|
|
17
|
+
|
|
18
|
+
export default eslintConfig;
|