@platned/ui 0.1.4 → 0.5.1

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,15 +1,134 @@
1
1
  // src/sidebar/index.tsx
2
- import { useEffect, useRef, useState } from "react";
3
- import { NavLink } from "react-router-dom";
4
- import clsx4 from "clsx";
5
-
6
- // src/index.tsx
7
- import clsx3 from "clsx";
2
+ import { NavLink as NavLink2 } from "react-router-dom";
3
+ import { LuChevronLeft, LuChevronRight, LuEllipsisVertical, LuLogOut } from "react-icons/lu";
8
4
 
9
- // src/components/avatar.tsx
5
+ // src/lib/slot.tsx
6
+ import {
7
+ cloneElement,
8
+ forwardRef,
9
+ isValidElement
10
+ } from "react";
10
11
  import clsx from "clsx";
12
+ var Slot = forwardRef(function Slot2({ children, wrapChildren, ...slotProps }, forwardedRef) {
13
+ if (!isValidElement(children)) return null;
14
+ const child = children;
15
+ const merged = {
16
+ ...mergeProps(slotProps, child.props),
17
+ ref: composeRefs(forwardedRef, getElementRef(child))
18
+ };
19
+ return wrapChildren ? cloneElement(child, merged, wrapChildren(child.props.children)) : cloneElement(child, merged);
20
+ });
21
+ function mergeProps(slotProps, childProps) {
22
+ const merged = { ...slotProps };
23
+ for (const key in childProps) {
24
+ const slotValue = slotProps[key];
25
+ const childValue = childProps[key];
26
+ if (/^on[A-Z]/.test(key)) {
27
+ if (typeof slotValue === "function" && typeof childValue === "function") {
28
+ merged[key] = (...args) => {
29
+ slotValue(...args);
30
+ childValue(...args);
31
+ };
32
+ } else {
33
+ merged[key] = childValue ?? slotValue;
34
+ }
35
+ } else if (key === "className") {
36
+ merged[key] = clsx(slotValue, childValue);
37
+ } else if (key === "style") {
38
+ merged[key] = { ...slotValue, ...childValue };
39
+ } else {
40
+ merged[key] = childValue;
41
+ }
42
+ }
43
+ return merged;
44
+ }
45
+ function getElementRef(element) {
46
+ const asRecord = element;
47
+ return asRecord.props?.ref ?? asRecord.ref;
48
+ }
49
+ function composeRefs(...refs) {
50
+ return (node) => {
51
+ for (const ref of refs) {
52
+ if (!ref) continue;
53
+ if (typeof ref === "function") {
54
+ ref(node);
55
+ } else {
56
+ ref.current = node;
57
+ }
58
+ }
59
+ };
60
+ }
61
+
62
+ // src/components/forms/control-size.tsx
63
+ import { createContext, useContext } from "react";
11
64
  import { jsx } from "react/jsx-runtime";
