@asteby/metacore-runtime-react 39.2.0 → 39.2.2

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,17 @@
1
1
  # @asteby/metacore-runtime-react
2
2
 
3
+ ## 39.2.2
4
+
5
+ ### Patch Changes
6
+
7
+ - 4556375: Add data-aby-target model.{Model}.create on ModelActionToolbar create button for Aby UI tours.
8
+
9
+ ## 39.2.1
10
+
11
+ ### Patch Changes
12
+
13
+ - dcc37b2: Discord-style SidebarLayoutManager (drag, rename, resolveLabel).
14
+
3
15
  ## 39.2.0
4
16
 
5
17
  ### Minor Changes
@@ -1 +1 @@
1
- {"version":3,"file":"model-action-toolbar.d.ts","sourceRoot":"","sources":["../src/model-action-toolbar.tsx"],"names":[],"mappings":"AA2BA,OAAO,KAAK,EAAE,gBAAgB,EAAiC,MAAM,SAAS,CAAA;AAE9E,MAAM,MAAM,eAAe,GAAG,KAAK,GAAG,OAAO,GAAG,QAAQ,CAAA;AAExD,MAAM,WAAW,uBAAuB;IACpC,oEAAoE;IACpE,KAAK,EAAE,MAAM,CAAA;IACb,8EAA8E;IAC9E,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;;OAIG;IACH,OAAO,CAAC,EAAE,gBAAgB,EAAE,CAAA;IAC5B,qEAAqE;IACrE,UAAU,CAAC,EAAE,eAAe,EAAE,CAAA;IAC9B,qDAAqD;IACrD,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAA;IACrB,iDAAiD;IACjD,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB;;;;OAIG;IACH,cAAc,CAAC,EAAE,CAAC,MAAM,EAAE,gBAAgB,KAAK,IAAI,CAAA;CACtD;AAID;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAgBzD;AAiBD;;;GAGG;AACH,wBAAgB,eAAe,CAC3B,KAAK,EAAE,MAAM,EACb,UAAU,GAAE,eAAe,EAAuB,EAClD,QAAQ,CAAC,EAAE,gBAAgB,EAAE,GAC9B,gBAAgB,EAAE,CA2BpB;AAED,wBAAgB,kBAAkB,CAAC,EAC/B,KAAK,EACL,QAAQ,EACR,OAAO,EACP,UAA+B,EAC/B,QAAQ,EACR,SAAS,EACT,cAAc,GACjB,EAAE,uBAAuB,sCAgEzB"}
1
+ {"version":3,"file":"model-action-toolbar.d.ts","sourceRoot":"","sources":["../src/model-action-toolbar.tsx"],"names":[],"mappings":"AA2BA,OAAO,KAAK,EAAE,gBAAgB,EAAiC,MAAM,SAAS,CAAA;AAE9E,MAAM,MAAM,eAAe,GAAG,KAAK,GAAG,OAAO,GAAG,QAAQ,CAAA;AAExD,MAAM,WAAW,uBAAuB;IACpC,oEAAoE;IACpE,KAAK,EAAE,MAAM,CAAA;IACb,8EAA8E;IAC9E,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;;OAIG;IACH,OAAO,CAAC,EAAE,gBAAgB,EAAE,CAAA;IAC5B,qEAAqE;IACrE,UAAU,CAAC,EAAE,eAAe,EAAE,CAAA;IAC9B,qDAAqD;IACrD,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAA;IACrB,iDAAiD;IACjD,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB;;;;OAIG;IACH,cAAc,CAAC,EAAE,CAAC,MAAM,EAAE,gBAAgB,KAAK,IAAI,CAAA;CACtD;AAID;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAgBzD;AAiBD;;;GAGG;AACH,wBAAgB,eAAe,CAC3B,KAAK,EAAE,MAAM,EACb,UAAU,GAAE,eAAe,EAAuB,EAClD,QAAQ,CAAC,EAAE,gBAAgB,EAAE,GAC9B,gBAAgB,EAAE,CA2BpB;AAED,wBAAgB,kBAAkB,CAAC,EAC/B,KAAK,EACL,QAAQ,EACR,OAAO,EACP,UAA+B,EAC/B,QAAQ,EACR,SAAS,EACT,cAAc,GACjB,EAAE,uBAAuB,sCAqEzB"}
@@ -109,7 +109,9 @@ export function ModelActionToolbar({ model, endpoint, actions, placements = DEFA
109
109
  return null;
110
110
  return (_jsxs(_Fragment, { children: [_jsx("div", { className: className ?? 'flex items-center gap-2', children: surfaced.map((a) => {
111
111
  const isCreate = (a.placement ?? 'row') === 'create';
112
- return (_jsxs(Button, { variant: isCreate ? 'default' : 'outline', onClick: () => {
112
+ return (_jsxs(Button, { variant: isCreate ? 'default' : 'outline', "data-aby-target": isCreate
113
+ ? `model.${model}.create`
114
+ : `model.${model}.action.${a.key}`, onClick: () => {
113
115
  onActionIntent?.(a);
114
116
  setActive(toActionMetadata(a));
115
117
  }, onMouseEnter: () => onActionIntent?.(a), onFocus: () => onActionIntent?.(a), style: a.color && !isCreate ? { borderColor: a.color, color: a.color } : undefined, children: [_jsx(DynamicIcon, { name: a.icon || (isCreate ? 'Plus' : 'Zap'), className: "mr-2 h-4 w-4" }), t(a.label, { defaultValue: humanizeActionLabel(a.label) })] }, a.key));
@@ -1,7 +1,6 @@
1
1
  /**
2
- * SidebarLayoutManager — admin editor for the org-wide sidebar overlay.
3
- * Transport-agnostic (loaders/mutators via props), same pattern as
4
- * PermissionsManager. Hosts wire fetchers to /api/org/sidebar-layout.
2
+ * SidebarLayoutManager — Discord-style org sidebar editor.
3
+ * The canvas IS the sidebar: sections, folders, leaves — drag, rename, nest.
5
4
  */
6
5
  import * as React from 'react';
7
6
  import { type CatalogLeaf, type SidebarLayoutDoc, type SidebarLayoutNode } from './apply-sidebar-layout';
@@ -9,12 +8,7 @@ export interface SidebarLayoutManagerProps {
9
8
  loadLayout: () => Promise<SidebarLayoutDoc>;
10
9
  saveLayout: (doc: SidebarLayoutDoc) => Promise<void>;
11
10
  resetLayout: () => Promise<void>;
12
- /** Live catalog of every leaf the shell can show (core + addons). */
13
11
  catalog: CatalogLeaf[];
14
- /**
15
- * Optional: current live nav groups — used by "Partir del menú actual" to
16
- * seed a tree when the org has no layout yet.
17
- */
18
12
  currentGroups?: Array<{
19
13
  title: string;
20
14
  items: Array<{
@@ -28,9 +22,11 @@ export interface SidebarLayoutManagerProps {
28
22
  }>;
29
23
  }>;
30
24
  }>;
25
+ /** Resolve i18n keys / raw titles to human labels. */
26
+ resolveLabel?: (title: string) => string;
31
27
  title?: string;
32
28
  description?: string;
33
29
  }
34
- export declare function SidebarLayoutManager({ loadLayout, saveLayout, resetLayout, catalog, currentGroups, title, description, }: SidebarLayoutManagerProps): React.JSX.Element;
30
+ export declare function SidebarLayoutManager({ loadLayout, saveLayout, resetLayout, catalog, currentGroups, resolveLabel, title, description, }: SidebarLayoutManagerProps): React.JSX.Element;
35
31
  export type { CatalogLeaf, SidebarLayoutDoc, SidebarLayoutNode };
36
32
  //# sourceMappingURL=sidebar-layout-manager.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"sidebar-layout-manager.d.ts","sourceRoot":"","sources":["../src/sidebar-layout-manager.tsx"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AA2B9B,OAAO,EAML,KAAK,WAAW,EAChB,KAAK,gBAAgB,EACrB,KAAK,iBAAiB,EACvB,MAAM,wBAAwB,CAAA;AAE/B,MAAM,WAAW,yBAAyB;IACxC,UAAU,EAAE,MAAM,OAAO,CAAC,gBAAgB,CAAC,CAAA;IAC3C,UAAU,EAAE,CAAC,GAAG,EAAE,gBAAgB,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;IACpD,WAAW,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;IAChC,qEAAqE;IACrE,OAAO,EAAE,WAAW,EAAE,CAAA;IACtB;;;OAGG;IACH,aAAa,CAAC,EAAE,KAAK,CAAC;QACpB,KAAK,EAAE,MAAM,CAAA;QACb,KAAK,EAAE,KAAK,CAAC;YACX,KAAK,EAAE,MAAM,CAAA;YACb,GAAG,EAAE,MAAM,CAAA;YACX,IAAI,CAAC,EAAE,MAAM,CAAA;YACb,KAAK,CAAC,EAAE,KAAK,CAAC;gBAAE,KAAK,EAAE,MAAM,CAAC;gBAAC,GAAG,EAAE,MAAM,CAAC;gBAAC,IAAI,CAAC,EAAE,MAAM,CAAA;aAAE,CAAC,CAAA;SAC7D,CAAC,CAAA;KACH,CAAC,CAAA;IACF,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB;AAmED,wBAAgB,oBAAoB,CAAC,EACnC,UAAU,EACV,UAAU,EACV,WAAW,EACX,OAAO,EACP,aAAa,EACb,KAAsB,EACtB,WAAsI,GACvI,EAAE,yBAAyB,qBA4X3B;AAED,YAAY,EAAE,WAAW,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,CAAA"}
1
+ {"version":3,"file":"sidebar-layout-manager.d.ts","sourceRoot":"","sources":["../src/sidebar-layout-manager.tsx"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAwC9B,OAAO,EAML,KAAK,WAAW,EAChB,KAAK,gBAAgB,EACrB,KAAK,iBAAiB,EACvB,MAAM,wBAAwB,CAAA;AAE/B,MAAM,WAAW,yBAAyB;IACxC,UAAU,EAAE,MAAM,OAAO,CAAC,gBAAgB,CAAC,CAAA;IAC3C,UAAU,EAAE,CAAC,GAAG,EAAE,gBAAgB,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;IACpD,WAAW,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;IAChC,OAAO,EAAE,WAAW,EAAE,CAAA;IACtB,aAAa,CAAC,EAAE,KAAK,CAAC;QACpB,KAAK,EAAE,MAAM,CAAA;QACb,KAAK,EAAE,KAAK,CAAC;YACX,KAAK,EAAE,MAAM,CAAA;YACb,GAAG,EAAE,MAAM,CAAA;YACX,IAAI,CAAC,EAAE,MAAM,CAAA;YACb,KAAK,CAAC,EAAE,KAAK,CAAC;gBAAE,KAAK,EAAE,MAAM,CAAC;gBAAC,GAAG,EAAE,MAAM,CAAC;gBAAC,IAAI,CAAC,EAAE,MAAM,CAAA;aAAE,CAAC,CAAA;SAC7D,CAAC,CAAA;KACH,CAAC,CAAA;IACF,sDAAsD;IACtD,YAAY,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,CAAA;IACxC,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB;AA4TD,wBAAgB,oBAAoB,CAAC,EACnC,UAAU,EACV,UAAU,EACV,WAAW,EACX,OAAO,EACP,aAAa,EACb,YAAuB,EACvB,KAAsB,EACtB,WAA8F,GAC/F,EAAE,yBAAyB,qBAgb3B;AAED,YAAY,EAAE,WAAW,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,CAAA"}
@@ -1,14 +1,84 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  /**
3
- * SidebarLayoutManager — admin editor for the org-wide sidebar overlay.
4
- * Transport-agnostic (loaders/mutators via props), same pattern as
5
- * PermissionsManager. Hosts wire fetchers to /api/org/sidebar-layout.
3
+ * SidebarLayoutManager — Discord-style org sidebar editor.
4
+ * The canvas IS the sidebar: sections, folders, leaves — drag, rename, nest.
6
5
  */
7
6
  import * as React from 'react';
8
- import { ChevronDown, ChevronRight, FolderPlus, GripVertical, RotateCcw, Save, Trash2, Eye, EyeOff, Folder, FileText, } from 'lucide-react';
7
+ import { DndContext, DragOverlay, PointerSensor, closestCenter, useSensor, useSensors, } from '@dnd-kit/core';
8
+ import { SortableContext, arrayMove, useSortable, verticalListSortingStrategy, } from '@dnd-kit/sortable';
9
+ import { CSS } from '@dnd-kit/utilities';
10
+ import { ChevronDown, ChevronRight, FolderPlus, GripVertical, Pencil, Plus, RotateCcw, Save, Trash2, Eye, EyeOff, Hash, Folder, } from 'lucide-react';
9
11
  import { toast } from 'sonner';
10
- import { Button, Card, CardContent, CardDescription, CardHeader, CardTitle, Input, Label, ScrollArea, Badge, } from '@asteby/metacore-ui/primitives';
11
- import { applySidebarLayout, collectFolderKeys, folderAccessCapability, layoutTreeFromNavGroups, slugifyFolderKey, } from './apply-sidebar-layout';
12
+ import { Button, Input, ScrollArea, } from '@asteby/metacore-ui/primitives';
13
+ import { cn } from '@asteby/metacore-ui/lib';
14
+ import { collectFolderKeys, folderAccessCapability, layoutTreeFromNavGroups, leafRefFor, slugifyFolderKey, } from './apply-sidebar-layout';
15
+ function pathKey(path) {
16
+ return path.length ? path.join('.') : 'root';
17
+ }
18
+ function getAt(tree, path) {
19
+ let cur = tree;
20
+ let node = null;
21
+ for (const i of path) {
22
+ node = cur[i] ?? null;
23
+ if (!node)
24
+ return null;
25
+ cur = node.children ?? [];
26
+ }
27
+ return node;
28
+ }
29
+ function setChildren(tree, parentPath, children) {
30
+ if (parentPath.length === 0)
31
+ return children;
32
+ const clone = structuredClone(tree);
33
+ let cur = clone;
34
+ for (let d = 0; d < parentPath.length - 1; d++) {
35
+ const n = cur[parentPath[d]];
36
+ if (!n)
37
+ return tree;
38
+ n.children = n.children ?? [];
39
+ cur = n.children;
40
+ }
41
+ const parent = cur[parentPath[parentPath.length - 1]];
42
+ if (!parent)
43
+ return tree;
44
+ parent.children = children;
45
+ return clone;
46
+ }
47
+ function removeAt(tree, path) {
48
+ if (path.length === 0)
49
+ return { tree, node: null };
50
+ const parentPath = path.slice(0, -1);
51
+ const idx = path[path.length - 1];
52
+ const parentKids = parentPath.length === 0
53
+ ? [...tree]
54
+ : [...(getAt(tree, parentPath)?.children ?? [])];
55
+ const [node] = parentKids.splice(idx, 1);
56
+ return { tree: setChildren(tree, parentPath, parentKids), node: node ?? null };
57
+ }
58
+ function insertAt(tree, parentPath, index, node) {
59
+ const kids = parentPath.length === 0
60
+ ? [...tree]
61
+ : [...(getAt(tree, parentPath)?.children ?? [])];
62
+ const i = Math.max(0, Math.min(index, kids.length));
63
+ kids.splice(i, 0, node);
64
+ return setChildren(tree, parentPath, kids);
65
+ }
66
+ function updateAt(tree, path, patch) {
67
+ const clone = structuredClone(tree);
68
+ let cur = clone;
69
+ for (let d = 0; d < path.length - 1; d++) {
70
+ const n = cur[path[d]];
71
+ if (!n)
72
+ return tree;
73
+ n.children = n.children ?? [];
74
+ cur = n.children;
75
+ }
76
+ const i = path[path.length - 1];
77
+ if (!cur[i])
78
+ return tree;
79
+ cur[i] = { ...cur[i], ...patch };
80
+ return clone;
81
+ }
12
82
  function collectUsedRefs(tree) {
13
83
  const used = new Set();
14
84
  const walk = (nodes) => {
@@ -22,86 +92,177 @@ function collectUsedRefs(tree) {
22
92
  walk(tree);
23
93
  return used;
24
94
  }
25
- function updateAt(tree, path, fn) {
26
- if (path.length === 0)
27
- return tree;
28
- const [head, ...rest] = path;
29
- return tree
30
- .map((n, i) => {
31
- if (i !== head)
32
- return n;
33
- if (rest.length === 0)
34
- return fn(n);
35
- const kids = updateAt(n.children ?? [], rest, fn);
36
- return { ...n, children: kids };
37
- })
38
- .filter((n) => n != null);
39
- }
40
- function removeAt(tree, path) {
41
- return updateAt(tree, path, () => null).filter(Boolean);
95
+ function labelOf(node, catalogByRef, resolveLabel) {
96
+ if (node.type === 'leaf') {
97
+ const leaf = node.ref ? catalogByRef.get(node.ref) : undefined;
98
+ const raw = node.title || leaf?.title || node.ref || '…';
99
+ return resolveLabel(raw);
100
+ }
101
+ return resolveLabel(node.title || node.key || '…');
42
102
  }
43
- function moveSibling(tree, path, dir) {
44
- if (path.length === 0)
45
- return tree;
46
- const parentPath = path.slice(0, -1);
47
- const idx = path[path.length - 1];
48
- const getList = (nodes, p) => {
49
- if (p.length === 0)
50
- return nodes;
51
- let cur = nodes;
52
- for (const i of p) {
53
- cur = cur[i]?.children ?? [];
54
- }
55
- return cur;
103
+ // ── Sortable row ──────────────────────────────────────────────────────────
104
+ function SortableRow({ id, depth, kind, label, hidden, collapsed, onToggleCollapse, onRename, onToggleHidden, onDelete, onAddFolder, onAddLeaf, unusedLeaves, children, }) {
105
+ const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id });
106
+ const [editing, setEditing] = React.useState(false);
107
+ const [draft, setDraft] = React.useState(label);
108
+ const inputRef = React.useRef(null);
109
+ React.useEffect(() => {
110
+ if (editing) {
111
+ setDraft(label);
112
+ requestAnimationFrame(() => inputRef.current?.select());
113
+ }
114
+ }, [editing, label]);
115
+ const style = {
116
+ transform: CSS.Transform.toString(transform),
117
+ transition,
118
+ opacity: isDragging ? 0.45 : hidden ? 0.45 : 1,
119
+ paddingLeft: 8 + depth * 14,
56
120
  };
57
- const list = [...getList(tree, parentPath)];
58
- const j = idx + dir;
59
- if (j < 0 || j >= list.length)
60
- return tree;
61
- [list[idx], list[j]] = [list[j], list[idx]];
62
- if (parentPath.length === 0)
63
- return list;
64
- return updateAt(tree, parentPath, (n) => ({ ...n, children: list }));
65
- }
66
- function appendChild(tree, parentPath, child) {
67
- if (parentPath.length === 0)
68
- return [...tree, child];
69
- return updateAt(tree, parentPath, (n) => ({
70
- ...n,
71
- children: [...(n.children ?? []), child],
72
- }));
121
+ const isCategory = kind === 'group' || kind === 'folder';
122
+ const commitRename = () => {
123
+ const next = draft.trim();
124
+ setEditing(false);
125
+ if (next && next !== label)
126
+ onRename(next);
127
+ };
128
+ return (_jsxs("div", { ref: setNodeRef, style: style, className: "group/row", children: [_jsxs("div", { className: cn('flex items-center gap-1 rounded-md py-1 pr-1', isCategory ? 'mt-2 first:mt-0' : 'hover:bg-muted/50', isDragging && 'bg-muted/80'), children: [_jsx("button", { type: "button", className: "text-muted-foreground hover:text-foreground cursor-grab px-0.5 active:cursor-grabbing", "aria-label": "Arrastrar", ...attributes, ...listeners, children: _jsx(GripVertical, { className: "size-3.5 opacity-40 group-hover/row:opacity-100" }) }), isCategory ? (_jsx("button", { type: "button", className: "text-muted-foreground hover:text-foreground p-0.5", onClick: onToggleCollapse, children: collapsed ? (_jsx(ChevronRight, { className: "size-3.5" })) : (_jsx(ChevronDown, { className: "size-3.5" })) })) : (_jsx(Hash, { className: "text-muted-foreground size-3.5 shrink-0 opacity-60" })), kind === 'folder' ? (_jsx(Folder, { className: "size-3.5 shrink-0 text-amber-500/90" })) : null, editing ? (_jsx(Input, { ref: inputRef, value: draft, onChange: (e) => setDraft(e.target.value), onBlur: commitRename, onKeyDown: (e) => {
129
+ if (e.key === 'Enter')
130
+ commitRename();
131
+ if (e.key === 'Escape')
132
+ setEditing(false);
133
+ }, className: "h-7 flex-1 text-sm" })) : (_jsx("button", { type: "button", className: cn('min-w-0 flex-1 truncate text-left text-sm', isCategory
134
+ ? 'text-muted-foreground text-[11px] font-semibold uppercase tracking-wider'
135
+ : 'font-medium'), onDoubleClick: () => setEditing(true), title: "Doble clic para renombrar", children: label })), _jsxs("div", { className: "flex shrink-0 items-center gap-0.5 opacity-0 transition-opacity group-hover/row:opacity-100 focus-within:opacity-100", children: [kind === 'group' || kind === 'folder' ? (_jsxs(_Fragment, { children: [onAddFolder ? (_jsx(Button, { type: "button", size: "icon", variant: "ghost", className: "size-6", title: "Nueva carpeta", onClick: onAddFolder, children: _jsx(FolderPlus, { className: "size-3" }) })) : null, onAddLeaf && unusedLeaves && unusedLeaves.length > 0 ? (_jsxs("select", { className: "border-input bg-background h-6 max-w-[110px] rounded border px-1 text-[10px]", defaultValue: "", title: "Agregar \u00EDtem", onChange: (e) => {
136
+ const ref = e.target.value;
137
+ e.target.value = '';
138
+ if (ref)
139
+ onAddLeaf(ref);
140
+ }, children: [_jsx("option", { value: "", children: "+ \u00EDtem" }), unusedLeaves.map((l) => (_jsx("option", { value: l.ref, children: l.title }, l.ref)))] })) : null] })) : null, _jsx(Button, { type: "button", size: "icon", variant: "ghost", className: "size-6", title: "Renombrar", onClick: () => setEditing(true), children: _jsx(Pencil, { className: "size-3" }) }), _jsx(Button, { type: "button", size: "icon", variant: "ghost", className: "size-6", title: hidden ? 'Mostrar' : 'Ocultar', onClick: onToggleHidden, children: hidden ? _jsx(EyeOff, { className: "size-3" }) : _jsx(Eye, { className: "size-3" }) }), _jsx(Button, { type: "button", size: "icon", variant: "ghost", className: "text-destructive size-6", title: "Quitar", onClick: onDelete, children: _jsx(Trash2, { className: "size-3" }) })] })] }), !collapsed ? children : null] }));
73
141
  }
74
- export function SidebarLayoutManager({ loadLayout, saveLayout, resetLayout, catalog, currentGroups, title = 'Menú lateral', description = 'Organizá carpetas y el orden del sidebar para toda la organización. Los permisos de cada carpeta aparecen en Permisos.', }) {
142
+ // ── Manager ───────────────────────────────────────────────────────────────
143
+ export function SidebarLayoutManager({ loadLayout, saveLayout, resetLayout, catalog, currentGroups, resolveLabel = (s) => s, title = 'Menú lateral', description = 'Arrastrá, renombrá y agrupá como en Discord. Así lo verá toda la organización.', }) {
75
144
  const [tree, setTree] = React.useState([]);
76
145
  const [baseline, setBaseline] = React.useState('[]');
77
146
  const [loading, setLoading] = React.useState(true);
78
147
  const [saving, setSaving] = React.useState(false);
79
- const [newFolderTitle, setNewFolderTitle] = React.useState('');
80
- const [expanded, setExpanded] = React.useState({});
148
+ const [collapsed, setCollapsed] = React.useState({});
149
+ const [activeId, setActiveId] = React.useState(null);
150
+ const [newCategory, setNewCategory] = React.useState('');
151
+ const catalogByRef = React.useMemo(() => {
152
+ const m = new Map();
153
+ for (const l of catalog) {
154
+ m.set(l.ref, { ...l, title: resolveLabel(l.title) });
155
+ for (const a of l.aliases ?? [])
156
+ m.set(a, m.get(l.ref));
157
+ }
158
+ return m;
159
+ }, [catalog, resolveLabel]);
160
+ const unused = React.useMemo(() => {
161
+ const used = collectUsedRefs(tree);
162
+ return catalog
163
+ .filter((l) => !used.has(l.ref) && !(l.aliases ?? []).some((a) => used.has(a)))
164
+ .map((l) => ({ ...l, title: resolveLabel(l.title) }));
165
+ }, [catalog, tree, resolveLabel]);
166
+ const sensors = useSensors(useSensor(PointerSensor, { activationConstraint: { distance: 6 } }));
167
+ const seedFromCurrent = React.useCallback(() => {
168
+ if (!currentGroups?.length) {
169
+ toast.message('No hay menú actual para copiar');
170
+ return [];
171
+ }
172
+ return layoutTreeFromNavGroups(currentGroups, (u) => leafRefFor(u));
173
+ }, [currentGroups]);
81
174
  const reload = React.useCallback(async () => {
82
175
  setLoading(true);
83
176
  try {
84
177
  const doc = await loadLayout();
85
- const t = doc.tree ?? [];
178
+ let t = doc.tree ?? [];
179
+ // Empty overlay → show live menu as editable draft (Discord empty-server feel).
180
+ if (t.length === 0 && currentGroups?.length) {
181
+ t = seedFromCurrent();
182
+ setBaseline('[]'); // still "unsaved overlay"
183
+ }
184
+ else {
185
+ setBaseline(JSON.stringify(t));
186
+ }
86
187
  setTree(t);
87
- setBaseline(JSON.stringify(t));
88
188
  }
89
189
  catch (e) {
90
- toast.error(e instanceof Error ? e.message : 'No se pudo cargar el layout');
190
+ toast.error(e instanceof Error ? e.message : 'No se pudo cargar el menú');
91
191
  }
92
192
  finally {
93
193
  setLoading(false);
94
194
  }
95
- }, [loadLayout]);
195
+ }, [loadLayout, currentGroups, seedFromCurrent]);
96
196
  React.useEffect(() => {
97
197
  void reload();
98
198
  }, [reload]);
99
199
  const dirty = JSON.stringify(tree) !== baseline;
100
- const used = React.useMemo(() => collectUsedRefs(tree), [tree]);
101
- const unused = React.useMemo(() => catalog.filter((l) => !used.has(l.ref) && !(l.aliases ?? []).some((a) => used.has(a))), [catalog, used]);
102
200
  const folderKeys = React.useMemo(() => collectFolderKeys(tree), [tree]);
103
- const preview = React.useMemo(() => applySidebarLayout(catalog, tree), [catalog, tree]);
104
- const pathKey = (path) => path.join('.');
201
+ // Flat id → path index for DnD (ids are path keys)
202
+ const idToPath = React.useMemo(() => {
203
+ const map = new Map();
204
+ const walk = (nodes, parent) => {
205
+ nodes.forEach((_, i) => {
206
+ const path = [...parent, i];
207
+ map.set(pathKey(path), path);
208
+ const n = nodes[i];
209
+ if (n.children?.length)
210
+ walk(n.children, path);
211
+ });
212
+ };
213
+ walk(tree, []);
214
+ return map;
215
+ }, [tree]);
216
+ const handleDragStart = (e) => {
217
+ setActiveId(String(e.active.id));
218
+ };
219
+ const handleDragEnd = (e) => {
220
+ setActiveId(null);
221
+ const { active, over } = e;
222
+ if (!over || active.id === over.id)
223
+ return;
224
+ const fromPath = idToPath.get(String(active.id));
225
+ const toPath = idToPath.get(String(over.id));
226
+ if (!fromPath || !toPath)
227
+ return;
228
+ // Same parent → reorder siblings
229
+ const fromParent = fromPath.slice(0, -1);
230
+ const toParent = toPath.slice(0, -1);
231
+ if (pathKey(fromParent) === pathKey(toParent)) {
232
+ const kids = fromParent.length === 0
233
+ ? [...tree]
234
+ : [...(getAt(tree, fromParent)?.children ?? [])];
235
+ const oldIndex = fromPath[fromPath.length - 1];
236
+ const newIndex = toPath[toPath.length - 1];
237
+ setTree(setChildren(tree, fromParent, arrayMove(kids, oldIndex, newIndex)));
238
+ return;
239
+ }
240
+ // Different parent → move node under target's parent at target index
241
+ // (or into target if it's a folder/group)
242
+ const overNode = getAt(tree, toPath);
243
+ let destParent = toParent;
244
+ let destIndex = toPath[toPath.length - 1];
245
+ if (overNode && (overNode.type === 'group' || overNode.type === 'folder')) {
246
+ destParent = toPath;
247
+ destIndex = overNode.children?.length ?? 0;
248
+ }
249
+ // Prevent dropping into own descendant
250
+ const fromKey = pathKey(fromPath);
251
+ if (pathKey(destParent).startsWith(fromKey + '.') || pathKey(destParent) === fromKey) {
252
+ return;
253
+ }
254
+ setTree((prev) => {
255
+ const { tree: without, node } = removeAt(prev, fromPath);
256
+ if (!node)
257
+ return prev;
258
+ // Recompute dest index if we removed from earlier sibling in same parent
259
+ let idx = destIndex;
260
+ if (pathKey(fromParent) === pathKey(destParent) && fromPath[fromPath.length - 1] < destIndex) {
261
+ idx = Math.max(0, destIndex - 1);
262
+ }
263
+ return insertAt(without, destParent, idx, node);
264
+ });
265
+ };
105
266
  const handleSave = async () => {
106
267
  setSaving(true);
107
268
  try {
@@ -120,9 +281,10 @@ export function SidebarLayoutManager({ loadLayout, saveLayout, resetLayout, cata
120
281
  setSaving(true);
121
282
  try {
122
283
  await resetLayout();
123
- setTree([]);
284
+ const seeded = seedFromCurrent();
285
+ setTree(seeded);
124
286
  setBaseline('[]');
125
- toast.success('Menú restaurado al default (addons + core)');
287
+ toast.success('Overlay restaurado — mostrando el menú default');
126
288
  }
127
289
  catch (e) {
128
290
  toast.error(e instanceof Error ? e.message : 'Error al resetear');
@@ -131,74 +293,92 @@ export function SidebarLayoutManager({ loadLayout, saveLayout, resetLayout, cata
131
293
  setSaving(false);
132
294
  }
133
295
  };
134
- const seedFromCurrent = () => {
135
- if (!currentGroups?.length) {
136
- toast.message('No hay menú actual para copiar');
137
- return;
138
- }
139
- const seeded = layoutTreeFromNavGroups(currentGroups);
140
- setTree(seeded);
141
- toast.message('Árbol partido del menú actual — guardá para aplicar');
142
- };
143
- const addRootFolder = () => {
144
- const titleText = newFolderTitle.trim() || 'Nueva carpeta';
145
- const key = slugifyFolderKey(titleText);
296
+ const addRootCategory = () => {
297
+ const name = newCategory.trim() || 'Nueva sección';
298
+ const key = slugifyFolderKey(name);
146
299
  setTree((t) => [
147
300
  ...t,
148
301
  {
149
302
  type: 'group',
150
- key: `group_${key}`,
151
- title: titleText,
152
- children: [{ type: 'folder', key, title: titleText, icon: 'Folder', children: [] }],
303
+ key: `group_${key}_${Date.now() % 10000}`,
304
+ title: name,
305
+ children: [],
153
306
  },
154
307
  ]);
155
- setNewFolderTitle('');
156
- };
157
- const addLeafTo = (parentPath, leaf) => {
158
- setTree((t) => appendChild(t, parentPath, {
159
- type: 'leaf',
160
- ref: leaf.ref,
161
- title: leaf.title,
162
- icon: leaf.icon,
163
- }));
308
+ setNewCategory('');
164
309
  };
165
- const renderNode = (node, path) => {
166
- const k = pathKey(path);
167
- const isOpen = expanded[k] ?? true;
168
- const isFolderish = node.type === 'group' || node.type === 'folder';
169
- return (_jsxs("div", { className: "border-border/60 rounded-md border bg-background/50", children: [_jsxs("div", { className: "flex items-center gap-1 px-2 py-1.5", children: [isFolderish ? (_jsx("button", { type: "button", className: "text-muted-foreground hover:text-foreground p-0.5", onClick: () => setExpanded((e) => ({ ...e, [k]: !isOpen })), children: isOpen ? _jsx(ChevronDown, { className: "size-4" }) : _jsx(ChevronRight, { className: "size-4" }) })) : (_jsx(GripVertical, { className: "text-muted-foreground size-4 shrink-0 opacity-40" })), node.type === 'folder' ? (_jsx(Folder, { className: "text-amber-600 size-4 shrink-0" })) : node.type === 'leaf' ? (_jsx(FileText, { className: "text-muted-foreground size-4 shrink-0" })) : null, _jsx("span", { className: "min-w-0 flex-1 truncate text-sm font-medium", children: node.title || node.key || node.ref }), node.type === 'folder' && node.key ? (_jsx(Badge, { variant: "outline", className: "font-mono text-[10px]", children: folderAccessCapability(node.key) })) : null, _jsx(Button, { type: "button", size: "icon", variant: "ghost", className: "size-7", title: node.hidden ? 'Mostrar' : 'Ocultar', onClick: () => setTree((t) => updateAt(t, path, (n) => ({ ...n, hidden: !n.hidden }))), children: node.hidden ? _jsx(EyeOff, { className: "size-3.5" }) : _jsx(Eye, { className: "size-3.5" }) }), _jsx(Button, { type: "button", size: "icon", variant: "ghost", className: "size-7", onClick: () => setTree((t) => moveSibling(t, path, -1)), children: "\u2191" }), _jsx(Button, { type: "button", size: "icon", variant: "ghost", className: "size-7", onClick: () => setTree((t) => moveSibling(t, path, 1)), children: "\u2193" }), _jsx(Button, { type: "button", size: "icon", variant: "ghost", className: "text-destructive size-7", onClick: () => setTree((t) => removeAt(t, path)), children: _jsx(Trash2, { className: "size-3.5" }) })] }), isFolderish && isOpen ? (_jsxs("div", { className: "border-border/40 space-y-1 border-t px-2 py-2 pl-6", children: [(node.children ?? []).map((child, i) => renderNode(child, [...path, i])), unused.length > 0 ? (_jsxs("select", { className: "border-input bg-background w-full rounded-md border px-2 py-1 text-xs", defaultValue: "", onChange: (e) => {
170
- const ref = e.target.value;
171
- e.target.value = '';
172
- const leaf = catalog.find((l) => l.ref === ref);
173
- if (leaf)
174
- addLeafTo(path, leaf);
175
- }, children: [_jsx("option", { value: "", children: "+ Agregar \u00EDtem\u2026" }), unused.map((l) => (_jsxs("option", { value: l.ref, children: [l.title, " (", l.ref, ")"] }, l.ref)))] })) : null, node.type === 'group' ? (_jsxs(Button, { type: "button", size: "sm", variant: "outline", className: "h-7 text-xs", onClick: () => {
176
- const titleText = 'Nueva carpeta';
177
- const key = slugifyFolderKey(`${titleText}_${path.join('')}_${Date.now() % 1000}`);
178
- setTree((t) => appendChild(t, path, {
179
- type: 'folder',
180
- key,
181
- title: titleText,
182
- icon: 'Folder',
183
- children: [],
184
- }));
185
- }, children: [_jsx(FolderPlus, { className: "mr-1 size-3.5" }), "Subcarpeta"] })) : null] })) : null] }, k));
310
+ const renderList = (nodes, parentPath) => {
311
+ const ids = nodes.map((_, i) => pathKey([...parentPath, i]));
312
+ return (_jsx(SortableContext, { items: ids, strategy: verticalListSortingStrategy, children: nodes.map((node, i) => {
313
+ const path = [...parentPath, i];
314
+ const id = pathKey(path);
315
+ const depth = parentPath.length;
316
+ const isOpen = !(collapsed[id] ?? false);
317
+ const lbl = labelOf(node, catalogByRef, resolveLabel);
318
+ return (_jsx(SortableRow, { id: id, depth: depth, kind: node.type, label: lbl, hidden: node.hidden, collapsed: !isOpen, onToggleCollapse: () => setCollapsed((c) => ({ ...c, [id]: !(c[id] ?? false) })), onRename: (next) => setTree((t) => updateAt(t, path, { title: next })), onToggleHidden: () => setTree((t) => updateAt(t, path, { hidden: !node.hidden })), onDelete: () => setTree((t) => removeAt(t, path).tree), onAddFolder: node.type === 'group' || node.type === 'folder'
319
+ ? () => {
320
+ const key = slugifyFolderKey(`carpeta_${Date.now() % 1000}`);
321
+ setTree((t) => insertAt(t, path, node.children?.length ?? 0, {
322
+ type: 'folder',
323
+ key,
324
+ title: 'Nueva carpeta',
325
+ icon: 'Folder',
326
+ children: [],
327
+ }));
328
+ setCollapsed((c) => ({ ...c, [id]: false }));
329
+ }
330
+ : undefined, onAddLeaf: node.type === 'group' || node.type === 'folder'
331
+ ? (ref) => {
332
+ const leaf = catalog.find((l) => l.ref === ref);
333
+ setTree((t) => insertAt(t, path, node.children?.length ?? 0, {
334
+ type: 'leaf',
335
+ ref,
336
+ title: leaf?.title,
337
+ icon: leaf?.icon,
338
+ }));
339
+ }
340
+ : undefined, unusedLeaves: unused, children: node.type !== 'leaf' && isOpen
341
+ ? renderList(node.children ?? [], path)
342
+ : null }, id));
343
+ }) }));
186
344
  };
187
- return (_jsxs("div", { className: "space-y-4", children: [_jsxs("div", { className: "flex flex-wrap items-start justify-between gap-3", children: [_jsxs("div", { className: "space-y-1", children: [_jsx("h2", { className: "text-xl font-semibold tracking-tight", children: title }), _jsx("p", { className: "text-muted-foreground max-w-2xl text-sm", children: description })] }), _jsxs("div", { className: "flex flex-wrap gap-2", children: [_jsx(Button, { type: "button", variant: "outline", size: "sm", onClick: seedFromCurrent, children: "Partir del men\u00FA actual" }), _jsxs(Button, { type: "button", variant: "outline", size: "sm", disabled: saving, onClick: () => void handleReset(), children: [_jsx(RotateCcw, { className: "mr-1 size-3.5" }), "Restaurar default"] }), _jsxs(Button, { type: "button", size: "sm", disabled: !dirty || saving, onClick: () => void handleSave(), children: [_jsx(Save, { className: "mr-1 size-3.5" }), saving ? 'Guardando…' : dirty ? 'Guardar' : 'Guardado'] })] })] }), loading ? (_jsx("p", { className: "text-muted-foreground text-sm", children: "Cargando\u2026" })) : (_jsxs("div", { className: "grid gap-4 lg:grid-cols-2", children: [_jsxs(Card, { children: [_jsxs(CardHeader, { className: "pb-3", children: [_jsx(CardTitle, { className: "text-base", children: "\u00C1rbol del men\u00FA" }), _jsxs(CardDescription, { children: ["Carpetas nuevas otorgan el permiso", ' ', _jsx("code", { className: "text-xs", children: "folder.<key>.access" }), "."] })] }), _jsxs(CardContent, { className: "space-y-3", children: [_jsxs("div", { className: "flex gap-2", children: [_jsxs("div", { className: "flex-1 space-y-1", children: [_jsx(Label, { className: "text-xs", children: "Nueva secci\u00F3n + carpeta" }), _jsx(Input, { value: newFolderTitle, onChange: (e) => setNewFolderTitle(e.target.value), placeholder: "Configuraci\u00F3n" })] }), _jsxs(Button, { type: "button", className: "mt-5", size: "sm", onClick: addRootFolder, children: [_jsx(FolderPlus, { className: "mr-1 size-3.5" }), "Crear"] })] }), _jsx(ScrollArea, { className: "h-[420px] pr-2", children: _jsx("div", { className: "space-y-2", children: tree.length === 0 ? (_jsx("p", { className: "text-muted-foreground text-sm", children: "Sin overlay \u2014 el sidebar usa el merge default. Part\u00ED del men\u00FA actual o cre\u00E1 una carpeta." })) : (tree.map((n, i) => renderNode(n, [i]))) }) }), folderKeys.length > 0 ? (_jsxs("p", { className: "text-muted-foreground text-xs", children: ["Carpetas con permiso: ", folderKeys.map((k) => folderAccessCapability(k)).join(', ')] })) : null] })] }), _jsxs(Card, { children: [_jsxs(CardHeader, { className: "pb-3", children: [_jsx(CardTitle, { className: "text-base", children: "Cat\u00E1logo sin usar / vista previa" }), _jsx(CardDescription, { children: "\u00CDtems disponibles para colocar. La vista previa aplica el overlay sobre el cat\u00E1logo vivo." })] }), _jsxs(CardContent, { className: "space-y-4", children: [_jsx(ScrollArea, { className: "h-[200px] rounded-md border p-2", children: unused.length === 0 ? (_jsx("p", { className: "text-muted-foreground text-xs", children: "Todos los \u00EDtems est\u00E1n en el \u00E1rbol." })) : (_jsx("ul", { className: "space-y-1", children: unused.map((l) => (_jsxs("li", { className: "flex items-center justify-between gap-2 text-xs", children: [_jsxs("span", { className: "truncate", children: [l.title, ' ', _jsx("span", { className: "text-muted-foreground font-mono", children: l.ref })] }), _jsx(Button, { type: "button", size: "sm", variant: "ghost", className: "h-6 shrink-0 text-xs", onClick: () => {
188
- if (tree.length === 0) {
189
- setTree([
190
- {
191
- type: 'group',
192
- key: 'sidebar.general',
193
- title: 'sidebar.general',
194
- children: [
195
- { type: 'leaf', ref: l.ref, title: l.title, icon: l.icon },
196
- ],
197
- },
198
- ]);
199
- }
200
- else {
201
- addLeafTo([0], l);
202
- }
203
- }, children: "+" })] }, l.ref))) })) }), _jsxs("div", { children: [_jsx("p", { className: "mb-2 text-xs font-medium uppercase tracking-wide text-muted-foreground", children: "Preview" }), !preview ? (_jsx("p", { className: "text-muted-foreground text-sm", children: "Default merge (sin overlay)." })) : (_jsx("ul", { className: "space-y-2 text-sm", children: preview.map((g) => (_jsxs("li", { children: [_jsx("div", { className: "text-muted-foreground text-xs font-semibold uppercase tracking-wide", children: g.title }), _jsx("ul", { className: "mt-1 space-y-0.5 pl-2", children: g.items.map((it) => (_jsxs("li", { children: [it.folderKey ? `📁 ${it.title}` : it.title, it.items?.length ? (_jsx("ul", { className: "text-muted-foreground pl-4 text-xs", children: it.items.map((c) => (_jsx("li", { children: c.title }, c.ref || c.url))) })) : null] }, `${it.title}-${it.url}`))) })] }, g.title))) }))] })] })] })] }))] }));
345
+ const activeLabel = activeId
346
+ ? (() => {
347
+ const p = idToPath.get(activeId);
348
+ if (!p)
349
+ return null;
350
+ const n = getAt(tree, p);
351
+ return n ? labelOf(n, catalogByRef, resolveLabel) : null;
352
+ })()
353
+ : null;
354
+ return (_jsxs("div", { className: "space-y-4", children: [_jsxs("div", { className: "flex flex-wrap items-start justify-between gap-3", children: [_jsxs("div", { className: "space-y-1", children: [_jsx("h2", { className: "text-xl font-semibold tracking-tight", children: title }), _jsx("p", { className: "text-muted-foreground max-w-xl text-sm", children: description })] }), _jsxs("div", { className: "flex flex-wrap gap-2", children: [_jsx(Button, { type: "button", variant: "outline", size: "sm", disabled: saving, onClick: () => {
355
+ setTree(seedFromCurrent());
356
+ toast.message('Árbol recargado desde el menú actual');
357
+ }, children: "Recargar men\u00FA actual" }), _jsxs(Button, { type: "button", variant: "outline", size: "sm", disabled: saving, onClick: () => void handleReset(), children: [_jsx(RotateCcw, { className: "mr-1 size-3.5" }), "Restaurar default"] }), _jsxs(Button, { type: "button", size: "sm", disabled: !dirty || saving, onClick: () => void handleSave(), children: [_jsx(Save, { className: "mr-1 size-3.5" }), saving ? 'Guardando…' : dirty ? 'Guardar cambios' : 'Guardado'] })] })] }), loading ? (_jsx("p", { className: "text-muted-foreground text-sm", children: "Cargando men\u00FA\u2026" })) : (_jsxs("div", { className: "grid gap-4 lg:grid-cols-[minmax(0,1fr)_240px]", children: [_jsxs("div", { className: "bg-muted/20 border-border overflow-hidden rounded-xl border", children: [_jsxs("div", { className: "border-border/60 flex items-center justify-between border-b px-3 py-2", children: [_jsx("span", { className: "text-muted-foreground text-[11px] font-semibold uppercase tracking-wider", children: "Vista del men\u00FA" }), baseline === '[]' && dirty ? (_jsx("span", { className: "text-amber-600 text-[11px]", children: "Sin guardar \u00B7 borrador del men\u00FA actual" })) : dirty ? (_jsx("span", { className: "text-amber-600 text-[11px]", children: "Cambios sin guardar" })) : (_jsx("span", { className: "text-muted-foreground text-[11px]", children: folderKeys.length
358
+ ? `${folderKeys.length} carpeta(s) con permiso`
359
+ : 'Layout activo' }))] }), _jsx(ScrollArea, { className: "h-[min(70vh,640px)]", children: _jsxs("div", { className: "p-2 pb-4", children: [_jsxs(DndContext, { sensors: sensors, collisionDetection: closestCenter, onDragStart: handleDragStart, onDragEnd: handleDragEnd, children: [tree.length === 0 ? (_jsx("p", { className: "text-muted-foreground px-3 py-8 text-center text-sm", children: "Men\u00FA vac\u00EDo. Cre\u00E1 una secci\u00F3n o recarg\u00E1 el men\u00FA actual." })) : (renderList(tree, [])), _jsx(DragOverlay, { children: activeLabel ? (_jsx("div", { className: "bg-background border-border rounded-md border px-3 py-1.5 text-sm shadow-lg", children: activeLabel })) : null })] }), _jsxs("div", { className: "mt-3 flex gap-2 px-2", children: [_jsx(Input, { value: newCategory, onChange: (e) => setNewCategory(e.target.value), placeholder: "Nombre de secci\u00F3n\u2026", className: "h-8 text-sm", onKeyDown: (e) => {
360
+ if (e.key === 'Enter')
361
+ addRootCategory();
362
+ } }), _jsxs(Button, { type: "button", size: "sm", className: "h-8 shrink-0", onClick: addRootCategory, children: [_jsx(Plus, { className: "mr-1 size-3.5" }), "Secci\u00F3n"] })] })] }) })] }), _jsxs("div", { className: "bg-muted/10 border-border flex flex-col overflow-hidden rounded-xl border", children: [_jsxs("div", { className: "border-border/60 border-b px-3 py-2", children: [_jsx("p", { className: "text-muted-foreground text-[11px] font-semibold uppercase tracking-wider", children: "Sin colocar" }), _jsx("p", { className: "text-muted-foreground mt-0.5 text-[11px]", children: "Agregalos desde el men\u00FA + de cada secci\u00F3n" })] }), _jsx(ScrollArea, { className: "flex-1", children: _jsx("ul", { className: "space-y-1 p-2", children: unused.length === 0 ? (_jsx("li", { className: "text-muted-foreground px-1 py-4 text-center text-xs", children: "Todo est\u00E1 en el men\u00FA" })) : (unused.map((l) => (_jsx("li", { children: _jsxs("button", { type: "button", className: "hover:bg-muted/60 flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left text-xs", title: l.ref, onClick: () => {
363
+ if (tree.length === 0) {
364
+ setTree([
365
+ {
366
+ type: 'group',
367
+ key: 'sidebar.general',
368
+ title: 'General',
369
+ children: [
370
+ { type: 'leaf', ref: l.ref, title: l.title, icon: l.icon },
371
+ ],
372
+ },
373
+ ]);
374
+ }
375
+ else {
376
+ setTree((t) => insertAt(t, [0], t[0]?.children?.length ?? 0, {
377
+ type: 'leaf',
378
+ ref: l.ref,
379
+ title: l.title,
380
+ icon: l.icon,
381
+ }));
382
+ }
383
+ }, children: [_jsx(Hash, { className: "text-muted-foreground size-3 shrink-0" }), _jsx("span", { className: "truncate font-medium", children: l.title }), _jsx(Plus, { className: "text-muted-foreground ml-auto size-3 shrink-0 opacity-50" })] }) }, l.ref)))) }) }), folderKeys.length > 0 ? (_jsxs("div", { className: "border-border/60 text-muted-foreground border-t px-3 py-2 text-[10px] leading-relaxed", children: ["Permisos:", ' ', folderKeys.map((k) => folderAccessCapability(k)).join(', ')] })) : null] })] }))] }));
204
384
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@asteby/metacore-runtime-react",
3
- "version": "39.2.0",
3
+ "version": "39.2.2",
4
4
  "description": "React runtime for metacore hosts — renders addon contributions dynamically",
5
5
  "repository": {
6
6
  "type": "git",
@@ -166,6 +166,11 @@ export function ModelActionToolbar({
166
166
  <Button
167
167
  key={a.key}
168
168
  variant={isCreate ? 'default' : 'outline'}
169
+ data-aby-target={
170
+ isCreate
171
+ ? `model.${model}.create`
172
+ : `model.${model}.action.${a.key}`
173
+ }
169
174
  onClick={() => {
170
175
  onActionIntent?.(a)
171
176
  setActive(toActionMetadata(a))