@exxatdesignux/ui 0.2.9 → 0.2.11

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.
Files changed (126) hide show
  1. package/consumer-extras/cursor-skills/exxat-card-vs-list-rows/SKILL.md +20 -0
  2. package/consumer-extras/cursor-skills/exxat-collaboration-access/SKILL.md +33 -0
  3. package/consumer-extras/cursor-skills/exxat-dedicated-search-surfaces/SKILL.md +45 -0
  4. package/consumer-extras/cursor-skills/exxat-drawer-vs-dialog/SKILL.md +20 -0
  5. package/consumer-extras/cursor-skills/exxat-ds-skill/SKILL.md +31 -5
  6. package/consumer-extras/cursor-skills/exxat-kpi-max-four/SKILL.md +19 -0
  7. package/consumer-extras/cursor-skills/exxat-kpi-trends/SKILL.md +27 -0
  8. package/consumer-extras/cursor-skills/exxat-primary-nav-secondary-panel/SKILL.md +1 -0
  9. package/consumer-extras/patterns/collaboration-access-pattern.md +114 -0
  10. package/consumer-extras/patterns/data-views-pattern.md +12 -4
  11. package/package.json +4 -1
  12. package/src/components/ui/banner.tsx +20 -7
  13. package/src/components/ui/date-picker-field.tsx +3 -3
  14. package/src/components/ui/dropdown-menu.tsx +17 -6
  15. package/src/components/ui/input-group.tsx +1 -1
  16. package/src/components/ui/input.tsx +1 -1
  17. package/src/components/ui/select.tsx +1 -1
  18. package/src/components/ui/separator.tsx +2 -2
  19. package/src/components/ui/sidebar.tsx +31 -3
  20. package/src/components/ui/textarea.tsx +1 -1
  21. package/src/globals.css +0 -1
  22. package/src/index.ts +1 -0
  23. package/src/lib/date-filter.ts +13 -4
  24. package/src/lib/dropdown-menu-surface.ts +13 -0
  25. package/template/.claude/skills/exxat-ds-skill/SKILL.md +27 -9
  26. package/template/.cursor/rules/exxat-data-tables.mdc +1 -0
  27. package/template/.nvmrc +1 -1
  28. package/template/AGENTS.md +82 -27
  29. package/template/app/(app)/examples/page.tsx +2 -1
  30. package/template/app/(app)/help/page.tsx +6 -0
  31. package/template/app/(app)/layout.tsx +7 -4
  32. package/template/app/(app)/question-bank/find/page.tsx +12 -0
  33. package/template/app/(app)/question-bank/layout.tsx +46 -0
  34. package/template/app/(app)/question-bank/library/page.tsx +11 -0
  35. package/template/app/(app)/question-bank/list/page.tsx +12 -0
  36. package/template/app/(app)/question-bank/page.tsx +4 -3
  37. package/template/app/globals.css +1 -2
  38. package/template/components/app-sidebar.tsx +51 -13
  39. package/template/components/ask-leo-composer.tsx +173 -45
  40. package/template/components/ask-leo-sidebar.tsx +9 -1
  41. package/template/components/chart-area-interactive.tsx +3 -13
  42. package/template/components/charts-overview.tsx +33 -6
  43. package/template/components/collaboration-access-flow.tsx +144 -0
  44. package/template/components/compliance-page-header.tsx +1 -1
  45. package/template/components/compliance-table.tsx +2 -2
  46. package/template/components/dashboard-tabs.tsx +4 -3
  47. package/template/components/data-list-table-cells.tsx +1 -1
  48. package/template/components/data-list-table.tsx +1 -1
  49. package/template/components/data-table/index.tsx +5 -5
  50. package/template/components/data-table/use-table-state.ts +18 -2
  51. package/template/components/data-view-dashboard-charts-compliance.tsx +8 -5
  52. package/template/components/data-view-dashboard-charts-team.tsx +8 -5
  53. package/template/components/data-view-dashboard-charts.tsx +62 -227
  54. package/template/components/dedicated-search-recents.tsx +96 -0
  55. package/template/components/dedicated-search-url-composer.tsx +112 -0
  56. package/template/components/getting-started.tsx +1 -1
  57. package/template/components/hub-tree-panel-view.tsx +10 -26
  58. package/template/components/invite-collaborators-drawer.tsx +453 -0
  59. package/template/components/key-metrics.tsx +54 -8
  60. package/template/components/nav-documents.tsx +1 -1
  61. package/template/components/new-placement-form.tsx +3 -3
  62. package/template/components/page-header.tsx +76 -59
  63. package/template/components/placements-board-view.tsx +3 -3
  64. package/template/components/placements-page-header.tsx +1 -1
  65. package/template/components/placements-table-columns.tsx +3 -2
  66. package/template/components/product-switcher.tsx +0 -1
  67. package/template/components/question-bank-board-view.tsx +35 -47
  68. package/template/components/question-bank-client.tsx +293 -81
  69. package/template/components/question-bank-dashboard-charts.tsx +174 -0
  70. package/template/components/question-bank-favorite-button.tsx +46 -0
  71. package/template/components/question-bank-hub-client.tsx +436 -0
  72. package/template/components/question-bank-list-view.tsx +26 -19
  73. package/template/components/question-bank-new-folder-sheet.tsx +56 -42
  74. package/template/components/question-bank-os-folder-view.tsx +3 -14
  75. package/template/components/question-bank-page-header.tsx +85 -53
  76. package/template/components/question-bank-panel-activator.tsx +3 -4
  77. package/template/components/question-bank-secondary-nav.tsx +523 -65
  78. package/template/components/question-bank-table.tsx +125 -343
  79. package/template/components/secondary-panel.tsx +130 -63
  80. package/template/components/settings-client.tsx +3 -1
  81. package/template/components/sidebar-shell.tsx +2 -0
  82. package/template/components/sites-all-client.tsx +1 -1
  83. package/template/components/sites-table.tsx +1 -1
  84. package/template/components/system-banner-slot.tsx +2 -1
  85. package/template/components/table-properties/drawer.tsx +3 -3
  86. package/template/components/table-properties/sort-card.tsx +1 -1
  87. package/template/components/team-page-header.tsx +1 -1
  88. package/template/components/team-table.tsx +8 -4
  89. package/template/components/templates/dedicated-search-landing-template.tsx +58 -0
  90. package/template/components/templates/dedicated-search-results-template.tsx +19 -0
  91. package/template/components/templates/discovery-hub-template.tsx +273 -0
  92. package/template/components/templates/list-page.tsx +11 -4
  93. package/template/components/templates/nested-secondary-panel-shell.tsx +57 -0
  94. package/template/components/templates/secondary-panel-hub-template.tsx +54 -0
  95. package/template/docs/card-vs-rows-pattern.md +36 -0
  96. package/template/docs/collaboration-access-pattern.md +114 -0
  97. package/template/docs/data-views-pattern.md +12 -4
  98. package/template/docs/drawer-vs-dialog-pattern.md +50 -0
  99. package/template/docs/kpi-strip-max-four-pattern.md +29 -0
  100. package/template/docs/kpi-trend-pattern.md +43 -0
  101. package/template/fontawesome-subset.manifest.json +2 -2
  102. package/template/hooks/use-location-hash.ts +14 -8
  103. package/template/hooks/use-secondary-panel-hub-nav.ts +98 -0
  104. package/template/lib/ask-leo-route-context.ts +24 -0
  105. package/template/lib/collaborator-access.ts +92 -0
  106. package/template/lib/command-menu-config.ts +8 -1
  107. package/template/lib/command-menu-search-data.ts +11 -8
  108. package/template/lib/data-list-display-options.ts +1 -1
  109. package/template/lib/data-view-dashboard-placements-layout.ts +215 -0
  110. package/template/lib/date-filter.ts +1 -0
  111. package/template/lib/dedicated-search-recents.ts +76 -0
  112. package/template/lib/dedicated-search-url.ts +23 -0
  113. package/template/lib/discovery-hub.ts +15 -0
  114. package/template/lib/list-status-badges.ts +1 -21
  115. package/template/lib/mock/navigation.tsx +4 -2
  116. package/template/lib/mock/placements.ts +9 -9
  117. package/template/lib/mock/question-bank-folders.ts +7 -0
  118. package/template/lib/mock/question-bank-header-collaborators.ts +45 -5
  119. package/template/lib/mock/question-bank-inspector.ts +1 -2
  120. package/template/lib/mock/question-bank-kpi.ts +38 -26
  121. package/template/lib/mock/question-bank.ts +43 -16
  122. package/template/lib/question-bank-dedicated-search.ts +19 -0
  123. package/template/lib/question-bank-hub-search.ts +90 -0
  124. package/template/lib/question-bank-nav.ts +322 -6
  125. package/template/lib/question-bank-recent-searches.ts +22 -0
  126. package/template/package.json +1 -2
