laif-ds 0.2.92 → 1.0.0

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.
@@ -8,24 +8,68 @@ High-level app sidebar built on top of `Sidebar` primitives. Renders navigation
8
8
 
9
9
  ## Props
10
10
 
11
- | Prop | Type | Default | Description |
12
- | ------------------ | --------------------------------- | ------------- | ------------------------------------------ |
13
- | `navigation` | `NavGroup[]` | **required** | Main navigation groups. |
14
- | `navigationFooter` | `NavGroup[]` | `undefined` | Footer navigation groups. |
15
- | `versions` | `string[]` | `undefined` | Optional versions list. |
16
- | `defaultVersion` | `string` | `undefined` | Default selected version. |
17
- | `headerContent` | `React.ReactNode` | `undefined` | Custom content at the top. |
18
- | `footerContent` | `React.ReactNode` | `undefined` | Custom content at the bottom. |
19
- | `showRail` | `boolean` | `true` | Show the draggable sidebar rail. |
20
- | `collapsible` | `"offcanvas" \| "icon" \| "none"` | `"offcanvas"` | Collapse behavior, forwarded to `Sidebar`. |
21
- | `linkComponent` | `React.ComponentType<any>` | `undefined` | Custom link component for items. |
22
- | `linkProps` | `Record<string, any>` | `{}` | Extra props passed to the link component. |
11
+ | Prop | Type | Default | Description |
12
+ | ------------------- | --------------------------------- | ------------- | ------------------------------------------ |
13
+ | `navigation` | `NavGroup[]` | **required** | Main navigation groups. |
14
+ | `navigationFooter` | `NavGroup[]` | `undefined` | Footer navigation groups. |
15
+ | `navigationDisplay` | `"list" \| "tab"` | `"list"` | How the `navigation` groups are laid out. |
16
+ | `versions` | `string[]` | `undefined` | Optional versions list. |
17
+ | `defaultVersion` | `string` | `undefined` | Default selected version. |
18
+ | `headerContent` | `React.ReactNode` | `undefined` | Custom content at the top. |
19
+ | `footerContent` | `React.ReactNode` | `undefined` | Custom content at the bottom. |
20
+ | `showRail` | `boolean` | `true` | Show the draggable sidebar rail. |
21
+ | `collapsible` | `"offcanvas" \| "icon" \| "none"` | `"offcanvas"` | Collapse behavior, forwarded to `Sidebar`. |
22
+ | `linkComponent` | `React.ComponentType<any>` | `undefined` | Custom link component for items. |
23
+ | `linkProps` | `Record<string, any>` | `{}` | Extra props passed to the link component. |
23
24
 
24
- `NavGroup` → `{ title: string; url?: string; items: NavItem[] }`
25
+ `NavGroup` → `{ title: string; url?: string; items: NavItem[]; iconName?: IconName; icon?: NavIcon }`
25
26
 
26
- `NavItem` → `{ title: string; url: string; isActive?: boolean; iconName?: IconName; subItems?: NavSubItem[] }`
27
+ `NavItem` → `{ title: string; url: string; isActive?: boolean; iconName?: IconName; icon?: NavIcon; subItems?: NavSubItem[] }`
27
28
 