65
+ var ControlSizeContext = createContext(null);
66
+ function useControlSize(explicit) {
67
+ const inherited = useContext(ControlSizeContext);
68
+ return explicit ?? inherited ?? "md";
69
+ }
70
+
71
+ // src/components/forms/icon-button.tsx
72
+ import { forwardRef as forwardRef2 } from "react";
73
+ import clsx2 from "clsx";
74
+ import { jsx as jsx2 } from "react/jsx-runtime";
75
+ var variantStyles = {
76
+ primary: clsx2(
77
+ "bg-brand border border-brand text-brand-on-brand",
78
+ "hover:bg-brand-hover",
79
+ "disabled:bg-disabled disabled:border-disabled disabled:text-disabled-on-disabled"
80
+ ),
81
+ // Neutral uses secondary background (light gray) for visual weight without a text label
82
+ neutral: clsx2(
83
+ "bg-default-secondary border border-default text-default",
84
+ // icon turns brand blue on hover (Figma spec)
85
+ "hover:bg-default-secondary-hover hover:text-brand",
86
+ "disabled:bg-disabled disabled:border-disabled disabled:text-disabled-on-disabled"
87
+ ),
88
+ subtle: clsx2(
89
+ "bg-transparent text-neutral",
90
+ // hover uses background-default-default-hover (#f5f5f5), icon turns brand blue (Figma spec)
91
+ "hover:bg-default-hover hover:text-brand",
92
+ // disabled needs explicit border since there is no base border
93
+ "disabled:bg-disabled disabled:border disabled:border-disabled disabled:text-disabled-on-disabled"
94
+ )
95
+ };
12
96
  var sizeStyles = {
97
+ lg: "size-12 rounded-2xl [&_svg]:size-5",
98
+ md: "size-9 rounded-lg [&_svg]:size-5",
99
+ sm: "size-6 rounded-full [&_svg]:size-4"
100
+ };
101
+ var IconButton = forwardRef2(function IconButton2({ variant = "primary", size: sizeProp, asChild = false, type, children, className, ...props }, ref) {
102
+ const size2 = useControlSize(sizeProp);
103
+ const rootProps = {
104
+ className: clsx2(
105
+ "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",
108
+ variantStyles[variant],
109
+ sizeStyles[size2],
110
+ className
111
+ ),
112
+ ...props
113
+ };
114
+ if (asChild) {
115
+ return /* @__PURE__ */ jsx2(Slot, { ref, ...rootProps, children });
116
+ }
117
+ return /* @__PURE__ */ jsx2(
118
+ "button",
119
+ {
120
+ ref,
121
+ type: type ?? "button",
122
+ ...rootProps,
123
+ children
124
+ }
125
+ );
126
+ });
127
+
128
+ // src/components/avatar/avatar.tsx
129
+ import clsx3 from "clsx";
130
+ import { jsx as jsx3 } from "react/jsx-runtime";
131
+ var sizeStyles2 = {
13
132
  lg: "size-10",
14
133
  md: "size-8",
15
134
  sm: "size-6"
@@ -19,20 +138,20 @@ var initialTextStyles = {
19
138
  md: "text-body-xs font-semibold",
20
139
  sm: "text-body-tiny font-semibold"
21
140
  };
22
- function getShapeClass(shape, size) {
141
+ function getShapeClass(shape, size2) {
23
142
  if (shape === "circle") return "rounded-full";
24
- return size === "sm" ? "rounded" : "rounded-lg";
143
+ return size2 === "sm" ? "rounded" : "rounded-lg";
25
144
  }
26
145
  function Avatar(props) {
27
- const { size = "lg", shape = "circle", className } = props;
28
- const base = clsx(
146
+ const { size: size2 = "lg", shape = "circle", className } = props;
147
+ const base = clsx3(
29
148
  "shrink-0 overflow-hidden",
30
- sizeStyles[size],
31
- getShapeClass(shape, size),
149
+ sizeStyles2[size2],
150
+ getShapeClass(shape, size2),
32
151
  className
33
152
  );
34
153
  if (props.type === "image") {
35
- return /* @__PURE__ */ jsx("div", { className: base, children: /* @__PURE__ */ jsx(
154
+ return /* @__PURE__ */ jsx3("div", { className: base, children: /* @__PURE__ */ jsx3(
36
155
  "img",
37
156
  {
38
157
  src: props.src,
@@ -41,19 +160,19 @@ function Avatar(props) {
41
160
  }
42
161
  ) });
43
162
  }
44
- return /* @__PURE__ */ jsx(
163
+ return /* @__PURE__ */ jsx3(
45
164
  "div",
46
165
  {
47
- className: clsx(base, "flex items-center justify-center"),
166
+ className: clsx3(base, "flex items-center justify-center"),
48
167
  style: {
49
168
  background: "linear-gradient(135deg, var(--color-graphic-blue) 30%, var(--color-graphic-red) 100%)"
50
169
  },
51
- children: /* @__PURE__ */ jsx(
170
+ children: /* @__PURE__ */ jsx3(
52
171
  "span",
53
172
  {
54
- className: clsx(
173
+ className: clsx3(
55
174
  "select-none leading-none text-brand-on-brand",
56
- initialTextStyles[size]
175
+ initialTextStyles[size2]
57
176
  ),
58
177
  children: props.initial.charAt(0).toUpperCase()
59
178
  }
@@ -62,306 +181,1327 @@ function Avatar(props) {
62
181
  );
63
182
  }
64
183
 
65
- // src/components/nav-item.tsx
66
- import clsx2 from "clsx";
67
- import { jsx as jsx2, jsxs } from "react/jsx-runtime";
68
- function NavItem({
69
- icon,
70
- label,
71
- isActive = false,
72
- indicator = false,
73
- collapsed = false,
74
- className
75
- }) {
76
- return /* @__PURE__ */ jsxs(
77
- "span",
78
- {
79
- className: clsx2(
80
- "relative flex h-11 w-full items-center gap-3 rounded-xl px-3.5 py-2.5 transition-colors",
81
- collapsed && "justify-center",
82
- isActive ? "bg-brand text-brand-on-brand" : [
83
- "border border-default bg-default text-default-secondary",
84
- "hover:bg-brand-tertiary-hover hover:border-brand-tertiary hover:text-brand"
85
- ],
86
- className
87
- ),
88
- children: [
89
- icon !== void 0 && /* @__PURE__ */ jsx2("span", { className: "shrink-0 [&_svg]:size-4", children: icon }),
90
- !collapsed && /* @__PURE__ */ jsx2("span", { className: "flex-1 min-w-0 truncate text-body-sm font-regular leading-snug", children: label }),
91
- indicator && /* @__PURE__ */ jsx2("span", { className: "absolute -right-px -top-px size-2 rounded-full bg-danger" })
92
- ]
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] }]
93
223
  }
94
- );
224
+ }
225
+ });
226
+ function cn(...inputs) {
227
+ return twMerge(clsx4(inputs));
95
228
  }
96
229
 
97
- // src/index.tsx
98
- import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
99
-
100
- // src/sidebar/index.tsx
101
- import { Fragment, jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
102
- function GridIcon() {
103
- return /* @__PURE__ */ jsxs3("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true, children: [
104
- /* @__PURE__ */ jsx4("rect", { x: "3", y: "3", width: "7", height: "7", rx: "1" }),
105
- /* @__PURE__ */ jsx4("rect", { x: "14", y: "3", width: "7", height: "7", rx: "1" }),
106
- /* @__PURE__ */ jsx4("rect", { x: "14", y: "14", width: "7", height: "7", rx: "1" }),
107
- /* @__PURE__ */ jsx4("rect", { x: "3", y: "14", width: "7", height: "7", rx: "1" })
108
- ] });
230
+ // src/lib/use-controllable-state.ts
231
+ import { useCallback, useRef, useState } from "react";
232
+ function useControllableState({
233
+ value,
234
+ defaultValue,
235
+ onChange
236
+ }) {
237
+ const [internal, setInternal] = useState(defaultValue);
238
+ const isControlled = value !== void 0;
239
+ const current = isControlled ? value : internal;
240
+ const onChangeRef = useRef(onChange);
241
+ onChangeRef.current = onChange;
242
+ const currentRef = useRef(current);
243
+ currentRef.current = current;
244
+ const isControlledRef = useRef(isControlled);
245
+ isControlledRef.current = isControlled;
246
+ const setValue = useCallback((next) => {
247
+ const resolved = typeof next === "function" ? next(currentRef.current) : next;
248
+ if (resolved === currentRef.current) return;
249
+ if (!isControlledRef.current) setInternal(resolved);
250
+ onChangeRef.current?.(resolved);
251
+ }, []);
252
+ return [current, setValue];
109
253
  }
110
- function LayersIcon() {
111
- return /* @__PURE__ */ jsxs3("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true, children: [
112
- /* @__PURE__ */ jsx4("polygon", { points: "12 2 2 7 12 12 22 7 12 2" }),
113
- /* @__PURE__ */ jsx4("polyline", { points: "2 17 12 22 22 17" }),
114
- /* @__PURE__ */ jsx4("polyline", { points: "2 12 12 17 22 12" })
115
- ] });
254
+
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
+ };
116
386
  }
117
- function BuildingIcon() {
118
- return /* @__PURE__ */ jsxs3("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true, children: [
119
- /* @__PURE__ */ jsx4("rect", { x: "2", y: "7", width: "20", height: "14", rx: "2" }),
120
- /* @__PURE__ */ jsx4("path", { d: "M16 21V5a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v16" })
121
- ] });
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 };
122
444
  }
123
- function UsersIcon() {
124
- return /* @__PURE__ */ jsxs3("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true, children: [
125
- /* @__PURE__ */ jsx4("path", { d: "M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2" }),
126
- /* @__PURE__ */ jsx4("circle", { cx: "9", cy: "7", r: "4" }),
127
- /* @__PURE__ */ jsx4("path", { d: "M23 21v-2a4 4 0 0 0-3-3.87" }),
128
- /* @__PURE__ */ jsx4("path", { d: "M16 3.13a4 4 0 0 1 0 7.75" })
129
- ] });
445
+
446
+ // src/components/overlays/menu-styles.ts
447
+ import clsx6 from "clsx";
448
+
449
+ // src/lib/scrollbar.ts
450
+ import clsx5 from "clsx";
451
+ var SCROLLBAR_CLASSES = clsx5(
452
+ // Firefox and the standard property.
453
+ "[scrollbar-color:var(--color-background-default-tertiary)_transparent] [scrollbar-width:thin]",
454
+ // WebKit/Blink.
455
+ "[&::-webkit-scrollbar]:size-2",
456
+ "[&::-webkit-scrollbar-track]:bg-transparent",
457
+ "[&::-webkit-scrollbar-thumb]:rounded-full [&::-webkit-scrollbar-thumb]:bg-default-tertiary",
458
+ "[&::-webkit-scrollbar-thumb:hover]:bg-neutral-secondary",
459
+ "[&::-webkit-scrollbar-corner]:bg-transparent"
460
+ );
461
+
462
+ // src/components/overlays/menu-styles.ts
463
+ var OVERLAY_PANEL = "rounded-lg border border-default bg-default p-1 shadow-300";
464
+ var OVERLAY_PANEL_MENU = clsx6(OVERLAY_PANEL, "min-w-[168px]");
465
+ var OVERLAY_PANEL_SCROLL = clsx6(
466
+ "max-h-[min(16rem,var(--panel-available-height,16rem))] overflow-y-auto",
467
+ // A floating panel is already its own scroll container and keeps focus on the input or a
468
+ // roving option, so it needs the scrollbar styling but not `ScrollArea`'s tab stop.
469
+ SCROLLBAR_CLASSES
470
+ );
471
+ var OVERLAY_ITEM_BASE = "flex w-full cursor-pointer select-none items-start gap-2 rounded-md text-left outline-none transition-colors";
472
+ var OVERLAY_ITEM_SIZE = {
473
+ lg: "px-3 py-2.5 text-body",
474
+ md: "px-2.5 py-2 text-body-sm",
475
+ sm: "px-2 py-1.5 text-body-tiny"
476
+ };
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";
130
480
  }
