pixelize-design-library 2.4.2-beta.58 → 2.4.2-beta.59

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 (123) hide show
  1. package/CHANGELOG.md +75 -0
  2. package/COMPONENT-GOTCHAS.md +88 -2
  3. package/DESIGN-GUIDE.md +26 -3
  4. package/dist/Components/AppSwitcher/AppSwitcher.js +5 -1
  5. package/dist/Components/AppSwitcher/AppSwitcherProps.d.ts +2 -0
  6. package/dist/Components/AppSwitcher/AppSwitcherProps.js +7 -4
  7. package/dist/Components/Breadcrumbs/Breadcrumbs.d.ts +1 -1
  8. package/dist/Components/Breadcrumbs/Breadcrumbs.js +33 -10
  9. package/dist/Components/Breadcrumbs/BreadcrumbsProps.d.ts +16 -2
  10. package/dist/Components/Card/Card.d.ts +3 -1
  11. package/dist/Components/Card/Card.js +12 -2
  12. package/dist/Components/Card/CardProps.d.ts +5 -1
  13. package/dist/Components/Editor/Editor.js +50 -2
  14. package/dist/Components/Editor/EditorFieldBar.d.ts +15 -0
  15. package/dist/Components/Editor/EditorFieldBar.js +19 -0
  16. package/dist/Components/Editor/EditorProps.d.ts +74 -1
  17. package/dist/Components/Editor/MakeDynamicDialog.d.ts +12 -0
  18. package/dist/Components/Editor/MakeDynamicDialog.js +59 -0
  19. package/dist/Components/Editor/editorLabels.d.ts +4 -0
  20. package/dist/Components/Editor/editorLabels.js +24 -0
  21. package/dist/Components/Editor/editorTokenStyles.d.ts +28 -0
  22. package/dist/Components/Editor/editorTokenStyles.js +30 -0
  23. package/dist/Components/Editor/editorTokens.d.ts +26 -0
  24. package/dist/Components/Editor/editorTokens.js +134 -0
  25. package/dist/Components/Editor/useEditorBridge.d.ts +40 -0
  26. package/dist/Components/Editor/useEditorBridge.js +141 -0
  27. package/dist/Components/Kbd/Kbd.d.ts +6 -0
  28. package/dist/Components/Kbd/Kbd.js +33 -0
  29. package/dist/Components/Kbd/KbdProps.d.ts +17 -0
  30. package/dist/Components/Kbd/KbdProps.js +2 -0
  31. package/dist/Components/LauncherTrigger/LauncherTrigger.d.ts +4 -0
  32. package/dist/Components/LauncherTrigger/LauncherTrigger.js +41 -0
  33. package/dist/Components/LauncherTrigger/LauncherTriggerProps.d.ts +16 -0
  34. package/dist/Components/LauncherTrigger/LauncherTriggerProps.js +2 -0
  35. package/dist/Components/NavLauncher/NavLauncher.d.ts +3 -0
  36. package/dist/Components/NavLauncher/NavLauncher.js +395 -0
  37. package/dist/Components/NavLauncher/NavLauncherProps.d.ts +187 -0
  38. package/dist/Components/NavLauncher/NavLauncherProps.js +2 -0
  39. package/dist/Components/NavLauncher/NewTabLink.d.ts +3 -0
  40. package/dist/Components/NavLauncher/NewTabLink.js +14 -0
  41. package/dist/Components/NavLauncher/PinToggle.d.ts +3 -0
  42. package/dist/Components/NavLauncher/PinToggle.js +21 -0
  43. package/dist/Components/NavLauncher/launcherParts.d.ts +93 -0
  44. package/dist/Components/NavLauncher/launcherParts.js +130 -0
  45. package/dist/Components/NavLauncher/match.d.ts +53 -0
  46. package/dist/Components/NavLauncher/match.js +178 -0
  47. package/dist/Components/NavLauncher/revealStyles.d.ts +4 -0
  48. package/dist/Components/NavLauncher/revealStyles.js +19 -0
  49. package/dist/Components/NavigationBar/NavigationBar.d.ts +1 -1
  50. package/dist/Components/NavigationBar/NavigationBar.js +2 -2
  51. package/dist/Components/NavigationBar/NavigationBarProps.d.ts +2 -0
  52. package/dist/Components/PageHeader/PageHeader.d.ts +1 -1
  53. package/dist/Components/PageHeader/PageHeader.js +9 -2
  54. package/dist/Components/PageHeader/PageHeaderProps.d.ts +8 -0
  55. package/dist/Components/PdfViewer/PdfViewer.d.ts +21 -4
  56. package/dist/Components/PdfViewer/PdfViewer.js +124 -3
  57. package/dist/Components/Search/Search.js +2 -1
  58. package/dist/Components/SideBar/SecondaryBar.js +4 -3
  59. package/dist/Components/SideBar/SideBar.d.ts +1 -1
  60. package/dist/Components/SideBar/SideBar.js +30 -9
  61. package/dist/Components/SideBar/SideBarProps.d.ts +32 -0
  62. package/dist/Components/SideBar/components/MenuItemBox.d.ts +6 -0
  63. package/dist/Components/SideBar/components/MenuItemBox.js +8 -4
  64. package/dist/Components/SideBar/components/MenuPopoverContent.js +3 -2
  65. package/dist/Components/SideBar/components/OtherApps.js +2 -1
  66. package/dist/Components/SideBar/components/PinnedSection.d.ts +17 -0
  67. package/dist/Components/SideBar/components/PinnedSection.js +98 -0
  68. package/dist/Components/SideBar/sidebarFocus.d.ts +23 -0
  69. package/dist/Components/SideBar/sidebarFocus.js +20 -0
  70. package/dist/Components/StageProgress/StageProgress.js +2 -2
  71. package/dist/Components/StageProgress/StageProgressProps.d.ts +2 -0
  72. package/dist/Components/StageProgress/StepperStage.js +5 -2
  73. package/dist/Components/WizardDialog/WizardDialog.d.ts +9 -0
  74. package/dist/Components/WizardDialog/WizardDialog.js +102 -0
  75. package/dist/Components/WizardDialog/WizardDialogProps.d.ts +48 -0
  76. package/dist/Components/WizardDialog/WizardDialogProps.js +2 -0
  77. package/dist/Components/WizardDialog/useWizardFlow.d.ts +22 -0
  78. package/dist/Components/WizardDialog/useWizardFlow.js +47 -0
  79. package/dist/Components/WizardDialog/wizardSteps.d.ts +31 -0
  80. package/dist/Components/WizardDialog/wizardSteps.js +40 -0
  81. package/dist/esm/Components/AppSwitcher/AppSwitcher.js +2 -1
  82. package/dist/esm/Components/AppSwitcher/AppSwitcherProps.js +5 -3
  83. package/dist/esm/Components/Breadcrumbs/Breadcrumbs.js +35 -12
  84. package/dist/esm/Components/Card/Card.js +12 -2
  85. package/dist/esm/Components/Editor/Editor.js +48 -3
  86. package/dist/esm/Components/Editor/EditorFieldBar.js +13 -0
  87. package/dist/esm/Components/Editor/MakeDynamicDialog.js +20 -0
  88. package/dist/esm/Components/Editor/editorLabels.js +20 -0
  89. package/dist/esm/Components/Editor/editorTokenStyles.js +25 -0
  90. package/dist/esm/Components/Editor/editorTokens.js +121 -0
  91. package/dist/esm/Components/Editor/useEditorBridge.js +138 -0
  92. package/dist/esm/Components/Kbd/Kbd.js +28 -0
  93. package/dist/esm/Components/Kbd/KbdProps.js +1 -0
  94. package/dist/esm/Components/LauncherTrigger/LauncherTrigger.js +37 -0
  95. package/dist/esm/Components/LauncherTrigger/LauncherTriggerProps.js +1 -0
  96. package/dist/esm/Components/NavLauncher/NavLauncher.js +356 -0
  97. package/dist/esm/Components/NavLauncher/NavLauncherProps.js +1 -0
  98. package/dist/esm/Components/NavLauncher/NewTabLink.js +11 -0
  99. package/dist/esm/Components/NavLauncher/PinToggle.js +18 -0
  100. package/dist/esm/Components/NavLauncher/launcherParts.js +110 -0
  101. package/dist/esm/Components/NavLauncher/match.js +172 -0
  102. package/dist/esm/Components/NavLauncher/revealStyles.js +15 -0
  103. package/dist/esm/Components/NavigationBar/NavigationBar.js +3 -3
  104. package/dist/esm/Components/PageHeader/PageHeader.js +9 -2
  105. package/dist/esm/Components/PdfViewer/PdfViewer.js +123 -2
  106. package/dist/esm/Components/Search/Search.js +3 -2
  107. package/dist/esm/Components/SideBar/SecondaryBar.js +4 -3
  108. package/dist/esm/Components/SideBar/SideBar.js +31 -10
  109. package/dist/esm/Components/SideBar/components/MenuItemBox.js +4 -2
  110. package/dist/esm/Components/SideBar/components/MenuPopoverContent.js +3 -2
  111. package/dist/esm/Components/SideBar/components/OtherApps.js +2 -1
  112. package/dist/esm/Components/SideBar/components/PinnedSection.js +92 -0
  113. package/dist/esm/Components/SideBar/sidebarFocus.js +15 -0
  114. package/dist/esm/Components/StageProgress/StageProgress.js +2 -2
  115. package/dist/esm/Components/StageProgress/StepperStage.js +5 -2
  116. package/dist/esm/Components/WizardDialog/WizardDialog.js +58 -0
  117. package/dist/esm/Components/WizardDialog/WizardDialogProps.js +1 -0
  118. package/dist/esm/Components/WizardDialog/useWizardFlow.js +44 -0
  119. package/dist/esm/Components/WizardDialog/wizardSteps.js +35 -0
  120. package/dist/esm/index.js +7 -0
  121. package/dist/index.d.ts +19 -4
  122. package/dist/index.js +21 -1
  123. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -18,6 +18,57 @@ next major.