28
- `NavSubItem` → `{ title: string; url: string; isActive?: boolean; iconName?: IconName }`
29
+ `NavSubItem` → `{ title: string; url: string; isActive?: boolean; iconName?: IconName; icon?: NavIcon }`
30
+
31
+ `NavIcon` → `React.ComponentType<{ className?: string }>`
32
+
33
+ `NavGroup.iconName` / `NavGroup.icon` are read **only** in
34
+ `navigationDisplay="tab"`, where they are the tab's icon; they are ignored by the
35
+ list rendering and by the mobile menu.
36
+
37
+ ### Icons: `iconName` or `icon`
38
+
39
+ Every row takes its icon from one of two channels:
40
+
41
+ - `iconName` — the name of a **lucide** icon, rendered through the `Icon` atom.
42
+ This is the default and covers the whole lucide set.
43
+ - `icon` — **any component that accepts `className`**, for third-party icon
44
+ libraries (`react-icons`, `@heroicons/react`, `@tabler/icons-react`) or a
45
+ project's own SVG components. Pass the component itself, not an element:
46
+ `icon: FaHouse`, never `icon: <FaHouse />`.
47
+
48
+ `icon` **takes precedence** when both are set on the same row.
49
+
50
+ The size is imposed by the sidebar, not by the consumer: it passes a size
51
+ _class_ (`size-5` in a row, `size-4` in a collapsed dropdown or a tab), which is
52
+ why the contract is `className` and not a `size` prop. A CSS width/height
53
+ overrides the `width`/`height` attributes libraries like `react-icons` put on
54
+ their `<svg>`, so the same component lands at the right size in every context.
55
+ Colour is inherited (`currentColor`), so an icon that does not hardcode a `fill`
56
+ follows the active/hover states like a lucide one.
57
+
58
+ ```tsx
59
+ import { FaHouse, FaUsers } from "react-icons/fa6";
60
+
61
+ const navigation = [
62
+ {
63
+ title: "App",
64
+ items: [
65
+ { title: "Dashboard", url: "/", isActive: true, icon: FaHouse },
66
+ { title: "Utenti", url: "/users", icon: FaUsers },
67
+ // Mixing the two channels in one list is fine.
68
+ { title: "Impostazioni", url: "/settings", iconName: "Settings" },
69
+ ],
70
+ },
71
+ ];
72
+ ```
29
73
 
30
74
  `NavItem.url` is **ignored when the item has `subItems`**: that button toggles the
31
75
  submenu (or opens the icon-mode dropdown) instead of navigating. Only the
@@ -44,6 +88,13 @@ Inherits other props from `Sidebar` via `React.ComponentProps<typeof Sidebar>`.
44
88
  item: collapsing it by hand is remembered and never undone by a re-render.
45
89
  - **Duplicate titles are safe**: expansion state is keyed by group + url + title,
46
90
  so the same label in two groups keeps two independent states.
91
+ - **Group display**: `navigationDisplay` switches the `navigation` groups
92
+ between a stacked list (default) and a tab bar — see the dedicated section.
93
+ - **A lone `navigation` group has no label**: when `navigation` holds a single
94
+ group its `SidebarGroupLabel` is not rendered, in either display and on mobile
95
+ too — the name of the only group distinguishes it from nothing. This applies
96
+ to `navigation` only: `navigationFooter` always keeps its label (it is usually
97
+ a single group by nature, and hiding it would change every existing consumer).
47
98
  - **Links**: Use `linkComponent` for framework routers (e.g., Next.js `Link`).
48
99
  - **Rail**: `showRail` displays a thin draggable handle to collapse/expand.
49
100
  - **No interactive nesting**: the menu button _is_ the link (`asChild`), so there
@@ -51,6 +102,99 @@ Inherits other props from `Sidebar` via `React.ComponentProps<typeof Sidebar>`.
51
102
 
52
103
  ---
53
104
 