131
- function RssIcon() {
132
- return /* @__PURE__ */ jsxs3("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true, children: [
133
- /* @__PURE__ */ jsx4("path", { d: "M4 11a9 9 0 0 1 9 9" }),
134
- /* @__PURE__ */ jsx4("path", { d: "M4 4a16 16 0 0 1 16 16" }),
135
- /* @__PURE__ */ jsx4("circle", { cx: "5", cy: "19", r: "1", fill: "currentColor" })
136
- ] });
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();
137
492
  }
138
- function FileTextIcon() {
139
- return /* @__PURE__ */ jsxs3("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true, children: [
140
- /* @__PURE__ */ jsx4("path", { d: "M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" }),
141
- /* @__PURE__ */ jsx4("polyline", { points: "14 2 14 8 20 8" }),
142
- /* @__PURE__ */ jsx4("line", { x1: "16", y1: "13", x2: "8", y2: "13" }),
143
- /* @__PURE__ */ jsx4("line", { x1: "16", y1: "17", x2: "8", y2: "17" })
144
- ] });
493
+ function topmost() {
494
+ return layers[layers.length - 1]?.id;
145
495
  }
146
- function BellIcon() {
147
- return /* @__PURE__ */ jsxs3("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true, children: [
148
- /* @__PURE__ */ jsx4("path", { d: "M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9" }),
149
- /* @__PURE__ */ jsx4("path", { d: "M13.73 21a2 2 0 0 1-3.46 0" })
150
- ] });
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
+ };
151
514
  }
152
- function ServerIcon() {
153
- return /* @__PURE__ */ jsxs3("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true, children: [
154
- /* @__PURE__ */ jsx4("rect", { x: "2", y: "2", width: "20", height: "8", rx: "2" }),
155
- /* @__PURE__ */ jsx4("rect", { x: "2", y: "14", width: "20", height: "8", rx: "2" }),
156
- /* @__PURE__ */ jsx4("line", { x1: "6", y1: "6", x2: "6.01", y2: "6" }),
157
- /* @__PURE__ */ jsx4("line", { x1: "6", y1: "18", x2: "6.01", y2: "18" })
158
- ] });
515
+ function unlockScroll() {
516
+ scrollLocks = Math.max(0, scrollLocks - 1);
517
+ if (scrollLocks === 0 && restoreBody) {
518
+ restoreBody();
519
+ restoreBody = null;
520
+ }
159
521
  }
160
- function SettingsIcon() {
161
- return /* @__PURE__ */ jsxs3("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true, children: [
162
- /* @__PURE__ */ jsx4("circle", { cx: "12", cy: "12", r: "3" }),
163
- /* @__PURE__ */ jsx4("path", { d: "M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z" })
164
- ] });
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
+ }
165
535
  }
166
- function ClipboardListIcon() {
167
- return /* @__PURE__ */ jsxs3("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true, children: [
168
- /* @__PURE__ */ jsx4("path", { d: "M9 5H7a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2h-2" }),
169
- /* @__PURE__ */ jsx4("rect", { x: "9", y: "3", width: "6", height: "4", rx: "1" }),
170
- /* @__PURE__ */ jsx4("line", { x1: "9", y1: "12", x2: "15", y2: "12" }),
171
- /* @__PURE__ */ jsx4("line", { x1: "9", y1: "16", x2: "13", y2: "16" })
172
- ] });
536
+ function releaseInert() {
537
+ for (const el of inerted) el.removeAttribute("inert");
538
+ inerted.clear();
173
539
  }
