@platned/ui 0.6.1 → 0.7.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/sidebar.js CHANGED
@@ -1,6 +1,57 @@
1
1
  // src/sidebar/index.tsx
2
- import { NavLink as NavLink2 } from "react-router-dom";
3
- import { LuChevronLeft, LuChevronRight, LuEllipsisVertical, LuLogOut } from "react-icons/lu";
2
+ import { createContext as createContext4, useContext as useContext4, useMemo as useMemo2 } from "react";
3
+ import { Link, useLocation } from "react-router-dom";
4
+ import { ChevronLeftIcon, ChevronRightIcon as ChevronRightIcon2, EllipsisVerticalIcon, LogOutIcon } from "lucide-react";
5
+
6
+ // src/lib/cn.ts
7
+ import { clsx } from "clsx";
8
+ import { extendTailwindMerge } from "tailwind-merge";
9
+ var FONT_SIZES = [
10
+ "body-micro",
11
+ "body-tiny",
12
+ "body-xs",
13
+ "body-sm",
14
+ "body",
15
+ "body-lg",
16
+ "heading-sm",
17
+ "heading",
18
+ "heading-lg",
19
+ "subheading-sm",
20
+ "subheading",
21
+ "subheading-lg",
22
+ "subtitle-sm",
23
+ "subtitle",
24
+ "subtitle-lg",
25
+ "title-sm",
26
+ "title",
27
+ "title-lg",
28
+ "title-hero",
29
+ "scale-01",
30
+ "scale-02",
31
+ "scale-03",
32
+ "scale-04",
33
+ "scale-05",
34
+ "scale-06",
35
+ "scale-07",
36
+ "scale-08",
37
+ "scale-09",
38
+ "scale-10",
39
+ "scale-12"
40
+ ];
41
+ var twMerge = extendTailwindMerge({
42
+ extend: {
43
+ classGroups: {
44
+ "font-size": [{ text: [...FONT_SIZES] }]
45
+ }
46
+ }
47
+ });
48
+ function cn(...inputs) {
49
+ return twMerge(clsx(inputs));
50
+ }
51
+
52
+ // src/lib/focus-ring.ts
53
+ var FOCUS_RING = "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand focus-visible:ring-offset-1 focus-visible:ring-offset-default";
54
+ var FOCUS_RING_INSET = "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-brand";
4
55
 
5
56
  // src/lib/slot.tsx
