@exxatdesignux/ui 0.2.17 → 0.2.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +30 -0
- package/consumer-extras/AGENTS.md +76 -0
- package/consumer-extras/README.md +5 -1
- package/consumer-extras/cursor-skills/exxat-centralized-list-dataset/SKILL.md +14 -3
- package/consumer-extras/cursor-skills/exxat-consumer-app/SKILL.md +37 -0
- package/consumer-extras/cursor-skills/exxat-ds-skill/SKILL.md +22 -7
- package/consumer-extras/cursor-skills/exxat-focused-workflow-page/SKILL.md +57 -0
- package/consumer-extras/cursor-skills/exxat-kpi-flat-band/SKILL.md +38 -0
- package/consumer-extras/cursor-skills/exxat-primary-nav-secondary-panel/SKILL.md +10 -3
- package/consumer-extras/patterns/consumer-app-pattern.md +39 -0
- package/consumer-extras/patterns/consumer-upgrade-checklist.md +20 -0
- package/consumer-extras/patterns/data-views-pattern.md +42 -3
- package/consumer-extras/patterns/focused-workflow-page-pattern.md +84 -0
- package/consumer-extras/patterns/kpi-flat-band-pattern.md +57 -0
- package/consumer-extras/patterns/shell-surface-elevation-pattern.md +54 -0
- package/package.json +2 -1
- package/src/components/ui/button-group.tsx +81 -0
- package/src/components/ui/button.tsx +4 -4
- package/src/components/ui/sidebar.tsx +2 -2
- package/src/globals.css +7 -1807
- package/src/theme.css +10 -1126
- package/src/tokens/README.md +15 -0
- package/src/tokens/base.css +337 -0
- package/src/tokens/high-contrast.css +1195 -0
- package/src/tokens/layers.css +224 -0
- package/src/tokens/tailwind-bridge.css +118 -0
- package/src/tokens/themes.css +201 -0
- package/template/.cursor/rules/exxat-kbd-shortcuts.mdc +1 -1
- package/template/AGENTS.md +66 -21
- package/template/app/(app)/dashboard/loading.tsx +3 -15
- package/template/app/(app)/dashboard/page.tsx +2 -14
- package/template/app/(app)/data-list/layout.tsx +43 -0
- package/template/app/(app)/data-list/page.tsx +2 -2
- package/template/app/(app)/error.tsx +22 -6
- package/template/app/(app)/examples/focused-workflow/page.tsx +5 -0
- package/template/app/(app)/examples/page.tsx +1 -0
- package/template/app/(app)/layout.tsx +13 -6
- package/template/app/(app)/loading.tsx +1 -18
- package/template/app/(app)/question-bank/find/page.tsx +2 -1
- package/template/app/(app)/question-bank/library/page.tsx +2 -1
- package/template/app/(app)/question-bank/list/page.tsx +2 -1
- package/template/app/(app)/question-bank/new/page.tsx +15 -23
- package/template/app/(app)/question-bank/page.tsx +2 -1
- package/template/app/(app)/settings/page.tsx +4 -5
- package/template/app/global-error.tsx +63 -0
- package/template/app/globals.css +7 -1934
- package/template/app/layout.tsx +2 -0
- package/template/components/app-route-loading.tsx +14 -0
- package/template/components/app-sidebar.tsx +71 -55
- package/template/components/data-table/index.tsx +31 -67
- package/template/components/data-table/use-table-state.ts +33 -6
- package/template/components/data-views/index.ts +37 -9
- package/template/components/data-views/list-page-calendar-view.tsx +593 -0
- package/template/components/data-views/list-page-connected-view-body.tsx +66 -0
- package/template/components/data-views/list-page-folder-columns-panel.tsx +345 -0
- package/template/components/data-views/list-page-split-hub-chrome.tsx +8 -0
- package/template/components/dev-chunk-load-recovery.tsx +41 -0
- package/template/components/examples/focused-workflow-showcase.tsx +183 -0
- package/template/components/exxat-product-logo.tsx +2 -6
- package/template/components/key-metrics.tsx +54 -22
- package/template/components/list-hub-board-view.tsx +68 -0
- package/template/components/list-hub-client.tsx +186 -0
- package/template/components/list-hub-list-view.tsx +36 -0
- package/template/components/list-hub-panel-activator.tsx +8 -0
- package/template/components/list-hub-secondary-nav.tsx +121 -0
- package/template/components/list-hub-table.tsx +336 -0
- package/template/components/new-question-composer.tsx +6 -24
- package/template/components/product-switcher.tsx +5 -5
- package/template/components/product-wordmark.tsx +4 -7
- package/template/components/question-bank-client.tsx +4 -1
- package/template/components/question-bank-folder-columns-panel.tsx +104 -0
- package/template/components/question-bank-hub-client.tsx +2 -5
- package/template/components/question-bank-table.tsx +155 -509
- package/template/components/secondary-panel/nav-link-rows.tsx +83 -0
- package/template/components/secondary-panel.tsx +4 -44
- package/template/components/secondary-panels/list-hub-panel.tsx +39 -0
- package/template/components/secondary-panels/question-bank-panel.tsx +39 -0
- package/template/components/secondary-panels/registry.tsx +15 -0
- package/template/components/settings-appearance-card.tsx +3 -2
- package/template/components/settings-client.tsx +59 -15
- package/template/components/settings-form-row.tsx +9 -4
- package/template/components/sidebar-shell.tsx +2 -1
- package/template/components/table-properties/drawer-button.tsx +51 -20
- package/template/components/table-properties/drawer.tsx +81 -17
- package/template/components/templates/focused-workflow-layouts.tsx +448 -0
- package/template/components/templates/focused-workflow-page-template.tsx +69 -0
- package/template/components/templates/list-page.tsx +40 -13
- package/template/components/templates/nested-secondary-panel-shell.tsx +3 -2
- package/template/components/templates/page-loading-shell.tsx +262 -0
- package/template/components/ui/button-group.tsx +1 -0
- package/template/contexts/product-context.tsx +21 -2
- package/template/docs/consumer-app-pattern.md +39 -0
- package/template/docs/data-views-pattern.md +42 -3
- package/template/docs/drawer-vs-dialog-pattern.md +3 -1
- package/template/docs/focused-workflow-page-pattern.md +84 -0
- package/template/docs/kpi-flat-band-pattern.md +57 -0
- package/template/docs/kpi-strip-max-four-pattern.md +1 -0
- package/template/docs/shell-surface-elevation-pattern.md +54 -0
- package/template/lib/chunk-load-error.ts +13 -0
- package/template/lib/command-menu-search-data.ts +11 -27
- package/template/lib/conditional-rule-match.ts +87 -22
- package/template/lib/data-list-display-options.ts +16 -2
- package/template/lib/data-list-view-registry.ts +104 -0
- package/template/lib/data-list-view-surface.ts +15 -1
- package/template/lib/data-list-view.ts +16 -1
- package/template/lib/data-view-dashboard-storage.ts +38 -35
- package/template/lib/hub-connected-view-renderers.ts +58 -0
- package/template/lib/list-hub-nav.ts +121 -0
- package/template/lib/list-hub-supported-views.ts +10 -0
- package/template/lib/list-page-table-properties.ts +3 -7
- package/template/lib/list-status-badges.ts +4 -97
- package/template/lib/mock/list-hub-directory.ts +27 -0
- package/template/lib/mock/list-hub-kpi.ts +27 -0
- package/template/lib/mock/navigation.tsx +1 -0
- package/template/lib/page-loading-variant.ts +40 -0
- package/template/lib/question-bank-supported-views.ts +13 -0
- package/template/lib/sidebar-state-cookie.ts +9 -0
- package/template/lib/table-state-lifecycle.ts +60 -13
- package/template/app/(app)/data-list/[id]/page.tsx +0 -44
- package/template/app/(app)/data-list/new/page.tsx +0 -34
- package/template/components/compliance-board-view.tsx +0 -142
- package/template/components/compliance-client.tsx +0 -92
- package/template/components/compliance-list-view.tsx +0 -54
- package/template/components/compliance-page-header.tsx +0 -89
- package/template/components/compliance-table.tsx +0 -632
- package/template/components/data-view-dashboard-charts-compliance.tsx +0 -963
- package/template/components/data-view-dashboard-charts-team.tsx +0 -971
- package/template/components/data-view-dashboard-charts.tsx +0 -1503
- package/template/components/new-placement-back-btn.tsx +0 -28
- package/template/components/new-placement-form.tsx +0 -1068
- package/template/components/placement-board-card.tsx +0 -262
- package/template/components/placement-detail.tsx +0 -438
- package/template/components/placements-board-view.tsx +0 -404
- package/template/components/placements-client.tsx +0 -252
- package/template/components/placements-list-view.tsx +0 -171
- package/template/components/placements-page-header.tsx +0 -166
- package/template/components/placements-table-cells.test.tsx +0 -22
- package/template/components/placements-table-cells.tsx +0 -173
- package/template/components/placements-table-columns.tsx +0 -640
- package/template/components/placements-table.tsx +0 -1675
- package/template/components/rotations-empty-state.tsx +0 -50
- package/template/components/rotations-panel-activator.tsx +0 -8
- package/template/components/sites-all-client.tsx +0 -154
- package/template/components/sites-board-view.tsx +0 -67
- package/template/components/sites-list-view.tsx +0 -42
- package/template/components/sites-table.tsx +0 -402
- package/template/components/team-board-view.tsx +0 -122
- package/template/components/team-client.tsx +0 -100
- package/template/components/team-list-view.tsx +0 -59
- package/template/components/team-page-header.tsx +0 -92
- package/template/components/team-table.tsx +0 -714
- package/template/lib/data-view-dashboard-placements-layout.ts +0 -215
- package/template/lib/mock/compliance-kpi.ts +0 -61
- package/template/lib/mock/compliance.ts +0 -146
- package/template/lib/mock/placements-kpi.ts +0 -134
- package/template/lib/mock/placements.ts +0 -183
- package/template/lib/mock/sites-directory.ts +0 -16
- package/template/lib/mock/sites-kpi.ts +0 -25
- package/template/lib/mock/team-kpi.ts +0 -60
- package/template/lib/mock/team.ts +0 -118
- package/template/lib/placement-board-card-layout.ts +0 -79
- package/template/lib/placement-lifecycle.ts +0 -5
package/template/app/layout.tsx
CHANGED
|
@@ -6,6 +6,7 @@ import "./globals.css"
|
|
|
6
6
|
import { ThemeProvider } from "@/components/theme-provider"
|
|
7
7
|
import { TooltipProvider } from "@/components/ui/tooltip"
|
|
8
8
|
import { ProductProvider } from "@/contexts/product-context"
|
|
9
|
+
import { DevChunkLoadRecovery } from "@/components/dev-chunk-load-recovery"
|
|
9
10
|
import { ThemeColorSync } from "@/components/theme-color-sync"
|
|
10
11
|
import { cn } from "@/lib/utils"
|
|
11
12
|
|
|
@@ -90,6 +91,7 @@ export default function RootLayout({
|
|
|
90
91
|
/>
|
|
91
92
|
</head>
|
|
92
93
|
<body className="bg-sidebar text-foreground font-sans">
|
|
94
|
+
<DevChunkLoadRecovery />
|
|
93
95
|
{/*
|
|
94
96
|
* Font Awesome Pro Kit — subset via fontawesome-subset.manifest.json +
|
|
95
97
|
* fontawesome.com/kits (Icon Selection).
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { usePathname } from "next/navigation"
|
|
4
|
+
|
|
5
|
+
import { PageLoadingByVariant } from "@/components/templates/page-loading-shell"
|
|
6
|
+
import { resolvePageLoadingVariant } from "@/lib/page-loading-variant"
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Route-level loading UI — keeps `SidebarInset` + header chrome; skeleton matches destination template.
|
|
10
|
+
*/
|
|
11
|
+
export function AppRouteLoading() {
|
|
12
|
+
const pathname = usePathname()
|
|
13
|
+
return <PageLoadingByVariant variant={resolvePageLoadingVariant(pathname)} />
|
|
14
|
+
}
|
|
@@ -105,11 +105,22 @@ function normalizedLocationHash(locationHash: string): string {
|
|
|
105
105
|
return locationHash.startsWith("#") ? locationHash.slice(1) : locationHash
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
+
/**
|
|
109
|
+
* Paths where a sibling sidebar row uses `#fragment` on the same pathname
|
|
110
|
+
* (e.g. Settings vs Tokens & themes). The plain-path row must not stay active
|
|
111
|
+
* when the hash belongs to that sibling.
|
|
112
|
+
*/
|
|
113
|
+
const NAV_EXCLUSIVE_HASH_BY_PATH: Readonly<Record<string, readonly string[]>> = {
|
|
114
|
+
"/settings": ["appearance"],
|
|
115
|
+
"/help": ["more"],
|
|
116
|
+
}
|
|
117
|
+
|
|
108
118
|
/**
|
|
109
119
|
* Whether `pathname` (+ optional `location.hash`) matches a sidebar `href`.
|
|
110
120
|
* When several links share the same path (e.g. `/settings`), disambiguate with `#fragment`
|
|
111
121
|
* in each `href` — those rows use the `frag !== null` branch below.
|
|
112
|
-
* For `href` without `#…`, an in-page hash (e.g. QB view tabs) does not clear the match
|
|
122
|
+
* For `href` without `#…`, an in-page hash (e.g. QB view tabs) does not clear the match,
|
|
123
|
+
* except when the hash is reserved for a hash-sibling row on that path.
|
|
113
124
|
*/
|
|
114
125
|
function isNavActive(pathname: string, url: string, locationHash = ""): boolean {
|
|
115
126
|
const pathOnly = navUrlPath(url)
|
|
@@ -131,7 +142,13 @@ function isNavActive(pathname: string, url: string, locationHash = ""): boolean
|
|
|
131
142
|
|
|
132
143
|
if (pathOnly === "/") return pathname === "/" && h === ""
|
|
133
144
|
/** Exact path match — ignore `location.hash` when the nav `href` has no `#…` fragment (QB view tabs use hash). */
|
|
134
|
-
if (pathname === pathOnly)
|
|
145
|
+
if (pathname === pathOnly) {
|
|
146
|
+
if (h !== "") {
|
|
147
|
+
const exclusive = NAV_EXCLUSIVE_HASH_BY_PATH[pathOnly]
|
|
148
|
+
if (exclusive?.includes(h)) return false
|
|
149
|
+
}
|
|
150
|
+
return true
|
|
151
|
+
}
|
|
135
152
|
// Design system library — active on hub and detail routes.
|
|
136
153
|
if (pathOnly === "/library") {
|
|
137
154
|
return pathname.startsWith("/library/")
|
|
@@ -827,60 +844,58 @@ function ProductLogoButton() {
|
|
|
827
844
|
|
|
828
845
|
return (
|
|
829
846
|
<DropdownMenu>
|
|
830
|
-
<
|
|
831
|
-
<
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
</DropdownMenuTrigger>
|
|
877
|
-
</TooltipTrigger>
|
|
878
|
-
<TooltipContent side="right" align="center" hidden={state !== "collapsed" || isMobile}>
|
|
879
|
-
{current.label}
|
|
880
|
-
</TooltipContent>
|
|
881
|
-
</Tooltip>
|
|
847
|
+
<DropdownMenuTrigger asChild>
|
|
848
|
+
<SidebarMenuButton
|
|
849
|
+
size="lg"
|
|
850
|
+
tooltip={iconRail ? current.label : undefined}
|
|
851
|
+
className={cn(
|
|
852
|
+
"py-2 text-sidebar-foreground data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground",
|
|
853
|
+
expandedOrMobile &&
|
|
854
|
+
"h-auto min-h-12 !overflow-visible items-center [&>span:last-child]:!overflow-visible [&>span:last-child]:!whitespace-normal [&>span:last-child]:text-clip",
|
|
855
|
+
"group-data-[collapsible=icon]:items-center group-data-[collapsible=icon]:justify-center",
|
|
856
|
+
iconRail &&
|
|
857
|
+
"group-data-[collapsible=icon]:!size-9 group-data-[collapsible=icon]:!min-h-9 group-data-[collapsible=icon]:!max-h-9 group-data-[collapsible=icon]:!p-0 group-data-[collapsible=icon]:overflow-visible",
|
|
858
|
+
)}
|
|
859
|
+
aria-label={`Current product: ${current.label}. Switch product`}
|
|
860
|
+
suppressHydrationWarning
|
|
861
|
+
>
|
|
862
|
+
{iconRail ? (
|
|
863
|
+
// Match the school selector footprint in the icon rail (32px frame,
|
|
864
|
+
// 28px mark — same visual weight as the avatar with inset padding).
|
|
865
|
+
<span className="flex size-8 shrink-0 items-center justify-center">
|
|
866
|
+
<ExxatProductMark product={current.id} className="size-7" />
|
|
867
|
+
</span>
|
|
868
|
+
) : (
|
|
869
|
+
<span className="flex min-h-0 min-w-0 flex-1 items-stretch gap-2">
|
|
870
|
+
<span
|
|
871
|
+
className="flex min-h-0 min-w-0 flex-1 items-center justify-start overflow-visible"
|
|
872
|
+
aria-hidden="true"
|
|
873
|
+
>
|
|
874
|
+
<ExxatProductLogo
|
|
875
|
+
product={current.id}
|
|
876
|
+
variant="mutedSuffix"
|
|
877
|
+
className="w-auto max-w-[min(100%,280px)] object-left object-contain"
|
|
878
|
+
/>
|
|
879
|
+
</span>
|
|
880
|
+
<span
|
|
881
|
+
className="flex w-6 shrink-0 items-center justify-center self-stretch text-muted-foreground"
|
|
882
|
+
aria-hidden="true"
|
|
883
|
+
>
|
|
884
|
+
<i
|
|
885
|
+
className="fa-light fa-chevron-down block text-xs leading-none"
|
|
886
|
+
aria-hidden="true"
|
|
887
|
+
/>
|
|
888
|
+
</span>
|
|
889
|
+
</span>
|
|
890
|
+
)}
|
|
891
|
+
</SidebarMenuButton>
|
|
892
|
+
</DropdownMenuTrigger>
|
|
882
893
|
|
|
883
|
-
<DropdownMenuContent
|
|
894
|
+
<DropdownMenuContent
|
|
895
|
+
align="start"
|
|
896
|
+
side={iconRail ? "right" : "bottom"}
|
|
897
|
+
sideOffset={iconRail ? 8 : 4}
|
|
898
|
+
>
|
|
884
899
|
<DropdownMenuLabel className="text-xs text-muted-foreground">
|
|
885
900
|
Switch product
|
|
886
901
|
</DropdownMenuLabel>
|
|
@@ -1004,6 +1019,7 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
|
|
1004
1019
|
<SidebarGroupLabel
|
|
1005
1020
|
id="sidebar-documents-heading"
|
|
1006
1021
|
className="text-xs font-medium uppercase tracking-wide px-2 text-sidebar-section-label"
|
|
1022
|
+
suppressHydrationWarning
|
|
1007
1023
|
>
|
|
1008
1024
|
{NAV_DOCUMENTS_LABEL}
|
|
1009
1025
|
</SidebarGroupLabel>
|
|
@@ -56,7 +56,8 @@ import {
|
|
|
56
56
|
TooltipTrigger,
|
|
57
57
|
} from "@/components/ui/tooltip"
|
|
58
58
|
import { OPERATOR_LABELS } from "@/components/table-properties/types"
|
|
59
|
-
import type { ActiveFilter
|
|
59
|
+
import type { ActiveFilter } from "@/components/table-properties/types"
|
|
60
|
+
import { getConditionalCellBackground } from "@/lib/conditional-rule-match"
|
|
60
61
|
import { formatYmdForDisplay } from "@/lib/date-filter"
|
|
61
62
|
import { FilterDateCalendar } from "@/components/data-table/filter-date-calendar"
|
|
62
63
|
import { FilterTextValueInput } from "@/components/data-table/filter-text-value-input"
|
|
@@ -81,26 +82,6 @@ function resolvedColumnLabel<TData>(col: ColumnDef<TData>): string {
|
|
|
81
82
|
return defaultColumnHeaderLabel(col.key) ?? col.key
|
|
82
83
|
}
|
|
83
84
|
|
|
84
|
-
function conditionalTextMatches(
|
|
85
|
-
cellVal: string,
|
|
86
|
-
needle: string,
|
|
87
|
-
op: "contains" | "not_contains",
|
|
88
|
-
textMask: FilterTextMask | undefined,
|
|
89
|
-
) {
|
|
90
|
-
const v = cellVal.trim()
|
|
91
|
-
const n = needle.trim()
|
|
92
|
-
if (!n) return op === "not_contains"
|
|
93
|
-
if (textMask === "phone" || textMask === "zip") {
|
|
94
|
-
const nd = n.replace(/\D/g, "")
|
|
95
|
-
const hay = v.replace(/\D/g, "")
|
|
96
|
-
if (!nd) return op === "not_contains"
|
|
97
|
-
const hit = hay.includes(nd)
|
|
98
|
-
return op === "contains" ? hit : !hit
|
|
99
|
-
}
|
|
100
|
-
const hit = v.toLowerCase().includes(n.toLowerCase())
|
|
101
|
-
return op === "contains" ? hit : !hit
|
|
102
|
-
}
|
|
103
|
-
|
|
104
85
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
105
86
|
// Internal sub-components
|
|
106
87
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -802,12 +783,19 @@ function DataTableInner<TData extends Record<string, unknown>>({
|
|
|
802
783
|
setSheetOpen,
|
|
803
784
|
} = state
|
|
804
785
|
|
|
805
|
-
// Mount overflow check
|
|
786
|
+
// Mount overflow check + scrollport width for sticky group headers on horizontal scroll.
|
|
806
787
|
React.useEffect(() => {
|
|
807
|
-
|
|
788
|
+
const syncScrollport = () => {
|
|
789
|
+
const el = scrollRef.current
|
|
790
|
+
if (el) {
|
|
791
|
+
el.style.setProperty("--dt-scrollport-width", `${el.clientWidth}px`)
|
|
792
|
+
}
|
|
793
|
+
checkOverflow()
|
|
794
|
+
}
|
|
795
|
+
syncScrollport()
|
|
808
796
|
const el = scrollRef.current
|
|
809
797
|
if (!el) return
|
|
810
|
-
const ro = new ResizeObserver(
|
|
798
|
+
const ro = new ResizeObserver(syncScrollport)
|
|
811
799
|
ro.observe(el)
|
|
812
800
|
return () => ro.disconnect()
|
|
813
801
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
@@ -1351,18 +1339,19 @@ function DataTableInner<TData extends Record<string, unknown>>({
|
|
|
1351
1339
|
<React.Fragment key={groupKey ?? "__all__"}>
|
|
1352
1340
|
{groupLabel && (
|
|
1353
1341
|
<tr>
|
|
1354
|
-
<td
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1342
|
+
<td colSpan={displayCols.length} className="p-0 border-b border-border bg-dt-group-bg">
|
|
1343
|
+
<div
|
|
1344
|
+
className={cn(
|
|
1345
|
+
"sticky left-0 z-[25] px-4 py-1.5 text-xs font-semibold text-muted-foreground tracking-wide bg-dt-group-bg select-none",
|
|
1346
|
+
!isReflowViewport && "shadow-[4px_0_8px_-4px_var(--sticky-edge-fade)]",
|
|
1347
|
+
)}
|
|
1348
|
+
style={{ width: "var(--dt-scrollport-width, 100%)" }}
|
|
1349
|
+
>
|
|
1350
|
+
{groupLabel}
|
|
1351
|
+
<span className="ml-2 font-normal normal-case opacity-60 tracking-normal">
|
|
1352
|
+
{groupRows.length} record{groupRows.length !== 1 ? "s" : ""}
|
|
1353
|
+
</span>
|
|
1354
|
+
</div>
|
|
1366
1355
|
</td>
|
|
1367
1356
|
</tr>
|
|
1368
1357
|
)}
|
|
@@ -1419,37 +1408,12 @@ function DataTableInner<TData extends Record<string, unknown>>({
|
|
|
1419
1408
|
]
|
|
1420
1409
|
)
|
|
1421
1410
|
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
const textMask =
|
|
1429
|
-
ruleCol?.filter?.type === "text" ? ruleCol.filter.textMask : undefined
|
|
1430
|
-
switch (rule.operator) {
|
|
1431
|
-
case "is":
|
|
1432
|
-
return rule.values.length > 0 && rule.values.includes(v)
|
|
1433
|
-
case "is_not":
|
|
1434
|
-
return rule.values.length > 0 && !rule.values.includes(v)
|
|
1435
|
-
case "contains":
|
|
1436
|
-
return (
|
|
1437
|
-
rule.values.length > 0 &&
|
|
1438
|
-
rule.values.some(val =>
|
|
1439
|
-
conditionalTextMatches(v, val, "contains", textMask),
|
|
1440
|
-
)
|
|
1441
|
-
)
|
|
1442
|
-
case "not_contains":
|
|
1443
|
-
return (
|
|
1444
|
-
rule.values.length > 0 &&
|
|
1445
|
-
!rule.values.some(val =>
|
|
1446
|
-
conditionalTextMatches(v, val, "contains", textMask),
|
|
1447
|
-
)
|
|
1448
|
-
)
|
|
1449
|
-
default:
|
|
1450
|
-
return false
|
|
1451
|
-
}
|
|
1452
|
-
})?.bgColor
|
|
1411
|
+
const conditionalBg = getConditionalCellBackground(
|
|
1412
|
+
row,
|
|
1413
|
+
col.key,
|
|
1414
|
+
conditionalRules,
|
|
1415
|
+
columns,
|
|
1416
|
+
)
|
|
1453
1417
|
|
|
1454
1418
|
const tdStyle = conditionalBg
|
|
1455
1419
|
? { ...cs, background: conditionalBg }
|
|
@@ -112,7 +112,8 @@ export function useTableState<TData extends Record<string, unknown>>(
|
|
|
112
112
|
const addSortRule = React.useCallback((fieldKey: string) => {
|
|
113
113
|
setSortRules(prev => {
|
|
114
114
|
if (prev.some(r => r.fieldKey === fieldKey)) return prev
|
|
115
|
-
|
|
115
|
+
// New drawer sorts are primary (same as column-header sort), not trailing.
|
|
116
|
+
return [{ id: `sort-${Date.now()}`, fieldKey, direction: "asc" }, ...prev]
|
|
116
117
|
})
|
|
117
118
|
}, [setSortRules])
|
|
118
119
|
|
|
@@ -178,9 +179,12 @@ export function useTableState<TData extends Record<string, unknown>>(
|
|
|
178
179
|
}
|
|
179
180
|
return f.operators?.[0] ?? "contains"
|
|
180
181
|
})()
|
|
181
|
-
|
|
182
|
+
const newFilter: ActiveFilter = { id, fieldKey, operator: firstOperator, values: [] }
|
|
183
|
+
setActiveFilters(prev => [...prev, newFilter])
|
|
182
184
|
if (fromDrawer) {
|
|
183
|
-
setDrawerExpandedFilters(new Set([id]))
|
|
185
|
+
setDrawerExpandedFilters(() => new Set([id]))
|
|
186
|
+
// Keep toolbar pills hidden until a value is chosen — avoids mounting every
|
|
187
|
+
// FilterPill (heavy) on each drawer "Add filter" click.
|
|
184
188
|
} else {
|
|
185
189
|
setOpenFilterId(id)
|
|
186
190
|
setFilterBarVisible(true)
|
|
@@ -188,8 +192,24 @@ export function useTableState<TData extends Record<string, unknown>>(
|
|
|
188
192
|
}, [columns, setActiveFilters, setDrawerExpandedFilters, setOpenFilterId, setFilterBarVisible])
|
|
189
193
|
|
|
190
194
|
const updateFilter = React.useCallback((id: string, patch: Partial<ActiveFilter>) => {
|
|
191
|
-
|
|
192
|
-
|
|
195
|
+
let shouldShowFilterBar = false
|
|
196
|
+
setActiveFilters(prev => {
|
|
197
|
+
const next = prev.map(f => {
|
|
198
|
+
if (f.id !== id) return f
|
|
199
|
+
const merged = { ...f, ...patch }
|
|
200
|
+
const col = columns.find(c => c.key === merged.fieldKey)
|
|
201
|
+
if (merged.values.length > 0) {
|
|
202
|
+
shouldShowFilterBar =
|
|
203
|
+
col?.filter?.type === "text"
|
|
204
|
+
? (merged.values[0] ?? "").trim().length > 0
|
|
205
|
+
: true
|
|
206
|
+
}
|
|
207
|
+
return merged
|
|
208
|
+
})
|
|
209
|
+
return next
|
|
210
|
+
})
|
|
211
|
+
if (shouldShowFilterBar) setFilterBarVisible(true)
|
|
212
|
+
}, [columns, setActiveFilters, setFilterBarVisible])
|
|
193
213
|
|
|
194
214
|
const removeFilter = React.useCallback((id: string) => {
|
|
195
215
|
// Use functional updates only — no stale-closure risk on activeFilters.
|
|
@@ -342,7 +362,14 @@ export function useTableState<TData extends Record<string, unknown>>(
|
|
|
342
362
|
result = result.filter(r => getSearchableText(r).includes(q))
|
|
343
363
|
}
|
|
344
364
|
|
|
345
|
-
const activeWithValues = activeFilters.filter(f =>
|
|
365
|
+
const activeWithValues = activeFilters.filter(f => {
|
|
366
|
+
if (f.values.length === 0) return false
|
|
367
|
+
const col = columnsByKey.get(f.fieldKey)
|
|
368
|
+
if (col?.filter?.type === "text") {
|
|
369
|
+
return (f.values[0] ?? "").trim().length > 0
|
|
370
|
+
}
|
|
371
|
+
return true
|
|
372
|
+
})
|
|
346
373
|
if (activeWithValues.length > 0) {
|
|
347
374
|
// Pre-resolve column, operator, normalised needle, and select-value Set
|
|
348
375
|
// for each active filter ONCE (instead of per row).
|
|
@@ -1,15 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Central exports for list-page data surfaces and shared view chrome.
|
|
3
3
|
*
|
|
4
|
-
* **Pattern:** `ListPageTemplate` +
|
|
4
|
+
* **Pattern:** `ListPageTemplate` + hub `*-table.tsx` — one `useTableState`, one toolbar,
|
|
5
5
|
* table | list | board | dashboard from the same component (`AGENTS.md` §4, `docs/data-views-pattern.md`).
|
|
6
6
|
*
|
|
7
7
|
* **View UI:** `ViewSegmentedControl` matches the template’s views toolbar (`bg-muted/60` pills).
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
export { PlacementsTable } from "@/components/placements-table"
|
|
11
|
-
export type { PlacementsTableProps, PlacementsTableHandle } from "@/components/placements-table"
|
|
12
|
-
export type { PlacementLifecycleTabId } from "@/lib/placement-lifecycle"
|
|
13
10
|
export type { DataListViewType } from "@/lib/data-list-view"
|
|
14
11
|
export { DATA_LIST_VIEW_TILES, dataListViewIcon, dataListViewLabel } from "@/lib/data-list-view"
|
|
15
12
|
|
|
@@ -39,6 +36,7 @@ export {
|
|
|
39
36
|
export {
|
|
40
37
|
ListPageSplitHubChrome,
|
|
41
38
|
LIST_PAGE_SPLIT_HUB_HEIGHT_STYLE,
|
|
39
|
+
LIST_PAGE_CALENDAR_HEIGHT_STYLE,
|
|
42
40
|
type ListPageSplitHubChromeProps,
|
|
43
41
|
} from "@/components/data-views/list-page-split-hub-chrome"
|
|
44
42
|
|
|
@@ -63,8 +61,7 @@ export {
|
|
|
63
61
|
type OutlineTreeSurface,
|
|
64
62
|
} from "@/components/data-views/outline-tree-menu"
|
|
65
63
|
|
|
66
|
-
|
|
67
|
-
export type { QuestionBankFolderTreeBranchProps } from "@/components/data-views/question-bank-folder-tree-branch"
|
|
64
|
+
/** Question-bank nav only — import from `@/components/data-views/question-bank-folder-tree-branch`. */
|
|
68
65
|
|
|
69
66
|
export {
|
|
70
67
|
LIST_PAGE_SPLIT_MILLER_COLUMN_PANEL_CLASS,
|
|
@@ -83,10 +80,12 @@ export {
|
|
|
83
80
|
type FolderDetailsShellProps,
|
|
84
81
|
} from "@/components/folder-details-shell"
|
|
85
82
|
|
|
83
|
+
/** Hub-specific tree+inspector — import from `@/components/hub-tree-panel-view` (not generic DS). */
|
|
84
|
+
|
|
86
85
|
export {
|
|
87
|
-
|
|
88
|
-
type
|
|
89
|
-
} from "@/components/
|
|
86
|
+
ListPageFolderColumnsPanel,
|
|
87
|
+
type ListPageFolderColumnsPanelProps,
|
|
88
|
+
} from "@/components/data-views/list-page-folder-columns-panel"
|
|
90
89
|
|
|
91
90
|
export {
|
|
92
91
|
ListPageTreePanelShell,
|
|
@@ -103,6 +102,35 @@ export {
|
|
|
103
102
|
/** Generic folder icon-grid — reusable across all list hubs. */
|
|
104
103
|
export { FolderGridView, type FolderGridViewProps } from "@/components/data-views/folder-grid-view"
|
|
105
104
|
|
|
105
|
+
/** Month calendar — same `tableState.rows` as table / list / board. */
|
|
106
|
+
export {
|
|
107
|
+
ListPageCalendarView,
|
|
108
|
+
type ListPageCalendarViewProps,
|
|
109
|
+
} from "@/components/data-views/list-page-calendar-view"
|
|
110
|
+
|
|
111
|
+
/** Hub view router — switch on `DataListViewRenderKind`; missing renderer = explicit empty state. */
|
|
112
|
+
export {
|
|
113
|
+
ListPageConnectedViewBody,
|
|
114
|
+
ListPageViewNotConfigured,
|
|
115
|
+
type ListPageConnectedViewBodyProps,
|
|
116
|
+
type ListPageConnectedViewRenderers,
|
|
117
|
+
} from "@/components/data-views/list-page-connected-view-body"
|
|
118
|
+
|
|
119
|
+
export {
|
|
120
|
+
DATA_LIST_VIEW_REGISTRY,
|
|
121
|
+
dataListViewDefinition,
|
|
122
|
+
dataListViewTilesForHub,
|
|
123
|
+
showsListPageHubMetricsStrip,
|
|
124
|
+
isDataListViewTypeSupported,
|
|
125
|
+
} from "@/lib/data-list-view-registry"
|
|
126
|
+
|
|
127
|
+
export {
|
|
128
|
+
defineHubViewRenderers,
|
|
129
|
+
hubRenderKindsForSupported,
|
|
130
|
+
type HubConnectedViewRenderers,
|
|
131
|
+
type HubRenderKindForViews,
|
|
132
|
+
} from "@/lib/hub-connected-view-renderers"
|
|
133
|
+
|
|
106
134
|
/** Generic vertical row list — used by every hub's "list" tab. Composes
|
|
107
135
|
* `ListPageBoardCard layout="row"` via a `renderRow` prop. */
|
|
108
136
|
export { DataRowList, type DataRowListProps } from "@/components/data-views/data-row-list"
|