174
- function GlobeIcon() {
175
- return /* @__PURE__ */ jsxs3("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true, children: [
176
- /* @__PURE__ */ jsx4("circle", { cx: "12", cy: "12", r: "10" }),
177
- /* @__PURE__ */ jsx4("line", { x1: "2", y1: "12", x2: "22", y2: "12" }),
178
- /* @__PURE__ */ jsx4("path", { d: "M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z" })
179
- ] });
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 };
180
573
  }
181
- function ChevronLeftIcon() {
182
- return /* @__PURE__ */ jsx4("svg", { width: "12", height: "12", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.5", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true, children: /* @__PURE__ */ jsx4("polyline", { points: "15 18 9 12 15 6" }) });
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]);
183
701
  }
184
- function ChevronRightIcon() {
185
- return /* @__PURE__ */ jsx4("svg", { width: "12", height: "12", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.5", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true, children: /* @__PURE__ */ jsx4("polyline", { points: "9 18 15 12 9 6" }) });
702
+
703
+ // src/components/overlays/portal.tsx
704
+ import { useEffect as useEffect4, useState as useState4 } from "react";
705
+ 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);
186
713
  }
187
- function MoreVerticalIcon() {
188
- return /* @__PURE__ */ jsxs3("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true, children: [
189
- /* @__PURE__ */ jsx4("circle", { cx: "12", cy: "5", r: "1", fill: "currentColor" }),
190
- /* @__PURE__ */ jsx4("circle", { cx: "12", cy: "12", r: "1", fill: "currentColor" }),
191
- /* @__PURE__ */ jsx4("circle", { cx: "12", cy: "19", r: "1", fill: "currentColor" })
714
+
715
+ // 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";
727
+ function Tooltip({
728
+ children,
729
+ title,
730
+ body,
731
+ side = "top",
732
+ align = "center",
733
+ openDelay = 150,
734
+ closeDelay = 0,
735
+ open: openProp,
736
+ defaultOpen = false,
737
+ onOpenChange,
738
+ association = "describedby",
739
+ className
740
+ }) {
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;
761
+ }
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
+ ),
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",
820
+ {
821
+ ref: refs.setFloating,
822
+ id: tooltipId,
823
+ role: "tooltip",
824
+ style: floatingStyles,
825
+ className: clsx7(
826
+ "z-tooltip max-w-[260px] rounded-lg bg-neutral px-3 py-2 shadow-300",
827
+ "text-body-xs text-neutral-on-neutral",
828
+ !open && "sr-only",
829
+ className
830
+ ),
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
+ ]
835
+ }
836
+ ) })
192
837
  ] });
193
838
  }
194
- function LogOutIcon() {
195
- return /* @__PURE__ */ jsxs3("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true, children: [
196
- /* @__PURE__ */ jsx4("path", { d: "M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" }),
197
- /* @__PURE__ */ jsx4("polyline", { points: "16 17 21 12 16 7" }),
198
- /* @__PURE__ */ jsx4("line", { x1: "21", y1: "12", x2: "9", y2: "12" })
199
- ] });
839
+
840
+ // src/components/overlays/menu-core.tsx
841
+ import {
842
+ createContext as createContext2,
843
+ useCallback as useCallback4,
844
+ useContext as useContext2,
845
+ useId as useId2,
846
+ useMemo,
847
+ useRef as useRef7
848
+ } 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;
859
+ }
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)
869
+ });
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
+ );
200
887
  }
201
- function SidebarLogo({ collapsed }) {
202
- return /* @__PURE__ */ jsxs3(
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?.();
912
+ };
913
+ return /* @__PURE__ */ jsxs2(
203
914
  "div",
204
915
  {
205
- className: clsx4(
206
- "flex shrink-0 items-center overflow-hidden p-4",
207
- collapsed ? "justify-center" : "gap-2.5"
208
- ),
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,
209
933
  children: [
210
- /* @__PURE__ */ jsx4("div", { className: "flex shrink-0 size-10 items-center justify-center rounded-xl bg-brand-hover overflow-hidden", children: /* @__PURE__ */ jsxs3("div", { className: "grid grid-cols-2 gap-[3px]", children: [
211
- /* @__PURE__ */ jsx4("div", { className: "size-2 rounded-[2px] bg-white" }),
212
- /* @__PURE__ */ jsx4("div", { className: "size-2 rounded-[2px] bg-white/50" }),
213
- /* @__PURE__ */ jsx4("div", { className: "size-2 rounded-[2px] bg-white/50" }),
214
- /* @__PURE__ */ jsx4("div", { className: "size-2 rounded-[2px] bg-white" })
215
- ] }) }),
216
- !collapsed && /* @__PURE__ */ jsx4("span", { className: "text-heading font-semibold text-default tracking-tight whitespace-nowrap", children: "SCIC" })
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 })
217
940
  ]
218
941
  }
219
942
  );
220
943
  }
221
- function NavLinkItem({ to, icon, label, indicator, collapsed, end }) {
222
- return /* @__PURE__ */ jsx4(NavLink, { to, end, className: "block w-full no-underline", children: ({ isActive }) => /* @__PURE__ */ jsx4(
223
- NavItem,
944
+ var MenuRadioContext = createContext2(null);
945
+
946
+ // 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";
950
+ var OVERFLOW = {
951
+ vertical: "overflow-y-auto overflow-x-hidden",
952
+ horizontal: "overflow-x-auto overflow-y-hidden",
953
+ both: "overflow-auto"
954
+ };
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(() => {
959
+ const node = localRef.current;
960
+ if (!node) return;
961
+ const measure = () => {
962
+ const vertical = node.scrollHeight > node.clientHeight + 1;
963
+ const horizontal = node.scrollWidth > node.clientWidth + 1;
964
+ setOverflows(
965
+ axis === "vertical" ? vertical : axis === "horizontal" ? horizontal : vertical || horizontal
966
+ );
967
+ };
968
+ measure();
969
+ if (typeof ResizeObserver === "undefined") return;
970
+ const observer = new ResizeObserver(measure);
971
+ observer.observe(node);
972
+ if (node.firstElementChild) observer.observe(node.firstElementChild);
973
+ return () => observer.disconnect();
974
+ }, [axis, children]);
975
+ return /* @__PURE__ */ jsx6(
976
+ "div",
224
977
  {
225
- icon,
226
- label,
227
- isActive,
228
- indicator,
229
- collapsed
978
+ ref: composeRefs(localRef, ref),
979
+ role: overflows && label ? "region" : void 0,
980
+ "aria-label": overflows ? label : void 0,
981
+ tabIndex: overflows ? 0 : void 0,
982
+ "data-slot": "scroll-area",
983
+ className: clsx9(
984
+ OVERFLOW[axis],
985
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand focus-visible:ring-offset-1 focus-visible:ring-offset-default",
986
+ SCROLLBAR_CLASSES,
987
+ className
988
+ ),
989
+ ...rest,
990
+ children
230
991
  }
231
- ) });
992
+ );
993
+ });
994
+
995
+ // src/components/overlays/dropdown-menu.tsx
996
+ import {
997
+ createContext as createContext3,
998
+ useContext as useContext3,
999
+ useEffect as useEffect7,
1000
+ useMemo as useMemo2,
1001
+ useRef as useRef9,
1002
+ useState as useState7
1003
+ } 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>");
1011
+ return value;
232
1012
  }
