@geomak/ui 3.0.0 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.cts CHANGED
@@ -1812,40 +1812,84 @@ interface AutoCompleteProps {
1812
1812
  */
1813
1813
  declare function AutoComplete({ disabled, label, placeholder, name, inputStyle, style, layout, items, onItemClick, emptyText, }: AutoCompleteProps): react_jsx_runtime.JSX.Element;
1814
1814
 
1815
- interface TreeSelectItem {
1815
+ interface TreeSelectNode {
1816
1816
  key: string | number;
1817
1817
  label: React$1.ReactNode;
1818
1818
  icon?: React$1.ReactNode;
1819
+ /** Nested children. If present, this node is treated as a parent (branch). */
1820
+ children?: TreeSelectNode[];
1821
+ /** Render the node disabled — visible but not selectable. */
1822
+ disabled?: boolean;
1819
1823
  }
1820
1824
  interface TreeSelectProps {
1821
- hasSearch?: boolean;
1822
- label?: React$1.ReactNode;
1823
- name?: string;
1824
- value?: any;
1825
+ /** Tree data. Each node may optionally carry `children` for sub-branches. */
1826
+ items: TreeSelectNode[];
1827
+ /** Currently selected key (single-select). Pass `undefined`/`null` for unset. */
1828
+ value?: string | number | null;
1829
+ /** Fires with the next selected key (and the corresponding event-like target). */
1825
1830
  onChange?: (e: {
1826
1831
  target: {
1827
- value: any;
1832
+ value: string | number;
1828
1833
  id?: string;
1829
1834
  name?: string;
1830
1835
  };
1831
1836
  }) => void;
1832
- onBlur?: React$1.FocusEventHandler;
1837
+ label?: React$1.ReactNode;
1838
+ placeholder?: string;
1839
+ /** Form control id linkage. */
1840
+ htmlFor?: string;
1841
+ name?: string;
1842
+ /** Label/trigger orientation. Defaults to `'horizontal'`. */
1843
+ layout?: 'horizontal' | 'vertical';
1833
1844
  disabled?: boolean;
1834
- /** 'horizontal' | 'vertical' */
1835
- layout?: string;
1836
1845
  errorMessage?: React$1.ReactNode;
1837
1846
  style?: React$1.CSSProperties;
1838
- htmlFor?: string;
1839
- items?: TreeSelectItem[];
1847
+ /**
1848
+ * Whether parent (branch) nodes can be selected. When `false`, parents
1849
+ * only expand/collapse and only leaves are picked. Default `true`.
1850
+ */
1851
+ parentsSelectable?: boolean;
1852
+ /** Keys of nodes that should start expanded. */
1853
+ defaultExpandedKeys?: (string | number)[];
1840
1854
  }
1841
1855
  /**
1842
- * Single-value select with a flat list, powered by Radix Popover.
1843
- * Functionally similar to Dropdown (single-select only).
1856
+ * Hierarchical single-select with expandable branches.
1857
+ *
1858
+ * Built on `@radix-ui/react-popover` for portal positioning and click-outside.
1859
+ * Tree state (expanded keys) is local; selected value is controlled by the
1860
+ * parent.
1861
+ *
1862
+ * **Keyboard model** (focus is on the trigger or any item):
1863
+ * - `Enter` / `Space` / `↓` / `↑` on the trigger — open the popover
1864
+ * - `↓` / `↑` — move active item through the visible (un-collapsed) list
1865
+ * - `→` — expand a branch (or move into it if already expanded)
1866
+ * - `←` — collapse a branch (or move to its parent if already collapsed)
1867
+ * - `Enter` / `Space` — select the active item (if selectable)
1868
+ * - `Esc` — close the popover, return focus to the trigger
1844
1869
  *
1845
1870
  * @example
1846
- * <TreeSelect label="Fleet" items={fleets} value={form.fleet} onChange={handleChange} htmlFor="fleet" />
1871
+ * ```tsx
1872
+ * type FleetKey = number
1873
+ * const [fleet, setFleet] = useState<FleetKey | null>(null)
1874
+ *
1875
+ * const tree: TreeSelectNode[] = [
1876
+ * { key: 'eu', label: 'Europe', children: [
1877
+ * { key: 1, label: 'Aegean Fleet' },
1878
+ * { key: 2, label: 'Adriatic Fleet' },
1879
+ * ]},
1880
+ * { key: 'asia', label: 'Asia', children: [{ key: 3, label: 'Pacific Fleet' }] },
1881
+ * ]
1882
+ *
1883
+ * <TreeSelect
1884
+ * label="Fleet"
1885
+ * items={tree}
1886
+ * value={fleet}
1887
+ * onChange={({ target }) => setFleet(target.value as FleetKey)}
1888
+ * parentsSelectable={false}
1889
+ * />
1890
+ * ```
1847
1891
  */
1848
- declare function TreeSelect({ label, name, value, onChange, disabled, layout, errorMessage, style, htmlFor, items, }: TreeSelectProps): react_jsx_runtime.JSX.Element;
1892
+ declare function TreeSelect({ label, name, value, onChange, disabled, layout, errorMessage, style, htmlFor, items, placeholder, parentsSelectable, defaultExpandedKeys, }: TreeSelectProps): react_jsx_runtime.JSX.Element;
1849
1893
 
1850
1894
  interface FileInputProps {
1851
1895
  allowMultiple?: boolean;
package/dist/index.d.ts CHANGED
@@ -1812,40 +1812,84 @@ interface AutoCompleteProps {
1812
1812
  */
1813
1813
  declare function AutoComplete({ disabled, label, placeholder, name, inputStyle, style, layout, items, onItemClick, emptyText, }: AutoCompleteProps): react_jsx_runtime.JSX.Element;
1814
1814
 
1815
- interface TreeSelectItem {
1815
+ interface TreeSelectNode {
1816
1816
  key: string | number;
1817
1817
  label: React$1.ReactNode;
1818
1818
  icon?: React$1.ReactNode;
1819
+ /** Nested children. If present, this node is treated as a parent (branch). */
1820
+ children?: TreeSelectNode[];
1821
+ /** Render the node disabled — visible but not selectable. */
1822
+ disabled?: boolean;
1819
1823
  }
1820
1824
  interface TreeSelectProps {
1821
- hasSearch?: boolean;
1822
- label?: React$1.ReactNode;
1823
- name?: string;
1824
- value?: any;
1825
+ /** Tree data. Each node may optionally carry `children` for sub-branches. */
1826
+ items: TreeSelectNode[];
1827
+ /** Currently selected key (single-select). Pass `undefined`/`null` for unset. */
1828
+ value?: string | number | null;
1829
+ /** Fires with the next selected key (and the corresponding event-like target). */
1825
1830
  onChange?: (e: {
1826
1831
  target: {
1827
- value: any;
1832
+ value: string | number;
1828
1833
  id?: string;
1829
1834
  name?: string;
1830
1835
  };
1831
1836
  }) => void;
1832
- onBlur?: React$1.FocusEventHandler;
1837
+ label?: React$1.ReactNode;
1838
+ placeholder?: string;
1839
+ /** Form control id linkage. */
1840
+ htmlFor?: string;
1841
+ name?: string;
1842
+ /** Label/trigger orientation. Defaults to `'horizontal'`. */
1843
+ layout?: 'horizontal' | 'vertical';
1833
1844
  disabled?: boolean;
1834
- /** 'horizontal' | 'vertical' */
1835
- layout?: string;
1836
1845
  errorMessage?: React$1.ReactNode;
1837
1846
  style?: React$1.CSSProperties;
1838
- htmlFor?: string;
1839
- items?: TreeSelectItem[];
1847
+ /**
1848
+ * Whether parent (branch) nodes can be selected. When `false`, parents
1849
+ * only expand/collapse and only leaves are picked. Default `true`.
1850
+ */
1851
+ parentsSelectable?: boolean;
1852
+ /** Keys of nodes that should start expanded. */
1853
+ defaultExpandedKeys?: (string | number)[];
1840
1854
  }
1841
1855
  /**
1842
- * Single-value select with a flat list, powered by Radix Popover.
1843
- * Functionally similar to Dropdown (single-select only).
1856
+ * Hierarchical single-select with expandable branches.
1857
+ *
1858
+ * Built on `@radix-ui/react-popover` for portal positioning and click-outside.
1859
+ * Tree state (expanded keys) is local; selected value is controlled by the
1860
+ * parent.
1861
+ *
1862
+ * **Keyboard model** (focus is on the trigger or any item):
1863
+ * - `Enter` / `Space` / `↓` / `↑` on the trigger — open the popover
1864
+ * - `↓` / `↑` — move active item through the visible (un-collapsed) list
1865
+ * - `→` — expand a branch (or move into it if already expanded)
1866
+ * - `←` — collapse a branch (or move to its parent if already collapsed)
1867
+ * - `Enter` / `Space` — select the active item (if selectable)
1868
+ * - `Esc` — close the popover, return focus to the trigger
1844
1869
  *
1845
1870
  * @example
1846
- * <TreeSelect label="Fleet" items={fleets} value={form.fleet} onChange={handleChange} htmlFor="fleet" />
1871
+ * ```tsx
1872
+ * type FleetKey = number
1873
+ * const [fleet, setFleet] = useState<FleetKey | null>(null)
1874
+ *
1875
+ * const tree: TreeSelectNode[] = [
1876
+ * { key: 'eu', label: 'Europe', children: [
1877
+ * { key: 1, label: 'Aegean Fleet' },
1878
+ * { key: 2, label: 'Adriatic Fleet' },
1879
+ * ]},
1880
+ * { key: 'asia', label: 'Asia', children: [{ key: 3, label: 'Pacific Fleet' }] },
1881
+ * ]
1882
+ *
1883
+ * <TreeSelect
1884
+ * label="Fleet"
1885
+ * items={tree}
1886
+ * value={fleet}
1887
+ * onChange={({ target }) => setFleet(target.value as FleetKey)}
1888
+ * parentsSelectable={false}
1889
+ * />
1890
+ * ```
1847
1891
  */
1848
- declare function TreeSelect({ label, name, value, onChange, disabled, layout, errorMessage, style, htmlFor, items, }: TreeSelectProps): react_jsx_runtime.JSX.Element;
1892
+ declare function TreeSelect({ label, name, value, onChange, disabled, layout, errorMessage, style, htmlFor, items, placeholder, parentsSelectable, defaultExpandedKeys, }: TreeSelectProps): react_jsx_runtime.JSX.Element;
1849
1893
 
1850
1894
  interface FileInputProps {
1851
1895
  allowMultiple?: boolean;
package/dist/index.js CHANGED
@@ -3143,6 +3143,26 @@ function AutoComplete({
3143
3143
  }
3144
3144
  ) });
3145
3145
  }
3146
+ function flattenVisible(items, expanded, depth = 0, out = []) {
3147
+ for (const node of items) {
3148
+ const isParent2 = !!node.children && node.children.length > 0;
3149
+ out.push({ node, depth, isParent: isParent2 });
3150
+ if (isParent2 && expanded.has(node.key)) {
3151
+ flattenVisible(node.children, expanded, depth + 1, out);
3152
+ }
3153
+ }
3154
+ return out;
3155
+ }
3156
+ function findNodeByKey(items, key) {
3157
+ for (const n of items) {
3158
+ if (n.key === key) return n;
3159
+ if (n.children) {
3160
+ const found = findNodeByKey(n.children, key);
3161
+ if (found) return found;
3162
+ }
3163
+ }
3164
+ return null;
3165
+ }
3146
3166
  function TreeSelect({
3147
3167
  label,
3148
3168
  name,
@@ -3151,106 +3171,231 @@ function TreeSelect({
3151
3171
  disabled,
3152
3172
  layout = "horizontal",
3153
3173
  errorMessage,
3154
- style = {},
3174
+ style,
3155
3175
  htmlFor,
3156
- items = []
3176
+ items = [],
3177
+ placeholder = "Select\u2026",
3178
+ parentsSelectable = true,
3179
+ defaultExpandedKeys = []
3157
3180
  }) {
3181
+ const errorId = useId();
3182
+ const hasError = errorMessage != null;
3158
3183
  const [open, setOpen] = useState(false);
3159
- const [hoveredItem, setHoveredItem] = useState(null);
3160
- const [innerItems, setInnerItems] = useState([]);
3184
+ const [expanded, setExpanded] = useState(() => new Set(defaultExpandedKeys));
3185
+ const [activeIndex, setActiveIndex] = useState(0);
3186
+ const listRef = useRef(null);
3187
+ const visible = useMemo(() => flattenVisible(items, expanded), [items, expanded]);
3161
3188
  useEffect(() => {
3162
- setInnerItems(items);
3163
- }, [items]);
3164
- const selectItem = (key) => {
3189
+ if (!open) return;
3190
+ const selectedIdx = visible.findIndex((v) => v.node.key === value);
3191
+ setActiveIndex(selectedIdx >= 0 ? selectedIdx : 0);
3192
+ }, [open, visible, value]);
3193
+ const selectedNode = useMemo(
3194
+ () => value != null ? findNodeByKey(items, value) : null,
3195
+ [items, value]
3196
+ );
3197
+ const toggleExpand = (key) => {
3198
+ setExpanded((prev) => {
3199
+ const next = new Set(prev);
3200
+ if (next.has(key)) next.delete(key);
3201
+ else next.add(key);
3202
+ return next;
3203
+ });
3204
+ };
3205
+ const selectKey = (key) => {
3165
3206
  onChange?.({ target: { value: key, id: htmlFor, name } });
3166
3207
  setOpen(false);
3167
3208
  };
3168
- return /* @__PURE__ */ jsxs("div", { className: "mt-2", children: [
3169
- /* @__PURE__ */ jsxs(
3170
- "div",
3171
- {
3172
- className: `flex ${layout === "vertical" ? "flex-col" : "flex-row items-center gap-2"}`,
3173
- children: [
3174
- label && /* @__PURE__ */ jsx(
3175
- "label",
3176
- {
3177
- className: "text-md font-bold ml-1 max-content select-none text-prussian-blue dark:text-white",
3178
- htmlFor,
3179
- children: label
3180
- }
3181
- ),
3182
- /* @__PURE__ */ jsxs(Popover.Root, { open: open && !disabled, onOpenChange: (o) => !disabled && setOpen(o), children: [
3183
- /* @__PURE__ */ jsx(Popover.Trigger, { asChild: true, children: /* @__PURE__ */ jsxs(
3209
+ const onListKey = (e) => {
3210
+ if (visible.length === 0) return;
3211
+ const cur = visible[activeIndex];
3212
+ if (e.key === "ArrowDown") {
3213
+ e.preventDefault();
3214
+ setActiveIndex((i) => Math.min(i + 1, visible.length - 1));
3215
+ } else if (e.key === "ArrowUp") {
3216
+ e.preventDefault();
3217
+ setActiveIndex((i) => Math.max(i - 1, 0));
3218
+ } else if (e.key === "ArrowRight") {
3219
+ e.preventDefault();
3220
+ if (cur.isParent) {
3221
+ if (!expanded.has(cur.node.key)) toggleExpand(cur.node.key);
3222
+ else setActiveIndex((i) => Math.min(i + 1, visible.length - 1));
3223
+ }
3224
+ } else if (e.key === "ArrowLeft") {
3225
+ e.preventDefault();
3226
+ if (cur.isParent && expanded.has(cur.node.key)) {
3227
+ toggleExpand(cur.node.key);
3228
+ } else if (cur.depth > 0) {
3229
+ for (let i = activeIndex - 1; i >= 0; i--) {
3230
+ if (visible[i].depth < cur.depth) {
3231
+ setActiveIndex(i);
3232
+ break;
3233
+ }
3234
+ }
3235
+ }
3236
+ } else if (e.key === "Enter" || e.key === " ") {
3237
+ e.preventDefault();
3238
+ if (cur.node.disabled) return;
3239
+ if (cur.isParent && !parentsSelectable) {
3240
+ toggleExpand(cur.node.key);
3241
+ } else {
3242
+ selectKey(cur.node.key);
3243
+ }
3244
+ } else if (e.key === "Escape") {
3245
+ e.preventDefault();
3246
+ setOpen(false);
3247
+ }
3248
+ };
3249
+ return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1", children: [
3250
+ /* @__PURE__ */ jsxs("div", { className: `flex ${layout === "vertical" ? "flex-col gap-1" : "flex-row items-center gap-2"}`, children: [
3251
+ label && /* @__PURE__ */ jsx(
3252
+ "label",
3253
+ {
3254
+ className: "text-sm font-medium ml-1 max-content select-none text-foreground",
3255
+ htmlFor,
3256
+ children: label
3257
+ }
3258
+ ),
3259
+ /* @__PURE__ */ jsxs(Popover.Root, { open: open && !disabled, onOpenChange: (o) => !disabled && setOpen(o), children: [
3260
+ /* @__PURE__ */ jsx(Popover.Trigger, { asChild: true, children: /* @__PURE__ */ jsxs(
3261
+ "button",
3262
+ {
3263
+ id: htmlFor,
3264
+ type: "button",
3265
+ style,
3266
+ role: "combobox",
3267
+ "aria-expanded": open,
3268
+ "aria-haspopup": "listbox",
3269
+ "aria-invalid": hasError || void 0,
3270
+ "aria-describedby": hasError ? errorId : void 0,
3271
+ disabled,
3272
+ className: `flex items-center justify-between h-9 rounded-lg border ${hasError ? "border-status-error" : "border-border"} px-3 cursor-pointer select-none focus:outline-none focus-visible:ring-2 focus-visible:ring-accent ${disabled ? "cursor-not-allowed bg-surface-raised text-foreground-muted" : "bg-surface text-foreground"} ${!style?.width ? "min-w-[240px]" : ""}`,
3273
+ children: [
3274
+ /* @__PURE__ */ jsx("span", { className: "text-sm truncate text-left", children: selectedNode ? selectedNode.label : /* @__PURE__ */ jsx("span", { className: "text-foreground-muted", children: placeholder }) }),
3275
+ /* @__PURE__ */ jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, className: `h-4 w-4 flex-shrink-0 transition-transform duration-200 ${open ? "rotate-180" : ""}`, "aria-hidden": "true", children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M19 9l-7 7-7-7" }) })
3276
+ ]
3277
+ }
3278
+ ) }),
3279
+ /* @__PURE__ */ jsx(Popover.Portal, { children: /* @__PURE__ */ jsx(
3280
+ Popover.Content,
3281
+ {
3282
+ align: "start",
3283
+ sideOffset: 4,
3284
+ style: { width: style?.width || 280 },
3285
+ className: "bg-surface text-foreground border border-border rounded-lg shadow-md z-50 p-1 animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95",
3286
+ onOpenAutoFocus: (e) => {
3287
+ e.preventDefault();
3288
+ listRef.current?.focus();
3289
+ },
3290
+ children: /* @__PURE__ */ jsx(
3184
3291
  "div",
3185
3292
  {
3186
- id: htmlFor,
3187
- style,
3188
- role: "combobox",
3189
- "aria-expanded": open,
3190
- "aria-haspopup": "listbox",
3191
- className: `flex items-center justify-between relative h-9 rounded-lg p-2 cursor-pointer ${disabled ? "cursor-not-allowed bg-disabled" : "bg-white"}`,
3192
- tabIndex: disabled ? -1 : 0,
3193
- children: [
3194
- /* @__PURE__ */ jsx("div", { className: `h-7 ${!style?.width ? "min-w-[240px]" : ""} focus:outline-none text-prussian-blue flex items-center gap-1`, children: Array.isArray(value) ? /* @__PURE__ */ jsxs(Fragment, { children: [
3195
- value.slice(0, 1).map((val, id) => /* @__PURE__ */ jsx(
3196
- DropdownPill,
3197
- {
3198
- hasSiblings: value.length > 1,
3199
- value: innerItems.find((it) => it.key === val)?.label
3200
- },
3201
- id
3202
- )),
3203
- value.length > 1 && /* @__PURE__ */ jsx(DropdownPill, { value: `+${value.length - 1} more` })
3204
- ] }) : value != null ? /* @__PURE__ */ jsx(DropdownPill, { value: innerItems.find((it) => it.key === value)?.label }) : null }),
3205
- /* @__PURE__ */ jsx("div", { className: `transition-transform duration-300 ml-2 ${open ? "rotate-180" : "rotate-0"}`, children: /* @__PURE__ */ jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: colors_default.PALETTE["prussian-blue"], strokeWidth: 2, className: "h-4 w-4", children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M19 9l-7 7-7-7" }) }) })
3206
- ]
3207
- }
3208
- ) }),
3209
- /* @__PURE__ */ jsx(Popover.Portal, { children: /* @__PURE__ */ jsx(
3210
- Popover.Content,
3211
- {
3212
- align: "start",
3213
- sideOffset: 4,
3214
- style: { width: style?.width || 240 },
3215
- className: "bg-ice rounded-lg shadow-md z-50 p-2 animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95",
3216
- children: /* @__PURE__ */ jsx("div", { role: "listbox", className: "max-h-40 overflow-y-auto", children: innerItems.map((item, idx) => /* @__PURE__ */ jsxs(
3217
- "div",
3293
+ ref: listRef,
3294
+ role: "tree",
3295
+ tabIndex: -1,
3296
+ "aria-activedescendant": visible[activeIndex] ? `tree-opt-${visible[activeIndex].node.key}` : void 0,
3297
+ onKeyDown: onListKey,
3298
+ className: "max-h-72 overflow-y-auto outline-none",
3299
+ children: visible.length === 0 ? /* @__PURE__ */ jsx("div", { className: "px-3 py-4 text-sm text-foreground-secondary text-center", children: "No items" }) : visible.map((v, idx) => /* @__PURE__ */ jsx(
3300
+ TreeNodeRow,
3218
3301
  {
3219
- role: "option",
3220
- "aria-selected": value === item.key,
3221
- "aria-rowindex": idx,
3222
- className: `flex items-center justify-between p-2 hover:bg-prussian-blue hover:text-white transition-all duration-150 text-sm text-prussian-blue rounded-lg cursor-pointer`,
3223
- onClick: () => selectItem(item.key),
3224
- onMouseEnter: () => setHoveredItem(item.key),
3225
- onMouseLeave: () => setHoveredItem(null),
3226
- children: [
3227
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 text-xs", children: [
3228
- item.icon && /* @__PURE__ */ jsx("div", { children: item.icon }),
3229
- item.label
3230
- ] }),
3231
- value === item.key && /* @__PURE__ */ jsx("svg", { width: "16", height: "16", viewBox: "0 0 20 20", fill: "none", children: /* @__PURE__ */ jsx(
3232
- "path",
3233
- {
3234
- d: "M4 10l4.5 4.5L16 6",
3235
- stroke: hoveredItem === item.key ? "#fff" : colors_default.PALETTE["prussian-blue"],
3236
- strokeWidth: "2",
3237
- strokeLinecap: "round",
3238
- strokeLinejoin: "round"
3239
- }
3240
- ) })
3241
- ]
3302
+ node: v.node,
3303
+ depth: v.depth,
3304
+ isParent: v.isParent,
3305
+ isExpanded: expanded.has(v.node.key),
3306
+ isActive: idx === activeIndex,
3307
+ isSelected: value === v.node.key,
3308
+ parentsSelectable,
3309
+ onActivate: () => {
3310
+ if (v.node.disabled) return;
3311
+ if (v.isParent && !parentsSelectable) toggleExpand(v.node.key);
3312
+ else selectKey(v.node.key);
3313
+ },
3314
+ onToggle: () => toggleExpand(v.node.key),
3315
+ onHover: () => setActiveIndex(idx)
3242
3316
  },
3243
- item.key
3244
- )) })
3317
+ v.node.key
3318
+ ))
3245
3319
  }
3246
- ) })
3247
- ] })
3248
- ]
3249
- }
3250
- ),
3251
- /* @__PURE__ */ jsx("div", { className: "text-center text-error dark:text-prussian-blue min-h-0", children: errorMessage })
3320
+ )
3321
+ }
3322
+ ) })
3323
+ ] })
3324
+ ] }),
3325
+ hasError && /* @__PURE__ */ jsx("div", { id: errorId, className: "text-xs text-status-error ml-1", children: errorMessage })
3252
3326
  ] });
3253
3327
  }
3328
+ function TreeNodeRow({
3329
+ node,
3330
+ depth,
3331
+ isParent: isParent2,
3332
+ isExpanded,
3333
+ isActive,
3334
+ isSelected,
3335
+ parentsSelectable,
3336
+ onActivate,
3337
+ onToggle,
3338
+ onHover
3339
+ }) {
3340
+ const disabled = node.disabled;
3341
+ return /* @__PURE__ */ jsxs(
3342
+ "div",
3343
+ {
3344
+ id: `tree-opt-${node.key}`,
3345
+ role: "treeitem",
3346
+ "aria-selected": isSelected,
3347
+ "aria-expanded": isParent2 ? isExpanded : void 0,
3348
+ "aria-disabled": disabled || void 0,
3349
+ "data-active": isActive ? "" : void 0,
3350
+ onMouseEnter: onHover,
3351
+ className: `flex items-center gap-1 rounded-md px-1 py-1.5 select-none cursor-pointer transition-colors duration-100 ${disabled ? "opacity-40 cursor-not-allowed" : isActive ? "bg-accent text-accent-fg" : isSelected ? "bg-surface-raised" : "hover:bg-surface-raised"}`,
3352
+ style: { paddingLeft: depth * 16 + 4 },
3353
+ children: [
3354
+ isParent2 ? /* @__PURE__ */ jsx(
3355
+ "button",
3356
+ {
3357
+ type: "button",
3358
+ onClick: (e) => {
3359
+ e.stopPropagation();
3360
+ onToggle();
3361
+ },
3362
+ "aria-label": isExpanded ? "Collapse" : "Expand",
3363
+ tabIndex: -1,
3364
+ className: "w-5 h-5 inline-flex items-center justify-center rounded hover:bg-black/10 focus:outline-none",
3365
+ children: /* @__PURE__ */ jsx(
3366
+ "svg",
3367
+ {
3368
+ viewBox: "0 0 24 24",
3369
+ fill: "none",
3370
+ stroke: "currentColor",
3371
+ strokeWidth: 2.5,
3372
+ className: `h-3 w-3 transition-transform duration-150 ${isExpanded ? "rotate-0" : "-rotate-90"}`,
3373
+ "aria-hidden": "true",
3374
+ children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M19 9l-7 7-7-7" })
3375
+ }
3376
+ )
3377
+ }
3378
+ ) : /* @__PURE__ */ jsx("span", { className: "w-5 h-5 inline-block", "aria-hidden": "true" }),
3379
+ /* @__PURE__ */ jsxs(
3380
+ "button",
3381
+ {
3382
+ type: "button",
3383
+ onClick: onActivate,
3384
+ disabled,
3385
+ tabIndex: -1,
3386
+ className: "flex-1 flex items-center gap-2 text-sm text-left focus:outline-none disabled:cursor-not-allowed",
3387
+ children: [
3388
+ node.icon,
3389
+ /* @__PURE__ */ jsx("span", { className: "truncate", children: node.label }),
3390
+ isParent2 && !parentsSelectable && /* @__PURE__ */ jsx("span", { className: "ml-auto text-xs text-foreground-muted", children: "parent" })
3391
+ ]
3392
+ }
3393
+ ),
3394
+ isSelected && /* @__PURE__ */ jsx("svg", { width: "16", height: "16", viewBox: "0 0 20 20", fill: "none", "aria-hidden": "true", className: "ml-1", children: /* @__PURE__ */ jsx("path", { d: "M4 10l4.5 4.5L16 6", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round" }) })
3395
+ ]
3396
+ }
3397
+ );
3398
+ }
3254
3399
  function FileInput({
3255
3400
  allowMultiple = false,
3256
3401
  onChange,