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