105
+ ## Navigation display (`navigationDisplay`)
106
+
107
+ `"list"` (the default, and the pre-existing behavior) stacks every `navigation`
108
+ group one under the other, each under its own `SidebarGroupLabel`.
109
+
110
+ `"tab"` turns the groups into a **tab bar at the top of the sidebar content**
111
+ and shows **one group at a time**:
112
+
113
+ ```tsx
114
+ <AppSidebar
115
+ navigationDisplay="tab"
116
+ navigation={[
117
+ { title: "App", iconName: "LayoutDashboard", items: appItems },
118
+ { title: "Admin", iconName: "ShieldCheck", items: adminItems },
119
+ ]}
120
+ navigationFooter={footerGroups}
121
+ />
122
+ ```
123
+
124
+ - **Only the selected tab shows its label.** Every other tab is a square
125
+ icon-only button; the selected one **animates its width open** until it fits
126
+ its title exactly (160ms, `cubic-bezier(0.23, 1, 0.32, 1)`), and closes again
127
+ when another tab is picked. Tabs are styled as sidebar rows (same height,
128
+ padding, radius and accent as `SidebarMenuButton`), sized by their content and
129
+ packed to the left: the width the selected tab does not need is left empty.
130
+ - The label is **never unmounted**, only clipped and faded: it is the tab's
131
+ accessible name, and a `role="tab"` without one is unusable with a screen
132
+ reader (`opacity` keeps it in the accessibility tree, where `display` or
133
+ `visibility` would not). The width animation is the grid trick —
134
+ `grid-template-columns: 0fr -> 1fr` on a wrapper whose child is
135
+ `overflow-hidden whitespace-nowrap` — because `width: auto` is not animatable
136
+ and this resolves to exactly the content's width with no JS measurement and no
137
+ `max-width` to keep in sync with the longest title.
138
+ - **The label fade is asymmetric on purpose**, and both halves share the width
139
+ animation's curve. Opening, it is delayed 70ms and runs 140ms, so the text
140
+ only appears once the width has already reached roughly 88%. Closing, it runs
141
+ 60ms with no delay: the curve is front-loaded enough that the tab is ~91% shut
142
+ after 43ms, so the label has to clear in well under half the nominal 160ms —
143
+ measured, it is at 2% opacity while the tab is still 9% open. Anything slower
144
+ leaves the text visibly sliding out under the closing edge.
145
+ - **No press feedback on the tabs.** They are styled as sidebar rows, and no row
146
+ in the sidebar has a press state — `SidebarMenuButton` has none, and the
147
+ mobile menu turns the shared `interaction.active` squash down explicitly as
148
+ "the button squash, wrong on a list row". A tab that squashed above nav rows
149
+ that do not would break the rail's homogeneity. The icon-mode selector button
150
+ refuses it for the same reason.
151
+ - **Hover**: only _unselected_ tabs take a hover tint, and only behind
152
+ `@media (hover: hover) and (pointer: fine)`. The selected tab is excluded
153
+ because its own background is a translucent accent and the hover rule (higher
154
+ specificity) halved that alpha — the hover _lightened_ the selected tab rather
155
+ than darkening it. The pointer query stops a tap from leaving the tint stuck
156
+ on a tablet, where this bar is still rendered (>= 768px).
157
+ - **Reduced motion**: under `prefers-reduced-motion: reduce` both the width and
158
+ the fade transitions are dropped (`motion-reduce:transition-none`) and the
159
+ label appears immediately.
160
+ - **An icon is effectively required in tab mode**: since an unselected tab is
161
+ icon-only, a group with neither `iconName` nor `icon` falls back to the **initial of its title**, the
162
+ same fallback top-level items use in icon mode. It is never a blank square.
163
+ - **Tooltips**: an unselected tab shows its group title in a tooltip on the
164
+ right. The selected one does not — its label is already visible.
165
+ - Built on the `Tabs` primitive, so `role="tab"` / `aria-selected`, the roving
166
+ tabindex and ←/→ keyboard navigation come for free. Only the selected group's
167
+ items are mounted.
168
+ - **A title longer than the sidebar** does not push the other tabs out: the
169
+ selected tab shrinks to the space left by them and the label is **truncated
170
+ with an ellipsis** (`Amministrazione e configurazione avanzata` renders as
171
+ `Amministrazione e …`). The delayed fade above is what keeps this from
172
+ flickering through the opening animation: the narrow intermediate states
173
+ (`A…`, `Am…`) play out while the text is still transparent. Keep group titles
174
+ short anyway — an ellipsis is a fallback, not a layout.
175
+ - The group label is **not** repeated inside the panel — the tab is the label.
176
+ - **`navigationFooter` is never tabbed.** It stays pinned at the bottom and
177
+ fully visible in both displays; only `navigation` is affected.
178
+ - **Selected tab**: uncontrolled. At mount the selected tab is the group holding
179
+ the active page (an item with `isActive`, or one whose `subItems` contain an
180
+ active one), not blindly the first group; it falls back to the first group when
181
+ nothing is active. Afterwards the user's choice stands, and it is re-synced
182
+ only when navigation moves the active item into a **different** group. There is
183
+ no `activeGroup` / `onActiveGroupChange` prop: `isActive` is already the single
184
+ source of truth for "where am I", and a second one could contradict it.
185
+ - **Single group**: with `navigation.length <= 1` the tab bar is dropped and the
186
+ group renders as a plain list — a bar with one permanently selected tab is not
187
+ a choice. Its `SidebarGroupLabel` is dropped too, see below. This is decided
188
+ before any width is considered, so it holds in **every** state: expanded,
189
+ `collapsible="icon"` expanded, and `collapsible="icon"` collapsed all render
190
+ zero tabs. There is no icon-only strip for a lone group.
191
+ - **Icon mode**: see below.
192
+ - **Mobile (`<768px`)**: `navigationDisplay` is **ignored**. The drill-down menu
193
+ already presents each group as its own card, so a tab bar would separate the
194
+ same groups twice.
195
+
196
+ ---
197
+
54
198
  ## Icon mode (`collapsible="icon"`)
