@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.mjs
CHANGED
|
@@ -24,10 +24,9 @@ function cn(...inputs) {
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
// src/lib/initials.ts
|
|
27
|
-
function
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
return parts.map((p) => p[0]?.toUpperCase() ?? "").join("") || "\xB7\xB7";
|
|
27
|
+
function firstInitial(name) {
|
|
28
|
+
const trimmed = (name ?? "").trim();
|
|
29
|
+
return trimmed ? trimmed[0].toUpperCase() : "?";
|
|
31
30
|
}
|
|
32
31
|
|
|
33
32
|
// src/lib/monogram.ts
|
|
@@ -56,7 +55,8 @@ function monogramColor(seed) {
|
|
|
56
55
|
}
|
|
57
56
|
|
|
58
57
|
// src/brand-glyph.tsx
|
|
59
|
-
import { jsx } from "react/jsx-runtime";
|
|
58
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
59
|
+
var REEVE_ACCENT = "#FF6B2B";
|
|
60
60
|
function faviconUrl(domain) {
|
|
61
61
|
if (!domain) return null;
|
|
62
62
|
const host = domain.trim().toLowerCase();
|
|
@@ -99,44 +99,47 @@ function BrandGlyph({ brand, size = "sm", subtle = false, className }) {
|
|
|
99
99
|
}
|
|
100
100
|
);
|
|
101
101
|
}
|
|
102
|
-
|
|
102
|
+
const letter = firstInitial(brand.name);
|
|
103
|
+
const hasRealLetter = letter !== "?";
|
|
104
|
+
return /* @__PURE__ */ jsxs(
|
|
103
105
|
"span",
|
|
104
106
|
{
|
|
105
107
|
"aria-hidden": true,
|
|
106
108
|
"data-testid": "brand-monogram",
|
|
107
109
|
className: cn(
|
|
108
|
-
"flex shrink-0 select-none items-
|
|
109
|
-
size === "lg" ? "text-[
|
|
110
|
+
"flex shrink-0 select-none items-baseline justify-center font-semibold uppercase",
|
|
111
|
+
size === "lg" ? "text-[13px]" : "text-[11px]",
|
|
110
112
|
box,
|
|
111
113
|
className
|
|
112
114
|
),
|
|
113
|
-
style: {
|
|
114
|
-
children:
|
|
115
|
+
style: { color: brand.color || monogramColor(brand.name || "?") },
|
|
116
|
+
children: [
|
|
117
|
+
letter,
|
|
118
|
+
hasRealLetter && /* @__PURE__ */ jsx("span", { "data-testid": "brand-monogram-dot", style: { color: brand.accentColor || REEVE_ACCENT }, children: "." })
|
|
119
|
+
]
|
|
115
120
|
}
|
|
116
121
|
);
|
|
117
122
|
}
|
|
118
123
|
|
|
119
124
|
// src/responsive-header.tsx
|
|
120
|
-
import { useEffect, useId, useRef, useState as
|
|
125
|
+
import { useEffect as useEffect2, useId, useRef, useState as useState3 } from "react";
|
|
121
126
|
import { Menu, X } from "lucide-react";
|
|
122
127
|
|
|
123
|
-
// src/lib/use-
|
|
124
|
-
import {
|
|
125
|
-
function
|
|
126
|
-
const
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
const getSnapshot = () => typeof window !== "undefined" && typeof window.matchMedia === "function" ? window.matchMedia(query).matches : false;
|
|
139
|
-
return useSyncExternalStore(subscribe, getSnapshot, () => false);
|
|
128
|
+
// src/lib/use-container-width-at-least.ts
|
|
129
|
+
import { useEffect, useState as useState2 } from "react";
|
|
130
|
+
function useContainerWidthAtLeast(ref, px) {
|
|
131
|
+
const [atLeast, setAtLeast] = useState2(false);
|
|
132
|
+
useEffect(() => {
|
|
133
|
+
const el = ref.current;
|
|
134
|
+
if (!el || typeof ResizeObserver === "undefined") return;
|
|
135
|
+
const ro = new ResizeObserver(([entry]) => {
|
|
136
|
+
if (!entry) return;
|
|
137
|
+
setAtLeast(entry.contentRect.width >= px);
|
|
138
|
+
});
|
|
139
|
+
ro.observe(el);
|
|
140
|
+
return () => ro.disconnect();
|
|
141
|
+
}, [ref, px]);
|
|
142
|
+
return atLeast;
|
|
140
143
|
}
|
|
141
144
|
|
|
142
145
|
// src/tokens.ts
|
|
@@ -156,24 +159,30 @@ var BREAKPOINTS = {
|
|
|
156
159
|
/** Tailwind `md` — the header switches to its full desktop layout at/above this. */
|
|
157
160
|
md: 768
|
|
158
161
|
};
|
|
162
|
+
var TOUCH_TARGET_STYLE = {
|
|
163
|
+
minWidth: MIN_SIZES.touchTarget,
|
|
164
|
+
minHeight: MIN_SIZES.touchTarget
|
|
165
|
+
};
|
|
159
166
|
|
|
160
167
|
// src/responsive-header.tsx
|
|
161
|
-
import { Fragment, jsx as jsx2, jsxs } from "react/jsx-runtime";
|
|
168
|
+
import { Fragment, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
162
169
|
function MenuIcon({ open }) {
|
|
163
170
|
return open ? /* @__PURE__ */ jsx2(X, { className: "h-5 w-5", "aria-hidden": "true" }) : /* @__PURE__ */ jsx2(Menu, { className: "h-5 w-5", "aria-hidden": "true" });
|
|
164
171
|
}
|
|
165
172
|
function SecondaryControls({
|
|
166
173
|
nav,
|
|
167
174
|
auth,
|
|
168
|
-
themeToggle
|
|
175
|
+
themeToggle,
|
|
176
|
+
linkComponent
|
|
169
177
|
}) {
|
|
170
|
-
|
|
178
|
+
const LinkEl = linkComponent ?? "a";
|
|
179
|
+
return /* @__PURE__ */ jsxs2(Fragment, { children: [
|
|
171
180
|
nav?.map(
|
|
172
181
|
(item) => (
|
|
173
182
|
// A link navigates; an action-only item (no href) is a button — never an
|
|
174
183
|
// <a href="#"> that scroll-jumps and pushes a history entry.
|
|
175
184
|
item.href ? /* @__PURE__ */ jsx2(
|
|
176
|
-
|
|
185
|
+
LinkEl,
|
|
177
186
|
{
|
|
178
187
|
href: item.href,
|
|
179
188
|
onClick: item.onSelect,
|
|
@@ -181,15 +190,22 @@ function SecondaryControls({
|
|
|
181
190
|
children: item.label
|
|
182
191
|
},
|
|
183
192
|
item.label
|
|
184
|
-
) :
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
+
) : (
|
|
194
|
+
// A real <button> secondary control — MIN_SIZES.control is the
|
|
195
|
+
// token's exact stated purpose ("Generic minimum width/height for
|
|
196
|
+
// a secondary control (button, select)"), previously defined but
|
|
197
|
+
// unused anywhere in this package (DEV-4249).
|
|
198
|
+
/* @__PURE__ */ jsx2(
|
|
199
|
+
"button",
|
|
200
|
+
{
|
|
201
|
+
type: "button",
|
|
202
|
+
onClick: item.onSelect,
|
|
203
|
+
style: { minWidth: MIN_SIZES.control, minHeight: MIN_SIZES.control },
|
|
204
|
+
className: "flex items-center justify-start text-left text-sm font-medium text-foreground/80 hover:text-foreground",
|
|
205
|
+
children: item.label
|
|
206
|
+
},
|
|
207
|
+
item.label
|
|
208
|
+
)
|
|
193
209
|
)
|
|
194
210
|
)
|
|
195
211
|
),
|
|
@@ -201,20 +217,24 @@ function ResponsiveHeader({
|
|
|
201
217
|
brand,
|
|
202
218
|
search,
|
|
203
219
|
nav,
|
|
220
|
+
brandHref,
|
|
221
|
+
linkComponent,
|
|
204
222
|
auth,
|
|
205
223
|
themeToggle,
|
|
206
224
|
menuLabel = "Menu",
|
|
207
225
|
className
|
|
208
226
|
}) {
|
|
209
|
-
const
|
|
210
|
-
const [open, setOpen] =
|
|
227
|
+
const LinkEl = linkComponent ?? "a";
|
|
228
|
+
const [open, setOpen] = useState3(false);
|
|
211
229
|
const panelId = useId();
|
|
230
|
+
const headerRef = useRef(null);
|
|
212
231
|
const buttonRef = useRef(null);
|
|
213
232
|
const panelRef = useRef(null);
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
233
|
+
const containerAtOrAboveMd = useContainerWidthAtLeast(headerRef, BREAKPOINTS.md);
|
|
234
|
+
useEffect2(() => {
|
|
235
|
+
if (containerAtOrAboveMd) setOpen(false);
|
|
236
|
+
}, [containerAtOrAboveMd]);
|
|
237
|
+
useEffect2(() => {
|
|
218
238
|
if (!open) return;
|
|
219
239
|
const onKey = (e) => {
|
|
220
240
|
if (e.key === "Escape") {
|
|
@@ -236,27 +256,41 @@ function ResponsiveHeader({
|
|
|
236
256
|
};
|
|
237
257
|
}, [open]);
|
|
238
258
|
const hasSecondary = Boolean(nav && nav.length > 0 || auth || themeToggle);
|
|
239
|
-
|
|
259
|
+
const brandLockup = /* @__PURE__ */ jsxs2(Fragment, { children: [
|
|
260
|
+
/* @__PURE__ */ jsx2(BrandGlyph, { brand, size: "lg" }),
|
|
261
|
+
/* @__PURE__ */ jsx2(
|
|
262
|
+
"span",
|
|
263
|
+
{
|
|
264
|
+
"data-testid": "header-wordmark",
|
|
265
|
+
className: "hidden truncate text-sm font-semibold text-foreground @min-[768px]:block",
|
|
266
|
+
style: { maxWidth: MIN_SIZES.wordmarkMaxWidth },
|
|
267
|
+
children: brand.name
|
|
268
|
+
}
|
|
269
|
+
)
|
|
270
|
+
] });
|
|
271
|
+
return /* @__PURE__ */ jsxs2(
|
|
240
272
|
"header",
|
|
241
273
|
{
|
|
274
|
+
ref: headerRef,
|
|
242
275
|
"data-testid": "responsive-header",
|
|
243
276
|
className: cn(
|
|
244
|
-
|
|
277
|
+
// `@container` makes this header itself the query context, so the
|
|
278
|
+
// variants below react to ITS width (e.g. a sidebar), not the viewport.
|
|
279
|
+
"@container flex w-full items-center gap-3 border-b border-border bg-background px-4 py-2",
|
|
245
280
|
className
|
|
246
281
|
),
|
|
247
282
|
children: [
|
|
248
|
-
/* @__PURE__ */
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
] }),
|
|
283
|
+
brandHref ? /* @__PURE__ */ jsx2(
|
|
284
|
+
LinkEl,
|
|
285
|
+
{
|
|
286
|
+
href: brandHref,
|
|
287
|
+
"aria-label": brand.name || "Home",
|
|
288
|
+
"data-testid": "header-brand",
|
|
289
|
+
style: TOUCH_TARGET_STYLE,
|
|
290
|
+
className: "flex shrink-0 items-center gap-2",
|
|
291
|
+
children: brandLockup
|
|
292
|
+
}
|
|
293
|
+
) : /* @__PURE__ */ jsx2("div", { "data-testid": "header-brand", className: "flex shrink-0 items-center gap-2", children: brandLockup }),
|
|
260
294
|
/* @__PURE__ */ jsx2(
|
|
261
295
|
"div",
|
|
262
296
|
{
|
|
@@ -266,40 +300,91 @@ function ResponsiveHeader({
|
|
|
266
300
|
children: search
|
|
267
301
|
}
|
|
268
302
|
),
|
|
269
|
-
hasSecondary &&
|
|
303
|
+
hasSecondary && /* @__PURE__ */ jsxs2(Fragment, { children: [
|
|
270
304
|
/* @__PURE__ */ jsx2(
|
|
271
|
-
"
|
|
305
|
+
"nav",
|
|
272
306
|
{
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
307
|
+
"data-testid": "header-actions",
|
|
308
|
+
className: "hidden shrink-0 items-center gap-3 @min-[768px]:flex",
|
|
309
|
+
children: /* @__PURE__ */ jsx2(
|
|
310
|
+
SecondaryControls,
|
|
311
|
+
{
|
|
312
|
+
nav,
|
|
313
|
+
auth,
|
|
314
|
+
themeToggle,
|
|
315
|
+
linkComponent
|
|
316
|
+
}
|
|
317
|
+
)
|
|
283
318
|
}
|
|
284
319
|
),
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
320
|
+
/* @__PURE__ */ jsxs2("div", { className: "relative shrink-0 @min-[768px]:hidden", children: [
|
|
321
|
+
/* @__PURE__ */ jsx2(
|
|
322
|
+
"button",
|
|
323
|
+
{
|
|
324
|
+
ref: buttonRef,
|
|
325
|
+
type: "button",
|
|
326
|
+
"data-testid": "header-menu-button",
|
|
327
|
+
"aria-label": menuLabel,
|
|
328
|
+
"aria-expanded": open,
|
|
329
|
+
"aria-controls": panelId,
|
|
330
|
+
onClick: () => setOpen((v) => !v),
|
|
331
|
+
className: "flex items-center justify-center rounded-md border border-border text-foreground",
|
|
332
|
+
style: TOUCH_TARGET_STYLE,
|
|
333
|
+
children: /* @__PURE__ */ jsx2(MenuIcon, { open })
|
|
334
|
+
}
|
|
335
|
+
),
|
|
336
|
+
open && /* @__PURE__ */ jsx2(
|
|
337
|
+
"div",
|
|
338
|
+
{
|
|
339
|
+
ref: panelRef,
|
|
340
|
+
id: panelId,
|
|
341
|
+
"data-testid": "header-menu-panel",
|
|
342
|
+
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",
|
|
343
|
+
children: /* @__PURE__ */ jsx2(
|
|
344
|
+
SecondaryControls,
|
|
345
|
+
{
|
|
346
|
+
nav,
|
|
347
|
+
auth,
|
|
348
|
+
themeToggle,
|
|
349
|
+
linkComponent
|
|
350
|
+
}
|
|
351
|
+
)
|
|
352
|
+
}
|
|
353
|
+
)
|
|
354
|
+
] })
|
|
355
|
+
] })
|
|
296
356
|
]
|
|
297
357
|
}
|
|
298
358
|
);
|
|
299
359
|
}
|
|
300
360
|
|
|
361
|
+
// src/lib/use-media-query.ts
|
|
362
|
+
import { useCallback, useSyncExternalStore } from "react";
|
|
363
|
+
function useMediaQuery(query) {
|
|
364
|
+
const subscribe = useCallback(
|
|
365
|
+
(onChange) => {
|
|
366
|
+
if (typeof window === "undefined" || typeof window.matchMedia !== "function") {
|
|
367
|
+
return () => {
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
const mql = window.matchMedia(query);
|
|
371
|
+
mql.addEventListener("change", onChange);
|
|
372
|
+
return () => mql.removeEventListener("change", onChange);
|
|
373
|
+
},
|
|
374
|
+
[query]
|
|
375
|
+
);
|
|
376
|
+
const getSnapshot = () => typeof window !== "undefined" && typeof window.matchMedia === "function" ? window.matchMedia(query).matches : false;
|
|
377
|
+
return useSyncExternalStore(subscribe, getSnapshot, () => false);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// src/use-responsive-placeholder.ts
|
|
381
|
+
function useResponsivePlaceholder(desktop, mobile) {
|
|
382
|
+
const isDesktop = useMediaQuery(`(min-width: ${BREAKPOINTS.md}px)`);
|
|
383
|
+
return isDesktop ? desktop : mobile ?? desktop;
|
|
384
|
+
}
|
|
385
|
+
|
|
301
386
|
// src/app-sidebar.tsx
|
|
302
|
-
import { useEffect as
|
|
387
|
+
import { useEffect as useEffect5, useId as useId3, useMemo, useRef as useRef4, useState as useState6 } from "react";
|
|
303
388
|
import { Menu as Menu2, X as X2 } from "lucide-react";
|
|
304
389
|
|
|
305
390
|
// src/lib/resolve-icon.ts
|
|
@@ -320,7 +405,7 @@ function resolveIcon(name) {
|
|
|
320
405
|
}
|
|
321
406
|
|
|
322
407
|
// src/lib/use-dismissable-disclosure.ts
|
|
323
|
-
import { useEffect as
|
|
408
|
+
import { useEffect as useEffect3 } from "react";
|
|
324
409
|
function useDismissableDisclosure({
|
|
325
410
|
open,
|
|
326
411
|
onClose,
|
|
@@ -328,10 +413,10 @@ function useDismissableDisclosure({
|
|
|
328
413
|
triggerRef,
|
|
329
414
|
initialFocusRef
|
|
330
415
|
}) {
|
|
331
|
-
|
|
416
|
+
useEffect3(() => {
|
|
332
417
|
if (open) initialFocusRef?.current?.focus();
|
|
333
418
|
}, [open]);
|
|
334
|
-
|
|
419
|
+
useEffect3(() => {
|
|
335
420
|
if (!open) return;
|
|
336
421
|
const onKey = (e) => {
|
|
337
422
|
if (e.key === "Escape") {
|
|
@@ -534,13 +619,13 @@ function resolveLadder(raw, registry, groupOrder) {
|
|
|
534
619
|
}
|
|
535
620
|
|
|
536
621
|
// src/ladder/use-ladder-rung.ts
|
|
537
|
-
import { useLayoutEffect, useRef as useRef2, useState as
|
|
622
|
+
import { useLayoutEffect, useRef as useRef2, useState as useState4 } from "react";
|
|
538
623
|
var DEFAULT_ROW_HEIGHT = 36;
|
|
539
624
|
var DEFAULT_HYSTERESIS_ROWS = 1;
|
|
540
625
|
function useLadderRung(ladder, containerRef, options = {}) {
|
|
541
626
|
const rowHeight = options.rowHeight ?? DEFAULT_ROW_HEIGHT;
|
|
542
627
|
const hysteresisRows = options.hysteresisRows ?? DEFAULT_HYSTERESIS_ROWS;
|
|
543
|
-
const [rungIndex, setRungIndex] =
|
|
628
|
+
const [rungIndex, setRungIndex] = useState4(0);
|
|
544
629
|
const rungIndexRef = useRef2(0);
|
|
545
630
|
const ladderSignature = `${ladder.version}:${ladder.rungs.map((r) => r.rows).join(",")}`;
|
|
546
631
|
useLayoutEffect(() => {
|
|
@@ -579,13 +664,13 @@ function useLadderRung(ladder, containerRef, options = {}) {
|
|
|
579
664
|
}
|
|
580
665
|
|
|
581
666
|
// src/ladder/flyout.tsx
|
|
582
|
-
import { useCallback as useCallback2, useEffect as
|
|
583
|
-
import { jsx as jsx3, jsxs as
|
|
667
|
+
import { useCallback as useCallback2, useEffect as useEffect4, useId as useId2, useLayoutEffect as useLayoutEffect2, useRef as useRef3, useState as useState5 } from "react";
|
|
668
|
+
import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
584
669
|
var PANEL_GAP = 4;
|
|
585
670
|
function Flyout({ label, icon, active, children, className }) {
|
|
586
|
-
const [hoverOpen, setHoverOpen] =
|
|
587
|
-
const [clickOpen, setClickOpen] =
|
|
588
|
-
const [panelPosition, setPanelPosition] =
|
|
671
|
+
const [hoverOpen, setHoverOpen] = useState5(false);
|
|
672
|
+
const [clickOpen, setClickOpen] = useState5(false);
|
|
673
|
+
const [panelPosition, setPanelPosition] = useState5(null);
|
|
589
674
|
const open = hoverOpen || clickOpen;
|
|
590
675
|
const panelId = useId2();
|
|
591
676
|
const triggerRef = useRef3(null);
|
|
@@ -600,7 +685,7 @@ function Flyout({ label, icon, active, children, className }) {
|
|
|
600
685
|
useLayoutEffect2(() => {
|
|
601
686
|
if (open) reposition();
|
|
602
687
|
}, [open, reposition]);
|
|
603
|
-
|
|
688
|
+
useEffect4(() => {
|
|
604
689
|
if (!open) return;
|
|
605
690
|
window.addEventListener("resize", reposition);
|
|
606
691
|
window.addEventListener("scroll", reposition, true);
|
|
@@ -618,10 +703,10 @@ function Flyout({ label, icon, active, children, className }) {
|
|
|
618
703
|
panelRef,
|
|
619
704
|
triggerRef
|
|
620
705
|
});
|
|
621
|
-
|
|
706
|
+
useEffect4(() => {
|
|
622
707
|
if (clickOpen && panelPosition) firstItemRef.current?.focus();
|
|
623
708
|
}, [clickOpen, panelPosition]);
|
|
624
|
-
return /* @__PURE__ */
|
|
709
|
+
return /* @__PURE__ */ jsxs3(
|
|
625
710
|
"div",
|
|
626
711
|
{
|
|
627
712
|
"data-testid": "ladder-flyout",
|
|
@@ -629,7 +714,7 @@ function Flyout({ label, icon, active, children, className }) {
|
|
|
629
714
|
onMouseEnter: () => setHoverOpen(true),
|
|
630
715
|
onMouseLeave: () => setHoverOpen(false),
|
|
631
716
|
children: [
|
|
632
|
-
/* @__PURE__ */
|
|
717
|
+
/* @__PURE__ */ jsxs3(
|
|
633
718
|
"button",
|
|
634
719
|
{
|
|
635
720
|
ref: triggerRef,
|
|
@@ -672,8 +757,9 @@ function Flyout({ label, icon, active, children, className }) {
|
|
|
672
757
|
ref: i === 0 ? firstItemRef : void 0,
|
|
673
758
|
href: child.href,
|
|
674
759
|
"aria-current": child.active ? "page" : void 0,
|
|
760
|
+
style: TOUCH_TARGET_STYLE,
|
|
675
761
|
className: cn(
|
|
676
|
-
"truncate rounded-md px-3 py-1.5 text-sm transition-colors",
|
|
762
|
+
"flex items-center truncate rounded-md px-3 py-1.5 text-sm transition-colors",
|
|
677
763
|
child.active ? "bg-primary/10 text-foreground" : "text-muted-foreground hover:bg-foreground/5 hover:text-foreground"
|
|
678
764
|
),
|
|
679
765
|
children: child.label
|
|
@@ -688,7 +774,7 @@ function Flyout({ label, icon, active, children, className }) {
|
|
|
688
774
|
}
|
|
689
775
|
|
|
690
776
|
// src/app-sidebar.tsx
|
|
691
|
-
import { Fragment as Fragment2, jsx as jsx4, jsxs as
|
|
777
|
+
import { Fragment as Fragment2, jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
692
778
|
function isHrefActive(activePath, href) {
|
|
693
779
|
return activePath === href || activePath.startsWith(`${href}/`);
|
|
694
780
|
}
|
|
@@ -700,7 +786,7 @@ function NavLink({
|
|
|
700
786
|
testId
|
|
701
787
|
}) {
|
|
702
788
|
const Icon = resolveIcon(icon);
|
|
703
|
-
return /* @__PURE__ */
|
|
789
|
+
return /* @__PURE__ */ jsxs4(
|
|
704
790
|
"a",
|
|
705
791
|
{
|
|
706
792
|
href,
|
|
@@ -736,7 +822,7 @@ function AppSidebar({
|
|
|
736
822
|
className
|
|
737
823
|
}) {
|
|
738
824
|
const isDesktop = useMediaQuery(`(min-width: ${BREAKPOINTS.md}px)`);
|
|
739
|
-
const [mobileOpen, setMobileOpen] =
|
|
825
|
+
const [mobileOpen, setMobileOpen] = useState6(false);
|
|
740
826
|
const containerRef = useRef4(null);
|
|
741
827
|
const drawerId = useId3();
|
|
742
828
|
const mobileTriggerRef = useRef4(null);
|
|
@@ -753,7 +839,7 @@ function AppSidebar({
|
|
|
753
839
|
);
|
|
754
840
|
const rung = useLadderRung(resolvedLadder, containerRef, ladderOptions);
|
|
755
841
|
const rung0 = resolvedLadder.rungs[0];
|
|
756
|
-
|
|
842
|
+
useEffect5(() => {
|
|
757
843
|
if (isDesktop) setMobileOpen(false);
|
|
758
844
|
}, [isDesktop]);
|
|
759
845
|
useDismissableDisclosure({
|
|
@@ -792,7 +878,7 @@ function AppSidebar({
|
|
|
792
878
|
const active = children.some((c) => c.active);
|
|
793
879
|
return /* @__PURE__ */ jsx4(Flyout, { label: item.label, icon: item.icon, active, children }, item.id);
|
|
794
880
|
}
|
|
795
|
-
const brandBlock = brand && /* @__PURE__ */
|
|
881
|
+
const brandBlock = brand && /* @__PURE__ */ jsxs4(
|
|
796
882
|
"a",
|
|
797
883
|
{
|
|
798
884
|
href: brand.href,
|
|
@@ -817,7 +903,7 @@ function AppSidebar({
|
|
|
817
903
|
}
|
|
818
904
|
) });
|
|
819
905
|
}
|
|
820
|
-
return /* @__PURE__ */
|
|
906
|
+
return /* @__PURE__ */ jsxs4(Fragment2, { children: [
|
|
821
907
|
!isDesktop && /* @__PURE__ */ jsx4(
|
|
822
908
|
"button",
|
|
823
909
|
{
|
|
@@ -828,11 +914,12 @@ function AppSidebar({
|
|
|
828
914
|
"aria-expanded": mobileOpen,
|
|
829
915
|
"aria-controls": drawerId,
|
|
830
916
|
onClick: () => setMobileOpen((v) => !v),
|
|
917
|
+
style: TOUCH_TARGET_STYLE,
|
|
831
918
|
className: "flex items-center justify-center rounded-md border border-border p-2 text-foreground",
|
|
832
919
|
children: mobileOpen ? /* @__PURE__ */ jsx4(X2, { className: "h-5 w-5", "aria-hidden": "true" }) : /* @__PURE__ */ jsx4(Menu2, { className: "h-5 w-5", "aria-hidden": "true" })
|
|
833
920
|
}
|
|
834
921
|
),
|
|
835
|
-
!isDesktop && mobileOpen && /* @__PURE__ */
|
|
922
|
+
!isDesktop && mobileOpen && /* @__PURE__ */ jsxs4("div", { className: "fixed inset-0 z-50", children: [
|
|
836
923
|
/* @__PURE__ */ jsx4(
|
|
837
924
|
"div",
|
|
838
925
|
{
|
|
@@ -842,7 +929,7 @@ function AppSidebar({
|
|
|
842
929
|
onClick: () => setMobileOpen(false)
|
|
843
930
|
}
|
|
844
931
|
),
|
|
845
|
-
/* @__PURE__ */
|
|
932
|
+
/* @__PURE__ */ jsxs4(
|
|
846
933
|
"div",
|
|
847
934
|
{
|
|
848
935
|
ref: mobileDrawerRef,
|
|
@@ -861,6 +948,7 @@ function AppSidebar({
|
|
|
861
948
|
"data-testid": "app-sidebar-mobile-close",
|
|
862
949
|
"aria-label": `Close ${menuLabel.toLowerCase()}`,
|
|
863
950
|
onClick: () => setMobileOpen(false),
|
|
951
|
+
style: TOUCH_TARGET_STYLE,
|
|
864
952
|
className: "flex items-center justify-center rounded-md p-2 text-foreground hover:bg-foreground/5",
|
|
865
953
|
children: /* @__PURE__ */ jsx4(X2, { className: "h-5 w-5", "aria-hidden": "true" })
|
|
866
954
|
}
|
|
@@ -874,7 +962,7 @@ function AppSidebar({
|
|
|
874
962
|
}
|
|
875
963
|
)
|
|
876
964
|
] }),
|
|
877
|
-
/* @__PURE__ */
|
|
965
|
+
/* @__PURE__ */ jsxs4(
|
|
878
966
|
"aside",
|
|
879
967
|
{
|
|
880
968
|
"data-testid": "app-sidebar-desktop",
|
|
@@ -896,15 +984,193 @@ function AppSidebar({
|
|
|
896
984
|
)
|
|
897
985
|
] });
|
|
898
986
|
}
|
|
987
|
+
|
|
988
|
+
// src/context-switcher.tsx
|
|
989
|
+
import { ChevronDown, Check } from "lucide-react";
|
|
990
|
+
|
|
991
|
+
// src/lib/group-by-org.ts
|
|
992
|
+
function groupItemsByOrg(rows, get) {
|
|
993
|
+
const byOrg = /* @__PURE__ */ new Map();
|
|
994
|
+
for (const row of rows) {
|
|
995
|
+
const key = get.orgId(row) ?? "";
|
|
996
|
+
let group = byOrg.get(key);
|
|
997
|
+
if (!group) {
|
|
998
|
+
group = {
|
|
999
|
+
orgId: get.orgId(row) ?? null,
|
|
1000
|
+
orgName: get.orgName(row) ?? null,
|
|
1001
|
+
items: []
|
|
1002
|
+
};
|
|
1003
|
+
byOrg.set(key, group);
|
|
1004
|
+
}
|
|
1005
|
+
group.items.push(row);
|
|
1006
|
+
}
|
|
1007
|
+
const groups = [...byOrg.values()];
|
|
1008
|
+
for (const g of groups) {
|
|
1009
|
+
g.items.sort((a, b) => get.name(a).localeCompare(get.name(b)));
|
|
1010
|
+
}
|
|
1011
|
+
groups.sort((a, b) => (a.orgName ?? "").localeCompare(b.orgName ?? ""));
|
|
1012
|
+
return groups;
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
// src/lib/dropdown-menu.tsx
|
|
1016
|
+
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
|
|
1017
|
+
import { jsx as jsx5 } from "react/jsx-runtime";
|
|
1018
|
+
function DropdownMenu(props) {
|
|
1019
|
+
return /* @__PURE__ */ jsx5(DropdownMenuPrimitive.Root, { "data-slot": "dropdown-menu", ...props });
|
|
1020
|
+
}
|
|
1021
|
+
function DropdownMenuTrigger(props) {
|
|
1022
|
+
return /* @__PURE__ */ jsx5(DropdownMenuPrimitive.Trigger, { "data-slot": "dropdown-menu-trigger", ...props });
|
|
1023
|
+
}
|
|
1024
|
+
function DropdownMenuContent({
|
|
1025
|
+
className,
|
|
1026
|
+
sideOffset = 4,
|
|
1027
|
+
...props
|
|
1028
|
+
}) {
|
|
1029
|
+
return /* @__PURE__ */ jsx5(DropdownMenuPrimitive.Portal, { children: /* @__PURE__ */ jsx5(
|
|
1030
|
+
DropdownMenuPrimitive.Content,
|
|
1031
|
+
{
|
|
1032
|
+
"data-slot": "dropdown-menu-content",
|
|
1033
|
+
sideOffset,
|
|
1034
|
+
className: cn(
|
|
1035
|
+
"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",
|
|
1036
|
+
className
|
|
1037
|
+
),
|
|
1038
|
+
...props
|
|
1039
|
+
}
|
|
1040
|
+
) });
|
|
1041
|
+
}
|
|
1042
|
+
function DropdownMenuItem({
|
|
1043
|
+
className,
|
|
1044
|
+
...props
|
|
1045
|
+
}) {
|
|
1046
|
+
return /* @__PURE__ */ jsx5(
|
|
1047
|
+
DropdownMenuPrimitive.Item,
|
|
1048
|
+
{
|
|
1049
|
+
"data-slot": "dropdown-menu-item",
|
|
1050
|
+
className: cn(
|
|
1051
|
+
"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",
|
|
1052
|
+
className
|
|
1053
|
+
),
|
|
1054
|
+
...props
|
|
1055
|
+
}
|
|
1056
|
+
);
|
|
1057
|
+
}
|
|
1058
|
+
function DropdownMenuLabel({
|
|
1059
|
+
className,
|
|
1060
|
+
...props
|
|
1061
|
+
}) {
|
|
1062
|
+
return /* @__PURE__ */ jsx5(
|
|
1063
|
+
DropdownMenuPrimitive.Label,
|
|
1064
|
+
{
|
|
1065
|
+
"data-slot": "dropdown-menu-label",
|
|
1066
|
+
className: cn("px-2 py-1.5 text-sm font-medium", className),
|
|
1067
|
+
...props
|
|
1068
|
+
}
|
|
1069
|
+
);
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
// src/context-switcher.tsx
|
|
1073
|
+
import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
1074
|
+
function groupByOrg(items) {
|
|
1075
|
+
return groupItemsByOrg(items, {
|
|
1076
|
+
orgId: (it) => it.orgId,
|
|
1077
|
+
orgName: (it) => it.orgName,
|
|
1078
|
+
name: (it) => it.name
|
|
1079
|
+
});
|
|
1080
|
+
}
|
|
1081
|
+
function toBrand(item) {
|
|
1082
|
+
return {
|
|
1083
|
+
name: item.name,
|
|
1084
|
+
favicon: item.favicon,
|
|
1085
|
+
logo: item.logo,
|
|
1086
|
+
color: item.color,
|
|
1087
|
+
accentColor: item.accentColor
|
|
1088
|
+
};
|
|
1089
|
+
}
|
|
1090
|
+
function ContextSwitcher({
|
|
1091
|
+
items,
|
|
1092
|
+
activeRefId,
|
|
1093
|
+
onSelect,
|
|
1094
|
+
footer,
|
|
1095
|
+
placeholder = "Select",
|
|
1096
|
+
collapsed = false,
|
|
1097
|
+
side = "bottom",
|
|
1098
|
+
align = "start",
|
|
1099
|
+
contentClassName
|
|
1100
|
+
}) {
|
|
1101
|
+
const active = items.find((it) => it.refId === activeRefId) ?? null;
|
|
1102
|
+
const groups = groupByOrg(items);
|
|
1103
|
+
const showOrgHeaders = groups.length > 1;
|
|
1104
|
+
return /* @__PURE__ */ jsxs5(DropdownMenu, { children: [
|
|
1105
|
+
/* @__PURE__ */ jsx6(DropdownMenuTrigger, { asChild: true, children: collapsed ? /* @__PURE__ */ jsx6(
|
|
1106
|
+
"button",
|
|
1107
|
+
{
|
|
1108
|
+
type: "button",
|
|
1109
|
+
title: active?.name || placeholder,
|
|
1110
|
+
"aria-label": active?.name || placeholder,
|
|
1111
|
+
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",
|
|
1112
|
+
children: /* @__PURE__ */ jsx6(BrandGlyph, { brand: toBrand(active ?? { name: "" }), size: "lg" })
|
|
1113
|
+
}
|
|
1114
|
+
) : /* @__PURE__ */ jsxs5(
|
|
1115
|
+
"button",
|
|
1116
|
+
{
|
|
1117
|
+
type: "button",
|
|
1118
|
+
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",
|
|
1119
|
+
children: [
|
|
1120
|
+
/* @__PURE__ */ jsx6(BrandGlyph, { brand: toBrand(active ?? { name: "" }) }),
|
|
1121
|
+
/* @__PURE__ */ jsx6("span", { className: "max-w-[140px] truncate text-foreground/90", children: active?.name || placeholder }),
|
|
1122
|
+
/* @__PURE__ */ jsx6(ChevronDown, { className: "h-3.5 w-3.5 text-muted-foreground transition-transform group-data-[state=open]:rotate-180" })
|
|
1123
|
+
]
|
|
1124
|
+
}
|
|
1125
|
+
) }),
|
|
1126
|
+
/* @__PURE__ */ jsxs5(
|
|
1127
|
+
DropdownMenuContent,
|
|
1128
|
+
{
|
|
1129
|
+
side,
|
|
1130
|
+
align,
|
|
1131
|
+
sideOffset: 6,
|
|
1132
|
+
collisionPadding: 8,
|
|
1133
|
+
className: cn("w-60 overflow-hidden p-0", contentClassName),
|
|
1134
|
+
children: [
|
|
1135
|
+
groups.map((group) => /* @__PURE__ */ jsxs5("div", { children: [
|
|
1136
|
+
showOrgHeaders && /* @__PURE__ */ jsx6(DropdownMenuLabel, { className: "px-3 pb-1 pt-2 text-[11px] font-semibold uppercase tracking-wide text-muted-foreground", children: group.orgName ?? "Other" }),
|
|
1137
|
+
/* @__PURE__ */ jsx6("div", { className: cn(showOrgHeaders && "ml-5 border-l border-border/60"), children: group.items.map((item) => {
|
|
1138
|
+
const isActive = item.refId === activeRefId;
|
|
1139
|
+
return /* @__PURE__ */ jsxs5(
|
|
1140
|
+
DropdownMenuItem,
|
|
1141
|
+
{
|
|
1142
|
+
"data-active": isActive || void 0,
|
|
1143
|
+
onSelect: () => onSelect(item),
|
|
1144
|
+
className: "flex w-full items-center gap-2.5 rounded-none px-3 py-2 text-sm",
|
|
1145
|
+
children: [
|
|
1146
|
+
/* @__PURE__ */ jsx6(BrandGlyph, { brand: toBrand(item), subtle: true }),
|
|
1147
|
+
/* @__PURE__ */ jsx6("span", { className: "flex-1 truncate text-left text-foreground/90", children: item.name }),
|
|
1148
|
+
isActive && /* @__PURE__ */ jsx6(Check, { className: "size-3.5 text-emerald-500" })
|
|
1149
|
+
]
|
|
1150
|
+
},
|
|
1151
|
+
item.refId
|
|
1152
|
+
);
|
|
1153
|
+
}) })
|
|
1154
|
+
] }, group.orgId ?? "unattributed")),
|
|
1155
|
+
footer && /* @__PURE__ */ jsx6("div", { className: "sticky bottom-0 border-t border-border bg-popover", children: footer })
|
|
1156
|
+
]
|
|
1157
|
+
}
|
|
1158
|
+
)
|
|
1159
|
+
] });
|
|
1160
|
+
}
|
|
899
1161
|
export {
|
|
900
1162
|
AppSidebar,
|
|
901
1163
|
BrandGlyph,
|
|
1164
|
+
ContextSwitcher,
|
|
902
1165
|
Flyout,
|
|
1166
|
+
REEVE_ACCENT,
|
|
903
1167
|
ResponsiveHeader,
|
|
904
1168
|
deriveFallbackLadder,
|
|
905
1169
|
faviconUrl,
|
|
1170
|
+
groupByOrg,
|
|
906
1171
|
reconcileLadder,
|
|
907
1172
|
resolveLadder,
|
|
908
1173
|
useLadderRung,
|
|
1174
|
+
useResponsivePlaceholder,
|
|
909
1175
|
zSidebarLadder
|
|
910
1176
|
};
|