@exxatdesignux/ui 0.0.6 → 0.0.8
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,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Merge saved layout with defaults for any dashboard canvas (Placements, Team, Compliance).
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export const KEY_METRICS_KPI_COUNT_MIN = 1
|
|
6
|
+
export const KEY_METRICS_KPI_COUNT_MAX = 4
|
|
7
|
+
export const KEY_METRICS_KPI_COUNT_DEFAULT = 4
|
|
8
|
+
|
|
9
|
+
export function clampKeyMetricsKpiCount(n: unknown): number {
|
|
10
|
+
const x = typeof n === "number" && Number.isFinite(n) ? Math.round(n) : KEY_METRICS_KPI_COUNT_DEFAULT
|
|
11
|
+
return Math.min(KEY_METRICS_KPI_COUNT_MAX, Math.max(KEY_METRICS_KPI_COUNT_MIN, x))
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface DashboardLayoutV1 {
|
|
15
|
+
visible: string[]
|
|
16
|
+
order: string[]
|
|
17
|
+
spans?: Record<string, 1 | 2>
|
|
18
|
+
chartTypes?: Record<string, string>
|
|
19
|
+
/** How many KPI cells to show on the key-metrics dashboard card (1–4). */
|
|
20
|
+
keyMetricsKpiCount?: number
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function mergeDashboardLayoutGeneric(
|
|
24
|
+
saved: DashboardLayoutV1 | null,
|
|
25
|
+
defaults: {
|
|
26
|
+
visible: string[]
|
|
27
|
+
order: string[]
|
|
28
|
+
spans: Record<string, 1 | 2>
|
|
29
|
+
chartTypes: Record<string, string>
|
|
30
|
+
keyMetricsKpiCount?: number
|
|
31
|
+
},
|
|
32
|
+
allCardIds: string[],
|
|
33
|
+
): DashboardLayoutV1 {
|
|
34
|
+
const defaultKpi = clampKeyMetricsKpiCount(defaults.keyMetricsKpiCount)
|
|
35
|
+
if (!saved) {
|
|
36
|
+
return {
|
|
37
|
+
visible: [...defaults.visible],
|
|
38
|
+
order: [...defaults.order],
|
|
39
|
+
spans: { ...defaults.spans },
|
|
40
|
+
chartTypes: { ...defaults.chartTypes },
|
|
41
|
+
keyMetricsKpiCount: defaultKpi,
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
let order = saved.order.length ? [...saved.order] : [...defaults.order]
|
|
45
|
+
let visible = saved.visible.length ? [...saved.visible] : [...defaults.visible]
|
|
46
|
+
for (const id of allCardIds) {
|
|
47
|
+
if (!order.includes(id)) order = [id, ...order.filter(x => x !== id)]
|
|
48
|
+
if (!visible.includes(id) && defaults.visible.includes(id)) {
|
|
49
|
+
visible = [...visible, id]
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
order = order.filter(id => allCardIds.includes(id))
|
|
53
|
+
visible = visible.filter(id => allCardIds.includes(id))
|
|
54
|
+
return {
|
|
55
|
+
visible: visible.length ? visible : [...defaults.visible],
|
|
56
|
+
order: order.length ? order : [...defaults.order],
|
|
57
|
+
spans: { ...defaults.spans, ...saved.spans },
|
|
58
|
+
chartTypes: { ...defaults.chartTypes, ...saved.chartTypes },
|
|
59
|
+
keyMetricsKpiCount: clampKeyMetricsKpiCount(
|
|
60
|
+
saved.keyMetricsKpiCount ?? defaultKpi,
|
|
61
|
+
),
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Display options for Data list (table / board / etc.) — shared across view types
|
|
3
|
+
* so hide/show preferences persist when switching views.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export type BoardLineCount = 1 | 2 | 3
|
|
7
|
+
|
|
8
|
+
export interface DataListDisplayOptions {
|
|
9
|
+
/**
|
|
10
|
+
* Board swimlanes: dataset field (table column key) used to split cards into columns.
|
|
11
|
+
* Each hub passes allowed keys via `TablePropertiesDrawer` `boardGroupByColumnOptions`.
|
|
12
|
+
*/
|
|
13
|
+
boardGroupByColumnKey: string
|
|
14
|
+
/** Max lines for primary text blocks on board cards */
|
|
15
|
+
boardLineCount: BoardLineCount
|
|
16
|
+
/** Page title block (Placements + subtitle) */
|
|
17
|
+
showViewTitle: boolean
|
|
18
|
+
/** Board: phase column titles + descriptions. Table: column header row. */
|
|
19
|
+
showColumnLabels: boolean
|
|
20
|
+
/** Board: “N cards” under each phase column */
|
|
21
|
+
showBoardColumnCounts: boolean
|
|
22
|
+
boardNewCardAbove: boolean
|
|
23
|
+
/** Toolbar search control (table view) */
|
|
24
|
+
showToolbarSearch: boolean
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const DEFAULT_DATA_LIST_DISPLAY_OPTIONS: DataListDisplayOptions = {
|
|
28
|
+
boardGroupByColumnKey: "status",
|
|
29
|
+
boardLineCount: 2,
|
|
30
|
+
showViewTitle: true,
|
|
31
|
+
showColumnLabels: true,
|
|
32
|
+
showBoardColumnCounts: true,
|
|
33
|
+
boardNewCardAbove: true,
|
|
34
|
+
showToolbarSearch: true,
|
|
35
|
+
}
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Persist Data list page UI: per-page shell (tabs, display options) and per–lifecycle-tab table state.
|
|
3
|
+
* Keys are versioned so future migrations can bump `v` or the key suffix.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { Dispatch, SetStateAction } from "react"
|
|
7
|
+
import type { RowHeight } from "@/lib/row-height"
|
|
8
|
+
import type { DataListDisplayOptions } from "@/lib/data-list-display-options"
|
|
9
|
+
import type { ActiveFilter, ConditionalRule, SortRule } from "@/components/table-properties/types"
|
|
10
|
+
import type { ViewTab } from "@/components/templates/list-page"
|
|
11
|
+
import type { DataListViewType } from "@/lib/data-list-view"
|
|
12
|
+
|
|
13
|
+
export const DATA_LIST_PAGE_STORAGE_KEY = "exxat-ds:data-list:page:v1"
|
|
14
|
+
|
|
15
|
+
export function lifecycleStorageKey(lifecycleTabId: string): string {
|
|
16
|
+
return `exxat-ds:data-list:lifecycle:v1:${lifecycleTabId}`
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const LIFECYCLE_SAVE_DEBOUNCE_MS = 400
|
|
20
|
+
const PAGE_SAVE_DEBOUNCE_MS = 400
|
|
21
|
+
|
|
22
|
+
const lifecycleTimers = new Map<string, ReturnType<typeof setTimeout>>()
|
|
23
|
+
const pageTimer: { t?: ReturnType<typeof setTimeout> } = {}
|
|
24
|
+
|
|
25
|
+
export interface PersistedLifecycleV1 {
|
|
26
|
+
v: 1
|
|
27
|
+
sortRules: SortRule[]
|
|
28
|
+
search: string
|
|
29
|
+
activeFilters: ActiveFilter[]
|
|
30
|
+
filterConnectors: Record<string, "and" | "or">
|
|
31
|
+
groupBy: string | null
|
|
32
|
+
colOrder: string[]
|
|
33
|
+
hiddenCols: string[]
|
|
34
|
+
colWidths: Record<string, number>
|
|
35
|
+
colPins: Record<string, "left" | "right">
|
|
36
|
+
colWrap: Record<string, boolean>
|
|
37
|
+
colMenuSearch: Record<string, string>
|
|
38
|
+
rowHeight: RowHeight
|
|
39
|
+
showGridlines: boolean
|
|
40
|
+
filterBarVisible: boolean
|
|
41
|
+
searchOpen: boolean
|
|
42
|
+
conditionalRules: ConditionalRule[]
|
|
43
|
+
pagination: boolean
|
|
44
|
+
paginationPage: number
|
|
45
|
+
paginationPageSize: number
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface PersistedPageV1 {
|
|
49
|
+
v: 1
|
|
50
|
+
displayOptions: DataListDisplayOptions
|
|
51
|
+
showMetrics: boolean
|
|
52
|
+
tabs: ViewTab[]
|
|
53
|
+
activeTabId: string
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Narrow surface used to hydrate / snapshot table state without importing the hook implementation. */
|
|
57
|
+
export interface TableStatePersistSlice {
|
|
58
|
+
sortRules: SortRule[]
|
|
59
|
+
search: string
|
|
60
|
+
activeFilters: ActiveFilter[]
|
|
61
|
+
filterConnectors: Record<string, "and" | "or">
|
|
62
|
+
groupBy: string | null
|
|
63
|
+
colOrder: string[]
|
|
64
|
+
hiddenCols: Set<string>
|
|
65
|
+
colWidths: Record<string, number>
|
|
66
|
+
colPins: Record<string, "left" | "right">
|
|
67
|
+
colWrap: Record<string, boolean>
|
|
68
|
+
colMenuSearch: Record<string, string>
|
|
69
|
+
rowHeight: RowHeight
|
|
70
|
+
showGridlines: boolean
|
|
71
|
+
filterBarVisible: boolean
|
|
72
|
+
searchOpen: boolean
|
|
73
|
+
setSortRules: Dispatch<SetStateAction<SortRule[]>>
|
|
74
|
+
setSearch: Dispatch<SetStateAction<string>>
|
|
75
|
+
setActiveFilters: Dispatch<SetStateAction<ActiveFilter[]>>
|
|
76
|
+
setFilterConnectors: Dispatch<SetStateAction<Record<string, "and" | "or">>>
|
|
77
|
+
setGroupBy: Dispatch<SetStateAction<string | null>>
|
|
78
|
+
setColOrder: Dispatch<SetStateAction<string[]>>
|
|
79
|
+
setHiddenCols: Dispatch<SetStateAction<Set<string>>>
|
|
80
|
+
setColWidths: Dispatch<SetStateAction<Record<string, number>>>
|
|
81
|
+
setColPins: Dispatch<SetStateAction<Record<string, "left" | "right">>>
|
|
82
|
+
setColWrap: Dispatch<SetStateAction<Record<string, boolean>>>
|
|
83
|
+
setColMenuSearch: Dispatch<SetStateAction<Record<string, string>>>
|
|
84
|
+
setRowHeight: Dispatch<SetStateAction<RowHeight>>
|
|
85
|
+
setShowGridlines: Dispatch<SetStateAction<boolean>>
|
|
86
|
+
setFilterBarVisible: Dispatch<SetStateAction<boolean>>
|
|
87
|
+
setSearchOpen: Dispatch<SetStateAction<boolean>>
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const VIEW_TYPES: DataListViewType[] = ["table", "list", "board", "dashboard"]
|
|
91
|
+
|
|
92
|
+
function isViewType(v: unknown): v is DataListViewType {
|
|
93
|
+
return typeof v === "string" && (VIEW_TYPES as string[]).includes(v)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function parseViewTab(raw: unknown): ViewTab | null {
|
|
97
|
+
if (!raw || typeof raw !== "object") return null
|
|
98
|
+
const o = raw as Record<string, unknown>
|
|
99
|
+
if (typeof o.id !== "string" || typeof o.label !== "string") return null
|
|
100
|
+
if (!isViewType(o.viewType)) return null
|
|
101
|
+
if (typeof o.icon !== "string" || typeof o.filterId !== "string") return null
|
|
102
|
+
return {
|
|
103
|
+
id: o.id,
|
|
104
|
+
label: o.label,
|
|
105
|
+
viewType: o.viewType,
|
|
106
|
+
icon: o.icon,
|
|
107
|
+
filterId: o.filterId,
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function parsePersistedPage(raw: string | null): PersistedPageV1 | null {
|
|
112
|
+
if (!raw) return null
|
|
113
|
+
try {
|
|
114
|
+
const j = JSON.parse(raw) as unknown
|
|
115
|
+
if (!j || typeof j !== "object") return null
|
|
116
|
+
const o = j as Record<string, unknown>
|
|
117
|
+
if (o.v !== 1) return null
|
|
118
|
+
if (!o.displayOptions || typeof o.displayOptions !== "object") return null
|
|
119
|
+
if (typeof o.showMetrics !== "boolean") return null
|
|
120
|
+
if (!Array.isArray(o.tabs) || typeof o.activeTabId !== "string") return null
|
|
121
|
+
const tabs = o.tabs.map(parseViewTab).filter((t): t is ViewTab => t !== null)
|
|
122
|
+
if (tabs.length === 0) return null
|
|
123
|
+
return {
|
|
124
|
+
v: 1,
|
|
125
|
+
displayOptions: o.displayOptions as DataListDisplayOptions,
|
|
126
|
+
showMetrics: o.showMetrics,
|
|
127
|
+
tabs,
|
|
128
|
+
activeTabId: o.activeTabId,
|
|
129
|
+
}
|
|
130
|
+
} catch {
|
|
131
|
+
return null
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function parsePersistedLifecycle(raw: string | null): PersistedLifecycleV1 | null {
|
|
136
|
+
if (!raw) return null
|
|
137
|
+
try {
|
|
138
|
+
const j = JSON.parse(raw) as unknown
|
|
139
|
+
if (!j || typeof j !== "object") return null
|
|
140
|
+
const o = j as Record<string, unknown>
|
|
141
|
+
if (o.v !== 1) return null
|
|
142
|
+
if (!Array.isArray(o.sortRules)) return null
|
|
143
|
+
if (typeof o.search !== "string") return null
|
|
144
|
+
if (!Array.isArray(o.activeFilters)) return null
|
|
145
|
+
if (!o.filterConnectors || typeof o.filterConnectors !== "object") return null
|
|
146
|
+
if (o.groupBy !== null && typeof o.groupBy !== "string") return null
|
|
147
|
+
if (!Array.isArray(o.colOrder)) return null
|
|
148
|
+
if (!Array.isArray(o.hiddenCols)) return null
|
|
149
|
+
if (!o.colWidths || typeof o.colWidths !== "object") return null
|
|
150
|
+
if (!o.colPins || typeof o.colPins !== "object") return null
|
|
151
|
+
if (!o.colWrap || typeof o.colWrap !== "object") return null
|
|
152
|
+
if (!o.colMenuSearch || typeof o.colMenuSearch !== "object") return null
|
|
153
|
+
if (typeof o.rowHeight !== "string") return null
|
|
154
|
+
if (typeof o.showGridlines !== "boolean") return null
|
|
155
|
+
if (typeof o.filterBarVisible !== "boolean") return null
|
|
156
|
+
if (typeof o.searchOpen !== "boolean") return null
|
|
157
|
+
if (!Array.isArray(o.conditionalRules)) return null
|
|
158
|
+
if (typeof o.pagination !== "boolean") return null
|
|
159
|
+
if (typeof o.paginationPage !== "number" || typeof o.paginationPageSize !== "number") return null
|
|
160
|
+
return o as unknown as PersistedLifecycleV1
|
|
161
|
+
} catch {
|
|
162
|
+
return null
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function mergeColOrder(saved: string[], columnKeys: Set<string>): string[] {
|
|
167
|
+
const ordered = saved.filter(k => columnKeys.has(k))
|
|
168
|
+
for (const k of columnKeys) {
|
|
169
|
+
if (!ordered.includes(k)) ordered.push(k)
|
|
170
|
+
}
|
|
171
|
+
return ordered
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function filterRecordKeys<T extends Record<string, unknown>>(obj: T, keys: Set<string>): T {
|
|
175
|
+
const out = { ...obj }
|
|
176
|
+
for (const k of Object.keys(out)) {
|
|
177
|
+
if (!keys.has(k)) delete out[k]
|
|
178
|
+
}
|
|
179
|
+
return out
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export function applyLifecyclePersisted(
|
|
183
|
+
ts: TableStatePersistSlice,
|
|
184
|
+
p: PersistedLifecycleV1,
|
|
185
|
+
columnKeys: Set<string>,
|
|
186
|
+
): void {
|
|
187
|
+
const colOrder = mergeColOrder(p.colOrder, columnKeys)
|
|
188
|
+
const hidden = new Set(p.hiddenCols.filter(k => columnKeys.has(k)))
|
|
189
|
+
const colWidths = filterRecordKeys(p.colWidths, columnKeys) as Record<string, number>
|
|
190
|
+
const colPins = filterRecordKeys(p.colPins, columnKeys) as Record<string, "left" | "right">
|
|
191
|
+
const colWrap = filterRecordKeys(p.colWrap, columnKeys) as Record<string, boolean>
|
|
192
|
+
const colMenuSearch = filterRecordKeys(p.colMenuSearch, columnKeys) as Record<string, string>
|
|
193
|
+
|
|
194
|
+
ts.setSortRules(p.sortRules)
|
|
195
|
+
ts.setSearch(p.search)
|
|
196
|
+
ts.setActiveFilters(p.activeFilters)
|
|
197
|
+
ts.setFilterConnectors(p.filterConnectors)
|
|
198
|
+
ts.setGroupBy(p.groupBy)
|
|
199
|
+
ts.setColOrder(colOrder)
|
|
200
|
+
ts.setHiddenCols(hidden)
|
|
201
|
+
ts.setColWidths(colWidths)
|
|
202
|
+
ts.setColPins(colPins)
|
|
203
|
+
ts.setColWrap(colWrap)
|
|
204
|
+
ts.setColMenuSearch(colMenuSearch)
|
|
205
|
+
ts.setRowHeight(p.rowHeight)
|
|
206
|
+
ts.setShowGridlines(p.showGridlines)
|
|
207
|
+
ts.setFilterBarVisible(p.filterBarVisible)
|
|
208
|
+
ts.setSearchOpen(p.searchOpen)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export function serializeLifecycle(
|
|
212
|
+
ts: TableStatePersistSlice,
|
|
213
|
+
extras: {
|
|
214
|
+
conditionalRules: ConditionalRule[]
|
|
215
|
+
pagination: boolean
|
|
216
|
+
paginationPage: number
|
|
217
|
+
paginationPageSize: number
|
|
218
|
+
},
|
|
219
|
+
): PersistedLifecycleV1 {
|
|
220
|
+
return {
|
|
221
|
+
v: 1,
|
|
222
|
+
sortRules: ts.sortRules,
|
|
223
|
+
search: ts.search,
|
|
224
|
+
activeFilters: ts.activeFilters,
|
|
225
|
+
filterConnectors: ts.filterConnectors,
|
|
226
|
+
groupBy: ts.groupBy,
|
|
227
|
+
colOrder: ts.colOrder,
|
|
228
|
+
hiddenCols: [...ts.hiddenCols],
|
|
229
|
+
colWidths: { ...ts.colWidths },
|
|
230
|
+
colPins: { ...ts.colPins },
|
|
231
|
+
colWrap: { ...ts.colWrap },
|
|
232
|
+
colMenuSearch: { ...ts.colMenuSearch },
|
|
233
|
+
rowHeight: ts.rowHeight,
|
|
234
|
+
showGridlines: ts.showGridlines,
|
|
235
|
+
filterBarVisible: ts.filterBarVisible,
|
|
236
|
+
searchOpen: ts.searchOpen,
|
|
237
|
+
conditionalRules: extras.conditionalRules,
|
|
238
|
+
pagination: extras.pagination,
|
|
239
|
+
paginationPage: extras.paginationPage,
|
|
240
|
+
paginationPageSize: extras.paginationPageSize,
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export function loadLifecycleFromStorage(lifecycleTabId: string): PersistedLifecycleV1 | null {
|
|
245
|
+
if (typeof window === "undefined") return null
|
|
246
|
+
return parsePersistedLifecycle(localStorage.getItem(lifecycleStorageKey(lifecycleTabId)))
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export function scheduleLifecycleSave(lifecycleTabId: string, payload: PersistedLifecycleV1): void {
|
|
250
|
+
if (typeof window === "undefined") return
|
|
251
|
+
const prev = lifecycleTimers.get(lifecycleTabId)
|
|
252
|
+
if (prev) clearTimeout(prev)
|
|
253
|
+
const t = setTimeout(() => {
|
|
254
|
+
lifecycleTimers.delete(lifecycleTabId)
|
|
255
|
+
try {
|
|
256
|
+
localStorage.setItem(lifecycleStorageKey(lifecycleTabId), JSON.stringify(payload))
|
|
257
|
+
} catch {
|
|
258
|
+
/* quota / private mode */
|
|
259
|
+
}
|
|
260
|
+
}, LIFECYCLE_SAVE_DEBOUNCE_MS)
|
|
261
|
+
lifecycleTimers.set(lifecycleTabId, t)
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export function loadPageFromStorage(): PersistedPageV1 | null {
|
|
265
|
+
if (typeof window === "undefined") return null
|
|
266
|
+
return parsePersistedPage(localStorage.getItem(DATA_LIST_PAGE_STORAGE_KEY))
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
export function schedulePageSave(payload: PersistedPageV1): void {
|
|
270
|
+
if (typeof window === "undefined") return
|
|
271
|
+
if (pageTimer.t) clearTimeout(pageTimer.t)
|
|
272
|
+
pageTimer.t = setTimeout(() => {
|
|
273
|
+
pageTimer.t = undefined
|
|
274
|
+
try {
|
|
275
|
+
localStorage.setItem(DATA_LIST_PAGE_STORAGE_KEY, JSON.stringify(payload))
|
|
276
|
+
} catch {
|
|
277
|
+
/* quota */
|
|
278
|
+
}
|
|
279
|
+
}, PAGE_SAVE_DEBOUNCE_MS)
|
|
280
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Maps `DataListViewType` to the UI surface pattern for list pages.
|
|
3
|
+
*
|
|
4
|
+
* **Data:** One `useTableState(fullRows, columns, …)` per tab; **filtered/sorted rows**
|
|
5
|
+
* (`tableState.rows`) are the single source of truth for List, Board, and Dashboard.
|
|
6
|
+
* Table view renders the same state via `DataTable`.
|
|
7
|
+
*
|
|
8
|
+
* | View | Surface |
|
|
9
|
+
* |------------|---------|
|
|
10
|
+
* | `table` | `DataTable` |
|
|
11
|
+
* | `list` | `DataTableToolbar` + list layout |
|
|
12
|
+
* | `board` | `DataTableToolbar` + board / kanban |
|
|
13
|
+
* | `dashboard`| `DataTableToolbar` + KPI (`KeyMetrics`) + optional charts (`ChartCard`, Recharts, etc.) |
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type { DataListViewType } from "@/lib/data-list-view"
|
|
17
|
+
|
|
18
|
+
/** What to render for the active view tab (routing / branching). */
|
|
19
|
+
export type DataListViewRenderKind =
|
|
20
|
+
| "data-table"
|
|
21
|
+
| "list-with-toolbar"
|
|
22
|
+
| "board-with-toolbar"
|
|
23
|
+
| "dashboard-with-toolbar"
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Stable classification for switch/if chains. **Every** `DataListViewType` maps to exactly one kind.
|
|
27
|
+
* Use this so `dashboard` is never mistaken for `board` (a common bug when only `list` is special-cased).
|
|
28
|
+
*/
|
|
29
|
+
export function getDataListViewRenderKind(view: DataListViewType): DataListViewRenderKind {
|
|
30
|
+
switch (view) {
|
|
31
|
+
case "table":
|
|
32
|
+
return "data-table"
|
|
33
|
+
case "list":
|
|
34
|
+
return "list-with-toolbar"
|
|
35
|
+
case "board":
|
|
36
|
+
return "board-with-toolbar"
|
|
37
|
+
case "dashboard":
|
|
38
|
+
return "dashboard-with-toolbar"
|
|
39
|
+
default: {
|
|
40
|
+
const _x: never = view
|
|
41
|
+
return _x
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function usesDataTableComponent(view: DataListViewType): boolean {
|
|
47
|
+
return view === "table"
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** KPI band + optional charts — not the kanban board. */
|
|
51
|
+
export function usesDashboardSurface(view: DataListViewType): boolean {
|
|
52
|
+
return view === "dashboard"
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Shared toolbar (search, filters, properties); body differs by view. */
|
|
56
|
+
export function usesToolbarWithFilteredRows(view: DataListViewType): boolean {
|
|
57
|
+
return view === "list" || view === "board" || view === "dashboard"
|
|
58
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Data list “view type” — shared by Properties drawer, ListPageTemplate tabs, and client state.
|
|
3
|
+
*
|
|
4
|
+
* **Single source of truth** for view labels/icons: use `DATA_LIST_VIEW_TILES` and
|
|
5
|
+
* `dataListViewLabel` / `dataListViewIcon` on every page so Table / List / Board / Dashboard
|
|
6
|
+
* stay consistent and stay wired to the same `useTableState` dataset (see `docs/data-views-pattern.md`).
|
|
7
|
+
*/
|
|
8
|
+
export type DataListViewType = "table" | "list" | "board" | "dashboard"
|
|
9
|
+
|
|
10
|
+
export const DATA_LIST_VIEW_TILES: readonly {
|
|
11
|
+
value: DataListViewType
|
|
12
|
+
label: string
|
|
13
|
+
icon: string
|
|
14
|
+
}[] = [
|
|
15
|
+
{ value: "table", icon: "fa-table", label: "Table view" },
|
|
16
|
+
{ value: "list", icon: "fa-list", label: "List view" },
|
|
17
|
+
{ value: "board", icon: "fa-table-columns", label: "Board view" },
|
|
18
|
+
{ value: "dashboard", icon: "fa-chart-mixed", label: "Dashboard view" },
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
/** User-facing name for tabs, Properties summary rows, and tooltips (not entity-specific). */
|
|
22
|
+
export function dataListViewLabel(view: DataListViewType): string {
|
|
23
|
+
return DATA_LIST_VIEW_TILES.find(t => t.value === view)?.label ?? view
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Font Awesome icon class (no prefix) for tab / toolbar state when view changes. */
|
|
27
|
+
export function dataListViewIcon(view: DataListViewType): string {
|
|
28
|
+
return DATA_LIST_VIEW_TILES.find(t => t.value === view)?.icon ?? "fa-table"
|
|
29
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Centralized localStorage for **Data** view dashboard canvas (Placements, Team, Compliance).
|
|
3
|
+
* Single bundle key; per-scope slices. Migrates legacy per-hub keys when a scope is missing.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { DashboardLayoutV1 } from "@/lib/dashboard-layout-merge"
|
|
7
|
+
|
|
8
|
+
const BUNDLE_KEY = "exxat-ds:data-view-dashboards:v1"
|
|
9
|
+
|
|
10
|
+
/** Legacy keys (pre-bundle) — read when that scope is absent from the bundle. */
|
|
11
|
+
const LEGACY_KEYS: Record<DataViewScope, string> = {
|
|
12
|
+
placements: "exxat-dashboard-cards",
|
|
13
|
+
team: "exxat-team-dashboard-cards",
|
|
14
|
+
compliance: "exxat-compliance-dashboard-cards",
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type DataViewScope = "placements" | "team" | "compliance"
|
|
18
|
+
|
|
19
|
+
type LayoutBundle = Partial<Record<DataViewScope, DashboardLayoutV1>>
|
|
20
|
+
|
|
21
|
+
function parseLayout(raw: unknown): DashboardLayoutV1 | null {
|
|
22
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null
|
|
23
|
+
const o = raw as Record<string, unknown>
|
|
24
|
+
if (!Array.isArray(o.visible) || !Array.isArray(o.order)) return null
|
|
25
|
+
const km = typeof o.keyMetricsKpiCount === "number" ? o.keyMetricsKpiCount : undefined
|
|
26
|
+
return {
|
|
27
|
+
visible: o.visible as string[],
|
|
28
|
+
order: o.order as string[],
|
|
29
|
+
spans:
|
|
30
|
+
o.spans && typeof o.spans === "object" && !Array.isArray(o.spans)
|
|
31
|
+
? (o.spans as Record<string, 1 | 2>)
|
|
32
|
+
: undefined,
|
|
33
|
+
chartTypes:
|
|
34
|
+
o.chartTypes && typeof o.chartTypes === "object" && !Array.isArray(o.chartTypes)
|
|
35
|
+
? (o.chartTypes as Record<string, string>)
|
|
36
|
+
: undefined,
|
|
37
|
+
keyMetricsKpiCount: km,
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function readBundleRaw(): LayoutBundle {
|
|
42
|
+
if (typeof window === "undefined") return {}
|
|
43
|
+
try {
|
|
44
|
+
const raw = localStorage.getItem(BUNDLE_KEY)
|
|
45
|
+
if (!raw) return {}
|
|
46
|
+
const parsed = JSON.parse(raw) as unknown
|
|
47
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {}
|
|
48
|
+
return parsed as LayoutBundle
|
|
49
|
+
} catch {
|
|
50
|
+
return {}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function writeBundle(bundle: LayoutBundle) {
|
|
55
|
+
if (typeof window === "undefined") return
|
|
56
|
+
try {
|
|
57
|
+
localStorage.setItem(BUNDLE_KEY, JSON.stringify(bundle))
|
|
58
|
+
} catch {
|
|
59
|
+
/* ignore quota */
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Merge any missing scopes from legacy keys into the bundle (one-time per scope per session edge cases OK).
|
|
65
|
+
*/
|
|
66
|
+
function ensureBundleWithLegacy(): LayoutBundle {
|
|
67
|
+
let bundle = readBundleRaw()
|
|
68
|
+
let changed = false
|
|
69
|
+
for (const scope of ["placements", "team", "compliance"] as const) {
|
|
70
|
+
if (bundle[scope]) continue
|
|
71
|
+
try {
|
|
72
|
+
const raw = localStorage.getItem(LEGACY_KEYS[scope])
|
|
73
|
+
if (!raw) continue
|
|
74
|
+
const layout = parseLayout(JSON.parse(raw) as unknown)
|
|
75
|
+
if (layout) {
|
|
76
|
+
bundle = { ...bundle, [scope]: layout }
|
|
77
|
+
changed = true
|
|
78
|
+
}
|
|
79
|
+
} catch {
|
|
80
|
+
/* ignore */
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (changed) writeBundle(bundle)
|
|
84
|
+
return bundle
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Load persisted layout for a hub (Placements / Team / Compliance Data view).
|
|
89
|
+
*/
|
|
90
|
+
export function loadDataViewLayout(scope: DataViewScope): DashboardLayoutV1 | null {
|
|
91
|
+
const bundle = ensureBundleWithLegacy()
|
|
92
|
+
return bundle[scope] ?? null
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Save layout for one hub; updates the shared bundle atomically.
|
|
97
|
+
*/
|
|
98
|
+
export function saveDataViewLayout(scope: DataViewScope, layout: DashboardLayoutV1) {
|
|
99
|
+
const bundle = ensureBundleWithLegacy()
|
|
100
|
+
writeBundle({ ...bundle, [scope]: layout })
|
|
101
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"
|
|
2
|
+
|
|
3
|
+
import { devLog } from "./dev-log"
|
|
4
|
+
|
|
5
|
+
describe("devLog", () => {
|
|
6
|
+
const originalEnv = process.env.NODE_ENV
|
|
7
|
+
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
vi.spyOn(console, "log").mockImplementation(() => {})
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
afterEach(() => {
|
|
13
|
+
vi.restoreAllMocks()
|
|
14
|
+
process.env.NODE_ENV = originalEnv
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
it("logs in development", () => {
|
|
18
|
+
process.env.NODE_ENV = "development"
|
|
19
|
+
devLog("hello", 1)
|
|
20
|
+
expect(console.log).toHaveBeenCalledWith("hello", 1)
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it("does not log in production", () => {
|
|
24
|
+
process.env.NODE_ENV = "production"
|
|
25
|
+
devLog("silent")
|
|
26
|
+
expect(console.log).not.toHaveBeenCalled()
|
|
27
|
+
})
|
|
28
|
+
})
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* True when focus is in a field where global shortcuts should not fire.
|
|
3
|
+
*/
|
|
4
|
+
export function isEditableTarget(target: EventTarget | null): boolean {
|
|
5
|
+
const el = target as HTMLElement | null
|
|
6
|
+
if (!el) return false
|
|
7
|
+
if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement || el instanceof HTMLSelectElement)
|
|
8
|
+
return true
|
|
9
|
+
return el.getAttribute?.("contenteditable") === "true"
|
|
10
|
+
}
|