laif-ds 1.0.0 → 1.0.2-app-data-grid.76

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 (76) hide show
  1. package/dist/CHANGELOG.md +282 -0
  2. package/dist/_virtual/index7.js +5 -2
  3. package/dist/_virtual/index8.js +2 -5
  4. package/dist/agent-docs/adoption-report.json +164 -83
  5. package/dist/agent-docs/components/AppBottomNav.md +180 -49
  6. package/dist/agent-docs/components/AppCard.md +25 -21
  7. package/dist/agent-docs/components/AppDataGrid.md +2602 -0
  8. package/dist/agent-docs/components/AppEditor.md +18 -13
  9. package/dist/agent-docs/components/AppSelect.md +50 -0
  10. package/dist/agent-docs/components/AppSidebar.md +504 -11
  11. package/dist/agent-docs/components/DataTable.md +34 -3
  12. package/dist/agent-docs/components/DatePicker.md +7 -7
  13. package/dist/agent-docs/components/FileUploader.md +12 -12
  14. package/dist/agent-docs/components/GridFilterPanel.md +112 -0
  15. package/dist/agent-docs/components/Sidebar.md +72 -1
  16. package/dist/agent-docs/components/SidebarMenuBadge.md +87 -0
  17. package/dist/agent-docs/components-list.md +47 -2
  18. package/dist/agent-docs/manifest.json +310 -36
  19. package/dist/components/design-tokens.js +138 -66
  20. package/dist/components/nav-tokens.js +41 -0
  21. package/dist/components/ui/app-bottom-nav.js +125 -130
  22. package/dist/components/ui/app-data-grid/app-data-grid.js +874 -0
  23. package/dist/components/ui/app-data-grid/engine/use-grid-column-resize.js +110 -0
  24. package/dist/components/ui/app-data-grid/engine/use-grid-engine.js +598 -0
  25. package/dist/components/ui/app-data-grid/engine/use-grid-infinite-loader.js +50 -0
  26. package/dist/components/ui/app-data-grid/engine/use-grid-virtualizer.js +90 -0
  27. package/dist/components/ui/app-data-grid/grid-aggregate.js +5 -0
  28. package/dist/components/ui/app-data-grid/grid-column-registry.js +28 -0
  29. package/dist/components/ui/app-data-grid/grid-columns.js +204 -0
  30. package/dist/components/ui/app-data-grid/grid-filter-compile.js +33 -0
  31. package/dist/components/ui/app-data-grid/grid-filter-operators.js +139 -0
  32. package/dist/components/ui/app-data-grid/grid-grouping.js +104 -0
  33. package/dist/components/ui/app-data-grid/grid-header-matrix.js +112 -0
  34. package/dist/components/ui/app-data-grid/grid-i18n.js +207 -0
  35. package/dist/components/ui/app-data-grid/grid-mount-count.js +8 -0
  36. package/dist/components/ui/app-data-grid/grid-reducer.js +200 -0
  37. package/dist/components/ui/app-data-grid/grid-result-scope.js +35 -0
  38. package/dist/components/ui/app-data-grid/grid-tokens.js +169 -0
  39. package/dist/components/ui/app-data-grid/grid-view-preset.js +97 -0
  40. package/dist/components/ui/app-data-grid/parts/grid-actions-row.js +86 -0
  41. package/dist/components/ui/app-data-grid/parts/grid-cell-text.js +67 -0
  42. package/dist/components/ui/app-data-grid/parts/grid-column-registry-panel.js +320 -0
  43. package/dist/components/ui/app-data-grid/parts/grid-density-control.js +88 -0
  44. package/dist/components/ui/app-data-grid/parts/grid-filter-chips.js +224 -0
  45. package/dist/components/ui/app-data-grid/parts/grid-filter-editor.js +197 -0
  46. package/dist/components/ui/app-data-grid/parts/grid-filter-panel.js +386 -0
  47. package/dist/components/ui/app-data-grid/parts/grid-filter-row.js +273 -0
  48. package/dist/components/ui/app-data-grid/parts/grid-fullscreen-toggle.js +29 -0
  49. package/dist/components/ui/app-data-grid/parts/grid-group-row.js +135 -0
  50. package/dist/components/ui/app-data-grid/parts/grid-header.js +338 -0
  51. package/dist/components/ui/app-data-grid/parts/grid-pagination-control.js +218 -0
  52. package/dist/components/ui/app-data-grid/parts/grid-row.js +181 -0
  53. package/dist/components/ui/app-data-grid/parts/grid-scroll-area.js +186 -0
  54. package/dist/components/ui/app-data-grid/parts/grid-selection-bar.js +195 -0
  55. package/dist/components/ui/app-data-grid/parts/grid-status-surface.js +66 -0
  56. package/dist/components/ui/app-data-grid/parts/grid-table.js +482 -0
  57. package/dist/components/ui/app-data-grid/parts/grid-toolbar-slots.js +192 -0
  58. package/dist/components/ui/app-data-grid/use-grid-filters.js +173 -0
  59. package/dist/components/ui/app-select.js +332 -222
  60. package/dist/components/ui/app-sidebar-mobile.js +119 -104
  61. package/dist/components/ui/app-sidebar.js +516 -313
  62. package/dist/components/ui/button.js +2 -1
  63. package/dist/components/ui/gantt/components/Chart/Chart.js +1 -1
  64. package/dist/components/ui/nav-badge.js +66 -0
  65. package/dist/components/ui/sidebar.js +272 -207
  66. package/dist/components/ui/tables/data-cross-table/data-cross-table.js +6 -6
  67. package/dist/components/ui/tables/data-table/data-table-constants.js +36 -16
  68. package/dist/components/ui/tables/data-table/data-table-i18n.js +135 -1
  69. package/dist/css-for-template.css +27 -0
  70. package/dist/index.d.ts +5872 -144
  71. package/dist/index.js +473 -406
  72. package/dist/node_modules/eventemitter3/index2.js +1 -1
  73. package/dist/styles.css +2 -1
  74. package/dist/styles.v3.css +1 -1
  75. package/package.json +25 -7
  76. /package/dist/agent-docs/components/{truncated-cell.md → TruncatedCell.md} +0 -0
