@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.cjs CHANGED
@@ -30,20 +30,138 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  // src/sidebar/index.tsx
31
31
  var sidebar_exports = {};
32
32
  __export(sidebar_exports, {
33
- AppSidebar: () => AppSidebar
33
+ AppSidebar: () => AppSidebar,
34
+ NavTabs: () => NavTabs,
35
+ SidebarSeparator: () => SidebarSeparator,
36
+ SidebarUserCard: () => SidebarUserCard
34
37
  });
35
38
  module.exports = __toCommonJS(sidebar_exports);
36
- var import_react = require("react");
37
- var import_react_router_dom = require("react-router-dom");
38
- var import_clsx4 = __toESM(require("clsx"), 1);
39
-
40
- // src/index.tsx
41
- var import_clsx3 = __toESM(require("clsx"), 1);
39
+ var import_react_router_dom2 = require("react-router-dom");
40
+ var import_lu3 = require("react-icons/lu");
42
41
 
43
- // src/components/avatar.tsx
42
+ // src/lib/slot.tsx
43
+ var import_react = require("react");
44
44
  var import_clsx = __toESM(require("clsx"), 1);
45
+ var Slot = (0, import_react.forwardRef)(function Slot2({ children, wrapChildren, ...slotProps }, forwardedRef) {
46
+ if (!(0, import_react.isValidElement)(children)) return null;
47
+ const child = children;
48
+ const merged = {
49
+ ...mergeProps(slotProps, child.props),
50
+ ref: composeRefs(forwardedRef, getElementRef(child))
51
+ };
52
+ return wrapChildren ? (0, import_react.cloneElement)(child, merged, wrapChildren(child.props.children)) : (0, import_react.cloneElement)(child, merged);
53
+ });
54
+ function mergeProps(slotProps, childProps) {
55
+ const merged = { ...slotProps };
56
+ for (const key in childProps) {
57
+ const slotValue = slotProps[key];
58
+ const childValue = childProps[key];
59
+ if (/^on[A-Z]/.test(key)) {
60
+ if (typeof slotValue === "function" && typeof childValue === "function") {
61
+ merged[key] = (...args) => {
62
+ slotValue(...args);
63
+ childValue(...args);
64
+ };
65
+ } else {
66
+ merged[key] = childValue ?? slotValue;
67
+ }
68
+ } else if (key === "className") {
69
+ merged[key] = (0, import_clsx.default)(slotValue, childValue);
70
+ } else if (key === "style") {
71
+ merged[key] = { ...slotValue, ...childValue };
72
+ } else {
73
+ merged[key] = childValue;
74
+ }
75
+ }
76
+ return merged;
77
+ }
78
+ function getElementRef(element) {
79
+ const asRecord = element;
80
+ return asRecord.props?.ref ?? asRecord.ref;
81
+ }
82
+ function composeRefs(...refs) {
83
+ return (node) => {
84
+ for (const ref of refs) {
85
+ if (!ref) continue;
86
+ if (typeof ref === "function") {
87
+ ref(node);
88
+ } else {
89
+ ref.current = node;
90
+ }
91
+ }
92
+ };
93
+ }
94
+
95
+ // src/components/forms/control-size.tsx
96
+ var import_react2 = require("react");
45
97
  var import_jsx_runtime = require("react/jsx-runtime");