18
18
 
19
19
  ### Added
20
20
 
21
+ - **HRMS library requests** — a navigation launcher, key caps, a merge-field `Editor`, a wizard and
22
+ smaller gaps found by the HRMS migration:
23
+ - `NavLauncher` — full-height navigation launcher (browse modules, pins, Frequent, Recent, search
24
+ with actions and remote groups, optional suite-apps section below `md`), with `PinToggle`,
25
+ `NewTabLink`, `searchLauncher`, `searchLauncherSections` (sectioned search: modules, then their
26
+ screens, keyword-only hits get a `· keyword` hint) and `flattenScreens`. The pin control is a
27
+ star, shown with the ↗ link on row hover/focus only (always on touch); pinned rail items are tiles. It binds no global chord. `LauncherTrigger`
28
+ is the chip (10px radius, 36px, accent ink, one ⌘K hint) that opens an app-owned launcher (`aria-haspopup="dialog"`, `aria-expanded`,
29
+ `aria-keyshortcuts`). Types: `NavLauncherProps`, `LauncherScreen`, `LauncherModule`,
30
+ `LauncherSection`, `LauncherSearchItem`, `LauncherLabels` and friends, `LauncherTriggerProps`.
31
+ `NavLauncher` follows canvas `Launcher.dc.html`: stacked sections with "(n)" counts and a 1/2/3-column
32
+ grid, a 300px "Quick access" rail (Pinned tiles, Frequently visited, Recent), "Pins follow your
33
+ account" footer. New optional props `frequentLimit`, `suggestions` (no-results chips), `searchHint`,
34
+ `LauncherModule.isNew`, and labels `quickAccess`, `newBadge`, `screens`, `pinShort`, `footerNote`,
35
+ `suggestionsHint`, `found`, `pinTip`, `unpinTip`. The rail landmark is named "Quick access".
36
+ Keyboard: ←/→ and ↑/↓ are grid-aware (row-major, ↑/↓ cross sections), and `P` pins or unpins the
37
+ focused item (never while typing). New optional `openShortcutHint` (e.g. "⌘K or /", app-owned),
38
+ labels `hintOpenMenu` and `hintPin`; hint label defaults are lowercase and follow the canvas order.
39
+ - `Kbd` / `KbdShortcut` — a key cap and a chord (`mod+k` renders ⌘ K or Ctrl K), in the
40
+ `AppSwitcher` notation. `Search` and `AppSwitcher` render their shortcut hints with `Kbd`.
41
+ - `NavigationBar` `leading` — a slot between the sidebar toggle/back button and the title, outside
42
+ the title `<p>`.
43
+ - `SideBar` pinned section: `pinnedItems`, `pinnedLabel`, `onUnpin`, `activePinnedId`,
44
+ `pinnedLimit`, `pinnedLabels`; `collapseTransition` (`"none"` or `{ speed, animateShadow }`);
45
+ `autoCollapse` (default `true`; `false` stops the rail calling `changeToggle` itself, which also
46
+ silences the `open` prop). Types `SidebarPinnedItem`, `SidebarPinnedLabels`,
47
+ `SidebarCollapseTransition`.
48
+ - `Breadcrumbs` `overflow` (`"nowrap"` | `"wrap"` | `"truncate"`), `maxItems` (default 3) and
49
+ `labels.showMore`; `"truncate"` folds middle crumbs into a "…" menu. A `BreadcrumbItem` without
50
+ `path` renders as plain text.
51
+ - `PageHeader` `titleSize` (`"md"` | `"lg"` | `"xl"`) and `actionsLayout`
52
+ (`"auto"` | `"stacked"` | `"inline"`).
53
+ - `Card` `padding` (`"none"` | `"2xs"` | `"xs"` | `"sm"` | `"md"` | `"lg"`) — one padding for
54
+ header, body and footer.
55
+ - `Editor` merge fields: `mergeFields` adds an Insert field menu (grouped, searchable),
56
+ non-editable token chips (stored HTML keeps the raw `${path}`), unknown-field error chips with
57
+ `onUnknownTokens`, and a Make dynamic dialog; plus `tokenSyntax`, `showMakeDynamic`,
58
+ `toolbarButtons`, `apiRef` / `onReady` (`EditorApi`) and `labels` (incl. `dynamicFieldPlaceholder`, default "Choose a field"). `Editor` also takes `label`,
59
+ `isRequired`, `helperText`, `error`, `errorMessage` and `data-testid`; the editable area gets
60
+ `role="textbox"` with `aria-labelledby` / `aria-invalid` / `aria-describedby` / `aria-required`.
61
+ Types `EditorProps`, `EditorApi`, `EditorMergeField`, `EditorTokenSyntax`, `EditorToolbarButton`,
62
+ `EditorLabels`.
63
+ - `WizardDialog` and inline `Wizard` — a multi-step flow that owns step state (validated
64
+ Continue, locked future steps, error summary, review on the last step); `wizardTransition` and
65
+ `firstInvalidStep` are exported.
66
+ - `PdfViewer` `getBlob` (authed files: the viewer makes and revokes the object URL, skeleton while
67
+ loading), `fallback`, `onLoad`, `onError`, `labels`; inline support is detected with
68
+ `navigator.pdfViewerEnabled`. `PdfViewerProps` / `PdfViewerLabels` are now exported types.
69
+ - Page examples: **App launcher** (`/examples/launcher`), **Wizard** (`/examples/wizard`),
70
+ **Template editor** (`/examples/template-editor`).
71
+
21
72
  - **Commerce kit** — the components for a shop front, designed mobile-first (Pebble principles:
22
73
  chromeless tonal cards, one loud action per view, selection never by colour alone). Product:
23
74
  `Rating`, `PriceBlock`, `StockStatus`, `ProductBadges`, `WishlistToggle`, `OptionChip`,