@@ -23,20 +23,26 @@ scrolls instead of the page container the bar scrolls off-screen, and any
23
23
  horizontal padding on an ancestor breaks the full bleed — that one is a layout
24
24
  bug, not only a cosmetic one.
25
25
 
26
- Total height is **55px** (measured) plus the safe-area reserve, which the
27
- component adds itself (`pb-[env(safe-area-inset-bottom)]`, with no `max()`
28
- floor) — budget that column in the page layout. It breaks down as 1px of
29
- `border-t` + 4px of top inset + a 46px pill (34px of content — a 16px icon, a
30
- 2px gap and a 16px line — plus 12px of vertical padding) + 4px of bottom inset.
26
+ Total height is **67px** — 12px over the 55px that was measured when the cell
27
+ had `py-1.5` and a 16px glyph, which is exactly what those two changes add —
28
+ plus the safe-area reserve, which the component adds itself
29
+ (`pb-[env(safe-area-inset-bottom)]`, with no `max()` floor). Budget that column
30
+ in the page layout. It breaks down as 1px of `border-t` + 4px of top inset + a
31
+ 58px pill (38px of content — a 20px icon, a 2px gap and a 16px line — plus 20px
32
+ of vertical padding) + 4px of bottom inset.
31
33
  The reserve is **painted bar surface**: under a home indicator you see bar
32
34
  colour, not page.
33
35
 
34
- The 46px cell clears the 44px touch floor (`min-h-11`) by **2px**, where the
35
- 20px-icon version cleared it by 6. Measured, `min-h-11` is not engaged — the
36
- cell renders at 46px, so the height still comes from `py-1.5` — but the margin
37
- is thin: trim the padding, the icon or the label's line-height any further and
38
- `min-h-11` takes the height over, at which point the written padding stops
39
- describing what is rendered. **Measure the cell, not the padding.**
36
+ The vertical padding is `py-2.5`, taken from `designTokens.sizes.touch` — the
37
+ only part of that token a six-cell row can afford (`px-4` would leave 29px for
38
+ the label, `text-sm` would widen it further), and the same value the mobile
39
+ sidebar rows get from the token entire. The 58px cell clears the 44px touch
40
+ floor (`min-h-11`) by **14px**, where `py-1.5` over a 16px glyph cleared it by
41
+ 2. `min-h-11` is not engaged — the height comes from the padding and the
42
+ content — and the margin is now wide enough that it stays that way: at the old
43
+ values any further trim to the padding, the icon or the label's line-height
44
+ would have handed the height to `min-h-11`, at which point the written padding
45
+ stops describing what is rendered. **Measure the cell, not the padding.**
40
46
 
