@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.mjs
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
// src/brand-glyph.tsx
|
|
4
|
-
import Image from "next/image";
|
|
5
4
|
import { useState } from "react";
|
|
6
5
|
|
|
7
6
|
// src/lib/cn.ts
|
|
@@ -24,12 +23,18 @@ function cn(...inputs) {
|
|
|
24
23
|
}
|
|
25
24
|
|
|
26
25
|
// src/lib/initials.ts
|
|
27
|
-
function
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
return parts.map((p) => p[0]?.toUpperCase() ?? "").join("") || "\xB7\xB7";
|
|
26
|
+
function firstInitial(name) {
|
|
27
|
+
const trimmed = (name ?? "").trim();
|
|
28
|
+
return trimmed ? trimmed[0].toUpperCase() : "?";
|
|
31
29
|
}
|
|
32
30
|
|
|
31
|
+
// src/lib/lockup.ts
|
|
32
|
+
var MARK_CAP_RATIO = 1.08;
|
|
33
|
+
var MONOGRAM_TEXT_LG = "text-[15px]";
|
|
34
|
+
var LOCKUP_GAP = "gap-2";
|
|
35
|
+
var LOCKUP_ROW = "flex items-baseline";
|
|
36
|
+
var GLYPH_CANVAS_FILL = 0.65;
|
|
37
|
+
|
|
33
38
|
// src/lib/monogram.ts
|
|
34
39
|
var MONOGRAM_COLORS = [
|
|
35
40
|
"#7c3aed",
|
|
@@ -56,7 +61,8 @@ function monogramColor(seed) {
|
|
|
56
61
|
}
|
|
57
62
|
|
|
58
63
|
// src/brand-glyph.tsx
|
|
59
|
-
import { jsx } from "react/jsx-runtime";
|
|
64
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
65
|
+
var REEVE_ACCENT = "#FF6B2B";
|
|
60
66
|
function faviconUrl(domain) {
|
|
61
67
|
if (!domain) return null;
|
|
62
68
|
const host = domain.trim().toLowerCase();
|
|
@@ -77,21 +83,26 @@ function BrandGlyph({ brand, size = "sm", subtle = false, className }) {
|
|
|
77
83
|
"span",
|
|
78
84
|
{
|
|
79
85
|
className: cn(
|
|
80
|
-
|
|
86
|
+
// `self-center` opts image glyphs out of the lockup's baseline
|
|
87
|
+
// alignment (DEV-5139): an image box has no text baseline, so
|
|
88
|
+
// flexbox would seat its BOTTOM edge on the wordmark baseline and
|
|
89
|
+
// shove the icon upward. Centering preserves the pre-DEV-5139 look.
|
|
90
|
+
"flex shrink-0 items-center justify-center self-center overflow-hidden rounded",
|
|
81
91
|
// Tint needs a dark-mode counterpart or it vanishes on dark backgrounds.
|
|
82
92
|
subtle ? "bg-black/5 dark:bg-white/10" : "bg-black/10 dark:bg-white/15",
|
|
83
93
|
box,
|
|
84
94
|
className
|
|
85
95
|
),
|
|
86
96
|
children: /* @__PURE__ */ jsx(
|
|
87
|
-
|
|
97
|
+
"img",
|
|
88
98
|
{
|
|
89
99
|
src,
|
|
90
100
|
alt: "",
|
|
91
101
|
width: dim,
|
|
92
102
|
height: dim,
|
|
103
|
+
loading: "lazy",
|
|
104
|
+
decoding: "async",
|
|
93
105
|
"data-testid": "brand-image",
|
|
94
|
-
unoptimized: true,
|
|
95
106
|
onError: () => setFailed((prev) => new Set(prev).add(src)),
|
|
96
107
|
className: cn(img, "rounded-sm object-contain")
|
|
97
108
|
}
|
|
@@ -99,44 +110,51 @@ function BrandGlyph({ brand, size = "sm", subtle = false, className }) {
|
|
|
99
110
|
}
|
|
100
111
|
);
|
|
101
112
|
}
|
|
102
|
-
|
|
113
|
+
const letter = firstInitial(brand.name);
|
|
114
|
+
const hasRealLetter = letter !== "?";
|
|
115
|
+
return /* @__PURE__ */ jsxs(
|
|
103
116
|
"span",
|
|
104
117
|
{
|
|
105
118
|
"aria-hidden": true,
|
|
106
119
|
"data-testid": "brand-monogram",
|
|
107
120
|
className: cn(
|
|
108
|
-
"flex shrink-0 select-none items-
|
|
109
|
-
|
|
121
|
+
"flex shrink-0 select-none items-baseline justify-center font-semibold uppercase",
|
|
122
|
+
// DEV-5139 rule 2: at "lg" the letter runs 15px = the text-sm
|
|
123
|
+
// wordmark (14px) × MARK_CAP_RATIO (1.08) — a lone round glyph at
|
|
124
|
+
// exact parity optically reads small. "sm" has no wordmark partner
|
|
125
|
+
// in its surfaces; unchanged pending a lockup audit there.
|
|
126
|
+
size === "lg" ? MONOGRAM_TEXT_LG : "text-[11px]",
|
|
110
127
|
box,
|
|
111
128
|
className
|
|
112
129
|
),
|
|
113
|
-
style: {
|
|
114
|
-
children:
|
|
130
|
+
style: { color: brand.color || monogramColor(brand.name || "?") },
|
|
131
|
+
children: [
|
|
132
|
+
letter,
|
|
133
|
+
hasRealLetter && /* @__PURE__ */ jsx("span", { "data-testid": "brand-monogram-dot", style: { color: brand.accentColor || REEVE_ACCENT }, children: "." })
|
|
134
|
+
]
|
|
115
135
|
}
|
|
116
136
|
);
|
|
117
137
|
}
|
|
118
138
|
|
|
119
139
|
// src/responsive-header.tsx
|
|
120
|
-
import { useEffect, useId, useRef, useState as
|
|
140
|
+
import { useEffect as useEffect2, useId, useRef, useState as useState3 } from "react";
|
|
121
141
|
import { Menu, X } from "lucide-react";
|
|
122
142
|
|
|
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);
|
|
143
|
+
// src/lib/use-container-width-at-least.ts
|
|
144
|
+
import { useEffect, useState as useState2 } from "react";
|
|
145
|
+
function useContainerWidthAtLeast(ref, px) {
|
|
146
|
+
const [atLeast, setAtLeast] = useState2(false);
|
|
147
|
+
useEffect(() => {
|
|
148
|
+
const el = ref.current;
|
|
149
|
+
if (!el || typeof ResizeObserver === "undefined") return;
|
|
150
|
+
const ro = new ResizeObserver(([entry]) => {
|
|
151
|
+
if (!entry) return;
|
|
152
|
+
setAtLeast(entry.contentRect.width >= px);
|
|
153
|
+
});
|
|
154
|
+
ro.observe(el);
|
|
155
|
+
return () => ro.disconnect();
|
|
156
|
+
}, [ref, px]);
|
|
157
|
+
return atLeast;
|
|
140
158
|
}
|
|
141
159
|
|
|
142
160
|
// src/tokens.ts
|
|
@@ -156,24 +174,30 @@ var BREAKPOINTS = {
|
|
|
156
174
|
/** Tailwind `md` — the header switches to its full desktop layout at/above this. */
|
|
157
175
|
md: 768
|
|
158
176
|
};
|
|
177
|
+
var TOUCH_TARGET_STYLE = {
|
|
178
|
+
minWidth: MIN_SIZES.touchTarget,
|
|
179
|
+
minHeight: MIN_SIZES.touchTarget
|
|
180
|
+
};
|
|
159
181
|
|
|
160
182
|
// src/responsive-header.tsx
|
|
161
|
-
import { Fragment, jsx as jsx2, jsxs } from "react/jsx-runtime";
|
|
183
|
+
import { Fragment, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
162
184
|
function MenuIcon({ open }) {
|
|
163
185
|
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
186
|
}
|
|
165
187
|
function SecondaryControls({
|
|
166
188
|
nav,
|
|
167
189
|
auth,
|
|
168
|
-
themeToggle
|
|
190
|
+
themeToggle,
|
|
191
|
+
linkComponent
|
|
169
192
|
}) {
|
|
170
|
-
|
|
193
|
+
const LinkEl = linkComponent ?? "a";
|
|
194
|
+
return /* @__PURE__ */ jsxs2(Fragment, { children: [
|
|
171
195
|
nav?.map(
|
|
172
196
|
(item) => (
|
|
173
197
|
// A link navigates; an action-only item (no href) is a button — never an
|
|
174
198
|
// <a href="#"> that scroll-jumps and pushes a history entry.
|
|
175
199
|
item.href ? /* @__PURE__ */ jsx2(
|
|
176
|
-
|
|
200
|
+
LinkEl,
|
|
177
201
|
{
|
|
178
202
|
href: item.href,
|
|
179
203
|
onClick: item.onSelect,
|
|
@@ -181,15 +205,22 @@ function SecondaryControls({
|
|
|
181
205
|
children: item.label
|
|
182
206
|
},
|
|
183
207
|
item.label
|
|
184
|
-
) :
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
208
|
+
) : (
|
|
209
|
+
// A real <button> secondary control — MIN_SIZES.control is the
|
|
210
|
+
// token's exact stated purpose ("Generic minimum width/height for
|
|
211
|
+
// a secondary control (button, select)"), previously defined but
|
|
212
|
+
// unused anywhere in this package (DEV-4249).
|
|
213
|
+
/* @__PURE__ */ jsx2(
|
|
214
|
+
"button",
|
|
215
|
+
{
|
|
216
|
+
type: "button",
|
|
217
|
+
onClick: item.onSelect,
|
|
218
|
+
style: { minWidth: MIN_SIZES.control, minHeight: MIN_SIZES.control },
|
|
219
|
+
className: "flex items-center justify-start text-left text-sm font-medium text-foreground/80 hover:text-foreground",
|
|
220
|
+
children: item.label
|
|
221
|
+
},
|
|
222
|
+
item.label
|
|
223
|
+
)
|
|
193
224
|
)
|
|
194
225
|
)
|
|
195
226
|
),
|
|
@@ -201,20 +232,24 @@ function ResponsiveHeader({
|
|
|
201
232
|
brand,
|
|
202
233
|
search,
|
|
203
234
|
nav,
|
|
235
|
+
brandHref,
|
|
236
|
+
linkComponent,
|
|
204
237
|
auth,
|
|
205
238
|
themeToggle,
|
|
206
239
|
menuLabel = "Menu",
|
|
207
240
|
className
|
|
208
241
|
}) {
|
|
209
|
-
const
|
|
210
|
-
const [open, setOpen] =
|
|
242
|
+
const LinkEl = linkComponent ?? "a";
|
|
243
|
+
const [open, setOpen] = useState3(false);
|
|
211
244
|
const panelId = useId();
|
|
245
|
+
const headerRef = useRef(null);
|
|
212
246
|
const buttonRef = useRef(null);
|
|
213
247
|
const panelRef = useRef(null);
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
248
|
+
const containerAtOrAboveMd = useContainerWidthAtLeast(headerRef, BREAKPOINTS.md);
|
|
249
|
+
useEffect2(() => {
|
|
250
|
+
if (containerAtOrAboveMd) setOpen(false);
|
|
251
|
+
}, [containerAtOrAboveMd]);
|
|
252
|
+
useEffect2(() => {
|
|
218
253
|
if (!open) return;
|
|
219
254
|
const onKey = (e) => {
|
|
220
255
|
if (e.key === "Escape") {
|
|
@@ -236,27 +271,41 @@ function ResponsiveHeader({
|
|
|
236
271
|
};
|
|
237
272
|
}, [open]);
|
|
238
273
|
const hasSecondary = Boolean(nav && nav.length > 0 || auth || themeToggle);
|
|
239
|
-
|
|
274
|
+
const brandLockup = /* @__PURE__ */ jsxs2("span", { "data-testid": "header-lockup", className: cn(LOCKUP_ROW, LOCKUP_GAP), children: [
|
|
275
|
+
/* @__PURE__ */ jsx2(BrandGlyph, { brand, size: "lg" }),
|
|
276
|
+
/* @__PURE__ */ jsx2(
|
|
277
|
+
"span",
|
|
278
|
+
{
|
|
279
|
+
"data-testid": "header-wordmark",
|
|
280
|
+
className: "hidden truncate text-sm font-semibold text-foreground @min-[768px]:block",
|
|
281
|
+
style: { maxWidth: MIN_SIZES.wordmarkMaxWidth },
|
|
282
|
+
children: brand.name
|
|
283
|
+
}
|
|
284
|
+
)
|
|
285
|
+
] });
|
|
286
|
+
return /* @__PURE__ */ jsxs2(
|
|
240
287
|
"header",
|
|
241
288
|
{
|
|
289
|
+
ref: headerRef,
|
|
242
290
|
"data-testid": "responsive-header",
|
|
243
291
|
className: cn(
|
|
244
|
-
|
|
292
|
+
// `@container` makes this header itself the query context, so the
|
|
293
|
+
// variants below react to ITS width (e.g. a sidebar), not the viewport.
|
|
294
|
+
"@container flex w-full items-center gap-3 border-b border-border bg-background px-4 py-2",
|
|
245
295
|
className
|
|
246
296
|
),
|
|
247
297
|
children: [
|
|
248
|
-
/* @__PURE__ */
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
] }),
|
|
298
|
+
brandHref ? /* @__PURE__ */ jsx2(
|
|
299
|
+
LinkEl,
|
|
300
|
+
{
|
|
301
|
+
href: brandHref,
|
|
302
|
+
"aria-label": brand.name || "Home",
|
|
303
|
+
"data-testid": "header-brand",
|
|
304
|
+
style: TOUCH_TARGET_STYLE,
|
|
305
|
+
className: "flex shrink-0 items-center",
|
|
306
|
+
children: brandLockup
|
|
307
|
+
}
|
|
308
|
+
) : /* @__PURE__ */ jsx2("div", { "data-testid": "header-brand", className: "flex shrink-0 items-center", children: brandLockup }),
|
|
260
309
|
/* @__PURE__ */ jsx2(
|
|
261
310
|
"div",
|
|
262
311
|
{
|
|
@@ -266,40 +315,91 @@ function ResponsiveHeader({
|
|
|
266
315
|
children: search
|
|
267
316
|
}
|
|
268
317
|
),
|
|
269
|
-
hasSecondary &&
|
|
318
|
+
hasSecondary && /* @__PURE__ */ jsxs2(Fragment, { children: [
|
|
270
319
|
/* @__PURE__ */ jsx2(
|
|
271
|
-
"
|
|
320
|
+
"nav",
|
|
272
321
|
{
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
322
|
+
"data-testid": "header-actions",
|
|
323
|
+
className: "hidden shrink-0 items-center gap-3 @min-[768px]:flex",
|
|
324
|
+
children: /* @__PURE__ */ jsx2(
|
|
325
|
+
SecondaryControls,
|
|
326
|
+
{
|
|
327
|
+
nav,
|
|
328
|
+
auth,
|
|
329
|
+
themeToggle,
|
|
330
|
+
linkComponent
|
|
331
|
+
}
|
|
332
|
+
)
|
|
283
333
|
}
|
|
284
334
|
),
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
335
|
+
/* @__PURE__ */ jsxs2("div", { className: "relative shrink-0 @min-[768px]:hidden", children: [
|
|
336
|
+
/* @__PURE__ */ jsx2(
|
|
337
|
+
"button",
|
|
338
|
+
{
|
|
339
|
+
ref: buttonRef,
|
|
340
|
+
type: "button",
|
|
341
|
+
"data-testid": "header-menu-button",
|
|
342
|
+
"aria-label": menuLabel,
|
|
343
|
+
"aria-expanded": open,
|
|
344
|
+
"aria-controls": panelId,
|
|
345
|
+
onClick: () => setOpen((v) => !v),
|
|
346
|
+
className: "flex items-center justify-center rounded-md border border-border text-foreground",
|
|
347
|
+
style: TOUCH_TARGET_STYLE,
|
|
348
|
+
children: /* @__PURE__ */ jsx2(MenuIcon, { open })
|
|
349
|
+
}
|
|
350
|
+
),
|
|
351
|
+
open && /* @__PURE__ */ jsx2(
|
|
352
|
+
"div",
|
|
353
|
+
{
|
|
354
|
+
ref: panelRef,
|
|
355
|
+
id: panelId,
|
|
356
|
+
"data-testid": "header-menu-panel",
|
|
357
|
+
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",
|
|
358
|
+
children: /* @__PURE__ */ jsx2(
|
|
359
|
+
SecondaryControls,
|
|
360
|
+
{
|
|
361
|
+
nav,
|
|
362
|
+
auth,
|
|
363
|
+
themeToggle,
|
|
364
|
+
linkComponent
|
|
365
|
+
}
|
|
366
|
+
)
|
|
367
|
+
}
|
|
368
|
+
)
|
|
369
|
+
] })
|
|
370
|
+
] })
|
|
296
371
|
]
|
|
297
372
|
}
|
|
298
373
|
);
|
|
299
374
|
}
|
|
300
375
|
|
|
376
|
+
// src/lib/use-media-query.ts
|
|
377
|
+
import { useCallback, useSyncExternalStore } from "react";
|
|
378
|
+
function useMediaQuery(query) {
|
|
379
|
+
const subscribe = useCallback(
|
|
380
|
+
(onChange) => {
|
|
381
|
+
if (typeof window === "undefined" || typeof window.matchMedia !== "function") {
|
|
382
|
+
return () => {
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
const mql = window.matchMedia(query);
|
|
386
|
+
mql.addEventListener("change", onChange);
|
|
387
|
+
return () => mql.removeEventListener("change", onChange);
|
|
388
|
+
},
|
|
389
|
+
[query]
|
|
390
|
+
);
|
|
391
|
+
const getSnapshot = () => typeof window !== "undefined" && typeof window.matchMedia === "function" ? window.matchMedia(query).matches : false;
|
|
392
|
+
return useSyncExternalStore(subscribe, getSnapshot, () => false);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// src/use-responsive-placeholder.ts
|
|
396
|
+
function useResponsivePlaceholder(desktop, mobile) {
|
|
397
|
+
const isDesktop = useMediaQuery(`(min-width: ${BREAKPOINTS.md}px)`);
|
|
398
|
+
return isDesktop ? desktop : mobile ?? desktop;
|
|
399
|
+
}
|
|
400
|
+
|
|
301
401
|
// src/app-sidebar.tsx
|
|
302
|
-
import { useEffect as
|
|
402
|
+
import { useEffect as useEffect5, useId as useId3, useMemo, useRef as useRef4, useState as useState6 } from "react";
|
|
303
403
|
import { Menu as Menu2, X as X2 } from "lucide-react";
|
|
304
404
|
|
|
305
405
|
// src/lib/resolve-icon.ts
|
|
@@ -320,7 +420,7 @@ function resolveIcon(name) {
|
|
|
320
420
|
}
|
|
321
421
|
|
|
322
422
|
// src/lib/use-dismissable-disclosure.ts
|
|
323
|
-
import { useEffect as
|
|
423
|
+
import { useEffect as useEffect3 } from "react";
|
|
324
424
|
function useDismissableDisclosure({
|
|
325
425
|
open,
|
|
326
426
|
onClose,
|
|
@@ -328,10 +428,10 @@ function useDismissableDisclosure({
|
|
|
328
428
|
triggerRef,
|
|
329
429
|
initialFocusRef
|
|
330
430
|
}) {
|
|
331
|
-
|
|
431
|
+
useEffect3(() => {
|
|
332
432
|
if (open) initialFocusRef?.current?.focus();
|
|
333
433
|
}, [open]);
|
|
334
|
-
|
|
434
|
+
useEffect3(() => {
|
|
335
435
|
if (!open) return;
|
|
336
436
|
const onKey = (e) => {
|
|
337
437
|
if (e.key === "Escape") {
|
|
@@ -534,13 +634,13 @@ function resolveLadder(raw, registry, groupOrder) {
|
|
|
534
634
|
}
|
|
535
635
|
|
|
536
636
|
// src/ladder/use-ladder-rung.ts
|
|
537
|
-
import { useLayoutEffect, useRef as useRef2, useState as
|
|
637
|
+
import { useLayoutEffect, useRef as useRef2, useState as useState4 } from "react";
|
|
538
638
|
var DEFAULT_ROW_HEIGHT = 36;
|
|
539
639
|
var DEFAULT_HYSTERESIS_ROWS = 1;
|
|
540
640
|
function useLadderRung(ladder, containerRef, options = {}) {
|
|
541
641
|
const rowHeight = options.rowHeight ?? DEFAULT_ROW_HEIGHT;
|
|
542
642
|
const hysteresisRows = options.hysteresisRows ?? DEFAULT_HYSTERESIS_ROWS;
|
|
543
|
-
const [rungIndex, setRungIndex] =
|
|
643
|
+
const [rungIndex, setRungIndex] = useState4(0);
|
|
544
644
|
const rungIndexRef = useRef2(0);
|
|
545
645
|
const ladderSignature = `${ladder.version}:${ladder.rungs.map((r) => r.rows).join(",")}`;
|
|
546
646
|
useLayoutEffect(() => {
|
|
@@ -579,13 +679,13 @@ function useLadderRung(ladder, containerRef, options = {}) {
|
|
|
579
679
|
}
|
|
580
680
|
|
|
581
681
|
// src/ladder/flyout.tsx
|
|
582
|
-
import { useCallback as useCallback2, useEffect as
|
|
583
|
-
import { jsx as jsx3, jsxs as
|
|
682
|
+
import { useCallback as useCallback2, useEffect as useEffect4, useId as useId2, useLayoutEffect as useLayoutEffect2, useRef as useRef3, useState as useState5 } from "react";
|
|
683
|
+
import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
584
684
|
var PANEL_GAP = 4;
|
|
585
685
|
function Flyout({ label, icon, active, children, className }) {
|
|
586
|
-
const [hoverOpen, setHoverOpen] =
|
|
587
|
-
const [clickOpen, setClickOpen] =
|
|
588
|
-
const [panelPosition, setPanelPosition] =
|
|
686
|
+
const [hoverOpen, setHoverOpen] = useState5(false);
|
|
687
|
+
const [clickOpen, setClickOpen] = useState5(false);
|
|
688
|
+
const [panelPosition, setPanelPosition] = useState5(null);
|
|
589
689
|
const open = hoverOpen || clickOpen;
|
|
590
690
|
const panelId = useId2();
|
|
591
691
|
const triggerRef = useRef3(null);
|
|
@@ -600,7 +700,7 @@ function Flyout({ label, icon, active, children, className }) {
|
|
|
600
700
|
useLayoutEffect2(() => {
|
|
601
701
|
if (open) reposition();
|
|
602
702
|
}, [open, reposition]);
|
|
603
|
-
|
|
703
|
+
useEffect4(() => {
|
|
604
704
|
if (!open) return;
|
|
605
705
|
window.addEventListener("resize", reposition);
|
|
606
706
|
window.addEventListener("scroll", reposition, true);
|
|
@@ -618,10 +718,10 @@ function Flyout({ label, icon, active, children, className }) {
|
|
|
618
718
|
panelRef,
|
|
619
719
|
triggerRef
|
|
620
720
|
});
|
|
621
|
-
|
|
721
|
+
useEffect4(() => {
|
|
622
722
|
if (clickOpen && panelPosition) firstItemRef.current?.focus();
|
|
623
723
|
}, [clickOpen, panelPosition]);
|
|
624
|
-
return /* @__PURE__ */
|
|
724
|
+
return /* @__PURE__ */ jsxs3(
|
|
625
725
|
"div",
|
|
626
726
|
{
|
|
627
727
|
"data-testid": "ladder-flyout",
|
|
@@ -629,7 +729,7 @@ function Flyout({ label, icon, active, children, className }) {
|
|
|
629
729
|
onMouseEnter: () => setHoverOpen(true),
|
|
630
730
|
onMouseLeave: () => setHoverOpen(false),
|
|
631
731
|
children: [
|
|
632
|
-
/* @__PURE__ */
|
|
732
|
+
/* @__PURE__ */ jsxs3(
|
|
633
733
|
"button",
|
|
634
734
|
{
|
|
635
735
|
ref: triggerRef,
|
|
@@ -672,8 +772,9 @@ function Flyout({ label, icon, active, children, className }) {
|
|
|
672
772
|
ref: i === 0 ? firstItemRef : void 0,
|
|
673
773
|
href: child.href,
|
|
674
774
|
"aria-current": child.active ? "page" : void 0,
|
|
775
|
+
style: TOUCH_TARGET_STYLE,
|
|
675
776
|
className: cn(
|
|
676
|
-
"truncate rounded-md px-3 py-1.5 text-sm transition-colors",
|
|
777
|
+
"flex items-center truncate rounded-md px-3 py-1.5 text-sm transition-colors",
|
|
677
778
|
child.active ? "bg-primary/10 text-foreground" : "text-muted-foreground hover:bg-foreground/5 hover:text-foreground"
|
|
678
779
|
),
|
|
679
780
|
children: child.label
|
|
@@ -688,7 +789,7 @@ function Flyout({ label, icon, active, children, className }) {
|
|
|
688
789
|
}
|
|
689
790
|
|
|
690
791
|
// src/app-sidebar.tsx
|
|
691
|
-
import { Fragment as Fragment2, jsx as jsx4, jsxs as
|
|
792
|
+
import { Fragment as Fragment2, jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
692
793
|
function isHrefActive(activePath, href) {
|
|
693
794
|
return activePath === href || activePath.startsWith(`${href}/`);
|
|
694
795
|
}
|
|
@@ -700,7 +801,7 @@ function NavLink({
|
|
|
700
801
|
testId
|
|
701
802
|
}) {
|
|
702
803
|
const Icon = resolveIcon(icon);
|
|
703
|
-
return /* @__PURE__ */
|
|
804
|
+
return /* @__PURE__ */ jsxs4(
|
|
704
805
|
"a",
|
|
705
806
|
{
|
|
706
807
|
href,
|
|
@@ -736,7 +837,7 @@ function AppSidebar({
|
|
|
736
837
|
className
|
|
737
838
|
}) {
|
|
738
839
|
const isDesktop = useMediaQuery(`(min-width: ${BREAKPOINTS.md}px)`);
|
|
739
|
-
const [mobileOpen, setMobileOpen] =
|
|
840
|
+
const [mobileOpen, setMobileOpen] = useState6(false);
|
|
740
841
|
const containerRef = useRef4(null);
|
|
741
842
|
const drawerId = useId3();
|
|
742
843
|
const mobileTriggerRef = useRef4(null);
|
|
@@ -753,7 +854,7 @@ function AppSidebar({
|
|
|
753
854
|
);
|
|
754
855
|
const rung = useLadderRung(resolvedLadder, containerRef, ladderOptions);
|
|
755
856
|
const rung0 = resolvedLadder.rungs[0];
|
|
756
|
-
|
|
857
|
+
useEffect5(() => {
|
|
757
858
|
if (isDesktop) setMobileOpen(false);
|
|
758
859
|
}, [isDesktop]);
|
|
759
860
|
useDismissableDisclosure({
|
|
@@ -792,7 +893,7 @@ function AppSidebar({
|
|
|
792
893
|
const active = children.some((c) => c.active);
|
|
793
894
|
return /* @__PURE__ */ jsx4(Flyout, { label: item.label, icon: item.icon, active, children }, item.id);
|
|
794
895
|
}
|
|
795
|
-
const brandBlock = brand && /* @__PURE__ */
|
|
896
|
+
const brandBlock = brand && /* @__PURE__ */ jsxs4(
|
|
796
897
|
"a",
|
|
797
898
|
{
|
|
798
899
|
href: brand.href,
|
|
@@ -817,7 +918,7 @@ function AppSidebar({
|
|
|
817
918
|
}
|
|
818
919
|
) });
|
|
819
920
|
}
|
|
820
|
-
return /* @__PURE__ */
|
|
921
|
+
return /* @__PURE__ */ jsxs4(Fragment2, { children: [
|
|
821
922
|
!isDesktop && /* @__PURE__ */ jsx4(
|
|
822
923
|
"button",
|
|
823
924
|
{
|
|
@@ -828,11 +929,12 @@ function AppSidebar({
|
|
|
828
929
|
"aria-expanded": mobileOpen,
|
|
829
930
|
"aria-controls": drawerId,
|
|
830
931
|
onClick: () => setMobileOpen((v) => !v),
|
|
932
|
+
style: TOUCH_TARGET_STYLE,
|
|
831
933
|
className: "flex items-center justify-center rounded-md border border-border p-2 text-foreground",
|
|
832
934
|
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
935
|
}
|
|
834
936
|
),
|
|
835
|
-
!isDesktop && mobileOpen && /* @__PURE__ */
|
|
937
|
+
!isDesktop && mobileOpen && /* @__PURE__ */ jsxs4("div", { className: "fixed inset-0 z-50", children: [
|
|
836
938
|
/* @__PURE__ */ jsx4(
|
|
837
939
|
"div",
|
|
838
940
|
{
|
|
@@ -842,7 +944,7 @@ function AppSidebar({
|
|
|
842
944
|
onClick: () => setMobileOpen(false)
|
|
843
945
|
}
|
|
844
946
|
),
|
|
845
|
-
/* @__PURE__ */
|
|
947
|
+
/* @__PURE__ */ jsxs4(
|
|
846
948
|
"div",
|
|
847
949
|
{
|
|
848
950
|
ref: mobileDrawerRef,
|
|
@@ -861,6 +963,7 @@ function AppSidebar({
|
|
|
861
963
|
"data-testid": "app-sidebar-mobile-close",
|
|
862
964
|
"aria-label": `Close ${menuLabel.toLowerCase()}`,
|
|
863
965
|
onClick: () => setMobileOpen(false),
|
|
966
|
+
style: TOUCH_TARGET_STYLE,
|
|
864
967
|
className: "flex items-center justify-center rounded-md p-2 text-foreground hover:bg-foreground/5",
|
|
865
968
|
children: /* @__PURE__ */ jsx4(X2, { className: "h-5 w-5", "aria-hidden": "true" })
|
|
866
969
|
}
|
|
@@ -874,7 +977,7 @@ function AppSidebar({
|
|
|
874
977
|
}
|
|
875
978
|
)
|
|
876
979
|
] }),
|
|
877
|
-
/* @__PURE__ */
|
|
980
|
+
/* @__PURE__ */ jsxs4(
|
|
878
981
|
"aside",
|
|
879
982
|
{
|
|
880
983
|
"data-testid": "app-sidebar-desktop",
|
|
@@ -896,15 +999,197 @@ function AppSidebar({
|
|
|
896
999
|
)
|
|
897
1000
|
] });
|
|
898
1001
|
}
|
|
1002
|
+
|
|
1003
|
+
// src/context-switcher.tsx
|
|
1004
|
+
import { ChevronDown, Check } from "lucide-react";
|
|
1005
|
+
|
|
1006
|
+
// src/lib/group-by-org.ts
|
|
1007
|
+
function groupItemsByOrg(rows, get) {
|
|
1008
|
+
const byOrg = /* @__PURE__ */ new Map();
|
|
1009
|
+
for (const row of rows) {
|
|
1010
|
+
const key = get.orgId(row) ?? "";
|
|
1011
|
+
let group = byOrg.get(key);
|
|
1012
|
+
if (!group) {
|
|
1013
|
+
group = {
|
|
1014
|
+
orgId: get.orgId(row) ?? null,
|
|
1015
|
+
orgName: get.orgName(row) ?? null,
|
|
1016
|
+
items: []
|
|
1017
|
+
};
|
|
1018
|
+
byOrg.set(key, group);
|
|
1019
|
+
}
|
|
1020
|
+
group.items.push(row);
|
|
1021
|
+
}
|
|
1022
|
+
const groups = [...byOrg.values()];
|
|
1023
|
+
for (const g of groups) {
|
|
1024
|
+
g.items.sort((a, b) => get.name(a).localeCompare(get.name(b)));
|
|
1025
|
+
}
|
|
1026
|
+
groups.sort((a, b) => (a.orgName ?? "").localeCompare(b.orgName ?? ""));
|
|
1027
|
+
return groups;
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
// src/lib/dropdown-menu.tsx
|
|
1031
|
+
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
|
|
1032
|
+
import { jsx as jsx5 } from "react/jsx-runtime";
|
|
1033
|
+
function DropdownMenu(props) {
|
|
1034
|
+
return /* @__PURE__ */ jsx5(DropdownMenuPrimitive.Root, { "data-slot": "dropdown-menu", ...props });
|
|
1035
|
+
}
|
|
1036
|
+
function DropdownMenuTrigger(props) {
|
|
1037
|
+
return /* @__PURE__ */ jsx5(DropdownMenuPrimitive.Trigger, { "data-slot": "dropdown-menu-trigger", ...props });
|
|
1038
|
+
}
|
|
1039
|
+
function DropdownMenuContent({
|
|
1040
|
+
className,
|
|
1041
|
+
sideOffset = 4,
|
|
1042
|
+
...props
|
|
1043
|
+
}) {
|
|
1044
|
+
return /* @__PURE__ */ jsx5(DropdownMenuPrimitive.Portal, { children: /* @__PURE__ */ jsx5(
|
|
1045
|
+
DropdownMenuPrimitive.Content,
|
|
1046
|
+
{
|
|
1047
|
+
"data-slot": "dropdown-menu-content",
|
|
1048
|
+
sideOffset,
|
|
1049
|
+
className: cn(
|
|
1050
|
+
"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",
|
|
1051
|
+
className
|
|
1052
|
+
),
|
|
1053
|
+
...props
|
|
1054
|
+
}
|
|
1055
|
+
) });
|
|
1056
|
+
}
|
|
1057
|
+
function DropdownMenuItem({
|
|
1058
|
+
className,
|
|
1059
|
+
...props
|
|
1060
|
+
}) {
|
|
1061
|
+
return /* @__PURE__ */ jsx5(
|
|
1062
|
+
DropdownMenuPrimitive.Item,
|
|
1063
|
+
{
|
|
1064
|
+
"data-slot": "dropdown-menu-item",
|
|
1065
|
+
className: cn(
|
|
1066
|
+
"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",
|
|
1067
|
+
className
|
|
1068
|
+
),
|
|
1069
|
+
...props
|
|
1070
|
+
}
|
|
1071
|
+
);
|
|
1072
|
+
}
|
|
1073
|
+
function DropdownMenuLabel({
|
|
1074
|
+
className,
|
|
1075
|
+
...props
|
|
1076
|
+
}) {
|
|
1077
|
+
return /* @__PURE__ */ jsx5(
|
|
1078
|
+
DropdownMenuPrimitive.Label,
|
|
1079
|
+
{
|
|
1080
|
+
"data-slot": "dropdown-menu-label",
|
|
1081
|
+
className: cn("px-2 py-1.5 text-sm font-medium", className),
|
|
1082
|
+
...props
|
|
1083
|
+
}
|
|
1084
|
+
);
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
// src/context-switcher.tsx
|
|
1088
|
+
import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
1089
|
+
function groupByOrg(items) {
|
|
1090
|
+
return groupItemsByOrg(items, {
|
|
1091
|
+
orgId: (it) => it.orgId,
|
|
1092
|
+
orgName: (it) => it.orgName,
|
|
1093
|
+
name: (it) => it.name
|
|
1094
|
+
});
|
|
1095
|
+
}
|
|
1096
|
+
function toBrand(item) {
|
|
1097
|
+
return {
|
|
1098
|
+
name: item.name,
|
|
1099
|
+
favicon: item.favicon,
|
|
1100
|
+
logo: item.logo,
|
|
1101
|
+
color: item.color,
|
|
1102
|
+
accentColor: item.accentColor
|
|
1103
|
+
};
|
|
1104
|
+
}
|
|
1105
|
+
function ContextSwitcher({
|
|
1106
|
+
items,
|
|
1107
|
+
activeRefId,
|
|
1108
|
+
onSelect,
|
|
1109
|
+
footer,
|
|
1110
|
+
placeholder = "Select",
|
|
1111
|
+
collapsed = false,
|
|
1112
|
+
side = "bottom",
|
|
1113
|
+
align = "start",
|
|
1114
|
+
contentClassName
|
|
1115
|
+
}) {
|
|
1116
|
+
const active = items.find((it) => it.refId === activeRefId) ?? null;
|
|
1117
|
+
const groups = groupByOrg(items);
|
|
1118
|
+
const showOrgHeaders = groups.length > 1;
|
|
1119
|
+
return /* @__PURE__ */ jsxs5(DropdownMenu, { children: [
|
|
1120
|
+
/* @__PURE__ */ jsx6(DropdownMenuTrigger, { asChild: true, children: collapsed ? /* @__PURE__ */ jsx6(
|
|
1121
|
+
"button",
|
|
1122
|
+
{
|
|
1123
|
+
type: "button",
|
|
1124
|
+
title: active?.name || placeholder,
|
|
1125
|
+
"aria-label": active?.name || placeholder,
|
|
1126
|
+
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",
|
|
1127
|
+
children: /* @__PURE__ */ jsx6(BrandGlyph, { brand: toBrand(active ?? { name: "" }), size: "lg" })
|
|
1128
|
+
}
|
|
1129
|
+
) : /* @__PURE__ */ jsxs5(
|
|
1130
|
+
"button",
|
|
1131
|
+
{
|
|
1132
|
+
type: "button",
|
|
1133
|
+
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",
|
|
1134
|
+
children: [
|
|
1135
|
+
/* @__PURE__ */ jsx6(BrandGlyph, { brand: toBrand(active ?? { name: "" }) }),
|
|
1136
|
+
/* @__PURE__ */ jsx6("span", { className: "max-w-[140px] truncate text-foreground/90", children: active?.name || placeholder }),
|
|
1137
|
+
/* @__PURE__ */ jsx6(ChevronDown, { className: "h-3.5 w-3.5 text-muted-foreground transition-transform group-data-[state=open]:rotate-180" })
|
|
1138
|
+
]
|
|
1139
|
+
}
|
|
1140
|
+
) }),
|
|
1141
|
+
/* @__PURE__ */ jsxs5(
|
|
1142
|
+
DropdownMenuContent,
|
|
1143
|
+
{
|
|
1144
|
+
side,
|
|
1145
|
+
align,
|
|
1146
|
+
sideOffset: 6,
|
|
1147
|
+
collisionPadding: 8,
|
|
1148
|
+
className: cn("w-60 overflow-hidden p-0", contentClassName),
|
|
1149
|
+
children: [
|
|
1150
|
+
groups.map((group) => /* @__PURE__ */ jsxs5("div", { children: [
|
|
1151
|
+
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" }),
|
|
1152
|
+
/* @__PURE__ */ jsx6("div", { className: cn(showOrgHeaders && "ml-5 border-l border-border/60"), children: group.items.map((item) => {
|
|
1153
|
+
const isActive = item.refId === activeRefId;
|
|
1154
|
+
return /* @__PURE__ */ jsxs5(
|
|
1155
|
+
DropdownMenuItem,
|
|
1156
|
+
{
|
|
1157
|
+
"data-active": isActive || void 0,
|
|
1158
|
+
onSelect: () => onSelect(item),
|
|
1159
|
+
className: "flex w-full items-center gap-2.5 rounded-none px-3 py-2 text-sm",
|
|
1160
|
+
children: [
|
|
1161
|
+
/* @__PURE__ */ jsx6(BrandGlyph, { brand: toBrand(item), subtle: true }),
|
|
1162
|
+
/* @__PURE__ */ jsx6("span", { className: "flex-1 truncate text-left text-foreground/90", children: item.name }),
|
|
1163
|
+
isActive && /* @__PURE__ */ jsx6(Check, { className: "size-3.5 text-emerald-500" })
|
|
1164
|
+
]
|
|
1165
|
+
},
|
|
1166
|
+
item.refId
|
|
1167
|
+
);
|
|
1168
|
+
}) })
|
|
1169
|
+
] }, group.orgId ?? "unattributed")),
|
|
1170
|
+
footer && /* @__PURE__ */ jsx6("div", { className: "sticky bottom-0 border-t border-border bg-popover", children: footer })
|
|
1171
|
+
]
|
|
1172
|
+
}
|
|
1173
|
+
)
|
|
1174
|
+
] });
|
|
1175
|
+
}
|
|
899
1176
|
export {
|
|
900
1177
|
AppSidebar,
|
|
901
1178
|
BrandGlyph,
|
|
1179
|
+
ContextSwitcher,
|
|
902
1180
|
Flyout,
|
|
1181
|
+
GLYPH_CANVAS_FILL,
|
|
1182
|
+
LOCKUP_GAP,
|
|
1183
|
+
LOCKUP_ROW,
|
|
1184
|
+
MARK_CAP_RATIO,
|
|
1185
|
+
REEVE_ACCENT,
|
|
903
1186
|
ResponsiveHeader,
|
|
904
1187
|
deriveFallbackLadder,
|
|
905
1188
|
faviconUrl,
|
|
1189
|
+
groupByOrg,
|
|
906
1190
|
reconcileLadder,
|
|
907
1191
|
resolveLadder,
|
|
908
1192
|
useLadderRung,
|
|
1193
|
+
useResponsivePlaceholder,
|
|
909
1194
|
zSidebarLadder
|
|
910
1195
|
};
|