laif-ds 0.2.91 → 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.
@@ -16,6 +16,10 @@ const e = {
16
16
  default: "h-9 px-3 py-2 text-sm",
17
17
  sm: "h-8 px-2.5 py-1.5 text-xs",
18
18
  lg: "h-10 px-4 py-2 text-base",
19
+ // Minimum comfortable touch target (44px). `min-h-` rather than `h-`: a row
20
+ // that wraps onto a second line has to grow, not clip. The other sizes top
21
+ // out at h-10 = 40px, which is fine for a mouse and short for a thumb.
22
+ touch: "min-h-11 px-4 py-2.5 text-sm",
19
23
  icon: {
20
24
  default: "size-9"
21
25
  }
@@ -25,8 +29,24 @@ const e = {
25
29
  base: "text-sm font-medium whitespace-nowrap",
26
30
  placeholder: "placeholder:text-d-muted-foreground"
27
31
  },
32
+ // Motion
33
+ //
34
+ // The house curve: a strong, front-loaded ease-out that covers most of the
35
+ // distance immediately and settles, where Tailwind's built-in `ease-out`
36
+ // (cubic-bezier(0, 0, 0.2, 1)) decelerates too gently to read as deliberate
37
+ // over the short travels UI actually animates. `duration` is the paired
38
+ // value — long enough to be seen, short enough not to be waited on.
39
+ //
40
+ // `button.tsx` still spells both out inline; it predates this token and is
41
+ // deliberately left alone here. Adopting it there is a follow-up, so the
42
+ // curve has one source of truth instead of two.
43
+ motion: {
44
+ ease: "ease-[cubic-bezier(0.23,1,0.32,1)]",
45
+ duration: "duration-[160ms]"
46
+ },
28
47
  // Common interactive behaviors
29
48
  interaction: {
49
+ hover: "hover:bg-d-accent hover:text-d-accent-foreground",
30
50
  // Press feedback: the element is pushed down onto the surface and squashes
31
51
  // against it, so the shadow must shrink, never grow. Squash-and-stretch —
32
52
  // the 2% vertical compression is paid back as 2% horizontal spread
@@ -50,6 +70,70 @@ const e = {
50
70
  badge: {
51
71
  base: "inline-flex items-center justify-center border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 gap-1"
52
72
  },
73
+ // Navigation rows (sidebar / menu lists) specific.
74
+ //
75
+ // Not `dropdownItem.selected`: that one paints the accent colour *on a 10%
76
+ // dilution of itself* — measured 1.85:1 in light, 3.28 tangerine, 4.23
77
+ // claymorphism, 6.62 dark. Fine as a chip inside a dropdown, unreadable as a
78
+ // row label. Here the tint stays on the surface and the label keeps
79
+ // full-contrast `text-d-foreground` (12.19 / 10.35 / 9.35 / 10.22).
80
+ //
81
+ // The bar is `d-foreground`, not `d-primary`, for the same reason: in the
82
+ // light theme `--d-primary` is a pale cyan that measures 1.85:1 against the
83
+ // tinted row, so a primary bar would fail WCAG 1.4.11 (>= 3:1 for a non-text
84
+ // indicator) in the default theme. Shape and position carry the state; the
85
+ // tint adds the brand colour on top where the theme can afford it.
86
+ //
87
+ // `base` goes on every row so the selected row's 2px does not shift content.
88
+ navItem: {
89
+ base: "border-l-2 border-l-transparent",
90
+ selected: "bg-d-primary/10 border-l-d-foreground text-d-foreground font-semibold",
91
+ // A bottom bar's cells are columns, not rows, so the left rule has no edge
92
+ // to run along and the state has to be carried by the cell's own surface: a
93
+ // pill, no border on any side.
94
+ //
95
+ // The pill is the sidebar row's own state, ported: `bg-d-sidebar-accent` +
96
+ // `text-d-sidebar-accent-foreground` + a bold label, which is exactly what
97
+ // `sidebarMenuButtonVariants` paints under `data-[active=true]`. One
98
+ // vocabulary for "you are here" across the two navigations, which is worth
99
+ // more than either surface being optimal on its own.
100
+ //
101
+ // What comes with it is the sidebar's contrast, and it is very low.
102
+ // `--d-sidebar-accent` is `--colors--surface--tertiary` at 40% alpha, so on
103
+ // the bar (`d-card`, #fafafa light / #1f1f1f dark) it composites to #efefef
104
+ // and #2f2f2f — **1.10:1 in light and 1.23:1 in dark**, far under the 3:1 of
105
+ // WCAG 1.4.11 for a non-text state indicator. That is not a regression
106
+ // introduced here: the same fill on the sidebar's own surface (#f5f5f5)
107
+ // measures 1.08:1, so the desktop sidebar has always marked the current row
108
+ // by *weight*, with the tint as a hint. The label is never at risk either
109
+ // way — `--d-sidebar-accent-foreground` is `--colors--text--body-primary`,
110
+ // the colour the inactive cells already use, 11.9:1 on the composited fill.
111
+ //
112
+ // `font-bold` and not `semibold`: with a fill this faint the weight is the
113
+ // state, and it is the sidebar's own value. It costs label width on a cell
114
+ // that already truncates at 375px — if that has to be bought back,
115
+ // `font-semibold` is the place to spend it, not the fill.
116
+ //
117
+ // Measured alternatives, for whenever the 3:1 floor has to be met.
118
+ // `bg-d-primary` + `text-d-primary-foreground`: 1.98:1 light / 7.99:1 dark —
119
+ // the brand fill this replaced, under the floor in light but separating by
120
+ // hue, which 1.4.11 does not measure. `bg-d-foreground` + `text-d-background`:
121
+ // 13.01:1 / 12.49:1 — conforming, but it reads as a selected chip rather
122
+ // than the current tab and leaves the bar with no brand. Not a tint of
123
+ // primary: /10 is 1.07:1 light, /40 still 1.30:1, solid `bg-d-accent` 1.26:1.
124
+ //
125
+ // `rounded-md` (6px, = `radius.default`) and not `radius.sm`: `sm` is 4px in
126
+ // the v4 build and 2px in the v3 one, and this would be the single place
127
+ // where the two CSS bundles disagree. Not `rounded-d-md` either — that maps
128
+ // onto `--d-radius` and becomes 18px under claymorphism, while the dock it
129
+ // sits inside is a fixed `rounded-t-lg`.
130
+ //
131
+ // `basePill` reserves nothing (there is no border to reserve), so activating
132
+ // a cell cannot shift its content; it is there so every cell shares the
133
+ // radius and only the fill changes.
134
+ basePill: "rounded-md",
135
+ selectedPill: "bg-d-sidebar-accent text-d-sidebar-accent-foreground font-bold"
136
+ },
53
137
  // Dropdown item specific
54
138
  dropdownItem: {
55
139
  selected: "bg-d-primary/10 text-d-primary font-medium data-[selected=true]:text-d-primary data-[selected=true]:bg-d-primary/30 hover:text-d-primary hover:bg-d-primary/30",
@@ -0,0 +1,165 @@
1
+ "use client";
2
+ import { jsx as o, jsxs as y, Fragment as T } from "react/jsx-runtime";
3
+ import * as F from "react";
4
+ import { designTokens as u } from "../design-tokens.js";
5
+ import { Icon as K } from "./icon.js";
6
+ import { useSidebar as R } from "./sidebar.js";
7
+ import { cn as i } from "../../lib/utils.js";
8
+ const I = 6, w = /* @__PURE__ */ new Set(), c = (t) => {
9
+ w.has(t) || (w.add(t), console.warn(t));
10
+ }, $ = (t) => `${t.url} ${t.title}`, k = (t) => !!t.isActive || !!t.subItems?.some((n) => n.isActive), z = "md:hidden shrink-0 overflow-hidden bg-d-card border-d-border rounded-t-lg border-t pb-[env(safe-area-inset-bottom)]", D = "flex items-stretch p-1", m = i(
11
+ // `min-h-11` (44px) written out rather than `sizes.touch`: that token also
12
+ // carries `px-4`, and 16px of horizontal padding inside a ~61px cell would
13
+ // leave 29px for the label. `min-h-`, never `h-`: a cell must be able to grow.
14
+ // `py-1.5` and not `py-2.5`: the bar's breathing room is the `<nav>`'s 4px
15
+ // inset, and paying for it twice would put the cell at 54px and the bar at
16
+ // 63px — past the column budget for no legibility gained. It stayed at
17
+ // `py-1.5` when the icon dropped to 16px, deliberately: `py-2` would have held
18
+ // the bar at the 59px it had with a 20px glyph, and a lower bar was the point
19
+ // of the smaller glyph, not a side effect of it. 12px of vertical padding
20
+ // around 34px of content (16 + 2 + 16) puts the pill at 46px.
21
+ //
22
+ // Which leaves this value boxed in from both directions, and tighter than it
23
+ // was: upwards by the column budget, downwards by the 44px touch floor, now
24
+ // 2px away instead of 6. Trim `py-1.5`, the icon or the label's line-height
25
+ // any further and `min-h-11` takes the cell's height over from the padding —
26
+ // at which point the class list stops describing what renders, silently.
27
+ // Measure the rendered cell, not the padding.
28
+ // `cursor-pointer` because the two cell elements must not diverge: v4's
29
+ // preflight sets `cursor: default` on `<button>`, so without it the Menu cell
30
+ // would feel inert next to the `<a>` cells on a desktop-narrow window.
31
+ //
32
+ // `flex-1 min-w-0` is the equal-width guarantee, and both halves are load
33
+ // bearing. `flex-1` alone is `1 1 0%`, which distributes the row evenly only
34
+ // as long as no cell's content forces it wider: a flex item's automatic
35
+ // minimum size is its min-content width, so a cell whose contents cannot
36
+ // shrink below the fair share silently takes more than it and every other
37
+ // cell takes less. Here that floor is 24px — a `shrink-0` 16px icon plus
38
+ // `px-1` — and the narrowest cell the budget produces is 52px (six cells at
39
+ // 320px), so today the row is even by arithmetic. `min-w-0` makes it even by
40
+ // construction instead, at any width and whatever a cell holds, which is what
41
+ // the row's rhythm actually depends on. The label shrinks in exchange
42
+ // (`w-full truncate`, whose `overflow-hidden` already floors its own
43
+ // min-content size at 0) — the label is the right thing to spend.
44
+ "flex min-h-11 min-w-0 flex-1 cursor-pointer flex-col items-center justify-center gap-0.5 px-1 py-1.5 text-xs",
45
+ // The radius goes on every cell, not only the marked one: it costs nothing
46
+ // while the fill is transparent, and it keeps the hover and press surfaces of
47
+ // an inactive cell the same shape as the pill they can become.
48
+ u.navItem.basePill,
49
+ u.focusRing,
50
+ // `ring-inset` is still right, for a new reason: the cells now touch each
51
+ // other, not the dock's clipped corners, so a 3px outset ring would not be
52
+ // shaved — it would spill over the neighbouring cell and read as if two cells
53
+ // were focused.
54
+ "focus-visible:ring-inset"
55
+ ), p = i(
56
+ "text-d-foreground font-medium",
57
+ "[@media(hover:hover)_and_(pointer:fine)]:hover:bg-d-sidebar-accent [@media(hover:hover)_and_(pointer:fine)]:hover:text-d-sidebar-accent-foreground",
58
+ "active:bg-d-sidebar-accent active:text-d-sidebar-accent-foreground"
59
+ ), v = u.navItem.selectedPill, d = (t, n) => /* @__PURE__ */ y(T, { children: [
60
+ /* @__PURE__ */ o(K, { name: t, size: "xs", className: "shrink-0" }),
61
+ /* @__PURE__ */ o("span", { className: "w-full truncate text-center", children: n })
62
+ ] });
63
+ function J({
64
+ items: t,
65
+ showMenu: n = !0,
66
+ menuLabel: A = "Menu",
67
+ ariaLabel: B = "Main navigation",
68
+ menuIconName: M = "Menu",
69
+ linkComponent: b,
70
+ linkProps: S = {},
71
+ className: _,
72
+ ...j
73
+ }) {
74
+ const { openMobile: f, setOpenMobile: h, mobileNavTarget: L, setMobileNavTarget: g } = R(), s = n ? I - 1 : I, r = t.slice(0, s);
75
+ if (t.length > s) {
76
+ const e = t.slice(s).map((a) => `"${a.title}"`).join(", ");
77
+ c(
78
+ `AppBottomNav: ${t.length} items passed, ${s} slots available. Dropped: ${e}.`
79
+ );
80
+ }
81
+ const N = /* @__PURE__ */ new Set();
82
+ for (const e of r) {
83
+ const a = $(e);
84
+ N.has(a) && c(
85
+ `AppBottomNav: duplicate item "${a}" — one of them is ignored.`
86
+ ), N.add(a);
87
+ }
88
+ const l = r.findIndex(k);
89
+ r.filter(k).length > 1 && c(
90
+ `AppBottomNav: more than one active item — "${r[l].title}" wins.`
91
+ );
92
+ const x = n && l === -1, P = (e, a) => {
93
+ const C = {
94
+ className: i(
95
+ m,
96
+ a ? v : p
97
+ ),
98
+ "data-slot": "app-bottom-nav-item",
99
+ "data-active": a ? "true" : void 0,
100
+ // Not set unconditionally: `truncate` does not alter the text node, so
101
+ // the full title is already in the accessibility tree and a duplicating
102
+ // `aria-label` is dead weight.
103
+ "aria-label": e.ariaLabel,
104
+ "aria-current": a ? "page" : void 0
105
+ };
106
+ return b ? /* @__PURE__ */ o(b, { href: e.url, ...C, ...S, children: d(e.iconName, e.title) }) : /* @__PURE__ */ o("a", { href: e.url, ...C, children: d(e.iconName, e.title) });
107
+ }, O = (e, a) => (
108
+ // A button is not a page, so no `aria-current` even when the indicator is here.
109
+ // No `aria-controls` either — the drill panel's id is Radix-generated and
110
+ // unreachable from the bar, and a dangling reference is worse than none —
111
+ // and no `aria-haspopup`, which would promise arrow-key menu semantics the
112
+ // drill-down does not implement.
113
+ /* @__PURE__ */ o(
114
+ "button",
115
+ {
116
+ type: "button",
117
+ "data-slot": "app-bottom-nav-item",
118
+ "data-active": a ? "true" : void 0,
119
+ "aria-label": e.ariaLabel,
120
+ "aria-expanded": f && L === e.title,
121
+ className: i(
122
+ m,
123
+ a ? v : p
124
+ ),
125
+ onClick: () => {
126
+ g(e.title), h(!0);
127
+ },
128
+ children: d(e.iconName, e.title)
129
+ }
130
+ )
131
+ );
132
+ return r.length === 0 && !n ? (c(
133
+ "AppBottomNav: no items and showMenu={false} — nothing to render, the bar is omitted."
134
+ ), null) : /* @__PURE__ */ o(
135
+ "div",
136
+ {
137
+ "data-slot": "app-bottom-nav",
138
+ className: i(z, _),
139
+ ...j,
140
+ children: /* @__PURE__ */ y("nav", { "aria-label": B, className: D, children: [
141
+ r.map((e, a) => /* @__PURE__ */ o(F.Fragment, { children: e.subItems?.length ? O(e, a === l) : P(e, a === l) }, $(e))),
142
+ n && /* @__PURE__ */ o(
143
+ "button",
144
+ {
145
+ type: "button",
146
+ "data-slot": "app-bottom-nav-item",
147
+ "data-active": x ? "true" : void 0,
148
+ "aria-expanded": f,
149
+ className: i(
150
+ m,
151
+ x ? v : p
152
+ ),
153
+ onClick: () => {
154
+ g(null), h(!0);
155
+ },
156
+ children: d(M, A)
157
+ }
158
+ )
159
+ ] })
160
+ }
161
+ );
162
+ }
163
+ export {
164
+ J as AppBottomNav
165
+ };
@@ -0,0 +1,220 @@
1
+ "use client";
2
+ import { jsx as n, jsxs as f, Fragment as L } from "react/jsx-runtime";
3
+ import * as l from "react";
4
+ import { designTokens as c } from "../design-tokens.js";
5
+ import { Icon as R } from "./icon.js";
6
+ import { useSidebar as U } from "./sidebar.js";
7
+ import { cn as d } from "../../lib/utils.js";
8
+ const v = (t, i) => `${t} ${i.url} ${i.title}`, V = (t, i, a) => {
9
+ if (!t) return null;
10
+ for (const h of [...i, ...a ?? []]) {
11
+ const u = h.items.find(
12
+ (b) => b.title === t && b.subItems?.length
13
+ );
14
+ if (u) return { key: v(h.title, u), item: u };
15
+ }
16
+ return console.warn(
17
+ `AppSidebarMobile: no navigation item with sub-items titled "${t}" — opening at level 0.`
18
+ ), null;
19
+ }, W = (t) => t.defaultPrevented || t.metaKey || t.ctrlKey || t.shiftKey || t.altKey || t.button !== 0, M = d(
20
+ "flex w-full items-center gap-3 text-left",
21
+ c.sizes.touch,
22
+ c.navItem.base,
23
+ c.focusRing,
24
+ // The card clips its children (`overflow-hidden` is what keeps the rounded
25
+ // corners on the first and last row), so an outset ring would be cut off.
26
+ "focus-visible:ring-inset",
27
+ // Touch has no hover, and the hover style is sticky after a tap: the press
28
+ // state is the only immediate feedback a finger gets. Not
29
+ // `interaction.active` — that is the button squash, wrong on a list row.
30
+ "active:bg-d-accent"
31
+ ), $ = (t) => t ? c.navItem.selected : c.interaction.hover, X = "text-d-foreground px-4 pb-2 text-xs font-semibold tracking-wide uppercase", E = d(
32
+ "divide-d-border border-d-border bg-d-card divide-y overflow-hidden border",
33
+ c.radius.lg
34
+ ), Y = (t) => {
35
+ if (t.icon) {
36
+ const i = t.icon;
37
+ return /* @__PURE__ */ n(i, { className: "size-5 shrink-0" });
38
+ }
39
+ return t.iconName ? /* @__PURE__ */ n(R, { name: t.iconName, size: "sm", className: "shrink-0" }) : null;
40
+ }, T = (t) => !!t.iconName || !!t.icon, I = (t, { showIconColumn: i, hasChildren: a }) => /* @__PURE__ */ f(L, { children: [
41
+ i && (Y(t) ?? // Empty placeholder, not an initial: on a wide row the label is right
42
+ // there, so an initial would only add noise. Desktop icon mode needs
43
+ // the fallback because there the icon is the only affordance.
44
+ /* @__PURE__ */ n("span", { "aria-hidden": "true", className: "size-5 shrink-0" })),
45
+ /* @__PURE__ */ n("span", { className: "flex-1", children: t.title }),
46
+ a && /* @__PURE__ */ n(
47
+ R,
48
+ {
49
+ name: "ChevronRight",
50
+ size: "sm",
51
+ className: "text-d-muted-foreground shrink-0"
52
+ }
53
+ )
54
+ ] });
55
+ function se({
56
+ navigation: t,
57
+ navigationFooter: i,
58
+ headerContent: a,
59
+ footerContent: h,
60
+ linkComponent: u,
61
+ linkProps: b = {}
62
+ }) {
63
+ const { openMobile: C, setOpenMobile: j, mobileNavTarget: D } = U(), [r, x] = l.useState(
64
+ () => V(D, t, i)
65
+ ), [G, N] = l.useState(!1), g = l.useRef(!0), w = (!!r || G) && !(g.current && r);
66
+ l.useEffect(() => {
67
+ g.current = !1;
68
+ }, []), l.useEffect(() => {
69
+ C || (x(null), N(!1));
70
+ }, [C]);
71
+ const B = l.useRef(null), k = l.useRef(/* @__PURE__ */ new Map()), y = l.useRef(null), O = (e) => (s) => {
72
+ s ? k.current.set(e, s) : k.current.delete(e);
73
+ };
74
+ l.useLayoutEffect(() => {
75
+ if (r) {
76
+ if (g.current) return;
77
+ B.current?.focus();
78
+ return;
79
+ }
80
+ const e = y.current;
81
+ e && (y.current = null, k.current.get(e)?.focus());
82
+ }, [r]);
83
+ const P = (e) => {
84
+ N(!1), x(e);
85
+ }, q = () => {
86
+ y.current = r?.key ?? null, N(!0), x(null);
87
+ }, A = (e, s) => {
88
+ const m = d(M, $(e.isActive)), o = (p) => {
89
+ W(p) || j(!1);
90
+ };
91
+ return u ? /* @__PURE__ */ n(
92
+ u,
93
+ {
94
+ href: e.url,
95
+ className: m,
96
+ "aria-current": e.isActive ? "page" : void 0,
97
+ onClick: o,
98
+ ...b,
99
+ children: I(e, s)
100
+ }
101
+ ) : /* @__PURE__ */ n(
102
+ "a",
103
+ {
104
+ href: e.url,
105
+ className: m,
106
+ "aria-current": e.isActive ? "page" : void 0,
107
+ onClick: o,
108
+ children: I(e, s)
109
+ }
110
+ );
111
+ }, F = (e, s, m) => {
112
+ const o = v(e.title, s), p = !!s.subItems?.length, K = { showIconColumn: m, hasChildren: p }, J = s.isActive || !!s.subItems?.some((Q) => Q.isActive);
113
+ return p ? /* @__PURE__ */ n(
114
+ "button",
115
+ {
116
+ ref: O(o),
117
+ type: "button",
118
+ className: d(M, $(J)),
119
+ onClick: () => P({ key: o, item: s }),
120
+ children: I(s, K)
121
+ }
122
+ ) : A(s, K);
123
+ }, S = (e, s = !0) => {
124
+ const m = e.items.some(T);
125
+ return /* @__PURE__ */ f("section", { children: [
126
+ s && e.title && /* @__PURE__ */ n("h3", { className: X, children: e.title }),
127
+ /* @__PURE__ */ n("ul", { className: E, children: e.items.map((o) => /* @__PURE__ */ n("li", { children: F(e, o, m) }, v(e.title, o))) })
128
+ ] }, e.title);
129
+ }, z = r?.item.subItems ?? [], H = z.some(T);
130
+ return /* @__PURE__ */ n(
131
+ "div",
132
+ {
133
+ "data-slot": "app-sidebar-mobile",
134
+ className: d(
135
+ "bg-d-muted text-d-foreground flex min-h-0 flex-1 flex-col",
136
+ // The home indicator overlaps the bottom of the viewport, and
137
+ // `footerContent` is the last thing in the panel — so the reserve lives
138
+ // here, not on the scroll area above it.
139
+ "pb-[max(1rem,env(safe-area-inset-bottom))]"
140
+ ),
141
+ children: /* @__PURE__ */ f("div", { className: "flex min-h-0 w-full flex-1 flex-col sm:mx-auto sm:max-w-md", children: [
142
+ r ? /* @__PURE__ */ f(
143
+ "div",
144
+ {
145
+ "data-sidebar": "header",
146
+ className: "border-d-border shrink-0 border-b pb-2",
147
+ children: [
148
+ /* @__PURE__ */ n("div", { className: "p-2", children: /* @__PURE__ */ f(
149
+ "button",
150
+ {
151
+ type: "button",
152
+ "aria-label": "Back to menu",
153
+ onClick: q,
154
+ className: d(
155
+ "flex min-h-11 items-center gap-1 pr-3 pl-2",
156
+ c.radius.default,
157
+ c.focusRing,
158
+ c.interaction.hover,
159
+ "active:bg-d-accent"
160
+ ),
161
+ children: [
162
+ /* @__PURE__ */ n(R, { name: "ChevronLeft", size: "sm", className: "shrink-0" }),
163
+ /* @__PURE__ */ n("span", { className: "text-sm font-medium", children: "Menu" })
164
+ ]
165
+ }
166
+ ) }),
167
+ /* @__PURE__ */ n(
168
+ "h2",
169
+ {
170
+ ref: B,
171
+ tabIndex: -1,
172
+ className: "px-4 text-base font-semibold outline-none",
173
+ children: r.item.title
174
+ }
175
+ )
176
+ ]
177
+ }
178
+ ) : a && // `px-4` so the consumer's header lands on the same grid as the cards.
179
+ /* @__PURE__ */ n("div", { "data-sidebar": "header", className: "shrink-0 px-4 py-2", children: a }),
180
+ /* @__PURE__ */ n(
181
+ "nav",
182
+ {
183
+ className: d(
184
+ "min-h-0 flex-1 space-y-6 overflow-y-auto overscroll-contain px-4 pb-4",
185
+ // The sheet's close button floats over the top-right corner. A
186
+ // header (or the back bar) sits under it and gets `pr-12`; with
187
+ // neither, the list itself has to clear its 52px.
188
+ r || a ? "pt-3" : "pt-14",
189
+ w && "animate-in fade-in-0 duration-200 motion-reduce:animate-none",
190
+ w && (r ? "slide-in-from-right-4" : "slide-in-from-left-4")
191
+ ),
192
+ children: r ? (
193
+ // One card, no group heading: the level's title is already in the
194
+ // back bar.
195
+ /* @__PURE__ */ n("ul", { className: E, children: z.map((e) => /* @__PURE__ */ n("li", { children: A(e, {
196
+ showIconColumn: H,
197
+ hasChildren: !1
198
+ }) }, v(r.key, e))) })
199
+ ) : /* @__PURE__ */ f(L, { children: [
200
+ t.map(
201
+ (e) => S(e, t.length > 1)
202
+ ),
203
+ i?.length ? (
204
+ // Detached by a wider gap: the "system" block at the bottom of a
205
+ // native Settings list. `pt-6` on top of the `space-y-6` — 8px
206
+ // read as an accident, not as a separation.
207
+ /* @__PURE__ */ n("div", { className: "space-y-6 pt-6", children: i.map((e) => S(e)) })
208
+ ) : null
209
+ ] })
210
+ },
211
+ r ? `level-1 ${r.key}` : "level-0"
212
+ ),
213
+ h
214
+ ] })
215
+ }
216
+ );
217
+ }
218
+ export {
219
+ se as AppSidebarMobile
220
+ };