@@ -86,6 +137,22 @@ next major.
86
137
 
87
138
  ### Changed
88
139
 
140
+ - **`PageHeader` `actionsLayout` defaults to `"auto"`**: at `md` and up the actions wrap below the
141
+ title when they do not fit (before, they squeezed the title). Identical whenever title and
142
+ actions fit on one row; pass `actionsLayout="inline"` for the old behaviour.
143
+ - **`Card` `padding` is no longer Chakra's style prop** — it is omitted from the type and now takes
144
+ the `CardPadding` steps. A consumer passing `padding={4}` gets a compile error; use `p` or `sx`.
145
+ Staged for the next major (see Breaking); consumer blast radius not measured.
146
+ - **`SideBar` default box-shadow transition is 260ms** (was 200ms) so it follows `collapseTransition`
147
+ `speed="slow"`. The width transition is unchanged.
148
+ - **`StageProgress variant="stepper"` renders an `ol` of `li`** (`StepperStageProps.asListItem`).
149
+ Visual layout is unchanged; selectors that expected a `div` list no longer match.
150
+ - **`PdfViewer` `pdfUrl` is optional** — pass exactly one of `pdfUrl` / `getBlob`. In `getBlob` mode
151
+ the toolbar Open and Download are real buttons that fetch through it.
152
+ - `Editor` binds Jodit's `change` / `blur` itself on every Jodit instance and renders Jodit only once
153
+ its popup root exists (no throwaway first instance). Behaviour for existing props is unchanged.
154
+ - `BreadcrumbItem.path` is optional (type widening).
155
+
89
156
  - **`ProductCard` and `ProductDetails` are rewritten** — see Breaking below. `ProductCard` is a
90
157
  chromeless 4:5 card (grid / list / rail, swatch dots that swap the photo, quick-add sheet or
91
158
  direct add that morphs into a stepper, sold-out "Notify me", slots). `ProductDetails` is a
@@ -125,6 +192,10 @@ next major.
125
192
 
126
193
  ### Fixed
127
194
 
195
+ - **`SideBar` rail controls now show a `:focus-visible` ring** (rows, submenu and flyout rows,
196
+ `SecondaryBar`, collapse toggle, Add New, user footer, Collapse/Hide/Show, switch-app cells,
197
+ overlay close) — WCAG 2.4.7 / 2.4.11.
198
+
128
199
  - **Commerce performance (consumer report on 2.4.2-beta.57).** `ProductCard` no longer loads the
129
200
  hover image on devices that cannot hover; `QuickAddSheet` mounts on first open, not on first
130
201
  paint; `formatPrice` caches its `Intl.NumberFormat`; every `matchMedia` consumer