41
47
  It must be rendered inside a `SidebarProvider` (it reads the sidebar context to
42
48
  open the mobile panel); outside one, `useSidebar()` throws.
@@ -51,7 +57,8 @@ open the mobile panel); outside one, `useSidebar()` throws.
51
57
  | `showMenu` | `boolean` | `true` | `false` drops the Menu cell and spends its slot on a sixth destination. Only for an app that opens the mobile panel elsewhere — see below. |
52
58
  | `menuLabel` | `string` | `"Menu"` | Label of the menu cell. |
53
59
  | `ariaLabel` | `string` | `"Main navigation"` | Accessible name of the `<nav>` landmark. |
54
- | `menuIconName` | `IconName` | `"Menu"` | Icon of the menu cell. |
60
+ | `menuIcon` | `IconName \| (() => React.ReactNode)` | `"Menu"` | The menu cell's glyph: a lucide name, or a function returning the node to draw for shapes lucide lacks. Nullish back from the function falls back to the default. |
61
+ | `menuBadge` | `boolean` | `false` | A dot on the Menu cell — **never** a count. OR-ed with the badged items dropped past the cell budget. |
55
62
  | `linkComponent` | `React.ComponentType<React.AnchorHTMLAttributes<HTMLAnchorElement> & { href: string }>` | `undefined` | Router link component (e.g. Next.js `Link`); plain `<a>` otherwise. |
56
63
  | `linkProps` | `Record<string, unknown>` | `{}` | Extra props forwarded to `linkComponent`. |
57
64
  | `className` | `string` | `undefined` | Merged onto the dock wrapper. |
@@ -65,13 +72,15 @@ Inherits the remaining props from `Omit<React.ComponentProps<"div">, "children">
65
72
  | ----------- | -------------- | ------------ | ---------------------------------------------------------------------------------------------- |
66
73
  | `title` | `string` | **required** | Visible label. **Must equal the sidebar `NavItem.title`** of the same destination (see below). |
67
74
  | `url` | `string` | **required** | Ignored when `subItems` is non-empty; pass `"#"` there by convention. |
68
- | `iconName` | `IconName` | **required** | No fallback — at 16px the glyph no longer leads; what makes it required is the row, where a cell without one breaks the rhythm and reads as a failed image. |
75
+ | `iconName` | `IconName` | **required** | No fallback — at 20px the glyph is what the eye lands on first, and a cell without one breaks the rhythm of the row and reads as a failed image. |
69
76
  | `isActive` | `boolean` | `undefined` | Leaf level only — the subtree state is derived from `subItems`. |
70
77
  | `subItems` | `NavSubItem[]` | `undefined` | Presence makes the cell open the mobile menu at this level instead of navigating. |
71
78
  | `ariaLabel` | `string` | `undefined` | Only when the spoken name must differ from the label; it must still contain the visible text. |
79
+ | `badge` | `number \| string \| true` | `undefined` | Count, short text or dot — the same field as `AppSidebar`'s `NavItem.badge`. This surface is `compact`: numbers render as a superscript on the glyph, **text degrades to a dot**. |
80
+ | `badgeLabel`| `string` | `undefined` | The spoken form of `badge`, announced by an `sr-only` span inside the cell. Effectively required for `badge: true`. |
72
81
 
73
82
  `NavSubItem` is the same type `AppSidebar` uses:
74
- `{ title: string; url: string; isActive?: boolean; iconName?: IconName }`.
83
+ `{ title: string; url: string; isActive?: boolean; iconName?: IconName; badge?: number | string | true; badgeLabel?: string }`.
75
84
 
76
85
  ---
77
86
 
@@ -90,7 +99,7 @@ Inherits the remaining props from `Omit<React.ComponentProps<"div">, "children">
90
99
  a flex item's automatic minimum size is its min-content width. `min-w-0`