98
+ var ControlSizeContext = (0, import_react2.createContext)(null);
99
+ function useControlSize(explicit) {
100
+ const inherited = (0, import_react2.useContext)(ControlSizeContext);
101
+ return explicit ?? inherited ?? "md";
102
+ }
103
+
104
+ // src/components/forms/icon-button.tsx
105
+ var import_react3 = require("react");
106
+ var import_clsx2 = __toESM(require("clsx"), 1);
107
+ var import_jsx_runtime2 = require("react/jsx-runtime");
108
+ var variantStyles = {
109
+ primary: (0, import_clsx2.default)(
110
+ "bg-brand border border-brand text-brand-on-brand",
111
+ "hover:bg-brand-hover",
112
+ "disabled:bg-disabled disabled:border-disabled disabled:text-disabled-on-disabled"
113
+ ),
114
+ // Neutral uses secondary background (light gray) for visual weight without a text label
115
+ neutral: (0, import_clsx2.default)(
116
+ "bg-default-secondary border border-default text-default",
117
+ // icon turns brand blue on hover (Figma spec)
118
+ "hover:bg-default-secondary-hover hover:text-brand",
119
+ "disabled:bg-disabled disabled:border-disabled disabled:text-disabled-on-disabled"
120
+ ),
121
+ subtle: (0, import_clsx2.default)(
122
+ "bg-transparent text-neutral",
123
+ // hover uses background-default-default-hover (#f5f5f5), icon turns brand blue (Figma spec)
124
+ "hover:bg-default-hover hover:text-brand",
125
+ // disabled needs explicit border since there is no base border
126
+ "disabled:bg-disabled disabled:border disabled:border-disabled disabled:text-disabled-on-disabled"
127
+ )
128
+ };
46
129
  var sizeStyles = {
130
+ lg: "size-12 rounded-2xl [&_svg]:size-5",
131
+ md: "size-9 rounded-lg [&_svg]:size-5",
132
+ sm: "size-6 rounded-full [&_svg]:size-4"
133
+ };
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);
136
+ const rootProps = {
137
+ className: (0, import_clsx2.default)(
138
+ "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",
141
+ variantStyles[variant],
142
+ sizeStyles[size2],
143
+ className
144
+ ),
145
+ ...props
146
+ };
147
+ if (asChild) {
148
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Slot, { ref, ...rootProps, children });
149
+ }
150
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
151
+ "button",
152
+ {
153
+ ref,
154
+ type: type ?? "button",
155
+ ...rootProps,
156
+ children
157
+ }
158
+ );
159
+ });
160
+
161
+ // src/components/avatar/avatar.tsx
162
+ var import_clsx3 = __toESM(require("clsx"), 1);
163
+ var import_jsx_runtime3 = require("react/jsx-runtime");
164
+ var sizeStyles2 = {
47
165
  lg: "size-10",
48
166
  md: "size-8",
49
167
  sm: "size-6"
@@ -53,20 +171,20 @@ var initialTextStyles = {
53
171
  md: "text-body-xs font-semibold",
54
172
  sm: "text-body-tiny font-semibold"
55
173
  };
56
- function getShapeClass(shape, size) {
174
+ function getShapeClass(shape, size2) {
57
175
  if (shape === "circle") return "rounded-full";
58
- return size === "sm" ? "rounded" : "rounded-lg";
176
+ return size2 === "sm" ? "rounded" : "rounded-lg";
59
177
  }
60
178
  function Avatar(props) {
61
- const { size = "lg", shape = "circle", className } = props;
62
- const base = (0, import_clsx.default)(
179
+ const { size: size2 = "lg", shape = "circle", className } = props;
180
+ const base = (0, import_clsx3.default)(
63
181
  "shrink-0 overflow-hidden",
64
- sizeStyles[size],
65
- getShapeClass(shape, size),
182
+ sizeStyles2[size2],
183
+ getShapeClass(shape, size2),
66
184
  className
67
185
  );
68
186
  if (props.type === "image") {
69
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: base, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
187
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: base, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
70
188
  "img",
71
189
  {
72
190
  src: props.src,
@@ -75,19 +193,19 @@ function Avatar(props) {
75
193
  }
76
194
  ) });
77
195
  }
78
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
196
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
79
197
  "div",
80
198
  {
81
- className: (0, import_clsx.default)(base, "flex items-center justify-center"),
199
+ className: (0, import_clsx3.default)(base, "flex items-center justify-center"),
82
200
  style: {
83
201
  background: "linear-gradient(135deg, var(--color-graphic-blue) 30%, var(--color-graphic-red) 100%)"
84
202
  },
85
- children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
203
+ children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
86
204
  "span",
87
205
  {
88
- className: (0, import_clsx.default)(
206
+ className: (0, import_clsx3.default)(
89
207
  "select-none leading-none text-brand-on-brand",
90
- initialTextStyles[size]
208
+ initialTextStyles[size2]
91
209
  ),
92
210
  children: props.initial.charAt(0).toUpperCase()
93
211
  }
@@ -96,307 +214,1298 @@ function Avatar(props) {
96
214
  );
97
215
  }
98
216
 
99
- // src/components/nav-item.tsx
100
- var import_clsx2 = __toESM(require("clsx"), 1);
101
- var import_jsx_runtime2 = require("react/jsx-runtime");
102
- function NavItem({
103
- icon,
104
- label,
105
- isActive = false,
106
- indicator = false,
107
- collapsed = false,
108
- className
109
- }) {
110
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
111
- "span",
112
- {
113
- className: (0, import_clsx2.default)(
114
- "relative flex h-11 w-full items-center gap-3 rounded-xl px-3.5 py-2.5 transition-colors",
115
- collapsed && "justify-center",
116
- isActive ? "bg-brand text-brand-on-brand" : [
117
- "border border-default bg-default text-default-secondary",
118
- "hover:bg-brand-tertiary-hover hover:border-brand-tertiary hover:text-brand"
119
- ],
120
- className
121
- ),
122
- children: [
123
- icon !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "shrink-0 [&_svg]:size-4", children: icon }),
124
- !collapsed && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "flex-1 min-w-0 truncate text-body-sm font-regular leading-snug", children: label }),
125
- indicator && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "absolute -right-px -top-px size-2 rounded-full bg-danger" })
126
- ]
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] }]
127
256
  }
128
- );
257
+ }
258
+ });
259
+ function cn(...inputs) {
260
+ return twMerge((0, import_clsx4.clsx)(inputs));
129
261
  }
130
262
 
131
- // src/index.tsx
132
- var import_jsx_runtime3 = require("react/jsx-runtime");
133
-
134
- // src/sidebar/index.tsx
135
- var import_jsx_runtime4 = require("react/jsx-runtime");
136
- function GridIcon() {
137
- return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true, children: [
138
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("rect", { x: "3", y: "3", width: "7", height: "7", rx: "1" }),
139
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("rect", { x: "14", y: "3", width: "7", height: "7", rx: "1" }),
140
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("rect", { x: "14", y: "14", width: "7", height: "7", rx: "1" }),
141
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("rect", { x: "3", y: "14", width: "7", height: "7", rx: "1" })
142
- ] });
263
+ // src/lib/use-controllable-state.ts
264
+ var import_react4 = require("react");
265
+ function useControllableState({
266
+ value,
267
+ defaultValue,
268
+ onChange
269
+ }) {
270
+ const [internal, setInternal] = (0, import_react4.useState)(defaultValue);
271
+ const isControlled = value !== void 0;
272
+ const current = isControlled ? value : internal;
273
+ const onChangeRef = (0, import_react4.useRef)(onChange);
274
+ onChangeRef.current = onChange;
275
+ const currentRef = (0, import_react4.useRef)(current);
276
+ currentRef.current = current;
277
+ const isControlledRef = (0, import_react4.useRef)(isControlled);
278
+ isControlledRef.current = isControlled;
279
+ const setValue = (0, import_react4.useCallback)((next) => {
280
+ const resolved = typeof next === "function" ? next(currentRef.current) : next;
281
+ if (resolved === currentRef.current) return;
282
+ if (!isControlledRef.current) setInternal(resolved);
283
+ onChangeRef.current?.(resolved);
284
+ }, []);
285
+ return [current, setValue];
143
286
  }