6
57
  import {
@@ -8,7 +59,7 @@ import {
8
59
  forwardRef,
9
60
  isValidElement
10
61
  } from "react";
11
- import clsx from "clsx";
62
+ import clsx2 from "clsx";
12
63
  var Slot = forwardRef(function Slot2({ children, wrapChildren, ...slotProps }, forwardedRef) {
13
64
  if (!isValidElement(children)) return null;
14
65
  const child = children;
@@ -33,7 +84,7 @@ function mergeProps(slotProps, childProps) {
33
84
  merged[key] = childValue ?? slotValue;
34
85
  }
35
86
  } else if (key === "className") {
36
- merged[key] = clsx(slotValue, childValue);
87
+ merged[key] = clsx2(slotValue, childValue);
37
88
  } else if (key === "style") {
38
89
  merged[key] = { ...slotValue, ...childValue };
39
90
  } else {
@@ -70,27 +121,30 @@ function useControlSize(explicit) {
70
121
 
71
122
  // src/components/forms/icon-button.tsx
72
123
  import { forwardRef as forwardRef2 } from "react";
73
- import clsx2 from "clsx";
124
+ import clsx3 from "clsx";
74
125
  import { jsx as jsx2 } from "react/jsx-runtime";
75
126
  var variantStyles = {
76
- primary: clsx2(
127
+ primary: clsx3(
77
128
  "bg-brand border border-brand text-brand-on-brand",
78
129
  "hover:bg-brand-hover",
79
- "disabled:bg-disabled disabled:border-disabled disabled:text-disabled-on-disabled"
130
+ "disabled:bg-disabled disabled:border-disabled disabled:text-disabled-on-disabled",
131
+ "aria-disabled:bg-disabled aria-disabled:border-disabled aria-disabled:text-disabled-on-disabled"
80
132
  ),
81
133
  // Neutral uses secondary background (light gray) for visual weight without a text label
82
- neutral: clsx2(
134
+ neutral: clsx3(
83
135
  "bg-default-secondary border border-default text-default",
84
136
  // icon turns brand blue on hover (Figma spec)
85
137
  "hover:bg-default-secondary-hover hover:text-brand",
86
- "disabled:bg-disabled disabled:border-disabled disabled:text-disabled-on-disabled"
138
+ "disabled:bg-disabled disabled:border-disabled disabled:text-disabled-on-disabled",
139
+ "aria-disabled:bg-disabled aria-disabled:border-disabled aria-disabled:text-disabled-on-disabled"
87
140
  ),
88
- subtle: clsx2(
141
+ subtle: clsx3(
89
142
  "bg-transparent text-neutral",
90
143
  // hover uses background-default-default-hover (#f5f5f5), icon turns brand blue (Figma spec)
91
144
  "hover:bg-default-hover hover:text-brand",
92
145
  // disabled needs explicit border since there is no base border
93
- "disabled:bg-disabled disabled:border disabled:border-disabled disabled:text-disabled-on-disabled"
146
+ "disabled:bg-disabled disabled:border disabled:border-disabled disabled:text-disabled-on-disabled",
147
+ "aria-disabled:bg-disabled aria-disabled:border aria-disabled:border-disabled aria-disabled:text-disabled-on-disabled"
94
148
  )
95
149
  };
96
150
  var sizeStyles = {
@@ -98,27 +152,52 @@ var sizeStyles = {
98
152
  md: "size-9 rounded-lg [&_svg]:size-5",
99
153
  sm: "size-6 rounded-full [&_svg]:size-4"
100
154
  };
101
- var IconButton = forwardRef2(function IconButton2({ variant = "primary", size: sizeProp, asChild = false, type, children, className, ...props }, ref) {
102
- const size2 = useControlSize(sizeProp);
155
+ var IconButton = forwardRef2(function IconButton2({
156
+ variant = "primary",
157
+ size: sizeProp,
158
+ asChild = false,
159
+ type,
160
+ disabled,
161
+ onClick,
162
+ children,
163
+ className,
164
+ ...props
165
+ }, ref) {
166
+ const size = useControlSize(sizeProp);
167
+ const softDisabled = asChild && Boolean(disabled);
168
+ const handleClick = (event) => {
169
+ if (softDisabled) {
170
+ event.preventDefault();
171
+ event.stopPropagation();
172
+ return;
173
+ }
174
+ onClick?.(event);
175
+ };
103
176
  const rootProps = {
104
- className: clsx2(
177
+ // `cn`, so a caller's `className` beats the size's radius and the variant's colours.
178
+ className: cn(
105
179
  "inline-flex shrink-0 items-center justify-center transition-colors",
106
- "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand focus-visible:ring-offset-1 focus-visible:ring-offset-default",
107
- "disabled:cursor-not-allowed",
180
+ FOCUS_RING,
181
+ "disabled:cursor-not-allowed aria-disabled:cursor-not-allowed",
108
182
  variantStyles[variant],
109
- sizeStyles[size2],
183
+ sizeStyles[size],
110
184
  className
111
185
  ),
186
+ "data-variant": variant,
187
+ "data-size": size,
188
+ "aria-disabled": softDisabled || void 0,
189
+ onClick: handleClick,
112
190
  ...props
113
191
  };
114
192
  if (asChild) {
115
- return /* @__PURE__ */ jsx2(Slot, { ref, ...rootProps, children });
193
+ return /* @__PURE__ */ jsx2(Slot, { ref, tabIndex: disabled ? -1 : void 0, ...rootProps, children });
116
194
  }
117
195
  return /* @__PURE__ */ jsx2(
118
196
  "button",
119
197
  {
120
198
  ref,
121
199
  type: type ?? "button",
200
+ disabled,
122
201
  ...rootProps,
123
202
  children
124
203
  }
@@ -126,7 +205,7 @@ var IconButton = forwardRef2(function IconButton2({ variant = "primary", size: s
126
205
  });
127
206
 
128
207
  // src/components/avatar/avatar.tsx
129
- import clsx3 from "clsx";
208
+ import clsx4 from "clsx";
130
209
  import { jsx as jsx3 } from "react/jsx-runtime";
131
210
  var sizeStyles2 = {
132
211
  lg: "size-10",
@@ -138,16 +217,16 @@ var initialTextStyles = {
138
217
  md: "text-body-xs font-semibold",
139
218
  sm: "text-body-tiny font-semibold"
140
219
  };
141
- function getShapeClass(shape, size2) {
220
+ function getShapeClass(shape, size) {
142
221
  if (shape === "circle") return "rounded-full";
143
- return size2 === "sm" ? "rounded" : "rounded-lg";
222
+ return size === "sm" ? "rounded" : "rounded-lg";
144
223
  }
145
224
  function Avatar(props) {
146
- const { size: size2 = "lg", shape = "circle", className } = props;
147
- const base = clsx3(
225
+ const { size = "lg", shape = "circle", className } = props;
226
+ const base = clsx4(
148
227
  "shrink-0 overflow-hidden",
149
- sizeStyles2[size2],
150
- getShapeClass(shape, size2),
228
+ sizeStyles2[size],
229
+ getShapeClass(shape, size),
151
230
  className
152
231
  );
153
232
  if (props.type === "image") {
@@ -163,16 +242,16 @@ function Avatar(props) {
163
242
  return /* @__PURE__ */ jsx3(
164
243
  "div",
165
244
  {
166
- className: clsx3(base, "flex items-center justify-center"),
245
+ className: clsx4(base, "flex items-center justify-center"),
167
246
  style: {
168
247
  background: "linear-gradient(135deg, var(--color-graphic-blue) 30%, var(--color-graphic-red) 100%)"
169
248
  },
170
249
  children: /* @__PURE__ */ jsx3(
171
250
  "span",
172
251
  {
173
- className: clsx3(
252
+ className: clsx4(
174
253
  "select-none leading-none text-brand-on-brand",
175
- initialTextStyles[size2]
254
+ initialTextStyles[size]
176
255
  ),
177
256
  children: props.initial.charAt(0).toUpperCase()
178
257
  }
@@ -181,52 +260,6 @@ function Avatar(props) {
181
260
  );
182
261
  }
183
262
 
184
- // src/lib/cn.ts
185
- import { clsx as clsx4 } from "clsx";
186
- import { extendTailwindMerge } from "tailwind-merge";
187
- var FONT_SIZES = [
188
- "body-micro",
189
- "body-tiny",
190
- "body-xs",
191
- "body-sm",
192
- "body",
193
- "body-lg",
194
- "heading-sm",
195
- "heading",
196
- "heading-lg",
197
- "subheading-sm",
198
- "subheading",
199
- "subheading-lg",
200
- "subtitle-sm",
201
- "subtitle",
202
- "subtitle-lg",
203
- "title-sm",
204
- "title",
205
- "title-lg",
206
- "title-hero",
207
- "scale-01",
208
- "scale-02",
209
- "scale-03",
210
- "scale-04",
211
- "scale-05",
212
- "scale-06",
213
- "scale-07",
214
- "scale-08",
215
- "scale-09",
216
- "scale-10",
217
- "scale-12"
218
- ];
219
- var twMerge = extendTailwindMerge({
220
- extend: {
221
- classGroups: {
222
- "font-size": [{ text: [...FONT_SIZES] }]
223
- }
224
- }
225
- });
226
- function cn(...inputs) {
227
- return twMerge(clsx4(inputs));
228
- }
229
-
230
263
  // src/lib/use-controllable-state.ts
231
264
  import { useCallback, useRef, useState } from "react";
232
265
  function useControllableState({
@@ -252,197 +285,6 @@ function useControllableState({
252
285
  return [current, setValue];
253
286
  }
254
287
 
255
- // src/lib/use-roving-focus.ts
256
- import { useCallback as useCallback2, useEffect, useRef as useRef2, useState as useState2 } from "react";
257
- function useRovingFocus({
258
- orientation = "horizontal",
259
- loop = true,
260
- typeahead = false,
261
- defaultIndex = 0,
262
- onEscape
263
- } = {}) {
264
- const [activeIndex, setActiveIndex] = useState2(defaultIndex);
265
- const items = useRef2([]);
266
- const meta = useRef2([]);
267
- const search = useRef2("");
268
- const searchTimer = useRef2(null);
269
- useEffect(
270
- () => () => {
271
- if (searchTimer.current) clearTimeout(searchTimer.current);
272
- },
273
- []
274
- );
275
- const focusIndex = useCallback2((index) => {
276
- const node = items.current[index];
277
- if (!node) return;
278
- setActiveIndex(index);
279
- node.focus();
280
- }, []);
281
- const move = useCallback2(
282
- (start, step) => {
283
- const total = items.current.length;
284
- if (total === 0) return;
285
- for (let i = 1; i <= total; i++) {
286
- let next = start + step * i;
287
- if (loop) {
288
- next = (next % total + total) % total;
289
- } else if (next < 0 || next >= total) {
290
- return;
291
- }
292
- if (items.current[next] && !meta.current[next]?.disabled) {
293
- focusIndex(next);
294
- return;
295
- }
296
- }
297
- },
298
- [loop, focusIndex]
299
- );
300
- const moveToEdge = useCallback2(
301
- (from) => {
302
- const total = items.current.length;
303
- if (total === 0) return;
304
- move(from === "start" ? -1 : total, from === "start" ? 1 : -1);
305
- },
306
- [move]
307
- );
308
- const runTypeahead = useCallback2(
309
- (char) => {
310
- if (searchTimer.current) clearTimeout(searchTimer.current);
311
- search.current += char.toLowerCase();
312
- searchTimer.current = setTimeout(() => {
313
- search.current = "";
314
- }, 500);
315
- const total = items.current.length;
316
- const query = search.current;
317
- for (let i = 1; i <= total; i++) {
318
- const index = (activeIndex + i) % total;
319
- if (meta.current[index]?.disabled) continue;
320
- const label = meta.current[index]?.label?.toLowerCase();
321
- if (label?.startsWith(query)) {
322
- focusIndex(index);
323
- return;
324
- }
325
- }
326
- },
327
- [activeIndex, focusIndex]
328
- );
329
- const onKeyDown = useCallback2(
330
- (event) => {
331
- const { key } = event;
332
- const vertical = orientation === "vertical" || orientation === "both";
333
- const horizontal = orientation === "horizontal" || orientation === "both";
334
- const next = vertical && key === "ArrowDown" || horizontal && key === "ArrowRight";
335
- const prev = vertical && key === "ArrowUp" || horizontal && key === "ArrowLeft";
336
- if (next || prev) {
337
- event.preventDefault();
338
- move(activeIndex, next ? 1 : -1);
339
- return;
340
- }
341
- if (key === "Home") {
342
- event.preventDefault();
343
- moveToEdge("start");
344
- return;
345
- }
346
- if (key === "End") {
347
- event.preventDefault();
348
- moveToEdge("end");
349
- return;
350
- }
351
- if (key === "Escape" && onEscape) {
352
- onEscape();
353
- return;
354
- }
355
- if (typeahead && key.length === 1 && key !== " " && !event.ctrlKey && !event.metaKey && !event.altKey) {
356
- event.preventDefault();
357
- runTypeahead(key);
358
- }
359
- },
360
- [orientation, activeIndex, move, moveToEdge, onEscape, typeahead, runTypeahead]
361
- );
362
- const itemProps = useCallback2(
363
- (index, options = {}) => {
364
- meta.current[index] = options;
365
- return {
366
- ref: (node) => {
367
- items.current[index] = node;
368
- },
369
- // The roving part: exactly one item is in the tab sequence, so Tab enters the
370
- // composite once and Tab again leaves it.
371
- tabIndex: index === activeIndex ? 0 : -1,
372
- onFocus: () => setActiveIndex(index),
373
- "data-roving-index": index
374
- };
375
- },
376
- [activeIndex]
377
- );
378
- return {
379
- activeIndex,
380
- setActiveIndex,
381
- focusIndex,
382
- moveToEdge,
383
- itemProps,
384
- containerProps: { onKeyDown }
385
- };
386
- }
387
-
388
- // src/components/overlays/floating.tsx
389
- import { useRef as useRef3 } from "react";
390
- import {
391
- arrow,
392
- autoUpdate,
393
- flip,
394
- offset,
395
- shift,
396
- size,
397
- useFloating
398
- } from "@floating-ui/react";
399
- function useFloatingPanel({
400
- open,
401
- onOpenChange,
402
- side = "bottom",
403
- align = "center",
404
- sideOffset = 6,
405
- alignOffset = 0,
406
- collisionPadding = 8,
407
- withArrow = false,
408
- matchAnchorWidth = false
409
- }) {
410
- const arrowRef = useRef3(null);
411
- const placement = align === "center" ? side : `${side}-${align}`;
412
- const floating = useFloating({
413
- open,
414
- onOpenChange,
415
- placement,
416
- // Only track while the panel is actually on screen. `autoUpdate` attaches scroll and
417
- // resize listeners plus a ResizeObserver, so leaving it running on a closed panel
418
- // would cost every consumer a listener per menu on the page.
419
- whileElementsMounted: autoUpdate,
420
- middleware: [
421
- offset({ mainAxis: sideOffset, crossAxis: alignOffset }),
422
- // `flip` before `shift`: try the opposite edge first, and only slide along the
423
- // current one if neither edge fits. The reverse order produces panels that cling to
424
- // the viewport edge when they had room to simply flip.
425
- flip({ padding: collisionPadding }),
426
- shift({ padding: collisionPadding }),
427
- matchAnchorWidth && size({
428
- padding: collisionPadding,
429
- apply({ rects, elements, availableHeight }) {
430
- elements.floating.style.setProperty(
431
- "--panel-anchor-width",
432
- `${rects.reference.width}px`
433
- );
434
- elements.floating.style.setProperty(
435
- "--panel-available-height",
436
- `${availableHeight}px`
437
- );
438
- }
439
- }),
440
- withArrow && arrow({ element: arrowRef, padding: 4 })
441
- ].filter(Boolean)
442
- });
443
- return { ...floating, arrowRef };
444
- }
445
-
446
288
  // src/components/overlays/menu-styles.ts
447
289
  import clsx6 from "clsx";
448
290
 
@@ -474,256 +316,30 @@ var OVERLAY_ITEM_SIZE = {
474
316
  md: "px-2.5 py-2 text-body-sm",
475
317
  sm: "px-2 py-1.5 text-body-tiny"
476
318
  };
477
- function overlayItemTone(tone, disabled) {
478
- if (disabled) return "cursor-not-allowed text-disabled";
479
- return tone === "danger" ? "text-danger hover:bg-danger-tertiary focus:bg-danger-tertiary" : "text-default hover:bg-default-secondary focus:bg-default-secondary";
480
- }
481
-
482
- // src/components/overlays/overlay.ts
483
- import { useEffect as useEffect3, useRef as useRef5 } from "react";
484
- import { tabbable } from "tabbable";
485
-
486
- // src/components/overlays/overlay-stack.ts
487
- import { useEffect as useEffect2, useRef as useRef4, useState as useState3 } from "react";
488
- var layers = [];
489
- var listeners = /* @__PURE__ */ new Set();
490
- function notify() {
491
- for (const listener of listeners) listener();
492
- }
493
- function topmost() {
494
- return layers[layers.length - 1]?.id;
495
- }
496
- var scrollLocks = 0;
497
- var restoreBody = null;
498
- function lockScroll() {
499
- scrollLocks += 1;
500
- if (scrollLocks > 1 || typeof document === "undefined") return;
501
- const { body, documentElement } = document;
502
- const previousOverflow = body.style.overflow;
503
- const previousPaddingRight = body.style.paddingRight;
504
- const scrollbarWidth = window.innerWidth - documentElement.clientWidth;
505
- if (scrollbarWidth > 0) {
506
- const current = parseFloat(window.getComputedStyle(body).paddingRight) || 0;
507
- body.style.paddingRight = `${current + scrollbarWidth}px`;
508
- }
509
- body.style.overflow = "hidden";
510
- restoreBody = () => {
511
- body.style.overflow = previousOverflow;
512
- body.style.paddingRight = previousPaddingRight;
513
- };
514
- }
515
- function unlockScroll() {
516
- scrollLocks = Math.max(0, scrollLocks - 1);
517
- if (scrollLocks === 0 && restoreBody) {
518
- restoreBody();
519
- restoreBody = null;
520
- }
521
- }
522
- var inerted = /* @__PURE__ */ new Set();
523
- function applyInert(node) {
524
- if (typeof document === "undefined" || !node) return;
525
- let root = node;
526
- while (root.parentElement && root.parentElement !== document.body) {
527
- root = root.parentElement;
528
- }
529
- for (const child of Array.from(document.body.children)) {
530
- if (child === root || !(child instanceof HTMLElement)) continue;
531
- if (child.hasAttribute("inert")) continue;
532
- child.setAttribute("inert", "");
533
- inerted.add(child);
534
- }
535
- }
536
- function releaseInert() {
537
- for (const el of inerted) el.removeAttribute("inert");
538
- inerted.clear();
539
- }
540
- function useOverlayLayer(active, {
541
- lockScroll: shouldLockScroll = true,
542
- inertBackground = true,
543
- panelRef
544
- } = {}) {
545
- const id = useRef4();
546
- if (!id.current) id.current = /* @__PURE__ */ Symbol("overlay-layer");
547
- const [isTopmost, setIsTopmost] = useState3(false);
548
- useEffect2(() => {
549
- if (!active) return;
550
- const self = id.current;
551
- layers.push({ id: self });
552
- const sync = () => setIsTopmost(topmost() === self);
553
- listeners.add(sync);
554
- sync();
555
- notify();
556
- if (shouldLockScroll) lockScroll();
557
- return () => {
558
- const index = layers.findIndex((layer) => layer.id === self);
559
- if (index !== -1) layers.splice(index, 1);
560
- listeners.delete(sync);
561
- notify();
562
- if (shouldLockScroll) unlockScroll();
563
- };
564
- }, [active, shouldLockScroll]);
565
- useEffect2(() => {
566
- if (!active || !inertBackground) return;
567
- if (inerted.size === 0) applyInert(panelRef?.current ?? null);
568
- return () => {
569
- if (layers.length <= 1) releaseInert();
570
- };
571
- }, [active, inertBackground, panelRef]);
572
- return { isTopmost };
573
- }
574
-
575
- // src/components/overlays/overlay.ts
576
- function useOverlayDismiss({
577
- onDismiss,
578
- panelRef,
579
- active = true,
580
- closeOnEscape = true,
581
- closeOnOutsidePress = true,
582
- trapFocus = true,
583
- lockScroll: lockScroll2 = true,
584
- inertBackground = true,
585
- autoFocus = true,
586
- initialFocus,
587
- returnFocus,
588
- onEscapeKeyDown,
589
- onInteractOutside
590
- }) {
591
- const { isTopmost } = useOverlayLayer(active, {
592
- lockScroll: lockScroll2,
593
- inertBackground,
594
- panelRef
595
- });
596
- const handlers = useRef5({ onDismiss, onEscapeKeyDown, onInteractOutside });
597
- handlers.current = { onDismiss, onEscapeKeyDown, onInteractOutside };
598
- const isTopmostRef = useRef5(isTopmost);
599
- isTopmostRef.current = isTopmost;
600
- useEffect3(() => {
601
- if (!active || !closeOnEscape) return;
602
- function onKeyDown(event) {
603
- if (event.key !== "Escape") return;
604
- if (!isTopmostRef.current) return;
605
- handlers.current.onEscapeKeyDown?.(event);
606
- if (event.defaultPrevented) return;
607
- handlers.current.onDismiss();
608
- }
609
- document.addEventListener("keydown", onKeyDown);
610
- return () => document.removeEventListener("keydown", onKeyDown);
611
- }, [active, closeOnEscape]);
612
- useEffect3(() => {
613
- if (!active || !closeOnOutsidePress) return;
614
- function onPointerDown(event) {
615
- if (!isTopmostRef.current) return;
616
- const panel = panelRef.current;
617
- if (!panel || panel.contains(event.target)) return;
618
- handlers.current.onInteractOutside?.(event);
619
- if (event.defaultPrevented) return;
620
- handlers.current.onDismiss();
621
- }
622
- document.addEventListener("pointerdown", onPointerDown);
623
- return () => document.removeEventListener("pointerdown", onPointerDown);
624
- }, [active, closeOnOutsidePress, panelRef]);
625
- useEffect3(() => {
626
- if (!active || !trapFocus) return;
627
- function onKeyDown(event) {
628
- if (event.key !== "Tab" || !isTopmostRef.current) return;
629
- const panel = panelRef.current;
630
- if (!panel) return;
631
- const items = tabbable(panel);
632
- if (items.length === 0) {
633
- event.preventDefault();
634
- panel.focus({ preventScroll: true });
635
- return;
636
- }
637
- const first = items[0];
638
- const last = items[items.length - 1];
639
- const activeElement = document.activeElement;
640
- const inside = panel.contains(activeElement);
641
- if (event.shiftKey && (activeElement === first || !inside)) {
642
- event.preventDefault();
643
- last.focus();
644
- } else if (!event.shiftKey && (activeElement === last || !inside)) {
645
- event.preventDefault();
646
- first.focus();
647
- }
648
- }
649
- function onFocusIn(event) {
650
- if (!isTopmostRef.current) return;
651
- const panel = panelRef.current;
652
- if (!panel) return;
653
- if (panel.contains(event.target)) return;
654
- const items = tabbable(panel);
655
- (items[0] ?? panel).focus({ preventScroll: true });
656
- }
657
- document.addEventListener("keydown", onKeyDown);
658
- document.addEventListener("focusin", onFocusIn);
659
- return () => {
660
- document.removeEventListener("keydown", onKeyDown);
661
- document.removeEventListener("focusin", onFocusIn);
662
- };
663
- }, [active, trapFocus, panelRef]);
664
- useEffect3(() => {
665
- if (!active) return;
666
- const previouslyFocused = document.activeElement;
667
- const panel = panelRef.current;
668
- let observer = null;
669
- if (panel && autoFocus) {
670
- if (!panel.hasAttribute("tabindex")) panel.setAttribute("tabindex", "-1");
671
- const focusTarget = () => {
672
- const explicit = initialFocus?.current;
673
- if (explicit) {
674
- explicit.focus({ preventScroll: true });
675
- return true;
676
- }
677
- const items = tabbable(panel);
678
- if (items.length > 0) {
679
- items[0].focus({ preventScroll: true });
680
- return true;
681
- }
682
- return false;
683
- };
684
- if (!focusTarget()) {
685
- panel.focus({ preventScroll: true });
686
- observer = new MutationObserver(() => {
687
- if (focusTarget()) {
688
- observer?.disconnect();
689
- observer = null;
690
- }
691
- });
692
- observer.observe(panel, { childList: true, subtree: true });
693
- }
694
- }
695
- return () => {
696
- observer?.disconnect();
697
- const target = returnFocus ? returnFocus.current : previouslyFocused;
698
- if (target?.isConnected) target.focus({ preventScroll: true });
699
- };
700
- }, [active, panelRef, initialFocus, returnFocus, autoFocus]);
701
- }
319
+ var OVERLAY_HEADING = "px-2.5 py-1.5 text-body-tiny font-semibold uppercase tracking-wide text-default-tertiary";
320
+ var OVERLAY_SEPARATOR = "-mx-1 my-1 h-px bg-divider";
702
321
 
703
322
  // src/components/overlays/portal.tsx
704
- import { useEffect as useEffect4, useState as useState4 } from "react";
323
+ import { createContext as createContext2, useContext as useContext2, useEffect, useState as useState2 } from "react";
705
324
  import { createPortal } from "react-dom";
706
- function Portal({ children, container }) {
707
- const [mounted, setMounted] = useState4(false);
708
- useEffect4(() => setMounted(true), []);
709
- if (!mounted) return null;
710
- const target = container ?? (typeof document === "undefined" ? null : document.body);
711
- if (!target) return null;
712
- return createPortal(children, target);
325
+ import { jsx as jsx4 } from "react/jsx-runtime";
326
+ var PortalContainerContext = createContext2(null);
327
+ function usePortalContainer() {
328
+ return useContext2(PortalContainerContext);
329
+ }
330
+
331
+ // src/lib/compose-event-handlers.ts
332
+ function composeEventHandlers(theirs, ours) {
333
+ return (event) => {
334
+ theirs?.(event);
335
+ if (!event.defaultPrevented) ours(event);
336
+ };
713
337
  }
714
338
 
715
339
  // src/components/overlays/tooltip.tsx
716
- import {
717
- cloneElement as cloneElement2,
718
- isValidElement as isValidElement2,
719
- useCallback as useCallback3,
720
- useEffect as useEffect5,
721
- useId,
722
- useRef as useRef6,
723
- useState as useState5
724
- } from "react";
725
- import clsx7 from "clsx";
726
- import { Fragment, jsx as jsx4, jsxs } from "react/jsx-runtime";
340
+ import { useId } from "react";
341
+ import { Tooltip as TooltipPrimitive } from "radix-ui";
342
+ import { Fragment, jsx as jsx5, jsxs } from "react/jsx-runtime";
727
343
  function Tooltip({
728
344
  children,
729
345
  title,
@@ -731,231 +347,243 @@ function Tooltip({
731
347
  side = "top",
732
348
  align = "center",
733
349
  openDelay = 150,
734
- closeDelay = 0,
735
- open: openProp,
736
- defaultOpen = false,
350
+ open,
351
+ defaultOpen,
737
352
  onOpenChange,
738
353
  association = "describedby",
739
354
  className
740
355
  }) {
741
- const [open, setOpen] = useControllableState({
742
- value: openProp,
743
- defaultValue: defaultOpen,
744
- onChange: onOpenChange
745
- });
746
- const tooltipId = useId();
747
- const [triggerNode, setTriggerNode] = useState5(null);
748
- const timer = useRef6(null);
749
- const clearTimer = useCallback3(() => {
750
- if (timer.current) {
751
- clearTimeout(timer.current);
752
- timer.current = null;
753
- }
754
- }, []);
755
- const schedule = useCallback3(
756
- (next, delay) => {
757
- clearTimer();
758
- if (delay === 0) {
759
- setOpen(next);
760
- return;
356
+ const container = usePortalContainer();
357
+ const labelId = useId();
358
+ const labelled = association === "labelledby";
359
+ const content = /* @__PURE__ */ jsxs(Fragment, { children: [
360
+ title && /* @__PURE__ */ jsx5("p", { className: "font-semibold", children: title }),
361
+ body && /* @__PURE__ */ jsx5("p", { className: cn(title && "mt-0.5 opacity-80"), children: body })
362
+ ] });
363
+ return /* @__PURE__ */ jsx5(TooltipPrimitive.Provider, { delayDuration: openDelay, children: /* @__PURE__ */ jsxs(TooltipPrimitive.Root, { open, defaultOpen, onOpenChange, children: [
364
+ /* @__PURE__ */ jsx5(
365
+ TooltipPrimitive.Trigger,
366
+ {
367
+ asChild: true,
368
+ ...labelled ? { "aria-labelledby": labelId, "aria-describedby": void 0 } : {},
369
+ children
761
370
  }
762
- timer.current = setTimeout(() => setOpen(next), delay);
763
- },
764
- [clearTimer, setOpen]
765
- );
766
- useEffect5(() => clearTimer, [clearTimer]);
767
- const { refs, floatingStyles } = useFloatingPanel({
768
- open,
769
- onOpenChange: setOpen,
770
- side,
771
- align,
772
- sideOffset: 6
773
- });
774
- useEffect5(() => {
775
- refs.setReference(triggerNode);
776
- }, [refs, triggerNode]);
777
- useEffect5(() => {
778
- if (!open) return;
779
- function onKeyDown(event) {
780
- if (event.key === "Escape") setOpen(false);
781
- }
782
- document.addEventListener("keydown", onKeyDown);
783
- return () => document.removeEventListener("keydown", onKeyDown);
784
- }, [open, setOpen]);
785
- if (!isValidElement2(children)) return null;
786
- const child = children;
787
- const childProps = child.props;
788
- const describedBy = association === "describedby" ? tooltipId : void 0;
789
- const labelledBy = association === "labelledby" ? tooltipId : void 0;
790
- const trigger = cloneElement2(child, {
791
- ref: composeRefs(
792
- (node) => setTriggerNode(node),
793
- child.ref
794
371
  ),
795
- "aria-describedby": [childProps["aria-describedby"], open ? describedBy : void 0].filter(Boolean).join(" ") || void 0,
796
- "aria-labelledby": [childProps["aria-labelledby"], labelledBy].filter(Boolean).join(" ") || void 0,
797
- onPointerEnter: (event) => {
798
- childProps.onPointerEnter?.(event);
799
- schedule(true, openDelay);
800
- },
801
- onPointerLeave: (event) => {
802
- childProps.onPointerLeave?.(event);
803
- schedule(false, closeDelay);
804
- },
805
- // Focus, not just hover — this is the difference between a tooltip a keyboard user can
806
- // read and one they cannot.
807
- onFocus: (event) => {
808
- childProps.onFocus?.(event);
809
- schedule(true, 0);
810
- },
811
- onBlur: (event) => {
812
- childProps.onBlur?.(event);
813
- schedule(false, 0);
814
- }
815
- });
816
- return /* @__PURE__ */ jsxs(Fragment, { children: [
817
- trigger,
818
- (open || association === "labelledby") && /* @__PURE__ */ jsx4(Portal, { children: /* @__PURE__ */ jsxs(
819
- "div",
372
+ labelled && /* @__PURE__ */ jsxs("span", { id: labelId, hidden: true, children: [
373
+ title,
374
+ body ? " " : null,
375
+ body
376
+ ] }),
377
+ /* @__PURE__ */ jsx5(TooltipPrimitive.Portal, { container: container ?? void 0, children: /* @__PURE__ */ jsx5(
378
+ TooltipPrimitive.Content,
820
379
  {
821
- ref: refs.setFloating,
822
- id: tooltipId,
823
- role: "tooltip",
824
- style: floatingStyles,
825
- className: clsx7(
380
+ side,
381
+ align,
382
+ sideOffset: 6,
383
+ collisionPadding: 8,
384
+ className: cn(
826
385
  "z-tooltip max-w-[260px] rounded-lg bg-neutral px-3 py-2 shadow-300",
827
386
  "text-body-xs text-neutral-on-neutral",
828
- !open && "sr-only",
829
387
  className
830
388
  ),
831
- children: [
832
- title && /* @__PURE__ */ jsx4("p", { className: "font-semibold", children: title }),
833
- body && /* @__PURE__ */ jsx4("p", { className: clsx7(title && "mt-0.5 opacity-80"), children: body })
834
- ]
389
+ children: content
835
390
  }
836
391
  ) })
837
- ] });
392
+ ] }) });
838
393
  }
839
394
 
840
395
  // src/components/overlays/menu-core.tsx
841
396
  import {
842
- createContext as createContext2,
843
- useCallback as useCallback4,
844
- useContext as useContext2,
845
- useId as useId2,
846
- useMemo,
847
- useRef as useRef7
397
+ forwardRef as forwardRef3,
398
+ useId as useId2
848
399
  } from "react";
849
- import clsx8 from "clsx";
850
- import { LuCheck } from "react-icons/lu";
851
- import { jsx as jsx5, jsxs as jsxs2 } from "react/jsx-runtime";
852
- var MenuContext = createContext2(null);
853
- function useMenu(part) {
854
- const context = useContext2(MenuContext);
855
- if (!context) {
856
- throw new Error(`<${part}> must be used inside a menu root (DropdownMenu, ContextMenu, Menubar)`);
857
- }
858
- return context;
400
+ import clsx7 from "clsx";
401
+ import { CheckIcon, ChevronRightIcon } from "lucide-react";
402
+ import { jsx as jsx6, jsxs as jsxs2 } from "react/jsx-runtime";
403
+ var MENU_PANEL_CLASSES = cn("z-popover", OVERLAY_PANEL_MENU, "focus:outline-none");
404
+ var ITEM_BASE = clsx7(
405
+ OVERLAY_ITEM_BASE,
406
+ OVERLAY_ITEM_SIZE.md,
407
+ // Highlight follows Radix's `data-highlighted`, which tracks pointer and keyboard alike.
408
+ // The ring marks the keyboard position on top of it: the tint alone, shared with hover,
409
+ // cannot tell a keyboard user which of two tinted items has focus.
410
+ FOCUS_RING_INSET,
411
+ "data-[disabled]:cursor-not-allowed data-[disabled]:text-disabled"
412
+ );
413
+ var ITEM_TONE = {
414
+ default: "text-default data-[highlighted]:bg-default-secondary",
415
+ danger: "text-danger data-[highlighted]:bg-danger-tertiary"
416
+ };
417
+ function stopToAncestors(event) {
418
+ event.stopPropagation();
859
419
  }
860
- function useMenuState({ open, setOpen }) {
861
- const baseId = useId2();
862
- const cursor = useRef7(0);
863
- const pendingEdge = useRef7("start");
864
- const roving = useRovingFocus({
865
- orientation: "vertical",
866
- loop: true,
867
- typeahead: true,
868
- onEscape: () => setOpen(false)
420
+ function focusMenuEdge(event, edge) {
421
+ if (event.target !== event.currentTarget) return;
422
+ const panel = event.currentTarget;
423
+ queueMicrotask(() => {
424
+ const items = panel.querySelectorAll('[role^="menuitem"]:not([data-disabled])');
425
+ if (edge === "end") items[items.length - 1]?.focus();
426
+ else if (document.activeElement === panel) items[0]?.focus();
869
427
  });
870
- const claimIndex = useCallback4(() => cursor.current++, []);
871
- const resetIndices = useCallback4(() => {
872
- cursor.current = 0;
873
- }, []);
874
- return useMemo(
875
- () => ({
876
- open,
877
- setOpen,
878
- menuId: `${baseId}-menu`,
879
- triggerId: `${baseId}-trigger`,
880
- claimIndex,
881
- resetIndices,
882
- pendingEdge,
883
- roving
884
- }),
885
- [open, setOpen, baseId, claimIndex, resetIndices, roving]
886
- );
887
428
  }
888
- var MENU_PANEL_CLASSES = OVERLAY_PANEL_MENU;
889
- var ITEM_BASE = clsx8(OVERLAY_ITEM_BASE, OVERLAY_ITEM_SIZE.md);
890
- var itemToneClasses = overlayItemTone;
891
- function MenuItem({
892
- children,
893
- onSelect,
894
- tone = "default",
895
- disabled = false,
896
- icon,
897
- description,
898
- shortcut,
899
- textValue,
900
- className,
901
- ...rest
902
- }) {
903
- const menu = useMenu("MenuItem");
904
- const index = menu.claimIndex();
905
- const descriptionId = `${menu.menuId}-desc-${index}`;
906
- const label = textValue ?? (typeof children === "string" ? children : void 0);
907
- const itemProps = menu.roving.itemProps(index, { disabled, label });
908
- const activate = () => {
909
- if (disabled) return;
910
- menu.setOpen(false);
911
- onSelect?.();
429
+ function createMenuParts(P) {
430
+ const Item = forwardRef3(function MenuItem({ children, onSelect, tone = "default", disabled = false, icon, description, shortcut, textValue, className, ...rest }, ref) {
431
+ const descriptionId = useId2();
432
+ return /* @__PURE__ */ jsxs2(
433
+ P.Item,
434
+ {
435
+ ref,
436
+ disabled,
437
+ textValue,
438
+ onSelect,
439
+ "data-tone": tone,
440
+ "aria-describedby": description ? descriptionId : void 0,
441
+ className: cn(ITEM_BASE, ITEM_TONE[tone], className),
442
+ ...rest,
443
+ children: [
444
+ icon && /* @__PURE__ */ jsx6("span", { "aria-hidden": true, className: "mt-0.5 flex size-4 shrink-0 items-center justify-center", children: icon }),
445
+ /* @__PURE__ */ jsxs2("span", { className: "min-w-0 flex-1", children: [
446
+ /* @__PURE__ */ jsx6("span", { className: "block truncate", children }),
447
+ description && /* @__PURE__ */ jsx6("span", { id: descriptionId, className: "mt-0.5 block text-body-tiny text-default-secondary", children: description })
448
+ ] }),
449
+ shortcut && /* @__PURE__ */ jsx6("span", { className: "ml-auto shrink-0 pl-4 text-body-tiny text-default-tertiary", children: shortcut })
450
+ ]
451
+ }
452
+ );
453
+ });
454
+ const CheckboxItem = forwardRef3(function MenuCheckboxItem({ children, checked, onCheckedChange, disabled = false, tone = "default", shortcut, textValue, className, ...rest }, ref) {
455
+ return /* @__PURE__ */ jsxs2(
456
+ P.CheckboxItem,
457
+ {
458
+ ref,
459
+ checked,
460
+ onCheckedChange: (next) => onCheckedChange?.(next === true),
461
+ onSelect: (event) => event.preventDefault(),
462
+ disabled,
463
+ textValue,
464
+ "data-tone": tone,
465
+ className: cn(ITEM_BASE, "items-center", ITEM_TONE[tone], className),
466
+ ...rest,
467
+ children: [
468
+ /* @__PURE__ */ jsx6("span", { "aria-hidden": true, className: "flex size-4 shrink-0 items-center justify-center", children: /* @__PURE__ */ jsx6(P.ItemIndicator, { children: /* @__PURE__ */ jsx6(CheckIcon, { className: "size-3.5", strokeWidth: 3 }) }) }),
469
+ /* @__PURE__ */ jsx6("span", { className: "min-w-0 flex-1 truncate", children }),
470
+ shortcut && /* @__PURE__ */ jsx6("span", { className: "ml-auto shrink-0 pl-4 text-body-tiny text-default-tertiary", children: shortcut })
471
+ ]
472
+ }
473
+ );
474
+ });
475
+ function RadioGroup({ children, value, onValueChange }) {
476
+ return /* @__PURE__ */ jsx6(P.RadioGroup, { value, onValueChange, children });
477
+ }
478
+ const RadioItem = forwardRef3(function MenuRadioItem({ children, value, disabled = false, tone = "default", textValue, className, ...rest }, ref) {
479
+ return /* @__PURE__ */ jsxs2(
480
+ P.RadioItem,
481
+ {
482
+ ref,
483
+ value,
484
+ disabled,
485
+ textValue,
486
+ "data-tone": tone,
487
+ className: cn(ITEM_BASE, "items-center", ITEM_TONE[tone], className),
488
+ ...rest,
489
+ children: [
490
+ /* @__PURE__ */ jsx6("span", { "aria-hidden": true, className: "flex size-4 shrink-0 items-center justify-center", children: /* @__PURE__ */ jsx6(P.ItemIndicator, { children: /* @__PURE__ */ jsx6("span", { className: "block size-1.5 rounded-full bg-current" }) }) }),
491
+ /* @__PURE__ */ jsx6("span", { className: "min-w-0 flex-1 truncate", children })
492
+ ]
493
+ }
494
+ );
495
+ });
496
+ const Group = forwardRef3(function MenuGroup({ children, label, className, ...rest }, ref) {
497
+ const labelId = useId2();
498
+ return /* @__PURE__ */ jsxs2(P.Group, { ref, "aria-labelledby": label ? labelId : void 0, className, ...rest, children: [
499
+ label && /* @__PURE__ */ jsx6("div", { id: labelId, className: OVERLAY_HEADING, children: label }),
500
+ children
501
+ ] });
502
+ });
503
+ const Label = forwardRef3(function MenuLabel({ className, ...rest }, ref) {
504
+ return /* @__PURE__ */ jsx6(P.Label, { ref, className: cn(OVERLAY_HEADING, className), ...rest });
505
+ });
506
+ const Separator = forwardRef3(function MenuSeparator({ className, ...rest }, ref) {
507
+ return /* @__PURE__ */ jsx6(P.Separator, { ref, className: cn(OVERLAY_SEPARATOR, className), ...rest });
508
+ });
509
+ const Shortcut = forwardRef3(function MenuShortcut({ className, ...rest }, ref) {
510
+ return /* @__PURE__ */ jsx6("span", { ref, className: cn("ml-auto pl-4 text-body-tiny text-default-tertiary", className), ...rest });
511
+ });
512
+ function Sub({ children, open, defaultOpen, onOpenChange }) {
513
+ return /* @__PURE__ */ jsx6(P.Sub, { open, defaultOpen, onOpenChange, children });
514
+ }
515
+ const SubTrigger = forwardRef3(function MenuSubTrigger({ children, icon, disabled = false, textValue, className, ...rest }, ref) {
516
+ return /* @__PURE__ */ jsxs2(
517
+ P.SubTrigger,
518
+ {
519
+ ref,
520
+ disabled,
521
+ textValue,
522
+ className: cn(
523
+ ITEM_BASE,
524
+ "items-center",
525
+ ITEM_TONE.default,
526
+ "data-[state=open]:bg-default-secondary",
527
+ className
528
+ ),
529
+ ...rest,
530
+ children: [
531
+ icon && /* @__PURE__ */ jsx6("span", { "aria-hidden": true, className: "flex size-4 shrink-0 items-center justify-center", children: icon }),
532
+ /* @__PURE__ */ jsx6("span", { className: "min-w-0 flex-1 truncate", children }),
533
+ /* @__PURE__ */ jsx6(ChevronRightIcon, { "aria-hidden": true, className: "ml-auto size-4 shrink-0 text-default-tertiary" })
534
+ ]
535
+ }
536
+ );
537
+ });
538
+ const SubContent = forwardRef3(function MenuSubContent({ className, onClick, onKeyDown, ...rest }, ref) {
539
+ const container = usePortalContainer();
540
+ return /* @__PURE__ */ jsx6(P.Portal, { container: container ?? void 0, children: /* @__PURE__ */ jsx6(
541
+ P.SubContent,
542
+ {
543
+ ref,
544
+ sideOffset: 4,
545
+ collisionPadding: 8,
546
+ className: cn(MENU_PANEL_CLASSES, className),
547
+ onClick: (event) => {
548
+ onClick?.(event);
549
+ stopToAncestors(event);
550
+ },
551
+ onKeyDown: (event) => {
552
+ onKeyDown?.(event);
553
+ stopToAncestors(event);
554
+ },
555
+ ...rest
556
+ }
557
+ ) });
558
+ });
559
+ return {
560
+ Item,
561
+ CheckboxItem,
562
+ RadioGroup,
563
+ RadioItem,
564
+ Group,
565
+ Label,
566
+ Separator,
567
+ Shortcut,
568
+ Sub,
569
+ SubTrigger,
570
+ SubContent
912
571
  };
913
- return /* @__PURE__ */ jsxs2(
914
- "div",
915
- {
916
- role: "menuitem",
917
- "aria-disabled": disabled || void 0,
918
- "aria-describedby": description ? descriptionId : void 0,
919
- ...itemProps,
920
- tabIndex: itemProps.tabIndex,
921
- className: clsx8(ITEM_BASE, itemToneClasses(tone, disabled), className),
922
- onClick: (event) => {
923
- event.stopPropagation();
924
- activate();
925
- },
926
- onKeyDown: (event) => {
927
- if (event.key === "Enter" || event.key === " ") {
928
- event.preventDefault();
929
- activate();
930
- }
931
- },
932
- ...rest,
933
- children: [
934
- icon && /* @__PURE__ */ jsx5("span", { "aria-hidden": true, className: "mt-0.5 flex size-4 shrink-0 items-center justify-center", children: icon }),
935
- /* @__PURE__ */ jsxs2("span", { className: "min-w-0 flex-1", children: [
936
- /* @__PURE__ */ jsx5("span", { className: "block truncate", children }),
937
- description && /* @__PURE__ */ jsx5("span", { id: descriptionId, className: "mt-0.5 block text-body-tiny text-default-secondary", children: description })
938
- ] }),
939
- shortcut && /* @__PURE__ */ jsx5("span", { className: "ml-auto shrink-0 pl-4 text-body-tiny text-default-tertiary", children: shortcut })
940
- ]
941
- }
942
- );
943
572
  }
944
- var MenuRadioContext = createContext2(null);
945
573
 
946
574
  // src/components/cards/scroll-area.tsx
947
- import { forwardRef as forwardRef3, useEffect as useEffect6, useRef as useRef8, useState as useState6 } from "react";
948
- import clsx9 from "clsx";
949
- import { jsx as jsx6 } from "react/jsx-runtime";
575
+ import { forwardRef as forwardRef4, useEffect as useEffect2, useRef as useRef2, useState as useState3 } from "react";
576
+ import clsx8 from "clsx";
577
+ import { jsx as jsx7 } from "react/jsx-runtime";
950
578
  var OVERFLOW = {
951
579
  vertical: "overflow-y-auto overflow-x-hidden",
952
580
  horizontal: "overflow-x-auto overflow-y-hidden",
953
581
  both: "overflow-auto"
954
582
  };
955
- var ScrollArea = forwardRef3(function ScrollArea2({ children, axis = "vertical", label, className, ...rest }, ref) {
956
- const localRef = useRef8(null);
957
- const [overflows, setOverflows] = useState6(false);
958
- useEffect6(() => {
583
+ var ScrollArea = forwardRef4(function ScrollArea2({ children, axis = "vertical", label, className, ...rest }, ref) {
584
+ const localRef = useRef2(null);
585
+ const [overflows, setOverflows] = useState3(false);
586
+ useEffect2(() => {
959
587
  const node = localRef.current;
960
588
  if (!node) return;
961
589
  const measure = () => {
@@ -972,7 +600,7 @@ var ScrollArea = forwardRef3(function ScrollArea2({ children, axis = "vertical",
972
600
  if (node.firstElementChild) observer.observe(node.firstElementChild);
973
601
  return () => observer.disconnect();
974
602
  }, [axis, children]);
975
- return /* @__PURE__ */ jsx6(
603
+ return /* @__PURE__ */ jsx7(
976
604
  "div",
977
605
  {
978
606
  ref: composeRefs(localRef, ref),
@@ -980,7 +608,7 @@ var ScrollArea = forwardRef3(function ScrollArea2({ children, axis = "vertical",
980
608
  "aria-label": overflows ? label : void 0,
981
609
  tabIndex: overflows ? 0 : void 0,
982
610
  "data-slot": "scroll-area",
983
- className: clsx9(
611
+ className: clsx8(
984
612
  OVERFLOW[axis],
985
613
  "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand focus-visible:ring-offset-1 focus-visible:ring-offset-default",
986
614
  SCROLLBAR_CLASSES,
@@ -995,156 +623,134 @@ var ScrollArea = forwardRef3(function ScrollArea2({ children, axis = "vertical",
995
623
  // src/components/overlays/dropdown-menu.tsx
996
624
  import {
997
625
  createContext as createContext3,
626
+ forwardRef as forwardRef5,
998
627
  useContext as useContext3,
999
- useEffect as useEffect7,
1000
- useMemo as useMemo2,
1001
- useRef as useRef9,
1002
- useState as useState7
628
+ useMemo,
629
+ useRef as useRef3
1003
630
  } from "react";
1004
- import clsx10 from "clsx";
1005
- import { LuEllipsis } from "react-icons/lu";
1006
- import { jsx as jsx7 } from "react/jsx-runtime";
1007
- var TriggerNodeContext = createContext3(null);
1008
- function useTriggerNode() {
1009
- const value = useContext3(TriggerNodeContext);
1010
- if (!value) throw new Error("DropdownMenu parts must be used inside <DropdownMenu>");
631
+ import { DropdownMenu as DropdownMenuPrimitive } from "radix-ui";
632
+ import { EllipsisIcon } from "lucide-react";
633
+ import { jsx as jsx8 } from "react/jsx-runtime";
634
+ var DropdownMenuStateContext = createContext3(null);
635
+ function useDropdownMenuState(part) {
636
+ const value = useContext3(DropdownMenuStateContext);
637
+ if (!value) throw new Error(`<${part}> must be used inside <DropdownMenu>`);
1011
638
  return value;
1012
639
  }
1013
640
  function DropdownMenu({
1014
641
  children,
1015
642
  open: openProp,
1016
643
  defaultOpen = false,
1017
- onOpenChange
644
+ onOpenChange,
645
+ modal = false
1018
646
  }) {
1019
647
  const [open, setOpen] = useControllableState({
1020
648
  value: openProp,
1021
649
  defaultValue: defaultOpen,
1022
650
  onChange: onOpenChange
1023
651
  });
1024
- const menu = useMenuState({ open, setOpen });
1025
- const [triggerNode, setTriggerNode] = useState7(null);
1026
- const anchor = useMemo2(() => ({ triggerNode, setTriggerNode }), [triggerNode]);
1027
- return /* @__PURE__ */ jsx7(MenuContext.Provider, { value: menu, children: /* @__PURE__ */ jsx7(TriggerNodeContext.Provider, { value: anchor, children }) });
652
+ const pendingEdge = useRef3("start");
653
+ const state = useMemo(() => ({ setOpen, pendingEdge }), [setOpen]);
654
+ return /* @__PURE__ */ jsx8(DropdownMenuPrimitive.Root, { open, onOpenChange: setOpen, modal, children: /* @__PURE__ */ jsx8(DropdownMenuStateContext.Provider, { value: state, children }) });
1028
655
  }
1029
- function DropdownMenuTrigger({
1030
- children,
1031
- asChild = false,
1032
- label = "Open menu",
1033
- ...rest
1034
- }) {
1035
- const menu = useMenu("DropdownMenuTrigger");
1036
- const anchor = useTriggerNode();
1037
- const triggerProps = {
1038
- id: menu.triggerId,
1039
- "aria-haspopup": "menu",
1040
- "aria-expanded": menu.open,
1041
- "aria-controls": menu.open ? menu.menuId : void 0,
1042
- onClick: (event) => {
1043
- event.stopPropagation();
1044
- menu.setOpen(!menu.open);
1045
- },
1046
- onKeyDown: (event) => {
1047
- if (event.key === "ArrowDown" || event.key === "ArrowUp") {
1048
- event.preventDefault();
1049
- menu.pendingEdge.current = event.key === "ArrowUp" ? "end" : "start";
1050
- menu.setOpen(true);
656
+ var DropdownMenuTrigger = forwardRef5(
657
+ function DropdownMenuTrigger2({ children, asChild = false, label = "Open menu", onClick, onPointerDown, onMouseDown, onKeyDown, ...rest }, ref) {
658
+ const state = useDropdownMenuState("DropdownMenuTrigger");
659
+ const handlers = {
660
+ onClick: (event) => {
661
+ onClick?.(event);
662
+ stopToAncestors(event);
663
+ },
664
+ onPointerDown: (event) => {
665
+ onPointerDown?.(event);
666
+ event.stopPropagation();
667
+ },
668
+ onMouseDown: (event) => {
669
+ onMouseDown?.(event);
670
+ stopToAncestors(event);
671
+ },
672
+ onKeyDown: (event) => {
673
+ onKeyDown?.(event);
674
+ if (event.defaultPrevented) return;
675
+ if (event.key === "ArrowUp") {
676
+ event.preventDefault();
677
+ state.pendingEdge.current = "end";
678
+ state.setOpen(true);
679
+ }
680
+ if (event.key === "Enter" || event.key === " ") stopToAncestors(event);
1051
681
  }
1052
- },
1053
- // Stopped at the trigger so the menu works inside a clickable table row — the
1054
- // behaviour the 0.2.0 `RowMenu` had and the reason this component exists.
1055
- onMouseDown: (event) => event.stopPropagation(),
682
+ };
683
+ if (asChild) {
684
+ return /* @__PURE__ */ jsx8(DropdownMenuPrimitive.Trigger, { ref, asChild: true, ...rest, ...handlers, children });
685
+ }
686
+ return /* @__PURE__ */ jsx8(DropdownMenuPrimitive.Trigger, { ref, asChild: true, ...rest, ...handlers, children: /* @__PURE__ */ jsx8(IconButton, { variant: "neutral", size: "sm", "aria-label": label, children: children ?? /* @__PURE__ */ jsx8(EllipsisIcon, { className: "size-4", "aria-hidden": true }) }) });
687
+ }
688
+ );
689
+ var DropdownMenuContent = forwardRef5(
690
+ function DropdownMenuContent2({
691
+ children,
692
+ side = "bottom",
693
+ align = "end",
694
+ sideOffset = 6,
695
+ container,
696
+ className,
697
+ onClick,
698
+ onKeyDown,
699
+ onFocus,
700
+ onCloseAutoFocus,
1056
701
  ...rest
1057
- };
1058
- if (asChild) {
1059
- return /* @__PURE__ */ jsx7(Slot, { ref: composeRefs(anchor.setTriggerNode), ...triggerProps, children });
702
+ }, ref) {
703
+ const state = useDropdownMenuState("DropdownMenuContent");
704
+ const inherited = usePortalContainer();
705
+ return /* @__PURE__ */ jsx8(DropdownMenuPrimitive.Portal, { container: container ?? inherited ?? void 0, children: /* @__PURE__ */ jsx8(
706
+ DropdownMenuPrimitive.Content,
707
+ {
708
+ ref,
709
+ side,
710
+ align,
711
+ sideOffset,
712
+ collisionPadding: 8,
713
+ loop: true,
714
+ className: cn(MENU_PANEL_CLASSES, className),
715
+ onCloseAutoFocus: composeEventHandlers(onCloseAutoFocus, () => {
716
+ state.pendingEdge.current = "start";
717
+ }),
718
+ onFocus: composeEventHandlers(onFocus, (event) => {
719
+ const edge = state.pendingEdge.current;
720
+ state.pendingEdge.current = "start";
721
+ focusMenuEdge(event, edge);
722
+ }),
723
+ onClick: (event) => {
724
+ onClick?.(event);
725
+ stopToAncestors(event);
726
+ },
727
+ onKeyDown: (event) => {
728
+ onKeyDown?.(event);
729
+ stopToAncestors(event);
730
+ if (event.key === "Tab") state.setOpen(false);
731
+ },
732
+ ...rest,
733
+ children
734
+ }
735
+ ) });
1060
736
  }
1061
- return /* @__PURE__ */ jsx7(
1062
- IconButton,
1063
- {
1064
- ref: composeRefs(anchor.setTriggerNode),
1065
- variant: "neutral",
1066
- size: "sm",
1067
- "aria-label": label,
1068
- ...triggerProps,
1069
- children: children ?? /* @__PURE__ */ jsx7(LuEllipsis, { className: "size-4", "aria-hidden": true })
1070
- }
1071
- );
1072
- }
1073
- function DropdownMenuContent({
1074
- children,
1075
- side = "bottom",
1076
- align = "end",
1077
- sideOffset = 6,
1078
- className,
1079
- ...rest
1080
- }) {
1081
- const menu = useMenu("DropdownMenuContent");
1082
- const anchor = useTriggerNode();
1083
- const panelRef = useRef9(null);
1084
- const [panelNode, setPanelNode] = useState7(null);
1085
- menu.resetIndices();
1086
- const { refs, floatingStyles } = useFloatingPanel({
1087
- open: menu.open,
1088
- onOpenChange: menu.setOpen,
1089
- side,
1090
- align,
1091
- sideOffset
1092
- });
1093
- useEffect7(() => {
1094
- refs.setReference(anchor.triggerNode);
1095
- }, [refs, anchor.triggerNode]);
1096
- useOverlayDismiss({
1097
- onDismiss: () => menu.setOpen(false),
1098
- panelRef,
1099
- active: menu.open,
1100
- // A menu is not modal: the page behind stays readable and interactive, so none of the
1101
- // dialog-grade side effects apply. Only Escape, outside-press and focus restore.
1102
- trapFocus: false,
1103
- lockScroll: false,
1104
- inertBackground: false,
1105
- // The menu owns its own initial focus below; a generic "first tabbable element" scan
1106
- // would fight the roving tabindex, which exposes exactly one item at a time.
1107
- autoFocus: false
1108
- });
1109
- const wasOpen = useRef9(false);
1110
- useEffect7(() => {
1111
- if (menu.open && panelNode && !wasOpen.current) {
1112
- menu.roving.moveToEdge(menu.pendingEdge.current);
1113
- menu.pendingEdge.current = "start";
1114
- wasOpen.current = true;
1115
- }
1116
- if (!menu.open) wasOpen.current = false;
1117
- }, [menu.open, panelNode, menu.roving, menu.pendingEdge]);
1118
- if (!menu.open) return null;
1119
- return /* @__PURE__ */ jsx7(Portal, { children: /* @__PURE__ */ jsx7(
1120
- "div",
1121
- {
1122
- ref: composeRefs(
1123
- panelRef,
1124
- (node) => setPanelNode(node),
1125
- refs.setFloating
1126
- ),
1127
- id: menu.menuId,
1128
- role: "menu",
1129
- tabIndex: -1,
1130
- "aria-orientation": "vertical",
1131
- "aria-labelledby": menu.triggerId,
1132
- style: floatingStyles,
1133
- className: clsx10("z-popover", MENU_PANEL_CLASSES, className),
1134
- ...menu.roving.containerProps,
1135
- onKeyDown: (event) => {
1136
- menu.roving.containerProps.onKeyDown(event);
1137
- if (event.key === "Tab") menu.setOpen(false);
1138
- },
1139
- ...rest,
1140
- children
1141
- }
1142
- ) });
1143
- }
737
+ );
738
+ var parts = createMenuParts(DropdownMenuPrimitive);
739
+ var DropdownMenuItem = parts.Item;
740
+ var DropdownMenuCheckboxItem = parts.CheckboxItem;
741
+ var DropdownMenuRadioGroup = parts.RadioGroup;
742
+ var DropdownMenuRadioItem = parts.RadioItem;
743
+ var DropdownMenuGroup = parts.Group;
744
+ var DropdownMenuLabel = parts.Label;
745
+ var DropdownMenuSeparator = parts.Separator;
746
+ var DropdownMenuShortcut = parts.Shortcut;
747
+ var DropdownMenuSub = parts.Sub;
748
+ var DropdownMenuSubTrigger = parts.SubTrigger;
749
+ var DropdownMenuSubContent = parts.SubContent;
1144
750
 
1145
751
  // src/components/chrome/nav-item.tsx
1146
- import clsx11 from "clsx";
1147
- import { Fragment as Fragment2, jsx as jsx8, jsxs as jsxs3 } from "react/jsx-runtime";
752
+ import clsx9 from "clsx";
753
+ import { Fragment as Fragment2, jsx as jsx9, jsxs as jsxs3 } from "react/jsx-runtime";
1148
754
  var SIZE = {
1149
755
  sm: { box: "h-9 gap-2 rounded-lg px-3 py-1.5", label: "text-body-xs", square: "w-9" },
1150
756
  md: { box: "h-11 gap-3 rounded-xl px-3.5 py-2.5", label: "text-body-sm", square: "w-11" }
@@ -1171,12 +777,12 @@ var VARIANT = {
1171
777
  // `border-transparent` holds the 1px so the icon and label do not shift outward when a
1172
778
  // row becomes the current one — the same trick, for the same reason, as `button.tsx`.
1173
779
  // `bg-brand` still paints edge to edge, since backgrounds clip to the border box.
1174
- active: clsx11("border border-transparent", STATE.active),
1175
- inactive: clsx11("border border-default bg-default text-default-secondary", STATE.hover)
780
+ active: clsx9("border border-transparent", STATE.active),
781
+ inactive: clsx9("border border-default bg-default text-default-secondary", STATE.hover)
1176
782
  },
1177
783
  plain: {
1178
784
  active: STATE.active,
1179
- inactive: clsx11("text-default-secondary", STATE.hover)
785
+ inactive: clsx9("text-default-secondary", STATE.hover)
1180
786
  }
1181
787
  };
1182
788
  function NavItem({
@@ -1185,14 +791,16 @@ function NavItem({
1185
791
  isActive = false,
1186
792
  indicator = false,
1187
793
  collapsed = false,
1188
- size: size2 = "md",
794
+ size = "md",
1189
795
  variant = "card",
1190
796
  asChild = false,
797
+ disabled = false,
1191
798
  children,
1192
799
  className,
800
+ onClick,
1193
801
  ...rest
1194
802
  }) {
1195
- const s = SIZE[size2];
803
+ const s = SIZE[size];
1196
804
  const v = VARIANT[variant];
1197
805
  const rootProps = {
1198
806
  // Only meaningful when `asChild` renders a real link — a decorative span with
@@ -1203,7 +811,9 @@ function NavItem({
1203
811
  // you *are* (`aria-current="page"`), which is a different thing from a row you have
1204
812
  // *picked* (`aria-selected`) — `TableRow` is the latter and says `selected`.
1205
813
  "data-state": isActive ? "active" : void 0,
1206
- className: clsx11(
814
+ "data-disabled": disabled || void 0,
815
+ "aria-disabled": disabled || void 0,
816
+ className: clsx9(
1207
817
  "relative flex items-center transition-colors",
1208
818
  // Unconditional: a row can also become focusable by the caller putting it inside a
1209
819
  // link, which is how `src/sidebar/` uses it. A ring that only appears under `asChild`
@@ -1216,20 +826,31 @@ function NavItem({
1216
826
  "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand focus-visible:ring-offset-1 focus-visible:ring-offset-default-secondary",
1217
827
  s.box,
1218
828
  // Expanded rows fill the nav; a collapsed one is a centred square, not a stretched row.
1219
- collapsed ? clsx11("justify-center", s.square) : "w-full",
829
+ collapsed ? clsx9("justify-center", s.square) : "w-full",
1220
830
  isActive ? v.active : v.inactive,
831
+ "data-[disabled]:cursor-not-allowed data-[disabled]:opacity-40 data-[disabled]:hover:bg-transparent data-[disabled]:hover:text-default-secondary",
1221
832
  className
1222
833
  ),
1223
- ...rest
834
+ ...rest,
835
+ // After the spread: a disabled row refuses the press (a router Link skips navigation when
836
+ // the event is prevented) and leaves the tab order, as a disabled control does.
837
+ ...disabled ? { tabIndex: -1 } : {},
838
+ onClick: (event) => {
839
+ if (disabled) {
840
+ event.preventDefault();
841
+ return;
842
+ }
843
+ onClick?.(event);
844
+ }
1224
845
  };
1225
846
  const content = /* @__PURE__ */ jsxs3(Fragment2, { children: [
1226
- icon != null ? /* @__PURE__ */ jsx8("span", { "aria-hidden": true, className: "shrink-0 [&_svg]:size-4", children: icon }) : (
847
+ icon != null ? /* @__PURE__ */ jsx9("span", { "aria-hidden": true, className: "shrink-0 [&_svg]:size-4", children: icon }) : (
1227
848
  // Icons are optional, but the collapsed rail hides the label — so a row with neither
1228
849
  // renders as an empty box. Through 0.5.0 that is exactly what a nav without icons
1229
850
  // collapsed to, the docs site's own 36-row sidebar included. The monogram is
1230
851
  // decorative in the same way the icon is: the `sr-only` label below still names the
1231
852
  // row.
1232
- collapsed && /* @__PURE__ */ jsx8(
853
+ collapsed && /* @__PURE__ */ jsx9(
1233
854
  "span",
1234
855
  {
1235
856
  "aria-hidden": true,
@@ -1238,10 +859,10 @@ function NavItem({
1238
859
  }
1239
860
  )
1240
861
  ),
1241
- /* @__PURE__ */ jsx8(
862
+ /* @__PURE__ */ jsx9(
1242
863
  "span",
1243
864
  {
1244
- className: clsx11(
865
+ className: clsx9(
1245
866
  "min-w-0 truncate font-regular leading-snug",
1246
867
  s.label,
1247
868
  collapsed ? "sr-only" : "flex-1"
@@ -1249,7 +870,7 @@ function NavItem({
1249
870
  children: label
1250
871
  }
1251
872
  ),
1252
- indicator && /* @__PURE__ */ jsx8(
873
+ indicator && /* @__PURE__ */ jsx9(
1253
874
  "span",
1254
875
  {
1255
876
  role: "status",
@@ -1259,32 +880,35 @@ function NavItem({
1259
880
  )
1260
881
  ] });
1261
882
  if (asChild) {
1262
- return /* @__PURE__ */ jsx8(Slot, { wrapChildren: () => content, ...rootProps, children });
883
+ return /* @__PURE__ */ jsx9(Slot, { wrapChildren: () => content, ...rootProps, children });
1263
884
  }
1264
- return /* @__PURE__ */ jsx8("span", { ...rootProps, children: content });
885
+ return /* @__PURE__ */ jsx9("span", { ...rootProps, children: content });
1265
886
  }
1266
887
 
888
+ // src/index.tsx
889
+ import { createColumnHelper } from "@tanstack/react-table";
890
+
1267
891
  // src/sidebar/nav-tabs.tsx
1268
892
  import { NavLink } from "react-router-dom";
1269
- import clsx12 from "clsx";
1270
- import { jsx as jsx9 } from "react/jsx-runtime";
893
+ import clsx10 from "clsx";
894
+ import { jsx as jsx10 } from "react/jsx-runtime";
1271
895
  function NavTabs({ items, className }) {
1272
896
  return (
1273
897
  // The links inside are the tab stops.
1274
- /* @__PURE__ */ jsx9(
898
+ /* @__PURE__ */ jsx10(
1275
899
  "div",
1276
900
  {
1277
- className: clsx12(
901
+ className: clsx10(
1278
902
  "flex gap-6 overflow-x-auto border-b border-default",
1279
903
  SCROLLBAR_CLASSES,
1280
904
  className
1281
905
  ),
1282
- children: items.map((tab) => /* @__PURE__ */ jsx9(
906
+ children: items.map((tab) => /* @__PURE__ */ jsx10(
1283
907
  NavLink,
1284
908
  {
1285
909
  to: tab.to,
1286
910
  end: tab.end,
1287
- className: ({ isActive }) => clsx12(
911
+ className: ({ isActive }) => clsx10(
1288
912
  "shrink-0 whitespace-nowrap border-b-2 pb-3 text-body-sm transition-colors",
1289
913
  isActive ? "border-brand-emphasis font-semibold text-brand-emphasis" : "border-transparent font-medium text-default-secondary hover:text-default"
1290
914
  ),
@@ -1298,7 +922,7 @@ function NavTabs({ items, className }) {
1298
922
  }
1299
923
 
1300
924
  // src/sidebar/index.tsx
1301
- import { Fragment as Fragment3, jsx as jsx10, jsxs as jsxs4 } from "react/jsx-runtime";
925
+ import { Fragment as Fragment3, jsx as jsx11, jsxs as jsxs4 } from "react/jsx-runtime";
1302
926
  function AppSidebar({
1303
927
  groups,
1304
928
  footerNav,
@@ -1307,14 +931,20 @@ function AppSidebar({
1307
931
  collapsible = true,
1308
932
  collapsed: collapsedProp = false,
1309
933
  onToggle,
1310
- size: size2 = "md",
934
+ size = "md",
1311
935
  variant = "card",
1312
936
  width = "w-80",
1313
937
  collapsedWidth = "w-20",
1314
938
  className
1315
939
  }) {
1316
940
  const collapsed = collapsible && collapsedProp;
1317
- return /* @__PURE__ */ jsxs4(
941
+ const hashTargets = useMemo2(
942
+ () => new Set(
943
+ [...groups.flatMap((group) => group.items), ...footerNav ?? []].map((item) => item.to).filter((to) => to.includes("#"))
944
+ ),
945
+ [groups, footerNav]
946
+ );
947
+ return /* @__PURE__ */ jsx11(HashTargets.Provider, { value: hashTargets, children: /* @__PURE__ */ jsxs4(
1318
948
  "aside",
1319
949
  {
1320
950
  className: cn(
@@ -1324,7 +954,7 @@ function AppSidebar({
1324
954
  className
1325
955
  ),
1326
956
  children: [
1327
- collapsible && onToggle && /* @__PURE__ */ jsx10(
957
+ collapsible && onToggle && /* @__PURE__ */ jsx11(
1328
958
  "button",
1329
959
  {
1330
960
  type: "button",
@@ -1340,10 +970,10 @@ function AppSidebar({
1340
970
  "shadow-100 transition-colors hover:bg-brand-tertiary-hover hover:text-brand",
1341
971
  "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand focus-visible:ring-offset-1 focus-visible:ring-offset-default"
1342
972
  ),
1343
- children: collapsed ? /* @__PURE__ */ jsx10(LuChevronRight, { className: "size-3", strokeWidth: 2.5, "aria-hidden": true }) : /* @__PURE__ */ jsx10(LuChevronLeft, { className: "size-3", strokeWidth: 2.5, "aria-hidden": true })
973
+ children: collapsed ? /* @__PURE__ */ jsx11(ChevronRightIcon2, { className: "size-3", strokeWidth: 2.5, "aria-hidden": true }) : /* @__PURE__ */ jsx11(ChevronLeftIcon, { className: "size-3", strokeWidth: 2.5, "aria-hidden": true })
1344
974
  }
1345
975
  ),
1346
- brand && /* @__PURE__ */ jsx10(
976
+ brand && /* @__PURE__ */ jsx11(
1347
977
  "div",
1348
978
  {
1349
979
  className: cn(
@@ -1353,24 +983,24 @@ function AppSidebar({
1353
983
  children: typeof brand === "function" ? brand({ collapsed }) : brand
1354
984
  }
1355
985
  ),
1356
- /* @__PURE__ */ jsx10(ScrollArea, { axis: "vertical", label: "Sidebar navigation", className: "min-h-0 flex-1", children: /* @__PURE__ */ jsx10("nav", { className: "flex flex-col gap-4 px-4 pb-3 pt-2", children: groups.map((group, groupIndex) => /* @__PURE__ */ jsx10(
986
+ /* @__PURE__ */ jsx11(ScrollArea, { axis: "vertical", label: "Sidebar navigation", className: "min-h-0 flex-1", children: /* @__PURE__ */ jsx11("nav", { className: "flex flex-col gap-4 px-4 pb-3 pt-2", children: groups.map((group, groupIndex) => /* @__PURE__ */ jsx11(
1357
987
  SidebarGroup,
1358
988
  {
1359
989
  group,
1360
990
  showSeparator: groupIndex > 0,
1361
991
  collapsed,
1362
- size: size2,
992
+ size,
1363
993
  variant
1364
994
  },
1365
995
  group.title ?? `group-${groupIndex}`
1366
996
  )) }) }),
1367
997
  (footerNav?.length || footer) && /* @__PURE__ */ jsxs4("div", { className: "flex shrink-0 flex-col", children: [
1368
- footerNav && footerNav.length > 0 && /* @__PURE__ */ jsx10("nav", { className: "flex flex-col gap-3 px-4 pt-2", children: footerNav.map((item) => /* @__PURE__ */ jsx10(
998
+ footerNav && footerNav.length > 0 && /* @__PURE__ */ jsx11("nav", { className: "flex flex-col gap-3 px-4 pt-2", children: footerNav.map((item) => /* @__PURE__ */ jsx11(
1369
999
  SidebarLink,
1370
1000
  {
1371
1001
  item,
1372
1002
  collapsed,
1373
- size: size2,
1003
+ size,
1374
1004
  variant
1375
1005
  },
1376
1006
  item.to
@@ -1379,13 +1009,13 @@ function AppSidebar({
1379
1009
  ] })
1380
1010
  ]
1381
1011
  }
1382
- );
1012
+ ) });
1383
1013
  }
1384
1014
  function SidebarGroup({
1385
1015
  group,
1386
1016
  showSeparator,
1387
1017
  collapsed,
1388
- size: size2,
1018
+ size,
1389
1019
  variant
1390
1020
  }) {
1391
1021
  return (
@@ -1393,8 +1023,8 @@ function SidebarGroup({
1393
1023
  // stack. Through 0.5.0 this was `gap-1`, which packed them into a near-continuous block.
1394
1024
  // The same gap spaces the separator and the heading, so neither needs a margin of its own.
1395
1025
  /* @__PURE__ */ jsxs4("div", { className: "flex flex-col gap-3", children: [
1396
- showSeparator && /* @__PURE__ */ jsx10(SidebarSeparator, {}),
1397
- group.title && /* @__PURE__ */ jsx10(
1026
+ showSeparator && /* @__PURE__ */ jsx11(SidebarSeparator, {}),
1027
+ group.title && /* @__PURE__ */ jsx11(
1398
1028
  "p",
1399
1029
  {
1400
1030
  className: cn(
@@ -1407,12 +1037,12 @@ function SidebarGroup({
1407
1037
  children: group.title
1408
1038
  }
1409
1039
  ),
1410
- group.items.map((item) => /* @__PURE__ */ jsx10(
1040
+ group.items.map((item) => /* @__PURE__ */ jsx11(
1411
1041
  SidebarLink,
1412
1042
  {
1413
1043
  item,
1414
1044
  collapsed,
1415
- size: size2,
1045
+ size,
1416
1046
  variant
1417
1047
  },
1418
1048
  item.to
@@ -1420,37 +1050,52 @@ function SidebarGroup({
1420
1050
  ] })
1421
1051
  );
1422
1052
  }
1053
+ var HashTargets = createContext4(/* @__PURE__ */ new Set());
1054
+ function useRowActive(item) {
1055
+ const location = useLocation();
1056
+ const hashTargets = useContext4(HashTargets);
1057
+ const [path, hash] = item.to.split("#");
1058
+ const pathMatches = item.end ? location.pathname === path : location.pathname === path || location.pathname.startsWith(`${path.replace(/\/$/, "")}/`);
1059
+ if (!pathMatches) return false;
1060
+ if (hash !== void 0) return location.hash === `#${hash}`;
1061
+ return !hashTargets.has(`${location.pathname}${location.hash}`);
1062
+ }
1423
1063
  function SidebarLink({
1424
1064
  item,
1425
1065
  collapsed,
1426
- size: size2,
1066
+ size,
1427
1067
  variant
1428
1068
  }) {
1429
- const link = /* @__PURE__ */ jsx10(
1430
- NavLink2,
1069
+ const active = useRowActive(item);
1070
+ const link = /* @__PURE__ */ jsx11(
1071
+ Link,
1431
1072
  {
1432
1073
  to: item.to,
1433
- end: item.end,
1074
+ "aria-current": active ? "page" : void 0,
1075
+ "aria-disabled": item.disabled || void 0,
1076
+ tabIndex: item.disabled ? -1 : void 0,
1077
+ onClick: item.disabled ? (event) => event.preventDefault() : void 0,
1434
1078
  className: cn("block w-full no-underline", collapsed && "flex justify-center"),
1435
- children: ({ isActive }) => /* @__PURE__ */ jsx10(
1079
+ children: /* @__PURE__ */ jsx11(
1436
1080
  NavItem,
1437
1081
  {
1438
1082
  icon: item.icon,
1439
1083
  label: item.label,
1440
- isActive,
1084
+ isActive: active,
1441
1085
  indicator: item.indicator,
1086
+ disabled: item.disabled,
1442
1087
  collapsed,
1443
- size: size2,
1088
+ size,
1444
1089
  variant
1445
1090
  }
1446
1091
  )
1447
1092
  }
1448
1093
  );
1449
1094
  if (!collapsed) return link;
1450
- return /* @__PURE__ */ jsx10(Tooltip, { title: item.label, side: "right", children: link });
1095
+ return /* @__PURE__ */ jsx11(Tooltip, { title: item.label, side: "right", children: link });
1451
1096
  }
1452
1097
  function SidebarSeparator({ className, ...rest }) {
1453
- return /* @__PURE__ */ jsx10("div", { "aria-hidden": true, className: cn("mx-1 h-px shrink-0 bg-divider", className), ...rest });
1098
+ return /* @__PURE__ */ jsx11("div", { "aria-hidden": true, className: cn("mx-1 h-px shrink-0 bg-divider", className), ...rest });
1454
1099
  }
1455
1100
  function SidebarUserCard({
1456
1101
  name,
@@ -1463,38 +1108,38 @@ function SidebarUserCard({
1463
1108
  const initial = name.trim().charAt(0);
1464
1109
  const items = /* @__PURE__ */ jsxs4(Fragment3, { children: [
1465
1110
  menu,
1466
- /* @__PURE__ */ jsx10(
1467
- MenuItem,
1111
+ /* @__PURE__ */ jsx11(
1112
+ DropdownMenuItem,
1468
1113
  {
1469
- icon: /* @__PURE__ */ jsx10(LuLogOut, { className: "size-3.5", "aria-hidden": true }),
1114
+ icon: /* @__PURE__ */ jsx11(LogOutIcon, { className: "size-3.5", "aria-hidden": true }),
1470
1115
  onSelect: onLogout,
1471
1116
  children: "Sign out"
1472
1117
  }
1473
1118
  )
1474
1119
  ] });
1475
1120
  if (collapsed) {
1476
- return /* @__PURE__ */ jsx10("div", { className: cn("flex shrink-0 items-center justify-center p-4", className), children: /* @__PURE__ */ jsxs4(DropdownMenu, { children: [
1477
- /* @__PURE__ */ jsx10(DropdownMenuTrigger, { asChild: true, label: "User options", children: /* @__PURE__ */ jsx10("button", { type: "button", "aria-label": `${name} \u2014 user options`, className: "rounded-full", children: /* @__PURE__ */ jsx10(Avatar, { initial, size: "lg", shape: "circle" }) }) }),
1478
- /* @__PURE__ */ jsx10(DropdownMenuContent, { side: "right", align: "end", children: items })
1121
+ return /* @__PURE__ */ jsx11("div", { className: cn("flex shrink-0 items-center justify-center p-4", className), children: /* @__PURE__ */ jsxs4(DropdownMenu, { children: [
1122
+ /* @__PURE__ */ jsx11(DropdownMenuTrigger, { asChild: true, label: "User options", children: /* @__PURE__ */ jsx11("button", { type: "button", "aria-label": `${name} \u2014 user options`, className: "rounded-full", children: /* @__PURE__ */ jsx11(Avatar, { initial, size: "lg", shape: "circle" }) }) }),
1123
+ /* @__PURE__ */ jsx11(DropdownMenuContent, { side: "right", align: "end", children: items })
1479
1124
  ] }) });
1480
1125
  }
1481
1126
  return /* @__PURE__ */ jsxs4("div", { className: cn("flex shrink-0 items-center gap-2.5 p-4", className), children: [
1482
- /* @__PURE__ */ jsx10(Avatar, { initial, size: "lg", shape: "circle" }),
1127
+ /* @__PURE__ */ jsx11(Avatar, { initial, size: "lg", shape: "circle" }),
1483
1128
  /* @__PURE__ */ jsxs4("div", { className: "flex min-w-0 flex-1 flex-col gap-0.5", children: [
1484
- /* @__PURE__ */ jsx10("span", { className: "truncate text-body-sm font-semibold leading-snug text-default", children: name }),
1485
- /* @__PURE__ */ jsx10("span", { className: "truncate text-body-tiny font-regular leading-none text-default-secondary", children: role })
1129
+ /* @__PURE__ */ jsx11("span", { className: "truncate text-body-sm font-semibold leading-snug text-default", children: name }),
1130
+ /* @__PURE__ */ jsx11("span", { className: "truncate text-body-tiny font-regular leading-none text-default-secondary", children: role })
1486
1131
  ] }),
1487
1132
  /* @__PURE__ */ jsxs4(DropdownMenu, { children: [
1488
- /* @__PURE__ */ jsx10(DropdownMenuTrigger, { asChild: true, label: "User options", children: /* @__PURE__ */ jsx10(
1133
+ /* @__PURE__ */ jsx11(DropdownMenuTrigger, { asChild: true, label: "User options", children: /* @__PURE__ */ jsx11(
1489
1134
  "button",
1490
1135
  {
1491
1136
  type: "button",
1492
1137
  "aria-label": `${name} \u2014 user options`,
1493
1138
  className: "flex size-6 shrink-0 items-center justify-center rounded-md text-default-secondary transition-colors hover:bg-brand-tertiary-hover hover:text-brand",
1494
- children: /* @__PURE__ */ jsx10(LuEllipsisVertical, { className: "size-5", "aria-hidden": true })
1139
+ children: /* @__PURE__ */ jsx11(EllipsisVerticalIcon, { className: "size-5", "aria-hidden": true })
1495
1140
  }
1496
1141
  ) }),
1497
- /* @__PURE__ */ jsx10(DropdownMenuContent, { side: "top", align: "end", children: items })
1142
+ /* @__PURE__ */ jsx11(DropdownMenuContent, { side: "top", align: "end", children: items })
1498
1143
  ] })
1499
1144
  ] });
1500
1145
  }