91
100
  removes that escape hatch, so the label truncates instead of the row going
92
101
  uneven. (Without it the row would still be even at these widths — the
93
- min-content floor is 24px, a `shrink-0` 16px icon plus `px-1`, against a
102
+ min-content floor is 28px, a `shrink-0` 20px icon plus `px-1`, against a
94
103
  narrowest cell of 52px — but by arithmetic rather than by construction.)
95
104
  - **Six is a maximum, not a count.** Nothing pads the row: with fewer items the
96
105
  cells simply get fatter, as every native tab bar does, and Menu stays pinned
@@ -128,34 +137,36 @@ Inherits the remaining props from `Omit<React.ComponentProps<"div">, "children">
128
137
  hint on some cells only breaks the even rhythm that makes a tab bar scannable.
129
138
  The feedback is the reveal itself.
130
139
 
131
- ### Icon size: a deliberate divergence from the native baselines
132
-
133
- Cell icons are `Icon size="xs"` — **16px** — over a 12px (`text-xs`) label. That
134
- is under both native baselines on purpose: iOS HIG draws a tab glyph at ~25pt
135
- over a 10pt label, Material 3 at 24dp over a 12sp one. At the 20px this bar used
136
- before it was already below both; 16px puts it at two thirds of MD3. **This is a
137
- decision, not an oversight — do not "correct" it upwards.** The bar is
138
- label-forward.
139
-
140
- The measurement behind the call: 16px against 20px is −20% linear and −36% in
141
- area, enough that the glyph stops out-reading its label and settles level with
142
- it. The word is what identifies the destination; the icon is what keeps the row
143
- scannable and what makes a cell look intact — which is why `iconName` is still
144
- required with no fallback. The size is the `xs` rung of the shared `Icon` scale
145
- and not a per-call `className` override: an 18px rung was considered and
140
+ ### Icon size
141
+
142
+ Cell icons are `Icon size="sm"` — **20px** — over a 12px (`text-xs`) label, one
143
+ rung up from the `xs` (16px) the bar carried before. Still under both native
144
+ baselines: iOS HIG draws a tab glyph at ~25pt over a 10pt label, Material 3 at
145
+ 24dp over a 12sp one, so 20px sits below MD3 rather than above it.
146
+
147
+ The measurement behind the step: 16px against 20px is −20% linear and −36% in
148
+ area. At 16px the glyph settled *level* with its label — a fine proportion to
149
+ read, a poor one to aim at from across the screen. At 20px the icon leads and
150
+ the label confirms, which is the order a tab bar is scanned in, and it is why
151
+ `iconName` is required with no fallback.
152
+
153
+ The size is a rung of the shared `Icon` scale, not a per-call `className`
154
+ override, and never an intermediate value: an 18px rung was considered and
146
155
  refused, because adding a step to `sizeMap` is a scale decision for ~93
147
- components taken to serve one bar.
156
+ components taken to serve one bar. **The next size up is `md` (24px)** — MD3
157
+ parity, and 4px more bar.
148
158
 
149
- The padding was **not** compensated when the icon shrank: `py-1.5` stayed, so
150
- the bar came down from 59px to 55px. A lower bar was the point of the smaller
151
- glyph, not a side effect of it.
159
+ Height history, so the trade is legible: the bar was 59px with a 20px glyph and
160
+ `py-1.5`, came down to 55px when the glyph shrank to 16px with the padding
161
+ uncompensated, and is 67px now — `py-2.5` from `designTokens.sizes.touch` (+8px,
162
+ spent on the touch target) and the glyph back to 20px (+4px, spent on reach).
152
163
 
153
164
  ### Active state
154
165
 
155
166
  Exactly one cell is ever marked, with `data-active="true"` and a **rounded
156
167
  pill**: `bg-d-sidebar-accent text-d-sidebar-accent-foreground font-bold` on a
157
168
  `rounded-md` (6px) surface, inset 4px from every edge of the dock
158
- (`designTokens.navItem.selectedPill`, with `navItem.basePill` carrying the
169
+ (`navTokens.item.selectedPill`, with `navTokens.item.basePill` carrying the
159
170
  radius on every cell). **No border on any side.**
160
171
 
161
172
  This is the desktop sidebar row's own active state, ported verbatim — the pair
@@ -207,7 +218,7 @@ already because it reads as a selected chip rather than as the current tab.
207
218
  floor in light at 1.98:1, but separating by hue, which 1.4.11 does not measure.
208
219
  A tint of primary is not an option, as the table shows.
209
220
 
210
- The sidebar's `navItem.selected` can still afford a 10% tint because a 2px rule
221
+ The sidebar's `navTokens.item.selected` can still afford a 10% tint because a 2px rule
211
222
  carries the state there; here there is no rule to fall back on.
212
223
 
213
224
  `rounded-md` and not `radius.sm`: `sm` is 4px in the Tailwind v4 bundle and 2px
@@ -220,14 +231,13 @@ claymorphism theme, while the dock around it is a fixed `rounded-t-lg`.
220
231
  - **Parent whose child is current** (any `subItems[].isActive`) → the cell is a
221
232
  `<button>`, marked, **without** `aria-current`: a button is not a page, it is a
222
233
  disclosure. So the app only has to mark the leaf.
223
- - **Nothing in the bar owns the route** → the **Menu** cell takes the pill, with
224
- no `aria-current` and no `aria-expanded="true"`. Five neutral cells read as
225
- "the component failed to work out where I am"; the truthful statement is "the
226
- page you are on is reachable through here", and it is redundant for a screen
227
- reader, so nothing is added to the accessibility tree. With
228
- `showMenu={false}` there is nothing to hand the pill to and the bar does
229
- render entirely neutral — the honest state when every cell is a destination
230
- and none of them is the current one.
234
+ - **Nothing in the bar owns the route** → **no cell is marked**, Menu included.
235
+ The bar renders entirely neutral, exactly as it always has with
236
+ `showMenu={false}`. The Menu cell is a disclosure, not a destination: the pill
237
+ means "this is the page you are on", and handing it to a button that opens a
238
+ panel says that about something which is not a page. A route outside the bar's
239
+ five destinations is a real state, and the honest way to render it is no pill
240
+ at all.
231
241
  - Two active items is an app bug: the first in order wins and one warning is
232
242
  logged.
233
243
 
@@ -255,6 +265,51 @@ no `interaction.active` squash: its scale is for buttons, and a tab-bar cell is
255
265
  surface you move between, not an object you push — an anisotropic scale on a
256
266
  12px truncating label distorts the one thing the cell exists to let you read.
257
267
 
268
+ ### Badges
269
+
270
+ `BottomNavItem.badge` is the same field as `AppSidebar`'s `NavItem.badge` — the
271
+ three forms (count, short text, dot), the `99+` overflow, the `0`-renders-nothing
272
+ rule and the warnings are all documented once, in `AppSidebar.md`. What is
273
+ specific to this surface:
274
+
275
+ - The capacity is **`compact`**: a **number** renders as a 14px superscript on
276
+ the glyph (`99+` included), and a **short text degrades to a dot**. Four
277
+ characters of superscript in a 52px cell either do not read or run into the
278
+ neighbouring cell, so text is not shrunk, it is dropped to the smallest form
279
+ that still says "there is something here".
280
+ - The badge sits on the **glyph**, not beside the label, which is what keeps the
281
+ ~8-character label budget documented above exactly as it was. Measured at
282
+ 320px, the tightest row the bar renders: the label box is 44px in the badged
283
+ story and 44px in `Default` — the badge costs the label nothing.
284
+ - It **does not bleed**. Six cells at 320px give a 44px content box around a
285
+ 20px glyph, so there are 12px of slack per side; the badge reaches 6px past
286
+ the glyph plus 2px of ring, and it sits ~11px below the top edge of the dock,
287
+ so it meets neither the `rounded-t-lg` corners nor the dock's
288
+ `overflow-hidden`.
289
+ - **No interaction with the active pill.** The pill marks the position, the
290
+ badge marks the quantity; inverting the badge on the current cell would tie
291
+ two orthogonal channels together. `ring-2 ring-d-card` is safe over the pill
292
+ unconditionally — pill and card measure 1.10:1 light / 1.23:1 dark against
293
+ each other, so one ring colour works in both states.
294
+ - A cell with `subItems` that has no badge of its own shows a **dot** when one
295
+ of its descendants is badged. Counts never aggregate.
296
+ - **The Menu cell takes `menuBadge`, a dot and never a count**: the Menu opens
297
+ the whole navigation, a superset of `items`, so any number the bar computed
298
+ would be a lie. It is OR-ed with the items **dropped past the cell budget**,
299
+ so a drop cannot hide state as well as a destination — the drop warning still
300
+ fires, naming them. The Menu dot carries no announcement: there is no label to
301
+ give it, and what it stands for is announced on the rows of the panel it
302
+ opens.
303
+ - **Live counts vs static ones** follow the contract written once in
304
+ `AppSidebar.md` ("Live counts vs static ones, and who announces them"): the
305
+ design system assumes neither and renders no live region; when a count changes
306
+ as the direct result of an action on the current screen, the app announces it
307
+ in its own status region next to that action.
308
+
309
+ The `WithBadges` and `WithBadgesSmall` stories are the same five labels as
310
+ `Default`, badged, at 390px and 375px; `MenuBadgeFromDroppedItem` is the drop
311
+ case.
312
+
258
313
  ### Opening the menu
259
314
 
260
315
  Both the Menu cell and any cell with `subItems` open `AppSidebar`'s mobile Sheet
@@ -299,6 +354,13 @@ suppress is noise. Turning it off is a claim; the app has to honour it.
299
354
  implement).
300
355
  - No `title` attributes: they do nothing on touch and some screen readers read
301
356
  them after the name.
357
+ - A badge is `aria-hidden="true"` and its `badgeLabel` rides an in-flow
358
+ `sr-only` span **inside the cell**, so the name composes as "Clienti, 12
359
+ clienti nuovi" and never "Clienti 12". `aria-current` is untouched, on the
360
+ destination cells as on the parent cells (which carry `aria-expanded` and no
361
+ `aria-current`, as above). An aggregate dot — a parent's, or the Menu cell's —
362
+ is visual only: it has no text of its own, and what it stands for is announced
363
+ on the row that owns it.
302
364
  - Focus ring is `designTokens.focusRing` + `focus-visible:ring-inset` — the
303
365
  cells touch each other, so a 3px outset ring would spill over the neighbouring
304
366
  cell and read as if two cells were focused.
@@ -311,18 +373,24 @@ suppress is noise. Turning it off is a claim; the app has to honour it.
311
373
 
312
374
  No entrance animation — the bar is chrome, present from first paint. The only
313
375
  transition is the colour change already carried by `designTokens.focusRing`, so
314
- there is nothing for `prefers-reduced-motion` to suppress. The absence of a
315
- shadow is equally deliberate (see Layout). The sheet's own 300ms open is owned
316
- by `SheetContent`.
376
+ there is nothing for `prefers-reduced-motion` to suppress. **Badges add none**:
377
+ no enter/exit transition, no pulse, no animated counter — a badge appearing is
378
+ not a reason to open that chapter. The absence of a shadow is equally deliberate
379
+ (see Layout). The sheet's own 300ms open is owned by `SheetContent`.
317
380
 
318
381
  ### Warnings (`console.warn`, deduplicated per message)
319
382
 
320
- - more `items` than slots, naming the dropped titles;
383
+ - more `items` than slots, naming the dropped titles — and if one of the dropped
384
+ items was badged, the Menu cell takes a dot so the state is not dropped with
385
+ it;
321
386
  - `items: []` together with `showMenu={false}`, where the bar renders `null`;
322
387
  - two or more active items;
323
388
  - a duplicate `url` + `title` pair (duplicate React keys make React reuse DOM
324
389
  and state across cells);
325
- - an unresolved drill target (logged by `AppSidebarMobile`).
390
+ - an unresolved drill target (logged by `AppSidebarMobile`);
391
+ - an invalid `badge` (negative, non-integer, empty string) or one over 8
392
+ characters, and a `badge: true` with no `badgeLabel` — all logged by the shared
393
+ normaliser, naming the row.
326
394
 
327
395
  ### Edge cases
328
396
 
@@ -398,6 +466,69 @@ export function Layout({ children }: { children: React.ReactNode }) {
398
466
  />
399
467
  ```