144
- function LayersIcon() {
145
- return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true, children: [
146
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("polygon", { points: "12 2 2 7 12 12 22 7 12 2" }),
147
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("polyline", { points: "2 17 12 22 22 17" }),
148
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("polyline", { points: "2 12 12 17 22 12" })
149
- ] });
287
+
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
+ };
150
419
  }
151
- function BuildingIcon() {
152
- return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true, children: [
153
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("rect", { x: "2", y: "7", width: "20", height: "14", rx: "2" }),
154
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("path", { d: "M16 21V5a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v16" })
155
- ] });
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 };
156
469
  }
157
- function UsersIcon() {
158
- return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true, children: [
159
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("path", { d: "M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2" }),
160
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("circle", { cx: "9", cy: "7", r: "4" }),
161
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("path", { d: "M23 21v-2a4 4 0 0 0-3-3.87" }),
162
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("path", { d: "M16 3.13a4 4 0 0 1 0 7.75" })
163
- ] });
470
+
471
+ // src/components/overlays/menu-styles.ts
472
+ var import_clsx6 = __toESM(require("clsx"), 1);
473
+
474
+ // src/lib/scrollbar.ts
475
+ var import_clsx5 = __toESM(require("clsx"), 1);
476
+ var SCROLLBAR_CLASSES = (0, import_clsx5.default)(
477
+ // Firefox and the standard property.
478
+ "[scrollbar-color:var(--color-background-default-tertiary)_transparent] [scrollbar-width:thin]",
479
+ // WebKit/Blink.
480
+ "[&::-webkit-scrollbar]:size-2",
481
+ "[&::-webkit-scrollbar-track]:bg-transparent",
482
+ "[&::-webkit-scrollbar-thumb]:rounded-full [&::-webkit-scrollbar-thumb]:bg-default-tertiary",
483
+ "[&::-webkit-scrollbar-thumb:hover]:bg-neutral-secondary",
484
+ "[&::-webkit-scrollbar-corner]:bg-transparent"
485
+ );
486
+
487
+ // src/components/overlays/menu-styles.ts
488
+ var OVERLAY_PANEL = "rounded-lg border border-default bg-default p-1 shadow-300";
489
+ var OVERLAY_PANEL_MENU = (0, import_clsx6.default)(OVERLAY_PANEL, "min-w-[168px]");
490
+ var OVERLAY_PANEL_SCROLL = (0, import_clsx6.default)(
491
+ "max-h-[min(16rem,var(--panel-available-height,16rem))] overflow-y-auto",
492
+ // A floating panel is already its own scroll container and keeps focus on the input or a
493
+ // roving option, so it needs the scrollbar styling but not `ScrollArea`'s tab stop.
494
+ SCROLLBAR_CLASSES
495
+ );
496
+ var OVERLAY_ITEM_BASE = "flex w-full cursor-pointer select-none items-start gap-2 rounded-md text-left outline-none transition-colors";
497
+ var OVERLAY_ITEM_SIZE = {
498
+ lg: "px-3 py-2.5 text-body",
499
+ md: "px-2.5 py-2 text-body-sm",
500
+ sm: "px-2 py-1.5 text-body-tiny"
501
+ };
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";
164
505
  }
165
- function RssIcon() {
166
- return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true, children: [
167
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("path", { d: "M4 11a9 9 0 0 1 9 9" }),
168
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("path", { d: "M4 4a16 16 0 0 1 16 16" }),
169
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("circle", { cx: "5", cy: "19", r: "1", fill: "currentColor" })
170
- ] });
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();
171
517
  }
172
- function FileTextIcon() {
173
- return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true, children: [
174
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("path", { d: "M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" }),
175
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("polyline", { points: "14 2 14 8 20 8" }),
176
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("line", { x1: "16", y1: "13", x2: "8", y2: "13" }),
177
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("line", { x1: "16", y1: "17", x2: "8", y2: "17" })
178
- ] });
518
+ function topmost() {
519
+ return layers[layers.length - 1]?.id;
179
520
  }
180
- function BellIcon() {
181
- return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true, children: [
182
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("path", { d: "M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9" }),
183
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("path", { d: "M13.73 21a2 2 0 0 1-3.46 0" })
184
- ] });
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
+ };
185
539
  }
186
- function ServerIcon() {
187
- return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true, children: [
188
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("rect", { x: "2", y: "2", width: "20", height: "8", rx: "2" }),
189
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("rect", { x: "2", y: "14", width: "20", height: "8", rx: "2" }),
190
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("line", { x1: "6", y1: "6", x2: "6.01", y2: "6" }),
191
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("line", { x1: "6", y1: "18", x2: "6.01", y2: "18" })
192
- ] });
540
+ function unlockScroll() {
541
+ scrollLocks = Math.max(0, scrollLocks - 1);
542
+ if (scrollLocks === 0 && restoreBody) {
543
+ restoreBody();
544
+ restoreBody = null;
545
+ }
193
546
  }