@@ -379,6 +450,10 @@ known consumers in the Pixelize repos. `ProductCard` drops `id`, `label`, `descr
379
450
  option and cart-callback props (it takes `product`, `selection`, `onAddToBag` and slots). Video
380
451
  and YouTube/Vimeo frames are now `ProductMedia` items in `MediaGallery`.
381
452
 
453
+ **`Card` `padding` narrows from Chakra's style prop to `CardPadding`.** `padding={4}` stops
454
+ compiling; use `p` / `sx` for arbitrary values. No use inside this repo; consumers not measured, so
455
+ run `tsc --noEmit` in each before upgrading.
456
+
382
457
  **`star` is a required `PaletteProps` token.** A hand-written palette without it throws in
383
458
  `assertPixelizePalette` naming `star`; palettes built from the library's builders get it for free.
384
459
 
@@ -233,6 +233,8 @@ and wires `aria-controls` to them. Pick by that, never by the picture. A screen
233
233
  | Top bar: back control + user-avatar profile/logout dropdown | `NavigationBar` | No page-nav links at all — `navMenu` is only the avatar dropdown's own items |
234
234
  | Top bar: horizontal page links with active-state highlighting + submenus | `Header` | Desktop submenus are hover-only, see gotcha below |
235
235
  | Collapsible/dockable left rail — primary in-app navigation + secondary panel | `SideBar` | The persistent nav shell, not for switching between whole apps |
236
+ | In-app launcher: browse modules, pins, recents, search | `NavLauncher` opened by a `LauncherTrigger` in `NavigationBar`'s `leading` | An app-owned screen registry, not a cross-app switcher; pins can also surface in `SideBar` `pinnedItems` |
237
+ | Key cap or chord hint (`mod+k`) | `Kbd` / `KbdShortcut` | Never hand-style a `<kbd>`; `Search` and `AppSwitcher` already use it |
236
238
  | Cross-app launcher (CRM/HRMS/Tickets/Billing/...) | `AppSwitcher` | Two-column popover grid of separate app origins — `SideBar`'s `otherApps` is the legacy predecessor; don't wire both into one shell |
237
239
  | Floating Jira-style dialog to view/edit one record without leaving the page | `WorkspaceWindow` | Modal-based floating panel, not page navigation; pairs with `WorkspaceTray` |
238
240
 
@@ -253,10 +255,10 @@ Four components open a file, and only one of them is the general answer.
253
255
  | --- | --- | --- |
254
256
  | A gallery of mixed media with next/prev, a filmstrip and per-type stages | `MediaViewer` | The full lightbox: image/video/audio/pdf/doc stages, keyboard nav, fullscreen, download. Collapses the filmstrip and stepper automatically for a single item |
255
257
  | One file, click-to-open, no gallery | `FilePreview` | Lighter controlled modal; auto-detects type, zoom/rotate for images |
256
- | Always-visible inline PDF panel in the page flow | `PdfViewer` | Non-modal, PDF-only, small open/download toolbar |
258
+ | Always-visible inline PDF panel in the page flow | `PdfViewer` | Non-modal, PDF-only, small open/download toolbar; `getBlob` for files behind auth |
257
259
  | Crop/preview a user avatar | `ProfilePhotoViewer` | Avatar-specific, not a general file viewer |
258
260
 
259
- `PdfViewer` and `FilePreview` both render a PDF through the identical unstyled `<embed type="application/pdf">` with no error handling if the browser can't render it inline — neither tells you when that happens. `MediaViewer` does handle it: formats a browser cannot decode (`.avi`, `.mkv`, `.m3u8`, `.heic`, `.tiff`) render an icon and a file-type chip with a download action instead of a blank frame.
261
+ `PdfViewer` shows a fallback when the browser cannot render a PDF inline (`navigator.pdfViewerEnabled === false`) or `getBlob` rejects; `FilePreview`'s `"pdf"` kind renders a bare `<embed type="application/pdf">` and tells you nothing. `MediaViewer` also handles it: formats a browser cannot decode (`.avi`, `.mkv`, `.m3u8`, `.heic`, `.tiff`) render an icon and a file-type chip with a download action instead of a blank frame.
260
262
 
261
263
  `MediaViewer.labels` takes a `Partial<MediaViewerLabels>` — that type and `defaultMediaViewerLabels` are both exported from the package root, so you can merge rather than restate every string.
262
264
 
@@ -948,6 +950,11 @@ draw nothing.
948
950
 
949
951
  **Apply:** drop the nested `CardBody`, or pass `bodyProps={{ p: 0 }}`.
950
952
 
953
+ To change the padding, use `padding="none" | "2xs" | "xs" | "sm" | "md" | "lg"`. `sx={{ p: ... }}` on
954
+ the container *adds* to the body padding (double inset) and `sx={{ p: 0 }}` does nothing. `padding`
955
+ is no longer Chakra's style prop (omitted from the type) — use `p` for a raw value. With
956
+ `padding="none"` the header keeps `xs` padding.
957
+
951
958
  ### Commerce kit — read before building a shop screen
952
959
 
953
960
  `ProductCard`, `ProductDetails`, `VariantSelector` and the bag pieces share one contract: pass
@@ -1114,6 +1121,39 @@ is also `null` until Jodit's lazy-loaded chunk resolves — a mount-time effect
1114
1121
  **Apply:** never set `theme`/`popupRoot` inside the `config` you pass to `Editor`. To force a
1115
1122
  remount, wrap `<Editor>` itself in a keyed parent, not via `Editor`'s own `key` prop.
1116
1123
 
1124
+ ### `PdfViewer` `getBlob` — fetched on mount only; the toolbar already has Open and Download
1125
+
1126
+ - **Swapping the file needs a new React `key`.** `getBlob` is read through a ref and fetched on mount,
1127
+ so an inline arrow is safe, but changing it never refetches — the old document silently stays.
1128
+ - **The default fallback shows only a message while the toolbar is visible**; with
1129
+ `showToolbar={false}` it adds its own Open and Download buttons. `getBlob` mode makes the toolbar's
1130
+ actions work for authed files, so an app need not render its own — or pass `showToolbar={false}`.
1131
+ - Pass exactly one of `pdfUrl` / `getBlob`.
1132
+
1133
+ ### `WizardDialog` — `validate` must be pure; controlled `stepId` does not unlock steps
1134
+
1135
+ `validate` runs on Continue, on Finish, and again for every step on Finish. Passing `stepId` controls
1136
+ the visible step only: stages past the furthest valid step stay disabled. Esc, close, Back and Cancel
1137
+ are disabled while `isFinishing`.
1138
+
1139
+ ### `PageHeader` actions wrap by default
1140
+
1141
+ `actionsLayout` defaults to `"auto"`, so actions that do not fit drop below the title at `md`+. A
1142
+ test asserting the old single-row layout needs `actionsLayout="inline"`.
1143
+
1144
+ ### `Editor` merge fields — chips, raw values and stale closures
1145
+
1146
+ - **Chips are `<span data-field contenteditable="false">`.** Do not style or strip `[data-field]` in
1147
+ consumer CSS or sanitisers, and sanitise the raw `${path}` value `onChange` reports, not Jodit's HTML.
1148
+ - **A hand-typed `${path}` stays plain text** until `value` is next applied from outside; only tokens
1149
+ inserted through the menu / `apiRef` or already present in `value` become chips. Unknown-token
1150
+ detection still runs on the raw value.
1151
+ - **`config.extraButtons[].exec` goes stale** (the memo key is `JSON.stringify`, which drops functions).
1152
+ Use `toolbarButtons` — React buttons, always current.
1153
+ - **`jodit-react` binds `onChange` / `onBlur` once, keyed on handler identity, not `config`.** After
1154
+ Jodit is re-created (readonly or height change) a stable handler stops receiving events. `Editor`
1155
+ binds them itself per instance; don't copy the pattern when wrapping Jodit directly.
1156
+
1117
1157
  ### `Editor`'s Jodit toolbar is fixed for screen readers and keyboard, from our wrapper only
1118
1158
 
1119
1159
  Jodit's own markup (`node_modules/jodit`) put `aria-label` on each button's `role="listitem"`
@@ -1182,6 +1222,34 @@ Each mounted `Search` registers its own `window` Ctrl+K listener, so two on one
1182
1222
  keypress. **Apply:** pass `shortcut={false}` on all but one. `showShortcutHint={false}` only hides
1183
1223
  the hint; the listener stays.
1184
1224
 
1225
+ The same applies to a launcher: `NavLauncher` and `LauncherTrigger` bind no chord, and the library
1226
+ reserves `mod+k` for the app's own palette. The app must pass `shortcut={false}` on every page
1227
+ `Search`, or claim the chord in capture phase (`keydown` on `window` with `capture: true`) before
1228
+ the `Search` listeners see it — otherwise the first mounted `Search` takes Ctrl+K.
1229
+
1230
+ ### `NavLauncher` — the app owns routing, registry, query and chord
1231
+
1232
+ - **`linkAs` must forward `className`, `ref`, `onClick`, `href`, `target`, `rel`, `tabIndex` and
1233
+ `aria-hidden`** to its anchor. Dropping `className` loses all row styling; dropping `onClick` means
1234
+ a click never closes the launcher.
1235
+ - **`onNavigate` fires for keyboard opens only** (Enter, mod+Enter). Pointer clicks follow the
1236
+ rendered links, so a router `linkAs` must do the navigation.
1237
+ - **Controlled `query`:** the launcher still calls `onQueryChange("")` when it closes; the app must
1238
+ apply it or the stale query reopens.
1239
+ - **Search rows are not nested links.** The visible label is a text-only `role="option"`; the real
1240
+ anchor is an `aria-hidden`, `tabIndex={-1}` overlay (so copy-link and mod-click work) and pin / ↗
1241
+ are `aria-hidden` siblings. Screen readers use Enter / mod+Enter. Do not nest links inside the
1242
+ option — axe `nested-interactive` fails.
1243
+ - **The apps section decides "compact" with `matchMedia("(min-width: 48em)")`** and falls back to
1244
+ desktop (hidden) where `matchMedia` is missing (SSR, jsdom). Tests must stub it to see the section.
1245
+ - **`P` on a focused NavLauncher row pins it** instead of starting type-ahead; in the search field it types the letter. Rows without a star (Recent, key screens, apps) keep type-ahead.
1246
+ - **`NavLauncher` `suggestions` empty falls back to `noResultsHint`** instead of showing chips.
1247
+ - **`PinToggle` / `NewTabLink` are opacity 0 until row hover or focus-within**, so a consumer's own row must carry `data-group` (or `role="group"`) to reveal them.
1248
+ - **`LauncherLabels.screensGroup` is unused** by search now (sections replaced the flat group); kept so label objects still type-check.
1249
+ - **`LauncherTrigger` does not bind its shortcut**; `shortcut={null}` removes the hint and
1250
+ `aria-keyshortcuts`. An explicit `aria-label` hides the visible label from assistive tech, so keep
1251
+ the visible text inside the label (WCAG 2.5.3).
1252
+
1185
1253
  ### `Header`'s desktop submenus open on hover only — no keyboard or click trigger
1186
1254
 
1187
1255
  Dropdown children are gated on local hover state (`onMouseEnter`/`onMouseLeave`), with no `onFocus`.
@@ -1217,6 +1285,9 @@ It's declared on the type but omitted from the component's destructure entirely.
1217
1285
 
1218
1286
  **Apply:** don't pass it expecting it to mark or style the final crumb — there's nothing it can do.
1219
1287
 
1288
+ With `overflow="wrap" | "truncate"` each crumb is `noOfLines={1}` with a `title`, so one very long
1289
+ label ellipsizes; the default `"nowrap"` is unchanged.
1290
+
1220
1291
  ### `Accordion`'s `items[].buttonProps` replaces, not merges, the expanded/hover highlight
1221
1292
 
1222
1293
  It spreads after the built-in `bg`/`_hover` that encode `isExpanded` state — passing `bg` or
@@ -1378,6 +1449,21 @@ it; now `@deprecated` in `SideBarProps.tsx`.
1378
1449
 
1379
1450
  **Apply:** don't pass `selectedSubMenu` expecting it to highlight a submenu row.
1380
1451
 
1452
+ ### `SideBar` pinned rows, `autoCollapse` and `collapseTransition`
1453
+
1454
+ - **`autoCollapse={false}` also silences the `open` prop** (`open` works by calling `changeToggle`).
1455
+ Drive collapse with `toggle` only.
1456
+ - **A pinned id highlights only in the pinned block** while `activePinnedId` matches, and the parent
1457
+ submenu of a pinned child does not auto-open. An `activePinnedId` past `pinnedLimit` expands the
1458
+ list so the active state never disappears.
1459
+ - **Pinned rows are anchors with `href={url}`**: a plain click calls `handleMenuClick` (and closes the
1460
+ overlay), modified clicks fall through to the browser. Unpin is a sibling button, never nested.
1461
+ - **`collapseTransition` affects the `lg`+ inline rail only** — overlay slide, drag and `mobileNav`
1462
+ are untouched.
1463
+ - **The rail focus ring is lifted per surface** (`sidebarFocus.ts`) because `colors.focusRing.ring` is
1464
+ derived for the page canvas and fails 3:1 on the rail for five light brands. The lasting fix is a
1465
+ `sidebar.focusRing` theme token.
1466
+
1381
1467
  ### `KanbanBoard`'s `kanbanSelect`/`onOpen` don't check `permissions` — decision: accepted as-is
1382
1468
 
1383
1469
  Looked like an asymmetry with the gated `kanbanEdit`/`kanbanCreate`/`onDelete`, but on inspection
package/DESIGN-GUIDE.md CHANGED
@@ -29,7 +29,7 @@ Calm, dense, professional SaaS admin — Linear/Stripe-dashboard, not a landing
29
29
  | Page padding | `px={{ base: 4, md: 6 }} py={{ base: 4, md: 5 }}` |
30
30
  | Between page sections | `gap={5}` (a `Flex direction="column"`) |
31
31
  | Between cards in a grid | `gap={4}` |
32
- | Inside a card | `Card`'s own padding — don't add more; `bodyProps={{ p: 0 }}` for edge-to-edge content |
32
+ | Inside a card | `Card`'s own padding — don't add more; `padding="none"` (or `"2xs"`/`"xs"` for dense) instead of `sx={{ p }}`, `bodyProps={{ p: 0 }}` for edge-to-edge content |
33
33
  | Between form fields | `gap={4}` vertical, `gap={3}` side by side |
34
34
  | Inline clusters (tags, meta, icon + text) | `gap={1.5}`–`gap={2}` |
35
35
  | Page header → content | the section gap (`gap={5}`) — no extra margin |
@@ -41,7 +41,7 @@ Borders are the one px value: `borderWidth="1px"`. Fixed widths (side panel, lis
41
41
 
42
42
  | Role | Props |
43
43
  | --- | --- |
44
- | Page title + subtitle | `PageHeader` (`lg`/600 h1, `sm` muted subtitle) — don't hand-build it |
44
+ | Page title + subtitle | `PageHeader` (18px/600 h1 by default, `sm` muted subtitle; `titleSize="xl"` for a top-level page) — don't hand-build it |
45
45
  | Section / card title | `Heading as="h2" fontSize="md" fontWeight={600} color={colors.header[900]}`, in `Card`'s `header` prop |
46
46
  | Body | `Text fontSize="sm" color={colors.text[800]}` |
47
47
  | Label / meta / helper | `Text fontSize="xs" color={colors.textMuted}` |
@@ -139,11 +139,15 @@ the row title is the label: `<Text as="label" htmlFor={id}>` pointing at the fie
139
139
  ```
