@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.mjs CHANGED
@@ -24,10 +24,9 @@ function cn(...inputs) {
24
24
  }
25
25
 
26
26
  // src/lib/initials.ts
27
- function initials(name) {
28
- if (!name) return "\xB7\xB7";
29
- const parts = name.trim().split(/\s+/).slice(0, 2);
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
- return /* @__PURE__ */ jsx(
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-center justify-center rounded font-semibold uppercase text-white",
109
- size === "lg" ? "text-[11px]" : "text-[9px]",
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: { backgroundColor: brand.color || monogramColor(brand.name || "?") },
114
- children: initials(brand.name)
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 useState2 } from "react";
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-media-query.ts
124
- import { useCallback, useSyncExternalStore } from "react";
125
- function useMediaQuery(query) {
126
- const subscribe = useCallback(
127
- (onChange) => {
128
- if (typeof window === "undefined" || typeof window.matchMedia !== "function") {
129
- return () => {
130
- };
131
- }
132
- const mql = window.matchMedia(query);
133
- mql.addEventListener("change", onChange);
134
- return () => mql.removeEventListener("change", onChange);
135
- },
136
- [query]
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
- return /* @__PURE__ */ jsxs(Fragment, { children: [
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
- "a",
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
- ) : /* @__PURE__ */ jsx2(
185
- "button",
186
- {
187
- type: "button",
188
- onClick: item.onSelect,
189
- className: "text-left text-sm font-medium text-foreground/80 hover:text-foreground",
190
- children: item.label
191
- },
192
- item.label
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 isDesktop = useMediaQuery(`(min-width: ${BREAKPOINTS.md}px)`);
210
- const [open, setOpen] = useState2(false);
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
- useEffect(() => {
215
- if (isDesktop) setOpen(false);
216
- }, [isDesktop]);
217
- useEffect(() => {
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
- return /* @__PURE__ */ jsxs(
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
- "flex w-full items-center gap-3 border-b border-border bg-background px-4 py-2",
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__ */ jsxs("div", { "data-testid": "header-brand", className: "flex shrink-0 items-center gap-2", children: [
249
- /* @__PURE__ */ jsx2(BrandGlyph, { brand, size: "lg" }),
250
- isDesktop && /* @__PURE__ */ jsx2(
251
- "span",
252
- {
253
- "data-testid": "header-wordmark",
254
- className: "truncate text-sm font-semibold text-foreground",
255
- style: { maxWidth: MIN_SIZES.wordmarkMaxWidth },
256
- children: brand.name
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 && (isDesktop ? /* @__PURE__ */ jsx2("nav", { "data-testid": "header-actions", className: "flex shrink-0 items-center gap-3", children: /* @__PURE__ */ jsx2(SecondaryControls, { nav, auth, themeToggle }) }) : /* @__PURE__ */ jsxs("div", { className: "relative shrink-0", children: [
303
+ hasSecondary && /* @__PURE__ */ jsxs2(Fragment, { children: [
270
304
  /* @__PURE__ */ jsx2(
271
- "button",
305
+ "nav",
272
306
  {
273
- ref: buttonRef,
274
- type: "button",
275
- "data-testid": "header-menu-button",
276
- "aria-label": menuLabel,
277
- "aria-expanded": open,
278
- "aria-controls": panelId,
279
- onClick: () => setOpen((v) => !v),
280
- className: "flex items-center justify-center rounded-md border border-border text-foreground",
281
- style: { minWidth: MIN_SIZES.touchTarget, minHeight: MIN_SIZES.touchTarget },
282
- children: /* @__PURE__ */ jsx2(MenuIcon, { open })
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
- open && /* @__PURE__ */ jsx2(
286
- "div",
287
- {
288
- ref: panelRef,
289
- id: panelId,
290
- "data-testid": "header-menu-panel",
291
- 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",
292
- children: /* @__PURE__ */ jsx2(SecondaryControls, { nav, auth, themeToggle })
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 useEffect4, useId as useId3, useMemo, useRef as useRef4, useState as useState5 } from "react";
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 useEffect2 } from "react";
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
- useEffect2(() => {
416
+ useEffect3(() => {
332
417
  if (open) initialFocusRef?.current?.focus();
333
418
  }, [open]);
334
- useEffect2(() => {
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 useState3 } from "react";
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] = useState3(0);
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 useEffect3, useId as useId2, useLayoutEffect as useLayoutEffect2, useRef as useRef3, useState as useState4 } from "react";
583
- import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
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] = useState4(false);
587
- const [clickOpen, setClickOpen] = useState4(false);
588
- const [panelPosition, setPanelPosition] = useState4(null);
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
- useEffect3(() => {
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
- useEffect3(() => {
706
+ useEffect4(() => {
622
707
  if (clickOpen && panelPosition) firstItemRef.current?.focus();
623
708
  }, [clickOpen, panelPosition]);
624
- return /* @__PURE__ */ jsxs2(
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__ */ jsxs2(
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 jsxs3 } from "react/jsx-runtime";
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__ */ jsxs3(
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] = useState5(false);
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
- useEffect4(() => {
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__ */ jsxs3(
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__ */ jsxs3(Fragment2, { children: [
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__ */ jsxs3("div", { className: "fixed inset-0 z-50", children: [
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__ */ jsxs3(
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__ */ jsxs3(
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
  };