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.
- package/dist/CHANGELOG.md +20 -0
- package/dist/_virtual/index3.js +5 -5
- package/dist/_virtual/index4.js +5 -5
- package/dist/_virtual/index5.js +4 -4
- package/dist/agent-docs/adoption-report.json +37 -38
- package/dist/agent-docs/components/AppBottomNav.md +426 -0
- package/dist/agent-docs/components/AppSidebar.md +302 -19
- package/dist/agent-docs/components/Sidebar.md +4 -0
- package/dist/agent-docs/components-list.md +2 -1
- package/dist/agent-docs/manifest.json +113 -14
- package/dist/components/design-tokens.js +84 -0
- package/dist/components/ui/app-bottom-nav.js +165 -0
- package/dist/components/ui/app-sidebar-mobile.js +220 -0
- package/dist/components/ui/app-sidebar.js +366 -109
- package/dist/components/ui/sheet.js +26 -24
- package/dist/components/ui/sidebar.js +190 -166
- package/dist/index.d.ts +92 -1
- package/dist/index.js +183 -181
- package/dist/node_modules/eventemitter3/index.js +1 -1
- package/dist/node_modules/hast-util-to-jsx-runtime/lib/index.js +1 -1
- package/dist/node_modules/recharts/es6/util/Events.js +1 -1
- package/dist/node_modules/unified/lib/index.js +1 -1
- package/dist/styles.v3.css +1 -1
- package/package.json +3 -2
|
@@ -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
|
|
12
|
-
|
|
|
13
|
-
| `navigation`
|
|
14
|
-
| `navigationFooter`
|
|
15
|
-
| `
|
|
16
|
-
| `
|
|
17
|
-
| `
|
|
18
|
-
| `
|
|
19
|
-
| `
|
|
20
|
-
| `
|
|
21
|
-
| `
|
|
22
|
-
| `
|
|
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
|
|
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
|
-
- **
|
|
77
|
-
|
|
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**:
|
|
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-
|
|
3
|
+
"generatedAt": "2026-09-02T16:31:00.468Z",
|
|
4
4
|
"package": {
|
|
5
5
|
"name": "laif-ds",
|
|
6
|
-
"version": "0.
|
|
6
|
+
"version": "1.0.0"
|
|
7
7
|
},
|
|
8
8
|
"stats": {
|
|
9
|
-
"documentedComponentCount":
|
|
10
|
-
"catalogedComponentCount":
|
|
9
|
+
"documentedComponentCount": 94,
|
|
10
|
+
"catalogedComponentCount": 93,
|
|
11
11
|
"missingFromDocsCount": 1,
|
|
12
12
|
"deprecatedComponentCount": 2,
|
|
13
|
-
"averageAiReadinessScore":
|
|
14
|
-
"highAiReadinessCount":
|
|
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": "
|
|
1048
|
+
"controlledPattern": "controlled-uncontrolled-documented",
|
|
953
1049
|
"requiredProps": [
|
|
954
1050
|
"navigation"
|
|
955
1051
|
]
|
|
956
1052
|
},
|
|
957
1053
|
"metadata": {
|
|
958
1054
|
"props": {
|
|
959
|
-
"totalProps":
|
|
1055
|
+
"totalProps": 11,
|
|
960
1056
|
"requiredPropsCount": 1,
|
|
961
|
-
"typedPropsCount":
|
|
962
|
-
"describedPropsCount":
|
|
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":
|
|
1070
|
+
"coveredStateCount": 4
|
|
973
1071
|
}
|
|
974
1072
|
},
|
|
975
1073
|
"aiReadiness": {
|
|
976
|
-
"score":
|
|
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": "
|
|
6278
|
+
"checksum": "7347dd0e46eb44f6186c98afd32585008fadbbbcdcb7909e342274f93f42e45d"
|
|
6180
6279
|
}
|