@ai-matrx/context-menu 0.0.0 → 0.1.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.
Files changed (45) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/README.md +14 -1
  3. package/dist/builder.cjs +1810 -0
  4. package/dist/builder.cjs.map +1 -0
  5. package/dist/builder.d.cts +22 -0
  6. package/dist/builder.d.ts +22 -0
  7. package/dist/builder.js +1795 -0
  8. package/dist/builder.js.map +1 -0
  9. package/dist/design-store.cjs +195 -0
  10. package/dist/design-store.cjs.map +1 -0
  11. package/dist/design-store.d.cts +42 -0
  12. package/dist/design-store.d.ts +42 -0
  13. package/dist/design-store.js +175 -0
  14. package/dist/design-store.js.map +1 -0
  15. package/dist/engine.cjs +3069 -0
  16. package/dist/engine.cjs.map +1 -0
  17. package/dist/engine.d.cts +731 -0
  18. package/dist/engine.d.ts +731 -0
  19. package/dist/engine.js +3250 -0
  20. package/dist/engine.js.map +1 -0
  21. package/dist/grouping-CB3zlD1T.d.ts +623 -0
  22. package/dist/grouping-CGcWsltu.d.cts +623 -0
  23. package/dist/host-types-AOiEYwR8.d.cts +9 -0
  24. package/dist/host-types-AOiEYwR8.d.ts +9 -0
  25. package/dist/index.cjs +583 -0
  26. package/dist/index.cjs.map +1 -0
  27. package/dist/index.d.cts +60 -0
  28. package/dist/index.d.ts +60 -0
  29. package/dist/index.js +561 -0
  30. package/dist/index.js.map +1 -0
  31. package/dist/menu-content.cjs +3146 -0
  32. package/dist/menu-content.cjs.map +1 -0
  33. package/dist/menu-content.d.cts +30 -0
  34. package/dist/menu-content.d.ts +30 -0
  35. package/dist/menu-content.js +3197 -0
  36. package/dist/menu-content.js.map +1 -0
  37. package/dist/model-CkLCR6WY.d.cts +123 -0
  38. package/dist/model-CkLCR6WY.d.ts +123 -0
  39. package/dist/react.cjs +3322 -0
  40. package/dist/react.cjs.map +1 -0
  41. package/dist/react.d.cts +133 -0
  42. package/dist/react.d.ts +133 -0
  43. package/dist/react.js +3345 -0
  44. package/dist/react.js.map +1 -0
  45. package/package.json +170 -1