194
- function SettingsIcon() {
195
- return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true, children: [
196
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("circle", { cx: "12", cy: "12", r: "3" }),
197
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("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" })
198
- ] });
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
+ }
199
560
  }
200
- function ClipboardListIcon() {
201
- return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true, children: [
202
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("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" }),
203
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("rect", { x: "9", y: "3", width: "6", height: "4", rx: "1" }),
204
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("line", { x1: "9", y1: "12", x2: "15", y2: "12" }),
205
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("line", { x1: "9", y1: "16", x2: "13", y2: "16" })
206
- ] });
561
+ function releaseInert() {
562
+ for (const el of inerted) el.removeAttribute("inert");
563
+ inerted.clear();
207
564
  }
208
- function GlobeIcon() {
209
- return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true, children: [
210
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("circle", { cx: "12", cy: "12", r: "10" }),
211
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("line", { x1: "2", y1: "12", x2: "22", y2: "12" }),
212
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("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" })
213
- ] });
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 };
214
598
  }
215
- function ChevronLeftIcon() {
216
- return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("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__ */ (0, import_jsx_runtime4.jsx)("polyline", { points: "15 18 9 12 15 6" }) });
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]);
217
726
  }
218
- function ChevronRightIcon() {
219
- return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("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__ */ (0, import_jsx_runtime4.jsx)("polyline", { points: "9 18 15 12 9 6" }) });
727
+
728
+ // src/components/overlays/portal.tsx
729
+ var import_react10 = require("react");
730
+ 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);
220
738
  }
221
- function MoreVerticalIcon() {
222
- return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true, children: [
223
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("circle", { cx: "12", cy: "5", r: "1", fill: "currentColor" }),
224
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("circle", { cx: "12", cy: "12", r: "1", fill: "currentColor" }),
225
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("circle", { cx: "12", cy: "19", r: "1", fill: "currentColor" })
739
+
740
+ // 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");
744
+ function Tooltip({
745
+ children,
746
+ title,
747
+ body,
748
+ side = "top",
749
+ align = "center",
750
+ openDelay = 150,
751
+ closeDelay = 0,
752
+ open: openProp,
753
+ defaultOpen = false,
754
+ onOpenChange,
755
+ association = "describedby",
756
+ className
757
+ }) {
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;
778
+ }
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
+ ),
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",
837
+ {
838
+ ref: refs.setFloating,
839
+ id: tooltipId,
840
+ role: "tooltip",
841
+ style: floatingStyles,
842
+ className: (0, import_clsx7.default)(
843
+ "z-tooltip max-w-[260px] rounded-lg bg-neutral px-3 py-2 shadow-300",
844
+ "text-body-xs text-neutral-on-neutral",
845
+ !open && "sr-only",
846
+ className
847
+ ),
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
+ ]
852
+ }
853
+ ) })
226
854
  ] });
227
855
  }
228
- function LogOutIcon() {
229
- return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true, children: [
230
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("path", { d: "M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" }),
231
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("polyline", { points: "16 17 21 12 16 7" }),
232
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("line", { x1: "21", y1: "12", x2: "9", y2: "12" })
233
- ] });
856
+
857
+ // 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;
234
869
  }
235
- function SidebarLogo({ collapsed }) {
236
- return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
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)
879
+ });
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
+ }
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?.();
922
+ };
923
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
237
924
  "div",
238
925
  {
239
- className: (0, import_clsx4.default)(
240
- "flex shrink-0 items-center overflow-hidden p-4",
241
- collapsed ? "justify-center" : "gap-2.5"
242
- ),
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,
243
943
  children: [
244
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "flex shrink-0 size-10 items-center justify-center rounded-xl bg-brand-hover overflow-hidden", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "grid grid-cols-2 gap-[3px]", children: [
245
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "size-2 rounded-[2px] bg-white" }),
246
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "size-2 rounded-[2px] bg-white/50" }),
247
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "size-2 rounded-[2px] bg-white/50" }),
248
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "size-2 rounded-[2px] bg-white" })
249
- ] }) }),
250
- !collapsed && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "text-heading font-semibold text-default tracking-tight whitespace-nowrap", children: "SCIC" })
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 })
251
950
  ]
252
951
  }
253
952
  );
254
953
  }
