@meetreeve/ui 0.2.0 → 0.5.2
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 +182 -6
- package/dist/index.d.ts +182 -6
- package/dist/index.js +403 -110
- package/dist/index.mjs +398 -113
- 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 +6 -1
package/dist/index.js
CHANGED
|
@@ -33,19 +33,26 @@ var src_exports = {};
|
|
|
33
33
|
__export(src_exports, {
|
|
34
34
|
AppSidebar: () => AppSidebar,
|
|
35
35
|
BrandGlyph: () => BrandGlyph,
|
|
36
|
+
ContextSwitcher: () => ContextSwitcher,
|
|
36
37
|
Flyout: () => Flyout,
|
|
38
|
+
GLYPH_CANVAS_FILL: () => GLYPH_CANVAS_FILL,
|
|
39
|
+
LOCKUP_GAP: () => LOCKUP_GAP,
|
|
40
|
+
LOCKUP_ROW: () => LOCKUP_ROW,
|
|
41
|
+
MARK_CAP_RATIO: () => MARK_CAP_RATIO,
|
|
42
|
+
REEVE_ACCENT: () => REEVE_ACCENT,
|
|
37
43
|
ResponsiveHeader: () => ResponsiveHeader,
|
|
38
44
|
deriveFallbackLadder: () => deriveFallbackLadder,
|
|
39
45
|
faviconUrl: () => faviconUrl,
|
|
46
|
+
groupByOrg: () => groupByOrg,
|
|
40
47
|
reconcileLadder: () => reconcileLadder,
|
|
41
48
|
resolveLadder: () => resolveLadder,
|
|
42
49
|
useLadderRung: () => useLadderRung,
|
|
50
|
+
useResponsivePlaceholder: () => useResponsivePlaceholder,
|
|
43
51
|
zSidebarLadder: () => zSidebarLadder
|
|
44
52
|
});
|
|
45
53
|
module.exports = __toCommonJS(src_exports);
|
|
46
54
|
|
|
47
55
|
// src/brand-glyph.tsx
|
|
48
|
-
var import_image = __toESM(require("next/image"));
|
|
49
56
|
var import_react = require("react");
|
|
50
57
|
|
|
51
58
|
// src/lib/cn.ts
|
|
@@ -68,12 +75,18 @@ function cn(...inputs) {
|
|
|
68
75
|
}
|
|
69
76
|
|
|
70
77
|
// src/lib/initials.ts
|
|
71
|
-
function
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
return parts.map((p) => p[0]?.toUpperCase() ?? "").join("") || "\xB7\xB7";
|
|
78
|
+
function firstInitial(name) {
|
|
79
|
+
const trimmed = (name ?? "").trim();
|
|
80
|
+
return trimmed ? trimmed[0].toUpperCase() : "?";
|
|
75
81
|
}
|
|
76
82
|
|
|
83
|
+
// src/lib/lockup.ts
|
|
84
|
+
var MARK_CAP_RATIO = 1.08;
|
|
85
|
+
var MONOGRAM_TEXT_LG = "text-[15px]";
|
|
86
|
+
var LOCKUP_GAP = "gap-2";
|
|
87
|
+
var LOCKUP_ROW = "flex items-baseline";
|
|
88
|
+
var GLYPH_CANVAS_FILL = 0.65;
|
|
89
|
+
|
|
77
90
|
// src/lib/monogram.ts
|
|
78
91
|
var MONOGRAM_COLORS = [
|
|
79
92
|
"#7c3aed",
|
|
@@ -101,6 +114,7 @@ function monogramColor(seed) {
|
|
|
101
114
|
|
|
102
115
|
// src/brand-glyph.tsx
|
|
103
116
|
var import_jsx_runtime = require("react/jsx-runtime");
|
|
117
|
+
var REEVE_ACCENT = "#FF6B2B";
|
|
104
118
|
function faviconUrl(domain) {
|
|
105
119
|
if (!domain) return null;
|
|
106
120
|
const host = domain.trim().toLowerCase();
|
|
@@ -121,21 +135,26 @@ function BrandGlyph({ brand, size = "sm", subtle = false, className }) {
|
|
|
121
135
|
"span",
|
|
122
136
|
{
|
|
123
137
|
className: cn(
|
|
124
|
-
|
|
138
|
+
// `self-center` opts image glyphs out of the lockup's baseline
|
|
139
|
+
// alignment (DEV-5139): an image box has no text baseline, so
|
|
140
|
+
// flexbox would seat its BOTTOM edge on the wordmark baseline and
|
|
141
|
+
// shove the icon upward. Centering preserves the pre-DEV-5139 look.
|
|
142
|
+
"flex shrink-0 items-center justify-center self-center overflow-hidden rounded",
|
|
125
143
|
// Tint needs a dark-mode counterpart or it vanishes on dark backgrounds.
|
|
126
144
|
subtle ? "bg-black/5 dark:bg-white/10" : "bg-black/10 dark:bg-white/15",
|
|
127
145
|
box,
|
|
128
146
|
className
|
|
129
147
|
),
|
|
130
148
|
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
131
|
-
|
|
149
|
+
"img",
|
|
132
150
|
{
|
|
133
151
|
src,
|
|
134
152
|
alt: "",
|
|
135
153
|
width: dim,
|
|
136
154
|
height: dim,
|
|
155
|
+
loading: "lazy",
|
|
156
|
+
decoding: "async",
|
|
137
157
|
"data-testid": "brand-image",
|
|
138
|
-
unoptimized: true,
|
|
139
158
|
onError: () => setFailed((prev) => new Set(prev).add(src)),
|
|
140
159
|
className: cn(img, "rounded-sm object-contain")
|
|
141
160
|
}
|
|
@@ -143,19 +162,28 @@ function BrandGlyph({ brand, size = "sm", subtle = false, className }) {
|
|
|
143
162
|
}
|
|
144
163
|
);
|
|
145
164
|
}
|
|
146
|
-
|
|
165
|
+
const letter = firstInitial(brand.name);
|
|
166
|
+
const hasRealLetter = letter !== "?";
|
|
167
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
147
168
|
"span",
|
|
148
169
|
{
|
|
149
170
|
"aria-hidden": true,
|
|
150
171
|
"data-testid": "brand-monogram",
|
|
151
172
|
className: cn(
|
|
152
|
-
"flex shrink-0 select-none items-
|
|
153
|
-
|
|
173
|
+
"flex shrink-0 select-none items-baseline justify-center font-semibold uppercase",
|
|
174
|
+
// DEV-5139 rule 2: at "lg" the letter runs 15px = the text-sm
|
|
175
|
+
// wordmark (14px) × MARK_CAP_RATIO (1.08) — a lone round glyph at
|
|
176
|
+
// exact parity optically reads small. "sm" has no wordmark partner
|
|
177
|
+
// in its surfaces; unchanged pending a lockup audit there.
|
|
178
|
+
size === "lg" ? MONOGRAM_TEXT_LG : "text-[11px]",
|
|
154
179
|
box,
|
|
155
180
|
className
|
|
156
181
|
),
|
|
157
|
-
style: {
|
|
158
|
-
children:
|
|
182
|
+
style: { color: brand.color || monogramColor(brand.name || "?") },
|
|
183
|
+
children: [
|
|
184
|
+
letter,
|
|
185
|
+
hasRealLetter && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { "data-testid": "brand-monogram-dot", style: { color: brand.accentColor || REEVE_ACCENT }, children: "." })
|
|
186
|
+
]
|
|
159
187
|
}
|
|
160
188
|
);
|
|
161
189
|
}
|
|
@@ -164,23 +192,21 @@ function BrandGlyph({ brand, size = "sm", subtle = false, className }) {
|
|
|
164
192
|
var import_react3 = require("react");
|
|
165
193
|
var import_lucide_react = require("lucide-react");
|
|
166
194
|
|
|
167
|
-
// src/lib/use-
|
|
195
|
+
// src/lib/use-container-width-at-least.ts
|
|
168
196
|
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);
|
|
197
|
+
function useContainerWidthAtLeast(ref, px) {
|
|
198
|
+
const [atLeast, setAtLeast] = (0, import_react2.useState)(false);
|
|
199
|
+
(0, import_react2.useEffect)(() => {
|
|
200
|
+
const el = ref.current;
|
|
201
|
+
if (!el || typeof ResizeObserver === "undefined") return;
|
|
202
|
+
const ro = new ResizeObserver(([entry]) => {
|
|
203
|
+
if (!entry) return;
|
|
204
|
+
setAtLeast(entry.contentRect.width >= px);
|
|
205
|
+
});
|
|
206
|
+
ro.observe(el);
|
|
207
|
+
return () => ro.disconnect();
|
|
208
|
+
}, [ref, px]);
|
|
209
|
+
return atLeast;
|
|
184
210
|
}
|
|
185
211
|
|
|
186
212
|
// src/tokens.ts
|
|
@@ -200,6 +226,10 @@ var BREAKPOINTS = {
|
|
|
200
226
|
/** Tailwind `md` — the header switches to its full desktop layout at/above this. */
|
|
201
227
|
md: 768
|
|
202
228
|
};
|
|
229
|
+
var TOUCH_TARGET_STYLE = {
|
|
230
|
+
minWidth: MIN_SIZES.touchTarget,
|
|
231
|
+
minHeight: MIN_SIZES.touchTarget
|
|
232
|
+
};
|
|
203
233
|
|
|
204
234
|
// src/responsive-header.tsx
|
|
205
235
|
var import_jsx_runtime2 = require("react/jsx-runtime");
|
|
@@ -209,15 +239,17 @@ function MenuIcon({ open }) {
|
|
|
209
239
|
function SecondaryControls({
|
|
210
240
|
nav,
|
|
211
241
|
auth,
|
|
212
|
-
themeToggle
|
|
242
|
+
themeToggle,
|
|
243
|
+
linkComponent
|
|
213
244
|
}) {
|
|
245
|
+
const LinkEl = linkComponent ?? "a";
|
|
214
246
|
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
|
|
215
247
|
nav?.map(
|
|
216
248
|
(item) => (
|
|
217
249
|
// A link navigates; an action-only item (no href) is a button — never an
|
|
218
250
|
// <a href="#"> that scroll-jumps and pushes a history entry.
|
|
219
251
|
item.href ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
220
|
-
|
|
252
|
+
LinkEl,
|
|
221
253
|
{
|
|
222
254
|
href: item.href,
|
|
223
255
|
onClick: item.onSelect,
|
|
@@ -225,15 +257,22 @@ function SecondaryControls({
|
|
|
225
257
|
children: item.label
|
|
226
258
|
},
|
|
227
259
|
item.label
|
|
228
|
-
) :
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
260
|
+
) : (
|
|
261
|
+
// A real <button> secondary control — MIN_SIZES.control is the
|
|
262
|
+
// token's exact stated purpose ("Generic minimum width/height for
|
|
263
|
+
// a secondary control (button, select)"), previously defined but
|
|
264
|
+
// unused anywhere in this package (DEV-4249).
|
|
265
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
266
|
+
"button",
|
|
267
|
+
{
|
|
268
|
+
type: "button",
|
|
269
|
+
onClick: item.onSelect,
|
|
270
|
+
style: { minWidth: MIN_SIZES.control, minHeight: MIN_SIZES.control },
|
|
271
|
+
className: "flex items-center justify-start text-left text-sm font-medium text-foreground/80 hover:text-foreground",
|
|
272
|
+
children: item.label
|
|
273
|
+
},
|
|
274
|
+
item.label
|
|
275
|
+
)
|
|
237
276
|
)
|
|
238
277
|
)
|
|
239
278
|
),
|
|
@@ -245,19 +284,23 @@ function ResponsiveHeader({
|
|
|
245
284
|
brand,
|
|
246
285
|
search,
|
|
247
286
|
nav,
|
|
287
|
+
brandHref,
|
|
288
|
+
linkComponent,
|
|
248
289
|
auth,
|
|
249
290
|
themeToggle,
|
|
250
291
|
menuLabel = "Menu",
|
|
251
292
|
className
|
|
252
293
|
}) {
|
|
253
|
-
const
|
|
294
|
+
const LinkEl = linkComponent ?? "a";
|
|
254
295
|
const [open, setOpen] = (0, import_react3.useState)(false);
|
|
255
296
|
const panelId = (0, import_react3.useId)();
|
|
297
|
+
const headerRef = (0, import_react3.useRef)(null);
|
|
256
298
|
const buttonRef = (0, import_react3.useRef)(null);
|
|
257
299
|
const panelRef = (0, import_react3.useRef)(null);
|
|
300
|
+
const containerAtOrAboveMd = useContainerWidthAtLeast(headerRef, BREAKPOINTS.md);
|
|
258
301
|
(0, import_react3.useEffect)(() => {
|
|
259
|
-
if (
|
|
260
|
-
}, [
|
|
302
|
+
if (containerAtOrAboveMd) setOpen(false);
|
|
303
|
+
}, [containerAtOrAboveMd]);
|
|
261
304
|
(0, import_react3.useEffect)(() => {
|
|
262
305
|
if (!open) return;
|
|
263
306
|
const onKey = (e) => {
|
|
@@ -280,27 +323,41 @@ function ResponsiveHeader({
|
|
|
280
323
|
};
|
|
281
324
|
}, [open]);
|
|
282
325
|
const hasSecondary = Boolean(nav && nav.length > 0 || auth || themeToggle);
|
|
326
|
+
const brandLockup = /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { "data-testid": "header-lockup", className: cn(LOCKUP_ROW, LOCKUP_GAP), children: [
|
|
327
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(BrandGlyph, { brand, size: "lg" }),
|
|
328
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
329
|
+
"span",
|
|
330
|
+
{
|
|
331
|
+
"data-testid": "header-wordmark",
|
|
332
|
+
className: "hidden truncate text-sm font-semibold text-foreground @min-[768px]:block",
|
|
333
|
+
style: { maxWidth: MIN_SIZES.wordmarkMaxWidth },
|
|
334
|
+
children: brand.name
|
|
335
|
+
}
|
|
336
|
+
)
|
|
337
|
+
] });
|
|
283
338
|
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
|
|
284
339
|
"header",
|
|
285
340
|
{
|
|
341
|
+
ref: headerRef,
|
|
286
342
|
"data-testid": "responsive-header",
|
|
287
343
|
className: cn(
|
|
288
|
-
|
|
344
|
+
// `@container` makes this header itself the query context, so the
|
|
345
|
+
// variants below react to ITS width (e.g. a sidebar), not the viewport.
|
|
346
|
+
"@container flex w-full items-center gap-3 border-b border-border bg-background px-4 py-2",
|
|
289
347
|
className
|
|
290
348
|
),
|
|
291
349
|
children: [
|
|
292
|
-
/* @__PURE__ */ (0, import_jsx_runtime2.
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
] }),
|
|
350
|
+
brandHref ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
351
|
+
LinkEl,
|
|
352
|
+
{
|
|
353
|
+
href: brandHref,
|
|
354
|
+
"aria-label": brand.name || "Home",
|
|
355
|
+
"data-testid": "header-brand",
|
|
356
|
+
style: TOUCH_TARGET_STYLE,
|
|
357
|
+
className: "flex shrink-0 items-center",
|
|
358
|
+
children: brandLockup
|
|
359
|
+
}
|
|
360
|
+
) : /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { "data-testid": "header-brand", className: "flex shrink-0 items-center", children: brandLockup }),
|
|
304
361
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
305
362
|
"div",
|
|
306
363
|
{
|
|
@@ -310,40 +367,91 @@ function ResponsiveHeader({
|
|
|
310
367
|
children: search
|
|
311
368
|
}
|
|
312
369
|
),
|
|
313
|
-
hasSecondary &&
|
|
370
|
+
hasSecondary && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
|
|
314
371
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
315
|
-
"
|
|
372
|
+
"nav",
|
|
316
373
|
{
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
374
|
+
"data-testid": "header-actions",
|
|
375
|
+
className: "hidden shrink-0 items-center gap-3 @min-[768px]:flex",
|
|
376
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
377
|
+
SecondaryControls,
|
|
378
|
+
{
|
|
379
|
+
nav,
|
|
380
|
+
auth,
|
|
381
|
+
themeToggle,
|
|
382
|
+
linkComponent
|
|
383
|
+
}
|
|
384
|
+
)
|
|
327
385
|
}
|
|
328
386
|
),
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
387
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "relative shrink-0 @min-[768px]:hidden", children: [
|
|
388
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
389
|
+
"button",
|
|
390
|
+
{
|
|
391
|
+
ref: buttonRef,
|
|
392
|
+
type: "button",
|
|
393
|
+
"data-testid": "header-menu-button",
|
|
394
|
+
"aria-label": menuLabel,
|
|
395
|
+
"aria-expanded": open,
|
|
396
|
+
"aria-controls": panelId,
|
|
397
|
+
onClick: () => setOpen((v) => !v),
|
|
398
|
+
className: "flex items-center justify-center rounded-md border border-border text-foreground",
|
|
399
|
+
style: TOUCH_TARGET_STYLE,
|
|
400
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(MenuIcon, { open })
|
|
401
|
+
}
|
|
402
|
+
),
|
|
403
|
+
open && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
404
|
+
"div",
|
|
405
|
+
{
|
|
406
|
+
ref: panelRef,
|
|
407
|
+
id: panelId,
|
|
408
|
+
"data-testid": "header-menu-panel",
|
|
409
|
+
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",
|
|
410
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
411
|
+
SecondaryControls,
|
|
412
|
+
{
|
|
413
|
+
nav,
|
|
414
|
+
auth,
|
|
415
|
+
themeToggle,
|
|
416
|
+
linkComponent
|
|
417
|
+
}
|
|
418
|
+
)
|
|
419
|
+
}
|
|
420
|
+
)
|
|
421
|
+
] })
|
|
422
|
+
] })
|
|
340
423
|
]
|
|
341
424
|
}
|
|
342
425
|
);
|
|
343
426
|
}
|
|
344
427
|
|
|
428
|
+
// src/lib/use-media-query.ts
|
|
429
|
+
var import_react4 = require("react");
|
|
430
|
+
function useMediaQuery(query) {
|
|
431
|
+
const subscribe = (0, import_react4.useCallback)(
|
|
432
|
+
(onChange) => {
|
|
433
|
+
if (typeof window === "undefined" || typeof window.matchMedia !== "function") {
|
|
434
|
+
return () => {
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
const mql = window.matchMedia(query);
|
|
438
|
+
mql.addEventListener("change", onChange);
|
|
439
|
+
return () => mql.removeEventListener("change", onChange);
|
|
440
|
+
},
|
|
441
|
+
[query]
|
|
442
|
+
);
|
|
443
|
+
const getSnapshot = () => typeof window !== "undefined" && typeof window.matchMedia === "function" ? window.matchMedia(query).matches : false;
|
|
444
|
+
return (0, import_react4.useSyncExternalStore)(subscribe, getSnapshot, () => false);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// src/use-responsive-placeholder.ts
|
|
448
|
+
function useResponsivePlaceholder(desktop, mobile) {
|
|
449
|
+
const isDesktop = useMediaQuery(`(min-width: ${BREAKPOINTS.md}px)`);
|
|
450
|
+
return isDesktop ? desktop : mobile ?? desktop;
|
|
451
|
+
}
|
|
452
|
+
|
|
345
453
|
// src/app-sidebar.tsx
|
|
346
|
-
var
|
|
454
|
+
var import_react8 = require("react");
|
|
347
455
|
var import_lucide_react2 = require("lucide-react");
|
|
348
456
|
|
|
349
457
|
// src/lib/resolve-icon.ts
|
|
@@ -364,7 +472,7 @@ function resolveIcon(name) {
|
|
|
364
472
|
}
|
|
365
473
|
|
|
366
474
|
// src/lib/use-dismissable-disclosure.ts
|
|
367
|
-
var
|
|
475
|
+
var import_react5 = require("react");
|
|
368
476
|
function useDismissableDisclosure({
|
|
369
477
|
open,
|
|
370
478
|
onClose,
|
|
@@ -372,10 +480,10 @@ function useDismissableDisclosure({
|
|
|
372
480
|
triggerRef,
|
|
373
481
|
initialFocusRef
|
|
374
482
|
}) {
|
|
375
|
-
(0,
|
|
483
|
+
(0, import_react5.useEffect)(() => {
|
|
376
484
|
if (open) initialFocusRef?.current?.focus();
|
|
377
485
|
}, [open]);
|
|
378
|
-
(0,
|
|
486
|
+
(0, import_react5.useEffect)(() => {
|
|
379
487
|
if (!open) return;
|
|
380
488
|
const onKey = (e) => {
|
|
381
489
|
if (e.key === "Escape") {
|
|
@@ -578,16 +686,16 @@ function resolveLadder(raw, registry, groupOrder) {
|
|
|
578
686
|
}
|
|
579
687
|
|
|
580
688
|
// src/ladder/use-ladder-rung.ts
|
|
581
|
-
var
|
|
689
|
+
var import_react6 = require("react");
|
|
582
690
|
var DEFAULT_ROW_HEIGHT = 36;
|
|
583
691
|
var DEFAULT_HYSTERESIS_ROWS = 1;
|
|
584
692
|
function useLadderRung(ladder, containerRef, options = {}) {
|
|
585
693
|
const rowHeight = options.rowHeight ?? DEFAULT_ROW_HEIGHT;
|
|
586
694
|
const hysteresisRows = options.hysteresisRows ?? DEFAULT_HYSTERESIS_ROWS;
|
|
587
|
-
const [rungIndex, setRungIndex] = (0,
|
|
588
|
-
const rungIndexRef = (0,
|
|
695
|
+
const [rungIndex, setRungIndex] = (0, import_react6.useState)(0);
|
|
696
|
+
const rungIndexRef = (0, import_react6.useRef)(0);
|
|
589
697
|
const ladderSignature = `${ladder.version}:${ladder.rungs.map((r) => r.rows).join(",")}`;
|
|
590
|
-
(0,
|
|
698
|
+
(0, import_react6.useLayoutEffect)(() => {
|
|
591
699
|
rungIndexRef.current = 0;
|
|
592
700
|
setRungIndex(0);
|
|
593
701
|
const node = containerRef.current;
|
|
@@ -623,28 +731,28 @@ function useLadderRung(ladder, containerRef, options = {}) {
|
|
|
623
731
|
}
|
|
624
732
|
|
|
625
733
|
// src/ladder/flyout.tsx
|
|
626
|
-
var
|
|
734
|
+
var import_react7 = require("react");
|
|
627
735
|
var import_jsx_runtime3 = require("react/jsx-runtime");
|
|
628
736
|
var PANEL_GAP = 4;
|
|
629
737
|
function Flyout({ label, icon, active, children, className }) {
|
|
630
|
-
const [hoverOpen, setHoverOpen] = (0,
|
|
631
|
-
const [clickOpen, setClickOpen] = (0,
|
|
632
|
-
const [panelPosition, setPanelPosition] = (0,
|
|
738
|
+
const [hoverOpen, setHoverOpen] = (0, import_react7.useState)(false);
|
|
739
|
+
const [clickOpen, setClickOpen] = (0, import_react7.useState)(false);
|
|
740
|
+
const [panelPosition, setPanelPosition] = (0, import_react7.useState)(null);
|
|
633
741
|
const open = hoverOpen || clickOpen;
|
|
634
|
-
const panelId = (0,
|
|
635
|
-
const triggerRef = (0,
|
|
636
|
-
const panelRef = (0,
|
|
637
|
-
const firstItemRef = (0,
|
|
742
|
+
const panelId = (0, import_react7.useId)();
|
|
743
|
+
const triggerRef = (0, import_react7.useRef)(null);
|
|
744
|
+
const panelRef = (0, import_react7.useRef)(null);
|
|
745
|
+
const firstItemRef = (0, import_react7.useRef)(null);
|
|
638
746
|
const prefersReducedMotion = useMediaQuery("(prefers-reduced-motion: reduce)");
|
|
639
747
|
const Icon = resolveIcon(icon);
|
|
640
|
-
const reposition = (0,
|
|
748
|
+
const reposition = (0, import_react7.useCallback)(() => {
|
|
641
749
|
const rect = triggerRef.current?.getBoundingClientRect();
|
|
642
750
|
if (rect) setPanelPosition({ top: rect.top, left: rect.right + PANEL_GAP });
|
|
643
751
|
}, []);
|
|
644
|
-
(0,
|
|
752
|
+
(0, import_react7.useLayoutEffect)(() => {
|
|
645
753
|
if (open) reposition();
|
|
646
754
|
}, [open, reposition]);
|
|
647
|
-
(0,
|
|
755
|
+
(0, import_react7.useEffect)(() => {
|
|
648
756
|
if (!open) return;
|
|
649
757
|
window.addEventListener("resize", reposition);
|
|
650
758
|
window.addEventListener("scroll", reposition, true);
|
|
@@ -662,7 +770,7 @@ function Flyout({ label, icon, active, children, className }) {
|
|
|
662
770
|
panelRef,
|
|
663
771
|
triggerRef
|
|
664
772
|
});
|
|
665
|
-
(0,
|
|
773
|
+
(0, import_react7.useEffect)(() => {
|
|
666
774
|
if (clickOpen && panelPosition) firstItemRef.current?.focus();
|
|
667
775
|
}, [clickOpen, panelPosition]);
|
|
668
776
|
return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
|
|
@@ -716,8 +824,9 @@ function Flyout({ label, icon, active, children, className }) {
|
|
|
716
824
|
ref: i === 0 ? firstItemRef : void 0,
|
|
717
825
|
href: child.href,
|
|
718
826
|
"aria-current": child.active ? "page" : void 0,
|
|
827
|
+
style: TOUCH_TARGET_STYLE,
|
|
719
828
|
className: cn(
|
|
720
|
-
"truncate rounded-md px-3 py-1.5 text-sm transition-colors",
|
|
829
|
+
"flex items-center truncate rounded-md px-3 py-1.5 text-sm transition-colors",
|
|
721
830
|
child.active ? "bg-primary/10 text-foreground" : "text-muted-foreground hover:bg-foreground/5 hover:text-foreground"
|
|
722
831
|
),
|
|
723
832
|
children: child.label
|
|
@@ -780,24 +889,24 @@ function AppSidebar({
|
|
|
780
889
|
className
|
|
781
890
|
}) {
|
|
782
891
|
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,
|
|
892
|
+
const [mobileOpen, setMobileOpen] = (0, import_react8.useState)(false);
|
|
893
|
+
const containerRef = (0, import_react8.useRef)(null);
|
|
894
|
+
const drawerId = (0, import_react8.useId)();
|
|
895
|
+
const mobileTriggerRef = (0, import_react8.useRef)(null);
|
|
896
|
+
const mobileDrawerRef = (0, import_react8.useRef)(null);
|
|
897
|
+
const mobileCloseRef = (0, import_react8.useRef)(null);
|
|
898
|
+
const moduleMap = (0, import_react8.useMemo)(() => new Map(modules.map((m) => [m.moduleId, m])), [modules]);
|
|
899
|
+
const registryForLadder = (0, import_react8.useMemo)(
|
|
791
900
|
() => modules.map((m) => ({ moduleId: m.moduleId, group: m.group })),
|
|
792
901
|
[modules]
|
|
793
902
|
);
|
|
794
|
-
const resolvedLadder = (0,
|
|
903
|
+
const resolvedLadder = (0, import_react8.useMemo)(
|
|
795
904
|
() => resolveLadder(ladder, registryForLadder, groupOrder),
|
|
796
905
|
[ladder, registryForLadder, groupOrder]
|
|
797
906
|
);
|
|
798
907
|
const rung = useLadderRung(resolvedLadder, containerRef, ladderOptions);
|
|
799
908
|
const rung0 = resolvedLadder.rungs[0];
|
|
800
|
-
(0,
|
|
909
|
+
(0, import_react8.useEffect)(() => {
|
|
801
910
|
if (isDesktop) setMobileOpen(false);
|
|
802
911
|
}, [isDesktop]);
|
|
803
912
|
useDismissableDisclosure({
|
|
@@ -872,6 +981,7 @@ function AppSidebar({
|
|
|
872
981
|
"aria-expanded": mobileOpen,
|
|
873
982
|
"aria-controls": drawerId,
|
|
874
983
|
onClick: () => setMobileOpen((v) => !v),
|
|
984
|
+
style: TOUCH_TARGET_STYLE,
|
|
875
985
|
className: "flex items-center justify-center rounded-md border border-border p-2 text-foreground",
|
|
876
986
|
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
987
|
}
|
|
@@ -905,6 +1015,7 @@ function AppSidebar({
|
|
|
905
1015
|
"data-testid": "app-sidebar-mobile-close",
|
|
906
1016
|
"aria-label": `Close ${menuLabel.toLowerCase()}`,
|
|
907
1017
|
onClick: () => setMobileOpen(false),
|
|
1018
|
+
style: TOUCH_TARGET_STYLE,
|
|
908
1019
|
className: "flex items-center justify-center rounded-md p-2 text-foreground hover:bg-foreground/5",
|
|
909
1020
|
children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_lucide_react2.X, { className: "h-5 w-5", "aria-hidden": "true" })
|
|
910
1021
|
}
|
|
@@ -940,16 +1051,198 @@ function AppSidebar({
|
|
|
940
1051
|
)
|
|
941
1052
|
] });
|
|
942
1053
|
}
|
|
1054
|
+
|
|
1055
|
+
// src/context-switcher.tsx
|
|
1056
|
+
var import_lucide_react3 = require("lucide-react");
|
|
1057
|
+
|
|
1058
|
+
// src/lib/group-by-org.ts
|
|
1059
|
+
function groupItemsByOrg(rows, get) {
|
|
1060
|
+
const byOrg = /* @__PURE__ */ new Map();
|
|
1061
|
+
for (const row of rows) {
|
|
1062
|
+
const key = get.orgId(row) ?? "";
|
|
1063
|
+
let group = byOrg.get(key);
|
|
1064
|
+
if (!group) {
|
|
1065
|
+
group = {
|
|
1066
|
+
orgId: get.orgId(row) ?? null,
|
|
1067
|
+
orgName: get.orgName(row) ?? null,
|
|
1068
|
+
items: []
|
|
1069
|
+
};
|
|
1070
|
+
byOrg.set(key, group);
|
|
1071
|
+
}
|
|
1072
|
+
group.items.push(row);
|
|
1073
|
+
}
|
|
1074
|
+
const groups = [...byOrg.values()];
|
|
1075
|
+
for (const g of groups) {
|
|
1076
|
+
g.items.sort((a, b) => get.name(a).localeCompare(get.name(b)));
|
|
1077
|
+
}
|
|
1078
|
+
groups.sort((a, b) => (a.orgName ?? "").localeCompare(b.orgName ?? ""));
|
|
1079
|
+
return groups;
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
// src/lib/dropdown-menu.tsx
|
|
1083
|
+
var DropdownMenuPrimitive = __toESM(require("@radix-ui/react-dropdown-menu"));
|
|
1084
|
+
var import_jsx_runtime5 = require("react/jsx-runtime");
|
|
1085
|
+
function DropdownMenu(props) {
|
|
1086
|
+
return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(DropdownMenuPrimitive.Root, { "data-slot": "dropdown-menu", ...props });
|
|
1087
|
+
}
|
|
1088
|
+
function DropdownMenuTrigger(props) {
|
|
1089
|
+
return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(DropdownMenuPrimitive.Trigger, { "data-slot": "dropdown-menu-trigger", ...props });
|
|
1090
|
+
}
|
|
1091
|
+
function DropdownMenuContent({
|
|
1092
|
+
className,
|
|
1093
|
+
sideOffset = 4,
|
|
1094
|
+
...props
|
|
1095
|
+
}) {
|
|
1096
|
+
return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(DropdownMenuPrimitive.Portal, { children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
|
|
1097
|
+
DropdownMenuPrimitive.Content,
|
|
1098
|
+
{
|
|
1099
|
+
"data-slot": "dropdown-menu-content",
|
|
1100
|
+
sideOffset,
|
|
1101
|
+
className: cn(
|
|
1102
|
+
"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",
|
|
1103
|
+
className
|
|
1104
|
+
),
|
|
1105
|
+
...props
|
|
1106
|
+
}
|
|
1107
|
+
) });
|
|
1108
|
+
}
|
|
1109
|
+
function DropdownMenuItem({
|
|
1110
|
+
className,
|
|
1111
|
+
...props
|
|
1112
|
+
}) {
|
|
1113
|
+
return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
|
|
1114
|
+
DropdownMenuPrimitive.Item,
|
|
1115
|
+
{
|
|
1116
|
+
"data-slot": "dropdown-menu-item",
|
|
1117
|
+
className: cn(
|
|
1118
|
+
"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",
|
|
1119
|
+
className
|
|
1120
|
+
),
|
|
1121
|
+
...props
|
|
1122
|
+
}
|
|
1123
|
+
);
|
|
1124
|
+
}
|
|
1125
|
+
function DropdownMenuLabel({
|
|
1126
|
+
className,
|
|
1127
|
+
...props
|
|
1128
|
+
}) {
|
|
1129
|
+
return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
|
|
1130
|
+
DropdownMenuPrimitive.Label,
|
|
1131
|
+
{
|
|
1132
|
+
"data-slot": "dropdown-menu-label",
|
|
1133
|
+
className: cn("px-2 py-1.5 text-sm font-medium", className),
|
|
1134
|
+
...props
|
|
1135
|
+
}
|
|
1136
|
+
);
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
// src/context-switcher.tsx
|
|
1140
|
+
var import_jsx_runtime6 = require("react/jsx-runtime");
|
|
1141
|
+
function groupByOrg(items) {
|
|
1142
|
+
return groupItemsByOrg(items, {
|
|
1143
|
+
orgId: (it) => it.orgId,
|
|
1144
|
+
orgName: (it) => it.orgName,
|
|
1145
|
+
name: (it) => it.name
|
|
1146
|
+
});
|
|
1147
|
+
}
|
|
1148
|
+
function toBrand(item) {
|
|
1149
|
+
return {
|
|
1150
|
+
name: item.name,
|
|
1151
|
+
favicon: item.favicon,
|
|
1152
|
+
logo: item.logo,
|
|
1153
|
+
color: item.color,
|
|
1154
|
+
accentColor: item.accentColor
|
|
1155
|
+
};
|
|
1156
|
+
}
|
|
1157
|
+
function ContextSwitcher({
|
|
1158
|
+
items,
|
|
1159
|
+
activeRefId,
|
|
1160
|
+
onSelect,
|
|
1161
|
+
footer,
|
|
1162
|
+
placeholder = "Select",
|
|
1163
|
+
collapsed = false,
|
|
1164
|
+
side = "bottom",
|
|
1165
|
+
align = "start",
|
|
1166
|
+
contentClassName
|
|
1167
|
+
}) {
|
|
1168
|
+
const active = items.find((it) => it.refId === activeRefId) ?? null;
|
|
1169
|
+
const groups = groupByOrg(items);
|
|
1170
|
+
const showOrgHeaders = groups.length > 1;
|
|
1171
|
+
return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(DropdownMenu, { children: [
|
|
1172
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)(DropdownMenuTrigger, { asChild: true, children: collapsed ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
|
|
1173
|
+
"button",
|
|
1174
|
+
{
|
|
1175
|
+
type: "button",
|
|
1176
|
+
title: active?.name || placeholder,
|
|
1177
|
+
"aria-label": active?.name || placeholder,
|
|
1178
|
+
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",
|
|
1179
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(BrandGlyph, { brand: toBrand(active ?? { name: "" }), size: "lg" })
|
|
1180
|
+
}
|
|
1181
|
+
) : /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
|
|
1182
|
+
"button",
|
|
1183
|
+
{
|
|
1184
|
+
type: "button",
|
|
1185
|
+
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",
|
|
1186
|
+
children: [
|
|
1187
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)(BrandGlyph, { brand: toBrand(active ?? { name: "" }) }),
|
|
1188
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "max-w-[140px] truncate text-foreground/90", children: active?.name || placeholder }),
|
|
1189
|
+
/* @__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" })
|
|
1190
|
+
]
|
|
1191
|
+
}
|
|
1192
|
+
) }),
|
|
1193
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
|
|
1194
|
+
DropdownMenuContent,
|
|
1195
|
+
{
|
|
1196
|
+
side,
|
|
1197
|
+
align,
|
|
1198
|
+
sideOffset: 6,
|
|
1199
|
+
collisionPadding: 8,
|
|
1200
|
+
className: cn("w-60 overflow-hidden p-0", contentClassName),
|
|
1201
|
+
children: [
|
|
1202
|
+
groups.map((group) => /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { children: [
|
|
1203
|
+
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" }),
|
|
1204
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: cn(showOrgHeaders && "ml-5 border-l border-border/60"), children: group.items.map((item) => {
|
|
1205
|
+
const isActive = item.refId === activeRefId;
|
|
1206
|
+
return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
|
|
1207
|
+
DropdownMenuItem,
|
|
1208
|
+
{
|
|
1209
|
+
"data-active": isActive || void 0,
|
|
1210
|
+
onSelect: () => onSelect(item),
|
|
1211
|
+
className: "flex w-full items-center gap-2.5 rounded-none px-3 py-2 text-sm",
|
|
1212
|
+
children: [
|
|
1213
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)(BrandGlyph, { brand: toBrand(item), subtle: true }),
|
|
1214
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "flex-1 truncate text-left text-foreground/90", children: item.name }),
|
|
1215
|
+
isActive && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_lucide_react3.Check, { className: "size-3.5 text-emerald-500" })
|
|
1216
|
+
]
|
|
1217
|
+
},
|
|
1218
|
+
item.refId
|
|
1219
|
+
);
|
|
1220
|
+
}) })
|
|
1221
|
+
] }, group.orgId ?? "unattributed")),
|
|
1222
|
+
footer && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "sticky bottom-0 border-t border-border bg-popover", children: footer })
|
|
1223
|
+
]
|
|
1224
|
+
}
|
|
1225
|
+
)
|
|
1226
|
+
] });
|
|
1227
|
+
}
|
|
943
1228
|
// Annotate the CommonJS export names for ESM import in node:
|
|
944
1229
|
0 && (module.exports = {
|
|
945
1230
|
AppSidebar,
|
|
946
1231
|
BrandGlyph,
|
|
1232
|
+
ContextSwitcher,
|
|
947
1233
|
Flyout,
|
|
1234
|
+
GLYPH_CANVAS_FILL,
|
|
1235
|
+
LOCKUP_GAP,
|
|
1236
|
+
LOCKUP_ROW,
|
|
1237
|
+
MARK_CAP_RATIO,
|
|
1238
|
+
REEVE_ACCENT,
|
|
948
1239
|
ResponsiveHeader,
|
|
949
1240
|
deriveFallbackLadder,
|
|
950
1241
|
faviconUrl,
|
|
1242
|
+
groupByOrg,
|
|
951
1243
|
reconcileLadder,
|
|
952
1244
|
resolveLadder,
|
|
953
1245
|
useLadderRung,
|
|
1246
|
+
useResponsivePlaceholder,
|
|
954
1247
|
zSidebarLadder
|
|
955
1248
|
});
|