@@ -0,0 +1,20 @@
1
+ # Exxat DS — cards vs rows vs lists
2
+
3
+ Use when choosing **card grids**, **`DataTable`**, or **simple list rows** for a surface.
4
+
5
+ ## Read first
6
+
7
+ - **`docs/card-vs-rows-pattern.md`**
8
+ - **`.cursor/rules/exxat-card-vs-list-rows.mdc`**, **`exxat-data-tables.mdc`**, **`exxat-board-cards.mdc`**
9
+ - **`exxat-centralized-list-dataset.mdc`** — one `tableState.rows` for table + board + cards
10
+
11
+ ## Checklist
12
+
13
+ 1. **10+ homogeneous records + compare/sort/filter?** → **`DataTable`** + hub template.
14
+ 2. **Kanban / visual tiles / folders?** → **`ListPageBoardCard`** or **`ListPageViewFrame`** + card primitives.
15
+ 3. **Medium vertical list without full table chrome?** → List row pattern; **promote** to `DataTable` when density and parity demand it.
16
+
17
+ ## MUST NOT
18
+
19
+ - Card wall for the **primary** sortable hub that other hubs implement as **`DataTable`** without product reason.
20
+ - Second mock row source for cards vs table.
@@ -0,0 +1,33 @@
1
+ ---
2
+ name: exxat-collaboration-access
3
+ description: Shared hub collaboration — PageHeader face rail, InviteCollaboratorsDrawer, library access vs directory role tags. Use when adding invite people, access roster, or collaboration variant headers.
4
+ user-invocable: true
5
+ ---
6
+
7
+ # Exxat DS — collaboration & access
8
+
9
+ **Handbook:** `apps/web/AGENTS.md` §4.7
10
+ **Narrative:** `apps/web/docs/collaboration-access-pattern.md`
11
+ **Cursor rule:** `.cursor/rules/exxat-collaboration-access.mdc`
12
+
13
+ ## Wiring checklist
14
+
15
+ 1. **`PageHeaderCollaborator`** — `name`, `email`, `access`, optional `roles[]`, avatar fields (`components/page-header.tsx`).
16
+ 2. **Mock/API** — `lib/mock/<entity>-header-collaborators.ts`; one shape for header + invite sheet.
17
+ 3. **Entity header** — `variant="collaboration"`; **Invite people** first in **⋯ More**; pass **`onAddCollaborator`** and **`onCollaboratorsOpen`**.
18
+ 4. **Hub client** — **`CollaborationAccessFlow`** (preferred) or `collaborators` + `inviteOpen` + **`InviteCollaboratorsDrawer`**; on invite, append to **`collaborators`**.
19
+ 5. **Access maps** — `lib/collaborator-access.ts` for Owner / Editor / Commenter / Viewer, invite options, and **`COLLABORATION_HEADER_ADD_LABEL`**.
20
+ 6. **Header** — empty roster → outline **Add collaborator**; non-empty → face rail; both open the invite sheet.
21
+ 7. **Invite sheet** — `InviteCollaboratorsDrawer`: export-style **`Sheet`**, combined email + access menu, grouped roster (name → email → role tags → access badge).
22
+
23
+ ## MUST NOT
24
+
25
+ - A second invite control beside a populated face rail.
26
+ - Per-hub access label forks or parallel collaborator types.
27
+ - Toast/snackbar for invite outcomes (**`exxat-no-toast.mdc`**).
28
+ - **`Select`** in **`InputGroupAddon`** for access inside the sheet.
29
+
30
+ ## Reference
31
+
32
+ - `components/collaboration-access-flow.tsx`, `components/question-bank-page-header.tsx`, `components/question-bank-client.tsx`
33
+ - `components/invite-collaborators-drawer.tsx`, `components/export-drawer.tsx`
@@ -0,0 +1,45 @@
1
+ ---
2
+ name: exxat-dedicated-search-surfaces
3
+ description: >-
4
+ Dedicated search landing vs results split, URL composer, recents storage, and templates
5
+ (generic DedicatedSearch*). Use when adding a route pair like empty ?q= landing + ListPageTemplate results,
6
+ or wiring localStorage recents without hydration mismatches.
7
+ ---
8
+
9
+ # Dedicated search surfaces (Exxat DS)
10
+
11
+ ## When to use
12
+
13
+ - A hub exposes **two URL states** on the same feature: **landing** (no primary query / empty `?q=`) vs **results** (non-empty query drives `ListPageTemplate` / `DataTable`).
14
+ - You need an **AI-style composer** that **updates the URL** with `router.replace` and **does not** open Ask Leo.
15
+ - You need **recent searches** persisted in `localStorage` **without** SSR/client markup drift.
16
+
17
+ ## MUST
18
+
19
+ 1. **Hydration-safe recents** — **MUST NOT** read `localStorage` in `useState` initializers. Initial client paint **MUST** match the server (`[]` then `useEffect` sync). See **`DedicatedSearchRecents`**.
20
+ 2. **Generic names** — Reusable pieces live under **`DedicatedSearch*`** (`components/dedicated-search-*.tsx`, **`components/templates/dedicated-search-*-template.tsx`**, **`lib/dedicated-search-*.ts`**). Hub code passes **`patchSearchParams`**, **`recents` controller**, copy, and entity table — **MUST NOT** fork parallel “question search landing” components for a second hub.
21
+ 3. **Landing shell** — Use **`DedicatedSearchLandingTemplate`** (`ListPageViewFrame` + title + composer slot + optional trailing).
22
+ 4. **Results chrome** — Reuse **`DEDICATED_SEARCH_RESULTS_OUTER_CONTENT_CLASSNAME`** on the hub content wrapper and **`DedicatedSearchResultsHeaderChrome`** around page header + composer strip.
23
+ 5. **Namespaces recents** — Use **`createDedicatedSearchRecentsController(namespace, legacy?)`** from **`lib/dedicated-search-recents.ts`**. When replacing an existing storage key, pass **`legacy: { storageKey, eventName }`** so users do not lose saved rows.
24
+
25
+ ## MUST NOT
26
+
27
+ - Gate render on **`typeof window`** for the **first** paint of recents or landing bodies.
28
+ - Duplicate the landing vertical rhythm (`mt-*` between title, composer, recents) outside the template without updating the template for product-wide consistency.
29
+
30
+ ## References (apps/web)
31
+
32
+ | Piece | Path |
33
+ |-------|------|
34
+ | Landing template | `components/templates/dedicated-search-landing-template.tsx` |
35
+ | Results chrome | `components/templates/dedicated-search-results-template.tsx` |
36
+ | URL composer | `components/dedicated-search-url-composer.tsx` |
37
+ | Recents list | `components/dedicated-search-recents.tsx` |
38
+ | Recents storage factory | `lib/dedicated-search-recents.ts` |
39
+ | Optional default `q` patcher | `lib/dedicated-search-url.ts` |
40
+ | Question bank adapter (placeholders + patch) | `lib/question-bank-dedicated-search.ts` |
41
+ | Question bank wiring | `components/question-bank-client.tsx` |
42
+
43
+ ## Cursor rule
44
+
45
+ - **`.cursor/rules/exxat-dedicated-search-surfaces.mdc`**
@@ -0,0 +1,20 @@
1
+ # Exxat DS — drawer vs dialog
2
+
3
+ Use when choosing **Radix `Dialog` / `AlertDialog`** vs **`Sheet` / `Drawer`** vs **route** for a flow.
4
+
5
+ ## Read first
6
+
7
+ - **`docs/drawer-vs-dialog-pattern.md`**
8
+ - **`AGENTS.md` §6.4** + **`docs/data-views-pattern.md`** (page vs drawer)
9
+ - **`.cursor/rules/exxat-drawer-vs-dialog.mdc`**, **`exxat-page-vs-drawer.mdc`**
10
+
11
+ ## Checklist
12
+
13
+ 1. **Must the user see the hub while acting?** Yes → **drawer/sheet** (properties, export, invite). No and short → **dialog**. Long / own URL → **route**.
14
+ 2. **Destructive confirm?** Prefer **dialog** (`AlertDialog`) unless the product explicitly keeps context in a drawer with the same safeguards.
15
+ 3. **Title + focus** — Dialog/drawer/sheet all need an accessible **title**; restore focus to trigger on close.
16
+
17
+ ## Repo references
18
+
19
+ - `TablePropertiesDrawer`, `ExportDrawer`, `InviteCollaboratorsDrawer` — drawer-class.
20
+ - Delete / irreversible — dialog pattern, not toast (**`exxat-no-toast.mdc`**).
@@ -21,7 +21,7 @@ description: >
21
21
  - **Stack:** Next.js 16 (App Router), React, TypeScript, Tailwind CSS, shadcn/ui primitives, Font Awesome icons