package/dist/index.cjs ADDED
@@ -0,0 +1,583 @@
1
+ "use client";
2
+ "use strict";
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+ var __copyProps = (to, from, except, desc) => {
12
+ if (from && typeof from === "object" || typeof from === "function") {
13
+ for (let key of __getOwnPropNames(from))
14
+ if (!__hasOwnProp.call(to, key) && key !== except)
15
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
16
+ }
17
+ return to;
18
+ };
19
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
20
+
21
+ // src/index.ts
22
+ var src_exports = {};
23
+ __export(src_exports, {
24
+ CATALOG: () => CATALOG,
25
+ CATALOG_GROUPS: () => CATALOG_GROUPS,
26
+ DEFAULT_LOOK: () => DEFAULT_LOOK,
27
+ DESIGN_FORMAT: () => DESIGN_FORMAT,
28
+ MENU_SURFACES: () => MENU_SURFACES,
29
+ MenuPreview: () => MenuPreview,
30
+ SLOT_SOURCES: () => SLOT_SOURCES,
31
+ TEMPLATES: () => TEMPLATES,
32
+ canMove: () => canMove,
33
+ catalogOption: () => catalogOption,
34
+ catalogUseCounts: () => catalogUseCounts,
35
+ cloneNode: () => cloneNode,
36
+ contains: () => contains,
37
+ countItems: () => countItems,
38
+ duplicateNode: () => duplicateNode,
39
+ emptyDesign: () => emptyDesign,
40
+ insertNode: () => insertNode,
41
+ isContainer: () => isContainer,
42
+ locate: () => locate,
43
+ moveNode: () => moveNode,
44
+ newId: () => newId,
45
+ parseDesign: () => parseDesign,
46
+ placedCatalogIds: () => placedCatalogIds,
47
+ removeNode: () => removeNode,
48
+ serializeDesign: () => serializeDesign,
49
+ slotOptions: () => slotOptions,
50
+ slotSource: () => slotSource,
51
+ unwrapNode: () => unwrapNode,
52
+ updateNode: () => updateNode,
53
+ wrapNode: () => wrapNode
54
+ });
55
+ module.exports = __toCommonJS(src_exports);
56
+
57
+ // src/design/model.ts
58
+ var MENU_SURFACES = [
59
+ "text-selection",
60
+ "editable-field",
61
+ "table-row",
62
+ "page",
63
+ "chat-message",
64
+ "file",
65
+ "anywhere"
66
+ ];
67
+ var DEFAULT_LOOK = {
68
+ density: "comfortable",
69
+ width: 280,
70
+ showIcons: true,
71
+ showShortcuts: true,
72
+ showHints: false,
73
+ search: false
74
+ };
75
+ var DESIGN_FORMAT = 1;
76
+ function newId(prefix = "n") {
77
+ const rnd = typeof crypto !== "undefined" && "randomUUID" in crypto ? crypto.randomUUID().slice(0, 8) : Math.random().toString(36).slice(2, 10);
78
+ return `${prefix}_${rnd}`;
79
+ }
80
+ function emptyDesign(name = "Untitled menu") {
81
+ return { designId: newId("d"), name, surface: "anywhere", look: { ...DEFAULT_LOOK }, nodes: [], notes: "" };
82
+ }
83
+ function isContainer(node) {
84
+ return node.kind === "section" || node.kind === "submenu";
85
+ }
86
+ function placedCatalogIds(nodes, out = /* @__PURE__ */ new Set()) {
87
+ for (const n of nodes) {
88
+ if (n.catalogId) out.add(n.catalogId);
89
+ if (isContainer(n)) placedCatalogIds(n.children, out);
90
+ }
91
+ return out;
92
+ }
93
+ function catalogUseCounts(nodes, out = /* @__PURE__ */ new Map()) {
94
+ for (const n of nodes) {
95
+ if (n.catalogId) out.set(n.catalogId, (out.get(n.catalogId) ?? 0) + 1);
96
+ if (isContainer(n)) catalogUseCounts(n.children, out);
97
+ }
98
+ return out;
99
+ }
100
+ function cloneNode(node) {
101
+ const copy = { ...node, id: newId(node.kind[0]) };
102
+ if (isContainer(copy)) copy.children = copy.children.map(cloneNode);
103
+ return copy;
104
+ }
105
+ function locate(nodes, id, parentId = null, depth = 0) {
106
+ for (let i = 0; i < nodes.length; i++) {
107
+ const node = nodes[i];
108
+ if (node.id === id) return { node, parentId, index: i, depth };
109
+ if (isContainer(node)) {
110
+ const inner = locate(node.children, id, node.id, depth + 1);
111
+ if (inner) return inner;
112
+ }
113
+ }
114
+ return null;
115
+ }
116
+ function contains(node, id) {
117
+ if (node.id === id) return true;
118
+ return isContainer(node) && node.children.some((c) => contains(c, id));
119
+ }
120
+ function countItems(nodes) {
121
+ let n = 0;
122
+ for (const node of nodes) {
123
+ if (node.kind === "item" || node.kind === "slot") n += 1;
124
+ else if (isContainer(node)) n += countItems(node.children) + (node.kind === "submenu" ? 1 : 0);
125
+ }
126
+ return n;
127
+ }
128
+ function mapChildren(nodes, parentId, fn) {
129
+ if (parentId === null) return fn([...nodes]);
130
+ return nodes.map((node) => {
131
+ if (!isContainer(node)) return node;
132
+ if (node.id === parentId) return { ...node, children: fn([...node.children]) };
133
+ return { ...node, children: mapChildren(node.children, parentId, fn) };
134
+ });
135
+ }
136
+ function insertNode(nodes, parentId, index, node) {
137
+ return mapChildren(nodes, parentId, (list) => {
138
+ list.splice(Math.max(0, Math.min(index, list.length)), 0, node);
139
+ return list;
140
+ });
141
+ }
142
+ function removeNode(nodes, id) {
143
+ return nodes.filter((n) => n.id !== id).map((n) => isContainer(n) ? { ...n, children: removeNode(n.children, id) } : n);
144
+ }
145
+ function updateNode(nodes, id, patch) {
146
+ return nodes.map((n) => {
147
+ if (n.id === id) return { ...n, ...patch, id: n.id, kind: n.kind };
148
+ if (isContainer(n)) return { ...n, children: updateNode(n.children, id, patch) };
149
+ return n;
150
+ });
151
+ }
152
+ function canMove(nodes, id, parentId) {
153
+ const from = locate(nodes, id);
154
+ return Boolean(from) && (parentId === null || !contains(from.node, parentId));
155
+ }
156
+ function moveNode(nodes, id, parentId, index) {
157
+ const from = locate(nodes, id);
158
+ if (!from) return nodes;
159
+ if (parentId !== null && contains(from.node, parentId)) return nodes;
160
+ let target = index;
161
+ if (from.parentId === parentId && from.index < index) target -= 1;
162
+ return insertNode(removeNode(nodes, id), parentId, target, from.node);
163
+ }
164
+ function duplicateNode(nodes, id) {
165
+ const at = locate(nodes, id);
166
+ if (!at) return [...nodes];
167
+ return insertNode(nodes, at.parentId, at.index + 1, cloneNode(at.node));
168
+ }
169
+ function wrapNode(nodes, id, kind) {
170
+ const at = locate(nodes, id);
171
+ if (!at) return [...nodes];
172
+ const wrapper = kind === "section" ? { id: newId("s"), kind: "section", label: "New section", children: [at.node] } : { id: newId("m"), kind: "submenu", label: "More", icon: "ChevronRight", children: [at.node] };
173
+ return insertNode(removeNode(nodes, id), at.parentId, at.index, wrapper);
174
+ }
175
+ function unwrapNode(nodes, id) {
176
+ const at = locate(nodes, id);
177
+ if (!at || !isContainer(at.node)) return [...nodes];
178
+ let out = removeNode(nodes, id);
179
+ at.node.children.forEach((child, i) => {
180
+ out = insertNode(out, at.parentId, at.index + i, child);
181
+ });
182
+ return out;
183
+ }
184
+ function parseDesign(raw) {
185
+ let value = raw;
186
+ if (typeof raw === "string") {
187
+ try {
188
+ value = JSON.parse(raw);
189
+ } catch {
190
+ return emptyDesign("Unreadable design");
191
+ }
192
+ }
193
+ const v = value ?? {};
194
+ const d = v.design ?? v;
195
+ return {
196
+ designId: typeof d.designId === "string" ? d.designId : newId("d"),
197
+ name: typeof d.name === "string" ? d.name : "Untitled menu",
198
+ surface: MENU_SURFACES.includes(d.surface) ? d.surface : "anywhere",
199
+ look: { ...DEFAULT_LOOK, ...d.look ?? {} },
200
+ nodes: Array.isArray(d.nodes) ? d.nodes : [],
201
+ notes: typeof d.notes === "string" ? d.notes : ""
202
+ };
203
+ }
204
+ function serializeDesign(design) {
205
+ return JSON.stringify({ format: DESIGN_FORMAT, design });
206
+ }
207
+
208
+ // src/design/catalog.ts
209
+ var o = (id, label, icon, group, extra = {}) => ({ id, label, icon, group, status: "live", ...extra });
210
+ var CATALOG = [
211
+ // Clipboard — the verb strip
212
+ o("cm:copy", "Copy", "Copy", "Clipboard", { shortcut: "\u2318C" }),
213
+ o("cm:cut", "Cut", "Scissors", "Clipboard", { shortcut: "\u2318X" }),
214
+ o("cm:paste", "Paste", "ClipboardPaste", "Clipboard", { shortcut: "\u2318V" }),
215
+ o("cm:undo", "Undo", "Undo2", "Clipboard", { shortcut: "\u2318Z" }),
216
+ o("cm:redo", "Redo", "Redo2", "Clipboard", { shortcut: "\u21E7\u2318Z" }),
217
+ o("cm:find", "Find & Replace", "Search", "Clipboard", { shortcut: "\u2318F" }),
218
+ o("cm:select-all", "Select All", "TextSelect", "Clipboard", { shortcut: "\u2318A" }),
219
+ o("cm:copy-as", "Copy as\u2026", "CopyPlus", "Clipboard"),
220
+ o("cm:json", "Copy as JSON", "Braces", "Clipboard"),
221
+ o("copy-html-page", "Copy as HTML page", "FileCode", "Clipboard"),
222
+ o("cm:insert-reference", "Insert / Copy reference\u2026", "AtSign", "Clipboard"),
223
+ // Edit
224
+ o("cm:save", "Save", "Save", "Edit", { shortcut: "\u2318S" }),
225
+ o("duplicate", "Duplicate", "CopyPlus", "Edit"),
226
+ o("rename", "Rename", "PenLine", "Edit"),
227
+ o("edit", "Edit", "Pencil", "Edit"),
228
+ o("favorite", "Add to favorites", "Star", "Edit"),
229
+ o("archive", "Archive", "Archive", "Edit", { danger: true }),
230
+ o("cm:delete", "Delete", "Trash2", "Edit", { danger: true }),
231
+ // AI
232
+ o("cm:chat", "Chat about this", "MessageSquare", "AI", { ai: true }),
233
+ o("cm:quick-actions", "Quick Actions", "Zap", "AI", { ai: true }),
234
+ o("cm:placement:ai-action", "AI Actions", "Sparkles", "AI", { ai: true }),
235
+ o("cm:placement:content-block", "Content blocks", "Blocks", "AI", { ai: true }),
236
+ o("cm:placement:organization-tool", "Org Items", "Building2", "AI", { ai: true }),
237
+ o("cm:placement:user-tool", "My Items", "User", "AI", { ai: true }),
238
+ o("cm:placement:quick-action", "Quick actions (placement)", "Zap", "AI", { ai: true }),
239
+ o("cm:placement:bound-agent", "Agents", "Bot", "AI", { ai: true }),
240
+ o("send-to-agent", "Send to agent\u2026", "Send", "AI", { ai: true }),
241
+ o("continue-in-chat", "Continue in chat", "MessagesSquare", "AI", { ai: true }),
242
+ o("regenerate-response", "Regenerate", "RefreshCw", "AI", { ai: true }),
243
+ o("code-block-chart", "Make a chart", "ChartColumn", "AI", { ai: true }),
244
+ o("ai-improve", "Improve writing", "Wand2", "AI", { ai: true, status: "proposed" }),
245
+ o("ai-summarize", "Summarize", "ListCollapse", "AI", { ai: true, status: "proposed" }),
246
+ o("ai-explain", "Explain", "Lightbulb", "AI", { ai: true, status: "proposed" }),
247
+ o("ai-translate", "Translate\u2026", "Languages", "AI", { ai: true, status: "proposed" }),
248
+ // Save & export
249
+ o("cm:export", "Export", "FileOutput", "Save & export"),
250
+ o("cm:convert", "Convert to\u2026", "ArrowRightLeft", "Save & export"),
251
+ o("save-as-file", "Save as file", "FileDown", "Save & export"),
252
+ o("download-pdf", "Download PDF", "FileText", "Save & export"),
253
+ o("download-docx", "Download Word", "FileType", "Save & export"),
254
+ o("download-html", "Download HTML", "FileCode2", "Save & export"),
255
+ o("print", "Print", "Printer", "Save & export", { shortcut: "\u2318P" }),
256
+ // Share & publish
257
+ o("cm:share", "Share\u2026", "Share2", "Share & publish"),
258
+ o("share-link", "Copy share link", "Link", "Share & publish"),
259
+ o("send-google-doc", "Send to Google Docs", "FileUp", "Share & publish"),
260
+ o("email-to-me", "Email to me", "Mail", "Share & publish"),
261
+ o("html-preview", "Preview as web page", "Eye", "Share & publish"),
262
+ o("share-webpage", "Publish as web page", "Globe", "Share & publish"),
263
+ // Organize
264
+ o("cm:attach", "Attach to\u2026", "Paperclip", "Organize"),
265
+ o("set-context-value", "Set as context", "Pin", "Organize"),
266
+ o("pin-message", "Pin", "Pin", "Organize"),
267
+ o("notes-and-comments", "Notes & comments", "StickyNote", "Organize"),
268
+ o("move", "Move to folder\u2026", "FolderInput", "Organize"),
269
+ o("tags", "Tags\u2026", "Tags", "Organize"),
270
+ // History & compare
271
+ o("cm:view-history", "View history", "History", "History & compare"),
272
+ o("edit-history", "Edit history", "GitCommitVertical", "History & compare"),
273
+ o("fork-at-message", "Fork from here", "GitBranch", "History & compare"),
274
+ o("cm:compare", "Compare\u2026", "GitCompareArrows", "History & compare"),
275
+ o("compare-with-clipboard", "Compare with clipboard", "ClipboardList", "History & compare"),
276
+ o("set-compare-base", "Set as compare base", "Flag", "History & compare"),
277
+ // Listen
278
+ o("cm:speak", "Read aloud", "Volume2", "Listen"),
279
+ o("cm:listen", "Dictate", "Mic", "Listen"),
280
+ o("tts-voice-settings", "Voice settings", "AudioLines", "Listen"),
281
+ // Page rows: notes
282
+ o("notes:close-tab", "Close tab", "X", "Page: notes"),
283
+ o("notes:close-others", "Close other tabs", "PanelTopClose", "Page: notes"),
284
+ o("notes:export-md", "Export as Markdown", "FileDown", "Page: notes"),
285
+ o("notes:share-clipboard", "Copy to clipboard", "ClipboardCopy", "Page: notes"),
286
+ o("notes:convert-artifacts", "Convert to artifacts", "Shapes", "Page: notes"),
287
+ o("notes:trash", "Move to Trash", "Trash2", "Page: notes", { danger: true }),
288
+ // Page rows: a record (quiz row, table row)
289
+ o("row:open", "Open", "SquareArrowOutUpRight", "Page: record"),
290
+ o("row:open-tab", "Open in new tab", "ExternalLink", "Page: record"),
291
+ o("row:take", "Take", "Play", "Page: record"),
292
+ o("row:results", "Results", "ChartBar", "Page: record"),
293
+ o("row:manage", "Manage", "Settings2", "Page: record"),
294
+ o("row:restore", "Restore", "ArchiveRestore", "Page: record"),
295
+ // Page rows: chat message
296
+ o("regenerate-latest", "Regenerate latest", "RotateCcw", "Page: chat message", { ai: true }),
297
+ o("fork-and-regenerate", "Fork and regenerate", "GitFork", "Page: chat message", { ai: true }),
298
+ o("feedback-up", "Good response", "ThumbsUp", "Page: chat message"),
299
+ o("feedback-down", "Bad response", "ThumbsDown", "Page: chat message"),
300
+ // Tools & admin
301
+ o("cm:x:surface", "This page (location, context, agents)", "MapPin", "Tools & admin"),
302
+ o("cm:admin", "Admin tools", "Shield", "Tools & admin"),
303
+ o("feedback", "Send feedback", "MessageSquareWarning", "Tools & admin"),
304
+ o("keyboard-shortcuts", "Keyboard shortcuts", "Keyboard", "Tools & admin", { status: "proposed" })
305
+ ];
306
+ var CATALOG_GROUPS = [...new Set(CATALOG.map((c) => c.group))];
307
+ function catalogOption(id) {
308
+ return CATALOG.find((c) => c.id === id);
309
+ }
310
+ var seq = 0;
311
+ var tid = (p) => `${p}_t${Date.now().toString(36)}${(seq++).toString(36)}`;
312
+ function itemFrom(id) {
313
+ const c = catalogOption(id);
314
+ return {
315
+ id: tid("i"),
316
+ kind: "item",
317
+ catalogId: id,
318
+ label: c?.label ?? id,
319
+ icon: c?.icon ?? "Circle",
320
+ ...c?.shortcut ? { shortcut: c.shortcut } : {},
321
+ ...c?.danger ? { danger: true } : {},
322
+ ...c?.ai ? { ai: true } : {}
323
+ };
324
+ }
325
+ var strip = (label, ids) => ({
326
+ id: tid("s"),
327
+ kind: "section",
328
+ label,
329
+ layout: "row",
330
+ children: ids.map(itemFrom)
331
+ });
332
+ var sub = (label, icon, ids) => ({
333
+ id: tid("m"),
334
+ kind: "submenu",
335
+ label,
336
+ icon,
337
+ children: ids.map(itemFrom)
338
+ });
339
+ var TEMPLATES = [
340
+ { id: "strip-thing", label: "Icon strip \u2014 a thing", icon: "Rows2", build: () => strip("", ["cm:copy", "cm:share", "duplicate", "favorite", "save-as-file", "archive"]) },
341
+ { id: "strip-edit", label: "Icon strip \u2014 editable text", icon: "Rows2", build: () => strip("", ["cm:cut", "cm:copy", "cm:paste", "cm:undo", "cm:redo", "cm:find"]) },
342
+ { id: "strip-read", label: "Icon strip \u2014 read-only text", icon: "Rows2", build: () => strip("", ["cm:copy", "cm:speak", "cm:find", "cm:share"]) },
343
+ { id: "sub-intelligence", label: "Intelligence \u25B8", icon: "Sparkles", build: () => sub("Intelligence", "Sparkles", ["cm:chat", "cm:placement:ai-action", "cm:placement:bound-agent", "send-to-agent"]) },
344
+ { id: "sub-save-share", label: "Save & share \u25B8", icon: "Share2", build: () => sub("Save & share", "Share2", ["cm:copy-as", "save-as-file", "cm:convert", "share-webpage", "cm:share"]) },
345
+ { id: "sub-organize", label: "Organize \u25B8", icon: "Tags", build: () => sub("Organize", "Tags", ["cm:attach", "pin-message", "cm:view-history", "cm:compare"]) },
346
+ { id: "sub-tools", label: "Tools \u25B8", icon: "Wrench", build: () => sub("Tools", "Wrench", ["cm:speak", "cm:quick-actions", "feedback", "cm:x:surface", "cm:admin"]) }
347
+ ];
348
+ var SLOT_SOURCES = [
349
+ { id: "any", label: "Everything not placed", phrase: "Everything else", icon: "Ellipsis" },
350
+ { id: "agents", label: "Agents & shortcuts (library)", phrase: "All other agents", icon: "Bot", runtime: true },
351
+ { id: "page-rows", label: "This page's own rows", phrase: "All of this page's actions", icon: "PanelTop", runtime: true },
352
+ ...CATALOG_GROUPS.map((g) => ({
353
+ id: `group:${g}`,
354
+ label: g,
355
+ phrase: g === "AI" ? "All other AI items" : `All other ${g.toLowerCase()} options`,
356
+ icon: CATALOG.find((c) => c.group === g)?.icon ?? "Circle"
357
+ }))
358
+ ];
359
+ function slotSource(id) {
360
+ return SLOT_SOURCES.find((s) => s.id === id);
361
+ }
362
+ function slotOptions(fills, placed) {
363
+ if (fills === "agents" || fills === "page-rows") return [];
364
+ const pool = fills === "any" ? CATALOG : CATALOG.filter((c) => `group:${c.group}` === fills);
365
+ return pool.filter((c) => !placed.has(c.id));
366
+ }
367
+
368
+ // src/design/MenuPreview.tsx
369
+ var import_react = require("react");
370
+ var import_jsx_runtime = require("react/jsx-runtime");
371
+ function matches(node, q) {
372
+ if (!q) return true;
373
+ if (node.kind === "divider") return false;
374
+ if (node.kind === "item" || node.kind === "slot") return node.label.toLowerCase().includes(q);
375
+ return node.label.toLowerCase().includes(q) || node.children.some((c) => matches(c, q));
376
+ }
377
+ function flatten(nodes, q, out = []) {
378
+ for (const n of nodes) {
379
+ if (n.kind === "item" && matches(n, q)) out.push(n);
380
+ else if (isContainer(n)) flatten(n.children, q, out);
381
+ }
382
+ return out;
383
+ }
384
+ function MenuPreview({ design, renderIcon, highlightId, onSelectNode, onPick, className, style }) {
385
+ const [query, setQuery] = (0, import_react.useState)("");
386
+ const q = query.trim().toLowerCase();
387
+ const { look } = design;
388
+ const results = (0, import_react.useMemo)(() => q ? flatten(design.nodes, q) : null, [design.nodes, q]);
389
+ const placed = (0, import_react.useMemo)(() => placedCatalogIds(design.nodes), [design.nodes]);
390
+ const ctx = { design, renderIcon, highlightId, onSelectNode, onPick, placed };
391
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
392
+ "div",
393
+ {
394
+ role: "menu",
395
+ "data-density": look.density,
396
+ className: `cm-menu ${className ?? ""}`,
397
+ style: { width: look.width, ...style },
398
+ children: [
399
+ look.search && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "cm-search", children: [
400
+ renderIcon("Search", "cm-icon"),
401
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("input", { value: query, onChange: (e) => setQuery(e.target.value), placeholder: "Type to filter\u2026", "aria-label": "Filter menu" })
402
+ ] }),
403
+ results ? results.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "cm-empty", children: "No matches" }) : results.map((n) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Item, { node: n, ctx }, n.id)) : design.nodes.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "cm-empty", children: "Empty menu \u2014 drag options in" }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Nodes, { nodes: design.nodes, ctx })
404
+ ]
405
+ }
406
+ );
407
+ }
408
+ function Nodes({ nodes, ctx }) {
409
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_jsx_runtime.Fragment, { children: nodes.map((n) => {
410
+ if (n.kind === "divider") return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { role: "separator", className: "cm-divider" }, n.id);
411
+ if (n.kind === "item") return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Item, { node: n, ctx }, n.id);
412
+ if (n.kind === "submenu") return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Submenu, { node: n, ctx }, n.id);
413
+ if (n.kind === "slot") return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Slot, { node: n, ctx }, n.id);
414
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Section, { node: n, ctx }, n.id);
415
+ }) });
416
+ }
417
+ function Section({ node, ctx }) {
418
+ const [open, setOpen] = (0, import_react.useState)(!node.collapsible);
419
+ const layout = node.layout ?? "list";
420
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: `cm-section ${ctx.highlightId === node.id ? "cm-hl" : ""}`, "data-layout": layout, children: [
421
+ node.label && // ui-exception: a menu row of the menu component itself
422
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
423
+ "button",
424
+ {
425
+ type: "button",
426
+ className: "cm-section-label",
427
+ onClick: () => {
428
+ ctx.onSelectNode?.(node.id);
429
+ if (node.collapsible) setOpen((o2) => !o2);
430
+ },
431
+ children: [
432
+ node.label,
433
+ node.collapsible && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "cm-caret", children: open ? "\u25BE" : "\u25B8" })
434
+ ]
435
+ }
436
+ ),
437
+ open && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "cm-section-body", children: layout === "list" ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Nodes, { nodes: node.children, ctx }) : node.children.map(
438
+ (c) => c.kind === "item" ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Tile, { node: c, ctx }, c.id) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Nodes, { nodes: [c], ctx }, c.id)
439
+ ) })
440
+ ] });
441
+ }
442
+ function Tile({ node, ctx }) {
443
+ return (
444
+ // ui-exception: a menu row of the menu component itself
445
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
446
+ "button",
447
+ {
448
+ type: "button",
449
+ role: "menuitem",
450
+ title: node.label + (node.shortcut ? ` ${node.shortcut}` : ""),
451
+ disabled: node.disabled,
452
+ className: `cm-tile ${node.danger ? "cm-danger" : ""} ${node.ai ? "cm-ai" : ""} ${ctx.highlightId === node.id ? "cm-hl" : ""}`,
453
+ onClick: () => {
454
+ ctx.onSelectNode?.(node.id);
455
+ ctx.onPick?.(node);
456
+ },
457
+ children: ctx.renderIcon(node.icon ?? "Circle", "cm-icon")
458
+ }
459
+ )
460
+ );
461
+ }
462
+ function Item({ node, ctx }) {
463
+ const { look } = ctx.design;
464
+ const [checked, setChecked] = (0, import_react.useState)(false);
465
+ return (
466
+ // ui-exception: a menu row of the menu component itself
467
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
468
+ "button",
469
+ {
470
+ type: "button",
471
+ role: node.toggle ? "menuitemcheckbox" : "menuitem",
472
+ "aria-checked": node.toggle ? checked : void 0,
473
+ disabled: node.disabled,
474
+ className: `cm-item ${node.danger ? "cm-danger" : ""} ${node.ai ? "cm-ai" : ""} ${ctx.highlightId === node.id ? "cm-hl" : ""}`,
475
+ onClick: () => {
476
+ ctx.onSelectNode?.(node.id);
477
+ if (node.toggle) setChecked((c) => !c);
478
+ ctx.onPick?.(node);
479
+ },
480
+ children: [
481
+ look.showIcons && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "cm-icon-slot", children: node.toggle ? checked ? ctx.renderIcon("Check", "cm-icon") : null : node.icon ? ctx.renderIcon(node.icon, "cm-icon") : null }),
482
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: "cm-text", children: [
483
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "cm-label", children: node.label }),
484
+ look.showHints && node.hint && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "cm-hint", children: node.hint })
485
+ ] }),
486
+ look.showShortcuts && node.shortcut && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("kbd", { className: "cm-shortcut", children: node.shortcut })
487
+ ]
488
+ }
489
+ )
490
+ );
491
+ }
492
+ function Submenu({ node, ctx }) {
493
+ const { look } = ctx.design;
494
+ const [open, setOpen] = (0, import_react.useState)(false);
495
+ const [side, setSide] = (0, import_react.useState)("right");
496
+ const show = (el) => {
497
+ setSide(flyoutSide(el, look.width));
498
+ setOpen(true);
499
+ };
500
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "cm-sub", onMouseEnter: (e) => show(e.currentTarget), onMouseLeave: () => setOpen(false), children: [
501
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
502
+ "button",
503
+ {
504
+ type: "button",
505
+ role: "menuitem",
506
+ "aria-haspopup": "menu",
507
+ "aria-expanded": open,
508
+ className: `cm-item ${ctx.highlightId === node.id ? "cm-hl" : ""}`,
509
+ onClick: () => {
510
+ ctx.onSelectNode?.(node.id);
511
+ setOpen(true);
512
+ },
513
+ children: [
514
+ look.showIcons && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "cm-icon-slot", children: node.icon ? ctx.renderIcon(node.icon, "cm-icon") : null }),
515
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: "cm-text", children: [
516
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "cm-label", children: node.label }),
517
+ look.showHints && node.hint && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "cm-hint", children: node.hint })
518
+ ] }),
519
+ ctx.renderIcon("ChevronRight", "cm-icon cm-chev")
520
+ ]
521
+ }
522
+ ),
523
+ open && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { role: "menu", className: "cm-menu cm-flyout", "data-side": side, "data-density": look.density, style: { width: look.width }, children: node.children.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "cm-empty", children: "Empty submenu" }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Nodes, { nodes: node.children, ctx }) })
524
+ ] });
525
+ }
526
+ function flyoutSide(row, width) {
527
+ const r = row.getBoundingClientRect();
528
+ const bounds = row.closest("[data-menu-bounds]")?.getBoundingClientRect() ?? { left: 0, right: window.innerWidth };
529
+ if (r.right + width + 8 <= bounds.right) return "right";
530
+ if (r.left - width - 8 >= bounds.left) return "left";
531
+ return "below";
532
+ }
533
+ function Slot({ node, ctx }) {
534
+ const { look } = ctx.design;
535
+ const source = slotSource(node.fills);
536
+ const options = slotOptions(node.fills, ctx.placed);
537
+ const [open, setOpen] = (0, import_react.useState)(false);
538
+ const [side, setSide] = (0, import_react.useState)("right");
539
+ const count = source?.runtime ? "live" : String(options.length);
540
+ const row = (
541
+ // ui-exception: a menu row of the menu component itself
542
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
543
+ "button",
544
+ {
545
+ type: "button",
546
+ role: "menuitem",
547
+ "aria-haspopup": node.shape === "submenu" ? "menu" : void 0,
548
+ title: source ? `Placeholder: ${source.label}` : "Placeholder",
549
+ className: `cm-item cm-slot ${ctx.highlightId === node.id ? "cm-hl" : ""}`,
550
+ onClick: () => {
551
+ ctx.onSelectNode?.(node.id);
552
+ if (node.shape === "submenu") setOpen(true);
553
+ },
554
+ children: [
555
+ look.showIcons && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "cm-icon-slot", children: ctx.renderIcon(node.icon ?? source?.icon ?? "Ellipsis", "cm-icon") }),
556
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "cm-text", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "cm-label", children: node.label }) }),
557
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "cm-count", children: count }),
558
+ node.shape === "submenu" && ctx.renderIcon("ChevronRight", "cm-icon cm-chev")
559
+ ]
560
+ }
561
+ )
562
+ );
563
+ if (node.shape === "items") return row;
564
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
565
+ "div",
566
+ {
567
+ className: "cm-sub",
568
+ onMouseEnter: (e) => {
569
+ setSide(flyoutSide(e.currentTarget, look.width));
570
+ setOpen(true);
571
+ },
572
+ onMouseLeave: () => setOpen(false),
573
+ children: [
574
+ row,
575
+ open && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { role: "menu", className: "cm-menu cm-flyout", "data-side": side, "data-density": look.density, style: { width: look.width }, children: source?.runtime ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "cm-runtime", children: [
576
+ "Filled at runtime: ",
577
+ source.label
578
+ ] }) : options.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "cm-empty", children: "Nothing left \u2014 every option of this kind is placed" }) : options.map((o2) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Item, { node: { id: `slot-${node.id}-${o2.id}`, kind: "item", label: o2.label, icon: o2.icon, catalogId: o2.id, ...o2.shortcut ? { shortcut: o2.shortcut } : {}, ...o2.ai ? { ai: true } : {}, ...o2.danger ? { danger: true } : {} }, ctx }, o2.id)) })
579
+ ]
580
+ }
581
+ );
582
+ }
583
+ //# sourceMappingURL=index.cjs.map