400
468
 
469
+ ### A Menu cell of the app's own
470
+
471
+ Both halves of the cell are configurable: `menuLabel` for the copy, `menuIcon`
472
+ for the glyph. **`menuIcon` is one prop with two forms**, told apart by
473
+ `typeof`:
474
+
475
+ | form | when | example |
476
+ | ----------------------- | ------------------------------- | ----------------------------- |
477
+ | `IconName` (string) | the shape is in lucide | `menuIcon="LayoutGrid"` |
478
+ | `() => React.ReactNode` | it is not — a brand mark, an SVG | `menuIcon={() => <AcmeMark/>}` |
479
+
480
+ `IconName` is `keyof typeof LucideIcons`, so the string form already covers the
481
+ whole set — the cell is not tied to the hamburger. Reach for that when the app
482
+ already has one in its header, where two hamburgers on one screen read as two
483
+ different menus.
484
+
485
+ ```tsx
486
+ <AppBottomNav items={bottomNavItems} menuLabel="Altro" menuIcon="LayoutGrid" />
487
+ ```
488
+
489
+ **Why a function and not a `ReactNode`.** A string is itself a valid
490
+ `ReactNode`, so `IconName | ReactNode` collapses to `ReactNode` and there is no
491
+ way left to tell a lucide name from literal text. The function form keeps the
492
+ union discriminable by `typeof menuIcon === "string"` — and one prop instead of
493
+ two makes "both passed" unrepresentable rather than something to warn about.
494
+
495
+ **The function is called, not rendered as a component.** Hooks inside it are
496
+ not supported; in exchange an inline arrow is safe, which is how this gets
497
+ written in practice — as an element type it would change identity on every
498
+ render and remount the subtree.
499
+
500
+ #### A glyph lucide does not have
501
+
502
+ ```tsx
503
+ <AppBottomNav
504
+ items={bottomNavItems}
505
+ menuLabel="Acme"
506
+ menuIcon={() => <AcmeMark />}
507
+ />
508
+ ```
509
+
510
+ Returning nullish falls back to the default glyph, so a conditional icon cannot
511
+ leave the cell bare: the row's argument for a mandatory glyph is that a cell
512
+ without one reads as a failed image, and that does not stop applying here.
513
+
514
+ **The geometry stays the design system's; only the drawing is the app's.** What
515
+ the function returns is rendered inside a box of exactly the size
516
+ `Icon size="sm"` occupies — 20px, `shrink-0` — and a direct `<svg>` child is
517
+ stretched to fill it (`[&>svg]:size-full`), which is the case the wrapper exists
518
+ for: a pasted SVG carries its own `width`/`height` and would otherwise land at
519
+ whatever those say. `preserveAspectRatio` defaults to `meet`, so a non-square
520
+ viewBox is letterboxed inside the box rather than distorted. A child that is not
521
+ a direct `<svg>` has to size itself; centring is handled either way. Measured on
522
+ the `CustomMenuIconNode` story, which returns a 48×32 SVG on purpose: it renders
523
+ at 20×20, the same box as the lucide glyph in the first cell.
524
+
525
+ This is the one cell whose glyph the design system does not control, and so the
526
+ one that can break the rhythm of the other five — a drawing with a different
527
+ stroke weight or optical density will read as foreign next to five lucide
528
+ glyphs, and no wrapper can fix that. `BottomNavItem.iconName` stays name-only
529
+ for exactly that reason: the escape hatch is deliberately this cell's alone.
530
+ **Prefer the string form wherever lucide has the shape.**
531
+
401
532
  ### Without the Menu cell
