@neuronection/assistant-ui 0.14.0 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,47 @@
1
1
  # @neuronection/assistant-ui
2
2
 
3
+ ## 0.15.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [`e7a75e3`](https://github.com/neuronection/assistant-ui/commit/e7a75e3a3970e01f7bdcaf9060f54abe145d5a27) Thanks [@constLiakos](https://github.com/constLiakos)! - ChipInput: the typed draft now renders as a dashed pending chip
8
+ (`data-pending` on the container while a draft exists) so it is visually
9
+ explicit that each entry is its own separate item, and a new `hint` prop
10
+ renders helper text below the field wired to the input via
11
+ `aria-describedby` — use it to spell out the "press Enter or Add after each
12
+ one" contract.
13
+
14
+ - [`163e5e4`](https://github.com/neuronection/assistant-ui/commit/163e5e4f13d5a097dea56727447fe7e1070e3395) Thanks [@constLiakos](https://github.com/constLiakos)! - ScaleSlider: modern track + thumb (filled progress, larger thumb, visible
15
+ focus ring) via new `styles.css` range rules scoped to
16
+ `[data-as='scale-slider']`; low/high labels are larger and readable
17
+ (text-xs, medium weight, muted color) instead of 9px uppercase.
18
+
19
+ ChipInput: new `addLabel` prop renders an explicit add button that commits
20
+ the draft (disabled while empty, re-focuses the input) — makes the
21
+ multi-entry affordance discoverable without keyboard knowledge.
22
+
23
+ - [`3826518`](https://github.com/neuronection/assistant-ui/commit/3826518fd86f03cbef56344bec9b34b2234086c9) Thanks [@constLiakos](https://github.com/constLiakos)! - New `SidebarNav` component: the family-standard vertical navigation panel
24
+ (groups + section dividers, icons, badges, controlled `activeId`, optional
25
+ collapsed icon rail with hover flyouts, header/footer slots). Fully
26
+ presentational + controlled per ADR-006 — routing, role filtering and i18n
27
+ stay app-side. Ships with keyboard-nav + axe assertions, Ladle stories and
28
+ a docs page; adoption across the family apps is tracked in the nav
29
+ primitives program (family ADR-0007).
30
+
31
+ - [`3df9ac1`](https://github.com/neuronection/assistant-ui/commit/3df9ac1dbc90079cf1910e34bd213f63e700688b) Thanks [@constLiakos](https://github.com/constLiakos)! - `SidebarNav`: top-level `NavItem` entries now accept `section` — a divider
32
+ label rendered above the item (first occurrence only) so flat sidebars can
33
+ group their entries visually without nested expand/collapse. Sections are
34
+ ignored in the collapsed rail. (Career's sidebar adoption is the first
35
+ consumer; matches the existing child-level `section` dividers.)
36
+
37
+ - [`3826518`](https://github.com/neuronection/assistant-ui/commit/3826518fd86f03cbef56344bec9b34b2234086c9) Thanks [@constLiakos](https://github.com/constLiakos)! - New `UserMenu` component: the family-standard user menu (avatar or
38
+ initials-disc trigger with name/email, identity header in the panel, item
39
+ model with `tone: 'danger'`, `pending`, and checkable items for
40
+ theme/language toggles). Built on the `Menu` primitives; also adds
41
+ `MenuCheckboxItem` (Radix checkbox menu item with an indicator) to the
42
+ menu module. Health's hand-rolled header dropdown and career's plain
43
+ sign-out button are the adoption targets (family ADR-0007 program).
44
+
3
45
  ## 0.14.0
4
46
 
5
47
  ### Minor Changes
package/README.md CHANGED
@@ -86,6 +86,8 @@ Visual reference: [gallery](https://neuronection.github.io/assistant-ui/).
86
86
  | [`info-button`](./docs/components/info-button.md) / [`field-label`](./docs/components/field-label.md) / [`copy-button`](./docs/components/copy-button.md) / [`breadcrumbs`](./docs/components/breadcrumbs.md) | `InfoButton` · `FieldLabel` · `CopyButton` (clipboard + copied state) · `Breadcrumbs` (SPA links via `linkComponent`) | [stories](https://neuronection.github.io/assistant-ui/?story=phase2--copy-buttons) |
87
87
  | [`ai-button`](./docs/components/ai-button.md) / [`ai-actions-dropdown`](./docs/components/ai-actions-dropdown.md) / [`ai-magic-fill`](./docs/components/ai-magic-fill.md) | AI affordances: ask-with-suggestions (controlled open, icon-only mode) · action menu + custom prompt · describe-in-words fill modal — API calls stay app-side | [stories](https://neuronection.github.io/assistant-ui/?story=ai-patterns--ai-buttons) |
88
88
  | [`settings-shell`](./docs/components/settings-shell.md) / [`provider-form`](./docs/components/provider-form.md) / [`connection-test-row`](./docs/components/connection-test-row.md) | `SettingsShell` (controlled two-pane nav) · `ProviderForm` (write-only API-key field) · `ConnectionTestRow` (idle/testing/ok/fail + latency, `inline` variant + meta slot) | [stories](https://neuronection.github.io/assistant-ui/?story=settings-trio--provider-form-story) |
89
+ | [`sidebar-nav`](./docs/components/sidebar-nav.md) | `SidebarNav` (family-standard vertical nav: groups + sections, badges, controlled active id, optional collapsed icon rail with hover flyouts, header/footer slots) | [stories](https://neuronection.github.io/assistant-ui/?story=sidebar-nav--groups-story) |
90
+ | [`user-menu`](./docs/components/user-menu.md) | `UserMenu` (avatar/identity trigger + action menu incl. checkable items; composed over `Menu`) | [stories](https://neuronection.github.io/assistant-ui/?story=user-menu--checkable-items-story) |
89
91
  | [`model-picker`](./docs/components/model-picker.md) / [`task-assignment-picker`](./docs/components/task-assignment-picker.md) / [`capability-chips`](./docs/components/capability-chips.md) | `ModelPicker` (grouped provider→model combobox, capability chips) · `TaskAssignmentPicker` (task → model mapping rows, capability-filtered, sections + fallback assignment, meta slot) · `CapabilityChips` (cap toggle group / badges, min-selected) | [stories](https://neuronection.github.io/assistant-ui/?story=model-assignment--task-assignment-v2-story) |
90
92
  | [`model-registry`](./docs/components/model-registry.md) | `ModelRegistry` (provider cards → registered model rows; add/edit in one catalog modal fed by app-fetched remote models; manual id entry; add-all of pending ids) | [stories](https://neuronection.github.io/assistant-ui/?story=model-registry--model-registry-story) |
91
93
  | [`upload-dropzone`](./docs/components/upload-dropzone.md) / [`file-card`](./docs/components/file-card.md) / [`file-queue`](./docs/components/file-queue.md) | file attachment surface: dropzone (block/row) · card (status, include toggle) · queue (summary + reorder) | [stories](https://neuronection.github.io/assistant-ui/?story=file-surface--dropzones) |
@@ -7,6 +7,8 @@ interface ChipInputProps extends Omit<React.ComponentProps<'div'>, 'onChange'> {
7
7
  separators?: string[];
8
8
  inputLabel?: string;
9
9
  removeLabel?: string;
10
+ addLabel?: string;
11
+ hint?: string;
10
12
  disabled?: boolean;
11
13
  }
12
14
  declare const ChipInput: React.ForwardRefExoticComponent<Omit<ChipInputProps, "ref"> & React.RefAttributes<HTMLDivElement>>;
@@ -1,3 +1,3 @@
1
- export { ChipInput } from './chunk-TPLQL62V.js';
1
+ export { ChipInput } from './chunk-HIRR2F4R.js';
2
2
  //# sourceMappingURL=chip-input.js.map
3
3
  //# sourceMappingURL=chip-input.js.map
@@ -0,0 +1,119 @@
1
+ import { Menu, MenuTrigger, MenuContent, MenuSeparator, MenuCheckboxItem, MenuItem } from './chunk-XSJ3TWWZ.js';
2
+ import { cn } from './chunk-BLJWR4ZV.js';
3
+ import * as React from 'react';
4
+ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
5
+
6
+ function deriveInitials(name) {
7
+ if (!name) return "";
8
+ const parts = name.trim().split(/\s+/).filter(Boolean);
9
+ if (parts.length === 0) return "";
10
+ const first = parts[0]?.charAt(0) ?? "";
11
+ const last = parts.length > 1 ? parts[parts.length - 1]?.charAt(0) ?? "" : "";
12
+ return (first + last).toUpperCase();
13
+ }
14
+ function UserGlyph() {
15
+ return /* @__PURE__ */ jsxs(
16
+ "svg",
17
+ {
18
+ viewBox: "0 0 24 24",
19
+ fill: "none",
20
+ stroke: "currentColor",
21
+ strokeWidth: "2",
22
+ strokeLinecap: "round",
23
+ strokeLinejoin: "round",
24
+ className: "size-4",
25
+ "aria-hidden": true,
26
+ children: [
27
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "8", r: "4" }),
28
+ /* @__PURE__ */ jsx("path", { d: "M4 21c0-4 3.6-6 8-6s8 2 8 6" })
29
+ ]
30
+ }
31
+ );
32
+ }
33
+ var UserMenu = React.forwardRef(
34
+ function UserMenu2({
35
+ name,
36
+ email,
37
+ avatarUrl,
38
+ initials,
39
+ items,
40
+ onItemSelect,
41
+ align = "end",
42
+ labels,
43
+ className
44
+ }, ref) {
45
+ const disc = initials ?? deriveInitials(name);
46
+ const hasIdentity = name !== void 0 || email !== void 0;
47
+ return /* @__PURE__ */ jsx("div", { ref, "data-as": "user-menu", className: cn("inline-flex", className), children: /* @__PURE__ */ jsxs(Menu, { children: [
48
+ /* @__PURE__ */ jsxs(
49
+ MenuTrigger,
50
+ {
51
+ type: "button",
52
+ "aria-label": labels?.openMenu ?? "Open user menu",
53
+ "aria-haspopup": "menu",
54
+ className: "flex cursor-pointer items-center gap-2.5 rounded-full p-1 pr-3 transition-colors hover:bg-[var(--as-secondary)] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--as-focus-ring)]",
55
+ children: [
56
+ avatarUrl ? /* @__PURE__ */ jsx(
57
+ "img",
58
+ {
59
+ src: avatarUrl,
60
+ alt: "",
61
+ className: "size-8 shrink-0 rounded-full object-cover"
62
+ }
63
+ ) : /* @__PURE__ */ jsx(
64
+ "span",
65
+ {
66
+ "aria-hidden": true,
67
+ className: "flex size-8 shrink-0 items-center justify-center rounded-full bg-[var(--as-primary)] text-xs font-bold text-[var(--as-primary-fg)]",
68
+ children: disc || /* @__PURE__ */ jsx(UserGlyph, {})
69
+ }
70
+ ),
71
+ name || email ? /* @__PURE__ */ jsxs("span", { className: "flex min-w-0 flex-col text-left leading-tight", children: [
72
+ name ? /* @__PURE__ */ jsx("span", { className: "max-w-40 truncate text-sm font-medium text-[var(--as-fg)]", children: name }) : null,
73
+ email ? /* @__PURE__ */ jsx("span", { className: "max-w-40 truncate text-xs text-[var(--as-muted-fg)]", children: email }) : null
74
+ ] }) : null
75
+ ]
76
+ }
77
+ ),
78
+ /* @__PURE__ */ jsxs(MenuContent, { align, sideOffset: 8, className: "min-w-48", children: [
79
+ hasIdentity ? /* @__PURE__ */ jsxs(Fragment, { children: [
80
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col px-2 py-1.5", children: [
81
+ name ? /* @__PURE__ */ jsx("span", { className: "truncate text-sm font-bold text-[var(--as-fg)]", children: name }) : null,
82
+ email ? /* @__PURE__ */ jsx("span", { className: "truncate text-xs text-[var(--as-muted-fg)]", children: email }) : null
83
+ ] }),
84
+ /* @__PURE__ */ jsx(MenuSeparator, {})
85
+ ] }) : null,
86
+ items.map(
87
+ (item) => item.checked !== void 0 ? /* @__PURE__ */ jsx(
88
+ MenuCheckboxItem,
89
+ {
90
+ icon: item.icon,
91
+ danger: item.tone === "danger",
92
+ disabled: item.disabled,
93
+ pending: item.pending,
94
+ checked: item.checked,
95
+ onSelect: () => onItemSelect(item.id),
96
+ children: item.label
97
+ },
98
+ item.id
99
+ ) : /* @__PURE__ */ jsx(
100
+ MenuItem,
101
+ {
102
+ icon: item.icon,
103
+ danger: item.tone === "danger",
104
+ disabled: item.disabled,
105
+ pending: item.pending,
106
+ onSelect: () => onItemSelect(item.id),
107
+ children: item.label
108
+ },
109
+ item.id
110
+ )
111
+ )
112
+ ] })
113
+ ] }) });
114
+ }
115
+ );
116
+
117
+ export { UserMenu };
118
+ //# sourceMappingURL=chunk-535TM7SK.js.map
119
+ //# sourceMappingURL=chunk-535TM7SK.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/components/user-menu/UserMenu.tsx"],"names":["UserMenu"],"mappings":"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","file":"chunk-535TM7SK.js","sourcesContent":["import * as React from 'react'\nimport type { LucideIcon } from 'lucide-react'\nimport {\n Menu,\n MenuCheckboxItem,\n MenuContent,\n MenuItem,\n MenuSeparator,\n MenuTrigger,\n} from '../menu/Menu'\nimport { cn } from '../../lib/utils'\n\nexport interface UserMenuItem {\n id: string\n label: string\n icon?: LucideIcon\n tone?: 'default' | 'danger'\n disabled?: boolean\n pending?: boolean\n /** Renders as a checkable item (aria-checked) — e.g. theme/language toggles. */\n checked?: boolean\n}\n\nexport interface UserMenuProps {\n name?: string\n email?: string\n avatarUrl?: string\n /** Fallback disc text when no avatarUrl; derived from `name` when omitted. */\n initials?: string\n items: UserMenuItem[]\n onItemSelect: (id: string) => void\n align?: 'start' | 'end'\n labels?: { openMenu?: string }\n className?: string\n}\n\nfunction deriveInitials(name?: string): string {\n if (!name) return ''\n const parts = name.trim().split(/\\s+/).filter(Boolean)\n if (parts.length === 0) return ''\n const first = parts[0]?.charAt(0) ?? ''\n const last = parts.length > 1 ? (parts[parts.length - 1]?.charAt(0) ?? '') : ''\n return (first + last).toUpperCase()\n}\n\nfunction UserGlyph() {\n return (\n <svg\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n className=\"size-4\"\n aria-hidden\n >\n <circle cx=\"12\" cy=\"8\" r=\"4\" />\n <path d=\"M4 21c0-4 3.6-6 8-6s8 2 8 6\" />\n </svg>\n )\n}\n\nexport const UserMenu = React.forwardRef<HTMLDivElement, UserMenuProps>(\n function UserMenu(\n {\n name,\n email,\n avatarUrl,\n initials,\n items,\n onItemSelect,\n align = 'end',\n labels,\n className,\n },\n ref,\n ) {\n const disc = initials ?? deriveInitials(name)\n const hasIdentity = name !== undefined || email !== undefined\n\n return (\n <div ref={ref} data-as=\"user-menu\" className={cn('inline-flex', className)}>\n <Menu>\n <MenuTrigger\n type=\"button\"\n aria-label={labels?.openMenu ?? 'Open user menu'}\n aria-haspopup=\"menu\"\n className=\"flex cursor-pointer items-center gap-2.5 rounded-full p-1 pr-3 transition-colors hover:bg-[var(--as-secondary)] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--as-focus-ring)]\"\n >\n {avatarUrl ? (\n <img\n src={avatarUrl}\n alt=\"\"\n className=\"size-8 shrink-0 rounded-full object-cover\"\n />\n ) : (\n <span\n aria-hidden\n className=\"flex size-8 shrink-0 items-center justify-center rounded-full bg-[var(--as-primary)] text-xs font-bold text-[var(--as-primary-fg)]\"\n >\n {disc || <UserGlyph />}\n </span>\n )}\n {name || email ? (\n <span className=\"flex min-w-0 flex-col text-left leading-tight\">\n {name ? (\n <span className=\"max-w-40 truncate text-sm font-medium text-[var(--as-fg)]\">\n {name}\n </span>\n ) : null}\n {email ? (\n <span className=\"max-w-40 truncate text-xs text-[var(--as-muted-fg)]\">\n {email}\n </span>\n ) : null}\n </span>\n ) : null}\n </MenuTrigger>\n <MenuContent align={align} sideOffset={8} className=\"min-w-48\">\n {hasIdentity ? (\n <>\n <div className=\"flex flex-col px-2 py-1.5\">\n {name ? (\n <span className=\"truncate text-sm font-bold text-[var(--as-fg)]\">\n {name}\n </span>\n ) : null}\n {email ? (\n <span className=\"truncate text-xs text-[var(--as-muted-fg)]\">\n {email}\n </span>\n ) : null}\n </div>\n <MenuSeparator />\n </>\n ) : null}\n {items.map((item) =>\n item.checked !== undefined ? (\n <MenuCheckboxItem\n key={item.id}\n icon={item.icon}\n danger={item.tone === 'danger'}\n disabled={item.disabled}\n pending={item.pending}\n checked={item.checked}\n onSelect={() => onItemSelect(item.id)}\n >\n {item.label}\n </MenuCheckboxItem>\n ) : (\n <MenuItem\n key={item.id}\n icon={item.icon}\n danger={item.tone === 'danger'}\n disabled={item.disabled}\n pending={item.pending}\n onSelect={() => onItemSelect(item.id)}\n >\n {item.label}\n </MenuItem>\n ),\n )}\n </MenuContent>\n </Menu>\n </div>\n )\n },\n)\n"]}
@@ -0,0 +1,428 @@
1
+ import { cn } from './chunk-BLJWR4ZV.js';
2
+ import * as React from 'react';
3
+ import * as PopoverPrimitive from '@radix-ui/react-popover';
4
+ import { ChevronRight, ChevronLeft, ChevronDown } from 'lucide-react';
5
+ import { jsxs, jsx } from 'react/jsx-runtime';
6
+
7
+ var FLYOUT_DELAY_MS = 150;
8
+ var itemButtonClass = (active) => cn(
9
+ "flex w-full cursor-pointer items-center gap-3 rounded-[var(--as-radius)] px-3 py-2.5 text-left text-sm transition-colors focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-[var(--as-focus-ring)] disabled:pointer-events-none disabled:opacity-50",
10
+ active ? "bg-[color-mix(in_srgb,var(--as-primary)_12%,transparent)] font-bold text-[var(--as-primary)]" : "font-medium text-[var(--as-fg)] hover:bg-[var(--as-muted)]"
11
+ );
12
+ var flyoutContentClass = "as-anim-pop z-[var(--as-z-popover)] min-w-44 rounded-[var(--as-radius-lg)] border border-[var(--as-border)] bg-[var(--as-surface-raised)] p-1 text-[var(--as-fg)] shadow-[var(--as-shadow-3)] focus:outline-none";
13
+ function Badge({ value, active }) {
14
+ return /* @__PURE__ */ jsx(
15
+ "span",
16
+ {
17
+ className: cn(
18
+ "inline-flex min-w-5 shrink-0 items-center justify-center rounded-full px-1.5 py-0.5 text-[11px] font-bold leading-none",
19
+ active ? "bg-[var(--as-primary)] text-[var(--as-primary-fg)]" : "bg-[var(--as-muted)] text-[var(--as-muted-fg)]"
20
+ ),
21
+ children: value
22
+ }
23
+ );
24
+ }
25
+ function ItemBadge({ badge, active }) {
26
+ if (badge === void 0) return null;
27
+ return /* @__PURE__ */ jsx("span", { className: "ml-auto", children: /* @__PURE__ */ jsx(Badge, { value: badge, active }) });
28
+ }
29
+ var SidebarNav = React.forwardRef(
30
+ function SidebarNav2({
31
+ items,
32
+ activeId,
33
+ onNavigate,
34
+ collapsed = false,
35
+ onCollapsedChange,
36
+ collapsible = false,
37
+ expandedIds,
38
+ onExpandedIdsChange,
39
+ header,
40
+ footer,
41
+ labels,
42
+ className,
43
+ navClassName
44
+ }, ref) {
45
+ const baseId = React.useId();
46
+ const listRef = React.useRef(null);
47
+ const flyoutTimer = React.useRef(null);
48
+ const [openFlyout, setOpenFlyout] = React.useState(null);
49
+ const [internalExpanded, setInternalExpanded] = React.useState([]);
50
+ const controlledExpanded = expandedIds !== void 0 && onExpandedIdsChange !== void 0;
51
+ const expanded = controlledExpanded ? expandedIds : internalExpanded;
52
+ const setExpandedList = React.useCallback(
53
+ (next) => {
54
+ if (controlledExpanded) onExpandedIdsChange?.(next);
55
+ else setInternalExpanded(next);
56
+ },
57
+ [controlledExpanded, onExpandedIdsChange]
58
+ );
59
+ const toggleGroup = React.useCallback(
60
+ (id) => {
61
+ setExpandedList(
62
+ expanded.includes(id) ? expanded.filter((g) => g !== id) : [...expanded, id]
63
+ );
64
+ },
65
+ [expanded, setExpandedList]
66
+ );
67
+ React.useEffect(() => {
68
+ if (controlledExpanded || activeId == null) return;
69
+ const owner = items.find((it) => it.children?.some((c) => c.id === activeId));
70
+ if (owner && !expanded.includes(owner.id)) {
71
+ setExpandedList([...expanded, owner.id]);
72
+ }
73
+ }, [activeId, items, controlledExpanded]);
74
+ React.useEffect(() => {
75
+ return () => {
76
+ if (flyoutTimer.current) clearTimeout(flyoutTimer.current);
77
+ };
78
+ }, []);
79
+ const flyoutEnter = (id) => {
80
+ if (flyoutTimer.current) clearTimeout(flyoutTimer.current);
81
+ flyoutTimer.current = setTimeout(() => setOpenFlyout(id), FLYOUT_DELAY_MS);
82
+ };
83
+ const flyoutLeave = () => {
84
+ if (flyoutTimer.current) clearTimeout(flyoutTimer.current);
85
+ flyoutTimer.current = setTimeout(() => setOpenFlyout(null), FLYOUT_DELAY_MS);
86
+ };
87
+ const flyoutEnterCancel = () => {
88
+ if (flyoutTimer.current) clearTimeout(flyoutTimer.current);
89
+ };
90
+ const navigate = (id) => {
91
+ setOpenFlyout(null);
92
+ onNavigate?.(id);
93
+ };
94
+ const visibleItems = () => {
95
+ const list = listRef.current;
96
+ if (!list) return [];
97
+ return Array.from(
98
+ list.querySelectorAll("[data-as-nav-item]")
99
+ ).filter((el) => el.closest("[hidden]") === null);
100
+ };
101
+ const focusSibling = (current, offset) => {
102
+ const all = visibleItems();
103
+ const index = all.indexOf(current);
104
+ if (index === -1) return;
105
+ const next = all[Math.min(Math.max(index + offset, 0), all.length - 1)];
106
+ next?.focus();
107
+ };
108
+ const handleFlyoutArrowKeys = (e) => {
109
+ if (e.key !== "ArrowDown" && e.key !== "ArrowUp") return;
110
+ e.preventDefault();
111
+ const buttons = Array.from(
112
+ e.currentTarget.querySelectorAll(
113
+ "button[data-as-flyout-item]:not([disabled])"
114
+ )
115
+ );
116
+ const index = buttons.indexOf(document.activeElement);
117
+ const step = e.key === "ArrowDown" ? 1 : buttons.length - 1;
118
+ buttons[(index + step) % buttons.length]?.focus();
119
+ };
120
+ const groupTriggerFor = (id) => Array.from(
121
+ listRef.current?.querySelectorAll(
122
+ "[data-as-nav-group-id]"
123
+ ) ?? []
124
+ ).find((b) => b.dataset.asNavGroupId === id);
125
+ const firstChildFor = (id) => Array.from(
126
+ listRef.current?.querySelectorAll(
127
+ "[data-as-nav-parent-id]"
128
+ ) ?? []
129
+ ).find((b) => b.dataset.asNavParentId === id && !b.disabled);
130
+ const handleListKeyDown = (e) => {
131
+ const button = e.target.closest(
132
+ "[data-as-nav-item]"
133
+ );
134
+ if (!button) return;
135
+ const groupId = button.dataset.asNavGroupId;
136
+ const parentId = button.dataset.asNavParentId;
137
+ switch (e.key) {
138
+ case "ArrowDown":
139
+ e.preventDefault();
140
+ focusSibling(button, 1);
141
+ break;
142
+ case "ArrowUp":
143
+ e.preventDefault();
144
+ focusSibling(button, -1);
145
+ break;
146
+ case "Home": {
147
+ e.preventDefault();
148
+ visibleItems()[0]?.focus();
149
+ break;
150
+ }
151
+ case "End": {
152
+ e.preventDefault();
153
+ const all = visibleItems();
154
+ all[all.length - 1]?.focus();
155
+ break;
156
+ }
157
+ case "ArrowRight":
158
+ if (groupId) {
159
+ e.preventDefault();
160
+ if (!expanded.includes(groupId)) {
161
+ setExpandedList([...expanded, groupId]);
162
+ } else {
163
+ firstChildFor(groupId)?.focus();
164
+ }
165
+ }
166
+ break;
167
+ case "ArrowLeft":
168
+ if (parentId) {
169
+ e.preventDefault();
170
+ groupTriggerFor(parentId)?.focus();
171
+ } else if (groupId && expanded.includes(groupId)) {
172
+ e.preventDefault();
173
+ toggleGroup(groupId);
174
+ }
175
+ break;
176
+ }
177
+ };
178
+ const navAria = labels?.navAria ?? "Main navigation";
179
+ const rail = collapsed;
180
+ const flyoutHeader = (text) => /* @__PURE__ */ jsx("div", { className: "border-b border-[var(--as-border)] px-2 py-1.5 text-xs font-bold text-[var(--as-muted-fg)]", children: text });
181
+ return /* @__PURE__ */ jsxs(
182
+ "nav",
183
+ {
184
+ ref,
185
+ "data-as": "sidebar-nav",
186
+ "aria-label": navAria,
187
+ className: cn(
188
+ "relative flex h-full flex-col border-r border-[var(--as-border)] bg-[var(--as-surface)] text-[var(--as-fg)] transition-[width] duration-200",
189
+ rail ? "w-20" : "w-64",
190
+ className
191
+ ),
192
+ children: [
193
+ collapsible ? /* @__PURE__ */ jsx("div", { className: "pointer-events-none absolute inset-y-0 -right-3 z-10 hidden flex-col justify-center lg:flex", children: /* @__PURE__ */ jsx(
194
+ "button",
195
+ {
196
+ type: "button",
197
+ onClick: () => onCollapsedChange?.(!collapsed),
198
+ title: collapsed ? labels?.expand ?? "Expand sidebar" : labels?.collapse ?? "Collapse sidebar",
199
+ "aria-label": collapsed ? labels?.expand ?? "Expand sidebar" : labels?.collapse ?? "Collapse sidebar",
200
+ className: "pointer-events-auto flex size-6 items-center justify-center rounded-lg border border-[var(--as-border)] bg-[var(--as-surface-raised)] text-[var(--as-muted-fg)] shadow-[var(--as-shadow-2)] transition-colors hover:text-[var(--as-fg)] focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-[var(--as-focus-ring)]",
201
+ children: collapsed ? /* @__PURE__ */ jsx(ChevronRight, { className: "size-3.5", "aria-hidden": true }) : /* @__PURE__ */ jsx(ChevronLeft, { className: "size-3.5", "aria-hidden": true })
202
+ }
203
+ ) }) : null,
204
+ header ? /* @__PURE__ */ jsx("div", { className: "shrink-0", children: header }) : null,
205
+ /* @__PURE__ */ jsx(
206
+ "div",
207
+ {
208
+ ref: listRef,
209
+ onKeyDown: handleListKeyDown,
210
+ className: cn("min-h-0 flex-1 overflow-y-auto px-3 py-3", navClassName),
211
+ children: /* @__PURE__ */ jsx("ul", { className: "space-y-1", children: items.map((item, index) => {
212
+ if (item.children !== void 0 && item.children.length === 0) {
213
+ return null;
214
+ }
215
+ const sectionSeen = !rail && item.section !== void 0 && items.findIndex((i) => i.section === item.section) === index;
216
+ const sectionDivider = sectionSeen ? /* @__PURE__ */ jsx(
217
+ "div",
218
+ {
219
+ role: "presentation",
220
+ className: "mt-2 border-t border-[var(--as-border)] px-3 pb-1 pt-3 text-[11px] font-bold uppercase tracking-wide text-[var(--as-muted-fg)]",
221
+ children: item.section
222
+ }
223
+ ) : null;
224
+ const active = item.id === activeId;
225
+ const childActive = item.children?.some((c) => c.id === activeId) ?? false;
226
+ const isExpanded = expanded.includes(item.id);
227
+ const Icon = item.icon;
228
+ const groupId = `${baseId}-group-${item.id}`;
229
+ if (item.children && item.children.length > 0 && rail) {
230
+ return /* @__PURE__ */ jsxs("li", { children: [
231
+ sectionDivider,
232
+ /* @__PURE__ */ jsxs(
233
+ PopoverPrimitive.Root,
234
+ {
235
+ open: openFlyout === item.id,
236
+ onOpenChange: (open) => {
237
+ if (!open) setOpenFlyout(null);
238
+ },
239
+ children: [
240
+ /* @__PURE__ */ jsx(PopoverPrimitive.Anchor, { asChild: true, children: /* @__PURE__ */ jsx(
241
+ "button",
242
+ {
243
+ type: "button",
244
+ "data-as-nav-item": true,
245
+ "data-as-nav-group-id": item.id,
246
+ disabled: item.disabled,
247
+ "aria-label": labels?.openGroup ?? `Open ${item.label} menu`,
248
+ "aria-haspopup": "true",
249
+ "aria-expanded": openFlyout === item.id,
250
+ onClick: () => setOpenFlyout(item.id),
251
+ onKeyDown: (e) => {
252
+ if (e.key === "Enter" || e.key === " ") {
253
+ e.preventDefault();
254
+ setOpenFlyout(item.id);
255
+ }
256
+ },
257
+ onMouseEnter: () => flyoutEnter(item.id),
258
+ onMouseLeave: flyoutLeave,
259
+ className: cn(itemButtonClass(active || childActive), "justify-center px-0"),
260
+ children: Icon ? /* @__PURE__ */ jsx(
261
+ Icon,
262
+ {
263
+ className: cn(
264
+ "size-5 shrink-0",
265
+ active || childActive ? "text-[var(--as-primary)]" : "text-[var(--as-muted-fg)]"
266
+ ),
267
+ "aria-hidden": true
268
+ }
269
+ ) : null
270
+ }
271
+ ) }),
272
+ /* @__PURE__ */ jsx(PopoverPrimitive.Portal, { children: /* @__PURE__ */ jsxs(
273
+ PopoverPrimitive.Content,
274
+ {
275
+ side: "right",
276
+ sideOffset: 8,
277
+ "aria-label": item.label,
278
+ onCloseAutoFocus: (e) => {
279
+ e.preventDefault();
280
+ groupTriggerFor(item.id)?.focus();
281
+ },
282
+ onMouseEnter: flyoutEnterCancel,
283
+ onMouseLeave: flyoutLeave,
284
+ onKeyDown: handleFlyoutArrowKeys,
285
+ className: flyoutContentClass,
286
+ children: [
287
+ flyoutHeader(item.label),
288
+ /* @__PURE__ */ jsx("ul", { className: "mt-1 space-y-0.5", children: item.children.map((child) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs(
289
+ "button",
290
+ {
291
+ type: "button",
292
+ "data-as-flyout-item": true,
293
+ disabled: child.disabled,
294
+ onClick: () => navigate(child.id),
295
+ "aria-current": child.id === activeId ? "page" : void 0,
296
+ className: cn(
297
+ "flex w-full cursor-pointer items-center gap-2 rounded-[calc(var(--as-radius-sm)-2px)] px-2 py-1.5 text-left text-sm outline-none transition-colors focus:bg-[var(--as-secondary)] disabled:pointer-events-none disabled:opacity-50",
298
+ child.id === activeId && "font-bold text-[var(--as-primary)]"
299
+ ),
300
+ children: [
301
+ child.icon ? /* @__PURE__ */ jsx(child.icon, { "aria-hidden": true }) : null,
302
+ /* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate", children: child.label }),
303
+ /* @__PURE__ */ jsx(ItemBadge, { badge: child.badge, active: child.id === activeId })
304
+ ]
305
+ }
306
+ ) }, child.id)) })
307
+ ]
308
+ }
309
+ ) })
310
+ ]
311
+ }
312
+ )
313
+ ] }, item.id);
314
+ }
315
+ if (item.children && item.children.length > 0) {
316
+ return /* @__PURE__ */ jsxs("li", { children: [
317
+ sectionDivider,
318
+ /* @__PURE__ */ jsxs(
319
+ "button",
320
+ {
321
+ type: "button",
322
+ "data-as-nav-item": true,
323
+ "data-as-nav-group-id": item.id,
324
+ disabled: item.disabled,
325
+ "aria-expanded": isExpanded,
326
+ "aria-controls": groupId,
327
+ onClick: () => toggleGroup(item.id),
328
+ className: itemButtonClass(active || childActive),
329
+ children: [
330
+ Icon ? /* @__PURE__ */ jsx(
331
+ Icon,
332
+ {
333
+ className: cn(
334
+ "size-4 shrink-0",
335
+ active || childActive ? "text-[var(--as-primary)]" : "text-[var(--as-muted-fg)]"
336
+ ),
337
+ "aria-hidden": true
338
+ }
339
+ ) : null,
340
+ /* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate", children: item.label }),
341
+ /* @__PURE__ */ jsx(ItemBadge, { badge: item.badge, active: active || childActive }),
342
+ isExpanded ? /* @__PURE__ */ jsx(ChevronDown, { className: "size-4 shrink-0 text-[var(--as-muted-fg)]", "aria-hidden": true }) : /* @__PURE__ */ jsx(ChevronRight, { className: "size-4 shrink-0 text-[var(--as-muted-fg)]", "aria-hidden": true })
343
+ ]
344
+ }
345
+ ),
346
+ /* @__PURE__ */ jsx("ul", { id: groupId, hidden: !isExpanded, className: "mt-1 space-y-0.5 pl-6", children: item.children.map((child) => {
347
+ const childActiveItem = child.id === activeId;
348
+ const ChildIcon = child.icon;
349
+ const sectionSeen2 = child.section !== void 0 && item.children.findIndex(
350
+ (c) => c.section === child.section
351
+ ) === item.children.indexOf(child);
352
+ return /* @__PURE__ */ jsxs("li", { children: [
353
+ sectionSeen2 ? /* @__PURE__ */ jsx(
354
+ "div",
355
+ {
356
+ role: "presentation",
357
+ className: "mt-2 border-t border-[var(--as-border)] px-3 pb-1 pt-3 text-[11px] font-bold uppercase tracking-wide text-[var(--as-muted-fg)]",
358
+ children: child.section
359
+ }
360
+ ) : null,
361
+ /* @__PURE__ */ jsxs(
362
+ "button",
363
+ {
364
+ type: "button",
365
+ "data-as-nav-item": true,
366
+ "data-as-nav-parent-id": item.id,
367
+ disabled: child.disabled,
368
+ "aria-current": childActiveItem ? "page" : void 0,
369
+ onClick: () => navigate(child.id),
370
+ className: cn(
371
+ itemButtonClass(childActiveItem),
372
+ "py-2 pl-3 pr-2"
373
+ ),
374
+ children: [
375
+ ChildIcon ? /* @__PURE__ */ jsx(ChildIcon, { className: "size-4 shrink-0", "aria-hidden": true }) : null,
376
+ /* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate", children: child.label }),
377
+ /* @__PURE__ */ jsx(ItemBadge, { badge: child.badge, active: childActiveItem })
378
+ ]
379
+ }
380
+ )
381
+ ] }, child.id);
382
+ }) })
383
+ ] }, item.id);
384
+ }
385
+ return /* @__PURE__ */ jsxs("li", { children: [
386
+ sectionDivider,
387
+ /* @__PURE__ */ jsxs(
388
+ "button",
389
+ {
390
+ type: "button",
391
+ "data-as-nav-item": true,
392
+ disabled: item.disabled,
393
+ title: rail ? item.label : void 0,
394
+ "aria-label": rail ? item.label : void 0,
395
+ "aria-current": active ? "page" : void 0,
396
+ onClick: () => navigate(item.id),
397
+ className: cn(itemButtonClass(active), rail && "justify-center px-0"),
398
+ children: [
399
+ Icon ? /* @__PURE__ */ jsx(
400
+ Icon,
401
+ {
402
+ className: cn(
403
+ "shrink-0",
404
+ rail ? "size-5" : "size-4",
405
+ active ? "text-[var(--as-primary)]" : "text-[var(--as-muted-fg)]"
406
+ ),
407
+ "aria-hidden": true
408
+ }
409
+ ) : null,
410
+ !rail ? /* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate", children: item.label }) : null,
411
+ !rail ? /* @__PURE__ */ jsx(ItemBadge, { badge: item.badge, active }) : null
412
+ ]
413
+ }
414
+ )
415
+ ] }, item.id);
416
+ }) })
417
+ }
418
+ ),
419
+ footer ? /* @__PURE__ */ jsx("div", { className: "shrink-0 border-t border-[var(--as-border)] p-3", children: footer }) : null
420
+ ]
421
+ }
422
+ );
423
+ }
424
+ );
425
+
426
+ export { SidebarNav };
427
+ //# sourceMappingURL=chunk-EZTQMJ5I.js.map
428
+ //# sourceMappingURL=chunk-EZTQMJ5I.js.map