233
- function UserCard({ collapsed, name, role, onLogout }) {
234
- const [menuOpen, setMenuOpen] = useState(false);
235
- const menuRef = useRef(null);
236
- const initial = name.trim().charAt(0);
237
- useEffect(() => {
238
- if (!menuOpen) return;
239
- function handleClickOutside(e) {
240
- if (menuRef.current && !menuRef.current.contains(e.target)) {
241
- setMenuOpen(false);
1013
+ function DropdownMenu({
1014
+ children,
1015
+ open: openProp,
1016
+ defaultOpen = false,
1017
+ onOpenChange
1018
+ }) {
1019
+ const [open, setOpen] = useControllableState({
1020
+ value: openProp,
1021
+ defaultValue: defaultOpen,
1022
+ onChange: onOpenChange
1023
+ });
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 }) });
1028
+ }
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);
242
1051
  }
243
- }
244
- document.addEventListener("mousedown", handleClickOutside);
245
- return () => document.removeEventListener("mousedown", handleClickOutside);
246
- }, [menuOpen]);
247
- const menuItems = /* @__PURE__ */ jsxs3(
248
- "button",
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(),
1056
+ ...rest
1057
+ };
1058
+ if (asChild) {
1059
+ return /* @__PURE__ */ jsx7(Slot, { ref: composeRefs(anchor.setTriggerNode), ...triggerProps, children });
1060
+ }
1061
+ return /* @__PURE__ */ jsx7(
1062
+ IconButton,
249
1063
  {
250
- type: "button",
251
- onClick: () => {
252
- setMenuOpen(false);
253
- onLogout();
254
- },
255
- className: "flex w-full items-center gap-2 px-3 py-2 text-body-sm text-default hover:bg-default-hover transition-colors",
256
- children: [
257
- /* @__PURE__ */ jsx4(LogOutIcon, {}),
258
- "Sign out"
259
- ]
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 })
260
1070
  }
261
1071
  );
