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