255
- function NavLinkItem({ to, icon, label, indicator, collapsed, end }) {
256
- return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_router_dom.NavLink, { to, end, className: "block w-full no-underline", children: ({ isActive }) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
257
- NavItem,
954
+ var MenuRadioContext = (0, import_react12.createContext)(null);
955
+
956
+ // 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");
960
+ var OVERFLOW = {
961
+ vertical: "overflow-y-auto overflow-x-hidden",
962
+ horizontal: "overflow-x-auto overflow-y-hidden",
963
+ both: "overflow-auto"
964
+ };
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)(() => {
969
+ const node = localRef.current;
970
+ if (!node) return;
971
+ const measure = () => {
972
+ const vertical = node.scrollHeight > node.clientHeight + 1;
973
+ const horizontal = node.scrollWidth > node.clientWidth + 1;
974
+ setOverflows(
975
+ axis === "vertical" ? vertical : axis === "horizontal" ? horizontal : vertical || horizontal
976
+ );
977
+ };
978
+ measure();
979
+ if (typeof ResizeObserver === "undefined") return;
980
+ const observer = new ResizeObserver(measure);
981
+ observer.observe(node);
982
+ if (node.firstElementChild) observer.observe(node.firstElementChild);
983
+ return () => observer.disconnect();
984
+ }, [axis, children]);
985
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
986
+ "div",
258
987
  {
259
- icon,
260
- label,
261
- isActive,
262
- indicator,
263
- collapsed
988
+ ref: composeRefs(localRef, ref),
989
+ role: overflows && label ? "region" : void 0,
990
+ "aria-label": overflows ? label : void 0,
991
+ tabIndex: overflows ? 0 : void 0,
992
+ "data-slot": "scroll-area",
993
+ className: (0, import_clsx9.default)(
994
+ OVERFLOW[axis],
995
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand focus-visible:ring-offset-1 focus-visible:ring-offset-default",
996
+ SCROLLBAR_CLASSES,
997
+ className
998
+ ),
999
+ ...rest,
1000
+ children
264
1001
  }
265
- ) });
1002
+ );
1003
+ });
1004
+
1005
+ // 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>");
1014
+ return value;
266
1015
  }
267
- function UserCard({ collapsed, name, role, onLogout }) {
268
- const [menuOpen, setMenuOpen] = (0, import_react.useState)(false);
269
- const menuRef = (0, import_react.useRef)(null);
270
- const initial = name.trim().charAt(0);
271
- (0, import_react.useEffect)(() => {
272
- if (!menuOpen) return;
273
- function handleClickOutside(e) {
274
- if (menuRef.current && !menuRef.current.contains(e.target)) {
275
- setMenuOpen(false);
1016
+ function DropdownMenu({
1017
+ children,
1018
+ open: openProp,
1019
+ defaultOpen = false,
1020
+ onOpenChange
1021
+ }) {
1022
+ const [open, setOpen] = useControllableState({
1023
+ value: openProp,
1024
+ defaultValue: defaultOpen,
1025
+ onChange: onOpenChange
1026
+ });
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 }) });
1031
+ }
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);
276
1054
  }
277
- }
278
- document.addEventListener("mousedown", handleClickOutside);
279
- return () => document.removeEventListener("mousedown", handleClickOutside);
280
- }, [menuOpen]);
281
- const menuItems = /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
282
- "button",
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(),
1059
+ ...rest
1060
+ };
1061
+ if (asChild) {
1062
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Slot, { ref: composeRefs(anchor.setTriggerNode), ...triggerProps, children });
1063
+ }
1064
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1065
+ IconButton,
283
1066
  {
284
- type: "button",
285
- onClick: () => {
286
- setMenuOpen(false);
287
- onLogout();
288
- },
289
- className: "flex w-full items-center gap-2 px-3 py-2 text-body-sm text-default hover:bg-default-hover transition-colors",
290
- children: [
291
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(LogOutIcon, {}),
292
- "Sign out"
293
- ]
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 })
294
1073
  }
295
1074
  );
296
- return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
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)(
297
1123
  "div",
298
1124
  {
299
- className: (0, import_clsx4.default)(
300
- "flex shrink-0 items-center p-4",
301
- collapsed ? "justify-center" : "gap-2.5"
1125
+ ref: composeRefs(
1126
+ panelRef,
1127
+ (node) => setPanelNode(node),
1128
+ refs.setFloating
302
1129
  ),
303
- children: collapsed ? /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "relative", ref: menuRef, children: [
304
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
305
- "button",
306
- {
307
- type: "button",
308
- "aria-label": "User options",
309
- "aria-haspopup": "menu",
310
- "aria-expanded": menuOpen,
311
- onClick: () => setMenuOpen((v) => !v),
312
- className: "rounded-full",
313
- children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Avatar, { initial, size: "md", shape: "circle" })
314
- }
315
- ),
316
- menuOpen && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("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 })
317
- ] }) : /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_jsx_runtime4.Fragment, { children: [
318
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Avatar, { initial, size: "md", shape: "circle" }),
319
- /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "flex flex-1 min-w-0 flex-col gap-0.5", children: [
320
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "truncate text-body-sm font-semibold text-default leading-snug", children: name }),
321
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "truncate text-body-tiny font-regular text-default-secondary leading-none", children: role })
322
- ] }),
323
- /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "relative shrink-0", ref: menuRef, children: [
324
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
325
- "button",
326
- {
327
- type: "button",
328
- "aria-label": "User options",
329
- "aria-haspopup": "menu",
330
- "aria-expanded": menuOpen,
331
- onClick: () => setMenuOpen((v) => !v),
332
- className: "flex size-6 items-center justify-center rounded-md text-default-secondary hover:bg-default-hover hover:text-default transition-colors",
333
- children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(MoreVerticalIcon, {})
334
- }
335
- ),
336
- menuOpen && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("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 })
337
- ] })
338
- ] })
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
339
1144
  }