402
533
 
403
534
  The header trigger is not optional here — it is what keeps the mobile panel
@@ -10,24 +10,24 @@ Enhanced card component built on top of the base Card component. Provides a simp
10
10
 
11
11
  ### AppCardProps
12
12
 
13
- | Prop | Type | Default | Description |
14
- | ------------------ | ------------------------------------------------------------------------------------------------------ | ----------- | ------------------------------------------------------------------- |
15
- | `title` | `string \| React.ReactNode` | `undefined` | Card title displayed in the header. |
16
- | `description` | `string \| React.ReactNode` | `undefined` | Card description displayed below the title. |
17
- | `header` | `React.ReactNode` | `undefined` | Custom header content; overrides `title` and `description`. |
18
- | `body` | `React.ReactNode` | `undefined` | Main content rendered inside CardContent. |
19
- | `children` | `React.ReactNode` | `undefined` | Fallback body content when `body` is not provided. |
20
- | `actions` | `React.ReactNode` | `undefined` | Right-aligned action elements inside the header. |
21
- | `footer` | `React.ReactNode` | `undefined` | Footer content (typically action buttons). |
22
- | `variant` | `AppCardVariant` | `"default"` | Visual variant of the card (see table below). |
23
- | `size` | `"sm" \| "default" \| "lg" \| "none"` | `undefined` | Controls internal padding via the base Card component. |
24
- | `isLoading` | `boolean` | `false` | When true, renders skeleton placeholders instead of content. |
25
- | `showHeaderBorder` | `boolean` | `false` | Displays a border between the header and body sections. |
26
- | `showFooterBorder` | `boolean` | `false` | Displays a border between the body and footer sections. |
27
- | `className` | `string` | `undefined` | Additional classes for the card container. |
28
- | `headerClassName` | `string` | `undefined` | Additional classes for the header section. |
29
- | `bodyClassName` | `string` | `undefined` | Additional classes for the body section. |
30
- | `footerClassName` | `string` | `undefined` | Additional classes for the footer section. |
13
+ | Prop | Type | Default | Description |
14
+ | ------------------ | ------------------------------------- | ----------- | ------------------------------------------------------------ |
15
+ | `title` | `string \| React.ReactNode` | `undefined` | Card title displayed in the header. |
16
+ | `description` | `string \| React.ReactNode` | `undefined` | Card description displayed below the title. |
17
+ | `header` | `React.ReactNode` | `undefined` | Custom header content; overrides `title` and `description`. |
18
+ | `body` | `React.ReactNode` | `undefined` | Main content rendered inside CardContent. |
19
+ | `children` | `React.ReactNode` | `undefined` | Fallback body content when `body` is not provided. |
20
+ | `actions` | `React.ReactNode` | `undefined` | Right-aligned action elements inside the header. |
21
+ | `footer` | `React.ReactNode` | `undefined` | Footer content (typically action buttons). |
22
+ | `variant` | `AppCardVariant` | `"default"` | Visual variant of the card (see table below). |
23
+ | `size` | `"sm" \| "default" \| "lg" \| "none"` | `undefined` | Controls internal padding via the base Card component. |
24
+ | `isLoading` | `boolean` | `false` | When true, renders skeleton placeholders instead of content. |
25
+ | `showHeaderBorder` | `boolean` | `false` | Displays a border between the header and body sections. |
26
+ | `showFooterBorder` | `boolean` | `false` | Displays a border between the body and footer sections. |
27
+ | `className` | `string` | `undefined` | Additional classes for the card container. |
28
+ | `headerClassName` | `string` | `undefined` | Additional classes for the header section. |
29
+ | `bodyClassName` | `string` | `undefined` | Additional classes for the body section. |
30
+ | `footerClassName` | `string` | `undefined` | Additional classes for the footer section. |
31
31
 
32
32
  ### Variant Reference
33
33
 
@@ -203,13 +203,17 @@ export function NestedCards() {
203
203
  <div className="flex flex-col gap-3">
204
204
  <AppCard
205
205
  title="Nested Item A"
206
- body={<p className="text-sm">Flat cards have no border or shadow.</p>}
206
+ body={
207
+ <p className="text-sm">Flat cards have no border or shadow.</p>
208
+ }
207
209
  variant="flat"
208
210
  size="sm"
209
211
  />
210
212
  <AppCard
211
213
  title="Nested Item B"
212
- body={<p className="text-sm">Good for secondary content regions.</p>}
214
+ body={
215
+ <p className="text-sm">Good for secondary content regions.</p>
216
+ }
213
217
  variant="flat"
214
218
  size="sm"
215
219
  />
@@ -228,7 +232,7 @@ import { AppCard } from "laif-ds";
228
232
 
229
233
  export function SizeVariants() {
230
234
  return (
231
- <div className="flex flex-col gap-6 w-[400px]">
235
+ <div className="flex w-[400px] flex-col gap-6">
232
236
  <AppCard
233
237
  size="sm"
234
238
  title="Size: Small"