140
140
 
141
141
  `PageHeader` also takes `breadcrumbs`, `leading` (avatar), `titleAddon` (status `Tag`), `meta` (icon
142
- row) and `tabs` — a record page's whole header. KPI rows are a `SimpleGrid` of `StatCard`s; card
142
+ row) and `tabs` — a record page's whole header. Actions wrap under the title on their own when they do
143
+ not fit (`actionsLayout="auto"`), so don't hand-build a stacked variant. KPI rows are a `SimpleGrid` of `StatCard`s; card
143
144
  sections are `Card` with the title in `header`.
144
145
 
145
146
  | Pattern | Component — don't hand-build it |
146
147
  | --- | --- |
148
+ | Multi-step flow (send, import, onboard) with validated Continue and a review step | `WizardDialog` (`Wizard` inline) — don't hand-roll Back / Continue / Finish around `StageProgress`; each step's `validate` returns its error messages, future steps stay locked until valid, each step's `summary` drives the review |
149
+ | Rich-text template with `${field}` placeholders | `Editor mergeFields={…}` — not a `Select` + caret tracking + dialog beside it; use its `label` / `helperText` / `errorMessage` |
150
+ | A PDF behind auth, shown inline | `PdfViewer getBlob={…}` — the toolbar's Open / Download then work with the token, so don't add your own buttons |
147
151
  | Toolbar: search · filters · active-filter chips · count · actions | `FilterBar` (`search`, children, `activeFilters` + `onRemoveFilter`/`onClearAll`, `summary`, `actions`) |
148
152
  | Read-only label/value grid (record details, key facts) | `DescriptionList` (`items`, `columns`, `layout="inline"` for a side panel, `copyable`, `isLoading`) |
149
153
  | Settings row (title + description left, control right) | `SettingRow` inside a `SettingsSection` (`isDanger` for the danger zone, `action` for a header-right button) |
@@ -160,6 +164,22 @@ sections are `Card` with the title in `header`.
160
164
  scrim `Box`. `Loading` is the older API and stays on the heavier house scrim by default; pass
161
165
  `variant="dim"` to route it through `Backdrop`, or just use `Backdrop` directly.
162
166
 
167
+ ### Navigation shell
168
+
169
+ `SideBar` is the rail, `NavigationBar` the top bar, `NavLauncher` the full-height "go anywhere" panel.
170
+ **The app owns the registry, permissions and persistence**; the library owns the look and the keyboard.
171
+
172
+ - Keep one registry (modules → screens) and derive the rail, the launcher and route guards from it.
173
+ Filter by permission *before* passing `sections` to `NavLauncher`; use `isLoading` while access is
174
+ unknown, `linkAs` for the router link, and `extraResults` + `isSearching` for remote (entity) results.
175
+ - Open the launcher with `<NavigationBar leading={<LauncherTrigger onClick isOpen />} />` — never inside
176
+ `navigationBarText.text`. Neither component binds the chord: the app owns the `mod+k` listener
177
+ (capture phase) and passes `shortcut={false}` to any page `Search`.
178
+ - Pins live in the app (server or local). Pass them to `NavLauncher` (`pinnedIds`, `onTogglePin`) *and*
179
+ to `SideBar pinnedItems` + `onUnpin` so the rail shows what the launcher pinned. Persist collapse with
180
+ `useSidebarPrefs`, and pass `autoCollapse={false}` if the app, not the rail, decides.
181
+ - Show shortcut hints with `KbdShortcut`, never a hand-styled `<kbd>`.
182
+
163
183
  ## 7. Every page has four states
164
184
 
165
185
  Design all four, not just the happy path.
@@ -213,6 +233,9 @@ not shipped in this package). Start from the closest one:
213
233
  | Settings / long form | `settings.tsx` | section nav, `SettingsSection` + `SettingRow`, `SaveBar`, danger zone |
214
234
  | Board | `pipeline.tsx` | `KanbanBoard` with custom cards and column totals (`height`/`maxH` to size it) |
215
235
  | Inbox / master–detail | `inbox.tsx` | selectable list rows, conversation pane, composer, mobile list↔detail |
236
+ | App launcher (browse, pins, recents, search with actions) | `launcher.tsx` (`/examples/launcher`) | `NavLauncher` + `LauncherTrigger` in `NavigationBar leading`, app-owned registry and permission filter, pinned / frequent / recent ids, `linkAs`, remote results via `extraResults`, `isLoading`, `SideBar pinnedItems` |
237
+ | Wizard / multi-step dialog | `wizard.tsx` (`/examples/wizard`) | `WizardDialog` and inline `Wizard`: per-step `validate` messages, locked future steps, error summary, `summary`-driven review, `isFinishing` |
238
+ | Template editor with merge fields | `templateEditor.tsx` (`/examples/template-editor`) | `Editor mergeFields`: grouped Insert field, chips, unknown-field error chips and `onUnknownTokens`, Make dynamic, side preview, label / helper / error |
216
239
  | Shop front (home → listing → product → bag → checkout → order) | `storefront/` | `ProductCard` grid / rail / quick-add, `ProductDetails` + `StickyBuyBar`, `CartItem`, `OrderSummary`, `CouponField`, address/payment groups, `OrderStatusTimeline`, `BagBar`; mobile frame + desktop |