22
22
  - **App root:** `exxat-ds/app/(app)/` — route group that wraps all authenticated pages
23
23
  - **Single source of truth:** `exxat-ds/AGENTS.md` for full prose explanations; this skill is the actionable summary
24
- - **Companion skills (narrow topics):** `exxat-fontawesome-icons`, `exxat-primary-nav-secondary-panel`, `exxat-centralized-list-dataset`, `exxat-list-page-view-shells`, `exxat-accessibility`, `exxat-board-cards` — live under `.cursor/skills/`; vetted copies ship with **`@exxatdesignux/ui`** in `consumer-extras/cursor-skills/` after **`pnpm --filter @exxatdesignux/ui vendor:consumer-extras`**.
24
+ - **Companion skills (narrow topics):** `exxat-fontawesome-icons`, `exxat-primary-nav-secondary-panel`, `exxat-centralized-list-dataset`, `exxat-list-page-view-shells`, `exxat-dedicated-search-surfaces`, `exxat-accessibility`, `exxat-board-cards`, `exxat-collaboration-access` — live under `.cursor/skills/`; vetted copies ship with **`@exxatdesignux/ui`** in `consumer-extras/cursor-skills/` after **`pnpm --filter @exxatdesignux/ui vendor:consumer-extras`**.
25
25
 
26
26
  ---
27
27
 
@@ -88,7 +88,7 @@ To add a primary nav item, append to `NAV_PRIMARY`:
88
88
  | Concern | Pattern |
89
89
  |--------|---------|
90
90
  | **Product (One / Prism)** | **`ExxatProductLogo`** (`components/exxat-product-logo.tsx`) for the header control and **`ProductSwitcher`** — **not** logo.dev rasters unless product explicitly changes that. |
91
- | **School/program menu width** | **`DropdownMenuContent`** ships with **`w-(--radix-dropdown-menu-trigger-width)`**, so the panel matches the **narrow sidebar trigger** and long names wrap too early. For **`TeamSwitcher`**, override with e.g. **`!w-max min-w-72 max-w-[min(100vw-2rem,28rem)]`**. |
91
+ | **School/program menu width** | **`DropdownMenuContent`** defaults to **intrinsic width** (**`min-w-52 w-max max-w-[min(24rem,calc(100vw-2rem))]`** via **`DROPDOWN_MENU_CONTENT_SURFACE_CLASS`** in **`@exxatdesignux/ui/lib/dropdown-menu-surface`**) pure CSS, no **`ResizeObserver`**. The **school / program** switcher still uses an explicit wider surface (**`!w-max min-w-72 max-w-[min(100vw-2rem,28rem)]`**) so dense rows stay readable. |
92
92
  | **School/program copy** | **Do not truncate** school or program names in the switcher; wrap (**`break-words`**, **`whitespace-normal`**, **`items-start`** on multi-line rows). The selected-school summary shows **school name + current program**. |
93
93
  | **Team switcher trigger** | **`SidebarMenuButton` `size="lg"`** uses **`h-12`** + **`overflow-hidden`**, which **clips** a second line (program). When the sidebar is **expanded** or **mobile**, add **`h-auto min-h-12`** and **`overflow-x-clip overflow-y-visible`**. On **icon rail**, hide label rows with **`group-data-[collapsible=icon]:hidden`** (tooltip still exposes the full string). Icon mode defaults **`size-8` + `p-2`** (~16px inner) **clips** school logos — override **`!size-9`**, **`!p-0`**, **`overflow-visible`**. Omit header **chevrons** next to logos if they look like stray chrome. |