1145
+ ) });
1146
+ }
1147
+
1148
+ // src/components/chrome/nav-item.tsx
1149
+ var import_clsx11 = __toESM(require("clsx"), 1);
1150
+ var import_jsx_runtime8 = require("react/jsx-runtime");
1151
+ var SIZE = {
1152
+ sm: { box: "h-9 gap-2 rounded-lg px-3 py-1.5", label: "text-body-xs", square: "w-9" },
1153
+ md: { box: "h-11 gap-3 rounded-xl px-3.5 py-2.5", label: "text-body-sm", square: "w-11" }
1154
+ };
1155
+ function monogram(label) {
1156
+ const words = label.split(/\s+/).filter((word) => /^[\p{L}\p{N}]/u.test(word));
1157
+ const [first, second] = words;
1158
+ if (!first) return Array.from(label.trim())[0]?.toUpperCase() ?? "";
1159
+ if (second) return (Array.from(first)[0] + Array.from(second)[0]).toUpperCase();
1160
+ const [a, b] = Array.from(first);
1161
+ return a.toUpperCase() + (b ?? "").toLowerCase();
1162
+ }
1163
+ var STATE = {
1164
+ // Figma MenuButton `state=hover`. No border change: Figma holds it at #D9D9D9 through
1165
+ // hover and moves only the fill and the text.
1166
+ hover: "hover:bg-brand-tertiary-hover hover:text-brand",
1167
+ // Figma MenuButton `state=focussed`, which is that file's name for selected — see the
1168
+ // `isActive` doc comment. Hovering the current row darkens it rather than reverting it to
1169
+ // the pale hover tint, which would read as the row losing its selection under the cursor.
1170
+ active: "bg-brand text-brand-on-brand hover:bg-brand-hover"
1171
+ };
1172
+ var VARIANT = {
1173
+ card: {
1174
+ // `border-transparent` holds the 1px so the icon and label do not shift outward when a
1175
+ // row becomes the current one — the same trick, for the same reason, as `button.tsx`.
1176
+ // `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)
1179
+ },
1180
+ plain: {
1181
+ active: STATE.active,
1182
+ inactive: (0, import_clsx11.default)("text-default-secondary", STATE.hover)
1183
+ }
1184
+ };
1185
+ function NavItem({
1186
+ icon,
1187
+ label,
1188
+ isActive = false,
1189
+ indicator = false,
1190
+ collapsed = false,
1191
+ size: size2 = "md",
1192
+ variant = "card",
1193
+ asChild = false,
1194
+ children,
1195
+ className,
1196
+ ...rest
1197
+ }) {
1198
+ const s = SIZE[size2];
1199
+ const v = VARIANT[variant];
1200
+ const rootProps = {
1201
+ // Only meaningful when `asChild` renders a real link — a decorative span with
1202
+ // aria-current would be a lie.
1203
+ "aria-current": asChild && isActive ? "page" : void 0,
1204
+ // Same attribute the disclosure components and `TableRow` use, so the state is
1205
+ // addressable from a consumer's CSS. `active` rather than `selected`: a nav row is where
1206
+ // you *are* (`aria-current="page"`), which is a different thing from a row you have
1207
+ // *picked* (`aria-selected`) — `TableRow` is the latter and says `selected`.
1208
+ "data-state": isActive ? "active" : void 0,
1209
+ className: (0, import_clsx11.default)(
1210
+ "relative flex items-center transition-colors",
1211
+ // Unconditional: a row can also become focusable by the caller putting it inside a
1212
+ // link, which is how `src/sidebar/` uses it. A ring that only appears under `asChild`
1213
+ // leaves that case with no visible focus at all.
1214
+ // The offset is `default-secondary`, not `default`: a nav row's home is the sidebar
1215
+ // rail, and `ring-offset-default` is #FFFFFF, so a focused `plain` row drew a 1px white
1216
+ // halo on the #F5F5F5 rail. The offset itself is load-bearing — in dark mode
1217
+ // `ring-brand` and `bg-brand` are both #F1F7FD, so without it a focused *active* row
1218
+ // would have no visible ring at its inner edge.
1219
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand focus-visible:ring-offset-1 focus-visible:ring-offset-default-secondary",
1220
+ s.box,
1221
+ // 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",
1223
+ isActive ? v.active : v.inactive,
1224
+ className
1225
+ ),
1226
+ ...rest
1227
+ };
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 }) : (
1230
+ // Icons are optional, but the collapsed rail hides the label — so a row with neither
1231
+ // renders as an empty box. Through 0.5.0 that is exactly what a nav without icons
1232
+ // collapsed to, the docs site's own 36-row sidebar included. The monogram is
1233
+ // decorative in the same way the icon is: the `sr-only` label below still names the
1234
+ // row.
1235
+ collapsed && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1236
+ "span",
1237
+ {
1238
+ "aria-hidden": true,
1239
+ className: "flex h-4 min-w-4 shrink-0 items-center justify-center text-body-tiny font-semibold leading-none tracking-tight",
1240
+ children: monogram(label)
1241
+ }
1242
+ )
1243
+ ),
1244
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1245
+ "span",
1246
+ {
1247
+ className: (0, import_clsx11.default)(
1248
+ "min-w-0 truncate font-regular leading-snug",
1249
+ s.label,
1250
+ collapsed ? "sr-only" : "flex-1"
1251
+ ),
1252
+ children: label
1253
+ }
1254
+ ),
1255
+ indicator && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1256
+ "span",
1257
+ {
1258
+ role: "status",
1259
+ "aria-label": "Unread",
1260
+ className: "absolute -right-px -top-px size-2 rounded-full bg-danger"
1261
+ }
1262
+ )
1263
+ ] });
1264
+ if (asChild) {
1265
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(Slot, { wrapChildren: () => content, ...rootProps, children });
1266
+ }
1267
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { ...rootProps, children: content });
1268
+ }
1269
+
1270
+ // src/sidebar/nav-tabs.tsx
1271
+ 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");
1274
+ function NavTabs({ items, className }) {
1275
+ return (
1276
+ // The links inside are the tab stops.
1277
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
1278
+ "div",
1279
+ {
1280
+ className: (0, import_clsx12.default)(
1281
+ "flex gap-6 overflow-x-auto border-b border-default",
1282
+ SCROLLBAR_CLASSES,
1283
+ className
1284
+ ),
1285
+ children: items.map((tab) => /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
1286
+ import_react_router_dom.NavLink,
1287
+ {
1288
+ to: tab.to,
1289
+ end: tab.end,
1290
+ className: ({ isActive }) => (0, import_clsx12.default)(
1291
+ "shrink-0 whitespace-nowrap border-b-2 pb-3 text-body-sm transition-colors",
1292
+ isActive ? "border-brand-emphasis font-semibold text-brand-emphasis" : "border-transparent font-medium text-default-secondary hover:text-default"
1293
+ ),
1294
+ children: tab.label
1295
+ },
1296
+ tab.to
1297
+ ))
1298
+ }
1299
+ )
340
1300
  );