262
- return /* @__PURE__ */ jsx4(
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(
263
1120
  "div",
264
1121
  {
265
- className: clsx4(
266
- "flex shrink-0 items-center p-4",
267
- collapsed ? "justify-center" : "gap-2.5"
1122
+ ref: composeRefs(
1123
+ panelRef,
1124
+ (node) => setPanelNode(node),
1125
+ refs.setFloating
268
1126
  ),
269
- children: collapsed ? /* @__PURE__ */ jsxs3("div", { className: "relative", ref: menuRef, children: [
270
- /* @__PURE__ */ jsx4(
271
- "button",
272
- {
273
- type: "button",
274
- "aria-label": "User options",
275
- "aria-haspopup": "menu",
276
- "aria-expanded": menuOpen,
277
- onClick: () => setMenuOpen((v) => !v),
278
- className: "rounded-full",
279
- children: /* @__PURE__ */ jsx4(Avatar, { initial, size: "md", shape: "circle" })
280
- }
281
- ),
282
- menuOpen && /* @__PURE__ */ jsx4("div", { className: "absolute bottom-0 left-full ml-2 w-36 rounded-lg border border-default bg-default shadow-md py-1 z-20", children: menuItems })
283
- ] }) : /* @__PURE__ */ jsxs3(Fragment, { children: [
284
- /* @__PURE__ */ jsx4(Avatar, { initial, size: "md", shape: "circle" }),
285
- /* @__PURE__ */ jsxs3("div", { className: "flex flex-1 min-w-0 flex-col gap-0.5", children: [
286
- /* @__PURE__ */ jsx4("span", { className: "truncate text-body-sm font-semibold text-default leading-snug", children: name }),
287
- /* @__PURE__ */ jsx4("span", { className: "truncate text-body-tiny font-regular text-default-secondary leading-none", children: role })
288
- ] }),
289
- /* @__PURE__ */ jsxs3("div", { className: "relative shrink-0", ref: menuRef, children: [
290
- /* @__PURE__ */ jsx4(
291
- "button",
292
- {
293
- type: "button",
294
- "aria-label": "User options",
295
- "aria-haspopup": "menu",
296
- "aria-expanded": menuOpen,
297
- onClick: () => setMenuOpen((v) => !v),
298
- className: "flex size-6 items-center justify-center rounded-md text-default-secondary hover:bg-default-hover hover:text-default transition-colors",
299
- children: /* @__PURE__ */ jsx4(MoreVerticalIcon, {})
300
- }
301
- ),
302
- menuOpen && /* @__PURE__ */ jsx4("div", { className: "absolute bottom-full right-0 mb-1 w-36 rounded-lg border border-default bg-default shadow-md py-1 z-20", children: menuItems })
303
- ] })
304
- ] })
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-dropdown", 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
305
1141
  }
1142
+ ) });
1143
+ }
1144
+
1145
+ // 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";
1148
+ var SIZE = {
1149
+ sm: { box: "h-9 gap-2 rounded-lg px-3 py-1.5", label: "text-body-xs", square: "w-9" },
1150
+ md: { box: "h-11 gap-3 rounded-xl px-3.5 py-2.5", label: "text-body-sm", square: "w-11" }
1151
+ };
1152
+ function monogram(label) {
1153
+ const words = label.split(/\s+/).filter((word) => /^[\p{L}\p{N}]/u.test(word));
1154
+ const [first, second] = words;
1155
+ if (!first) return Array.from(label.trim())[0]?.toUpperCase() ?? "";
1156
+ if (second) return (Array.from(first)[0] + Array.from(second)[0]).toUpperCase();
1157
+ const [a, b] = Array.from(first);
1158
+ return a.toUpperCase() + (b ?? "").toLowerCase();
1159
+ }
1160
+ var STATE = {
1161
+ // Figma MenuButton `state=hover`. No border change: Figma holds it at #D9D9D9 through
1162
+ // hover and moves only the fill and the text.
1163
+ hover: "hover:bg-brand-tertiary-hover hover:text-brand",
1164
+ // Figma MenuButton `state=focussed`, which is that file's name for selected — see the
1165
+ // `isActive` doc comment. Hovering the current row darkens it rather than reverting it to
1166
+ // the pale hover tint, which would read as the row losing its selection under the cursor.
1167
+ active: "bg-brand text-brand-on-brand hover:bg-brand-hover"
1168
+ };
1169
+ var VARIANT = {
1170
+ card: {
1171
+ // `border-transparent` holds the 1px so the icon and label do not shift outward when a
1172
+ // row becomes the current one — the same trick, for the same reason, as `button.tsx`.
1173
+ // `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)
1176
+ },
1177
+ plain: {
1178
+ active: STATE.active,
1179
+ inactive: clsx11("text-default-secondary", STATE.hover)
1180
+ }
1181
+ };
1182
+ function NavItem({
1183
+ icon,
1184
+ label,
1185
+ isActive = false,
1186
+ indicator = false,
1187
+ collapsed = false,
1188
+ size: size2 = "md",
1189
+ variant = "card",
1190
+ asChild = false,
1191
+ children,
1192
+ className,
1193
+ ...rest
1194
+ }) {
1195
+ const s = SIZE[size2];
1196
+ const v = VARIANT[variant];
1197
+ const rootProps = {
1198
+ // Only meaningful when `asChild` renders a real link — a decorative span with
1199
+ // aria-current would be a lie.
1200
+ "aria-current": asChild && isActive ? "page" : void 0,
1201
+ // Same attribute the disclosure components and `TableRow` use, so the state is
1202
+ // addressable from a consumer's CSS. `active` rather than `selected`: a nav row is where
1203
+ // you *are* (`aria-current="page"`), which is a different thing from a row you have
1204
+ // *picked* (`aria-selected`) — `TableRow` is the latter and says `selected`.
1205
+ "data-state": isActive ? "active" : void 0,
1206
+ className: clsx11(
1207
+ "relative flex items-center transition-colors",
1208
+ // Unconditional: a row can also become focusable by the caller putting it inside a
1209
+ // link, which is how `src/sidebar/` uses it. A ring that only appears under `asChild`
1210
+ // leaves that case with no visible focus at all.
1211
+ // The offset is `default-secondary`, not `default`: a nav row's home is the sidebar
1212
+ // rail, and `ring-offset-default` is #FFFFFF, so a focused `plain` row drew a 1px white
1213
+ // halo on the #F5F5F5 rail. The offset itself is load-bearing — in dark mode
1214
+ // `ring-brand` and `bg-brand` are both #F1F7FD, so without it a focused *active* row
1215
+ // would have no visible ring at its inner edge.
1216
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand focus-visible:ring-offset-1 focus-visible:ring-offset-default-secondary",
1217
+ s.box,
1218
+ // 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",
1220
+ isActive ? v.active : v.inactive,
1221
+ className
1222
+ ),
1223
+ ...rest
1224
+ };
1225
+ const content = /* @__PURE__ */ jsxs3(Fragment2, { children: [
1226
+ icon != null ? /* @__PURE__ */ jsx8("span", { "aria-hidden": true, className: "shrink-0 [&_svg]:size-4", children: icon }) : (
1227
+ // Icons are optional, but the collapsed rail hides the label — so a row with neither
1228
+ // renders as an empty box. Through 0.5.0 that is exactly what a nav without icons
1229
+ // collapsed to, the docs site's own 36-row sidebar included. The monogram is
1230
+ // decorative in the same way the icon is: the `sr-only` label below still names the
1231
+ // row.
1232
+ collapsed && /* @__PURE__ */ jsx8(
1233
+ "span",
1234
+ {
1235
+ "aria-hidden": true,
1236
+ className: "flex h-4 min-w-4 shrink-0 items-center justify-center text-body-tiny font-semibold leading-none tracking-tight",
1237
+ children: monogram(label)
1238
+ }
1239
+ )
1240
+ ),
1241
+ /* @__PURE__ */ jsx8(
1242
+ "span",
1243
+ {
1244
+ className: clsx11(
1245
+ "min-w-0 truncate font-regular leading-snug",
1246
+ s.label,
1247
+ collapsed ? "sr-only" : "flex-1"
1248
+ ),
1249
+ children: label
1250
+ }
1251
+ ),
1252
+ indicator && /* @__PURE__ */ jsx8(
1253
+ "span",
1254
+ {
1255
+ role: "status",
1256
+ "aria-label": "Unread",
1257
+ className: "absolute -right-px -top-px size-2 rounded-full bg-danger"
1258
+ }
1259
+ )
1260
+ ] });
1261
+ if (asChild) {
1262
+ return /* @__PURE__ */ jsx8(Slot, { wrapChildren: () => content, ...rootProps, children });
1263
+ }
1264
+ return /* @__PURE__ */ jsx8("span", { ...rootProps, children: content });
1265
+ }
1266
+
1267
+ // src/sidebar/nav-tabs.tsx
1268
+ import { NavLink } from "react-router-dom";
1269
+ import clsx12 from "clsx";
1270
+ import { jsx as jsx9 } from "react/jsx-runtime";
1271
+ function NavTabs({ items, className }) {
1272
+ return (
1273
+ // The links inside are the tab stops.
1274
+ /* @__PURE__ */ jsx9(
1275
+ "div",
1276
+ {
1277
+ className: clsx12(
1278
+ "flex gap-6 overflow-x-auto border-b border-default",
1279
+ SCROLLBAR_CLASSES,
1280
+ className
1281
+ ),
1282
+ children: items.map((tab) => /* @__PURE__ */ jsx9(
1283
+ NavLink,
1284
+ {
1285
+ to: tab.to,
1286
+ end: tab.end,
1287
+ className: ({ isActive }) => clsx12(
1288
+ "shrink-0 whitespace-nowrap border-b-2 pb-3 text-body-sm transition-colors",
1289
+ isActive ? "border-brand-emphasis font-semibold text-brand-emphasis" : "border-transparent font-medium text-default-secondary hover:text-default"
1290
+ ),
1291
+ children: tab.label
1292
+ },
1293
+ tab.to
1294
+ ))
1295
+ }
1296
+ )
306
1297
  );
307
1298
  }
