@meetreeve/ui 0.2.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/README.md +134 -0
- package/dist/index.d.mts +328 -0
- package/dist/index.d.ts +328 -0
- package/dist/index.js +955 -0
- package/dist/index.mjs +910 -0
- package/dist/tokens/index.d.mts +31 -0
- package/dist/tokens/index.d.ts +31 -0
- package/dist/tokens/index.js +47 -0
- package/dist/tokens/index.mjs +21 -0
- package/dist/tokens.css +25 -0
- package/package.json +75 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,910 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// src/brand-glyph.tsx
|
|
4
|
+
import Image from "next/image";
|
|
5
|
+
import { useState } from "react";
|
|
6
|
+
|
|
7
|
+
// src/lib/cn.ts
|
|
8
|
+
function cn(...inputs) {
|
|
9
|
+
const out = [];
|
|
10
|
+
for (const input of inputs) {
|
|
11
|
+
if (!input) continue;
|
|
12
|
+
if (typeof input === "string" || typeof input === "number") {
|
|
13
|
+
out.push(String(input));
|
|
14
|
+
} else if (Array.isArray(input)) {
|
|
15
|
+
const nested = cn(...input);
|
|
16
|
+
if (nested) out.push(nested);
|
|
17
|
+
} else if (typeof input === "object") {
|
|
18
|
+
for (const [key, value] of Object.entries(input)) {
|
|
19
|
+
if (value) out.push(key);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return out.join(" ");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// src/lib/initials.ts
|
|
27
|
+
function initials(name) {
|
|
28
|
+
if (!name) return "\xB7\xB7";
|
|
29
|
+
const parts = name.trim().split(/\s+/).slice(0, 2);
|
|
30
|
+
return parts.map((p) => p[0]?.toUpperCase() ?? "").join("") || "\xB7\xB7";
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// src/lib/monogram.ts
|
|
34
|
+
var MONOGRAM_COLORS = [
|
|
35
|
+
"#7c3aed",
|
|
36
|
+
// violet
|
|
37
|
+
"#2563eb",
|
|
38
|
+
// blue
|
|
39
|
+
"#0e7490",
|
|
40
|
+
// cyan
|
|
41
|
+
"#047857",
|
|
42
|
+
// emerald
|
|
43
|
+
"#c2410c",
|
|
44
|
+
// orange
|
|
45
|
+
"#dc2626",
|
|
46
|
+
// red
|
|
47
|
+
"#db2777",
|
|
48
|
+
// pink
|
|
49
|
+
"#4f46e5"
|
|
50
|
+
// indigo
|
|
51
|
+
];
|
|
52
|
+
function monogramColor(seed) {
|
|
53
|
+
let h = 0;
|
|
54
|
+
for (let i = 0; i < seed.length; i++) h = h * 31 + seed.charCodeAt(i) | 0;
|
|
55
|
+
return MONOGRAM_COLORS[Math.abs(h) % MONOGRAM_COLORS.length];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// src/brand-glyph.tsx
|
|
59
|
+
import { jsx } from "react/jsx-runtime";
|
|
60
|
+
function faviconUrl(domain) {
|
|
61
|
+
if (!domain) return null;
|
|
62
|
+
const host = domain.trim().toLowerCase();
|
|
63
|
+
if (!host.includes(".") || /\s/.test(host)) return null;
|
|
64
|
+
return `https://www.google.com/s2/favicons?domain=${encodeURIComponent(host)}&sz=128`;
|
|
65
|
+
}
|
|
66
|
+
function BrandGlyph({ brand, size = "sm", subtle = false, className }) {
|
|
67
|
+
const box = size === "lg" ? "h-6 w-6" : "h-5 w-5";
|
|
68
|
+
const img = size === "lg" ? "h-4 w-4" : "h-3.5 w-3.5";
|
|
69
|
+
const dim = size === "lg" ? 16 : 14;
|
|
70
|
+
const candidates = [];
|
|
71
|
+
if (brand.favicon) candidates.push(brand.favicon);
|
|
72
|
+
if (brand.logo) candidates.push(brand.logo);
|
|
73
|
+
const [failed, setFailed] = useState(() => /* @__PURE__ */ new Set());
|
|
74
|
+
const src = candidates.find((u) => !failed.has(u)) ?? null;
|
|
75
|
+
if (src) {
|
|
76
|
+
return /* @__PURE__ */ jsx(
|
|
77
|
+
"span",
|
|
78
|
+
{
|
|
79
|
+
className: cn(
|
|
80
|
+
"flex shrink-0 items-center justify-center overflow-hidden rounded",
|
|
81
|
+
// Tint needs a dark-mode counterpart or it vanishes on dark backgrounds.
|
|
82
|
+
subtle ? "bg-black/5 dark:bg-white/10" : "bg-black/10 dark:bg-white/15",
|
|
83
|
+
box,
|
|
84
|
+
className
|
|
85
|
+
),
|
|
86
|
+
children: /* @__PURE__ */ jsx(
|
|
87
|
+
Image,
|
|
88
|
+
{
|
|
89
|
+
src,
|
|
90
|
+
alt: "",
|
|
91
|
+
width: dim,
|
|
92
|
+
height: dim,
|
|
93
|
+
"data-testid": "brand-image",
|
|
94
|
+
unoptimized: true,
|
|
95
|
+
onError: () => setFailed((prev) => new Set(prev).add(src)),
|
|
96
|
+
className: cn(img, "rounded-sm object-contain")
|
|
97
|
+
}
|
|
98
|
+
)
|
|
99
|
+
}
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
return /* @__PURE__ */ jsx(
|
|
103
|
+
"span",
|
|
104
|
+
{
|
|
105
|
+
"aria-hidden": true,
|
|
106
|
+
"data-testid": "brand-monogram",
|
|
107
|
+
className: cn(
|
|
108
|
+
"flex shrink-0 select-none items-center justify-center rounded font-semibold uppercase text-white",
|
|
109
|
+
size === "lg" ? "text-[11px]" : "text-[9px]",
|
|
110
|
+
box,
|
|
111
|
+
className
|
|
112
|
+
),
|
|
113
|
+
style: { backgroundColor: brand.color || monogramColor(brand.name || "?") },
|
|
114
|
+
children: initials(brand.name)
|
|
115
|
+
}
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// src/responsive-header.tsx
|
|
120
|
+
import { useEffect, useId, useRef, useState as useState2 } from "react";
|
|
121
|
+
import { Menu, X } from "lucide-react";
|
|
122
|
+
|
|
123
|
+
// src/lib/use-media-query.ts
|
|
124
|
+
import { useCallback, useSyncExternalStore } from "react";
|
|
125
|
+
function useMediaQuery(query) {
|
|
126
|
+
const subscribe = useCallback(
|
|
127
|
+
(onChange) => {
|
|
128
|
+
if (typeof window === "undefined" || typeof window.matchMedia !== "function") {
|
|
129
|
+
return () => {
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
const mql = window.matchMedia(query);
|
|
133
|
+
mql.addEventListener("change", onChange);
|
|
134
|
+
return () => mql.removeEventListener("change", onChange);
|
|
135
|
+
},
|
|
136
|
+
[query]
|
|
137
|
+
);
|
|
138
|
+
const getSnapshot = () => typeof window !== "undefined" && typeof window.matchMedia === "function" ? window.matchMedia(query).matches : false;
|
|
139
|
+
return useSyncExternalStore(subscribe, getSnapshot, () => false);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// src/tokens.ts
|
|
143
|
+
var MIN_SIZES = {
|
|
144
|
+
/** WCAG 2.5.5 minimum interactive target, px. Applies to BOTH width and height. */
|
|
145
|
+
touchTarget: 44,
|
|
146
|
+
/** Floor width for the always-inline search field on mobile, px. */
|
|
147
|
+
searchInput: 200,
|
|
148
|
+
/** Generic minimum width/height for a secondary control (button, select), px. */
|
|
149
|
+
control: 44,
|
|
150
|
+
/** Max width of the brand wordmark before it truncates, px. */
|
|
151
|
+
wordmarkMaxWidth: 180
|
|
152
|
+
};
|
|
153
|
+
var BREAKPOINTS = {
|
|
154
|
+
/** Tailwind `sm`. */
|
|
155
|
+
sm: 640,
|
|
156
|
+
/** Tailwind `md` — the header switches to its full desktop layout at/above this. */
|
|
157
|
+
md: 768
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
// src/responsive-header.tsx
|
|
161
|
+
import { Fragment, jsx as jsx2, jsxs } from "react/jsx-runtime";
|
|
162
|
+
function MenuIcon({ open }) {
|
|
163
|
+
return open ? /* @__PURE__ */ jsx2(X, { className: "h-5 w-5", "aria-hidden": "true" }) : /* @__PURE__ */ jsx2(Menu, { className: "h-5 w-5", "aria-hidden": "true" });
|
|
164
|
+
}
|
|
165
|
+
function SecondaryControls({
|
|
166
|
+
nav,
|
|
167
|
+
auth,
|
|
168
|
+
themeToggle
|
|
169
|
+
}) {
|
|
170
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
171
|
+
nav?.map(
|
|
172
|
+
(item) => (
|
|
173
|
+
// A link navigates; an action-only item (no href) is a button — never an
|
|
174
|
+
// <a href="#"> that scroll-jumps and pushes a history entry.
|
|
175
|
+
item.href ? /* @__PURE__ */ jsx2(
|
|
176
|
+
"a",
|
|
177
|
+
{
|
|
178
|
+
href: item.href,
|
|
179
|
+
onClick: item.onSelect,
|
|
180
|
+
className: "text-sm font-medium text-foreground/80 hover:text-foreground",
|
|
181
|
+
children: item.label
|
|
182
|
+
},
|
|
183
|
+
item.label
|
|
184
|
+
) : /* @__PURE__ */ jsx2(
|
|
185
|
+
"button",
|
|
186
|
+
{
|
|
187
|
+
type: "button",
|
|
188
|
+
onClick: item.onSelect,
|
|
189
|
+
className: "text-left text-sm font-medium text-foreground/80 hover:text-foreground",
|
|
190
|
+
children: item.label
|
|
191
|
+
},
|
|
192
|
+
item.label
|
|
193
|
+
)
|
|
194
|
+
)
|
|
195
|
+
),
|
|
196
|
+
themeToggle,
|
|
197
|
+
auth
|
|
198
|
+
] });
|
|
199
|
+
}
|
|
200
|
+
function ResponsiveHeader({
|
|
201
|
+
brand,
|
|
202
|
+
search,
|
|
203
|
+
nav,
|
|
204
|
+
auth,
|
|
205
|
+
themeToggle,
|
|
206
|
+
menuLabel = "Menu",
|
|
207
|
+
className
|
|
208
|
+
}) {
|
|
209
|
+
const isDesktop = useMediaQuery(`(min-width: ${BREAKPOINTS.md}px)`);
|
|
210
|
+
const [open, setOpen] = useState2(false);
|
|
211
|
+
const panelId = useId();
|
|
212
|
+
const buttonRef = useRef(null);
|
|
213
|
+
const panelRef = useRef(null);
|
|
214
|
+
useEffect(() => {
|
|
215
|
+
if (isDesktop) setOpen(false);
|
|
216
|
+
}, [isDesktop]);
|
|
217
|
+
useEffect(() => {
|
|
218
|
+
if (!open) return;
|
|
219
|
+
const onKey = (e) => {
|
|
220
|
+
if (e.key === "Escape") {
|
|
221
|
+
setOpen(false);
|
|
222
|
+
buttonRef.current?.focus();
|
|
223
|
+
}
|
|
224
|
+
};
|
|
225
|
+
const onPointerDown = (e) => {
|
|
226
|
+
const target = e.target;
|
|
227
|
+
if (!panelRef.current?.contains(target) && !buttonRef.current?.contains(target)) {
|
|
228
|
+
setOpen(false);
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
document.addEventListener("keydown", onKey);
|
|
232
|
+
document.addEventListener("pointerdown", onPointerDown);
|
|
233
|
+
return () => {
|
|
234
|
+
document.removeEventListener("keydown", onKey);
|
|
235
|
+
document.removeEventListener("pointerdown", onPointerDown);
|
|
236
|
+
};
|
|
237
|
+
}, [open]);
|
|
238
|
+
const hasSecondary = Boolean(nav && nav.length > 0 || auth || themeToggle);
|
|
239
|
+
return /* @__PURE__ */ jsxs(
|
|
240
|
+
"header",
|
|
241
|
+
{
|
|
242
|
+
"data-testid": "responsive-header",
|
|
243
|
+
className: cn(
|
|
244
|
+
"flex w-full items-center gap-3 border-b border-border bg-background px-4 py-2",
|
|
245
|
+
className
|
|
246
|
+
),
|
|
247
|
+
children: [
|
|
248
|
+
/* @__PURE__ */ jsxs("div", { "data-testid": "header-brand", className: "flex shrink-0 items-center gap-2", children: [
|
|
249
|
+
/* @__PURE__ */ jsx2(BrandGlyph, { brand, size: "lg" }),
|
|
250
|
+
isDesktop && /* @__PURE__ */ jsx2(
|
|
251
|
+
"span",
|
|
252
|
+
{
|
|
253
|
+
"data-testid": "header-wordmark",
|
|
254
|
+
className: "truncate text-sm font-semibold text-foreground",
|
|
255
|
+
style: { maxWidth: MIN_SIZES.wordmarkMaxWidth },
|
|
256
|
+
children: brand.name
|
|
257
|
+
}
|
|
258
|
+
)
|
|
259
|
+
] }),
|
|
260
|
+
/* @__PURE__ */ jsx2(
|
|
261
|
+
"div",
|
|
262
|
+
{
|
|
263
|
+
"data-testid": "header-search",
|
|
264
|
+
className: "flex flex-1 items-center",
|
|
265
|
+
style: { minWidth: MIN_SIZES.searchInput },
|
|
266
|
+
children: search
|
|
267
|
+
}
|
|
268
|
+
),
|
|
269
|
+
hasSecondary && (isDesktop ? /* @__PURE__ */ jsx2("nav", { "data-testid": "header-actions", className: "flex shrink-0 items-center gap-3", children: /* @__PURE__ */ jsx2(SecondaryControls, { nav, auth, themeToggle }) }) : /* @__PURE__ */ jsxs("div", { className: "relative shrink-0", children: [
|
|
270
|
+
/* @__PURE__ */ jsx2(
|
|
271
|
+
"button",
|
|
272
|
+
{
|
|
273
|
+
ref: buttonRef,
|
|
274
|
+
type: "button",
|
|
275
|
+
"data-testid": "header-menu-button",
|
|
276
|
+
"aria-label": menuLabel,
|
|
277
|
+
"aria-expanded": open,
|
|
278
|
+
"aria-controls": panelId,
|
|
279
|
+
onClick: () => setOpen((v) => !v),
|
|
280
|
+
className: "flex items-center justify-center rounded-md border border-border text-foreground",
|
|
281
|
+
style: { minWidth: MIN_SIZES.touchTarget, minHeight: MIN_SIZES.touchTarget },
|
|
282
|
+
children: /* @__PURE__ */ jsx2(MenuIcon, { open })
|
|
283
|
+
}
|
|
284
|
+
),
|
|
285
|
+
open && /* @__PURE__ */ jsx2(
|
|
286
|
+
"div",
|
|
287
|
+
{
|
|
288
|
+
ref: panelRef,
|
|
289
|
+
id: panelId,
|
|
290
|
+
"data-testid": "header-menu-panel",
|
|
291
|
+
className: "absolute right-0 top-full z-50 mt-2 flex min-w-[12rem] flex-col gap-3 rounded-md border border-border bg-background p-3 shadow-lg",
|
|
292
|
+
children: /* @__PURE__ */ jsx2(SecondaryControls, { nav, auth, themeToggle })
|
|
293
|
+
}
|
|
294
|
+
)
|
|
295
|
+
] }))
|
|
296
|
+
]
|
|
297
|
+
}
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// src/app-sidebar.tsx
|
|
302
|
+
import { useEffect as useEffect4, useId as useId3, useMemo, useRef as useRef4, useState as useState5 } from "react";
|
|
303
|
+
import { Menu as Menu2, X as X2 } from "lucide-react";
|
|
304
|
+
|
|
305
|
+
// src/lib/resolve-icon.ts
|
|
306
|
+
import * as LucideIcons from "lucide-react";
|
|
307
|
+
var FALLBACK_ICON_NAME = "Layers";
|
|
308
|
+
var REACT_FORWARD_REF = /* @__PURE__ */ Symbol.for("react.forward_ref");
|
|
309
|
+
var NON_ICON_EXPORTS = /* @__PURE__ */ new Set(["Icon"]);
|
|
310
|
+
function isIconComponent(icons, name) {
|
|
311
|
+
if (NON_ICON_EXPORTS.has(name)) return false;
|
|
312
|
+
if (!Object.prototype.hasOwnProperty.call(icons, name)) return false;
|
|
313
|
+
const value = icons[name];
|
|
314
|
+
return typeof value === "object" && value !== null && value.$$typeof === REACT_FORWARD_REF;
|
|
315
|
+
}
|
|
316
|
+
function resolveIcon(name) {
|
|
317
|
+
const icons = LucideIcons;
|
|
318
|
+
const candidate = isIconComponent(icons, name) ? icons[name] : void 0;
|
|
319
|
+
return candidate ?? icons[FALLBACK_ICON_NAME];
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// src/lib/use-dismissable-disclosure.ts
|
|
323
|
+
import { useEffect as useEffect2 } from "react";
|
|
324
|
+
function useDismissableDisclosure({
|
|
325
|
+
open,
|
|
326
|
+
onClose,
|
|
327
|
+
panelRef,
|
|
328
|
+
triggerRef,
|
|
329
|
+
initialFocusRef
|
|
330
|
+
}) {
|
|
331
|
+
useEffect2(() => {
|
|
332
|
+
if (open) initialFocusRef?.current?.focus();
|
|
333
|
+
}, [open]);
|
|
334
|
+
useEffect2(() => {
|
|
335
|
+
if (!open) return;
|
|
336
|
+
const onKey = (e) => {
|
|
337
|
+
if (e.key === "Escape") {
|
|
338
|
+
onClose();
|
|
339
|
+
triggerRef.current?.focus();
|
|
340
|
+
}
|
|
341
|
+
};
|
|
342
|
+
const onPointerDown = (e) => {
|
|
343
|
+
const target = e.target;
|
|
344
|
+
if (!panelRef.current?.contains(target) && !triggerRef.current?.contains(target)) {
|
|
345
|
+
onClose();
|
|
346
|
+
}
|
|
347
|
+
};
|
|
348
|
+
document.addEventListener("keydown", onKey);
|
|
349
|
+
document.addEventListener("pointerdown", onPointerDown);
|
|
350
|
+
return () => {
|
|
351
|
+
document.removeEventListener("keydown", onKey);
|
|
352
|
+
document.removeEventListener("pointerdown", onPointerDown);
|
|
353
|
+
};
|
|
354
|
+
}, [open, onClose, panelRef, triggerRef]);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// src/ladder/types.ts
|
|
358
|
+
import { z } from "zod";
|
|
359
|
+
var zAppRungItem = z.object({
|
|
360
|
+
type: z.literal("app"),
|
|
361
|
+
moduleId: z.string().min(1)
|
|
362
|
+
});
|
|
363
|
+
var zGroupRungItem = z.object({
|
|
364
|
+
type: z.literal("group"),
|
|
365
|
+
id: z.string().min(1),
|
|
366
|
+
label: z.string().min(1),
|
|
367
|
+
icon: z.string().min(1),
|
|
368
|
+
children: z.array(z.string().min(1)).min(1)
|
|
369
|
+
});
|
|
370
|
+
var zRungItem = z.discriminatedUnion("type", [zAppRungItem, zGroupRungItem]);
|
|
371
|
+
var zSidebarRungShape = z.object({
|
|
372
|
+
rows: z.number().int().positive(),
|
|
373
|
+
items: z.array(zRungItem).min(1)
|
|
374
|
+
});
|
|
375
|
+
function moduleIdsOf(rung) {
|
|
376
|
+
const ids = [];
|
|
377
|
+
for (const item of rung.items) {
|
|
378
|
+
if (item.type === "app") {
|
|
379
|
+
ids.push(item.moduleId);
|
|
380
|
+
} else {
|
|
381
|
+
ids.push(...item.children);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
return ids;
|
|
385
|
+
}
|
|
386
|
+
var zSidebarLadder = z.object({
|
|
387
|
+
version: z.number().int().nonnegative(),
|
|
388
|
+
rungs: z.array(zSidebarRungShape).min(1)
|
|
389
|
+
}).superRefine((ladder, ctx) => {
|
|
390
|
+
const { rungs } = ladder;
|
|
391
|
+
if (rungs.length === 0) return;
|
|
392
|
+
const rung0HasGroup = rungs[0].items.some((item) => item.type === "group");
|
|
393
|
+
if (rung0HasGroup) {
|
|
394
|
+
ctx.addIssue({
|
|
395
|
+
code: z.ZodIssueCode.custom,
|
|
396
|
+
message: "rung 0 must be fully expanded \u2014 no folded groups allowed",
|
|
397
|
+
path: ["rungs", 0, "items"]
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
let baselineModuleSet = null;
|
|
401
|
+
rungs.forEach((rung, i) => {
|
|
402
|
+
if (rung.rows !== rung.items.length) {
|
|
403
|
+
ctx.addIssue({
|
|
404
|
+
code: z.ZodIssueCode.custom,
|
|
405
|
+
message: `rung ${i}: rows (${rung.rows}) must equal its item count (${rung.items.length})`,
|
|
406
|
+
path: ["rungs", i, "rows"]
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
if (i > 0 && rung.rows >= rungs[i - 1].rows) {
|
|
410
|
+
ctx.addIssue({
|
|
411
|
+
code: z.ZodIssueCode.custom,
|
|
412
|
+
message: `rung ${i}: rows (${rung.rows}) must be strictly less than rung ${i - 1}'s (${rungs[i - 1].rows})`,
|
|
413
|
+
path: ["rungs", i, "rows"]
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
const ids = moduleIdsOf(rung);
|
|
417
|
+
const seen = /* @__PURE__ */ new Set();
|
|
418
|
+
for (const id of ids) {
|
|
419
|
+
if (seen.has(id)) {
|
|
420
|
+
ctx.addIssue({
|
|
421
|
+
code: z.ZodIssueCode.custom,
|
|
422
|
+
message: `rung ${i}: module "${id}" appears more than once (as two rows, or as a row AND inside a group)`,
|
|
423
|
+
path: ["rungs", i]
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
seen.add(id);
|
|
427
|
+
}
|
|
428
|
+
const groupIds = /* @__PURE__ */ new Set();
|
|
429
|
+
for (const item of rung.items) {
|
|
430
|
+
if (item.type !== "group") continue;
|
|
431
|
+
if (groupIds.has(item.id)) {
|
|
432
|
+
ctx.addIssue({
|
|
433
|
+
code: z.ZodIssueCode.custom,
|
|
434
|
+
message: `rung ${i}: group id "${item.id}" appears more than once`,
|
|
435
|
+
path: ["rungs", i]
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
groupIds.add(item.id);
|
|
439
|
+
}
|
|
440
|
+
if (baselineModuleSet === null) {
|
|
441
|
+
baselineModuleSet = seen;
|
|
442
|
+
} else {
|
|
443
|
+
const missing = [...baselineModuleSet].filter((id) => !seen.has(id));
|
|
444
|
+
const extra = [...seen].filter((id) => !baselineModuleSet.has(id));
|
|
445
|
+
if (missing.length > 0 || extra.length > 0) {
|
|
446
|
+
ctx.addIssue({
|
|
447
|
+
code: z.ZodIssueCode.custom,
|
|
448
|
+
message: `rung ${i}: module set differs from rung 0 (missing: ${missing.join(", ") || "none"}; extra: ${extra.join(", ") || "none"})`,
|
|
449
|
+
path: ["rungs", i]
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
});
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
// src/ladder/fallback.ts
|
|
457
|
+
var FALLBACK_GROUP_ICON = "Layers";
|
|
458
|
+
function deriveFallbackLadder(registry, groupOrder) {
|
|
459
|
+
const rung0Items = registry.map((m) => ({
|
|
460
|
+
type: "app",
|
|
461
|
+
moduleId: m.moduleId
|
|
462
|
+
}));
|
|
463
|
+
const rungs = [{ rows: rung0Items.length, items: rung0Items }];
|
|
464
|
+
const effectiveGroupOrder = [.../* @__PURE__ */ new Set([...groupOrder, ...registry.map((m) => m.group)])];
|
|
465
|
+
const foldOrder = [...effectiveGroupOrder].reverse();
|
|
466
|
+
const folded = /* @__PURE__ */ new Set();
|
|
467
|
+
let previousRows = rungs[0].rows;
|
|
468
|
+
for (const group of foldOrder) {
|
|
469
|
+
const membersOfGroup = registry.filter((m) => m.group === group).map((m) => m.moduleId);
|
|
470
|
+
if (membersOfGroup.length === 0) continue;
|
|
471
|
+
folded.add(group);
|
|
472
|
+
const items = [];
|
|
473
|
+
for (const g of effectiveGroupOrder) {
|
|
474
|
+
const membersOfG = registry.filter((m) => m.group === g).map((m) => m.moduleId);
|
|
475
|
+
if (membersOfG.length === 0) continue;
|
|
476
|
+
if (folded.has(g)) {
|
|
477
|
+
items.push({ type: "group", id: g, label: g, icon: FALLBACK_GROUP_ICON, children: membersOfG });
|
|
478
|
+
} else {
|
|
479
|
+
for (const moduleId of membersOfG) {
|
|
480
|
+
items.push({ type: "app", moduleId });
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
if (items.length < previousRows) {
|
|
485
|
+
rungs.push({ rows: items.length, items });
|
|
486
|
+
previousRows = items.length;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
return { version: 1, rungs };
|
|
490
|
+
}
|
|
491
|
+
function reconcileLadder(ladder, registry) {
|
|
492
|
+
const validIds = new Set(registry.map((m) => m.moduleId));
|
|
493
|
+
function dropUnknown(items) {
|
|
494
|
+
const out = [];
|
|
495
|
+
for (const item of items) {
|
|
496
|
+
if (item.type === "app") {
|
|
497
|
+
if (validIds.has(item.moduleId)) out.push(item);
|
|
498
|
+
} else {
|
|
499
|
+
const children = item.children.filter((id) => validIds.has(id));
|
|
500
|
+
if (children.length > 0) out.push({ ...item, children });
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
return out;
|
|
504
|
+
}
|
|
505
|
+
const cleanedRungs = ladder.rungs.map((rung) => {
|
|
506
|
+
const items = dropUnknown(rung.items);
|
|
507
|
+
return { rows: items.length, items };
|
|
508
|
+
});
|
|
509
|
+
const rung0Ids = new Set(
|
|
510
|
+
(cleanedRungs[0]?.items ?? []).flatMap((i) => i.type === "app" ? [i.moduleId] : i.children)
|
|
511
|
+
);
|
|
512
|
+
const missing = registry.filter((m) => !rung0Ids.has(m.moduleId));
|
|
513
|
+
if (missing.length === 0) {
|
|
514
|
+
return { ...ladder, rungs: cleanedRungs };
|
|
515
|
+
}
|
|
516
|
+
const appended = missing.map((m) => ({ type: "app", moduleId: m.moduleId }));
|
|
517
|
+
const withAppended = cleanedRungs.map((rung) => ({
|
|
518
|
+
rows: rung.items.length + appended.length,
|
|
519
|
+
items: [...rung.items, ...appended]
|
|
520
|
+
}));
|
|
521
|
+
return { ...ladder, rungs: withAppended };
|
|
522
|
+
}
|
|
523
|
+
function resolveLadder(raw, registry, groupOrder) {
|
|
524
|
+
if (raw != null) {
|
|
525
|
+
const parsed = zSidebarLadder.safeParse(raw);
|
|
526
|
+
if (parsed.success) {
|
|
527
|
+
const reconciled = reconcileLadder(parsed.data, registry);
|
|
528
|
+
if (zSidebarLadder.safeParse(reconciled).success) {
|
|
529
|
+
return reconciled;
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
return deriveFallbackLadder(registry, groupOrder);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// src/ladder/use-ladder-rung.ts
|
|
537
|
+
import { useLayoutEffect, useRef as useRef2, useState as useState3 } from "react";
|
|
538
|
+
var DEFAULT_ROW_HEIGHT = 36;
|
|
539
|
+
var DEFAULT_HYSTERESIS_ROWS = 1;
|
|
540
|
+
function useLadderRung(ladder, containerRef, options = {}) {
|
|
541
|
+
const rowHeight = options.rowHeight ?? DEFAULT_ROW_HEIGHT;
|
|
542
|
+
const hysteresisRows = options.hysteresisRows ?? DEFAULT_HYSTERESIS_ROWS;
|
|
543
|
+
const [rungIndex, setRungIndex] = useState3(0);
|
|
544
|
+
const rungIndexRef = useRef2(0);
|
|
545
|
+
const ladderSignature = `${ladder.version}:${ladder.rungs.map((r) => r.rows).join(",")}`;
|
|
546
|
+
useLayoutEffect(() => {
|
|
547
|
+
rungIndexRef.current = 0;
|
|
548
|
+
setRungIndex(0);
|
|
549
|
+
const node = containerRef.current;
|
|
550
|
+
if (!node || typeof ResizeObserver === "undefined") return;
|
|
551
|
+
const select = (height) => {
|
|
552
|
+
const rowsAvailable = Math.floor(height / rowHeight);
|
|
553
|
+
const currentRows = ladder.rungs[rungIndexRef.current].rows;
|
|
554
|
+
let bestIndex = ladder.rungs.length - 1;
|
|
555
|
+
for (let i = 0; i < ladder.rungs.length; i++) {
|
|
556
|
+
if (ladder.rungs[i].rows <= rowsAvailable) {
|
|
557
|
+
bestIndex = i;
|
|
558
|
+
break;
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
const bestRows = ladder.rungs[bestIndex].rows;
|
|
562
|
+
const isSteppingUp = bestRows > currentRows;
|
|
563
|
+
if (isSteppingUp && rowsAvailable < bestRows + hysteresisRows) {
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
if (bestIndex !== rungIndexRef.current) {
|
|
567
|
+
rungIndexRef.current = bestIndex;
|
|
568
|
+
setRungIndex(bestIndex);
|
|
569
|
+
}
|
|
570
|
+
};
|
|
571
|
+
const ro = new ResizeObserver((entries) => {
|
|
572
|
+
const entry = entries[0];
|
|
573
|
+
if (entry) select(entry.contentRect.height);
|
|
574
|
+
});
|
|
575
|
+
ro.observe(node);
|
|
576
|
+
return () => ro.disconnect();
|
|
577
|
+
}, [ladderSignature, containerRef, rowHeight, hysteresisRows]);
|
|
578
|
+
return ladder.rungs[Math.min(rungIndex, ladder.rungs.length - 1)];
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
// src/ladder/flyout.tsx
|
|
582
|
+
import { useCallback as useCallback2, useEffect as useEffect3, useId as useId2, useLayoutEffect as useLayoutEffect2, useRef as useRef3, useState as useState4 } from "react";
|
|
583
|
+
import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
584
|
+
var PANEL_GAP = 4;
|
|
585
|
+
function Flyout({ label, icon, active, children, className }) {
|
|
586
|
+
const [hoverOpen, setHoverOpen] = useState4(false);
|
|
587
|
+
const [clickOpen, setClickOpen] = useState4(false);
|
|
588
|
+
const [panelPosition, setPanelPosition] = useState4(null);
|
|
589
|
+
const open = hoverOpen || clickOpen;
|
|
590
|
+
const panelId = useId2();
|
|
591
|
+
const triggerRef = useRef3(null);
|
|
592
|
+
const panelRef = useRef3(null);
|
|
593
|
+
const firstItemRef = useRef3(null);
|
|
594
|
+
const prefersReducedMotion = useMediaQuery("(prefers-reduced-motion: reduce)");
|
|
595
|
+
const Icon = resolveIcon(icon);
|
|
596
|
+
const reposition = useCallback2(() => {
|
|
597
|
+
const rect = triggerRef.current?.getBoundingClientRect();
|
|
598
|
+
if (rect) setPanelPosition({ top: rect.top, left: rect.right + PANEL_GAP });
|
|
599
|
+
}, []);
|
|
600
|
+
useLayoutEffect2(() => {
|
|
601
|
+
if (open) reposition();
|
|
602
|
+
}, [open, reposition]);
|
|
603
|
+
useEffect3(() => {
|
|
604
|
+
if (!open) return;
|
|
605
|
+
window.addEventListener("resize", reposition);
|
|
606
|
+
window.addEventListener("scroll", reposition, true);
|
|
607
|
+
return () => {
|
|
608
|
+
window.removeEventListener("resize", reposition);
|
|
609
|
+
window.removeEventListener("scroll", reposition, true);
|
|
610
|
+
};
|
|
611
|
+
}, [open, reposition]);
|
|
612
|
+
useDismissableDisclosure({
|
|
613
|
+
open,
|
|
614
|
+
onClose: () => {
|
|
615
|
+
setHoverOpen(false);
|
|
616
|
+
setClickOpen(false);
|
|
617
|
+
},
|
|
618
|
+
panelRef,
|
|
619
|
+
triggerRef
|
|
620
|
+
});
|
|
621
|
+
useEffect3(() => {
|
|
622
|
+
if (clickOpen && panelPosition) firstItemRef.current?.focus();
|
|
623
|
+
}, [clickOpen, panelPosition]);
|
|
624
|
+
return /* @__PURE__ */ jsxs2(
|
|
625
|
+
"div",
|
|
626
|
+
{
|
|
627
|
+
"data-testid": "ladder-flyout",
|
|
628
|
+
className: cn("relative", className),
|
|
629
|
+
onMouseEnter: () => setHoverOpen(true),
|
|
630
|
+
onMouseLeave: () => setHoverOpen(false),
|
|
631
|
+
children: [
|
|
632
|
+
/* @__PURE__ */ jsxs2(
|
|
633
|
+
"button",
|
|
634
|
+
{
|
|
635
|
+
ref: triggerRef,
|
|
636
|
+
type: "button",
|
|
637
|
+
"data-testid": "ladder-flyout-trigger",
|
|
638
|
+
"aria-expanded": open,
|
|
639
|
+
"aria-controls": panelId,
|
|
640
|
+
onClick: () => setClickOpen((v) => !v),
|
|
641
|
+
className: cn(
|
|
642
|
+
"group flex w-full items-center gap-2.5 rounded-md px-3 py-1.5 text-sm transition-colors",
|
|
643
|
+
active ? "bg-primary/10 text-foreground" : "text-muted-foreground hover:bg-foreground/5 hover:text-foreground"
|
|
644
|
+
),
|
|
645
|
+
children: [
|
|
646
|
+
/* @__PURE__ */ jsx3(
|
|
647
|
+
Icon,
|
|
648
|
+
{
|
|
649
|
+
className: cn("size-4 shrink-0", active ? "text-primary" : "text-muted-foreground"),
|
|
650
|
+
strokeWidth: 1.5,
|
|
651
|
+
"aria-hidden": "true"
|
|
652
|
+
}
|
|
653
|
+
),
|
|
654
|
+
/* @__PURE__ */ jsx3("span", { className: "truncate", children: label })
|
|
655
|
+
]
|
|
656
|
+
}
|
|
657
|
+
),
|
|
658
|
+
open && panelPosition && /* @__PURE__ */ jsx3(
|
|
659
|
+
"div",
|
|
660
|
+
{
|
|
661
|
+
ref: panelRef,
|
|
662
|
+
id: panelId,
|
|
663
|
+
"data-testid": "ladder-flyout-panel",
|
|
664
|
+
style: { position: "fixed", top: panelPosition.top, left: panelPosition.left },
|
|
665
|
+
className: cn(
|
|
666
|
+
"z-50 flex min-w-[12rem] flex-col gap-0.5 rounded-md border border-border bg-background p-2 shadow-lg",
|
|
667
|
+
!prefersReducedMotion && "transition-all duration-150 ease-out"
|
|
668
|
+
),
|
|
669
|
+
children: children.map((child, i) => /* @__PURE__ */ jsx3(
|
|
670
|
+
"a",
|
|
671
|
+
{
|
|
672
|
+
ref: i === 0 ? firstItemRef : void 0,
|
|
673
|
+
href: child.href,
|
|
674
|
+
"aria-current": child.active ? "page" : void 0,
|
|
675
|
+
className: cn(
|
|
676
|
+
"truncate rounded-md px-3 py-1.5 text-sm transition-colors",
|
|
677
|
+
child.active ? "bg-primary/10 text-foreground" : "text-muted-foreground hover:bg-foreground/5 hover:text-foreground"
|
|
678
|
+
),
|
|
679
|
+
children: child.label
|
|
680
|
+
},
|
|
681
|
+
child.moduleId
|
|
682
|
+
))
|
|
683
|
+
}
|
|
684
|
+
)
|
|
685
|
+
]
|
|
686
|
+
}
|
|
687
|
+
);
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
// src/app-sidebar.tsx
|
|
691
|
+
import { Fragment as Fragment2, jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
692
|
+
function isHrefActive(activePath, href) {
|
|
693
|
+
return activePath === href || activePath.startsWith(`${href}/`);
|
|
694
|
+
}
|
|
695
|
+
function NavLink({
|
|
696
|
+
href,
|
|
697
|
+
active,
|
|
698
|
+
icon,
|
|
699
|
+
label,
|
|
700
|
+
testId
|
|
701
|
+
}) {
|
|
702
|
+
const Icon = resolveIcon(icon);
|
|
703
|
+
return /* @__PURE__ */ jsxs3(
|
|
704
|
+
"a",
|
|
705
|
+
{
|
|
706
|
+
href,
|
|
707
|
+
"data-testid": testId,
|
|
708
|
+
"aria-current": active ? "page" : void 0,
|
|
709
|
+
className: cn(
|
|
710
|
+
"group flex items-center gap-2.5 rounded-md px-3 py-1.5 text-sm transition-colors",
|
|
711
|
+
active ? "bg-primary/10 text-foreground" : "text-muted-foreground hover:bg-foreground/5 hover:text-foreground"
|
|
712
|
+
),
|
|
713
|
+
children: [
|
|
714
|
+
/* @__PURE__ */ jsx4(
|
|
715
|
+
Icon,
|
|
716
|
+
{
|
|
717
|
+
className: cn("size-4 shrink-0", active ? "text-primary" : "text-muted-foreground"),
|
|
718
|
+
strokeWidth: 1.5,
|
|
719
|
+
"aria-hidden": "true"
|
|
720
|
+
}
|
|
721
|
+
),
|
|
722
|
+
/* @__PURE__ */ jsx4("span", { className: "truncate", children: label })
|
|
723
|
+
]
|
|
724
|
+
}
|
|
725
|
+
);
|
|
726
|
+
}
|
|
727
|
+
function AppSidebar({
|
|
728
|
+
ladder,
|
|
729
|
+
modules,
|
|
730
|
+
groupOrder,
|
|
731
|
+
activePath,
|
|
732
|
+
brand,
|
|
733
|
+
topItem,
|
|
734
|
+
menuLabel = "Menu",
|
|
735
|
+
ladderOptions,
|
|
736
|
+
className
|
|
737
|
+
}) {
|
|
738
|
+
const isDesktop = useMediaQuery(`(min-width: ${BREAKPOINTS.md}px)`);
|
|
739
|
+
const [mobileOpen, setMobileOpen] = useState5(false);
|
|
740
|
+
const containerRef = useRef4(null);
|
|
741
|
+
const drawerId = useId3();
|
|
742
|
+
const mobileTriggerRef = useRef4(null);
|
|
743
|
+
const mobileDrawerRef = useRef4(null);
|
|
744
|
+
const mobileCloseRef = useRef4(null);
|
|
745
|
+
const moduleMap = useMemo(() => new Map(modules.map((m) => [m.moduleId, m])), [modules]);
|
|
746
|
+
const registryForLadder = useMemo(
|
|
747
|
+
() => modules.map((m) => ({ moduleId: m.moduleId, group: m.group })),
|
|
748
|
+
[modules]
|
|
749
|
+
);
|
|
750
|
+
const resolvedLadder = useMemo(
|
|
751
|
+
() => resolveLadder(ladder, registryForLadder, groupOrder),
|
|
752
|
+
[ladder, registryForLadder, groupOrder]
|
|
753
|
+
);
|
|
754
|
+
const rung = useLadderRung(resolvedLadder, containerRef, ladderOptions);
|
|
755
|
+
const rung0 = resolvedLadder.rungs[0];
|
|
756
|
+
useEffect4(() => {
|
|
757
|
+
if (isDesktop) setMobileOpen(false);
|
|
758
|
+
}, [isDesktop]);
|
|
759
|
+
useDismissableDisclosure({
|
|
760
|
+
open: mobileOpen,
|
|
761
|
+
onClose: () => setMobileOpen(false),
|
|
762
|
+
panelRef: mobileDrawerRef,
|
|
763
|
+
triggerRef: mobileTriggerRef,
|
|
764
|
+
initialFocusRef: mobileCloseRef
|
|
765
|
+
});
|
|
766
|
+
function renderAppItem(item, testIdPrefix) {
|
|
767
|
+
const def = moduleMap.get(item.moduleId);
|
|
768
|
+
if (!def) return null;
|
|
769
|
+
return /* @__PURE__ */ jsx4(
|
|
770
|
+
NavLink,
|
|
771
|
+
{
|
|
772
|
+
href: def.href,
|
|
773
|
+
active: isHrefActive(activePath, def.href),
|
|
774
|
+
icon: def.icon,
|
|
775
|
+
label: def.label,
|
|
776
|
+
testId: `${testIdPrefix}-${item.moduleId}`
|
|
777
|
+
},
|
|
778
|
+
item.moduleId
|
|
779
|
+
);
|
|
780
|
+
}
|
|
781
|
+
function renderGroupItem(item) {
|
|
782
|
+
const children = item.children.map((moduleId) => {
|
|
783
|
+
const def = moduleMap.get(moduleId);
|
|
784
|
+
const href = def?.href ?? "#";
|
|
785
|
+
return {
|
|
786
|
+
moduleId,
|
|
787
|
+
label: def?.label ?? moduleId,
|
|
788
|
+
href,
|
|
789
|
+
active: isHrefActive(activePath, href)
|
|
790
|
+
};
|
|
791
|
+
});
|
|
792
|
+
const active = children.some((c) => c.active);
|
|
793
|
+
return /* @__PURE__ */ jsx4(Flyout, { label: item.label, icon: item.icon, active, children }, item.id);
|
|
794
|
+
}
|
|
795
|
+
const brandBlock = brand && /* @__PURE__ */ jsxs3(
|
|
796
|
+
"a",
|
|
797
|
+
{
|
|
798
|
+
href: brand.href,
|
|
799
|
+
"data-testid": "app-sidebar-brand",
|
|
800
|
+
className: "flex items-center gap-2.5 border-b border-border px-5 py-4",
|
|
801
|
+
children: [
|
|
802
|
+
/* @__PURE__ */ jsx4("span", { className: "grid size-7 place-items-center rounded-md bg-primary text-primary-foreground", children: /* @__PURE__ */ jsx4("span", { className: "font-mono text-sm font-bold", children: (brand.label[0] ?? "r").toUpperCase() }) }),
|
|
803
|
+
/* @__PURE__ */ jsx4("span", { className: "truncate text-sm font-semibold tracking-tight", children: brand.label })
|
|
804
|
+
]
|
|
805
|
+
}
|
|
806
|
+
);
|
|
807
|
+
function topItemBlock(testId) {
|
|
808
|
+
if (!topItem) return null;
|
|
809
|
+
return /* @__PURE__ */ jsx4("div", { className: "px-3 pt-4", children: /* @__PURE__ */ jsx4(
|
|
810
|
+
NavLink,
|
|
811
|
+
{
|
|
812
|
+
href: topItem.href,
|
|
813
|
+
active: isHrefActive(activePath, topItem.href),
|
|
814
|
+
icon: topItem.icon,
|
|
815
|
+
label: topItem.label,
|
|
816
|
+
testId
|
|
817
|
+
}
|
|
818
|
+
) });
|
|
819
|
+
}
|
|
820
|
+
return /* @__PURE__ */ jsxs3(Fragment2, { children: [
|
|
821
|
+
!isDesktop && /* @__PURE__ */ jsx4(
|
|
822
|
+
"button",
|
|
823
|
+
{
|
|
824
|
+
ref: mobileTriggerRef,
|
|
825
|
+
type: "button",
|
|
826
|
+
"data-testid": "app-sidebar-mobile-trigger",
|
|
827
|
+
"aria-label": menuLabel,
|
|
828
|
+
"aria-expanded": mobileOpen,
|
|
829
|
+
"aria-controls": drawerId,
|
|
830
|
+
onClick: () => setMobileOpen((v) => !v),
|
|
831
|
+
className: "flex items-center justify-center rounded-md border border-border p-2 text-foreground",
|
|
832
|
+
children: mobileOpen ? /* @__PURE__ */ jsx4(X2, { className: "h-5 w-5", "aria-hidden": "true" }) : /* @__PURE__ */ jsx4(Menu2, { className: "h-5 w-5", "aria-hidden": "true" })
|
|
833
|
+
}
|
|
834
|
+
),
|
|
835
|
+
!isDesktop && mobileOpen && /* @__PURE__ */ jsxs3("div", { className: "fixed inset-0 z-50", children: [
|
|
836
|
+
/* @__PURE__ */ jsx4(
|
|
837
|
+
"div",
|
|
838
|
+
{
|
|
839
|
+
"data-testid": "app-sidebar-mobile-scrim",
|
|
840
|
+
className: "absolute inset-0 bg-black/50",
|
|
841
|
+
"aria-hidden": "true",
|
|
842
|
+
onClick: () => setMobileOpen(false)
|
|
843
|
+
}
|
|
844
|
+
),
|
|
845
|
+
/* @__PURE__ */ jsxs3(
|
|
846
|
+
"div",
|
|
847
|
+
{
|
|
848
|
+
ref: mobileDrawerRef,
|
|
849
|
+
id: drawerId,
|
|
850
|
+
role: "dialog",
|
|
851
|
+
"aria-modal": "true",
|
|
852
|
+
"aria-label": "Navigation",
|
|
853
|
+
"data-testid": "app-sidebar-mobile-drawer",
|
|
854
|
+
className: "absolute inset-y-0 left-0 flex w-72 flex-col overflow-y-auto border-r border-border bg-sidebar",
|
|
855
|
+
children: [
|
|
856
|
+
/* @__PURE__ */ jsx4("div", { className: "sticky top-0 z-10 flex justify-end bg-sidebar p-2", children: /* @__PURE__ */ jsx4(
|
|
857
|
+
"button",
|
|
858
|
+
{
|
|
859
|
+
ref: mobileCloseRef,
|
|
860
|
+
type: "button",
|
|
861
|
+
"data-testid": "app-sidebar-mobile-close",
|
|
862
|
+
"aria-label": `Close ${menuLabel.toLowerCase()}`,
|
|
863
|
+
onClick: () => setMobileOpen(false),
|
|
864
|
+
className: "flex items-center justify-center rounded-md p-2 text-foreground hover:bg-foreground/5",
|
|
865
|
+
children: /* @__PURE__ */ jsx4(X2, { className: "h-5 w-5", "aria-hidden": "true" })
|
|
866
|
+
}
|
|
867
|
+
) }),
|
|
868
|
+
brandBlock,
|
|
869
|
+
topItemBlock("app-sidebar-mobile-top-item"),
|
|
870
|
+
/* @__PURE__ */ jsx4("nav", { className: "flex-1 px-3 py-4", children: /* @__PURE__ */ jsx4("div", { className: "flex flex-col gap-0.5", children: rung0.items.map(
|
|
871
|
+
(item) => item.type === "app" ? renderAppItem(item, "app-sidebar-mobile-navlink") : null
|
|
872
|
+
) }) })
|
|
873
|
+
]
|
|
874
|
+
}
|
|
875
|
+
)
|
|
876
|
+
] }),
|
|
877
|
+
/* @__PURE__ */ jsxs3(
|
|
878
|
+
"aside",
|
|
879
|
+
{
|
|
880
|
+
"data-testid": "app-sidebar-desktop",
|
|
881
|
+
className: cn("hidden w-64 shrink-0 flex-col border-r border-border bg-sidebar md:flex", className),
|
|
882
|
+
children: [
|
|
883
|
+
brandBlock,
|
|
884
|
+
topItemBlock("app-sidebar-top-item"),
|
|
885
|
+
/* @__PURE__ */ jsx4(
|
|
886
|
+
"nav",
|
|
887
|
+
{
|
|
888
|
+
ref: containerRef,
|
|
889
|
+
"data-testid": "app-sidebar-nav",
|
|
890
|
+
className: "flex-1 overflow-y-auto px-3 py-4",
|
|
891
|
+
children: /* @__PURE__ */ jsx4("div", { className: "flex flex-col gap-0.5", children: rung.items.map((item) => item.type === "app" ? renderAppItem(item, "app-sidebar-navlink") : renderGroupItem(item)) })
|
|
892
|
+
}
|
|
893
|
+
)
|
|
894
|
+
]
|
|
895
|
+
}
|
|
896
|
+
)
|
|
897
|
+
] });
|
|
898
|
+
}
|
|
899
|
+
export {
|
|
900
|
+
AppSidebar,
|
|
901
|
+
BrandGlyph,
|
|
902
|
+
Flyout,
|
|
903
|
+
ResponsiveHeader,
|
|
904
|
+
deriveFallbackLadder,
|
|
905
|
+
faviconUrl,
|
|
906
|
+
reconcileLadder,
|
|
907
|
+
resolveLadder,
|
|
908
|
+
useLadderRung,
|
|
909
|
+
zSidebarLadder
|
|
910
|
+
};
|