341
1301
  }
342
- function AppSidebar({ collapsed, onToggle, isAdmin = false, userName, userRole, onLogout }) {
343
- return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
1302
+
1303
+ // src/sidebar/index.tsx
1304
+ var import_jsx_runtime10 = require("react/jsx-runtime");
1305
+ function AppSidebar({
1306
+ groups,
1307
+ footerNav,
1308
+ brand,
1309
+ footer,
1310
+ collapsible = true,
1311
+ collapsed: collapsedProp = false,
1312
+ onToggle,
1313
+ size: size2 = "md",
1314
+ variant = "card",
1315
+ width = "w-80",
1316
+ collapsedWidth = "w-20",
1317
+ className
1318
+ }) {
1319
+ const collapsed = collapsible && collapsedProp;
1320
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(
344
1321
  "aside",
345
1322
  {
346
- className: (0, import_clsx4.default)(
347
- "relative flex flex-col shrink-0 transition-all duration-200",
348
- "bg-default-secondary border-r border-default rounded-xl",
349
- "h-full",
350
- collapsed ? "w-20" : "w-80"
1323
+ className: cn(
1324
+ "relative flex h-full shrink-0 flex-col py-2 transition-all duration-200",
1325
+ "rounded-xl border-r border-default bg-default-secondary",
1326
+ collapsed ? collapsedWidth : width,
1327
+ className
351
1328
  ),
352
1329
  children: [
353
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1330
+ collapsible && onToggle && /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
354
1331
  "button",
355
1332
  {
356
1333
  type: "button",
357
1334
  "aria-label": collapsed ? "Expand sidebar" : "Collapse sidebar",
1335
+ "aria-expanded": !collapsed,
358
1336
  onClick: onToggle,
359
- className: (0, import_clsx4.default)(
360
- "absolute -right-3 top-5 z-10 flex size-6 items-center justify-center rounded-full",
361
- "bg-default border border-default text-default-secondary",
362
- "hover:bg-default-hover shadow-sm transition-colors"
1337
+ className: cn(
1338
+ "absolute -right-3 top-5 z-raised flex size-6 items-center justify-center rounded-full",
1339
+ "border border-default bg-default text-default-secondary",
1340
+ // Not `hover:bg-default-hover` — that is #F5F5F5, exactly the rail colour, and
1341
+ // this button straddles the rail edge, so half of it dissolved into the rail on
1342
+ // hover. Same hover vocabulary as the rows instead.
1343
+ "shadow-100 transition-colors hover:bg-brand-tertiary-hover hover:text-brand",
1344
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand focus-visible:ring-offset-1 focus-visible:ring-offset-default"
363
1345
  ),
364
- children: collapsed ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(ChevronRightIcon, {}) : /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(ChevronLeftIcon, {})
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 })
365
1347
  }
366
1348
  ),
367
- /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "flex flex-1 min-h-0 flex-col gap-1", children: [
368
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(SidebarLogo, { collapsed }),
369
- /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("nav", { className: "flex flex-col gap-3 overflow-y-auto px-4 pt-2 pb-3", children: [
370
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(NavLinkItem, { to: "/dashboard", icon: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(GridIcon, {}), label: "Dashboard", collapsed, end: true }),
371
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(NavLinkItem, { to: "/entities", icon: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(LayersIcon, {}), label: "Entities", collapsed, indicator: true }),
372
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(NavLinkItem, { to: "/suppliers", icon: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(BuildingIcon, {}), label: "Suppliers", collapsed }),
373
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(NavLinkItem, { to: "/customers", icon: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(UsersIcon, {}), label: "Customers", collapsed }),
374
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(NavLinkItem, { to: "/sources", icon: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(FileTextIcon, {}), label: "Sources", collapsed }),
375
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(NavLinkItem, { to: "/articles", icon: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(RssIcon, {}), label: "Intelligence", collapsed }),
376
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(NavLinkItem, { to: "/alerts", icon: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(BellIcon, {}), label: "Alerts", collapsed }),
377
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(NavLinkItem, { to: "/website-scraper", icon: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(GlobeIcon, {}), label: "Website Intel", collapsed }),
378
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(NavLinkItem, { to: "/review", icon: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(ClipboardListIcon, {}), label: "Review queue", collapsed }),
379
- isAdmin && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(NavLinkItem, { to: "/queues", icon: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(ServerIcon, {}), label: "Queues", collapsed })
380
- ] })
381
- ] }),
382
- /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "flex flex-col", children: [
383
- isAdmin && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "px-4 pb-2", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
384
- NavLinkItem,
1349
+ brand && /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
1350
+ "div",
1351
+ {
1352
+ className: cn(
1353
+ "flex shrink-0 items-center overflow-hidden p-4",
1354
+ collapsed ? "justify-center" : "gap-2.5"
1355
+ ),
1356
+ children: typeof brand === "function" ? brand({ collapsed }) : brand
1357
+ }
1358
+ ),
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)(
1360
+ SidebarGroup,
1361
+ {
1362
+ group,
1363
+ showSeparator: groupIndex > 0,
1364
+ collapsed,
1365
+ size: size2,
1366
+ variant
1367
+ },
1368
+ group.title ?? `group-${groupIndex}`
1369
+ )) }) }),
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)(
1372
+ SidebarLink,
385
1373
  {
386
- to: "/settings",
387
- icon: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(SettingsIcon, {}),
388
- label: "Settings",
389
- collapsed
390
- }
391
- ) }),
392
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "border-t border-default", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(UserCard, { collapsed, name: userName, role: userRole, onLogout }) })
1374
+ item,
1375
+ collapsed,
1376
+ size: size2,
1377
+ variant
1378
+ },
1379
+ item.to
1380
+ )) }),
1381
+ footer
393
1382
  ] })
