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.
- package/dist/CHANGELOG.md +26 -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/css-for-template.css +2 -2
- 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
|
@@ -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
|
+
};
|