@meetreeve/ui 0.2.0 → 0.5.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 +46 -0
- package/dist/index.d.mts +139 -6
- package/dist/index.d.ts +139 -6
- package/dist/index.js +376 -106
- package/dist/index.mjs +375 -109
- package/dist/tokens/index.d.mts +15 -1
- package/dist/tokens/index.d.ts +15 -1
- package/dist/tokens/index.js +8 -2
- package/dist/tokens/index.mjs +6 -1
- package/package.json +5 -1
package/dist/index.js
CHANGED
|
@@ -33,13 +33,17 @@ var src_exports = {};
|
|
|
33
33
|
__export(src_exports, {
|
|
34
34
|
AppSidebar: () => AppSidebar,
|
|
35
35
|
BrandGlyph: () => BrandGlyph,
|
|
36
|
+
ContextSwitcher: () => ContextSwitcher,
|
|
36
37
|
Flyout: () => Flyout,
|
|
38
|
+
REEVE_ACCENT: () => REEVE_ACCENT,
|
|
37
39
|
ResponsiveHeader: () => ResponsiveHeader,
|
|
38
40
|
deriveFallbackLadder: () => deriveFallbackLadder,
|
|
39
41
|
faviconUrl: () => faviconUrl,
|
|
42
|
+
groupByOrg: () => groupByOrg,
|
|
40
43
|
reconcileLadder: () => reconcileLadder,
|
|
41
44
|
resolveLadder: () => resolveLadder,
|
|
42
45
|
useLadderRung: () => useLadderRung,
|
|
46
|
+
useResponsivePlaceholder: () => useResponsivePlaceholder,
|
|
43
47
|
zSidebarLadder: () => zSidebarLadder
|
|
44
48
|
});
|
|
45
49
|
module.exports = __toCommonJS(src_exports);
|
|
@@ -68,10 +72,9 @@ function cn(...inputs) {
|
|
|
68
72
|
}
|
|
69
73
|
|
|
70
74
|
// src/lib/initials.ts
|
|
71
|
-
function
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
return parts.map((p) => p[0]?.toUpperCase() ?? "").join("") || "\xB7\xB7";
|
|
75
|
+
function firstInitial(name) {
|
|
76
|
+
const trimmed = (name ?? "").trim();
|
|
77
|
+
return trimmed ? trimmed[0].toUpperCase() : "?";
|
|
75
78
|
}
|
|
76
79
|
|
|
77
80
|
// src/lib/monogram.ts
|
|
@@ -101,6 +104,7 @@ function monogramColor(seed) {
|
|
|
101
104
|
|
|
102
105
|
// src/brand-glyph.tsx
|
|
103
106
|
var import_jsx_runtime = require("react/jsx-runtime");
|
|
107
|
+
var REEVE_ACCENT = "#FF6B2B";
|
|
104
108
|
function faviconUrl(domain) {
|
|
105
109
|
if (!domain) return null;
|
|
106
110
|
const host = domain.trim().toLowerCase();
|
|
@@ -143,19 +147,24 @@ function BrandGlyph({ brand, size = "sm", subtle = false, className }) {
|
|
|
143
147
|
}
|
|
144
148
|
);
|
|
145
149
|
}
|
|
146
|
-
|
|
150
|
+
const letter = firstInitial(brand.name);
|
|
151
|
+
const hasRealLetter = letter !== "?";
|
|
152
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
147
153
|
"span",
|
|
148
154
|
{
|
|
149
155
|
"aria-hidden": true,
|
|
150
156
|
"data-testid": "brand-monogram",
|
|
151
157
|
className: cn(
|
|
152
|
-
"flex shrink-0 select-none items-
|
|
153
|
-
size === "lg" ? "text-[
|
|
158
|
+
"flex shrink-0 select-none items-baseline justify-center font-semibold uppercase",
|
|
159
|
+
size === "lg" ? "text-[13px]" : "text-[11px]",
|
|
154
160
|
box,
|
|
155
161
|
className
|
|
156
162
|
),
|
|
157
|
-
style: {
|
|
158
|
-
children:
|
|
163
|
+
style: { color: brand.color || monogramColor(brand.name || "?") },
|
|
164
|
+
children: [
|
|
165
|
+
letter,
|
|
166
|
+
hasRealLetter && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { "data-testid": "brand-monogram-dot", style: { color: brand.accentColor || REEVE_ACCENT }, children: "." })
|
|
167
|
+
]
|
|
159
168
|
}
|
|
160
169
|
);
|
|
161
170
|
}
|
|
@@ -164,23 +173,21 @@ function BrandGlyph({ brand, size = "sm", subtle = false, className }) {
|
|
|
164
173
|
var import_react3 = require("react");
|
|
165
174
|
var import_lucide_react = require("lucide-react");
|
|
166
175
|
|
|
167
|
-
// src/lib/use-
|
|
176
|
+
// src/lib/use-container-width-at-least.ts
|
|
168
177
|
var import_react2 = require("react");
|
|
169
|
-
function
|
|
170
|
-
const
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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);
|
|
178
|
+
function useContainerWidthAtLeast(ref, px) {
|
|
179
|
+
const [atLeast, setAtLeast] = (0, import_react2.useState)(false);
|
|
180
|
+
(0, import_react2.useEffect)(() => {
|
|
181
|
+
const el = ref.current;
|
|
182
|
+
if (!el || typeof ResizeObserver === "undefined") return;
|
|
183
|
+
const ro = new ResizeObserver(([entry]) => {
|
|
184
|
+
if (!entry) return;
|
|
185
|
+
setAtLeast(entry.contentRect.width >= px);
|
|
186
|
+
});
|
|
187
|
+
ro.observe(el);
|
|
188
|
+
return () => ro.disconnect();
|
|
189
|
+
}, [ref, px]);
|
|
190
|
+
return atLeast;
|
|
184
191
|
}
|
|
185
192
|
|
|
186
193
|
// src/tokens.ts
|
|
@@ -200,6 +207,10 @@ var BREAKPOINTS = {
|
|
|
200
207
|
/** Tailwind `md` — the header switches to its full desktop layout at/above this. */
|
|
201
208
|
md: 768
|
|
202
209
|
};
|
|
210
|
+
var TOUCH_TARGET_STYLE = {
|
|
211
|
+
minWidth: MIN_SIZES.touchTarget,
|
|
212
|
+
minHeight: MIN_SIZES.touchTarget
|
|
213
|
+
};
|
|
203
214
|
|
|
204
215
|
// src/responsive-header.tsx
|
|
205
216
|
var import_jsx_runtime2 = require("react/jsx-runtime");
|
|
@@ -209,15 +220,17 @@ function MenuIcon({ open }) {
|
|
|
209
220
|
function SecondaryControls({
|
|
210
221
|
nav,
|
|
211
222
|
auth,
|
|
212
|
-
themeToggle
|
|
223
|
+
themeToggle,
|
|
224
|
+
linkComponent
|
|
213
225
|
}) {
|
|
226
|
+
const LinkEl = linkComponent ?? "a";
|
|
214
227
|
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
|
|
215
228
|
nav?.map(
|
|
216
229
|
(item) => (
|
|
217
230
|
// A link navigates; an action-only item (no href) is a button — never an
|
|
218
231
|
// <a href="#"> that scroll-jumps and pushes a history entry.
|
|
219
232
|
item.href ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
220
|
-
|
|
233
|
+
LinkEl,
|
|
221
234
|
{
|
|
222
235
|
href: item.href,
|
|
223
236
|
onClick: item.onSelect,
|
|
@@ -225,15 +238,22 @@ function SecondaryControls({
|
|
|
225
238
|
children: item.label
|
|
226
239
|
},
|
|
227
240
|
item.label
|
|
228
|
-
) :
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
241
|
+
) : (
|
|
242
|
+
// A real <button> secondary control — MIN_SIZES.control is the
|
|
243
|
+
// token's exact stated purpose ("Generic minimum width/height for
|
|
244
|
+
// a secondary control (button, select)"), previously defined but
|
|
245
|
+
// unused anywhere in this package (DEV-4249).
|
|
246
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
247
|
+
"button",
|
|
248
|
+
{
|
|
249
|
+
type: "button",
|
|
250
|
+
onClick: item.onSelect,
|
|
251
|
+
style: { minWidth: MIN_SIZES.control, minHeight: MIN_SIZES.control },
|
|
252
|
+
className: "flex items-center justify-start text-left text-sm font-medium text-foreground/80 hover:text-foreground",
|
|
253
|
+
children: item.label
|
|
254
|
+
},
|
|
255
|
+
item.label
|
|
256
|
+
)
|
|
237
257
|
)
|
|
238
258
|
)
|
|
239
259
|
),
|
|
@@ -245,19 +265,23 @@ function ResponsiveHeader({
|
|
|
245
265
|
brand,
|
|
246
266
|
search,
|
|
247
267
|
nav,
|
|
268
|
+
brandHref,
|
|
269
|
+
linkComponent,
|
|
248
270
|
auth,
|
|
249
271
|
themeToggle,
|
|
250
272
|
menuLabel = "Menu",
|
|
251
273
|
className
|
|
252
274
|
}) {
|
|
253
|
-
const
|
|
275
|
+
const LinkEl = linkComponent ?? "a";
|
|
254
276
|
const [open, setOpen] = (0, import_react3.useState)(false);
|
|
255
277
|
const panelId = (0, import_react3.useId)();
|
|
278
|
+
const headerRef = (0, import_react3.useRef)(null);
|
|
256
279
|
const buttonRef = (0, import_react3.useRef)(null);
|
|
257
280
|
const panelRef = (0, import_react3.useRef)(null);
|
|
281
|
+
const containerAtOrAboveMd = useContainerWidthAtLeast(headerRef, BREAKPOINTS.md);
|
|
258
282
|
(0, import_react3.useEffect)(() => {
|
|
259
|
-
if (
|
|
260
|
-
}, [
|
|
283
|
+
if (containerAtOrAboveMd) setOpen(false);
|
|
284
|
+
}, [containerAtOrAboveMd]);
|
|
261
285
|
(0, import_react3.useEffect)(() => {
|
|
262
286
|
if (!open) return;
|
|
263
287
|
const onKey = (e) => {
|
|
@@ -280,27 +304,41 @@ function ResponsiveHeader({
|
|
|
280
304
|
};
|
|
281
305
|
}, [open]);
|
|
282
306
|
const hasSecondary = Boolean(nav && nav.length > 0 || auth || themeToggle);
|
|
307
|
+
const brandLockup = /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
|
|
308
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(BrandGlyph, { brand, size: "lg" }),
|
|
309
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
310
|
+
"span",
|
|
311
|
+
{
|
|
312
|
+
"data-testid": "header-wordmark",
|
|
313
|
+
className: "hidden truncate text-sm font-semibold text-foreground @min-[768px]:block",
|
|
314
|
+
style: { maxWidth: MIN_SIZES.wordmarkMaxWidth },
|
|
315
|
+
children: brand.name
|
|
316
|
+
}
|
|
317
|
+
)
|
|
318
|
+
] });
|
|
283
319
|
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
|
|
284
320
|
"header",
|
|
285
321
|
{
|
|
322
|
+
ref: headerRef,
|
|
286
323
|
"data-testid": "responsive-header",
|
|
287
324
|
className: cn(
|
|
288
|
-
|
|
325
|
+
// `@container` makes this header itself the query context, so the
|
|
326
|
+
// variants below react to ITS width (e.g. a sidebar), not the viewport.
|
|
327
|
+
"@container flex w-full items-center gap-3 border-b border-border bg-background px-4 py-2",
|
|
289
328
|
className
|
|
290
329
|
),
|
|
291
330
|
children: [
|
|
292
|
-
/* @__PURE__ */ (0, import_jsx_runtime2.
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
] }),
|
|
331
|
+
brandHref ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
332
|
+
LinkEl,
|
|
333
|
+
{
|
|
334
|
+
href: brandHref,
|
|
335
|
+
"aria-label": brand.name || "Home",
|
|
336
|
+
"data-testid": "header-brand",
|
|
337
|
+
style: TOUCH_TARGET_STYLE,
|
|
338
|
+
className: "flex shrink-0 items-center gap-2",
|
|
339
|
+
children: brandLockup
|
|
340
|
+
}
|
|
341
|
+
) : /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { "data-testid": "header-brand", className: "flex shrink-0 items-center gap-2", children: brandLockup }),
|
|
304
342
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
305
343
|
"div",
|
|
306
344
|
{
|
|
@@ -310,40 +348,91 @@ function ResponsiveHeader({
|
|
|
310
348
|
children: search
|
|
311
349
|
}
|
|
312
350
|
),
|
|
313
|
-
hasSecondary &&
|
|
351
|
+
hasSecondary && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
|
|
314
352
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
315
|
-
"
|
|
353
|
+
"nav",
|
|
316
354
|
{
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
355
|
+
"data-testid": "header-actions",
|
|
356
|
+
className: "hidden shrink-0 items-center gap-3 @min-[768px]:flex",
|
|
357
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
358
|
+
SecondaryControls,
|
|
359
|
+
{
|
|
360
|
+
nav,
|
|
361
|
+
auth,
|
|
362
|
+
themeToggle,
|
|
363
|
+
linkComponent
|
|
364
|
+
}
|
|
365
|
+
)
|
|
327
366
|
}
|
|
328
367
|
),
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
368
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "relative shrink-0 @min-[768px]:hidden", children: [
|
|
369
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
370
|
+
"button",
|
|
371
|
+
{
|
|
372
|
+
ref: buttonRef,
|
|
373
|
+
type: "button",
|
|
374
|
+
"data-testid": "header-menu-button",
|
|
375
|
+
"aria-label": menuLabel,
|
|
376
|
+
"aria-expanded": open,
|
|
377
|
+
"aria-controls": panelId,
|
|
378
|
+
onClick: () => setOpen((v) => !v),
|
|
379
|
+
className: "flex items-center justify-center rounded-md border border-border text-foreground",
|
|
380
|
+
style: TOUCH_TARGET_STYLE,
|
|
381
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(MenuIcon, { open })
|
|
382
|
+
}
|
|
383
|
+
),
|
|
384
|
+
open && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
385
|
+
"div",
|
|
386
|
+
{
|
|
387
|
+
ref: panelRef,
|
|
388
|
+
id: panelId,
|
|
389
|
+
"data-testid": "header-menu-panel",
|
|
390
|
+
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",
|
|
391
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
392
|
+
SecondaryControls,
|
|
393
|
+
{
|
|
394
|
+
nav,
|
|
395
|
+
auth,
|
|
396
|
+
themeToggle,
|
|
397
|
+
linkComponent
|
|
398
|
+
}
|
|
399
|
+
)
|
|
400
|
+
}
|
|
401
|
+
)
|
|
402
|
+
] })
|
|
403
|
+
] })
|
|
340
404
|
]
|
|
341
405
|
}
|
|
342
406
|
);
|
|
343
407
|
}
|
|
344
408
|
|
|
409
|
+
// src/lib/use-media-query.ts
|
|
410
|
+
var import_react4 = require("react");
|
|
411
|
+
function useMediaQuery(query) {
|
|
412
|
+
const subscribe = (0, import_react4.useCallback)(
|
|
413
|
+
(onChange) => {
|
|
414
|
+
if (typeof window === "undefined" || typeof window.matchMedia !== "function") {
|
|
415
|
+
return () => {
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
const mql = window.matchMedia(query);
|
|
419
|
+
mql.addEventListener("change", onChange);
|
|
420
|
+
return () => mql.removeEventListener("change", onChange);
|
|
421
|
+
},
|
|
422
|
+
[query]
|
|
423
|
+
);
|
|
424
|
+
const getSnapshot = () => typeof window !== "undefined" && typeof window.matchMedia === "function" ? window.matchMedia(query).matches : false;
|
|
425
|
+
return (0, import_react4.useSyncExternalStore)(subscribe, getSnapshot, () => false);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// src/use-responsive-placeholder.ts
|
|
429
|
+
function useResponsivePlaceholder(desktop, mobile) {
|
|
430
|
+
const isDesktop = useMediaQuery(`(min-width: ${BREAKPOINTS.md}px)`);
|
|
431
|
+
return isDesktop ? desktop : mobile ?? desktop;
|
|
432
|
+
}
|
|
433
|
+
|
|
345
434
|
// src/app-sidebar.tsx
|
|
346
|
-
var
|
|
435
|
+
var import_react8 = require("react");
|
|
347
436
|
var import_lucide_react2 = require("lucide-react");
|
|
348
437
|
|
|
349
438
|
// src/lib/resolve-icon.ts
|
|
@@ -364,7 +453,7 @@ function resolveIcon(name) {
|
|
|
364
453
|
}
|
|
365
454
|
|
|
366
455
|
// src/lib/use-dismissable-disclosure.ts
|
|
367
|
-
var
|
|
456
|
+
var import_react5 = require("react");
|
|
368
457
|
function useDismissableDisclosure({
|
|
369
458
|
open,
|
|
370
459
|
onClose,
|
|
@@ -372,10 +461,10 @@ function useDismissableDisclosure({
|
|
|
372
461
|
triggerRef,
|
|
373
462
|
initialFocusRef
|
|
374
463
|
}) {
|
|
375
|
-
(0,
|
|
464
|
+
(0, import_react5.useEffect)(() => {
|
|
376
465
|
if (open) initialFocusRef?.current?.focus();
|
|
377
466
|
}, [open]);
|
|
378
|
-
(0,
|
|
467
|
+
(0, import_react5.useEffect)(() => {
|
|
379
468
|
if (!open) return;
|
|
380
469
|
const onKey = (e) => {
|
|
381
470
|
if (e.key === "Escape") {
|
|
@@ -578,16 +667,16 @@ function resolveLadder(raw, registry, groupOrder) {
|
|
|
578
667
|
}
|
|
579
668
|
|
|
580
669
|
// src/ladder/use-ladder-rung.ts
|
|
581
|
-
var
|
|
670
|
+
var import_react6 = require("react");
|
|
582
671
|
var DEFAULT_ROW_HEIGHT = 36;
|
|
583
672
|
var DEFAULT_HYSTERESIS_ROWS = 1;
|
|
584
673
|
function useLadderRung(ladder, containerRef, options = {}) {
|
|
585
674
|
const rowHeight = options.rowHeight ?? DEFAULT_ROW_HEIGHT;
|
|
586
675
|
const hysteresisRows = options.hysteresisRows ?? DEFAULT_HYSTERESIS_ROWS;
|
|
587
|
-
const [rungIndex, setRungIndex] = (0,
|
|
588
|
-
const rungIndexRef = (0,
|
|
676
|
+
const [rungIndex, setRungIndex] = (0, import_react6.useState)(0);
|
|
677
|
+
const rungIndexRef = (0, import_react6.useRef)(0);
|
|
589
678
|
const ladderSignature = `${ladder.version}:${ladder.rungs.map((r) => r.rows).join(",")}`;
|
|
590
|
-
(0,
|
|
679
|
+
(0, import_react6.useLayoutEffect)(() => {
|
|
591
680
|
rungIndexRef.current = 0;
|
|
592
681
|
setRungIndex(0);
|
|
593
682
|
const node = containerRef.current;
|
|
@@ -623,28 +712,28 @@ function useLadderRung(ladder, containerRef, options = {}) {
|
|
|
623
712
|
}
|
|
624
713
|
|
|
625
714
|
// src/ladder/flyout.tsx
|
|
626
|
-
var
|
|
715
|
+
var import_react7 = require("react");
|
|
627
716
|
var import_jsx_runtime3 = require("react/jsx-runtime");
|
|
628
717
|
var PANEL_GAP = 4;
|
|
629
718
|
function Flyout({ label, icon, active, children, className }) {
|
|
630
|
-
const [hoverOpen, setHoverOpen] = (0,
|
|
631
|
-
const [clickOpen, setClickOpen] = (0,
|
|
632
|
-
const [panelPosition, setPanelPosition] = (0,
|
|
719
|
+
const [hoverOpen, setHoverOpen] = (0, import_react7.useState)(false);
|
|
720
|
+
const [clickOpen, setClickOpen] = (0, import_react7.useState)(false);
|
|
721
|
+
const [panelPosition, setPanelPosition] = (0, import_react7.useState)(null);
|
|
633
722
|
const open = hoverOpen || clickOpen;
|
|
634
|
-
const panelId = (0,
|
|
635
|
-
const triggerRef = (0,
|
|
636
|
-
const panelRef = (0,
|
|
637
|
-
const firstItemRef = (0,
|
|
723
|
+
const panelId = (0, import_react7.useId)();
|
|
724
|
+
const triggerRef = (0, import_react7.useRef)(null);
|
|
725
|
+
const panelRef = (0, import_react7.useRef)(null);
|
|
726
|
+
const firstItemRef = (0, import_react7.useRef)(null);
|
|
638
727
|
const prefersReducedMotion = useMediaQuery("(prefers-reduced-motion: reduce)");
|
|
639
728
|
const Icon = resolveIcon(icon);
|
|
640
|
-
const reposition = (0,
|
|
729
|
+
const reposition = (0, import_react7.useCallback)(() => {
|
|
641
730
|
const rect = triggerRef.current?.getBoundingClientRect();
|
|
642
731
|
if (rect) setPanelPosition({ top: rect.top, left: rect.right + PANEL_GAP });
|
|
643
732
|
}, []);
|
|
644
|
-
(0,
|
|
733
|
+
(0, import_react7.useLayoutEffect)(() => {
|
|
645
734
|
if (open) reposition();
|
|
646
735
|
}, [open, reposition]);
|
|
647
|
-
(0,
|
|
736
|
+
(0, import_react7.useEffect)(() => {
|
|
648
737
|
if (!open) return;
|
|
649
738
|
window.addEventListener("resize", reposition);
|
|
650
739
|
window.addEventListener("scroll", reposition, true);
|
|
@@ -662,7 +751,7 @@ function Flyout({ label, icon, active, children, className }) {
|
|
|
662
751
|
panelRef,
|
|
663
752
|
triggerRef
|
|
664
753
|
});
|
|
665
|
-
(0,
|
|
754
|
+
(0, import_react7.useEffect)(() => {
|
|
666
755
|
if (clickOpen && panelPosition) firstItemRef.current?.focus();
|
|
667
756
|
}, [clickOpen, panelPosition]);
|
|
668
757
|
return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
|
|
@@ -716,8 +805,9 @@ function Flyout({ label, icon, active, children, className }) {
|
|
|
716
805
|
ref: i === 0 ? firstItemRef : void 0,
|
|
717
806
|
href: child.href,
|
|
718
807
|
"aria-current": child.active ? "page" : void 0,
|
|
808
|
+
style: TOUCH_TARGET_STYLE,
|
|
719
809
|
className: cn(
|
|
720
|
-
"truncate rounded-md px-3 py-1.5 text-sm transition-colors",
|
|
810
|
+
"flex items-center truncate rounded-md px-3 py-1.5 text-sm transition-colors",
|
|
721
811
|
child.active ? "bg-primary/10 text-foreground" : "text-muted-foreground hover:bg-foreground/5 hover:text-foreground"
|
|
722
812
|
),
|
|
723
813
|
children: child.label
|
|
@@ -780,24 +870,24 @@ function AppSidebar({
|
|
|
780
870
|
className
|
|
781
871
|
}) {
|
|
782
872
|
const isDesktop = useMediaQuery(`(min-width: ${BREAKPOINTS.md}px)`);
|
|
783
|
-
const [mobileOpen, setMobileOpen] = (0,
|
|
784
|
-
const containerRef = (0,
|
|
785
|
-
const drawerId = (0,
|
|
786
|
-
const mobileTriggerRef = (0,
|
|
787
|
-
const mobileDrawerRef = (0,
|
|
788
|
-
const mobileCloseRef = (0,
|
|
789
|
-
const moduleMap = (0,
|
|
790
|
-
const registryForLadder = (0,
|
|
873
|
+
const [mobileOpen, setMobileOpen] = (0, import_react8.useState)(false);
|
|
874
|
+
const containerRef = (0, import_react8.useRef)(null);
|
|
875
|
+
const drawerId = (0, import_react8.useId)();
|
|
876
|
+
const mobileTriggerRef = (0, import_react8.useRef)(null);
|
|
877
|
+
const mobileDrawerRef = (0, import_react8.useRef)(null);
|
|
878
|
+
const mobileCloseRef = (0, import_react8.useRef)(null);
|
|
879
|
+
const moduleMap = (0, import_react8.useMemo)(() => new Map(modules.map((m) => [m.moduleId, m])), [modules]);
|
|
880
|
+
const registryForLadder = (0, import_react8.useMemo)(
|
|
791
881
|
() => modules.map((m) => ({ moduleId: m.moduleId, group: m.group })),
|
|
792
882
|
[modules]
|
|
793
883
|
);
|
|
794
|
-
const resolvedLadder = (0,
|
|
884
|
+
const resolvedLadder = (0, import_react8.useMemo)(
|
|
795
885
|
() => resolveLadder(ladder, registryForLadder, groupOrder),
|
|
796
886
|
[ladder, registryForLadder, groupOrder]
|
|
797
887
|
);
|
|
798
888
|
const rung = useLadderRung(resolvedLadder, containerRef, ladderOptions);
|
|
799
889
|
const rung0 = resolvedLadder.rungs[0];
|
|
800
|
-
(0,
|
|
890
|
+
(0, import_react8.useEffect)(() => {
|
|
801
891
|
if (isDesktop) setMobileOpen(false);
|
|
802
892
|
}, [isDesktop]);
|
|
803
893
|
useDismissableDisclosure({
|
|
@@ -872,6 +962,7 @@ function AppSidebar({
|
|
|
872
962
|
"aria-expanded": mobileOpen,
|
|
873
963
|
"aria-controls": drawerId,
|
|
874
964
|
onClick: () => setMobileOpen((v) => !v),
|
|
965
|
+
style: TOUCH_TARGET_STYLE,
|
|
875
966
|
className: "flex items-center justify-center rounded-md border border-border p-2 text-foreground",
|
|
876
967
|
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
968
|
}
|
|
@@ -905,6 +996,7 @@ function AppSidebar({
|
|
|
905
996
|
"data-testid": "app-sidebar-mobile-close",
|
|
906
997
|
"aria-label": `Close ${menuLabel.toLowerCase()}`,
|
|
907
998
|
onClick: () => setMobileOpen(false),
|
|
999
|
+
style: TOUCH_TARGET_STYLE,
|
|
908
1000
|
className: "flex items-center justify-center rounded-md p-2 text-foreground hover:bg-foreground/5",
|
|
909
1001
|
children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_lucide_react2.X, { className: "h-5 w-5", "aria-hidden": "true" })
|
|
910
1002
|
}
|
|
@@ -940,16 +1032,194 @@ function AppSidebar({
|
|
|
940
1032
|
)
|
|
941
1033
|
] });
|
|
942
1034
|
}
|
|
1035
|
+
|
|
1036
|
+
// src/context-switcher.tsx
|
|
1037
|
+
var import_lucide_react3 = require("lucide-react");
|
|
1038
|
+
|
|
1039
|
+
// src/lib/group-by-org.ts
|
|
1040
|
+
function groupItemsByOrg(rows, get) {
|
|
1041
|
+
const byOrg = /* @__PURE__ */ new Map();
|
|
1042
|
+
for (const row of rows) {
|
|
1043
|
+
const key = get.orgId(row) ?? "";
|
|
1044
|
+
let group = byOrg.get(key);
|
|
1045
|
+
if (!group) {
|
|
1046
|
+
group = {
|
|
1047
|
+
orgId: get.orgId(row) ?? null,
|
|
1048
|
+
orgName: get.orgName(row) ?? null,
|
|
1049
|
+
items: []
|
|
1050
|
+
};
|
|
1051
|
+
byOrg.set(key, group);
|
|
1052
|
+
}
|
|
1053
|
+
group.items.push(row);
|
|
1054
|
+
}
|
|
1055
|
+
const groups = [...byOrg.values()];
|
|
1056
|
+
for (const g of groups) {
|
|
1057
|
+
g.items.sort((a, b) => get.name(a).localeCompare(get.name(b)));
|
|
1058
|
+
}
|
|
1059
|
+
groups.sort((a, b) => (a.orgName ?? "").localeCompare(b.orgName ?? ""));
|
|
1060
|
+
return groups;
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
// src/lib/dropdown-menu.tsx
|
|
1064
|
+
var DropdownMenuPrimitive = __toESM(require("@radix-ui/react-dropdown-menu"));
|
|
1065
|
+
var import_jsx_runtime5 = require("react/jsx-runtime");
|
|
1066
|
+
function DropdownMenu(props) {
|
|
1067
|
+
return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(DropdownMenuPrimitive.Root, { "data-slot": "dropdown-menu", ...props });
|
|
1068
|
+
}
|
|
1069
|
+
function DropdownMenuTrigger(props) {
|
|
1070
|
+
return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(DropdownMenuPrimitive.Trigger, { "data-slot": "dropdown-menu-trigger", ...props });
|
|
1071
|
+
}
|
|
1072
|
+
function DropdownMenuContent({
|
|
1073
|
+
className,
|
|
1074
|
+
sideOffset = 4,
|
|
1075
|
+
...props
|
|
1076
|
+
}) {
|
|
1077
|
+
return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(DropdownMenuPrimitive.Portal, { children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
|
|
1078
|
+
DropdownMenuPrimitive.Content,
|
|
1079
|
+
{
|
|
1080
|
+
"data-slot": "dropdown-menu-content",
|
|
1081
|
+
sideOffset,
|
|
1082
|
+
className: cn(
|
|
1083
|
+
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
|
|
1084
|
+
className
|
|
1085
|
+
),
|
|
1086
|
+
...props
|
|
1087
|
+
}
|
|
1088
|
+
) });
|
|
1089
|
+
}
|
|
1090
|
+
function DropdownMenuItem({
|
|
1091
|
+
className,
|
|
1092
|
+
...props
|
|
1093
|
+
}) {
|
|
1094
|
+
return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
|
|
1095
|
+
DropdownMenuPrimitive.Item,
|
|
1096
|
+
{
|
|
1097
|
+
"data-slot": "dropdown-menu-item",
|
|
1098
|
+
className: cn(
|
|
1099
|
+
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
|
1100
|
+
className
|
|
1101
|
+
),
|
|
1102
|
+
...props
|
|
1103
|
+
}
|
|
1104
|
+
);
|
|
1105
|
+
}
|
|
1106
|
+
function DropdownMenuLabel({
|
|
1107
|
+
className,
|
|
1108
|
+
...props
|
|
1109
|
+
}) {
|
|
1110
|
+
return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
|
|
1111
|
+
DropdownMenuPrimitive.Label,
|
|
1112
|
+
{
|
|
1113
|
+
"data-slot": "dropdown-menu-label",
|
|
1114
|
+
className: cn("px-2 py-1.5 text-sm font-medium", className),
|
|
1115
|
+
...props
|
|
1116
|
+
}
|
|
1117
|
+
);
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
// src/context-switcher.tsx
|
|
1121
|
+
var import_jsx_runtime6 = require("react/jsx-runtime");
|
|
1122
|
+
function groupByOrg(items) {
|
|
1123
|
+
return groupItemsByOrg(items, {
|
|
1124
|
+
orgId: (it) => it.orgId,
|
|
1125
|
+
orgName: (it) => it.orgName,
|
|
1126
|
+
name: (it) => it.name
|
|
1127
|
+
});
|
|
1128
|
+
}
|
|
1129
|
+
function toBrand(item) {
|
|
1130
|
+
return {
|
|
1131
|
+
name: item.name,
|
|
1132
|
+
favicon: item.favicon,
|
|
1133
|
+
logo: item.logo,
|
|
1134
|
+
color: item.color,
|
|
1135
|
+
accentColor: item.accentColor
|
|
1136
|
+
};
|
|
1137
|
+
}
|
|
1138
|
+
function ContextSwitcher({
|
|
1139
|
+
items,
|
|
1140
|
+
activeRefId,
|
|
1141
|
+
onSelect,
|
|
1142
|
+
footer,
|
|
1143
|
+
placeholder = "Select",
|
|
1144
|
+
collapsed = false,
|
|
1145
|
+
side = "bottom",
|
|
1146
|
+
align = "start",
|
|
1147
|
+
contentClassName
|
|
1148
|
+
}) {
|
|
1149
|
+
const active = items.find((it) => it.refId === activeRefId) ?? null;
|
|
1150
|
+
const groups = groupByOrg(items);
|
|
1151
|
+
const showOrgHeaders = groups.length > 1;
|
|
1152
|
+
return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(DropdownMenu, { children: [
|
|
1153
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)(DropdownMenuTrigger, { asChild: true, children: collapsed ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
|
|
1154
|
+
"button",
|
|
1155
|
+
{
|
|
1156
|
+
type: "button",
|
|
1157
|
+
title: active?.name || placeholder,
|
|
1158
|
+
"aria-label": active?.name || placeholder,
|
|
1159
|
+
className: "flex h-10 w-10 items-center justify-center rounded-lg text-zinc-700 transition-colors hover:bg-zinc-900/5 dark:text-zinc-300 dark:hover:bg-white/5",
|
|
1160
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(BrandGlyph, { brand: toBrand(active ?? { name: "" }), size: "lg" })
|
|
1161
|
+
}
|
|
1162
|
+
) : /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
|
|
1163
|
+
"button",
|
|
1164
|
+
{
|
|
1165
|
+
type: "button",
|
|
1166
|
+
className: "group flex items-center gap-2 rounded-md border border-border bg-card/60 px-2.5 py-1.5 text-sm font-medium transition-colors hover:bg-muted",
|
|
1167
|
+
children: [
|
|
1168
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)(BrandGlyph, { brand: toBrand(active ?? { name: "" }) }),
|
|
1169
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "max-w-[140px] truncate text-foreground/90", children: active?.name || placeholder }),
|
|
1170
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_lucide_react3.ChevronDown, { className: "h-3.5 w-3.5 text-muted-foreground transition-transform group-data-[state=open]:rotate-180" })
|
|
1171
|
+
]
|
|
1172
|
+
}
|
|
1173
|
+
) }),
|
|
1174
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
|
|
1175
|
+
DropdownMenuContent,
|
|
1176
|
+
{
|
|
1177
|
+
side,
|
|
1178
|
+
align,
|
|
1179
|
+
sideOffset: 6,
|
|
1180
|
+
collisionPadding: 8,
|
|
1181
|
+
className: cn("w-60 overflow-hidden p-0", contentClassName),
|
|
1182
|
+
children: [
|
|
1183
|
+
groups.map((group) => /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { children: [
|
|
1184
|
+
showOrgHeaders && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(DropdownMenuLabel, { className: "px-3 pb-1 pt-2 text-[11px] font-semibold uppercase tracking-wide text-muted-foreground", children: group.orgName ?? "Other" }),
|
|
1185
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: cn(showOrgHeaders && "ml-5 border-l border-border/60"), children: group.items.map((item) => {
|
|
1186
|
+
const isActive = item.refId === activeRefId;
|
|
1187
|
+
return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
|
|
1188
|
+
DropdownMenuItem,
|
|
1189
|
+
{
|
|
1190
|
+
"data-active": isActive || void 0,
|
|
1191
|
+
onSelect: () => onSelect(item),
|
|
1192
|
+
className: "flex w-full items-center gap-2.5 rounded-none px-3 py-2 text-sm",
|
|
1193
|
+
children: [
|
|
1194
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)(BrandGlyph, { brand: toBrand(item), subtle: true }),
|
|
1195
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "flex-1 truncate text-left text-foreground/90", children: item.name }),
|
|
1196
|
+
isActive && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_lucide_react3.Check, { className: "size-3.5 text-emerald-500" })
|
|
1197
|
+
]
|
|
1198
|
+
},
|
|
1199
|
+
item.refId
|
|
1200
|
+
);
|
|
1201
|
+
}) })
|
|
1202
|
+
] }, group.orgId ?? "unattributed")),
|
|
1203
|
+
footer && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "sticky bottom-0 border-t border-border bg-popover", children: footer })
|
|
1204
|
+
]
|
|
1205
|
+
}
|
|
1206
|
+
)
|
|
1207
|
+
] });
|
|
1208
|
+
}
|
|
943
1209
|
// Annotate the CommonJS export names for ESM import in node:
|
|
944
1210
|
0 && (module.exports = {
|
|
945
1211
|
AppSidebar,
|
|
946
1212
|
BrandGlyph,
|
|
1213
|
+
ContextSwitcher,
|
|
947
1214
|
Flyout,
|
|
1215
|
+
REEVE_ACCENT,
|
|
948
1216
|
ResponsiveHeader,
|
|
949
1217
|
deriveFallbackLadder,
|
|
950
1218
|
faviconUrl,
|
|
1219
|
+
groupByOrg,
|
|
951
1220
|
reconcileLadder,
|
|
952
1221
|
resolveLadder,
|
|
953
1222
|
useLadderRung,
|
|
1223
|
+
useResponsivePlaceholder,
|
|
954
1224
|
zSidebarLadder
|
|
955
1225
|
});
|