@iris-ui-kit/react 0.1.1 → 0.2.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.
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/undo/useUndoStack.ts"],"names":[],"mappings":";;;;;;AAWA,SAAS,cAAA,CACP,OACA,QAAA,EACc;AACd,EAAA,MAAM,QAAA,uBAAe,GAAA,CAAI,CAAC,QAAQ,MAAA,EAAQ,MAAA,EAAQ,OAAO,CAAC,CAAA;AAC1D,EAAA,MAAM,KAAA,uBAAY,GAAA,EAA6C;AAC/D,EAAA,OAAO,IAAI,MAAM,KAAA,EAAO;AAAA,IACtB,GAAA,CAAI,MAAA,EAAQ,IAAA,EAAM,QAAA,EAAU;AAC1B,MAAA,IAAI,OAAO,IAAA,KAAS,QAAA,IAAY,QAAA,CAAS,GAAA,CAAI,IAAI,CAAA,EAAG;AAClD,QAAA,MAAM,MAAA,GAAS,OAAO,IAA0B,CAAA;AAChD,QAAA,IAAI,OAAO,MAAA,KAAW,UAAA,EAAY,OAAO,MAAA;AACzC,QAAA,IAAI,OAAA,GAAU,KAAA,CAAM,GAAA,CAAI,IAAI,CAAA;AAC5B,QAAA,IAAI,CAAC,OAAA,EAAS;AACZ,UAAA,OAAA,GAAU,IAAI,IAAA,KAAoB;AAChC,YAAA,MAAM,MAAA,GAAS,OAAA,CAAQ,KAAA,CAAM,MAAA,EAAQ,QAAQ,IAAI,CAAA;AACjD,YAAA,QAAA,EAAS;AACT,YAAA,OAAO,MAAA;AAAA,UACT,CAAA;AACA,UAAA,KAAA,CAAM,GAAA,CAAI,MAAM,OAAO,CAAA;AAAA,QACzB;AACA,QAAA,OAAO,OAAA;AAAA,MACT;AACA,MAAA,OAAO,OAAA,CAAQ,GAAA,CAAI,MAAA,EAAQ,IAAA,EAAM,QAAQ,CAAA;AAAA,IAC3C;AAAA,GACD,CAAA;AACH;AA+BO,SAAS,aAAgB,OAAA,EAa9B;AAEA,EAAA,MAAM,QAAA,GAAiB,aAA4B,IAAI,CAAA;AACvD,EAAA,IAAI,QAAA,CAAS,YAAY,IAAA,EAAM;AAC7B,IAAA,QAAA,CAAS,OAAA,GAAU,gBAAmB,OAAO,CAAA;AAAA,EAC/C;AACA,EAAA,MAAM,QAAQ,QAAA,CAAS,OAAA;AAKvB,EAAA,MAAM,GAAG,OAAO,CAAA,GAAU,eAAS,CAAC,CAAA;AACpC,EAAA,MAAM,IAAA,GAAa,KAAA,CAAA,WAAA,CAAY,MAAM,OAAA,CAAQ,CAAC,MAAM,CAAA,GAAI,CAAC,CAAA,EAAG,EAAE,CAAA;AAG9D,EAAA,MAAM,IAAA,GAAa,KAAA,CAAA,WAAA;AAAA,IACjB,CAAC,QAAA,KAAmB;AAClB,MAAA,MAAM,MAAA,GAAS,KAAA,CAAM,IAAA,CAAK,QAAQ,CAAA;AAClC,MAAA,IAAA,EAAK;AACL,MAAA,OAAO,MAAA;AAAA,IACT,CAAA;AAAA,IACA,CAAC,OAAO,IAAI;AAAA,GACd;AAEA,EAAA,MAAM,IAAA,GAAa,kBAAY,MAAqB;AAClD,IAAA,MAAM,MAAA,GAAS,MAAM,IAAA,EAAK;AAC1B,IAAA,IAAA,EAAK;AACL,IAAA,OAAO,MAAA;AAAA,EACT,CAAA,EAAG,CAAC,KAAA,EAAO,IAAI,CAAC,CAAA;AAEhB,EAAA,MAAM,IAAA,GAAa,kBAAY,MAAqB;AAClD,IAAA,MAAM,MAAA,GAAS,MAAM,IAAA,EAAK;AAC1B,IAAA,IAAA,EAAK;AACL,IAAA,OAAO,MAAA;AAAA,EACT,CAAA,EAAG,CAAC,KAAA,EAAO,IAAI,CAAC,CAAA;AAEhB,EAAA,MAAM,KAAA,GAAc,kBAAY,MAAM;AACpC,IAAA,KAAA,CAAM,KAAA,EAAM;AACZ,IAAA,IAAA,EAAK;AAAA,EACP,CAAA,EAAG,CAAC,KAAA,EAAO,IAAI,CAAC,CAAA;AAKhB,EAAA,MAAM,YAAA,GAAqB,KAAA,CAAA,OAAA;AAAA,IACzB,MAAM,cAAA,CAAe,KAAA,EAAO,IAAI,CAAA;AAAA,IAChC,CAAC,OAAO,IAAI;AAAA,GACd;AAIA,EAAA,MAAM,KAAA,GAAgC;AAAA,IACpC,OAAA,EAAS,MAAM,OAAA,EAAQ;AAAA,IACvB,OAAA,EAAS,MAAM,OAAA,EAAQ;AAAA,IACvB,OAAO,KAAA,CAAM,KAAA;AAAA,IACb,OAAO,KAAA,CAAM;AAAA,GACf;AAEA,EAAA,OAAO,EAAE,IAAA,EAAM,IAAA,EAAM,MAAM,KAAA,EAAO,KAAA,EAAO,cAAc,KAAA,EAAM;AAC/D","file":"chunk-INEPOSCN.js","sourcesContent":["import * as React from 'react'\nimport { createUndoStack, type UndoStack, type UndoStackOptions } from '@iris-ui-kit/core/undo'\n\n/**\n * Module-private helper: wrap a raw UndoStack so that every mutating call\n * (push/undo/redo/clear — including true no-ops and undefined-returning\n * undos) fires `onMutate`, keeping the bridge's reactive state in sync even\n * when consumers mutate the returned `stack` directly (the documented\n * \"stable reference\" contract). Written to be lifted to core verbatim so the\n * four bridges share one implementation.\n */\nfunction trackUndoStack<T>(\n stack: UndoStack<T>,\n onMutate: () => void,\n): UndoStack<T> {\n const mutating = new Set(['push', 'undo', 'redo', 'clear'])\n const cache = new Map<string, (...args: unknown[]) => unknown>()\n return new Proxy(stack, {\n get(target, prop, receiver) {\n if (typeof prop === 'string' && mutating.has(prop)) {\n const method = target[prop as keyof UndoStack<T>]\n if (typeof method !== 'function') return undefined\n let wrapped = cache.get(prop)\n if (!wrapped) {\n wrapped = (...args: unknown[]) => {\n const result = Reflect.apply(method, target, args)\n onMutate()\n return result\n }\n cache.set(prop, wrapped)\n }\n return wrapped\n }\n return Reflect.get(target, prop, receiver)\n },\n })\n}\n\n/**\n * Reactive snapshot metadata for the UndoStack: convenience booleans that\n * update on every push/undo/redo/clear for use in UI bindings (enable/disable\n * undo/redo buttons).\n */\nexport interface UndoStackReactiveState {\n canUndo: boolean\n canRedo: boolean\n depth: number\n index: number\n}\n\n/**\n * React bridge over the framework-agnostic {@link createUndoStack}.\n * Creates the stack once (via a factory memo) and returns a stable controller\n * plus a reactive `state` that tracks `canUndo`/`canRedo`/`depth`/`index` for\n * easy UI binding. Re-renders only when those metadata change.\n *\n * The returned `push`/`undo`/`redo`/`clear` are stable refs that automatically\n * update the reactive state — no manual `setState` needed.\n *\n * @example\n * const { push, undo, redo, state } = useUndoStack({ initial: formValues })\n * // After mutation:\n * push(currentValues)\n * // In UI:\n * <button disabled={!state.canUndo} onClick={undo}>Undo</button>\n * <button disabled={!state.canRedo} onClick={redo}>Redo</button>\n */\nexport function useUndoStack<T>(options?: UndoStackOptions<T>): {\n /** Push a new snapshot onto the stack. Returns the stored value. */\n push: (snapshot: T) => T\n /** Undo: returns the previous snapshot, or undefined if nothing to undo. */\n undo: () => T | undefined\n /** Redo: returns the next snapshot, or undefined if nothing to redo. */\n redo: () => T | undefined\n /** Clear all history. */\n clear: () => void\n /** The raw undo stack (stable reference, same across renders). */\n stack: UndoStack<T>\n /** Reactive metadata: canUndo, canRedo, depth, index. */\n state: UndoStackReactiveState\n} {\n // Create the stack once via factory — stable across renders.\n const stackRef = React.useRef<UndoStack<T> | null>(null)\n if (stackRef.current === null) {\n stackRef.current = createUndoStack<T>(options)\n }\n const stack = stackRef.current\n\n // Key to trigger re-render when undo/redo/push state changes.\n // Incremented on every mutation so canUndo/canRedo/depth/index are\n // always fresh in the returned `state`.\n const [, setTick] = React.useState(0)\n const bump = React.useCallback(() => setTick((n) => n + 1), [])\n\n // Stable wrappers that bump the tick after each operation.\n const push = React.useCallback(\n (snapshot: T): T => {\n const result = stack.push(snapshot)\n bump()\n return result\n },\n [stack, bump],\n )\n\n const undo = React.useCallback((): T | undefined => {\n const result = stack.undo()\n bump()\n return result\n }, [stack, bump])\n\n const redo = React.useCallback((): T | undefined => {\n const result = stack.redo()\n bump()\n return result\n }, [stack, bump])\n\n const clear = React.useCallback(() => {\n stack.clear()\n bump()\n }, [stack, bump])\n\n // Expose a tracked stack: direct mutations (api.stack.push/undo/clear)\n // keep the reactive state in sync (组(1) fix, aligned with vue 0.2.19+).\n // Stable across renders because the proxy caches its method wrappers.\n const trackedStack = React.useMemo(\n () => trackUndoStack(stack, bump),\n [stack, bump],\n )\n\n // Compute reactive state. `canUndo`/`canRedo` are methods on the interface,\n // so we call them each render to get current values.\n const state: UndoStackReactiveState = {\n canUndo: stack.canUndo(),\n canRedo: stack.canRedo(),\n depth: stack.depth,\n index: stack.index,\n }\n\n return { push, undo, redo, clear, stack: trackedStack, state }\n}\n"]}
@@ -0,0 +1,107 @@
1
+ 'use client'
2
+ import * as React from 'react';
3
+ import { createSortable } from '@iris-ui-kit/core';
4
+ import { jsx } from 'react/jsx-runtime';
5
+
6
+ // src/behaviors/Sortable.tsx
7
+ var SORTABLE_ITEM_ATTR = "data-iris-sortable-item";
8
+ function IrisSortable({
9
+ items,
10
+ onReorder,
11
+ getKey = (_item, index) => String(index),
12
+ disabled = false,
13
+ orientation = "vertical",
14
+ style,
15
+ className,
16
+ children,
17
+ ...rest
18
+ }) {
19
+ const containerRef = React.useRef(null);
20
+ const ctrlRef = React.useRef(null);
21
+ if (ctrlRef.current === null) ctrlRef.current = createSortable();
22
+ const ctrl = ctrlRef.current;
23
+ const [state, setState] = React.useState({ activeId: null, overId: null });
24
+ React.useEffect(() => ctrl.subscribe(setState), [ctrl]);
25
+ const dragging = state.activeId;
26
+ const indexByKey = (key) => Array.from(containerRef.current?.querySelectorAll(`[${SORTABLE_ITEM_ATTR}]`) ?? []).findIndex(
27
+ (el) => el.getAttribute(SORTABLE_ITEM_ATTR) === key
28
+ );
29
+ const onPointerDown = (e) => {
30
+ if (disabled) return;
31
+ const target = e.target.closest(`[${SORTABLE_ITEM_ATTR}]`);
32
+ if (!target) return;
33
+ const key = target.getAttribute(SORTABLE_ITEM_ATTR) ?? "";
34
+ if (!key) return;
35
+ ctrl.press(key, e.clientX, e.clientY);
36
+ };
37
+ const onPointerMove = (e) => {
38
+ if (!ctrl.getState().activeId) {
39
+ if (!ctrl.tryStart(e.clientX, e.clientY)) return;
40
+ }
41
+ const els = containerRef.current?.querySelectorAll(`[${SORTABLE_ITEM_ATTR}]`);
42
+ const rects = Array.from(els ?? []).map((el) => ({
43
+ id: el.getAttribute(SORTABLE_ITEM_ATTR) ?? "",
44
+ ...el.getBoundingClientRect()
45
+ }));
46
+ ctrl.moveOver({ x: e.clientX, y: e.clientY }, rects);
47
+ };
48
+ const onPointerUp = () => {
49
+ const result = ctrl.end();
50
+ if (result.activeId && result.overId && result.activeId !== result.overId) {
51
+ const from = indexByKey(result.activeId);
52
+ const to = indexByKey(result.overId);
53
+ if (from >= 0 && to >= 0 && from !== to) {
54
+ const next = [...items];
55
+ const [moved] = next.splice(from, 1);
56
+ next.splice(to, 0, moved);
57
+ onReorder(next);
58
+ }
59
+ }
60
+ };
61
+ return /* @__PURE__ */ jsx(
62
+ "div",
63
+ {
64
+ ...rest,
65
+ ref: containerRef,
66
+ "data-iris-sortable": "",
67
+ "data-state": dragging ? "dragging" : "idle",
68
+ className,
69
+ style: {
70
+ display: "flex",
71
+ flexDirection: orientation === "horizontal" ? "row" : "column",
72
+ gap: "var(--iris-gap-sm, 4px)",
73
+ opacity: disabled ? 0.6 : 1,
74
+ userSelect: dragging ? "none" : void 0,
75
+ ...style
76
+ },
77
+ onPointerDown,
78
+ onPointerMove,
79
+ onPointerUp,
80
+ onPointerLeave: onPointerUp,
81
+ children: React.Children.map(children, (child, index) => {
82
+ if (!React.isValidElement(child)) return child;
83
+ const key = getKey(items[index], index);
84
+ const isDragging = key === dragging;
85
+ return /* @__PURE__ */ jsx(
86
+ "div",
87
+ {
88
+ "data-iris-sortable-item": key,
89
+ "data-iris-sortable-dragging": isDragging ? "" : void 0,
90
+ style: {
91
+ transition: isDragging ? "none" : "transform 150ms ease",
92
+ opacity: isDragging ? 0.4 : 1,
93
+ position: "relative",
94
+ zIndex: isDragging ? 100 : void 0
95
+ },
96
+ children: child
97
+ },
98
+ key
99
+ );
100
+ })
101
+ }
102
+ );
103
+ }
104
+
105
+ export { IrisSortable, SORTABLE_ITEM_ATTR };
106
+ //# sourceMappingURL=chunk-K2F7SC2T.js.map
107
+ //# sourceMappingURL=chunk-K2F7SC2T.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/behaviors/Sortable.tsx"],"names":[],"mappings":"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","file":"chunk-K2F7SC2T.js","sourcesContent":["/**\n * Behavior wrapper: makes a list of items sortable via drag-and-drop.\n * Uses `createSortable` from `@iris-ui-kit/core` for the drag-and-drop controller.\n *\n * @example\n * const [items, setItems] = React.useState(['A', 'B', 'C'])\n * <IrisSortable items={items} onReorder={setItems}>\n * {items.map((label) => <div key={label}>{label}</div>)}\n * </IrisSortable>\n */\nimport * as React from 'react'\nimport { createSortable, type SortableState } from '@iris-ui-kit/core'\n\nexport interface IrisSortableProps<T = string> {\n items: readonly T[]\n onReorder: (next: T[]) => void\n getKey?: (item: T, index: number) => string\n children?: React.ReactNode\n className?: string\n style?: React.CSSProperties\n disabled?: boolean\n orientation?: 'vertical' | 'horizontal'\n}\n\nexport const SORTABLE_ITEM_ATTR = 'data-iris-sortable-item'\n\nexport function IrisSortable<T>({\n items,\n onReorder,\n getKey = (_item, index) => String(index),\n disabled = false,\n orientation = 'vertical',\n style,\n className,\n children,\n ...rest\n}: IrisSortableProps<T> &\n Omit<React.HTMLAttributes<HTMLDivElement>, 'children'>): React.ReactElement {\n const containerRef = React.useRef<HTMLDivElement | null>(null)\n const ctrlRef = React.useRef<ReturnType<typeof createSortable> | null>(null)\n if (ctrlRef.current === null) ctrlRef.current = createSortable()\n const ctrl = ctrlRef.current\n\n const [state, setState] = React.useState<SortableState>({ activeId: null, overId: null })\n React.useEffect(() => ctrl.subscribe(setState), [ctrl])\n\n const dragging = state.activeId\n\n const indexByKey = (key: string): number =>\n Array.from(containerRef.current?.querySelectorAll(`[${SORTABLE_ITEM_ATTR}]`) ?? []).findIndex(\n (el) => el.getAttribute(SORTABLE_ITEM_ATTR) === key,\n )\n\n const onPointerDown = (e: React.PointerEvent<HTMLElement>): void => {\n if (disabled) return\n const target = (e.target as HTMLElement).closest(`[${SORTABLE_ITEM_ATTR}]`)\n if (!target) return\n const key = target.getAttribute(SORTABLE_ITEM_ATTR) ?? ''\n if (!key) return\n ctrl.press(key, e.clientX, e.clientY)\n }\n\n const onPointerMove = (e: React.PointerEvent<HTMLElement>): void => {\n if (!ctrl.getState().activeId) {\n // Pending press — promote if past threshold\n if (!ctrl.tryStart(e.clientX, e.clientY)) return\n }\n const els = containerRef.current?.querySelectorAll<HTMLElement>(`[${SORTABLE_ITEM_ATTR}]`)\n const rects = Array.from(els ?? []).map((el) => ({\n id: el.getAttribute(SORTABLE_ITEM_ATTR) ?? '',\n ...el.getBoundingClientRect(),\n }))\n ctrl.moveOver({ x: e.clientX, y: e.clientY }, rects)\n }\n\n const onPointerUp = (): void => {\n const result = ctrl.end()\n if (result.activeId && result.overId && result.activeId !== result.overId) {\n const from = indexByKey(result.activeId)\n const to = indexByKey(result.overId)\n if (from >= 0 && to >= 0 && from !== to) {\n const next = [...items]\n const [moved] = next.splice(from, 1)\n next.splice(to, 0, moved!)\n onReorder(next)\n }\n }\n }\n\n return (\n <div\n {...rest}\n ref={containerRef}\n data-iris-sortable=\"\"\n data-state={dragging ? 'dragging' : 'idle'}\n className={className}\n style={{\n display: 'flex',\n flexDirection: orientation === 'horizontal' ? 'row' : 'column',\n gap: 'var(--iris-gap-sm, 4px)',\n opacity: disabled ? 0.6 : 1,\n userSelect: dragging ? 'none' : undefined,\n ...style,\n }}\n onPointerDown={onPointerDown}\n onPointerMove={onPointerMove}\n onPointerUp={onPointerUp}\n onPointerLeave={onPointerUp}\n >\n {React.Children.map(children, (child, index) => {\n if (!React.isValidElement(child)) return child\n const key = getKey(items[index]!, index)\n const isDragging = key === dragging\n return (\n <div\n key={key}\n data-iris-sortable-item={key}\n data-iris-sortable-dragging={isDragging ? '' : undefined}\n style={{\n transition: isDragging ? 'none' : 'transform 150ms ease',\n opacity: isDragging ? 0.4 : 1,\n position: 'relative',\n zIndex: isDragging ? 100 : undefined,\n }}\n >\n {child}\n </div>\n )\n })}\n </div>\n )\n}\n"]}
@@ -1,13 +1,14 @@
1
1
  'use client'
2
2
  import { useThemeOptional } from './chunk-XCK4RIXB.js';
3
- import { IrisSidebarLayout, IrisHeaderLayout } from './chunk-J5FB2UB2.js';
3
+ import { IrisHeaderLayout, IrisSidebarLayout } from './chunk-J5FB2UB2.js';
4
+ import { IrisSortable } from './chunk-K2F7SC2T.js';
4
5
  import { useI18n } from './chunk-OMOTUHYI.js';
5
6
  import { useFloating, useDismiss } from './chunk-6BMFMCMQ.js';
6
7
  import { useStore } from './chunk-RZNYIFV2.js';
7
8
  import * as React6 from 'react';
8
9
  import { useMemo } from 'react';
9
10
  import { composeEventHandlers, matchTypeahead, branchTrail, createExpansion, visibleNav, findNavPath, createFloatingMachine, createAdminShell, isClosable, isBranch, firstLeaf, findNavNode } from '@iris-ui-kit/core';
10
- export { createTabsNav, filterNavByAccess, findNavNode, findNavPath, firstLeaf, flattenNav, isBranch, isClosable, nodeAllowsRoles, visibleNav } from '@iris-ui-kit/core';
11
+ export { createAdminPreferences, createTabsNav, filterNavByAccess, findNavNode, findNavPath, firstLeaf, flattenNav, isBranch, isClosable, localStorageAdminPreferencesStorage, nodeAllowsRoles, visibleNav } from '@iris-ui-kit/core';
11
12
  import { resolveThemedIcon, defaultIconRegistry } from '@iris-ui-kit/icons';
12
13
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
13
14
  import { createPortal } from 'react-dom';
@@ -59,11 +60,13 @@ function IrisNavMenu({
59
60
  expandedKeys,
60
61
  defaultExpandedKeys,
61
62
  collapsed = false,
63
+ orientation = "vertical",
62
64
  ariaLabel,
63
65
  onSelect,
64
66
  onExpandedKeysChange
65
67
  }) {
66
68
  const { t } = useI18n();
69
+ const horizontal = orientation === "horizontal";
67
70
  const trailOf = React6.useCallback(
68
71
  (key) => key ? branchTrail(items, key) : [],
69
72
  [items]
@@ -99,11 +102,11 @@ function IrisNavMenu({
99
102
  display: "flex",
100
103
  alignItems: "center",
101
104
  gap: 10,
102
- width: "100%",
105
+ width: horizontal && opts.depth === 0 ? "auto" : "100%",
103
106
  boxSizing: "border-box",
104
107
  border: "none",
105
108
  borderRadius: "var(--iris-radius-md, 6px)",
106
- background: opts.active ? "var(--iris-primary)" : opts.hovered && !opts.disabled ? "var(--iris-surface)" : "transparent",
109
+ background: opts.active ? "var(--iris-primary)" : opts.hovered && !opts.disabled ? "var(--iris-surface-hover, var(--iris-surface))" : "transparent",
107
110
  color: opts.active ? "var(--iris-primary-foreground, #fff)" : opts.trail ? "var(--iris-primary)" : "var(--iris-foreground)",
108
111
  font: "inherit",
109
112
  fontSize: 14,
@@ -111,7 +114,8 @@ function IrisNavMenu({
111
114
  textAlign: "start",
112
115
  cursor: opts.disabled ? "not-allowed" : "pointer",
113
116
  opacity: opts.disabled ? 0.5 : 1,
114
- padding: collapsed ? 10 : `8px 10px 8px ${12 + opts.depth * 16}px`,
117
+ padding: collapsed ? 10 : "8px 10px",
118
+ paddingInlineStart: collapsed ? 10 : 12 + opts.depth * 16,
115
119
  justifyContent: collapsed ? "center" : "flex-start"
116
120
  });
117
121
  const iconNode = (node, size = 18) => node.icon ? /* @__PURE__ */ jsx(IrisIcon, { name: node.icon, size }) : null;
@@ -218,12 +222,48 @@ function IrisNavMenu({
218
222
  }
219
223
  );
220
224
  if (!branch || !open) {
221
- return /* @__PURE__ */ jsx("div", { "data-iris-nav-group": branch ? "" : void 0, children: row }, node.key);
225
+ return /* @__PURE__ */ jsx(
226
+ "div",
227
+ {
228
+ "data-iris-nav-group": branch ? "" : void 0,
229
+ style: { position: horizontal && depth === 0 ? "relative" : void 0 },
230
+ children: row
231
+ },
232
+ node.key
233
+ );
222
234
  }
223
- return /* @__PURE__ */ jsxs("div", { "data-iris-nav-group": "", "data-open": "true", children: [
224
- row,
225
- /* @__PURE__ */ jsx("div", { "data-iris-nav-children": "", role: "group", children: (node.children ?? []).map((child) => renderItem(child, depth + 1)) })
226
- ] }, node.key);
235
+ return /* @__PURE__ */ jsxs(
236
+ "div",
237
+ {
238
+ "data-iris-nav-group": "",
239
+ "data-open": "true",
240
+ style: { position: horizontal && depth === 0 ? "relative" : void 0 },
241
+ children: [
242
+ row,
243
+ /* @__PURE__ */ jsx(
244
+ "div",
245
+ {
246
+ "data-iris-nav-children": "",
247
+ role: "group",
248
+ style: horizontal && depth === 0 ? {
249
+ position: "absolute",
250
+ insetBlockStart: "calc(100% + 4px)",
251
+ insetInlineStart: 0,
252
+ zIndex: 60,
253
+ minWidth: 220,
254
+ padding: 6,
255
+ border: "1px solid var(--iris-border)",
256
+ borderRadius: "var(--iris-radius-md, 6px)",
257
+ background: "var(--iris-surface)",
258
+ boxShadow: "var(--iris-shadow-md)"
259
+ } : void 0,
260
+ children: (node.children ?? []).map((child) => renderItem(child, depth + 1))
261
+ }
262
+ )
263
+ ]
264
+ },
265
+ node.key
266
+ );
227
267
  };
228
268
  const onKeyDown = (e) => {
229
269
  const root = e.currentTarget;
@@ -271,9 +311,15 @@ function IrisNavMenu({
271
311
  {
272
312
  "data-iris-nav-menu": "",
273
313
  "data-collapsed": collapsed ? "true" : void 0,
314
+ "data-orientation": orientation,
274
315
  "aria-label": ariaLabel ?? t("admin.nav"),
275
316
  onKeyDown,
276
- style: { display: "flex", flexDirection: "column", gap: 2 },
317
+ style: {
318
+ display: "flex",
319
+ flexDirection: horizontal ? "row" : "column",
320
+ alignItems: horizontal ? "center" : void 0,
321
+ gap: 2
322
+ },
277
323
  children: collapsed ? tree.map((node) => renderCollapsed(node)) : tree.map((node) => renderItem(node, 0))
278
324
  }
279
325
  );
@@ -404,6 +450,7 @@ function useTabsNav(nav) {
404
450
  closeRight: nav.closeRight,
405
451
  refresh: nav.refresh,
406
452
  setPinned: nav.setPinned,
453
+ move: nav.move,
407
454
  cacheKey: nav.cacheKey
408
455
  };
409
456
  }
@@ -775,7 +822,9 @@ function IrisAdminTabs({
775
822
  nav,
776
823
  onChange,
777
824
  onClose,
778
- onRefresh
825
+ onRefresh,
826
+ reorderable = true,
827
+ onReorder
779
828
  }) {
780
829
  const t = useTabsNav(nav);
781
830
  const { t: translate } = useI18n();
@@ -801,6 +850,10 @@ function IrisAdminTabs({
801
850
  nav.refresh(key);
802
851
  onRefresh?.(key);
803
852
  };
853
+ const reorder = (tabs) => {
854
+ tabs.forEach((tab, index) => nav.move(tab.key, index));
855
+ onReorder?.(tabs);
856
+ };
804
857
  const onKeyDown = (e) => {
805
858
  const tabs = t.tabs;
806
859
  const idx = tabs.findIndex((x) => x.key === t.activeKey);
@@ -932,8 +985,19 @@ function IrisAdminTabs({
932
985
  role: "tablist",
933
986
  "aria-label": translate("admin.openPages"),
934
987
  onKeyDown,
935
- style: { display: "flex", alignItems: "center", gap: 6, overflowX: "auto", flex: 1 },
936
- children: t.tabs.map((tab) => chip(tab))
988
+ style: { overflowX: "auto", flex: 1 },
989
+ children: /* @__PURE__ */ jsx(
990
+ IrisSortable,
991
+ {
992
+ items: t.tabs,
993
+ getKey: (tab) => tab.key,
994
+ onReorder: reorder,
995
+ disabled: !reorderable,
996
+ orientation: "horizontal",
997
+ style: { alignItems: "center", gap: 6, width: "max-content" },
998
+ children: t.tabs.map((tab) => chip(tab))
999
+ }
1000
+ )
937
1001
  }
938
1002
  ),
939
1003
  /* @__PURE__ */ jsxs(IrisDropdown, { children: [
@@ -960,6 +1024,8 @@ function IrisAdminTabs({
960
1024
  /* @__PURE__ */ jsx(IrisDropdownItem, { onSelect: () => refresh(activeKey), children: translate("admin.refresh") }),
961
1025
  /* @__PURE__ */ jsx(IrisDropdownItem, { onSelect: () => close(activeKey), children: translate("admin.close") }),
962
1026
  /* @__PURE__ */ jsx(IrisDropdownSeparator, {}),
1027
+ /* @__PURE__ */ jsx(IrisDropdownItem, { onSelect: () => nav.closeLeft(activeKey), children: translate("admin.closeLeft") }),
1028
+ /* @__PURE__ */ jsx(IrisDropdownItem, { onSelect: () => nav.closeRight(activeKey), children: translate("admin.closeRight") }),
963
1029
  /* @__PURE__ */ jsx(IrisDropdownItem, { onSelect: () => nav.closeOthers(activeKey), children: translate("admin.closeOthers") }),
964
1030
  /* @__PURE__ */ jsx(IrisDropdownItem, { onSelect: () => nav.closeAll(), children: translate("admin.closeAll") })
965
1031
  ] }) : /* @__PURE__ */ jsx(IrisDropdownItem, { disabled: true, children: "No active tab" }) })
@@ -1025,7 +1091,13 @@ function IrisAdminLayout({
1025
1091
  mode = "sidebar",
1026
1092
  appTitle = "Iris Admin",
1027
1093
  tabs,
1094
+ showTabs = true,
1028
1095
  showBreadcrumb = true,
1096
+ stickyHeader = true,
1097
+ stickyTabs = true,
1098
+ menuAlign = "start",
1099
+ contentWidth = "fluid",
1100
+ contentHeight = "viewport",
1029
1101
  sidebarWidth = 240,
1030
1102
  collapsedWidth = 64,
1031
1103
  logo,
@@ -1096,6 +1168,37 @@ function IrisAdminLayout({
1096
1168
  }
1097
1169
  );
1098
1170
  const renderLogo = (state) => typeof logo === "function" ? logo(state) : logo ?? defaultLogo(state);
1171
+ const tabsBar = tabs && showTabs ? /* @__PURE__ */ jsx(
1172
+ "div",
1173
+ {
1174
+ "data-iris-admin-tabs-region": "",
1175
+ "data-sticky": stickyTabs ? "true" : void 0,
1176
+ style: {
1177
+ position: stickyTabs && !stickyHeader ? "sticky" : "relative",
1178
+ top: 0,
1179
+ zIndex: 49
1180
+ },
1181
+ children: /* @__PURE__ */ jsx(IrisAdminTabs, { nav: tabs })
1182
+ }
1183
+ ) : null;
1184
+ const contentRegion = /* @__PURE__ */ jsxs(
1185
+ "div",
1186
+ {
1187
+ "data-iris-admin-content": "",
1188
+ "data-width": contentWidth,
1189
+ style: {
1190
+ boxSizing: "border-box",
1191
+ width: "100%",
1192
+ maxWidth: contentWidth === "centered" ? "72rem" : void 0,
1193
+ marginInline: contentWidth === "centered" ? "auto" : void 0,
1194
+ padding: 16
1195
+ },
1196
+ children: [
1197
+ mode === "horizontal" && showBreadcrumb ? /* @__PURE__ */ jsx(IrisAdminBreadcrumb, { trail, onSelect: handleSelect }) : null,
1198
+ renderContent()
1199
+ ]
1200
+ }
1201
+ );
1099
1202
  const collapseToggle = /* @__PURE__ */ jsx(
1100
1203
  "button",
1101
1204
  {
@@ -1141,6 +1244,75 @@ function IrisAdminLayout({
1141
1244
  ]
1142
1245
  }
1143
1246
  );
1247
+ if (mode === "horizontal") {
1248
+ const justifyContent = menuAlign === "center" ? "center" : menuAlign === "end" ? "flex-end" : "flex-start";
1249
+ return /* @__PURE__ */ jsxs(
1250
+ "div",
1251
+ {
1252
+ "data-iris-admin-layout": "",
1253
+ "data-mode": "horizontal",
1254
+ style: {
1255
+ height: contentHeight === "viewport" ? "100vh" : "auto",
1256
+ minHeight: "100vh"
1257
+ },
1258
+ children: [
1259
+ tabs ? /* @__PURE__ */ jsx(TabsSync, { nav: tabs, activeKey: currentActive, onActivate: syncFromTab }) : null,
1260
+ /* @__PURE__ */ jsx(
1261
+ IrisHeaderLayout,
1262
+ {
1263
+ sticky: stickyHeader,
1264
+ footer,
1265
+ header: /* @__PURE__ */ jsxs("div", { "data-iris-admin-header": "", children: [
1266
+ /* @__PURE__ */ jsxs(
1267
+ "div",
1268
+ {
1269
+ "data-iris-admin-headerbar": "",
1270
+ style: {
1271
+ display: "flex",
1272
+ alignItems: "center",
1273
+ gap: 12,
1274
+ minHeight: 52,
1275
+ padding: "0 16px",
1276
+ background: "var(--iris-background)"
1277
+ },
1278
+ children: [
1279
+ renderLogo({ collapsed: false }),
1280
+ /* @__PURE__ */ jsx(
1281
+ "div",
1282
+ {
1283
+ "data-iris-admin-topnav": "",
1284
+ style: { display: "flex", flex: 1, minWidth: 0, justifyContent },
1285
+ children: /* @__PURE__ */ jsx(
1286
+ IrisNavMenu,
1287
+ {
1288
+ items: menus,
1289
+ activeKey: currentActive,
1290
+ orientation: "horizontal",
1291
+ onSelect: handleSelect
1292
+ }
1293
+ )
1294
+ }
1295
+ ),
1296
+ toolbar ? /* @__PURE__ */ jsx(
1297
+ "div",
1298
+ {
1299
+ "data-iris-admin-toolbar": "",
1300
+ style: { display: "flex", alignItems: "center", gap: 8 },
1301
+ children: toolbar
1302
+ }
1303
+ ) : null
1304
+ ]
1305
+ }
1306
+ ),
1307
+ tabsBar
1308
+ ] }),
1309
+ children: contentRegion
1310
+ }
1311
+ )
1312
+ ]
1313
+ }
1314
+ );
1315
+ }
1144
1316
  return /* @__PURE__ */ jsxs(
1145
1317
  IrisSidebarLayout,
1146
1318
  {
@@ -1181,20 +1353,34 @@ function IrisAdminLayout({
1181
1353
  /* @__PURE__ */ jsx(
1182
1354
  IrisHeaderLayout,
1183
1355
  {
1184
- sticky: true,
1356
+ sticky: stickyHeader,
1185
1357
  header: /* @__PURE__ */ jsxs("div", { "data-iris-admin-header": "", children: [
1186
1358
  headerBar,
1187
- tabs ? /* @__PURE__ */ jsx(IrisAdminTabs, { nav: tabs }) : null
1359
+ tabsBar
1188
1360
  ] }),
1189
1361
  footer,
1190
- children: /* @__PURE__ */ jsx("div", { "data-iris-admin-content": "", style: { padding: 16 }, children: renderContent() })
1362
+ children: contentRegion
1191
1363
  }
1192
1364
  )
1193
1365
  ]
1194
1366
  }
1195
1367
  );
1196
1368
  }
1369
+ function useAdminPreferences(preferences, hydrate = true) {
1370
+ const state = useStore(preferences.store);
1371
+ React6.useEffect(() => {
1372
+ if (hydrate) void preferences.hydrate();
1373
+ }, [hydrate, preferences]);
1374
+ return {
1375
+ state,
1376
+ set: preferences.set,
1377
+ patch: preferences.patch,
1378
+ reset: preferences.reset,
1379
+ hydrate: preferences.hydrate,
1380
+ flush: preferences.flush
1381
+ };
1382
+ }
1197
1383
 
1198
- export { DropdownContext, IrisAdminBreadcrumb, IrisAdminLayout, IrisAdminTabs, IrisBreadcrumb, IrisBreadcrumbItem, IrisDropdown, IrisDropdownItem, IrisDropdownMenu, IrisDropdownSeparator, IrisDropdownTrigger, IrisIcon, IrisNavMenu, IrisSlot, useAdminShell, useDropdownContext, useMachine, useTabsNav };
1199
- //# sourceMappingURL=chunk-VWCJJRSC.js.map
1200
- //# sourceMappingURL=chunk-VWCJJRSC.js.map
1384
+ export { DropdownContext, IrisAdminBreadcrumb, IrisAdminLayout, IrisAdminTabs, IrisBreadcrumb, IrisBreadcrumbItem, IrisDropdown, IrisDropdownItem, IrisDropdownMenu, IrisDropdownSeparator, IrisDropdownTrigger, IrisIcon, IrisNavMenu, IrisSlot, useAdminPreferences, useAdminShell, useDropdownContext, useMachine, useTabsNav };
1385
+ //# sourceMappingURL=chunk-LXMTXHAS.js.map
1386
+ //# sourceMappingURL=chunk-LXMTXHAS.js.map