55
199
 
56
200
  The default stays `"offcanvas"` (the sidebar slides out of view when collapsed).
@@ -70,11 +214,149 @@ Behavior when collapsed on desktop:
70
214
  - **Items with `subItems`** open a dropdown on **click**, anchored to the right of
71
215
  the icon, with one entry per sub-item (`url` preserved). Clicking outside
72
216
  closes it. The inline vertical submenu is used only in expanded mode.
73
- - **Items without `iconName`** fall back to the initial of their title, so they
217
+ - **Items without an icon** (neither `iconName` nor `icon`) fall back to the initial of their title, so they
74
218
  are never a blank square. The full title stays in the accessibility tree and in
75
219
  the tooltip.
76
- - **Mobile** is unaffected: the sidebar renders in a full-screen `Sheet`, so items
77
- keep labels and inline expansion.
220
+ - **Tab bar** (`navigationDisplay="tab"`): 3rem of rail has no room for a row of
221
+ tabs, so the bar becomes **a single group-selector button** above the active
222
+ group's items — the "one group at a time" model survives the collapse. See
223
+ the dedicated section below.
224
+
225
+ ### The group selector (`navigationDisplay="tab"` + `collapsible="icon"`, collapsed)
226
+
227
+ A **single 32x40 button**: the active group's icon with a chevron under it,
228
+ opening a menu of the other groups to the right. It replaces the vertical strip
229
+ of 32px squares an earlier version used — those squares were indistinguishable
230
+ from the nav rows right below them, so selector and navigation read as one list.
231
+
232
+ - **The icon does not move.** Its centre is at (24, 24) from the sidebar edge in
233
+ _both_ states — measured, expanded and collapsed. That is what makes the swap
234
+ read as "the label closed and a chevron appeared" rather than a jump, which is
235
+ why there is no transition animating between the two forms: they are different
236
+ DOM trees, and animating that would need FLIP or a shared-layout primitive for
237
+ continuity the geometry already provides. Only the chevron animates, fading in.
238
+ - **`<Tabs>` is not rendered at all** in this state. It is not hidden: a
239
+ `TabsContent` is a `role="tabpanel"` whose `aria-labelledby` points at its
240
+ trigger, so a hidden `TabsList` would leave a dangling IDREF and a tabpanel
241
+ with no tablist. Nothing is lost — the selection lives in component state and
242
+ `Tabs` was already controlled — and the group takes its accessible name from
243
+ `role="group"` + `aria-label` instead.
244
+ - **This is deliberately no longer the tab pattern**, it is WAI-ARIA APG "Menu
245
+ Button". A `tablist` with one visible tab would have a screen reader announce
246
+ "tab 1 of 1" with four groups present, and arrow-key iteration — the reason
247
+ Radix Tabs is used when expanded — would have nowhere to move. The ARIA
248
+ pattern changing on collapse is correct: the interaction changes too.
249
+ - **Menu items are `menuitemradio`**, not plain items: this is a single choice of
250
+ state, so `aria-checked` and the selected dot come from the primitive rather
251
+ than being restyled by hand.
252
+ - **Keyboard** comes entirely from the menu primitive: `Enter`/`Space`/`Down`
253
+ open, arrows and `Home`/`End` move, a-z is typeahead, `Esc` closes and returns
254
+ focus to the button. Focus stays on the button after a selection.
255
+ - **Hover feedback is on the chevron, not the surface**: the button rests on the
256
+ accent, which is the colour a hover would reach for. The chevron goes from 70%
257
+ to full foreground instead. Contrast measured at 4.79:1 (claymorphism) to
258
+ 6.27:1 (dark) against the accent — above the 3:1 that WCAG 1.4.11 asks of a
259
+ non-text indicator in all four themes.
260
+ - **The chevron does not rotate on open.** The menu opens to the _right_, so
261
+ flipping it would assert the popup is above.
262
+ - **Three signals separate it from the nav rows**, which are themselves
263
+ accent-tinted when active: the chevron, the 40px height against their 32, and
264
+ a separator underneath that exists only when collapsed.
265
+ - **Focus survives the collapse in both directions.** The selected tab and the
266
+ selector button register under the same key, so collapsing with the tab
267
+ focused moves focus to the button, and expanding moves it back.
268
+ - A group without an `iconName` falls back to the initial of its title, in the
269
+ button and in the menu alike. The **tooltip** on the right is what carries the
270
+ active group's full name, and unlike the expanded bar it is never suppressed —
271
+ this button has no visible label in any state.
272
+
273
+ - **Mobile** ignores `collapsible` entirely — below 768px the component renders
274
+ the Settings-style menu described in the next section.
275
+
276
+ ---
277
+
278
+ ## Mobile menu (`<768px`)
279
+
280
+ Below 768px (the `useIsMobile` breakpoint) `AppSidebar` **replaces** the desktop
281
+ tree with a mobile menu modelled on the iOS/Android Settings screen. This is the
282
+ default: there is no prop to opt in or out, and `collapsible` has no effect there
283
+ (`Sidebar` short-circuits to its full-screen `Sheet` before reading it, so even
284
+ `collapsible="none"` gets the sheet). The same `navigation` /
285
+ `navigationFooter` data drives both renderings.
286
+
287
+ Anatomy, level 0:
288
+
289
+ - The panel background is `bg-d-muted`; each `NavGroup` is a `bg-d-card` card
290
+ with a border, `radius.lg` and a divider between rows. The group `title`
291
+ renders as a small uppercase heading above its card, and is omitted when empty
292
+ — and also when `navigation` holds a **single** group, matching the desktop
293
+ rule above and what a drilled level already does. `navigationFooter` keeps its
294
+ heading regardless.
295
+ - Rows are at least **44px** tall (`designTokens.sizes.touch`), with the icon in
296
+ a fixed left column. The label wraps freely and is never clamped or
297
+ truncated — `min-h-11` lets the row grow instead, so a long label still reads
298
+ in full at 200% text zoom (WCAG 1.4.4).
299
+ - The icon column is reserved **only if at least one row in that list has an
300
+ icon** (`iconName` or `icon`); rows without one get an empty placeholder so a mixed list stays
301
+ aligned. There is no initial-letter fallback here (unlike desktop icon mode,
302
+ where the icon is the only affordance).
303
+ - A chevron on the right marks rows that have `subItems` — and only those.
304
+ - The active row uses `designTokens.navItem.selected`: a `bg-d-primary/10`
305
+ surface tint, a full-contrast `text-d-foreground` label, and a 2px left bar as
306
+ a second, non-colour channel. A parent row is tinted when **any** of its
307
+ `subItems` is active, so level 0 still shows where you are on a sub-page;
308
+ `aria-current="page"` stays on the real leaf only.
309
+ - `headerContent` stays at the top and does not scroll, and **is not focused on
310
+ open** — Radix would otherwise focus the first focusable node in it, which for
311
+ a search field means raising the keyboard over the menu. Focus goes to the
312
+ panel itself, and returns to whatever opened the sheet when it closes.
313
+ - `navigationFooter` is detached from the groups above it as the "system" block
314
+ of a native Settings list; `footerContent` is the last block, and the panel
315
+ reserves `env(safe-area-inset-bottom)` below it for the home indicator.
316
+ - Above 640px the column is capped and centred: a full-bleed card would leave
317
+ the chevron a screen-width away from its label.
318
+
319
+ **Drill-down instead of accordion.** Tapping a row with `subItems` pushes a
320
+ level 1 that shows the children in a single card, under a back bar. The back
321
+ control is labelled by its **destination** (`‹ Menu`, `aria-label="Back to
322
+ menu"`) so the visible text is part of the accessible name; the level's own title
323
+ sits below it as a focusable `<h2>`. Parents are never navigable (`url` is ignored, as
324
+ documented above), and a parent with a single child still drills — the chevron
325
+ always means the same thing. Opened from the sidebar trigger the level always
326
+ starts at 0, even if the current page is a sub-item.
327
+
328
+ The panel can also **open already drilled into a level**: whatever opens the
329
+ sheet may publish a `NavItem.title` as the sidebar context's `mobileNavTarget`
330
+ right before `setOpenMobile(true)`, and the panel resolves it once, at mount, to
331
+ the first item with that title and sub-items (`navigation` then
332
+ `navigationFooter`). This is what `AppBottomNav` uses to jump straight to a
333
+ level; an unresolved target opens level 0 with a warning, and the target is
334
+ cleared when the panel closes, so it never re-drills a later open.
335
+
336
+ **Behavior**
337
+
338
+ - **Tapping a navigable row closes the panel** (`setOpenMobile(false)`), so the
339
+ full-screen menu does not sit on top of the page that just loaded. A
340
+ cmd/ctrl/shift/alt/middle click leaves it open — the destination is opening
341
+ somewhere else.
342
+ - **Focus**: drilling down moves focus to the level `<h2>` (so a screen reader
343
+ announces the new level); going back restores focus to the parent row.
344
+ - **Motion**: level 1 slides in from the right, the return to level 0 from the
345
+ left, 200ms, suppressed under `prefers-reduced-motion`.
346
+ - **Markup** is `<nav>` → `<section>` + `<h3>` → `<ul>`/`<li>` with native links.
347
+ No `menu`/`menuitem` ARIA roles: this is navigation, not a command menu.
348
+ - **Scroll** is native on the card list, with `overscroll-contain` and a bottom
349
+ padding that clears the home indicator.
350
+
351
+ **Known limits**
352
+
353
+ - **The Android back button and edge swipe are not intercepted.** The library
354
+ does not touch `history`, which would fight the consumer's router. Going back
355
+ a level is on-screen only.
356
+ - **`openMobile` is internal state.** There is no prop to open or close the
357
+ mobile panel from outside; the only public affordances are `SidebarTrigger`
358
+ (or `useSidebar().setOpenMobile`) and the sheet's own close button.
359
+ - **No `SidebarRail` on mobile**, regardless of `showRail`.
78
360
 
