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