217
240
 
218
241
  ## 10. Shop screens (e-commerce)
@@ -32,6 +32,9 @@ var __importStar = (this && this.__importStar) || (function () {
32
32
  return result;
33
33
  };
34
34
  })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
35
38
  Object.defineProperty(exports, "__esModule", { value: true });
36
39
  const jsx_runtime_1 = require("react/jsx-runtime");
37
40
  const react_1 = require("react");
@@ -43,6 +46,7 @@ const accentText_1 = require("../../Theme/tokens/builders/accentText");
43
46
  const outlineRung_1 = require("../../Theme/tokens/builders/outlineRung");
44
47
  const rowStates_1 = require("../../Theme/tokens/builders/rowStates");
45
48
  const useExclusiveOverlay_1 = require("../../Hooks/useExclusiveOverlay");
49
+ const Kbd_1 = __importDefault(require("../Kbd/Kbd"));
46
50
  const AppMark_1 = __importStar(require("./AppMark"));
47
51
  const AppSwitcherProps_1 = require("./AppSwitcherProps");
48
52
  const useAppSwitcherShortcut_1 = require("./useAppSwitcherShortcut");
@@ -225,7 +229,7 @@ const AppSwitcher = ({ apps, currentAppId, onSelect, onOpenChange, isSearchable,
225
229
  return;
226
230
  }
227
231
  handlePanelKeyDown(e);
228
- }, children: (0, jsx_runtime_1.jsx)(lucide_react_1.Grip, { size: s.icon + 1 }) }) }), (0, jsx_runtime_1.jsx)(react_2.PopoverContent, { w: width, maxW: "calc(100vw - 1.5rem)", textAlign: "start", onKeyDown: handlePanelKeyDown, bg: theme.colors.surfaceRaised, borderRadius: "xl", overflow: "hidden", boxShadow: `0 10px 30px ${theme.colors.boxShadow.default}`, _focus: { outline: "none", boxShadow: `0 10px 30px ${theme.colors.boxShadow.default}` }, children: (0, jsx_runtime_1.jsxs)(react_2.PopoverBody, { p: 0, children: [(0, jsx_runtime_1.jsxs)(react_2.Flex, { align: "center", gap: 2, px: 3, pt: 3, pb: showSearch ? 2 : 1, children: [(0, jsx_runtime_1.jsx)(react_2.Text, { fontSize: "0.6875rem", fontWeight: 600, letterSpacing: "0.08em", textTransform: "uppercase", color: theme.colors.textMuted, children: HEADING }), (0, jsx_runtime_1.jsx)(react_2.Box, { flex: "1" }), hint && ((0, jsx_runtime_1.jsx)(react_2.Text, { as: "kbd", fontSize: "0.6875rem", fontFamily: "inherit", color: theme.colors.textMuted, border: "1px solid", borderRadius: "full", px: 1.5, py: 0.5, children: hint }))] }), showSearch && ((0, jsx_runtime_1.jsx)(react_2.Box, { px: 3, pb: 2, children: (0, jsx_runtime_1.jsxs)(react_2.InputGroup, { size: "sm", children: [(0, jsx_runtime_1.jsx)(react_2.InputLeftElement, { pointerEvents: "none", children: (0, jsx_runtime_1.jsx)(lucide_react_1.Search, { size: 15, color: theme.colors.textMuted }) }), (0, jsx_runtime_1.jsx)(react_2.Input, { ref: searchRef, value: query, onChange: (e) => setQuery(e.target.value), placeholder: "Search apps", borderRadius: "lg", "aria-label": "Search apps", onKeyDown: (e) => {
232
+ }, children: (0, jsx_runtime_1.jsx)(lucide_react_1.Grip, { size: s.icon + 1 }) }) }), (0, jsx_runtime_1.jsx)(react_2.PopoverContent, { w: width, maxW: "calc(100vw - 1.5rem)", textAlign: "start", onKeyDown: handlePanelKeyDown, bg: theme.colors.surfaceRaised, borderRadius: "xl", overflow: "hidden", boxShadow: `0 10px 30px ${theme.colors.boxShadow.default}`, _focus: { outline: "none", boxShadow: `0 10px 30px ${theme.colors.boxShadow.default}` }, children: (0, jsx_runtime_1.jsxs)(react_2.PopoverBody, { p: 0, children: [(0, jsx_runtime_1.jsxs)(react_2.Flex, { align: "center", gap: 2, px: 3, pt: 3, pb: showSearch ? 2 : 1, children: [(0, jsx_runtime_1.jsx)(react_2.Text, { fontSize: "0.6875rem", fontWeight: 600, letterSpacing: "0.08em", textTransform: "uppercase", color: theme.colors.textMuted, children: HEADING }), (0, jsx_runtime_1.jsx)(react_2.Box, { flex: "1" }), hint && (0, jsx_runtime_1.jsx)(Kbd_1.default, { size: "xs", children: hint })] }), showSearch && ((0, jsx_runtime_1.jsx)(react_2.Box, { px: 3, pb: 2, children: (0, jsx_runtime_1.jsxs)(react_2.InputGroup, { size: "sm", children: [(0, jsx_runtime_1.jsx)(react_2.InputLeftElement, { pointerEvents: "none", children: (0, jsx_runtime_1.jsx)(lucide_react_1.Search, { size: 15, color: theme.colors.textMuted }) }), (0, jsx_runtime_1.jsx)(react_2.Input, { ref: searchRef, value: query, onChange: (e) => setQuery(e.target.value), placeholder: "Search apps", borderRadius: "lg", "aria-label": "Search apps", onKeyDown: (e) => {
229
233
  if (e.key === "ArrowDown" && visible.length > 0) {
230
234
  e.preventDefault();
231
235
  focusItem(0);
@@ -52,6 +52,8 @@ export declare const filterApps: (apps: AppOption[], query: string) => AppOption
52
52
  export type ShortcutEvent = Pick<KeyboardEvent, "key" | "altKey" | "ctrlKey" | "metaKey" | "shiftKey">;
53
53
  /** "mod" is Cmd on Apple platforms and Ctrl everywhere else. */
54
54
  export declare const matchesShortcut: (event: ShortcutEvent, shortcut: string, isApple?: boolean) => boolean;
55
+ /** One label per key in a chord, so a named key such as `enter` stays a single cap. */
56
+ export declare const shortcutKeys: (shortcut: string, isApple?: boolean) => string[];
55
57
  export declare const formatShortcut: (shortcut: string, isApple?: boolean) => string;
56
58
  /** Input to `buildSuiteApps`: an `AppOption` keyed by its subdomain label, with `url` optional since it can be derived. */
57
59
  export interface SuiteAppSource extends Omit<AppOption, "url"> {
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.buildSuiteApps = exports.formatShortcut = exports.matchesShortcut = exports.filterApps = exports.appInitials = void 0;
3
+ exports.buildSuiteApps = exports.formatShortcut = exports.shortcutKeys = exports.matchesShortcut = exports.filterApps = exports.appInitials = void 0;
4
4
  const NON_WORD = /[^\p{L}\p{N}]+/u;
5
5
  /** Up to two initials, so a mark stays legible without an icon. */
6
6
  const appInitials = (name) => {
@@ -48,12 +48,15 @@ const matchesShortcut = (event, shortcut, isApple = typeof navigator !== "undefi
48
48
  exports.matchesShortcut = matchesShortcut;
49
49
  const APPLE_SYMBOLS = { mod: "⌘", cmd: "⌘", meta: "⌘", ctrl: "⌃", shift: "⇧", alt: "⌥" };
50
50
  const OTHER_LABELS = { mod: "Ctrl", cmd: "Win", meta: "Win", ctrl: "Ctrl", shift: "Shift", alt: "Alt" };
51
- const formatShortcut = (shortcut, isApple = typeof navigator !== "undefined" && /Mac|iPhone|iPad/.test(navigator.platform)) => {
51
+ const isAppleNow = () => typeof navigator !== "undefined" && /Mac|iPhone|iPad/.test(navigator.platform);
52
+ /** One label per key in a chord, so a named key such as `enter` stays a single cap. */
53
+ const shortcutKeys = (shortcut, isApple = isAppleNow()) => {
52
54
  const parts = shortcut.toLowerCase().split("+").map((p) => p.trim()).filter(Boolean);
53
55
  const table = isApple ? APPLE_SYMBOLS : OTHER_LABELS;
54
- const rendered = parts.map((p, i) => { var _a; return i === parts.length - 1 ? p.toUpperCase() : ((_a = table[p]) !== null && _a !== void 0 ? _a : p.toUpperCase()); });
55
- return isApple ? rendered.join("") : rendered.join("+");
56
+ return parts.map((p, i) => { var _a; return (i === parts.length - 1 ? p.toUpperCase() : ((_a = table[p]) !== null && _a !== void 0 ? _a : p.toUpperCase())); });
56
57
  };
58
+ exports.shortcutKeys = shortcutKeys;
59
+ const formatShortcut = (shortcut, isApple = isAppleNow()) => (0, exports.shortcutKeys)(shortcut, isApple).join(isApple ? "" : "+");
57
60
  exports.formatShortcut = formatShortcut;
58
61
  /**
59
62
  * Derives every app's origin from the host the consumer is already served on, so one deployment
@@ -1,2 +1,2 @@
1
1
  import { BreadcrumbsProps } from "./BreadcrumbsProps";
2
- export default function Breadcrumbs({ spacing, separator, fontWeight, fontSize, isCurrentPage, handleClick, items, }: BreadcrumbsProps): import("react/jsx-runtime").JSX.Element;
2
+ export default function Breadcrumbs({ spacing, separator, fontWeight, fontSize, isCurrentPage, handleClick, items, overflow, maxItems, labels, "data-testid": testId, }: BreadcrumbsProps): import("react/jsx-runtime").JSX.Element;
@@ -4,19 +4,42 @@ exports.default = Breadcrumbs;
4
4
  const jsx_runtime_1 = require("react/jsx-runtime");
5
5
  const react_1 = require("@chakra-ui/react");
6
6
  const useCustomTheme_1 = require("../../Theme/useCustomTheme");
7
+ const focusRing_styles_1 = require("../../Theme/chakra/focusRing.styles");
7
8
  const breadcrumbInk_1 = require("./breadcrumbInk");
8
- function Breadcrumbs({ spacing, separator, fontWeight, fontSize, isCurrentPage, handleClick, items, }) {
9
+ const DEFAULT_MAX_ITEMS = 3;
10
+ const MIN_MAX_ITEMS = 2;
11
+ const OVERFLOW_SX = {
12
+ nowrap: undefined,
13
+ wrap: { "& ol": { flexWrap: "wrap", rowGap: "4px" }, "& li": { maxWidth: "100%" } },
14
+ truncate: { "& ol": { flexWrap: "nowrap" }, "& li": { minWidth: 0 } },
15
+ };
16
+ const collapse = (entries, maxItems) => {
17
+ const keep = Math.max(MIN_MAX_ITEMS, Math.floor(maxItems));
18
+ if (entries.length <= keep)
19
+ return { head: entries, hidden: [], tail: [] };
20
+ return { head: entries.slice(0, 1), hidden: entries.slice(1, entries.length - (keep - 1)), tail: entries.slice(entries.length - (keep - 1)) };
21
+ };
22
+ function Breadcrumbs({ spacing, separator, fontWeight, fontSize, isCurrentPage, handleClick, items, overflow = "nowrap", maxItems = DEFAULT_MAX_ITEMS, labels, "data-testid": testId, }) {
9
23
  const theme = (0, react_1.useTheme)();
10
24
  const { colors } = (0, useCustomTheme_1.useCustomTheme)();
11
25
  const ink = (0, breadcrumbInk_1.breadcrumbInk)(colors);
12
26
  const effectiveSeparator = separator !== null && separator !== void 0 ? separator : ((0, jsx_runtime_1.jsx)(react_1.Box, { as: "span", color: ink.separator, children: "/" }));
13
- return ((0, jsx_runtime_1.jsx)(react_1.Breadcrumb, { spacing: spacing, separator: effectiveSeparator, fontWeight: fontWeight, fontSize: fontSize, fontFamily: theme.fonts.body, children: items.map((item, index) => {
14
- const isLast = isCurrentPage && index === items.length - 1;
15
- return ((0, jsx_runtime_1.jsx)(react_1.BreadcrumbItem, { isCurrentPage: isLast, children: (0, jsx_runtime_1.jsx)(react_1.BreadcrumbLink, { href: item.path, color: isLast ? ink.current : ink.link, fontWeight: isLast ? 600 : fontWeight, sx: {
16
- "&:hover, &:active": { textDecoration: "none", color: isLast ? ink.current : ink.linkHover },
17
- }, onClick: (event) => {
18
- event.preventDefault();
19
- handleClick(item.path);
20
- }, children: item.label }) }, index));
21
- }) }));
27
+ const clip = overflow !== "nowrap";
28
+ const clipProps = clip ? { noOfLines: 1 } : {};
29
+ const renderCrumb = ({ crumb, index }) => {
30
+ const isLast = Boolean(isCurrentPage) && index === items.length - 1;
31
+ const color = isLast ? ink.current : ink.link;
32
+ const common = { ...clipProps, title: clip ? crumb.label : undefined, color, fontWeight: isLast ? 600 : fontWeight };
33
+ return ((0, jsx_runtime_1.jsx)(react_1.BreadcrumbItem, { isCurrentPage: isLast, children: crumb.path === undefined ? ((0, jsx_runtime_1.jsx)(react_1.Box, { as: "span", "aria-current": isLast ? "page" : undefined, ...common, children: crumb.label })) : ((0, jsx_runtime_1.jsx)(react_1.BreadcrumbLink, { href: crumb.path, ...common, sx: { "&:hover, &:active": { textDecoration: "none", color: isLast ? ink.current : ink.linkHover } }, onClick: (event) => {
34
+ event.preventDefault();
35
+ handleClick(crumb.path);
36
+ }, children: crumb.label })) }, index));
37
+ };
38
+ const renderMenu = (hidden) => {
39
+ var _a;
40
+ return ((0, jsx_runtime_1.jsx)(react_1.BreadcrumbItem, { sx: { flexShrink: 0 }, children: (0, jsx_runtime_1.jsxs)(react_1.Menu, { isLazy: true, placement: "bottom-start", children: [(0, jsx_runtime_1.jsx)(react_1.MenuButton, { type: "button", "aria-label": ((_a = labels === null || labels === void 0 ? void 0 : labels.showMore) !== null && _a !== void 0 ? _a : ((n) => `Show ${n} more`))(hidden.length), color: ink.link, borderRadius: "md", px: 1, _hover: { color: ink.linkHover }, sx: (0, focusRing_styles_1.focusVisibleStyles)(colors), children: "\u2026" }), (0, jsx_runtime_1.jsx)(react_1.MenuList, { children: hidden.map(({ crumb, index }) => ((0, jsx_runtime_1.jsx)(react_1.MenuItem, { isDisabled: crumb.path === undefined, onClick: () => crumb.path !== undefined && handleClick(crumb.path), children: crumb.label }, index))) })] }) }, "overflow"));
41
+ };
42
+ const entries = items.map((crumb, index) => ({ crumb, index }));
43
+ const { head, hidden, tail } = overflow === "truncate" ? collapse(entries, maxItems) : { head: entries, hidden: [], tail: [] };
44
+ return ((0, jsx_runtime_1.jsxs)(react_1.Breadcrumb, { spacing: spacing, separator: effectiveSeparator, fontWeight: fontWeight, fontSize: fontSize, fontFamily: theme.fonts.body, sx: OVERFLOW_SX[overflow], "data-testid": testId, children: [head.map(renderCrumb), hidden.length > 0 && renderMenu(hidden), tail.map(renderCrumb)] }));
22
45
  }
@@ -4,11 +4,25 @@ export type BreadcrumbsProps = Pick<BreadcrumbProps, "spacing" | "separator" | "
4
4
  isCurrentPage?: boolean;
5
5
  /** Accepted but never read by Breadcrumbs — has no effect at all. */
6
6
  isLastChild?: boolean;
7
+ /** Called with a crumb's path; never called for a label-only crumb (no `path`). */
7
8
  handleClick: (path: string) => void;
8
9
  items: BreadcrumbItem[];
10
+ /** "wrap" flows crumbs onto new lines; "truncate" keeps one line and folds middle crumbs into a "…" menu. Default "nowrap". */
11
+ overflow?: "nowrap" | "wrap" | "truncate";
12
+ /** With overflow "truncate": most crumbs shown (first plus the last `maxItems - 1`); the rest go in the menu. Default 3, minimum 2. */
13
+ maxItems?: number;
14
+ /** Visible-string overrides; `showMore` is the collapsed menu button's aria-label. */
15
+ labels?: BreadcrumbsLabels;
16
+ "data-testid"?: string;
9
17
  };
10
- /** One crumb: `label` is the visible text, `path` is passed to both the link's href and `handleClick`. */
18
+ /** Visible strings, each with an English default. */
19
+ export type BreadcrumbsLabels = {
20
+ /** Default: `Show ${count} more`. */
21
+ showMore?: (count: number) => string;
22
+ };
23
+ /** One crumb: `label` is the visible text; `path` goes to the link's href and `handleClick`. */
11
24
  export type BreadcrumbItem = {
12
- path: string;
25
+ /** Omit for a label-only crumb: rendered as text, not a link, never passed to `handleClick`. */
26
+ path?: string;
13
27
  label: string;
14
28
  };
@@ -1,4 +1,5 @@
1
- declare const Card: import("react").ForwardRefExoticComponent<Omit<import("@chakra-ui/react").CardProps, "color" | "colorScheme" | "direction" | "size" | "justify" | "children" | "variant" | "align"> & {
1
+ import { CardPadding } from "./CardProps";
2
+ declare const Card: import("react").ForwardRefExoticComponent<Omit<import("@chakra-ui/react").CardProps, "color" | "colorScheme" | "direction" | "padding" | "size" | "justify" | "children" | "variant" | "align"> & {
2
3
  dividercolor?: "gray" | "red" | "orange" | "yellow" | "green" | "teal" | "blue" | "cyan" | "purple" | "pink" | "whiteAlpha" | "blackAlpha";
3
4
  dividersize?: string;
4
5
  dividervariant?: "solid" | "dashed";
@@ -13,6 +14,7 @@ declare const Card: import("react").ForwardRefExoticComponent<Omit<import("@chak
13
14
  direction?: import("@chakra-ui/react").ResponsiveValue<"row" | "column" | "row-reverse" | "column-reverse">;
14
15
  align?: import("@chakra-ui/react").CardBodyProps["alignItems"];
15
16
  justify?: import("@chakra-ui/react").CardBodyProps["justifyContent"];
17
+ padding?: CardPadding;
16
18
  bodyProps?: import("@chakra-ui/react").CardBodyProps;
17
19
  } & import("react").RefAttributes<HTMLDivElement>>;
18
20
  export default Card;
@@ -3,11 +3,21 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const jsx_runtime_1 = require("react/jsx-runtime");
4
4
  const react_1 = require("react");
5
5
  const react_2 = require("@chakra-ui/react");
6
- const Card = (0, react_1.forwardRef)(function Card({ header, footer, children, color, colorScheme, direction = "column", align, justify, bodyProps, ...rest }, ref) {
6
+ const HEADER_PADDING_WHEN_FLUSH = "0.75rem";
7
+ const PADDING_VALUES = {
8
+ none: "0",
9
+ "2xs": "0.375rem",
10
+ xs: "0.75rem",
11
+ sm: "0.75rem 1rem",
12
+ md: "1rem 1.25rem",
13
+ lg: "1.25rem 1.5rem",
14
+ };
15
+ const Card = (0, react_1.forwardRef)(function Card({ header, footer, children, color, colorScheme, direction = "column", align, justify, bodyProps, padding, ...rest }, ref) {
7
16
  const cardProps = { ...rest };
8
17
  delete cardProps.dividercolor;
9
18
  delete cardProps.dividersize;
10
19
  delete cardProps.dividervariant;
11
- return ((0, jsx_runtime_1.jsxs)(react_2.Card, { ref: ref, colorScheme: colorScheme !== null && colorScheme !== void 0 ? colorScheme : color, ...cardProps, children: [header && (0, jsx_runtime_1.jsx)(react_2.CardHeader, { children: header }), (0, jsx_runtime_1.jsx)(react_2.CardBody, { display: "flex", flexDirection: direction, alignItems: align, justifyContent: justify, ...bodyProps, children: children }), footer && (0, jsx_runtime_1.jsx)(react_2.CardFooter, { children: footer })] }));
20
+ const sx = padding ? { "--card-padding": PADDING_VALUES[padding], ...cardProps.sx } : cardProps.sx;
21
+ return ((0, jsx_runtime_1.jsxs)(react_2.Card, { ref: ref, colorScheme: colorScheme !== null && colorScheme !== void 0 ? colorScheme : color, ...cardProps, sx: sx, children: [header && ((0, jsx_runtime_1.jsx)(react_2.CardHeader, { sx: padding === "none" ? { "--card-padding": HEADER_PADDING_WHEN_FLUSH } : undefined, children: header })), (0, jsx_runtime_1.jsx)(react_2.CardBody, { display: "flex", flexDirection: direction, alignItems: align, justifyContent: justify, ...bodyProps, children: children }), footer && (0, jsx_runtime_1.jsx)(react_2.CardFooter, { children: footer })] }));
12
22
  });
13
23
  exports.default = Card;
@@ -6,8 +6,10 @@ type DeadDividerProps = {
6
6
  dividersize?: string;
7
7
  dividervariant?: "solid" | "dashed";
8
8
  };
9
+ /** Padding steps: "none" flush, "2xs" 0.375rem, "xs" 0.75rem all round, "sm" / "md" / "lg" match `size`. */
10
+ export type CardPadding = "none" | "2xs" | "xs" | "sm" | "md" | "lg";
9
11
  /** Themed outline surface with optional header/footer slots; children and direction/align/justify apply to the body. */
10
- export type CardProps = Omit<ChakraCardProps, "children" | "variant" | "size" | "color" | "colorScheme" | "direction" | "align" | "justify"> & DeadDividerProps & {
12
+ export type CardProps = Omit<ChakraCardProps, "children" | "variant" | "size" | "color" | "colorScheme" | "direction" | "align" | "justify" | "padding"> & DeadDividerProps & {
11
13
  /** Rendered in CardHeader (semibold header ink) only when truthy. */
12
14
  header?: React.ReactNode;
13
15
  /** Rendered in CardFooter only when truthy. */
@@ -27,6 +29,8 @@ export type CardProps = Omit<ChakraCardProps, "children" | "variant" | "size" |
27
29
  align?: CardBodyProps["alignItems"];
28
30
  /** Body justifyContent; `bodyProps` wins if it sets the same thing. */
29
31
  justify?: CardBodyProps["justifyContent"];
32
+ /** Overrides `size`'s padding for header, body and footer; "none" is flush (a header keeps "xs"), "2xs" is 6px. */
33
+ padding?: CardPadding;
30
34
  /** Props for the body wrapper, e.g. `p={0}` for edge-to-edge media. */
31
35
  bodyProps?: CardBodyProps;
32
36
  };