94
94
  | **Motion / Animate UI** | [Animate UI](https://animate-ui.com/docs) — open **copy-first** animated components (Motion + Tailwind). This repo uses **`motion/react`** + **`lib/motion-ui.ts`** presets; pull more animations from their registry into `components/` when needed. |
@@ -130,9 +130,10 @@ ListPageTemplate
130
130
  ### Page vs drawer (actions)
131
131
 
132
132
  - **Drawer / sheet** — Use when the user needs **the current page behind them** *and* a **quick view**, **quick actions**, or a **short step** (e.g. properties, export, glance at a row).
133
+ - **Dialog** — **Blocking** confirm/alert/short choice — **`docs/drawer-vs-dialog-pattern.md`**, **`.cursor/rules/exxat-drawer-vs-dialog.mdc`**.
133
134
  - **New page** — Use **otherwise**: **primary**, **long-form**, **multi-step**, or flows that need their **own URL** without the hub visible.
134
135
 
135
- Align with **`exxat-ds/AGENTS.md` §6.4**, **`docs/data-views-pattern.md`**, **`.cursor/rules/exxat-page-vs-drawer.mdc`**.
136
+ Align with **`exxat-ds/AGENTS.md` §6.4**, **`docs/data-views-pattern.md`**, **`docs/drawer-vs-dialog-pattern.md`**, **`.cursor/rules/exxat-page-vs-drawer.mdc`**, **`.cursor/rules/exxat-drawer-vs-dialog.mdc`**.
136
137
 
137
138
  ---
138
139
 
@@ -179,6 +180,25 @@ Align with **`exxat-ds/AGENTS.md` §6.4**, **`docs/data-views-pattern.md`**, **`
179
180
  - `select` column: `defaultPin: "left"`, `lockPin: true`
180
181
  - `actions` column: `defaultPin: "right"`, `lockPin: true`
181
182
 
183
+ ### 5.1 Data table and view-toolbar menus
184
+
185
+ **`DropdownMenuContent`** (from **`@/components/ui/dropdown-menu`**, backed by **`@exxatdesignux/ui`**) applies a **default surface** so **view settings**, **Add view**, **row ⋯**, **column ⋯**, and **filter field** menus get **`min-w-52`**, grow with **`w-max`**, and cap at **`max-w-[min(24rem,calc(100vw-2rem))]`** — all **static Tailwind** (no **`ResizeObserver`** / layout measurement).
186
+
187
+ - **Override** only when the UX needs a fixed rail (e.g. **`className="w-20"`** on the pagination page-size menu, **`w-(--radix-dropdown-menu-trigger-width) min-w-60`** on **`NavUser`**, **`!w-max min-w-72 …`** on the school/program switcher).
188
+ - **Reuse** **`DROPDOWN_MENU_CONTENT_SURFACE_CLASS`** if you build a custom menu primitive that does not wrap **`DropdownMenuContent`**.
189
+
190
+ ### 5.2 KPI trends (`KeyMetrics`, `*-kpi.ts`)
191
+
192
+ **`MetricItem.trend`** must match the **signed change** (arrow direction = truth). **`trendPolarity`** (`higher_is_better` default, **`lower_is_better`**, **`informational`**) controls **tints** and **`aria-label`** — e.g. **low PBI / review flags** rising → `trend: "up"` + **`lower_is_better`** → unfavourable (red), not green. **Doc:** **`docs/kpi-trend-pattern.md`** · **Rule:** **`.cursor/rules/exxat-kpi-trends.mdc`** · **Skill:** **`.cursor/skills/exxat-kpi-trends/SKILL.md`**.
193
+
194
+ ### 5.3 KPI count (max four)
195
+
196
+ **`ListPageTemplate`** metrics and **Data tab** key-metrics cards: **≤ 4** `MetricItem` — **`docs/kpi-strip-max-four-pattern.md`**, **`lib/dashboard-layout-merge.ts`**, **`.cursor/rules/exxat-kpi-max-four.mdc`**, **`.cursor/skills/exxat-kpi-max-four/SKILL.md`**.
197
+
198
+ ### 5.4 Cards vs table rows
199
+
200
+ Dense comparable hub → **`DataTable`**. Boards / folders / visual browse → **`ListPageBoardCard`** + **`ListPageViewFrame`**. **Doc:** **`docs/card-vs-rows-pattern.md`** · **Rule:** **`.cursor/rules/exxat-card-vs-list-rows.mdc`**.
201
+
182
202
  **DataTable must wrap in `<div className="pb-6">`.**
183
203
 
184
204
  ---
@@ -207,7 +227,7 @@ Use `PageHeader` from `@/components/page-header` for the content-area header (be
207
227
  </Button>
208
228
  </DropdownMenuTrigger>
209
229
  </Tip>
210
- <DropdownMenuContent align="end" className="w-52">
230
+ <DropdownMenuContent align="end">
211
231
  <DropdownMenuItem onClick={onExport}>
212
232
  <i className="fa-light fa-arrow-down-to-line" aria-hidden="true" />
213
233
  Export
@@ -230,6 +250,12 @@ Use `PageHeader` from `@/components/page-header` for the content-area header (be
230
250
  - Subtitle: `"{count} items · Last updated now"` format
231
251
  - Title uses Ivy Presto (`font-heading` variable) — applied automatically by `PageHeader`
232
252
 
253
+ ### 6.1 Collaboration & access (shared hubs)
254
+
255
+ When a hub is **shared**, use **`PageHeader` `variant="collaboration"`**: **empty roster** → outline **Add collaborator**; **non-empty** → face rail (faces / **`+N`** open the invite sheet). **Invite people** also lives under the entity header **⋯ More** and opens **`InviteCollaboratorsDrawer`** via **`CollaborationAccessFlow`** when possible. Library access (Owner / Editor / Commenter / Viewer) comes from **`lib/collaborator-access.ts`**; directory tags (Faculty, Program coordinator, Director) use **`PageHeaderCollaborator.roles`**.
256
+
257
+ **Handbook:** `apps/web/AGENTS.md` §4.7 · **Doc:** `docs/collaboration-access-pattern.md` · **Skill:** `.cursor/skills/exxat-collaboration-access/SKILL.md` · **Reference:** Question bank header + client.
258
+
233
259
  ---
234
260
 
235
261
  ## 7. Navigation: Breadcrumbs vs Back Link
@@ -281,7 +307,7 @@ Never install new packages or create parallel components. Always use what exists
281
307
  | Color | CSS design tokens only — no hardcoded hex/rgb |
282
308
  | Minimum font size | **`text-xs`** (11px at 16px root via `--text-xs`) or larger — never arbitrary classes below 11px (`AGENTS.md` §8.3) |
283
309
 
284
- Before adding any component: search `components/ui/` first. Add a prop/variant to an existing component rather than creating a parallel one.
310
+ Before adding any component: search `components/ui/` first. Add a prop/variant to an existing component rather than creating a parallel one. **If nothing fits** and you need a **new shared primitive or large bespoke widget**, **ask the user** before new files — **`.cursor/rules/exxat-reuse-before-custom.mdc`** (unless the task already approved greenfield).
285
311
 
286
312
  ---
287
313
 
@@ -0,0 +1,19 @@
1
+ # Exxat DS — KPI strip (max four)
2
+
3
+ Use when building **`KeyMetrics`** on **`ListPageTemplate`** or **Data tab** key-metrics cards.
4
+
5
+ ## Read first
6
+
7
+ - **`docs/kpi-strip-max-four-pattern.md`**
8
+ - **`lib/dashboard-layout-merge.ts`** — `KEY_METRICS_KPI_COUNT_MAX`, `clampKeyMetricsKpiCount`
9
+ - **`.cursor/rules/exxat-kpi-max-four.mdc`**
10
+
11
+ ## Checklist
12
+
13
+ 1. **`entityKpiMetrics`** returns **at most four** `MetricItem` for those surfaces (or `.slice(0, 4)` after priority sort).
14
+ 2. **Extra metrics** → `MetricInsight` description, a chart, or another card — not a fifth tile.
15
+ 3. **Dashboard persistence** — Respect clamped `keyMetricsKpiCount` (1–4); never bump max without design approval.
16
+
17
+ ## Related
18
+
19
+ - **`docs/kpi-trend-pattern.md`** — deltas and `trendPolarity`.
@@ -0,0 +1,27 @@
1
+ # Exxat DS — KPI trend arrows and polarity
2
+
3
+ Use when adding or reviewing **`KeyMetrics`**, **`lib/mock/*-kpi.ts`** helpers, or **`ChartCard`** **`miniMetrics`** / **`kpi-chart`** trends.
4
+
5
+ ## Authoritative references
6
+
7
+ - **`apps/web/docs/kpi-trend-pattern.md`** — product table + psychometrics example (PBI).
8
+ - **`.cursor/rules/exxat-kpi-trends.mdc`** — binding MUST/MUST NOT.
9
+ - **`apps/web/components/key-metrics.tsx`** — `MetricTrendPolarity`, `metricTrendTone`, `metricTrendAriaQualifier`, `MetricCell` rendering.
10
+
11
+ ## Checklist (new or changed KPI)
12
+
13
+ 1. **Delta honest?** `trend` follows the sign of the change vs the comparison period.
14
+ 2. **Polarity correct?** If “more” is bad → **`trendPolarity: "lower_is_better"`**. If no value judgment → **`"informational"`** (muted tints).
15
+ 3. **Copy contextual?** Label + value + delta tell users *what* moved, not only *how much*.
16
+ 4. **Screen readers** — Chip keeps icon + visible delta; `aria-label` reflects favorable / unfavorable wording automatically when polarity is set.
17
+ 5. **Chart cards** — When using **`ChartCard`** `miniMetrics`, pass **`trendPolarity`** the same way as on **`MetricItem`**.
18
+
19
+ ## Quick examples
20
+
21
+ | Metric | More is… | `trendPolarity` |
22
+ | --- | --- | --- |
23
+ | Total placements | Better (capacity) | default / `higher_is_better` |
24
+ | Pass rate | Better | `higher_is_better` |
25
+ | Open incidents | Worse | `lower_is_better` |
26
+ | Low PBI / review flags | Worse | `lower_is_better` |
27
+ | Items by type (mix %) | Neutral | `informational` |
@@ -15,6 +15,7 @@ user-invocable: true
15
15
  2. **`components/secondary-panel.tsx`** — add **`PANELS["<id>"]`** → panel shell (title, optional search) + secondary nav component.
16
16
  3. **Hub client** — mount **`*PanelActivator`** with **`useAutoPanel("<id>")`** (same id) for the lifetime of the route (e.g. `QuestionBankPanelActivator`).
17
17
  4. **Data** — keep **one** **`useTableState`** / **`tableState.rows`**; drive scope from **URL** + small helpers (see **`lib/question-bank-nav.ts`**).
18
+ 5. **Collapse control** — the nested rail header uses **`collapseActiveSecondaryPanel()`** (angles-left icon), not “close”, so the panel stays dismissed until **`openPanel`** runs again (nav, scope hook, or hub re-entry). Layout effects that auto-call **`openPanel`** must respect **`secondaryPanelAutoReopenSuppressed`** (see **`app/(app)/question-bank/layout.tsx`** + **`SecondaryPanelProvider`**).
18
19
 
19
20
  ## MUST NOT
20
21
 
@@ -0,0 +1,114 @@
1
+ # Collaboration & access pattern
2
+
3
+ Shared UI for **who can access a hub** (face stack in the header) and **inviting people** (floating sheet). **Reference:** Question bank — `QuestionBankPageHeader`, `QuestionBankClient`, `InviteCollaboratorsDrawer`.
4
+
5
+ ## When to use
6
+
7
+ - A list hub or library is **shared** across people (not a private directory).
8
+ - Users need to see **who has access**, **invite by email**, and assign **library access** (Owner / Editor / Commenter / Viewer).
9
+ - The hub already uses **`ListPageTemplate`** + **`PageHeader`** (or an entity header built on it).
10
+
11
+ **Do not** use this for org-wide **role** administration (Faculty, Program coordinator, Director) as the only story — those are **directory role tags** on people, not library access.
12
+
13
+ ## Vocabulary
14
+
15
+ | Concept | Meaning | Source |
16
+ |--------|---------|--------|
17
+ | **Library access** | What someone can do **in this hub** (Owner, Editor, Commenter, Viewer) | `lib/collaborator-access.ts` |
18
+ | **Directory roles** | Org/job tags on a person (Faculty, Program coordinator, Director) | `PageHeaderCollaborator.roles` |
19
+ | **Face rail** | Overlapping avatars in the header when the roster is non-empty | `PageHeader` `variant="collaboration"` |
20
+ | **Empty roster CTA** | Outline **Add collaborator** in the header when `collaborators` is empty | `PageHeader` + `COLLABORATION_HEADER_ADD_LABEL` |
21
+ | **Invite sheet** | Floating right **`Sheet`** for roster + invite form | `InviteCollaboratorsDrawer` |
22
+ | **Hub wiring** | Roster state + invite sheet in one render-prop shell | `CollaborationAccessFlow` |
23
+
24
+ ## Header (`PageHeader`)
25
+
26
+ - Set **`variant="collaboration"`**.
27
+ - Pass **`collaborators`** (`PageHeaderCollaborator[]`) and optional **`accessInfo`**.
28
+ - **Non-empty roster** — overlapping **face rail** only; each face and **`+N`** open the invite sheet via **`onCollaboratorsOpen`**.
29
+ - **Empty roster** — outline **`Add collaborator`** (`addCollaboratorLabel`, default **`COLLABORATION_HEADER_ADD_LABEL`**) opens the same sheet.
30
+ - **Invite** also lives under **⋯ More** on the entity page header (first item when `variant="collaboration"`).
31
+
32
+ ```tsx
33
+ <CollaborationAccessFlow
34
+ initialCollaborators={QUESTION_BANK_HEADER_COLLABORATORS}
35
+ resourceLabel={hubHeader.title}
36
+ >
37
+ {({ collaborators, openInvite }) => (
38
+ <QuestionBankPageHeader
39
+ variant="collaboration"
40
+ title={hubHeader.title}
41
+ questionCount={count}
42
+ collaborators={collaborators}
43
+ onAddCollaborator={openInvite}
44
+ onCollaboratorsOpen={openInvite}
45
+ onExport={() => setExportOpen(true)}
46
+ showMetrics={showMetrics}
47
+ onToggleMetrics={() => setShowMetrics(v => !v)}
48
+ />
49
+ )}
50
+ </CollaborationAccessFlow>
51
+ ```
52
+
53
+ ## Hub client state
54
+
55
+ - Prefer **`CollaborationAccessFlow`** — owns **`collaborators`**, **`openInvite`**, and **`InviteCollaboratorsDrawer`**; pass **`openInvite`** to **`onAddCollaborator`** / **`onCollaboratorsOpen`**.
56
+ - Without the flow: **`collaborators`** — `useState` seeded from `lib/mock/<entity>-header-collaborators.ts` (or API later); **`inviteOpen`** — boolean; mount **`InviteCollaboratorsDrawer`** beside **`ListPageTemplate`**.
57
+ - On invite success, append to **`collaborators`** so the **face rail** and sheet roster stay aligned.
58
+ - **Change access** — roster menu updates **`collaborators`** via **`onCollaboratorAccessChange`** ( **`CollaborationAccessFlow`** default).
59
+ - **Remove access** — confirm dialog then **`onCollaboratorRemove`**; blocked for the only **Owner**.
60
+
61
+ ## `PageHeaderCollaborator`
62
+
63
+ | Field | Use |
64
+ |-------|-----|
65
+ | `id`, `name`, `imageUrl`, `initials` | Face rail + roster row |
66
+ | `email` | Roster (below name); invite form |
67
+ | `access` | Library access badge (Owner … Viewer) |
68
+ | `roles` | Optional **outline** chips (Faculty, Program coordinator, Director) |
69
+
70
+ ## Invite sheet (`InviteCollaboratorsDrawer`)
71
+
72
+ Mirror **`ExportDrawer`** chrome: floating **`Sheet`**, no overlay, **`showCloseButton={false}`**, footer **Cancel** / **Send invite** with inline **`Kbd`** (**Esc** / **⏎**), **`Shortcut`** for Enter on the open surface.
73
+
74
+ **Invite field:** one bordered row — email input + **access** menu on the right.
75
+
76
+ - Use **`Select`** with **`SelectGroup`** for access (invite row in **`InputGroupAddon`**, roster row standalone); **`position="popper"`** inside the sheet; **no** toast on success (**§6.5**).
77
+
78
+ **People with access:** one **`rounded-lg border`** list with **`divide-y`** — **not** one card per person.
79
+
80
+ Row order:
81
+
82
+ 1. **Name** (`text-sm font-medium`)
83
+ 2. **Email** (`text-xs text-muted-foreground`)
84
+ 3. **Role tags** (`Badge variant="outline"`) when `roles` is set
85
+ 4. Trailing **library access** **`Select`** when the hub wires **`onCollaboratorAccessChange`**; **Remove access** (trash) opens a confirm **`Dialog`** when **`onCollaboratorRemove`** is set. The sole **Owner** cannot be removed or demoted until another owner exists.
86
+
87
+ ## Library access constants
88
+
89
+ - Types and invite options: **`lib/collaborator-access.ts`**
90
+ - **`INVITE_COLLABORATOR_ACCESS_OPTIONS`** — Editor / Commenter / Viewer (no Owner on invite)
91
+ - Customize option **descriptions** per hub; keep **values** stable for forms/API
92
+
93
+ ## File map
94
+
95
+ | Piece | Path |
96
+ |-------|------|
97
+ | Access types | `lib/collaborator-access.ts` |
98
+ | Hub flow | `components/collaboration-access-flow.tsx` |
99
+ | Collaborator type | `components/page-header.tsx` (`PageHeaderCollaborator`) |
100
+ | Invite sheet | `components/invite-collaborators-drawer.tsx` |
101
+ | Entity header | `components/question-bank-page-header.tsx` |
102
+ | Hub wiring | `components/question-bank-client.tsx` |
103
+ | Demo roster | `lib/mock/question-bank-header-collaborators.ts` |
104
+
105
+ ## Checklist (new hub)
106
+
107
+ - [ ] `PageHeader` / entity header uses **`variant="collaboration"`** when the product is shared.
108
+ - [ ] **Empty roster** shows **Add collaborator**; **non-empty** shows face rail; both open the invite sheet.
109
+ - [ ] **Invite people** under **⋯ More**; **`CollaborationAccessFlow`** (or equivalent) owns roster + sheet.
110
+ - [ ] Roster: single bordered list; **name → email → role tags**; access badge on the right.
111
+ - [ ] Invite row: email + access menu; **`FormDescription`** for format; **no** toast.
112
+ - [ ] Labels from **`collaborator-access.ts`**; mock/API rows extend **`PageHeaderCollaborator`** once.
113
+
114
+ **Handbook:** `AGENTS.md` §4.7 · **Rule:** `.cursor/rules/exxat-collaboration-access.mdc` · **Skill:** `.cursor/skills/exxat-collaboration-access/SKILL.md`
@@ -45,7 +45,7 @@ Non-table view branches (e.g. **folder** icon grid, **panel** finder, OS-style f
45
45
  ## Mock data and connected views
46
46
 
47
47
  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.
48
- 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.
48
+ 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`. Set **`MetricItem.trendPolarity`** when an increase is **not** favorable (see **`docs/kpi-trend-pattern.md`**). 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.
49
49
  3. **Single table component** — One component (e.g. `TeamTable`) receives **`members`** (full mock) + **`view`**. It calls **`useTableState(members, columns, …)`** once. Branch on `view`:
50
50
  - **`table`** → `DataTable` with that state.
51
51
  - **`list` / `board`** → `DataTableToolbar` + list/board UI with **`tableState.rows`**.
@@ -72,6 +72,12 @@ Non-table view branches (e.g. **folder** icon grid, **panel** finder, OS-style f
72
72
 
73
73
  **Import:** `@/components/table-properties` re-exports the drawer and types.
74
74
 
75
+ ## Dropdown menus (view settings, row actions)
76
+
77
+ **`DropdownMenuContent`** in **`@exxatdesignux/ui`** merges a **default surface** (**`DROPDOWN_MENU_CONTENT_SURFACE_CLASS`**: **`min-w-52 w-max max-w-[min(24rem,calc(100vw-2rem))]`**) so **ListPageTemplate** view settings / Add view, **`DataTable`** filter and column menus, and hub **row ⋯** menus size to their labels (including shortcut hints) without fixed **`w-40` / `w-48`** rails. Sizing is **pure CSS** — do **not** add **`ResizeObserver`** or per-open measurement for standard menus.
78
+
79
+ **Override** when product intent needs a fixed width (e.g. pagination **page size** **`w-20`**, **`NavUser`** trigger-width + **`min-w-60`**, school switcher **`!w-max min-w-72 …`**).
80
+
75
81
  ## Board UI reuse
76
82
 
77
83
  **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`**.
@@ -138,9 +144,11 @@ Reference: `components/placements-page-header.tsx`, `components/team-page-header
138
144
 
139
145
  **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.
140
146
 
141
- **Rule of thumb:** **Context + quick** → **drawer**; **otherwise** → **new page**.
147
+ **Rule of thumb:** **Context + quick** → **drawer**; **blocking short choice** → **dialog**; **otherwise** → **new page**.
148
+
149
+ **Modal vs side panel (same route):** When the overlay stays on the same URL, prefer **`docs/drawer-vs-dialog-pattern.md`** and **`.cursor/rules/exxat-drawer-vs-dialog.mdc`** — drawers keep the hub visible; dialogs trap focus for confirms.
142
150
 
143
- Canonical rules: **`AGENTS.md` §6.4**, root **`.cursor/rules/exxat-page-vs-drawer.mdc`**.
151
+ Canonical rules: **`AGENTS.md` §6.4**, root **`.cursor/rules/exxat-page-vs-drawer.mdc`**, **`.cursor/rules/exxat-drawer-vs-dialog.mdc`**.
144
152
 
145
153
  ---
146
154
 
@@ -160,7 +168,7 @@ When a route is a **primary** destination in nav (main hub for an entity) **and*
160
168
  - [ ] **>10 items** → search, filter, sort, properties (per surface type above).
161
169
  - [ ] **Has data to export** → **More** menu with **Export** + shared `ExportDrawer` pattern.
162
170
  - [ ] **Primary + large / main hub** → `ListPageTemplate`-style shell where applicable.
163
- - [ ] **Page vs drawer (§6.4)** — Quick actions with parent **context** → drawer/sheet; primary or long flows → **new route**.
171
+ - [ ] **Page vs drawer vs dialog (§6.4)** — Quick with parent **context** → drawer/sheet; **blocking** confirm → **dialog**; primary or long flows → **new route** (`docs/drawer-vs-dialog-pattern.md`).
164
172
  - [ ] **Primary button** → `Button` default variant (`size="lg"` for parity with Placements), not outline.
165
173
  - [ ] **Dashboard view tab** → `KeyMetrics` + shared KPI helpers from **`tableState.rows`**; no duplicate one-off metric cards.
166
174
  - [ ] **Data view charts** → `ChartFigure` + `chart-keyboard-selection`; layout persistence via **`data-view-dashboard-storage`** (see `AGENTS.md` §4.3).
package/package.json CHANGED
@@ -1,8 +1,11 @@
1
1
  {
2
2
  "name": "@exxatdesignux/ui",
3
- "version": "0.2.9",
3
+ "version": "0.2.11",
4
4
  "description": "Exxat shared design system (components, hooks, tokens). Monorepo setup: clone repo then pnpm bootstrap at workspace root — see github.com/ExxatDesign/Exxat-DS-Workspace README.",
5
5
  "type": "module",
6
+ "engines": {
7
+ "node": ">=22.0.0"
8
+ },
6
9
  "private": false,
7
10
  "publishConfig": {
8
11
  "access": "public",
@@ -63,7 +63,8 @@ type BannerVariant = keyof typeof VARIANT_CONFIG
63
63
  // ─────────────────────────────────────────────────────────────────────────────
64
64
 
65
65
  const systemBannerVariants = cva(
66
- "relative flex overflow-hidden rounded-lg border text-sm transition-all",
66
+ // No `overflow-hidden` on root it clips promo `boxShadow` (glow). Clip `decorativeOverlay` in a nested layer instead.
67
+ "relative isolate flex rounded-lg border text-sm transition-all",
67
68
  {
68
69
  variants: {
69
70
  variant: {
@@ -186,24 +187,36 @@ export function SystemBanner({
186
187
  }}
187
188
  {...props}
188
189
  >
189
- {decorativeOverlay}
190
+ {decorativeOverlay ? (
191
+ <div
192
+ className="pointer-events-none absolute inset-0 z-0 overflow-hidden rounded-lg"
193
+ aria-hidden
194
+ >
195
+ {decorativeOverlay}
196
+ </div>
197
+ ) : null}
190
198
  {/* Icon */}