308
- function AppSidebar({ collapsed, onToggle, isAdmin = false, userName, userRole, onLogout }) {
309
- return /* @__PURE__ */ jsxs3(
1299
+
1300
+ // src/sidebar/index.tsx
1301
+ import { Fragment as Fragment3, jsx as jsx10, jsxs as jsxs4 } from "react/jsx-runtime";
1302
+ function AppSidebar({
1303
+ groups,
1304
+ footerNav,
1305
+ brand,
1306
+ footer,
1307
+ collapsible = true,
1308
+ collapsed: collapsedProp = false,
1309
+ onToggle,
1310
+ size: size2 = "md",
1311
+ variant = "card",
1312
+ width = "w-80",
1313
+ collapsedWidth = "w-20",
1314
+ className
1315
+ }) {
1316
+ const collapsed = collapsible && collapsedProp;
1317
+ return /* @__PURE__ */ jsxs4(
310
1318
  "aside",
311
1319
  {
312
- className: clsx4(
313
- "relative flex flex-col shrink-0 transition-all duration-200",
314
- "bg-default-secondary border-r border-default rounded-xl",
315
- "h-full",
316
- collapsed ? "w-20" : "w-80"
1320
+ className: cn(
1321
+ "relative flex h-full shrink-0 flex-col py-2 transition-all duration-200",
1322
+ "rounded-xl border-r border-default bg-default-secondary",
1323
+ collapsed ? collapsedWidth : width,
1324
+ className
317
1325
  ),
318
1326
  children: [
319
- /* @__PURE__ */ jsx4(
1327
+ collapsible && onToggle && /* @__PURE__ */ jsx10(
320
1328
  "button",
321
1329
  {
322
1330
  type: "button",
323
1331
  "aria-label": collapsed ? "Expand sidebar" : "Collapse sidebar",
1332
+ "aria-expanded": !collapsed,
324
1333
  onClick: onToggle,
325
- className: clsx4(
326
- "absolute -right-3 top-5 z-10 flex size-6 items-center justify-center rounded-full",
327
- "bg-default border border-default text-default-secondary",
328
- "hover:bg-default-hover shadow-sm transition-colors"
1334
+ className: cn(
1335
+ "absolute -right-3 top-5 z-raised flex size-6 items-center justify-center rounded-full",
1336
+ "border border-default bg-default text-default-secondary",
1337
+ // Not `hover:bg-default-hover` — that is #F5F5F5, exactly the rail colour, and
1338
+ // this button straddles the rail edge, so half of it dissolved into the rail on
1339
+ // hover. Same hover vocabulary as the rows instead.
1340
+ "shadow-100 transition-colors hover:bg-brand-tertiary-hover hover:text-brand",
1341
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand focus-visible:ring-offset-1 focus-visible:ring-offset-default"
329
1342
  ),
330
- children: collapsed ? /* @__PURE__ */ jsx4(ChevronRightIcon, {}) : /* @__PURE__ */ jsx4(ChevronLeftIcon, {})
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 })
331
1344
  }
332
1345
  ),
333
- /* @__PURE__ */ jsxs3("div", { className: "flex flex-1 min-h-0 flex-col gap-1", children: [
334
- /* @__PURE__ */ jsx4(SidebarLogo, { collapsed }),
335
- /* @__PURE__ */ jsxs3("nav", { className: "flex flex-col gap-3 overflow-y-auto px-4 pt-2 pb-3", children: [
336
- /* @__PURE__ */ jsx4(NavLinkItem, { to: "/dashboard", icon: /* @__PURE__ */ jsx4(GridIcon, {}), label: "Dashboard", collapsed, end: true }),
337
- /* @__PURE__ */ jsx4(NavLinkItem, { to: "/entities", icon: /* @__PURE__ */ jsx4(LayersIcon, {}), label: "Entities", collapsed, indicator: true }),
338
- /* @__PURE__ */ jsx4(NavLinkItem, { to: "/suppliers", icon: /* @__PURE__ */ jsx4(BuildingIcon, {}), label: "Suppliers", collapsed }),
339
- /* @__PURE__ */ jsx4(NavLinkItem, { to: "/customers", icon: /* @__PURE__ */ jsx4(UsersIcon, {}), label: "Customers", collapsed }),
340
- /* @__PURE__ */ jsx4(NavLinkItem, { to: "/sources", icon: /* @__PURE__ */ jsx4(FileTextIcon, {}), label: "Sources", collapsed }),
341
- /* @__PURE__ */ jsx4(NavLinkItem, { to: "/articles", icon: /* @__PURE__ */ jsx4(RssIcon, {}), label: "Intelligence", collapsed }),
342
- /* @__PURE__ */ jsx4(NavLinkItem, { to: "/alerts", icon: /* @__PURE__ */ jsx4(BellIcon, {}), label: "Alerts", collapsed }),
343
- /* @__PURE__ */ jsx4(NavLinkItem, { to: "/website-scraper", icon: /* @__PURE__ */ jsx4(GlobeIcon, {}), label: "Website Intel", collapsed }),
344
- /* @__PURE__ */ jsx4(NavLinkItem, { to: "/review", icon: /* @__PURE__ */ jsx4(ClipboardListIcon, {}), label: "Review queue", collapsed }),
345
- isAdmin && /* @__PURE__ */ jsx4(NavLinkItem, { to: "/queues", icon: /* @__PURE__ */ jsx4(ServerIcon, {}), label: "Queues", collapsed })
346
- ] })
347
- ] }),
348
- /* @__PURE__ */ jsxs3("div", { className: "flex flex-col", children: [
349
- isAdmin && /* @__PURE__ */ jsx4("div", { className: "px-4 pb-2", children: /* @__PURE__ */ jsx4(
350
- NavLinkItem,
1346
+ brand && /* @__PURE__ */ jsx10(
1347
+ "div",
1348
+ {
1349
+ className: cn(
1350
+ "flex shrink-0 items-center overflow-hidden p-4",
1351
+ collapsed ? "justify-center" : "gap-2.5"
1352
+ ),
1353
+ children: typeof brand === "function" ? brand({ collapsed }) : brand
1354
+ }
1355
+ ),
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(
1357
+ SidebarGroup,
1358
+ {
1359
+ group,
1360
+ showSeparator: groupIndex > 0,
1361
+ collapsed,
1362
+ size: size2,
1363
+ variant
1364
+ },
1365
+ group.title ?? `group-${groupIndex}`
1366
+ )) }) }),
1367
+ (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(
1369
+ SidebarLink,
351
1370
  {
352
- to: "/settings",
353
- icon: /* @__PURE__ */ jsx4(SettingsIcon, {}),
354
- label: "Settings",
355
- collapsed
356
- }
357
- ) }),
358
- /* @__PURE__ */ jsx4("div", { className: "border-t border-default", children: /* @__PURE__ */ jsx4(UserCard, { collapsed, name: userName, role: userRole, onLogout }) })
1371
+ item,
1372
+ collapsed,
1373
+ size: size2,
1374
+ variant
1375
+ },
1376
+ item.to
1377
+ )) }),
1378
+ footer
359
1379
  ] })