394
1383
  ]
395
1384
  }
396
1385
  );
397
1386
  }
1387
+ function SidebarGroup({
1388
+ group,
1389
+ showSeparator,
1390
+ collapsed,
1391
+ size: size2,
1392
+ variant
1393
+ }) {
1394
+ return (
1395
+ // 12px between rows, per the design — the rows read as separate pills rather than a
1396
+ // stack. Through 0.5.0 this was `gap-1`, which packed them into a near-continuous block.
1397
+ // 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)(
1401
+ "p",
1402
+ {
1403
+ className: cn(
1404
+ "text-body-tiny font-semibold uppercase tracking-wider text-neutral-tertiary",
1405
+ // Flush with the rows rather than inset a further 12px — the heading lines up
1406
+ // with the left edge of the pills below it, not with their labels.
1407
+ // `sr-only` is out of flow, so a collapsed group's heading costs no gap.
1408
+ collapsed ? "sr-only" : "px-0"
1409
+ ),
1410
+ children: group.title
1411
+ }
1412
+ ),
1413
+ group.items.map((item) => /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
1414
+ SidebarLink,
1415
+ {
1416
+ item,
1417
+ collapsed,
1418
+ size: size2,
1419
+ variant
1420
+ },
1421
+ item.to
1422
+ ))
1423
+ ] })
1424
+ );
1425
+ }
1426
+ function SidebarLink({
1427
+ item,
1428
+ collapsed,
1429
+ size: size2,
1430
+ variant
1431
+ }) {
1432
+ const link = /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
1433
+ import_react_router_dom2.NavLink,
1434
+ {
1435
+ to: item.to,
1436
+ end: item.end,
1437
+ className: cn("block w-full no-underline", collapsed && "flex justify-center"),
1438
+ children: ({ isActive }) => /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
1439
+ NavItem,
1440
+ {
1441
+ icon: item.icon,
1442
+ label: item.label,
1443
+ isActive,
1444
+ indicator: item.indicator,
1445
+ collapsed,
1446
+ size: size2,
1447
+ variant
1448
+ }
1449
+ )
1450
+ }
1451
+ );
1452
+ if (!collapsed) return link;
1453
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(Tooltip, { title: item.label, side: "right", children: link });
1454
+ }
1455
+ 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 });
1457
+ }
1458
+ function SidebarUserCard({
1459
+ name,
1460
+ role,
1461
+ onLogout,
1462
+ collapsed = false,
1463
+ menu,
1464
+ className
1465
+ }) {
1466
+ const initial = name.trim().charAt(0);
1467
+ const items = /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(import_jsx_runtime10.Fragment, { children: [
1468
+ menu,
1469
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
1470
+ MenuItem,
1471
+ {
1472
+ icon: /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_lu3.LuLogOut, { className: "size-3.5", "aria-hidden": true }),
1473
+ onSelect: onLogout,
1474
+ children: "Sign out"
1475
+ }
1476
+ )
1477
+ ] });
1478
+ 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 })
1482
+ ] }) });
1483
+ }
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 })
1489
+ ] }),
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)(
1492
+ "button",
1493
+ {
1494
+ type: "button",
1495
+ "aria-label": `${name} \u2014 user options`,
1496
+ 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 })
1498
+ }
1499
+ ) }),
1500
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(DropdownMenuContent, { side: "top", align: "end", children: items })
1501
+ ] })
1502
+ ] });
1503
+ }
398
1504
  // Annotate the CommonJS export names for ESM import in node:
399
1505
  0 && (module.exports = {
400
- AppSidebar
1506
+ AppSidebar,
1507
+ NavTabs,
1508
+ SidebarSeparator,
1509
+ SidebarUserCard
401
1510
  });
402
1511
  //# sourceMappingURL=sidebar.cjs.map