79
361
  ---
80
362
 
@@ -164,5 +446,6 @@ export function WithHeaderFooter() {
164
446
  ## Notes
165
447
 
166
448
  - **Routing**: Prefer `linkComponent` to avoid full page reloads (e.g., `next/link`).
167
- - **Icons**: Use `laif-ds` `Icon` by passing `iconName` in navigation items.
449
+ - **Icons**: pass `iconName` for a lucide icon, or `icon` with a component from
450
+ any third-party icon library — see [Icons: `iconName` or `icon`](#icons-iconname-or-icon).
168
451
  - **Accessibility**: Sidebar primitives manage keyboard navigation and focus.
@@ -25,6 +25,10 @@ Low-level primitives to build responsive sidebars with desktop/offcanvas modes,
25
25
 
26
26
  - **Mobile**: full-screen `Sheet` (`side="full"`) that fades and scales in, with its
27
27
  close button visible — nothing of the page shows behind it; desktop uses a fixed panel.
28
+ Below 768px this branch **takes precedence over `collapsible="none"`**: an
29
+ always-visible sidebar would take the whole viewport, and the trigger would
30
+ toggle an `openMobile` no sheet is listening to, leaving the navigation
31
+ unreachable. So on mobile `collapsible` has no effect at all.
28
32
  - **Collapsible**: `icon` mode shows only icons and tooltips; `offcanvas` moves panel off-screen.
29
33
  - **Keyboard**: Toggle with ⌘/Ctrl + B.
30
34
  - **Tooltips**: Menu buttons show tooltips only when collapsed on desktop.
@@ -46,8 +46,9 @@ This document provides a complete mapping of all components available in the lai
46
46
  ### Layout & Structure Components
47
47
 
48
48
  - **Accordion** - Collapsible content panels for organizing information hierarchically
49
+ - **AppBottomNav** - Mobile bottom navigation bar (`md:hidden`, in the normal flow as the last child of `SidebarInset`): six equal cells — up to 5 primary destinations plus a Menu cell that opens `AppSidebar`'s mobile panel, optionally already drilled into a level, or up to 6 destinations with `showMenu={false}` when the app opens that panel from its own header
49
50
  - **AppCard** - Enhanced card component with variant styles, semantic states, size control, and loading skeleton support
50
- - **AppSidebar** - Application sidebar navigation component, with optional icon-only collapse (`collapsible="icon"`: tooltips on top-level items, dropdown for sub-items)
51
+ - **AppSidebar** - Application sidebar navigation component, with groups rendered as a stacked list or as a tab bar (`navigationDisplay="tab"`, one group at a time, only the selected tab labelled and animated open to fit it, `navigationFooter` always visible), optional icon-only collapse (`collapsible="icon"`: tooltips on top-level items, dropdown for sub-items) and, below 768px, a Settings-style mobile menu (grouped cards, 44px rows, drill-down on sub-items)
51
52
  - **AppStepper** - Step-by-step progress indicator for multi-step processes
52
53
  - **AspectRatio** - Container that maintains a specific aspect ratio for responsive layouts
53
54
  - **Breadcrumb** - Breadcrumb navigation component for hierarchical navigation
@@ -1,17 +1,17 @@
1
1
  {
2
2
  "schemaVersion": "1.1.0",
3
- "generatedAt": "2026-08-20T15:52:40.662Z",
3
+ "generatedAt": "2026-09-02T16:31:00.468Z",
4
4
  "package": {
5
5
  "name": "laif-ds",
6
- "version": "0.2.92"
6
+ "version": "1.0.0"
7
7
  },
8
8
  "stats": {
9
- "documentedComponentCount": 93,
10
- "catalogedComponentCount": 92,
9
+ "documentedComponentCount": 94,
10
+ "catalogedComponentCount": 93,
11
11
  "missingFromDocsCount": 1,
12
12
  "deprecatedComponentCount": 2,
13
- "averageAiReadinessScore": 55.54,
14
- "highAiReadinessCount": 37,
13
+ "averageAiReadinessScore": 56.17,
14
+ "highAiReadinessCount": 38,
15
15
  "lowAiReadinessCount": 41
16
16
  },
17
17
  "missingFromDocs": [
@@ -248,6 +248,92 @@
248
248
  "type AlertDialogTriggerProps = React_2.ComponentProps<typeof AlertDialogPrimitive.Trigger>;\r"
249
249
  ]
250
250
  },
251
+ {
252
+ "name": "AppBottomNav",
253
+ "markdownPath": "components/AppBottomNav.md",
254
+ "importPath": "laif-ds",
255
+ "deprecated": false,
256
+ "replacement": null,
257
+ "tags": {
258
+ "cataloged": true,
259
+ "aiReady": true
260
+ },
261
+ "docs": {
262
+ "sections": [
263
+ {
264
+ "title": "Overview",
265
+ "slug": "overview"
266
+ },
267
+ {
268
+ "title": "Props",
269
+ "slug": "props"
270
+ },
271
+ {
272
+ "title": "Behavior",
273
+ "slug": "behavior"
274
+ },
275
+ {
276
+ "title": "Examples",
277
+ "slug": "examples"
278
+ },
279
+ {
280
+ "title": "Notes",
281
+ "slug": "notes"
282
+ }
283
+ ],
284
+ "hasPropsTable": true,
285
+ "hasExamples": true,
286
+ "exampleTitles": [
287
+ "`BottomNavItem`",
288
+ "Layout",
289
+ "Icon size: a deliberate divergence from the native baselines",
290
+ "Active state",
291
+ "Opening the menu",
292
+ "Accessibility",
293
+ "Motion",
294
+ "Warnings (`console.warn`, deduplicated per message)",
295
+ "Edge cases",
296
+ "App shell",
297
+ "Localised copy",
298
+ "Without the Menu cell"
299
+ ],
300
+ "controlledPattern": "likely-controlled",
301
+ "requiredProps": [
302
+ "iconName",
303
+ "items",
304
+ "title",
305
+ "url"
306
+ ]
307
+ },
308
+ "metadata": {
309
+ "props": {
310
+ "totalProps": 15,
311
+ "requiredPropsCount": 4,
312
+ "typedPropsCount": 15,
313
+ "describedPropsCount": 14
314
+ },
315
+ "accessibility": {
316
+ "hasCoverage": true
317
+ },
318
+ "states": {
319
+ "coveredStates": [
320
+ "hover",
321
+ "focus",
322
+ "error",
323
+ "empty",
324
+ "selected"
325
+ ],
326
+ "coveredStateCount": 5
327
+ }
328
+ },
329
+ "aiReadiness": {
330
+ "score": 100,
331
+ "tier": "high"
332
+ },
333
+ "typeReferences": [
334
+ "interface AppBottomNavProps extends Omit<React_2.ComponentProps<\"div\">, \"children\"> {\r"
335
+ ]
336
+ },
251
337
  {
252
338
  "name": "AppCard",
253
339
  "markdownPath": "components/AppCard.md",
@@ -930,10 +1016,18 @@
930
1016
  "title": "Behavior",
931
1017
  "slug": "behavior"
932
1018
  },
1019
+ {
1020
+ "title": "Navigation display (`navigationDisplay`)",
1021
+ "slug": "navigation-display-navigationdisplay"
1022
+ },
933
1023
  {
934
1024
  "title": "Icon mode (`collapsible=\"icon\"`)",
935
1025
  "slug": "icon-mode-collapsible-icon"
936
1026
  },
1027
+ {
1028
+ "title": "Mobile menu (`<768px`)",
1029
+ "slug": "mobile-menu-768px"
1030
+ },
937
1031
  {
938
1032
  "title": "Examples",
939
1033
  "slug": "examples"
@@ -946,38 +1040,43 @@
946
1040
  "hasPropsTable": true,
947
1041
  "hasExamples": true,
948
1042
  "exampleTitles": [
1043
+ "Icons: `iconName` or `icon`",
1044
+ "The group selector (`navigationDisplay=\"tab\"` + `collapsible=\"icon\"`, collapsed)",
949
1045
  "Basic",
950
1046
  "With Header/Footer Content"
951
1047
  ],
952
- "controlledPattern": "not-specified",
1048
+ "controlledPattern": "controlled-uncontrolled-documented",
953
1049
  "requiredProps": [
954
1050
  "navigation"
955
1051
  ]
956
1052
  },
957
1053
  "metadata": {
958
1054
  "props": {
959
- "totalProps": 10,
1055
+ "totalProps": 11,
960
1056
  "requiredPropsCount": 1,
961
- "typedPropsCount": 10,
962
- "describedPropsCount": 10
1057
+ "typedPropsCount": 11,
1058
+ "describedPropsCount": 11
963
1059
  },
964
1060
  "accessibility": {
965
1061
  "hasCoverage": true
966
1062
  },
967
1063
  "states": {
968
1064
  "coveredStates": [
1065
+ "hover",
969
1066
  "focus",
1067
+ "empty",
970
1068
  "selected"
971
1069
  ],
972
- "coveredStateCount": 2
1070
+ "coveredStateCount": 4
973
1071
  }
974
1072
  },
975
1073
  "aiReadiness": {
976
- "score": 85,
1074
+ "score": 100,
977
1075
  "tier": "high"
978
1076
  },
979
1077
  "typeReferences": [
980
- "interface AppSidebarProps extends React_2.ComponentProps<typeof Sidebar> {\r"
1078
+ "interface AppSidebarProps extends React_2.ComponentProps<typeof Sidebar> {\r",
1079
+ "type AppSidebarNavigationDisplay = \"list\" | \"tab\";\r"
981
1080
  ]
982
1081
  },
983
1082
  {
@@ -6176,5 +6275,5 @@
6176
6275
  ]
6177
6276
  }
6178
6277
  ],
6179
- "checksum": "45a413c2a13515169ecf8186102b81f4397185b855629c059394297c6c9c3303"
6278
+ "checksum": "7347dd0e46eb44f6186c98afd32585008fadbbbcdcb7909e342274f93f42e45d"
6180
6279
  }