191
199
  <i
192
- className={cn(config.prefix, icon ?? config.icon, "shrink-0 text-[14px]", actionPosition === "bottom" ? "mt-0.5" : "")}
200
+ className={cn(
201
+ config.prefix,
202
+ icon ?? config.icon,
203
+ "relative z-[1] shrink-0 text-[14px]",
204
+ actionPosition === "bottom" ? "mt-0.5" : "",
205
+ )}
193
206
  aria-hidden="true"
194
207
  />
195
208
 
196
209
  {/* Content + inline action */}
197
210
  {actionPosition === "inline" ? (
198
211
  <>
199
- <div className="min-w-0 flex-1">
212
+ <div className="relative z-[1] min-w-0 flex-1">
200
213
  {title && <span className="font-semibold mr-1.5">{title}</span>}
201
214
  <span className="opacity-90">{children}</span>
202
215
  </div>
203
- {actionEl}
216
+ {actionEl ? <span className="relative z-[1] inline-flex shrink-0">{actionEl}</span> : null}
204
217
  </>
205
218
  ) : (
206
- <div className="min-w-0 flex-1">
219
+ <div className="relative z-[1] min-w-0 flex-1">
207
220
  {title && <p className="font-semibold leading-tight mb-0.5">{title}</p>}
208
221
  <p className="opacity-90 leading-relaxed">{children}</p>
209
222
  {actionEl && <div className="mt-1.5">{actionEl}</div>}
@@ -218,7 +231,7 @@ export function SystemBanner({
218
231
  aria-label="Dismiss banner"
219
232
  onClick={handleDismiss}
220
233
  className={cn(
221
- "inline-flex size-5 shrink-0 items-center justify-center rounded transition-colors",
234
+ "relative z-[2] inline-flex size-5 shrink-0 items-center justify-center rounded transition-colors",
222
235
  actionPosition === "bottom" ? "absolute top-2.5 right-2.5" : "",
223
236
  "hover:bg-current/10 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
224
237
  )}
@@ -3,7 +3,7 @@
3
3
  import * as React from "react"
4
4
 
5
5
  import { cn } from "../../lib/utils"
6
- import { formatDateUS } from "../../lib/date-filter"
6
+ import { formatDateFromDate } from "../../lib/date-filter"
7
7
  import { Button } from "./button"
8
8
  import { Calendar } from "./calendar"
9
9
  import {
@@ -59,11 +59,11 @@ export function DatePickerField({
59
59
  "w-full justify-start text-left font-normal",
60
60
  triggerClassName,
61
61
  )}
62
- aria-label={value ? formatDateUS(value.toISOString()) : "Pick a date"}
62
+ aria-label={value ? formatDateFromDate(value) : "Pick a date"}
63
63
  >
64
64
  <i className={cn(DATE_PICKER_ICON_CLASS, "mr-2 shrink-0 text-muted-foreground")} aria-hidden="true" />
65
65
  <span className={cn(!value && "text-muted-foreground")}>
66
- {value ? formatDateUS(value.toISOString()) : "MM/DD/YYYY"}
66
+ {value ? formatDateFromDate(value) : "MM/DD/YYYY"}
67
67
  </span>
68
68
  </Button>
69
69
  </PopoverTrigger>
@@ -4,6 +4,7 @@ import * as React from "react"
4
4
  import { DropdownMenu as DropdownMenuPrimitive } from "radix-ui"
5
5
 
6
6
  import { cn } from "../../lib/utils"
7
+ import { DROPDOWN_MENU_CONTENT_SURFACE_CLASS } from "../../lib/dropdown-menu-surface"
7
8
 
8
9
  function DropdownMenu({
9
10
  ...props
@@ -43,7 +44,11 @@ function DropdownMenuContent({
43
44
  data-slot="dropdown-menu-content"
44
45
  sideOffset={sideOffset}
45
46
  align={align}
46
- className={cn("z-50 max-h-(--radix-dropdown-menu-content-available-height) w-(--radix-dropdown-menu-trigger-width) min-w-32 origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-lg bg-popover p-1 text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:overflow-hidden data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95", className )}
47
+ className={cn(
48
+ "z-50 max-h-(--radix-dropdown-menu-content-available-height) origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-lg bg-popover p-1 text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:overflow-hidden data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
49
+ DROPDOWN_MENU_CONTENT_SURFACE_CLASS,
50
+ className,
51
+ )}
47
52
  {...props}
48
53
  />
49
54
  </DropdownMenuPrimitive.Portal>
@@ -81,7 +86,7 @@ function DropdownMenuItem({
81
86
  data-variant={variant}
82
87
  asChild={asChild}
83
88
  className={cn(
84
- "group/dropdown-menu-item relative flex cursor-default items-center gap-1.5 rounded-md px-1.5 py-1 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-inset:ps-7 data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive dark:data-[variant=destructive]:focus:bg-destructive/20 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([data-product-logo]):not([class*='size-'])]:size-4 data-[variant=destructive]:*:[svg]:text-destructive",
89
+ "group/dropdown-menu-item relative flex cursor-default items-center gap-1.5 rounded-md px-1.5 py-1 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-inset:ps-7 data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive dark:data-[variant=destructive]:focus:bg-destructive/20 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_i]:pointer-events-none [&_svg]:shrink-0 [&_i]:shrink-0 [&_svg:not([data-product-logo]):not([class*='size-'])]:size-4 data-[variant=destructive]:*:[svg]:text-destructive",
85
90
  className
86
91
  )}
87
92
  {...props}
@@ -247,7 +252,7 @@ function DropdownMenuCheckboxItem({
247
252
  data-slot="dropdown-menu-checkbox-item"
248
253
  data-inset={inset}
249
254
  className={cn(
250
- "relative flex cursor-default items-center gap-1.5 rounded-md py-1 pe-8 ps-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground data-inset:ps-7 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([data-product-logo]):not([class*='size-'])]:size-4",
255
+ "relative flex cursor-default items-center gap-1.5 rounded-md py-1 pe-8 ps-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground data-inset:ps-7 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_i]:pointer-events-none [&_svg]:shrink-0 [&_i]:shrink-0 [&_svg:not([data-product-logo]):not([class*='size-'])]:size-4",
251
256
  className
252
257
  )}
253
258
  checked={checked}
@@ -290,7 +295,7 @@ function DropdownMenuRadioItem({
290
295
  data-slot="dropdown-menu-radio-item"
291
296
  data-inset={inset}
292
297
  className={cn(
293
- "relative flex cursor-default items-center gap-1.5 rounded-md py-1 pe-8 ps-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground data-inset:ps-7 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([data-product-logo]):not([class*='size-'])]:size-4",
298
+ "relative flex cursor-default items-center gap-1.5 rounded-md py-1 pe-8 ps-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground data-inset:ps-7 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_i]:pointer-events-none [&_svg]:shrink-0 [&_i]:shrink-0 [&_svg:not([data-product-logo]):not([class*='size-'])]:size-4",
294
299
  className
295
300
  )}
296
301
  {...props}
@@ -377,7 +382,7 @@ function DropdownMenuSubTrigger({
377
382
  suppressHydrationWarning
378
383
  data-inset={inset}
379
384
  className={cn(
380
- "flex cursor-default items-center gap-1.5 rounded-md px-1.5 py-1 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-inset:ps-7 data-open:bg-accent data-open:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([data-product-logo]):not([class*='size-'])]:size-4",
385
+ "flex cursor-default items-center gap-1.5 rounded-md px-1.5 py-1 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-inset:ps-7 data-open:bg-accent data-open:text-accent-foreground [&_svg]:pointer-events-none [&_i]:pointer-events-none [&_svg]:shrink-0 [&_i]:shrink-0 [&_svg:not([data-product-logo]):not([class*='size-'])]:size-4",
381
386
  className
382
387
  )}
383
388
  {...props}
@@ -395,7 +400,11 @@ function DropdownMenuSubContent({
395
400
  return (
396
401
  <DropdownMenuPrimitive.SubContent
397
402
  data-slot="dropdown-menu-sub-content"
398
- className={cn("z-50 min-w-[96px] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-lg bg-popover p-1 text-popover-foreground shadow-lg ring-1 ring-foreground/10 duration-100 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95", className )}
403
+ className={cn(
404
+ "z-50 origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-lg bg-popover p-1 text-popover-foreground shadow-lg ring-1 ring-foreground/10 duration-100 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
405
+ DROPDOWN_MENU_CONTENT_SURFACE_CLASS,
406
+ className,
407
+ )}
399
408
  {...props}
400
409
  />
401
410
  )
@@ -420,3 +429,5 @@ export {
420
429
  Shortcut,
421
430
  useShortcut,
422
431
  }
432
+
433
+ export { DROPDOWN_MENU_CONTENT_SURFACE_CLASS } from "../../lib/dropdown-menu-surface"
@@ -14,7 +14,7 @@ function InputGroup({ className, ...props }: React.ComponentProps<"div">) {
14
14
  data-slot="input-group"
15
15
  role="group"
16
16
  className={cn(
17
- "group/input-group relative flex h-8 w-full min-w-0 items-center rounded-lg border border-input transition-colors outline-none in-data-[slot=combobox-content]:focus-within:border-inherit in-data-[slot=combobox-content]:focus-within:ring-0 has-disabled:bg-input/50 has-disabled:opacity-50 has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot=input-group-control]:focus-visible]:ring-3 has-[[data-slot=input-group-control]:focus-visible]:ring-ring/50 has-[[data-slot][aria-invalid=true]]:border-destructive has-[[data-slot][aria-invalid=true]]:ring-3 has-[[data-slot][aria-invalid=true]]:ring-destructive/20 has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>textarea]:h-auto dark:bg-input/30 dark:has-disabled:bg-input/80 dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40 has-[>[data-align=block-end]]:[&>input]:pt-3 has-[>[data-align=block-start]]:[&>input]:pb-3 has-[>[data-align=inline-end]]:[&>input]:pe-1.5 has-[>[data-align=inline-start]]:[&>input]:ps-1.5",
17
+ "group/input-group relative flex h-8 w-full min-w-0 items-center rounded-md border border-input transition-colors outline-none in-data-[slot=combobox-content]:focus-within:border-inherit in-data-[slot=combobox-content]:focus-within:ring-0 has-disabled:bg-input/50 has-disabled:opacity-50 has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot=input-group-control]:focus-visible]:ring-3 has-[[data-slot=input-group-control]:focus-visible]:ring-ring/50 has-[[data-slot][aria-invalid=true]]:border-destructive has-[[data-slot][aria-invalid=true]]:ring-3 has-[[data-slot][aria-invalid=true]]:ring-destructive/20 has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>textarea]:h-auto dark:bg-input/30 dark:has-disabled:bg-input/80 dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40 has-[>[data-align=block-end]]:[&>input]:pt-3 has-[>[data-align=block-start]]:[&>input]:pb-3 has-[>[data-align=inline-end]]:[&>input]:pe-1.5 has-[>[data-align=inline-start]]:[&>input]:ps-1.5",
18
18
  className
19
19
  )}
20
20
  {...props}