360
1380
  ]
361
1381
  }
362
1382
  );
363
1383
  }
1384
+ function SidebarGroup({
1385
+ group,
1386
+ showSeparator,
1387
+ collapsed,
1388
+ size: size2,
1389
+ variant
1390
+ }) {
1391
+ return (
1392
+ // 12px between rows, per the design — the rows read as separate pills rather than a
1393
+ // stack. Through 0.5.0 this was `gap-1`, which packed them into a near-continuous block.
1394
+ // The same gap spaces the separator and the heading, so neither needs a margin of its own.
1395
+ /* @__PURE__ */ jsxs4("div", { className: "flex flex-col gap-3", children: [
1396
+ showSeparator && /* @__PURE__ */ jsx10(SidebarSeparator, {}),
1397
+ group.title && /* @__PURE__ */ jsx10(
1398
+ "p",
1399
+ {
1400
+ className: cn(
1401
+ "text-body-tiny font-semibold uppercase tracking-wider text-neutral-tertiary",
1402
+ // Flush with the rows rather than inset a further 12px — the heading lines up
1403
+ // with the left edge of the pills below it, not with their labels.
1404
+ // `sr-only` is out of flow, so a collapsed group's heading costs no gap.
1405
+ collapsed ? "sr-only" : "px-0"
1406
+ ),
1407
+ children: group.title
1408
+ }
1409
+ ),
1410
+ group.items.map((item) => /* @__PURE__ */ jsx10(
1411
+ SidebarLink,
1412
+ {
1413
+ item,
1414
+ collapsed,
1415
+ size: size2,
1416
+ variant
1417
+ },
1418
+ item.to
1419
+ ))
1420
+ ] })
1421
+ );
1422
+ }
1423
+ function SidebarLink({
1424
+ item,
1425
+ collapsed,
1426
+ size: size2,
1427
+ variant
1428
+ }) {
1429
+ const link = /* @__PURE__ */ jsx10(
1430
+ NavLink2,
1431
+ {
1432
+ to: item.to,
1433
+ end: item.end,
1434
+ className: cn("block w-full no-underline", collapsed && "flex justify-center"),
1435
+ children: ({ isActive }) => /* @__PURE__ */ jsx10(
1436
+ NavItem,
1437
+ {
1438
+ icon: item.icon,
1439
+ label: item.label,
1440
+ isActive,
1441
+ indicator: item.indicator,
1442
+ collapsed,
1443
+ size: size2,
1444
+ variant
1445
+ }
1446
+ )
1447
+ }
1448
+ );
1449
+ if (!collapsed) return link;
1450
+ return /* @__PURE__ */ jsx10(Tooltip, { title: item.label, side: "right", children: link });
1451
+ }
1452
+ function SidebarSeparator({ className, ...rest }) {
1453
+ return /* @__PURE__ */ jsx10("div", { "aria-hidden": true, className: cn("mx-1 h-px shrink-0 bg-divider", className), ...rest });
1454
+ }
1455
+ function SidebarUserCard({
1456
+ name,
1457
+ role,
1458
+ onLogout,
1459
+ collapsed = false,
1460
+ menu,
1461
+ className
1462
+ }) {
1463
+ const initial = name.trim().charAt(0);
1464
+ const items = /* @__PURE__ */ jsxs4(Fragment3, { children: [
1465
+ menu,
1466
+ /* @__PURE__ */ jsx10(
1467
+ MenuItem,
1468
+ {
1469
+ icon: /* @__PURE__ */ jsx10(LuLogOut, { className: "size-3.5", "aria-hidden": true }),
1470
+ onSelect: onLogout,
1471
+ children: "Sign out"
1472
+ }
1473
+ )
1474
+ ] });
1475
+ 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 })
1479
+ ] }) });
1480
+ }
1481
+ 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" }),
1483
+ /* @__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 })
1486
+ ] }),
1487
+ /* @__PURE__ */ jsxs4(DropdownMenu, { children: [
1488
+ /* @__PURE__ */ jsx10(DropdownMenuTrigger, { asChild: true, label: "User options", children: /* @__PURE__ */ jsx10(
1489
+ "button",
1490
+ {
1491
+ type: "button",
1492
+ "aria-label": `${name} \u2014 user options`,
1493
+ 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 })
1495
+ }
1496
+ ) }),
1497
+ /* @__PURE__ */ jsx10(DropdownMenuContent, { side: "top", align: "end", children: items })
1498
+ ] })
1499
+ ] });
1500
+ }
364
1501
  export {
365
- AppSidebar
1502
+ AppSidebar,
1503
+ NavTabs,
1504
+ SidebarSeparator,
1505
+ SidebarUserCard
366
1506
  };
367
1507
  //# sourceMappingURL=sidebar.js.map