@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.
- package/CHANGELOG.md +23 -0
- package/README.md +14 -1
- package/dist/builder.cjs +1810 -0
- package/dist/builder.cjs.map +1 -0
- package/dist/builder.d.cts +22 -0
- package/dist/builder.d.ts +22 -0
- package/dist/builder.js +1795 -0
- package/dist/builder.js.map +1 -0
- package/dist/design-store.cjs +195 -0
- package/dist/design-store.cjs.map +1 -0
- package/dist/design-store.d.cts +42 -0
- package/dist/design-store.d.ts +42 -0
- package/dist/design-store.js +175 -0
- package/dist/design-store.js.map +1 -0
- package/dist/engine.cjs +3069 -0
- package/dist/engine.cjs.map +1 -0
- package/dist/engine.d.cts +731 -0
- package/dist/engine.d.ts +731 -0
- package/dist/engine.js +3250 -0
- package/dist/engine.js.map +1 -0
- package/dist/grouping-CB3zlD1T.d.ts +623 -0
- package/dist/grouping-CGcWsltu.d.cts +623 -0
- package/dist/host-types-AOiEYwR8.d.cts +9 -0
- package/dist/host-types-AOiEYwR8.d.ts +9 -0
- package/dist/index.cjs +583 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +60 -0
- package/dist/index.d.ts +60 -0
- package/dist/index.js +561 -0
- package/dist/index.js.map +1 -0
- package/dist/menu-content.cjs +3146 -0
- package/dist/menu-content.cjs.map +1 -0
- package/dist/menu-content.d.cts +30 -0
- package/dist/menu-content.d.ts +30 -0
- package/dist/menu-content.js +3197 -0
- package/dist/menu-content.js.map +1 -0
- package/dist/model-CkLCR6WY.d.cts +123 -0
- package/dist/model-CkLCR6WY.d.ts +123 -0
- package/dist/react.cjs +3322 -0
- package/dist/react.cjs.map +1 -0
- package/dist/react.d.cts +133 -0
- package/dist/react.d.ts +133 -0
- package/dist/react.js +3345 -0
- package/dist/react.js.map +1 -0
- package/package.json +170 -1
package/dist/builder.js
ADDED
|
@@ -0,0 +1,1795 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// src/builder/MenuBuilder.tsx
|
|
4
|
+
import { createContext, useCallback, useContext, useEffect, useMemo as useMemo2, useRef, useState as useState2 } from "react";
|
|
5
|
+
|
|
6
|
+
// src/design/catalog.ts
|
|
7
|
+
var o = (id, label, icon, group, extra = {}) => ({ id, label, icon, group, status: "live", ...extra });
|
|
8
|
+
var CATALOG = [
|
|
9
|
+
// Clipboard — the verb strip
|
|
10
|
+
o("cm:copy", "Copy", "Copy", "Clipboard", { shortcut: "\u2318C" }),
|
|
11
|
+
o("cm:cut", "Cut", "Scissors", "Clipboard", { shortcut: "\u2318X" }),
|
|
12
|
+
o("cm:paste", "Paste", "ClipboardPaste", "Clipboard", { shortcut: "\u2318V" }),
|
|
13
|
+
o("cm:undo", "Undo", "Undo2", "Clipboard", { shortcut: "\u2318Z" }),
|
|
14
|
+
o("cm:redo", "Redo", "Redo2", "Clipboard", { shortcut: "\u21E7\u2318Z" }),
|
|
15
|
+
o("cm:find", "Find & Replace", "Search", "Clipboard", { shortcut: "\u2318F" }),
|
|
16
|
+
o("cm:select-all", "Select All", "TextSelect", "Clipboard", { shortcut: "\u2318A" }),
|
|
17
|
+
o("cm:copy-as", "Copy as\u2026", "CopyPlus", "Clipboard"),
|
|
18
|
+
o("cm:json", "Copy as JSON", "Braces", "Clipboard"),
|
|
19
|
+
o("copy-html-page", "Copy as HTML page", "FileCode", "Clipboard"),
|
|
20
|
+
o("cm:insert-reference", "Insert / Copy reference\u2026", "AtSign", "Clipboard"),
|
|
21
|
+
// Edit
|
|
22
|
+
o("cm:save", "Save", "Save", "Edit", { shortcut: "\u2318S" }),
|
|
23
|
+
o("duplicate", "Duplicate", "CopyPlus", "Edit"),
|
|
24
|
+
o("rename", "Rename", "PenLine", "Edit"),
|
|
25
|
+
o("edit", "Edit", "Pencil", "Edit"),
|
|
26
|
+
o("favorite", "Add to favorites", "Star", "Edit"),
|
|
27
|
+
o("archive", "Archive", "Archive", "Edit", { danger: true }),
|
|
28
|
+
o("cm:delete", "Delete", "Trash2", "Edit", { danger: true }),
|
|
29
|
+
// AI
|
|
30
|
+
o("cm:chat", "Chat about this", "MessageSquare", "AI", { ai: true }),
|
|
31
|
+
o("cm:quick-actions", "Quick Actions", "Zap", "AI", { ai: true }),
|
|
32
|
+
o("cm:placement:ai-action", "AI Actions", "Sparkles", "AI", { ai: true }),
|
|
33
|
+
o("cm:placement:content-block", "Content blocks", "Blocks", "AI", { ai: true }),
|
|
34
|
+
o("cm:placement:organization-tool", "Org Items", "Building2", "AI", { ai: true }),
|
|
35
|
+
o("cm:placement:user-tool", "My Items", "User", "AI", { ai: true }),
|
|
36
|
+
o("cm:placement:quick-action", "Quick actions (placement)", "Zap", "AI", { ai: true }),
|
|
37
|
+
o("cm:placement:bound-agent", "Agents", "Bot", "AI", { ai: true }),
|
|
38
|
+
o("send-to-agent", "Send to agent\u2026", "Send", "AI", { ai: true }),
|
|
39
|
+
o("continue-in-chat", "Continue in chat", "MessagesSquare", "AI", { ai: true }),
|
|
40
|
+
o("regenerate-response", "Regenerate", "RefreshCw", "AI", { ai: true }),
|
|
41
|
+
o("code-block-chart", "Make a chart", "ChartColumn", "AI", { ai: true }),
|
|
42
|
+
o("ai-improve", "Improve writing", "Wand2", "AI", { ai: true, status: "proposed" }),
|
|
43
|
+
o("ai-summarize", "Summarize", "ListCollapse", "AI", { ai: true, status: "proposed" }),
|
|
44
|
+
o("ai-explain", "Explain", "Lightbulb", "AI", { ai: true, status: "proposed" }),
|
|
45
|
+
o("ai-translate", "Translate\u2026", "Languages", "AI", { ai: true, status: "proposed" }),
|
|
46
|
+
// Save & export
|
|
47
|
+
o("cm:export", "Export", "FileOutput", "Save & export"),
|
|
48
|
+
o("cm:convert", "Convert to\u2026", "ArrowRightLeft", "Save & export"),
|
|
49
|
+
o("save-as-file", "Save as file", "FileDown", "Save & export"),
|
|
50
|
+
o("download-pdf", "Download PDF", "FileText", "Save & export"),
|
|
51
|
+
o("download-docx", "Download Word", "FileType", "Save & export"),
|
|
52
|
+
o("download-html", "Download HTML", "FileCode2", "Save & export"),
|
|
53
|
+
o("print", "Print", "Printer", "Save & export", { shortcut: "\u2318P" }),
|
|
54
|
+
// Share & publish
|
|
55
|
+
o("cm:share", "Share\u2026", "Share2", "Share & publish"),
|
|
56
|
+
o("share-link", "Copy share link", "Link", "Share & publish"),
|
|
57
|
+
o("send-google-doc", "Send to Google Docs", "FileUp", "Share & publish"),
|
|
58
|
+
o("email-to-me", "Email to me", "Mail", "Share & publish"),
|
|
59
|
+
o("html-preview", "Preview as web page", "Eye", "Share & publish"),
|
|
60
|
+
o("share-webpage", "Publish as web page", "Globe", "Share & publish"),
|
|
61
|
+
// Organize
|
|
62
|
+
o("cm:attach", "Attach to\u2026", "Paperclip", "Organize"),
|
|
63
|
+
o("set-context-value", "Set as context", "Pin", "Organize"),
|
|
64
|
+
o("pin-message", "Pin", "Pin", "Organize"),
|
|
65
|
+
o("notes-and-comments", "Notes & comments", "StickyNote", "Organize"),
|
|
66
|
+
o("move", "Move to folder\u2026", "FolderInput", "Organize"),
|
|
67
|
+
o("tags", "Tags\u2026", "Tags", "Organize"),
|
|
68
|
+
// History & compare
|
|
69
|
+
o("cm:view-history", "View history", "History", "History & compare"),
|
|
70
|
+
o("edit-history", "Edit history", "GitCommitVertical", "History & compare"),
|
|
71
|
+
o("fork-at-message", "Fork from here", "GitBranch", "History & compare"),
|
|
72
|
+
o("cm:compare", "Compare\u2026", "GitCompareArrows", "History & compare"),
|
|
73
|
+
o("compare-with-clipboard", "Compare with clipboard", "ClipboardList", "History & compare"),
|
|
74
|
+
o("set-compare-base", "Set as compare base", "Flag", "History & compare"),
|
|
75
|
+
// Listen
|
|
76
|
+
o("cm:speak", "Read aloud", "Volume2", "Listen"),
|
|
77
|
+
o("cm:listen", "Dictate", "Mic", "Listen"),
|
|
78
|
+
o("tts-voice-settings", "Voice settings", "AudioLines", "Listen"),
|
|
79
|
+
// Page rows: notes
|
|
80
|
+
o("notes:close-tab", "Close tab", "X", "Page: notes"),
|
|
81
|
+
o("notes:close-others", "Close other tabs", "PanelTopClose", "Page: notes"),
|
|
82
|
+
o("notes:export-md", "Export as Markdown", "FileDown", "Page: notes"),
|
|
83
|
+
o("notes:share-clipboard", "Copy to clipboard", "ClipboardCopy", "Page: notes"),
|
|
84
|
+
o("notes:convert-artifacts", "Convert to artifacts", "Shapes", "Page: notes"),
|
|
85
|
+
o("notes:trash", "Move to Trash", "Trash2", "Page: notes", { danger: true }),
|
|
86
|
+
// Page rows: a record (quiz row, table row)
|
|
87
|
+
o("row:open", "Open", "SquareArrowOutUpRight", "Page: record"),
|
|
88
|
+
o("row:open-tab", "Open in new tab", "ExternalLink", "Page: record"),
|
|
89
|
+
o("row:take", "Take", "Play", "Page: record"),
|
|
90
|
+
o("row:results", "Results", "ChartBar", "Page: record"),
|
|
91
|
+
o("row:manage", "Manage", "Settings2", "Page: record"),
|
|
92
|
+
o("row:restore", "Restore", "ArchiveRestore", "Page: record"),
|
|
93
|
+
// Page rows: chat message
|
|
94
|
+
o("regenerate-latest", "Regenerate latest", "RotateCcw", "Page: chat message", { ai: true }),
|
|
95
|
+
o("fork-and-regenerate", "Fork and regenerate", "GitFork", "Page: chat message", { ai: true }),
|
|
96
|
+
o("feedback-up", "Good response", "ThumbsUp", "Page: chat message"),
|
|
97
|
+
o("feedback-down", "Bad response", "ThumbsDown", "Page: chat message"),
|
|
98
|
+
// Tools & admin
|
|
99
|
+
o("cm:x:surface", "This page (location, context, agents)", "MapPin", "Tools & admin"),
|
|
100
|
+
o("cm:admin", "Admin tools", "Shield", "Tools & admin"),
|
|
101
|
+
o("feedback", "Send feedback", "MessageSquareWarning", "Tools & admin"),
|
|
102
|
+
o("keyboard-shortcuts", "Keyboard shortcuts", "Keyboard", "Tools & admin", { status: "proposed" })
|
|
103
|
+
];
|
|
104
|
+
var CATALOG_GROUPS = [...new Set(CATALOG.map((c) => c.group))];
|
|
105
|
+
function catalogOption(id) {
|
|
106
|
+
return CATALOG.find((c) => c.id === id);
|
|
107
|
+
}
|
|
108
|
+
var seq = 0;
|
|
109
|
+
var tid = (p) => `${p}_t${Date.now().toString(36)}${(seq++).toString(36)}`;
|
|
110
|
+
function itemFrom(id) {
|
|
111
|
+
const c = catalogOption(id);
|
|
112
|
+
return {
|
|
113
|
+
id: tid("i"),
|
|
114
|
+
kind: "item",
|
|
115
|
+
catalogId: id,
|
|
116
|
+
label: c?.label ?? id,
|
|
117
|
+
icon: c?.icon ?? "Circle",
|
|
118
|
+
...c?.shortcut ? { shortcut: c.shortcut } : {},
|
|
119
|
+
...c?.danger ? { danger: true } : {},
|
|
120
|
+
...c?.ai ? { ai: true } : {}
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
var strip = (label, ids) => ({
|
|
124
|
+
id: tid("s"),
|
|
125
|
+
kind: "section",
|
|
126
|
+
label,
|
|
127
|
+
layout: "row",
|
|
128
|
+
children: ids.map(itemFrom)
|
|
129
|
+
});
|
|
130
|
+
var sub = (label, icon, ids) => ({
|
|
131
|
+
id: tid("m"),
|
|
132
|
+
kind: "submenu",
|
|
133
|
+
label,
|
|
134
|
+
icon,
|
|
135
|
+
children: ids.map(itemFrom)
|
|
136
|
+
});
|
|
137
|
+
var TEMPLATES = [
|
|
138
|
+
{ id: "strip-thing", label: "Icon strip \u2014 a thing", icon: "Rows2", build: () => strip("", ["cm:copy", "cm:share", "duplicate", "favorite", "save-as-file", "archive"]) },
|
|
139
|
+
{ 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"]) },
|
|
140
|
+
{ id: "strip-read", label: "Icon strip \u2014 read-only text", icon: "Rows2", build: () => strip("", ["cm:copy", "cm:speak", "cm:find", "cm:share"]) },
|
|
141
|
+
{ 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"]) },
|
|
142
|
+
{ 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"]) },
|
|
143
|
+
{ id: "sub-organize", label: "Organize \u25B8", icon: "Tags", build: () => sub("Organize", "Tags", ["cm:attach", "pin-message", "cm:view-history", "cm:compare"]) },
|
|
144
|
+
{ id: "sub-tools", label: "Tools \u25B8", icon: "Wrench", build: () => sub("Tools", "Wrench", ["cm:speak", "cm:quick-actions", "feedback", "cm:x:surface", "cm:admin"]) }
|
|
145
|
+
];
|
|
146
|
+
var SLOT_SOURCES = [
|
|
147
|
+
{ id: "any", label: "Everything not placed", phrase: "Everything else", icon: "Ellipsis" },
|
|
148
|
+
{ id: "agents", label: "Agents & shortcuts (library)", phrase: "All other agents", icon: "Bot", runtime: true },
|
|
149
|
+
{ id: "page-rows", label: "This page's own rows", phrase: "All of this page's actions", icon: "PanelTop", runtime: true },
|
|
150
|
+
...CATALOG_GROUPS.map((g) => ({
|
|
151
|
+
id: `group:${g}`,
|
|
152
|
+
label: g,
|
|
153
|
+
phrase: g === "AI" ? "All other AI items" : `All other ${g.toLowerCase()} options`,
|
|
154
|
+
icon: CATALOG.find((c) => c.group === g)?.icon ?? "Circle"
|
|
155
|
+
}))
|
|
156
|
+
];
|
|
157
|
+
function slotSource(id) {
|
|
158
|
+
return SLOT_SOURCES.find((s) => s.id === id);
|
|
159
|
+
}
|
|
160
|
+
function slotOptions(fills, placed) {
|
|
161
|
+
if (fills === "agents" || fills === "page-rows") return [];
|
|
162
|
+
const pool = fills === "any" ? CATALOG : CATALOG.filter((c) => `group:${c.group}` === fills);
|
|
163
|
+
return pool.filter((c) => !placed.has(c.id));
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// src/design/MenuPreview.tsx
|
|
167
|
+
import { useMemo, useState } from "react";
|
|
168
|
+
|
|
169
|
+
// src/design/model.ts
|
|
170
|
+
var MENU_SURFACES = [
|
|
171
|
+
"text-selection",
|
|
172
|
+
"editable-field",
|
|
173
|
+
"table-row",
|
|
174
|
+
"page",
|
|
175
|
+
"chat-message",
|
|
176
|
+
"file",
|
|
177
|
+
"anywhere"
|
|
178
|
+
];
|
|
179
|
+
var DEFAULT_LOOK = {
|
|
180
|
+
density: "comfortable",
|
|
181
|
+
width: 280,
|
|
182
|
+
showIcons: true,
|
|
183
|
+
showShortcuts: true,
|
|
184
|
+
showHints: false,
|
|
185
|
+
search: false
|
|
186
|
+
};
|
|
187
|
+
var DESIGN_FORMAT = 1;
|
|
188
|
+
function newId(prefix = "n") {
|
|
189
|
+
const rnd = typeof crypto !== "undefined" && "randomUUID" in crypto ? crypto.randomUUID().slice(0, 8) : Math.random().toString(36).slice(2, 10);
|
|
190
|
+
return `${prefix}_${rnd}`;
|
|
191
|
+
}
|
|
192
|
+
function emptyDesign(name = "Untitled menu") {
|
|
193
|
+
return { designId: newId("d"), name, surface: "anywhere", look: { ...DEFAULT_LOOK }, nodes: [], notes: "" };
|
|
194
|
+
}
|
|
195
|
+
function isContainer(node) {
|
|
196
|
+
return node.kind === "section" || node.kind === "submenu";
|
|
197
|
+
}
|
|
198
|
+
function placedCatalogIds(nodes, out = /* @__PURE__ */ new Set()) {
|
|
199
|
+
for (const n of nodes) {
|
|
200
|
+
if (n.catalogId) out.add(n.catalogId);
|
|
201
|
+
if (isContainer(n)) placedCatalogIds(n.children, out);
|
|
202
|
+
}
|
|
203
|
+
return out;
|
|
204
|
+
}
|
|
205
|
+
function catalogUseCounts(nodes, out = /* @__PURE__ */ new Map()) {
|
|
206
|
+
for (const n of nodes) {
|
|
207
|
+
if (n.catalogId) out.set(n.catalogId, (out.get(n.catalogId) ?? 0) + 1);
|
|
208
|
+
if (isContainer(n)) catalogUseCounts(n.children, out);
|
|
209
|
+
}
|
|
210
|
+
return out;
|
|
211
|
+
}
|
|
212
|
+
function cloneNode(node) {
|
|
213
|
+
const copy = { ...node, id: newId(node.kind[0]) };
|
|
214
|
+
if (isContainer(copy)) copy.children = copy.children.map(cloneNode);
|
|
215
|
+
return copy;
|
|
216
|
+
}
|
|
217
|
+
function locate(nodes, id, parentId = null, depth = 0) {
|
|
218
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
219
|
+
const node = nodes[i];
|
|
220
|
+
if (node.id === id) return { node, parentId, index: i, depth };
|
|
221
|
+
if (isContainer(node)) {
|
|
222
|
+
const inner = locate(node.children, id, node.id, depth + 1);
|
|
223
|
+
if (inner) return inner;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
228
|
+
function contains(node, id) {
|
|
229
|
+
if (node.id === id) return true;
|
|
230
|
+
return isContainer(node) && node.children.some((c) => contains(c, id));
|
|
231
|
+
}
|
|
232
|
+
function countItems(nodes) {
|
|
233
|
+
let n = 0;
|
|
234
|
+
for (const node of nodes) {
|
|
235
|
+
if (node.kind === "item" || node.kind === "slot") n += 1;
|
|
236
|
+
else if (isContainer(node)) n += countItems(node.children) + (node.kind === "submenu" ? 1 : 0);
|
|
237
|
+
}
|
|
238
|
+
return n;
|
|
239
|
+
}
|
|
240
|
+
function mapChildren(nodes, parentId, fn) {
|
|
241
|
+
if (parentId === null) return fn([...nodes]);
|
|
242
|
+
return nodes.map((node) => {
|
|
243
|
+
if (!isContainer(node)) return node;
|
|
244
|
+
if (node.id === parentId) return { ...node, children: fn([...node.children]) };
|
|
245
|
+
return { ...node, children: mapChildren(node.children, parentId, fn) };
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
function insertNode(nodes, parentId, index, node) {
|
|
249
|
+
return mapChildren(nodes, parentId, (list) => {
|
|
250
|
+
list.splice(Math.max(0, Math.min(index, list.length)), 0, node);
|
|
251
|
+
return list;
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
function removeNode(nodes, id) {
|
|
255
|
+
return nodes.filter((n) => n.id !== id).map((n) => isContainer(n) ? { ...n, children: removeNode(n.children, id) } : n);
|
|
256
|
+
}
|
|
257
|
+
function updateNode(nodes, id, patch) {
|
|
258
|
+
return nodes.map((n) => {
|
|
259
|
+
if (n.id === id) return { ...n, ...patch, id: n.id, kind: n.kind };
|
|
260
|
+
if (isContainer(n)) return { ...n, children: updateNode(n.children, id, patch) };
|
|
261
|
+
return n;
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
function canMove(nodes, id, parentId) {
|
|
265
|
+
const from = locate(nodes, id);
|
|
266
|
+
return Boolean(from) && (parentId === null || !contains(from.node, parentId));
|
|
267
|
+
}
|
|
268
|
+
function moveNode(nodes, id, parentId, index) {
|
|
269
|
+
const from = locate(nodes, id);
|
|
270
|
+
if (!from) return nodes;
|
|
271
|
+
if (parentId !== null && contains(from.node, parentId)) return nodes;
|
|
272
|
+
let target = index;
|
|
273
|
+
if (from.parentId === parentId && from.index < index) target -= 1;
|
|
274
|
+
return insertNode(removeNode(nodes, id), parentId, target, from.node);
|
|
275
|
+
}
|
|
276
|
+
function duplicateNode(nodes, id) {
|
|
277
|
+
const at = locate(nodes, id);
|
|
278
|
+
if (!at) return [...nodes];
|
|
279
|
+
return insertNode(nodes, at.parentId, at.index + 1, cloneNode(at.node));
|
|
280
|
+
}
|
|
281
|
+
function wrapNode(nodes, id, kind) {
|
|
282
|
+
const at = locate(nodes, id);
|
|
283
|
+
if (!at) return [...nodes];
|
|
284
|
+
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] };
|
|
285
|
+
return insertNode(removeNode(nodes, id), at.parentId, at.index, wrapper);
|
|
286
|
+
}
|
|
287
|
+
function unwrapNode(nodes, id) {
|
|
288
|
+
const at = locate(nodes, id);
|
|
289
|
+
if (!at || !isContainer(at.node)) return [...nodes];
|
|
290
|
+
let out = removeNode(nodes, id);
|
|
291
|
+
at.node.children.forEach((child, i) => {
|
|
292
|
+
out = insertNode(out, at.parentId, at.index + i, child);
|
|
293
|
+
});
|
|
294
|
+
return out;
|
|
295
|
+
}
|
|
296
|
+
function parseDesign(raw) {
|
|
297
|
+
let value = raw;
|
|
298
|
+
if (typeof raw === "string") {
|
|
299
|
+
try {
|
|
300
|
+
value = JSON.parse(raw);
|
|
301
|
+
} catch {
|
|
302
|
+
return emptyDesign("Unreadable design");
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
const v = value ?? {};
|
|
306
|
+
const d = v.design ?? v;
|
|
307
|
+
return {
|
|
308
|
+
designId: typeof d.designId === "string" ? d.designId : newId("d"),
|
|
309
|
+
name: typeof d.name === "string" ? d.name : "Untitled menu",
|
|
310
|
+
surface: MENU_SURFACES.includes(d.surface) ? d.surface : "anywhere",
|
|
311
|
+
look: { ...DEFAULT_LOOK, ...d.look ?? {} },
|
|
312
|
+
nodes: Array.isArray(d.nodes) ? d.nodes : [],
|
|
313
|
+
notes: typeof d.notes === "string" ? d.notes : ""
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
function serializeDesign(design2) {
|
|
317
|
+
return JSON.stringify({ format: DESIGN_FORMAT, design: design2 });
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// src/design/MenuPreview.tsx
|
|
321
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
322
|
+
function matches(node, q) {
|
|
323
|
+
if (!q) return true;
|
|
324
|
+
if (node.kind === "divider") return false;
|
|
325
|
+
if (node.kind === "item" || node.kind === "slot") return node.label.toLowerCase().includes(q);
|
|
326
|
+
return node.label.toLowerCase().includes(q) || node.children.some((c) => matches(c, q));
|
|
327
|
+
}
|
|
328
|
+
function flatten(nodes, q, out = []) {
|
|
329
|
+
for (const n of nodes) {
|
|
330
|
+
if (n.kind === "item" && matches(n, q)) out.push(n);
|
|
331
|
+
else if (isContainer(n)) flatten(n.children, q, out);
|
|
332
|
+
}
|
|
333
|
+
return out;
|
|
334
|
+
}
|
|
335
|
+
function MenuPreview({ design: design2, renderIcon, highlightId, onSelectNode, onPick, className, style }) {
|
|
336
|
+
const [query, setQuery] = useState("");
|
|
337
|
+
const q = query.trim().toLowerCase();
|
|
338
|
+
const { look } = design2;
|
|
339
|
+
const results = useMemo(() => q ? flatten(design2.nodes, q) : null, [design2.nodes, q]);
|
|
340
|
+
const placed = useMemo(() => placedCatalogIds(design2.nodes), [design2.nodes]);
|
|
341
|
+
const ctx = { design: design2, renderIcon, highlightId, onSelectNode, onPick, placed };
|
|
342
|
+
return /* @__PURE__ */ jsxs(
|
|
343
|
+
"div",
|
|
344
|
+
{
|
|
345
|
+
role: "menu",
|
|
346
|
+
"data-density": look.density,
|
|
347
|
+
className: `cm-menu ${className ?? ""}`,
|
|
348
|
+
style: { width: look.width, ...style },
|
|
349
|
+
children: [
|
|
350
|
+
look.search && /* @__PURE__ */ jsxs("div", { className: "cm-search", children: [
|
|
351
|
+
renderIcon("Search", "cm-icon"),
|
|
352
|
+
/* @__PURE__ */ jsx("input", { value: query, onChange: (e) => setQuery(e.target.value), placeholder: "Type to filter\u2026", "aria-label": "Filter menu" })
|
|
353
|
+
] }),
|
|
354
|
+
results ? results.length === 0 ? /* @__PURE__ */ jsx("div", { className: "cm-empty", children: "No matches" }) : results.map((n) => /* @__PURE__ */ jsx(Item, { node: n, ctx }, n.id)) : design2.nodes.length === 0 ? /* @__PURE__ */ jsx("div", { className: "cm-empty", children: "Empty menu \u2014 drag options in" }) : /* @__PURE__ */ jsx(Nodes, { nodes: design2.nodes, ctx })
|
|
355
|
+
]
|
|
356
|
+
}
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
function Nodes({ nodes, ctx }) {
|
|
360
|
+
return /* @__PURE__ */ jsx(Fragment, { children: nodes.map((n) => {
|
|
361
|
+
if (n.kind === "divider") return /* @__PURE__ */ jsx("div", { role: "separator", className: "cm-divider" }, n.id);
|
|
362
|
+
if (n.kind === "item") return /* @__PURE__ */ jsx(Item, { node: n, ctx }, n.id);
|
|
363
|
+
if (n.kind === "submenu") return /* @__PURE__ */ jsx(Submenu, { node: n, ctx }, n.id);
|
|
364
|
+
if (n.kind === "slot") return /* @__PURE__ */ jsx(Slot, { node: n, ctx }, n.id);
|
|
365
|
+
return /* @__PURE__ */ jsx(Section, { node: n, ctx }, n.id);
|
|
366
|
+
}) });
|
|
367
|
+
}
|
|
368
|
+
function Section({ node, ctx }) {
|
|
369
|
+
const [open, setOpen] = useState(!node.collapsible);
|
|
370
|
+
const layout = node.layout ?? "list";
|
|
371
|
+
return /* @__PURE__ */ jsxs("div", { className: `cm-section ${ctx.highlightId === node.id ? "cm-hl" : ""}`, "data-layout": layout, children: [
|
|
372
|
+
node.label && // ui-exception: a menu row of the menu component itself
|
|
373
|
+
/* @__PURE__ */ jsxs(
|
|
374
|
+
"button",
|
|
375
|
+
{
|
|
376
|
+
type: "button",
|
|
377
|
+
className: "cm-section-label",
|
|
378
|
+
onClick: () => {
|
|
379
|
+
ctx.onSelectNode?.(node.id);
|
|
380
|
+
if (node.collapsible) setOpen((o2) => !o2);
|
|
381
|
+
},
|
|
382
|
+
children: [
|
|
383
|
+
node.label,
|
|
384
|
+
node.collapsible && /* @__PURE__ */ jsx("span", { className: "cm-caret", children: open ? "\u25BE" : "\u25B8" })
|
|
385
|
+
]
|
|
386
|
+
}
|
|
387
|
+
),
|
|
388
|
+
open && /* @__PURE__ */ jsx("div", { className: "cm-section-body", children: layout === "list" ? /* @__PURE__ */ jsx(Nodes, { nodes: node.children, ctx }) : node.children.map(
|
|
389
|
+
(c) => c.kind === "item" ? /* @__PURE__ */ jsx(Tile, { node: c, ctx }, c.id) : /* @__PURE__ */ jsx(Nodes, { nodes: [c], ctx }, c.id)
|
|
390
|
+
) })
|
|
391
|
+
] });
|
|
392
|
+
}
|
|
393
|
+
function Tile({ node, ctx }) {
|
|
394
|
+
return (
|
|
395
|
+
// ui-exception: a menu row of the menu component itself
|
|
396
|
+
/* @__PURE__ */ jsx(
|
|
397
|
+
"button",
|
|
398
|
+
{
|
|
399
|
+
type: "button",
|
|
400
|
+
role: "menuitem",
|
|
401
|
+
title: node.label + (node.shortcut ? ` ${node.shortcut}` : ""),
|
|
402
|
+
disabled: node.disabled,
|
|
403
|
+
className: `cm-tile ${node.danger ? "cm-danger" : ""} ${node.ai ? "cm-ai" : ""} ${ctx.highlightId === node.id ? "cm-hl" : ""}`,
|
|
404
|
+
onClick: () => {
|
|
405
|
+
ctx.onSelectNode?.(node.id);
|
|
406
|
+
ctx.onPick?.(node);
|
|
407
|
+
},
|
|
408
|
+
children: ctx.renderIcon(node.icon ?? "Circle", "cm-icon")
|
|
409
|
+
}
|
|
410
|
+
)
|
|
411
|
+
);
|
|
412
|
+
}
|
|
413
|
+
function Item({ node, ctx }) {
|
|
414
|
+
const { look } = ctx.design;
|
|
415
|
+
const [checked, setChecked] = useState(false);
|
|
416
|
+
return (
|
|
417
|
+
// ui-exception: a menu row of the menu component itself
|
|
418
|
+
/* @__PURE__ */ jsxs(
|
|
419
|
+
"button",
|
|
420
|
+
{
|
|
421
|
+
type: "button",
|
|
422
|
+
role: node.toggle ? "menuitemcheckbox" : "menuitem",
|
|
423
|
+
"aria-checked": node.toggle ? checked : void 0,
|
|
424
|
+
disabled: node.disabled,
|
|
425
|
+
className: `cm-item ${node.danger ? "cm-danger" : ""} ${node.ai ? "cm-ai" : ""} ${ctx.highlightId === node.id ? "cm-hl" : ""}`,
|
|
426
|
+
onClick: () => {
|
|
427
|
+
ctx.onSelectNode?.(node.id);
|
|
428
|
+
if (node.toggle) setChecked((c) => !c);
|
|
429
|
+
ctx.onPick?.(node);
|
|
430
|
+
},
|
|
431
|
+
children: [
|
|
432
|
+
look.showIcons && /* @__PURE__ */ 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 }),
|
|
433
|
+
/* @__PURE__ */ jsxs("span", { className: "cm-text", children: [
|
|
434
|
+
/* @__PURE__ */ jsx("span", { className: "cm-label", children: node.label }),
|
|
435
|
+
look.showHints && node.hint && /* @__PURE__ */ jsx("span", { className: "cm-hint", children: node.hint })
|
|
436
|
+
] }),
|
|
437
|
+
look.showShortcuts && node.shortcut && /* @__PURE__ */ jsx("kbd", { className: "cm-shortcut", children: node.shortcut })
|
|
438
|
+
]
|
|
439
|
+
}
|
|
440
|
+
)
|
|
441
|
+
);
|
|
442
|
+
}
|
|
443
|
+
function Submenu({ node, ctx }) {
|
|
444
|
+
const { look } = ctx.design;
|
|
445
|
+
const [open, setOpen] = useState(false);
|
|
446
|
+
const [side, setSide] = useState("right");
|
|
447
|
+
const show = (el) => {
|
|
448
|
+
setSide(flyoutSide(el, look.width));
|
|
449
|
+
setOpen(true);
|
|
450
|
+
};
|
|
451
|
+
return /* @__PURE__ */ jsxs("div", { className: "cm-sub", onMouseEnter: (e) => show(e.currentTarget), onMouseLeave: () => setOpen(false), children: [
|
|
452
|
+
/* @__PURE__ */ jsxs(
|
|
453
|
+
"button",
|
|
454
|
+
{
|
|
455
|
+
type: "button",
|
|
456
|
+
role: "menuitem",
|
|
457
|
+
"aria-haspopup": "menu",
|
|
458
|
+
"aria-expanded": open,
|
|
459
|
+
className: `cm-item ${ctx.highlightId === node.id ? "cm-hl" : ""}`,
|
|
460
|
+
onClick: () => {
|
|
461
|
+
ctx.onSelectNode?.(node.id);
|
|
462
|
+
setOpen(true);
|
|
463
|
+
},
|
|
464
|
+
children: [
|
|
465
|
+
look.showIcons && /* @__PURE__ */ jsx("span", { className: "cm-icon-slot", children: node.icon ? ctx.renderIcon(node.icon, "cm-icon") : null }),
|
|
466
|
+
/* @__PURE__ */ jsxs("span", { className: "cm-text", children: [
|
|
467
|
+
/* @__PURE__ */ jsx("span", { className: "cm-label", children: node.label }),
|
|
468
|
+
look.showHints && node.hint && /* @__PURE__ */ jsx("span", { className: "cm-hint", children: node.hint })
|
|
469
|
+
] }),
|
|
470
|
+
ctx.renderIcon("ChevronRight", "cm-icon cm-chev")
|
|
471
|
+
]
|
|
472
|
+
}
|
|
473
|
+
),
|
|
474
|
+
open && /* @__PURE__ */ 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__ */ jsx("div", { className: "cm-empty", children: "Empty submenu" }) : /* @__PURE__ */ jsx(Nodes, { nodes: node.children, ctx }) })
|
|
475
|
+
] });
|
|
476
|
+
}
|
|
477
|
+
function flyoutSide(row, width) {
|
|
478
|
+
const r = row.getBoundingClientRect();
|
|
479
|
+
const bounds = row.closest("[data-menu-bounds]")?.getBoundingClientRect() ?? { left: 0, right: window.innerWidth };
|
|
480
|
+
if (r.right + width + 8 <= bounds.right) return "right";
|
|
481
|
+
if (r.left - width - 8 >= bounds.left) return "left";
|
|
482
|
+
return "below";
|
|
483
|
+
}
|
|
484
|
+
function Slot({ node, ctx }) {
|
|
485
|
+
const { look } = ctx.design;
|
|
486
|
+
const source = slotSource(node.fills);
|
|
487
|
+
const options = slotOptions(node.fills, ctx.placed);
|
|
488
|
+
const [open, setOpen] = useState(false);
|
|
489
|
+
const [side, setSide] = useState("right");
|
|
490
|
+
const count = source?.runtime ? "live" : String(options.length);
|
|
491
|
+
const row = (
|
|
492
|
+
// ui-exception: a menu row of the menu component itself
|
|
493
|
+
/* @__PURE__ */ jsxs(
|
|
494
|
+
"button",
|
|
495
|
+
{
|
|
496
|
+
type: "button",
|
|
497
|
+
role: "menuitem",
|
|
498
|
+
"aria-haspopup": node.shape === "submenu" ? "menu" : void 0,
|
|
499
|
+
title: source ? `Placeholder: ${source.label}` : "Placeholder",
|
|
500
|
+
className: `cm-item cm-slot ${ctx.highlightId === node.id ? "cm-hl" : ""}`,
|
|
501
|
+
onClick: () => {
|
|
502
|
+
ctx.onSelectNode?.(node.id);
|
|
503
|
+
if (node.shape === "submenu") setOpen(true);
|
|
504
|
+
},
|
|
505
|
+
children: [
|
|
506
|
+
look.showIcons && /* @__PURE__ */ jsx("span", { className: "cm-icon-slot", children: ctx.renderIcon(node.icon ?? source?.icon ?? "Ellipsis", "cm-icon") }),
|
|
507
|
+
/* @__PURE__ */ jsx("span", { className: "cm-text", children: /* @__PURE__ */ jsx("span", { className: "cm-label", children: node.label }) }),
|
|
508
|
+
/* @__PURE__ */ jsx("span", { className: "cm-count", children: count }),
|
|
509
|
+
node.shape === "submenu" && ctx.renderIcon("ChevronRight", "cm-icon cm-chev")
|
|
510
|
+
]
|
|
511
|
+
}
|
|
512
|
+
)
|
|
513
|
+
);
|
|
514
|
+
if (node.shape === "items") return row;
|
|
515
|
+
return /* @__PURE__ */ jsxs(
|
|
516
|
+
"div",
|
|
517
|
+
{
|
|
518
|
+
className: "cm-sub",
|
|
519
|
+
onMouseEnter: (e) => {
|
|
520
|
+
setSide(flyoutSide(e.currentTarget, look.width));
|
|
521
|
+
setOpen(true);
|
|
522
|
+
},
|
|
523
|
+
onMouseLeave: () => setOpen(false),
|
|
524
|
+
children: [
|
|
525
|
+
row,
|
|
526
|
+
open && /* @__PURE__ */ jsx("div", { role: "menu", className: "cm-menu cm-flyout", "data-side": side, "data-density": look.density, style: { width: look.width }, children: source?.runtime ? /* @__PURE__ */ jsxs("div", { className: "cm-runtime", children: [
|
|
527
|
+
"Filled at runtime: ",
|
|
528
|
+
source.label
|
|
529
|
+
] }) : options.length === 0 ? /* @__PURE__ */ jsx("div", { className: "cm-empty", children: "Nothing left \u2014 every option of this kind is placed" }) : options.map((o2) => /* @__PURE__ */ 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)) })
|
|
530
|
+
]
|
|
531
|
+
}
|
|
532
|
+
);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// src/design/starters.ts
|
|
536
|
+
function maker(slug) {
|
|
537
|
+
let n = 0;
|
|
538
|
+
const nid = () => `${slug}-${++n}`;
|
|
539
|
+
const clean = (o2) => JSON.parse(JSON.stringify(o2));
|
|
540
|
+
const item = (catalogId, extra = {}) => {
|
|
541
|
+
const c = catalogOption(catalogId);
|
|
542
|
+
if (!c) throw new Error(`starters: no catalog row ${catalogId}`);
|
|
543
|
+
return clean({
|
|
544
|
+
id: nid(),
|
|
545
|
+
kind: "item",
|
|
546
|
+
catalogId,
|
|
547
|
+
label: c.label,
|
|
548
|
+
icon: c.icon,
|
|
549
|
+
...c.shortcut ? { shortcut: c.shortcut } : {},
|
|
550
|
+
...c.danger ? { danger: true } : {},
|
|
551
|
+
...c.ai ? { ai: true } : {},
|
|
552
|
+
...extra
|
|
553
|
+
});
|
|
554
|
+
};
|
|
555
|
+
const own = (label, icon, extra = {}) => clean({ id: nid(), kind: "item", label, icon, ...extra });
|
|
556
|
+
const section = (label, children, layout = "list") => ({
|
|
557
|
+
id: nid(),
|
|
558
|
+
kind: "section",
|
|
559
|
+
label,
|
|
560
|
+
layout,
|
|
561
|
+
children
|
|
562
|
+
});
|
|
563
|
+
const strip2 = (children) => section("", children, "row");
|
|
564
|
+
const sub2 = (label, icon, children, extra = {}) => clean({ id: nid(), kind: "submenu", label, icon, ...extra, children });
|
|
565
|
+
const divider = () => ({ id: nid(), kind: "divider" });
|
|
566
|
+
return { item, own, section, strip: strip2, sub: sub2, divider };
|
|
567
|
+
}
|
|
568
|
+
function design(slug, name, surface, notes, nodes, look = {}) {
|
|
569
|
+
return { designId: `starter-${slug}`, name, surface, look: { ...DEFAULT_LOOK, ...look }, nodes, notes };
|
|
570
|
+
}
|
|
571
|
+
var RUNTIME = "Filled from the agent library at runtime.";
|
|
572
|
+
function aiPlacements(m) {
|
|
573
|
+
return [
|
|
574
|
+
m.item("cm:placement:ai-action", {
|
|
575
|
+
note: `${RUNTIME} Live: a submenu of AI actions (agent shortcuts) that fit the selection.`
|
|
576
|
+
}),
|
|
577
|
+
m.item("cm:placement:content-block", { note: "Filled at runtime with the content blocks that can be inserted here." }),
|
|
578
|
+
m.item("cm:placement:user-tool", { note: "Filled at runtime with this person's own shortcuts." }),
|
|
579
|
+
m.item("cm:placement:organization-tool", { note: "Filled at runtime with the organization's shared shortcuts." }),
|
|
580
|
+
m.item("cm:placement:bound-agent", { note: "Filled at runtime with the agents bound to this page." })
|
|
581
|
+
];
|
|
582
|
+
}
|
|
583
|
+
function intelligence(m) {
|
|
584
|
+
return m.sub("Intelligence", "Sparkles", [
|
|
585
|
+
m.item("cm:chat"),
|
|
586
|
+
m.item("cm:placement:ai-action", { note: RUNTIME }),
|
|
587
|
+
m.item("cm:placement:bound-agent", { note: RUNTIME }),
|
|
588
|
+
m.item("cm:placement:user-tool", { note: RUNTIME }),
|
|
589
|
+
m.item("cm:placement:organization-tool", { note: RUNTIME }),
|
|
590
|
+
m.item("cm:placement:content-block", { note: RUNTIME })
|
|
591
|
+
], { note: "Every AI entry in one row, on every menu." });
|
|
592
|
+
}
|
|
593
|
+
function saveShare(m) {
|
|
594
|
+
return m.sub("Save & share", "Share2", [
|
|
595
|
+
m.item("cm:copy-as", { note: "Copy block: every form the content can land on the clipboard in." }),
|
|
596
|
+
m.divider(),
|
|
597
|
+
m.item("cm:export", { label: "Download", icon: "Download", note: "Download block: PDF, Word, HTML, Markdown, print." }),
|
|
598
|
+
m.divider(),
|
|
599
|
+
m.item("cm:convert", { label: "Convert to\u2026", note: "Convert block: becomes a new thing in the app (note, task, document\u2026)." }),
|
|
600
|
+
m.divider(),
|
|
601
|
+
m.item("share-webpage", { label: "Publish", icon: "Globe", note: "Publish block: public web page." }),
|
|
602
|
+
m.divider(),
|
|
603
|
+
m.item("cm:share", { label: "Share", note: "Share block: people and places. Copy link folds into this dialog." })
|
|
604
|
+
]);
|
|
605
|
+
}
|
|
606
|
+
function organize(m) {
|
|
607
|
+
return m.sub("Organize", "FolderTree", [
|
|
608
|
+
m.item("cm:attach"),
|
|
609
|
+
m.item("pin-message", { label: "Pin" }),
|
|
610
|
+
m.item("cm:view-history", { label: "History" }),
|
|
611
|
+
m.item("cm:compare", { label: "Compare" })
|
|
612
|
+
]);
|
|
613
|
+
}
|
|
614
|
+
function tools(m) {
|
|
615
|
+
return m.sub("Tools", "Wrench", [
|
|
616
|
+
m.item("cm:speak", { label: "Read aloud" }),
|
|
617
|
+
m.item("cm:quick-actions"),
|
|
618
|
+
m.item("feedback", { label: "Feedback" }),
|
|
619
|
+
m.item("cm:x:surface", { label: "This page" }),
|
|
620
|
+
m.item("cm:admin", { label: "Admin", note: "Admins only." })
|
|
621
|
+
]);
|
|
622
|
+
}
|
|
623
|
+
var TIER_STRIP = (m) => m.strip([m.item("cm:copy"), m.item("cm:cut"), m.item("cm:paste"), m.item("cm:undo"), m.item("cm:redo"), m.item("cm:find")]);
|
|
624
|
+
function liveEditableField() {
|
|
625
|
+
const m = maker("live-editable-field");
|
|
626
|
+
return design(
|
|
627
|
+
"live-editable-field",
|
|
628
|
+
"Editable text field (selection in a textarea)",
|
|
629
|
+
"editable-field",
|
|
630
|
+
"Live today: the right-click menu on an editable field. Source: engine/ContextMenuV3, command layout (platform default since 2026-08-22).",
|
|
631
|
+
[
|
|
632
|
+
TIER_STRIP(m),
|
|
633
|
+
m.item("cm:select-all"),
|
|
634
|
+
m.divider(),
|
|
635
|
+
m.item("cm:chat"),
|
|
636
|
+
...aiPlacements(m),
|
|
637
|
+
m.item("cm:quick-actions", { note: "A submenu of quick tools." }),
|
|
638
|
+
m.divider(),
|
|
639
|
+
m.item("cm:listen"),
|
|
640
|
+
m.item("cm:speak"),
|
|
641
|
+
m.item("cm:copy-as"),
|
|
642
|
+
m.item("cm:insert-reference"),
|
|
643
|
+
m.divider(),
|
|
644
|
+
m.sub("History", "History", [m.item("cm:undo"), m.item("cm:redo"), m.item("cm:view-history"), m.item("cm:compare")], {
|
|
645
|
+
note: "In the tiered and command layouts History groups these four."
|
|
646
|
+
}),
|
|
647
|
+
m.item("cm:export"),
|
|
648
|
+
m.item("cm:convert"),
|
|
649
|
+
m.item("cm:share"),
|
|
650
|
+
m.item("cm:attach"),
|
|
651
|
+
m.divider(),
|
|
652
|
+
m.item("cm:x:surface"),
|
|
653
|
+
m.item("cm:admin", { note: "Admins only." })
|
|
654
|
+
],
|
|
655
|
+
{ search: true }
|
|
656
|
+
);
|
|
657
|
+
}
|
|
658
|
+
function liveReadOnlyMessage() {
|
|
659
|
+
const m = maker("live-chat-message");
|
|
660
|
+
return design(
|
|
661
|
+
"live-chat-message",
|
|
662
|
+
"Read-only text / chat message",
|
|
663
|
+
"chat-message",
|
|
664
|
+
"Live today: right-click on read-only text and on a chat message. Source: engine/NonEditableContextMenu plus the chat message rows.",
|
|
665
|
+
[
|
|
666
|
+
m.strip([m.item("cm:copy"), m.item("cm:find"), m.item("cm:speak", { label: "Read aloud" })]),
|
|
667
|
+
m.item("cm:select-all"),
|
|
668
|
+
m.divider(),
|
|
669
|
+
m.section("Message", [
|
|
670
|
+
m.item("regenerate-response", { note: "Assistant messages only." }),
|
|
671
|
+
m.item("fork-at-message"),
|
|
672
|
+
m.item("continue-in-chat"),
|
|
673
|
+
m.item("pin-message"),
|
|
674
|
+
m.item("feedback-up"),
|
|
675
|
+
m.item("feedback-down")
|
|
676
|
+
]),
|
|
677
|
+
m.divider(),
|
|
678
|
+
m.item("cm:chat"),
|
|
679
|
+
...aiPlacements(m),
|
|
680
|
+
m.item("cm:quick-actions"),
|
|
681
|
+
m.divider(),
|
|
682
|
+
m.item("cm:copy-as"),
|
|
683
|
+
m.item("cm:json"),
|
|
684
|
+
m.item("cm:export"),
|
|
685
|
+
m.item("cm:convert"),
|
|
686
|
+
m.item("cm:share"),
|
|
687
|
+
m.item("cm:attach"),
|
|
688
|
+
m.divider(),
|
|
689
|
+
m.item("cm:x:surface"),
|
|
690
|
+
m.item("cm:admin", { note: "Admins only." })
|
|
691
|
+
]
|
|
692
|
+
);
|
|
693
|
+
}
|
|
694
|
+
function liveNotesEditor() {
|
|
695
|
+
const m = maker("live-notes-editor");
|
|
696
|
+
return design(
|
|
697
|
+
"live-notes-editor",
|
|
698
|
+
"Notes editor",
|
|
699
|
+
"editable-field",
|
|
700
|
+
"Live today: right-click in a note. Source: features/notes/agent-context/notesEditorExtraSections (the Note section) on top of the standard editable menu.",
|
|
701
|
+
[
|
|
702
|
+
TIER_STRIP(m),
|
|
703
|
+
m.item("cm:select-all"),
|
|
704
|
+
m.divider(),
|
|
705
|
+
m.item("cm:chat"),
|
|
706
|
+
...aiPlacements(m),
|
|
707
|
+
m.item("cm:quick-actions"),
|
|
708
|
+
m.divider(),
|
|
709
|
+
m.section("Note", [
|
|
710
|
+
m.item("cm:save", { hint: "Greyed when the note has no unsaved changes." }),
|
|
711
|
+
m.item("duplicate"),
|
|
712
|
+
m.item("notes:export-md"),
|
|
713
|
+
m.item("share-link", { label: "Share link\u2026" }),
|
|
714
|
+
m.item("notes:share-clipboard"),
|
|
715
|
+
m.item("notes:convert-artifacts", { label: "Convert blocks to artifacts", note: "Only on notes that can be saved." }),
|
|
716
|
+
m.sub("Move to folder\u2026", "FolderInput", [
|
|
717
|
+
m.own("Inbox", "FolderClosed", { note: "One row per folder the person has; the current folder is greyed." }),
|
|
718
|
+
m.own("Projects", "FolderClosed"),
|
|
719
|
+
m.divider(),
|
|
720
|
+
m.own("New folder\u2026", "FolderPlus"),
|
|
721
|
+
m.own("Choose folder\u2026", "FolderInput")
|
|
722
|
+
]),
|
|
723
|
+
m.sub("Tabs", "X", [
|
|
724
|
+
m.item("notes:close-tab", { label: "Close Tab" }),
|
|
725
|
+
m.own("Close Other Tabs", "X", { note: "Greyed with one tab open." }),
|
|
726
|
+
m.own("Close All Tabs", "X")
|
|
727
|
+
]),
|
|
728
|
+
m.divider(),
|
|
729
|
+
m.item("notes:trash", { note: "Always last, red." })
|
|
730
|
+
]),
|
|
731
|
+
m.divider(),
|
|
732
|
+
m.item("cm:copy-as"),
|
|
733
|
+
m.item("cm:export"),
|
|
734
|
+
m.item("cm:convert"),
|
|
735
|
+
m.item("cm:share"),
|
|
736
|
+
m.item("cm:attach"),
|
|
737
|
+
m.item("cm:x:surface"),
|
|
738
|
+
m.item("cm:admin", { note: "Admins only." })
|
|
739
|
+
],
|
|
740
|
+
{ search: true }
|
|
741
|
+
);
|
|
742
|
+
}
|
|
743
|
+
function liveQuizRow() {
|
|
744
|
+
const m = maker("live-quiz-row");
|
|
745
|
+
return design(
|
|
746
|
+
"live-quiz-row",
|
|
747
|
+
"Quiz row",
|
|
748
|
+
"table-row",
|
|
749
|
+
"Live today: a quiz / practice-test row. Source: features/education/assessment/components/home/useAssessmentRowActions.",
|
|
750
|
+
[
|
|
751
|
+
m.section("", [m.item("row:open"), m.item("row:open-tab")]),
|
|
752
|
+
m.section("Quiz", [
|
|
753
|
+
m.item("row:take", { note: "Greyed when archived or the quiz has no questions." }),
|
|
754
|
+
m.item("edit", { label: "Edit questions", note: "Only where the person may edit." }),
|
|
755
|
+
m.item("row:results", { note: "Only once the person has a result." })
|
|
756
|
+
]),
|
|
757
|
+
m.section("Manage", [
|
|
758
|
+
m.item("archive", { note: "Shows Restore (ArchiveRestore) instead when the quiz is archived. Only for editors." })
|
|
759
|
+
])
|
|
760
|
+
]
|
|
761
|
+
);
|
|
762
|
+
}
|
|
763
|
+
function liveTableRow() {
|
|
764
|
+
const m = maker("live-table-row");
|
|
765
|
+
return design(
|
|
766
|
+
"live-table-row",
|
|
767
|
+
"Data table row (/data)",
|
|
768
|
+
"table-row",
|
|
769
|
+
"Live today: a row on the Data home. Source: /demos/context-menu-designs TableRowsPanel, the 'today' side.",
|
|
770
|
+
[
|
|
771
|
+
m.section("", [m.item("row:open"), m.item("row:open-tab")]),
|
|
772
|
+
m.section("", [m.item("favorite")])
|
|
773
|
+
]
|
|
774
|
+
);
|
|
775
|
+
}
|
|
776
|
+
function regroupDesign() {
|
|
777
|
+
const m = maker("proposal-grouped");
|
|
778
|
+
return design(
|
|
779
|
+
"proposal-grouped",
|
|
780
|
+
"Grouped by what you get (13 groups)",
|
|
781
|
+
"text-selection",
|
|
782
|
+
"Proposal: /demos/context-menu-regroup, PROPOSED_MENU_GROUPING v2. Strip and universal verbs stay on top; everything else is filed under 13 outcome submenus.",
|
|
783
|
+
[
|
|
784
|
+
TIER_STRIP(m),
|
|
785
|
+
m.item("cm:select-all"),
|
|
786
|
+
m.item("cm:quick-actions"),
|
|
787
|
+
m.item("cm:save", { note: "An editor's own Save / Delete stay one click away." }),
|
|
788
|
+
m.item("cm:delete"),
|
|
789
|
+
m.divider(),
|
|
790
|
+
m.sub("Edit", "Pencil", [
|
|
791
|
+
m.item("edit"),
|
|
792
|
+
m.own("Replace selection", "Replace"),
|
|
793
|
+
m.own("Insert below", "ArrowDownToLine"),
|
|
794
|
+
m.item("rename")
|
|
795
|
+
], { hint: "Change this item where it is." }),
|
|
796
|
+
m.sub("Copy", "Copy", [
|
|
797
|
+
m.item("cm:copy-as"),
|
|
798
|
+
m.item("cm:json"),
|
|
799
|
+
m.item("copy-html-page"),
|
|
800
|
+
m.item("cm:insert-reference")
|
|
801
|
+
], { hint: "Lands on your clipboard." }),
|
|
802
|
+
m.sub("Download", "Download", [
|
|
803
|
+
m.item("cm:export"),
|
|
804
|
+
m.item("save-as-file"),
|
|
805
|
+
m.item("download-pdf"),
|
|
806
|
+
m.item("download-docx"),
|
|
807
|
+
m.item("download-html"),
|
|
808
|
+
m.item("print")
|
|
809
|
+
], { hint: "Becomes a file outside the app." }),
|
|
810
|
+
m.sub("Convert to", "ArrowRightLeft", [
|
|
811
|
+
m.item("cm:convert", { label: "Convert to\u2026" }),
|
|
812
|
+
m.own("Note", "StickyNote"),
|
|
813
|
+
m.own("Document", "FileText"),
|
|
814
|
+
m.own("Task", "SquareCheck"),
|
|
815
|
+
m.own("Flashcards", "Layers"),
|
|
816
|
+
m.own("Quiz", "CircleHelp")
|
|
817
|
+
], { hint: "Becomes a new thing inside the app." }),
|
|
818
|
+
m.sub("Publish", "Globe", [m.item("html-preview"), m.item("share-webpage")], { hint: "Goes out to the world." }),
|
|
819
|
+
m.sub("Share", "Share2", [m.item("cm:share"), m.item("send-google-doc"), m.item("email-to-me")], {
|
|
820
|
+
hint: "Goes to specific people or places."
|
|
821
|
+
}),
|
|
822
|
+
m.sub("Organize", "Tags", [
|
|
823
|
+
m.item("cm:attach"),
|
|
824
|
+
m.item("set-context-value"),
|
|
825
|
+
m.item("pin-message", { label: "Pin" }),
|
|
826
|
+
m.item("notes-and-comments")
|
|
827
|
+
], { hint: "Filed, not changed." }),
|
|
828
|
+
m.sub("History", "History", [m.item("cm:view-history"), m.item("edit-history"), m.item("fork-at-message")], {
|
|
829
|
+
hint: "Earlier versions."
|
|
830
|
+
}),
|
|
831
|
+
m.sub("AI", "Bot", [
|
|
832
|
+
m.item("cm:chat"),
|
|
833
|
+
...aiPlacements(m),
|
|
834
|
+
m.item("send-to-agent"),
|
|
835
|
+
m.item("continue-in-chat"),
|
|
836
|
+
m.item("regenerate-response"),
|
|
837
|
+
m.item("code-block-chart")
|
|
838
|
+
], { hint: "An AI works on it." }),
|
|
839
|
+
m.sub("Read aloud", "Volume2", [m.item("cm:speak"), m.item("cm:listen"), m.item("tts-voice-settings")], { hint: "You hear it." }),
|
|
840
|
+
m.sub("Compare", "GitCompareArrows", [m.item("cm:compare"), m.item("compare-with-clipboard"), m.item("set-compare-base")], {
|
|
841
|
+
hint: "Side by side."
|
|
842
|
+
}),
|
|
843
|
+
m.sub("Feedback", "ThumbsUp", [m.item("feedback-up"), m.item("feedback-down"), m.item("feedback")], { hint: "Tell us how it went." }),
|
|
844
|
+
m.sub("Admin", "Shield", [m.item("cm:admin")], { hint: "Admins only." }),
|
|
845
|
+
m.divider(),
|
|
846
|
+
m.item("cm:x:surface")
|
|
847
|
+
]
|
|
848
|
+
);
|
|
849
|
+
}
|
|
850
|
+
function objectFirstThing() {
|
|
851
|
+
const m = maker("proposal-object-thing");
|
|
852
|
+
return design(
|
|
853
|
+
"proposal-object-thing",
|
|
854
|
+
"Object-first: a thing (data table row)",
|
|
855
|
+
"table-row",
|
|
856
|
+
"Proposal: /demos/context-menu-designs, proposed-arrangement for a clicked thing (the data-table row on that page).",
|
|
857
|
+
[
|
|
858
|
+
m.strip([
|
|
859
|
+
m.item("cm:copy"),
|
|
860
|
+
m.item("cm:share"),
|
|
861
|
+
m.item("duplicate"),
|
|
862
|
+
m.item("favorite", { label: "Favorite" }),
|
|
863
|
+
m.item("save-as-file", { label: "Download", icon: "Download" }),
|
|
864
|
+
m.item("archive")
|
|
865
|
+
]),
|
|
866
|
+
m.item("row:open"),
|
|
867
|
+
m.item("row:open-tab"),
|
|
868
|
+
m.item("rename"),
|
|
869
|
+
m.item("move", { label: "Move to\u2026" }),
|
|
870
|
+
m.sub("More table options", "Ellipsis", [
|
|
871
|
+
m.own("Add column", "Columns3"),
|
|
872
|
+
m.own("Import", "Upload"),
|
|
873
|
+
m.own("Settings", "Settings"),
|
|
874
|
+
m.item("cm:view-history")
|
|
875
|
+
], { note: "The thing's own rows beyond the first four." }),
|
|
876
|
+
intelligence(m),
|
|
877
|
+
saveShare(m),
|
|
878
|
+
organize(m),
|
|
879
|
+
tools(m)
|
|
880
|
+
],
|
|
881
|
+
{ search: true }
|
|
882
|
+
);
|
|
883
|
+
}
|
|
884
|
+
function objectFirstEditable() {
|
|
885
|
+
const m = maker("proposal-object-editable");
|
|
886
|
+
return design(
|
|
887
|
+
"proposal-object-editable",
|
|
888
|
+
"Object-first: editable text",
|
|
889
|
+
"editable-field",
|
|
890
|
+
"Proposal: /demos/context-menu-designs, proposed-arrangement for editable text (Cut, Copy, Paste, Undo, Redo, Find on top).",
|
|
891
|
+
[
|
|
892
|
+
TIER_STRIP(m),
|
|
893
|
+
m.item("cm:select-all"),
|
|
894
|
+
m.item("cm:save"),
|
|
895
|
+
m.item("cm:delete"),
|
|
896
|
+
m.item("edit"),
|
|
897
|
+
intelligence(m),
|
|
898
|
+
saveShare(m),
|
|
899
|
+
organize(m),
|
|
900
|
+
tools(m)
|
|
901
|
+
],
|
|
902
|
+
{ search: true }
|
|
903
|
+
);
|
|
904
|
+
}
|
|
905
|
+
function objectFirstReadOnly() {
|
|
906
|
+
const m = maker("proposal-object-readonly");
|
|
907
|
+
return design(
|
|
908
|
+
"proposal-object-readonly",
|
|
909
|
+
"Object-first: read-only text",
|
|
910
|
+
"text-selection",
|
|
911
|
+
"Proposal: /demos/context-menu-designs, proposed-arrangement for read-only text (Copy, Read aloud, Find, Share on top).",
|
|
912
|
+
[
|
|
913
|
+
m.strip([m.item("cm:copy"), m.item("cm:speak", { label: "Read aloud" }), m.item("cm:find"), m.item("cm:share")]),
|
|
914
|
+
intelligence(m),
|
|
915
|
+
saveShare(m),
|
|
916
|
+
organize(m),
|
|
917
|
+
tools(m)
|
|
918
|
+
],
|
|
919
|
+
{ search: true }
|
|
920
|
+
);
|
|
921
|
+
}
|
|
922
|
+
function notesRows(m) {
|
|
923
|
+
return [
|
|
924
|
+
m.item("cm:select-all"),
|
|
925
|
+
m.item("cm:chat"),
|
|
926
|
+
...aiPlacements(m),
|
|
927
|
+
m.item("cm:quick-actions"),
|
|
928
|
+
m.item("cm:listen"),
|
|
929
|
+
m.item("cm:speak"),
|
|
930
|
+
m.item("cm:copy-as"),
|
|
931
|
+
m.item("cm:json"),
|
|
932
|
+
m.item("cm:export"),
|
|
933
|
+
m.item("cm:convert"),
|
|
934
|
+
m.item("cm:share"),
|
|
935
|
+
m.item("cm:attach"),
|
|
936
|
+
m.item("cm:save"),
|
|
937
|
+
m.item("duplicate"),
|
|
938
|
+
m.item("notes:export-md"),
|
|
939
|
+
m.item("share-link", { label: "Share link\u2026" }),
|
|
940
|
+
m.item("notes:share-clipboard"),
|
|
941
|
+
m.item("move", { label: "Move to folder\u2026" }),
|
|
942
|
+
m.item("notes:close-tab", { label: "Tabs" }),
|
|
943
|
+
m.item("notes:trash"),
|
|
944
|
+
m.item("cm:x:surface"),
|
|
945
|
+
m.item("cm:admin")
|
|
946
|
+
];
|
|
947
|
+
}
|
|
948
|
+
function layoutCompact() {
|
|
949
|
+
const m = maker("proposal-layout-compact");
|
|
950
|
+
return design(
|
|
951
|
+
"proposal-layout-compact",
|
|
952
|
+
"Layout: Classic, compact rows",
|
|
953
|
+
"editable-field",
|
|
954
|
+
"Proposal: /demos/context-menu/layouts panel 2. Classic structure (every row at the top level), tighter rows and icons.",
|
|
955
|
+
[
|
|
956
|
+
m.item("cm:copy"),
|
|
957
|
+
m.item("cm:cut"),
|
|
958
|
+
m.item("cm:paste"),
|
|
959
|
+
m.item("cm:undo"),
|
|
960
|
+
m.item("cm:redo"),
|
|
961
|
+
m.item("cm:find"),
|
|
962
|
+
m.item("cm:view-history"),
|
|
963
|
+
m.item("cm:compare"),
|
|
964
|
+
m.divider(),
|
|
965
|
+
...notesRows(m)
|
|
966
|
+
],
|
|
967
|
+
{ density: "compact", width: 260 }
|
|
968
|
+
);
|
|
969
|
+
}
|
|
970
|
+
function tieredNodes(m) {
|
|
971
|
+
return [
|
|
972
|
+
TIER_STRIP(m),
|
|
973
|
+
m.sub("History", "History", [m.item("cm:undo"), m.item("cm:redo"), m.item("cm:view-history"), m.item("cm:compare")], {
|
|
974
|
+
note: "The one grouping the tiered layout adds; every other row keeps its Classic name and place."
|
|
975
|
+
}),
|
|
976
|
+
m.divider(),
|
|
977
|
+
...notesRows(m).filter((n) => n.kind !== "item" || n.label !== "Tabs"),
|
|
978
|
+
m.sub("Tabs", "X", [m.item("notes:close-tab", { label: "Close Tab" }), m.item("notes:close-others")])
|
|
979
|
+
];
|
|
980
|
+
}
|
|
981
|
+
function layoutTiered() {
|
|
982
|
+
const m = maker("proposal-layout-tiered");
|
|
983
|
+
return design(
|
|
984
|
+
"proposal-layout-tiered",
|
|
985
|
+
"Layout: Tiered (icon strip, nothing lost)",
|
|
986
|
+
"editable-field",
|
|
987
|
+
"Proposal: /demos/context-menu/layouts panel 3. Icon strip on top, every other row keeps its Classic name; History groups Undo / Redo / View History / Compare.",
|
|
988
|
+
tieredNodes(m)
|
|
989
|
+
);
|
|
990
|
+
}
|
|
991
|
+
function layoutCommand() {
|
|
992
|
+
const m = maker("proposal-layout-command");
|
|
993
|
+
return design(
|
|
994
|
+
"proposal-layout-command",
|
|
995
|
+
"Layout: Command (tiered + type to filter)",
|
|
996
|
+
"editable-field",
|
|
997
|
+
"Proposal: /demos/context-menu/layouts panel 4, the platform default. Tiered plus a filter box that flattens every action into one ranked list.",
|
|
998
|
+
tieredNodes(m),
|
|
999
|
+
{ search: true }
|
|
1000
|
+
);
|
|
1001
|
+
}
|
|
1002
|
+
var STARTER_DESIGNS = [
|
|
1003
|
+
{ id: "live-editable-field", group: "Live today", source: "Engine ContextMenuV3 \u2014 editable text field", design: liveEditableField() },
|
|
1004
|
+
{ id: "live-chat-message", group: "Live today", source: "Engine NonEditableContextMenu \u2014 read-only text and chat message", design: liveReadOnlyMessage() },
|
|
1005
|
+
{ id: "live-notes-editor", group: "Live today", source: "/notes \u2014 notesEditorExtraSections", design: liveNotesEditor() },
|
|
1006
|
+
{ id: "live-quiz-row", group: "Live today", source: "/education/quizzes \u2014 useAssessmentRowActions", design: liveQuizRow() },
|
|
1007
|
+
{ id: "live-table-row", group: "Live today", source: "/data row \u2014 /demos/context-menu-designs TableRowsPanel (today)", design: liveTableRow() },
|
|
1008
|
+
{ id: "proposal-grouped", group: "Proposal", source: "/demos/context-menu-regroup \u2014 grouped by what you get", design: regroupDesign() },
|
|
1009
|
+
{ id: "proposal-object-thing", group: "Proposal", source: "/demos/context-menu-designs \u2014 a thing", design: objectFirstThing() },
|
|
1010
|
+
{ id: "proposal-object-editable", group: "Proposal", source: "/demos/context-menu-designs \u2014 editable text", design: objectFirstEditable() },
|
|
1011
|
+
{ id: "proposal-object-readonly", group: "Proposal", source: "/demos/context-menu-designs \u2014 read-only text", design: objectFirstReadOnly() },
|
|
1012
|
+
{ id: "proposal-layout-compact", group: "Proposal", source: "/demos/context-menu/layouts \u2014 compact", design: layoutCompact() },
|
|
1013
|
+
{ id: "proposal-layout-tiered", group: "Proposal", source: "/demos/context-menu/layouts \u2014 tiered", design: layoutTiered() },
|
|
1014
|
+
{ id: "proposal-layout-command", group: "Proposal", source: "/demos/context-menu/layouts \u2014 command", design: layoutCommand() }
|
|
1015
|
+
];
|
|
1016
|
+
|
|
1017
|
+
// src/builder/MenuBuilder.tsx
|
|
1018
|
+
import {
|
|
1019
|
+
Badge,
|
|
1020
|
+
Button,
|
|
1021
|
+
ControlRow,
|
|
1022
|
+
DisclosureHeader,
|
|
1023
|
+
EmptyState,
|
|
1024
|
+
Field,
|
|
1025
|
+
NumberStepper,
|
|
1026
|
+
RegionSkeleton,
|
|
1027
|
+
RowGroup,
|
|
1028
|
+
SearchField,
|
|
1029
|
+
SegmentedControl,
|
|
1030
|
+
Select,
|
|
1031
|
+
SettingRow,
|
|
1032
|
+
Switch,
|
|
1033
|
+
Textarea,
|
|
1034
|
+
Tile as Tile2
|
|
1035
|
+
} from "@ai-matrx/design-system/controls";
|
|
1036
|
+
|
|
1037
|
+
// src/builder/icons.tsx
|
|
1038
|
+
import * as Lucide from "lucide-react";
|
|
1039
|
+
import { jsx as jsx2 } from "react/jsx-runtime";
|
|
1040
|
+
var table = Lucide;
|
|
1041
|
+
var ICON_NAMES = Object.keys(table).filter((k) => /^[A-Z]/.test(k) && !k.endsWith("Icon") && !k.startsWith("Lucide") && k !== "Icon" && k !== "createLucideIcon").sort();
|
|
1042
|
+
function renderLucide(name, className) {
|
|
1043
|
+
const Found = table[name];
|
|
1044
|
+
const Icon = typeof Found === "function" || typeof Found === "object" && Found !== null ? Found : table.CircleHelp;
|
|
1045
|
+
return /* @__PURE__ */ jsx2(Icon, { className, strokeWidth: 1.75 });
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
// src/builder/MenuBuilder.tsx
|
|
1049
|
+
import { Fragment as Fragment2, jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
1050
|
+
var SURFACE_LABELS = {
|
|
1051
|
+
"text-selection": "Opens on selected text",
|
|
1052
|
+
"editable-field": "Opens in an editable field",
|
|
1053
|
+
"table-row": "Opens on a table row",
|
|
1054
|
+
page: "Opens on the page",
|
|
1055
|
+
"chat-message": "Opens on a chat message",
|
|
1056
|
+
file: "Opens on a file",
|
|
1057
|
+
anywhere: "Opens anywhere"
|
|
1058
|
+
};
|
|
1059
|
+
var SURFACE_OPTIONS = MENU_SURFACES.map((value) => ({ value, label: SURFACE_LABELS[value] }));
|
|
1060
|
+
var DesignContext = createContext(null);
|
|
1061
|
+
var DRAFT_KEY = "matrx.context-menu.builder.draft.v1";
|
|
1062
|
+
function usePointerDrag(onDrop) {
|
|
1063
|
+
const [ghost, setGhost] = useState2(null);
|
|
1064
|
+
const [over, setOver] = useState2(null);
|
|
1065
|
+
const suppressClick = useRef(false);
|
|
1066
|
+
const start = (e, payload, label) => {
|
|
1067
|
+
if (e.button !== 0) return;
|
|
1068
|
+
const x0 = e.clientX;
|
|
1069
|
+
const y0 = e.clientY;
|
|
1070
|
+
let active = false;
|
|
1071
|
+
let current = null;
|
|
1072
|
+
const move = (ev) => {
|
|
1073
|
+
if (!active && Math.hypot(ev.clientX - x0, ev.clientY - y0) < 4) return;
|
|
1074
|
+
if (!active) {
|
|
1075
|
+
active = true;
|
|
1076
|
+
document.body.style.userSelect = "none";
|
|
1077
|
+
document.body.style.cursor = "grabbing";
|
|
1078
|
+
}
|
|
1079
|
+
setGhost({ label, x: ev.clientX, y: ev.clientY });
|
|
1080
|
+
const el = document.elementFromPoint(ev.clientX, ev.clientY)?.closest("[data-drop]");
|
|
1081
|
+
if (!el) {
|
|
1082
|
+
current = null;
|
|
1083
|
+
} else if (el.dataset.drop === "root") {
|
|
1084
|
+
current = { id: null, pos: "after" };
|
|
1085
|
+
} else {
|
|
1086
|
+
const r = el.getBoundingClientRect();
|
|
1087
|
+
const y = (ev.clientY - r.top) / r.height;
|
|
1088
|
+
const pos = el.dataset.container ? y < 0.28 ? "before" : y > 0.72 ? "after" : "inside" : y < 0.5 ? "before" : "after";
|
|
1089
|
+
current = { id: el.dataset.drop ?? null, pos };
|
|
1090
|
+
}
|
|
1091
|
+
setOver(current);
|
|
1092
|
+
};
|
|
1093
|
+
const up = () => {
|
|
1094
|
+
window.removeEventListener("pointermove", move);
|
|
1095
|
+
window.removeEventListener("pointerup", up);
|
|
1096
|
+
window.removeEventListener("pointercancel", up);
|
|
1097
|
+
document.body.style.userSelect = "";
|
|
1098
|
+
document.body.style.cursor = "";
|
|
1099
|
+
if (active) {
|
|
1100
|
+
suppressClick.current = true;
|
|
1101
|
+
setTimeout(() => suppressClick.current = false, 0);
|
|
1102
|
+
if (current) onDrop(payload, current);
|
|
1103
|
+
}
|
|
1104
|
+
setGhost(null);
|
|
1105
|
+
setOver(null);
|
|
1106
|
+
};
|
|
1107
|
+
window.addEventListener("pointermove", move);
|
|
1108
|
+
window.addEventListener("pointerup", up);
|
|
1109
|
+
window.addEventListener("pointercancel", up);
|
|
1110
|
+
};
|
|
1111
|
+
const click = (fn) => () => {
|
|
1112
|
+
if (!suppressClick.current) fn();
|
|
1113
|
+
};
|
|
1114
|
+
return { start, click, ghost, over };
|
|
1115
|
+
}
|
|
1116
|
+
function nodeFromPayload(p) {
|
|
1117
|
+
if (p.type === "catalog") {
|
|
1118
|
+
const c = catalogOption(p.id);
|
|
1119
|
+
if (!c) return null;
|
|
1120
|
+
return {
|
|
1121
|
+
id: newId("i"),
|
|
1122
|
+
kind: "item",
|
|
1123
|
+
catalogId: c.id,
|
|
1124
|
+
label: c.label,
|
|
1125
|
+
icon: c.icon,
|
|
1126
|
+
...c.shortcut ? { shortcut: c.shortcut } : {},
|
|
1127
|
+
...c.danger ? { danger: true } : {},
|
|
1128
|
+
...c.ai ? { ai: true } : {}
|
|
1129
|
+
};
|
|
1130
|
+
}
|
|
1131
|
+
if (p.type === "template") return TEMPLATES.find((t) => t.id === p.id)?.build() ?? null;
|
|
1132
|
+
if (p.type === "new") {
|
|
1133
|
+
if (p.kind === "section") return { id: newId("s"), kind: "section", label: "New section", children: [] };
|
|
1134
|
+
if (p.kind === "submenu") return { id: newId("m"), kind: "submenu", label: "More", icon: "Ellipsis", children: [] };
|
|
1135
|
+
if (p.kind === "divider") return { id: newId("d"), kind: "divider" };
|
|
1136
|
+
if (p.kind === "slot-items") return { id: newId("p"), kind: "slot", shape: "items", fills: "any", label: "Everything else" };
|
|
1137
|
+
if (p.kind === "slot-menu") return { id: newId("p"), kind: "slot", shape: "submenu", fills: "any", label: "More", icon: "Ellipsis" };
|
|
1138
|
+
return { id: newId("i"), kind: "item", label: "New option", icon: "Circle" };
|
|
1139
|
+
}
|
|
1140
|
+
return null;
|
|
1141
|
+
}
|
|
1142
|
+
function readDraft() {
|
|
1143
|
+
try {
|
|
1144
|
+
const raw = globalThis.localStorage?.getItem(DRAFT_KEY);
|
|
1145
|
+
return raw ? parseDesign(raw) : null;
|
|
1146
|
+
} catch {
|
|
1147
|
+
return null;
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
function useHistory(initial) {
|
|
1151
|
+
const [state, setState] = useState2({ past: [], present: initial, future: [] });
|
|
1152
|
+
const set = useCallback((next, opts) => {
|
|
1153
|
+
setState((s) => {
|
|
1154
|
+
const value = typeof next === "function" ? next(s.present) : next;
|
|
1155
|
+
if (value === s.present) return s;
|
|
1156
|
+
if (opts?.replace) return { past: [], present: value, future: [] };
|
|
1157
|
+
return { past: [...s.past.slice(-99), s.present], present: value, future: [] };
|
|
1158
|
+
});
|
|
1159
|
+
}, []);
|
|
1160
|
+
const undo = useCallback(() => setState((s) => s.past.length ? { past: s.past.slice(0, -1), present: s.past[s.past.length - 1], future: [s.present, ...s.future] } : s), []);
|
|
1161
|
+
const redo = useCallback(() => setState((s) => s.future.length ? { past: [...s.past, s.present], present: s.future[0], future: s.future.slice(1) } : s), []);
|
|
1162
|
+
return { design: state.present, set, undo, redo, canUndo: state.past.length > 0, canRedo: state.future.length > 0 };
|
|
1163
|
+
}
|
|
1164
|
+
function MenuBuilder({ store, storeLabel, initialDesign, accountSlot }) {
|
|
1165
|
+
const { design: design2, set, undo, redo, canUndo, canRedo } = useHistory(initialDesign ?? readDraft() ?? emptyDesign());
|
|
1166
|
+
const [selected, setSelected] = useState2(null);
|
|
1167
|
+
const [versions, setVersions] = useState2(null);
|
|
1168
|
+
const [status, setStatus] = useState2(null);
|
|
1169
|
+
const [saving, setSaving] = useState2(false);
|
|
1170
|
+
const [note, setNote] = useState2("");
|
|
1171
|
+
const [lastSaved, setLastSaved] = useState2(null);
|
|
1172
|
+
const designRef = useRef(design2);
|
|
1173
|
+
designRef.current = design2;
|
|
1174
|
+
useEffect(() => {
|
|
1175
|
+
try {
|
|
1176
|
+
globalThis.localStorage?.setItem(DRAFT_KEY, serializeDesign(design2));
|
|
1177
|
+
} catch {
|
|
1178
|
+
}
|
|
1179
|
+
}, [design2]);
|
|
1180
|
+
const refresh = useCallback(async () => {
|
|
1181
|
+
try {
|
|
1182
|
+
const all = await store.list();
|
|
1183
|
+
setVersions(all);
|
|
1184
|
+
const now = serializeDesign(designRef.current);
|
|
1185
|
+
setLastSaved(all.some((v) => serializeDesign(v.design) === now) ? now : null);
|
|
1186
|
+
} catch (e) {
|
|
1187
|
+
setStatus({ tone: "error", text: `Could not read saved designs: ${e instanceof Error ? e.message : String(e)}` });
|
|
1188
|
+
}
|
|
1189
|
+
}, [store]);
|
|
1190
|
+
useEffect(() => {
|
|
1191
|
+
void refresh();
|
|
1192
|
+
}, [refresh]);
|
|
1193
|
+
const dirty = lastSaved !== serializeDesign(design2);
|
|
1194
|
+
const save = async () => {
|
|
1195
|
+
setSaving(true);
|
|
1196
|
+
try {
|
|
1197
|
+
const saved = await store.save(design2, note.trim());
|
|
1198
|
+
setLastSaved(serializeDesign(design2));
|
|
1199
|
+
setNote("");
|
|
1200
|
+
setStatus({ tone: "ok", text: `Saved \u201C${saved.name}\u201D v${saved.version}` });
|
|
1201
|
+
await refresh();
|
|
1202
|
+
} catch (e) {
|
|
1203
|
+
setStatus({ tone: "error", text: `Save failed: ${e instanceof Error ? e.message : String(e)}` });
|
|
1204
|
+
} finally {
|
|
1205
|
+
setSaving(false);
|
|
1206
|
+
}
|
|
1207
|
+
};
|
|
1208
|
+
const open = (v) => {
|
|
1209
|
+
set(v.design);
|
|
1210
|
+
setLastSaved(serializeDesign(v.design));
|
|
1211
|
+
setSelected(null);
|
|
1212
|
+
setStatus({ tone: "info", text: `Opened \u201C${v.name}\u201D v${v.version}${dirty ? " \u2014 \u2318Z brings back your unsaved changes" : ""}` });
|
|
1213
|
+
};
|
|
1214
|
+
const addNode = (node) => {
|
|
1215
|
+
set((d) => {
|
|
1216
|
+
const sel = selected ? locate(d.nodes, selected) : null;
|
|
1217
|
+
const nests = sel && isContainer(sel.node) && !isContainer(node);
|
|
1218
|
+
if (sel && nests && isContainer(sel.node)) return { ...d, nodes: insertNode(d.nodes, sel.node.id, sel.node.children.length, node) };
|
|
1219
|
+
if (sel) return { ...d, nodes: insertNode(d.nodes, sel.parentId, sel.index + 1, node) };
|
|
1220
|
+
return { ...d, nodes: [...d.nodes, node] };
|
|
1221
|
+
});
|
|
1222
|
+
setSelected(node.id);
|
|
1223
|
+
};
|
|
1224
|
+
const drop = (p, { id: targetId, pos }) => {
|
|
1225
|
+
if (p.type === "move" && p.id === targetId) return;
|
|
1226
|
+
if (p.type === "move" && targetId) {
|
|
1227
|
+
const at = locate(design2.nodes, targetId);
|
|
1228
|
+
const parentId = at ? pos === "inside" && isContainer(at.node) ? at.node.id : at.parentId : null;
|
|
1229
|
+
if (!canMove(design2.nodes, p.id, parentId)) {
|
|
1230
|
+
setStatus({ tone: "error", text: "Can't move a section or submenu inside itself" });
|
|
1231
|
+
return;
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
const fresh = p.type === "move" ? null : nodeFromPayload(p);
|
|
1235
|
+
set((d) => {
|
|
1236
|
+
let parentId = null;
|
|
1237
|
+
let index = d.nodes.length;
|
|
1238
|
+
if (targetId) {
|
|
1239
|
+
const at = locate(d.nodes, targetId);
|
|
1240
|
+
if (!at) return d;
|
|
1241
|
+
if (pos === "inside" && isContainer(at.node)) {
|
|
1242
|
+
parentId = at.node.id;
|
|
1243
|
+
index = at.node.children.length;
|
|
1244
|
+
} else {
|
|
1245
|
+
parentId = at.parentId;
|
|
1246
|
+
index = at.index + (pos === "after" ? 1 : 0);
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
if (p.type === "move") return { ...d, nodes: moveNode(d.nodes, p.id, parentId, index) };
|
|
1250
|
+
return fresh ? { ...d, nodes: insertNode(d.nodes, parentId, index, fresh) } : d;
|
|
1251
|
+
});
|
|
1252
|
+
if (fresh) setSelected(fresh.id);
|
|
1253
|
+
else if (p.type === "move") setSelected(p.id);
|
|
1254
|
+
};
|
|
1255
|
+
const dnd = usePointerDrag(drop);
|
|
1256
|
+
const remove = (id) => {
|
|
1257
|
+
set((d) => ({ ...d, nodes: removeNode(d.nodes, id) }));
|
|
1258
|
+
if (selected === id) setSelected(null);
|
|
1259
|
+
};
|
|
1260
|
+
useEffect(() => {
|
|
1261
|
+
const onKey = (e) => {
|
|
1262
|
+
const typing = e.target instanceof HTMLElement && /^(INPUT|TEXTAREA|SELECT)$/.test(e.target.tagName);
|
|
1263
|
+
const mod = e.metaKey || e.ctrlKey;
|
|
1264
|
+
if (mod && e.key.toLowerCase() === "s") {
|
|
1265
|
+
e.preventDefault();
|
|
1266
|
+
void save();
|
|
1267
|
+
return;
|
|
1268
|
+
}
|
|
1269
|
+
if (typing) return;
|
|
1270
|
+
if (mod && e.key.toLowerCase() === "z") {
|
|
1271
|
+
e.preventDefault();
|
|
1272
|
+
if (e.shiftKey) redo();
|
|
1273
|
+
else undo();
|
|
1274
|
+
} else if (mod && e.key.toLowerCase() === "d" && selected) {
|
|
1275
|
+
e.preventDefault();
|
|
1276
|
+
set((d) => ({ ...d, nodes: duplicateNode(d.nodes, selected) }));
|
|
1277
|
+
} else if ((e.key === "Delete" || e.key === "Backspace") && selected) {
|
|
1278
|
+
e.preventDefault();
|
|
1279
|
+
remove(selected);
|
|
1280
|
+
} else if (e.key === "Escape") setSelected(null);
|
|
1281
|
+
};
|
|
1282
|
+
window.addEventListener("keydown", onKey);
|
|
1283
|
+
return () => window.removeEventListener("keydown", onKey);
|
|
1284
|
+
});
|
|
1285
|
+
const selectedNode = selected ? locate(design2.nodes, selected)?.node ?? null : null;
|
|
1286
|
+
const designs = useMemo2(() => {
|
|
1287
|
+
const byId = /* @__PURE__ */ new Map();
|
|
1288
|
+
for (const v of versions ?? []) byId.set(v.designId, [...byId.get(v.designId) ?? [], v]);
|
|
1289
|
+
return [...byId.values()];
|
|
1290
|
+
}, [versions]);
|
|
1291
|
+
return /* @__PURE__ */ jsx3(DesignContext.Provider, { value: design2, children: /* @__PURE__ */ jsxs2("div", { className: "flex h-dvh flex-col bg-background text-foreground", children: [
|
|
1292
|
+
/* @__PURE__ */ jsxs2("header", { className: "flex flex-wrap items-center gap-y-1 border-b border-border px-3 py-2", children: [
|
|
1293
|
+
/* @__PURE__ */ jsxs2(ControlRow, { className: "min-w-0 flex-1", children: [
|
|
1294
|
+
/* @__PURE__ */ jsx3(Field, { "aria-label": "Design name", width: "lg", value: design2.name, onChange: (e) => set((d) => ({ ...d, name: e.target.value })) }),
|
|
1295
|
+
/* @__PURE__ */ jsx3(
|
|
1296
|
+
Select,
|
|
1297
|
+
{
|
|
1298
|
+
"aria-label": "Where this menu opens",
|
|
1299
|
+
value: design2.surface,
|
|
1300
|
+
options: SURFACE_OPTIONS,
|
|
1301
|
+
onValueChange: (surface) => set((d) => ({ ...d, surface }))
|
|
1302
|
+
}
|
|
1303
|
+
),
|
|
1304
|
+
/* @__PURE__ */ jsxs2(Badge, { children: [
|
|
1305
|
+
countItems(design2.nodes),
|
|
1306
|
+
" options"
|
|
1307
|
+
] }),
|
|
1308
|
+
/* @__PURE__ */ jsx3(Button, { variant: "quiet", icon: renderLucide("Undo2"), "aria-label": "Undo (\u2318Z)", title: "Undo (\u2318Z)", onClick: undo, disabled: !canUndo }),
|
|
1309
|
+
/* @__PURE__ */ jsx3(Button, { variant: "quiet", icon: renderLucide("Redo2"), "aria-label": "Redo (\u21E7\u2318Z)", title: "Redo (\u21E7\u2318Z)", onClick: redo, disabled: !canRedo }),
|
|
1310
|
+
/* @__PURE__ */ jsx3(
|
|
1311
|
+
Button,
|
|
1312
|
+
{
|
|
1313
|
+
variant: "quiet",
|
|
1314
|
+
icon: renderLucide("FilePlus2"),
|
|
1315
|
+
onClick: () => {
|
|
1316
|
+
set(emptyDesign(), { replace: true });
|
|
1317
|
+
setLastSaved(null);
|
|
1318
|
+
setSelected(null);
|
|
1319
|
+
},
|
|
1320
|
+
children: "New"
|
|
1321
|
+
}
|
|
1322
|
+
),
|
|
1323
|
+
/* @__PURE__ */ jsx3(
|
|
1324
|
+
Button,
|
|
1325
|
+
{
|
|
1326
|
+
variant: "quiet",
|
|
1327
|
+
icon: renderLucide("CopyPlus"),
|
|
1328
|
+
title: "Keep this menu as a separate design",
|
|
1329
|
+
onClick: () => set((d) => ({ ...d, designId: newId("d"), name: `${d.name} (copy)` })),
|
|
1330
|
+
children: "Save as new design"
|
|
1331
|
+
}
|
|
1332
|
+
),
|
|
1333
|
+
/* @__PURE__ */ jsx3(
|
|
1334
|
+
Button,
|
|
1335
|
+
{
|
|
1336
|
+
variant: "quiet",
|
|
1337
|
+
icon: renderLucide("Braces"),
|
|
1338
|
+
onClick: () => {
|
|
1339
|
+
void navigator.clipboard?.writeText(serializeDesign(design2));
|
|
1340
|
+
setStatus({ tone: "info", text: "Design JSON copied" });
|
|
1341
|
+
},
|
|
1342
|
+
children: "Copy JSON"
|
|
1343
|
+
}
|
|
1344
|
+
)
|
|
1345
|
+
] }),
|
|
1346
|
+
/* @__PURE__ */ jsxs2(ControlRow, { children: [
|
|
1347
|
+
accountSlot ?? /* @__PURE__ */ jsx3(Badge, { children: storeLabel }),
|
|
1348
|
+
/* @__PURE__ */ jsx3(Field, { "aria-label": "Version note", width: "md", placeholder: "Version note", value: note, onChange: (e) => setNote(e.target.value) }),
|
|
1349
|
+
/* @__PURE__ */ jsx3(Button, { variant: "primary", icon: renderLucide("Save"), meta: "\u2318S", onClick: () => void save(), disabled: saving || !dirty, children: saving ? "Saving\u2026" : dirty ? "Save version" : "Saved" })
|
|
1350
|
+
] })
|
|
1351
|
+
] }),
|
|
1352
|
+
status && /* @__PURE__ */ jsx3("div", { className: `border-b border-border px-3 py-1 text-xs ${status.tone === "error" ? "text-destructive" : "text-muted-foreground"}`, children: status.text }),
|
|
1353
|
+
/* @__PURE__ */ jsxs2("div", { className: "grid min-h-0 flex-1 grid-cols-[minmax(200px,240px)_minmax(0,1fr)_minmax(0,1.1fr)]", children: [
|
|
1354
|
+
/* @__PURE__ */ jsx3(Palette, { onAdd: addNode, startDrag: dnd.start, click: dnd.click }),
|
|
1355
|
+
/* @__PURE__ */ jsx3(
|
|
1356
|
+
Structure,
|
|
1357
|
+
{
|
|
1358
|
+
nodes: design2.nodes,
|
|
1359
|
+
selected,
|
|
1360
|
+
onSelect: setSelected,
|
|
1361
|
+
startDrag: dnd.start,
|
|
1362
|
+
click: dnd.click,
|
|
1363
|
+
over: dnd.over,
|
|
1364
|
+
onRemove: remove,
|
|
1365
|
+
onDuplicate: (id) => set((d) => ({ ...d, nodes: duplicateNode(d.nodes, id) })),
|
|
1366
|
+
onWrap: (id, kind) => set((d) => ({ ...d, nodes: wrapNode(d.nodes, id, kind) })),
|
|
1367
|
+
onUnwrap: (id) => set((d) => ({ ...d, nodes: unwrapNode(d.nodes, id) })),
|
|
1368
|
+
versions: versions === null ? null : designs,
|
|
1369
|
+
currentDesignId: design2.designId,
|
|
1370
|
+
onOpen: open,
|
|
1371
|
+
onStart: (st) => {
|
|
1372
|
+
const copy = { ...st.design, designId: newId("d") };
|
|
1373
|
+
set(copy);
|
|
1374
|
+
setLastSaved(null);
|
|
1375
|
+
setSelected(null);
|
|
1376
|
+
setStatus({ tone: "info", text: `Started from \u201C${st.design.name}\u201D${dirty ? " \u2014 \u2318Z brings back your unsaved changes" : ""}` });
|
|
1377
|
+
},
|
|
1378
|
+
onArchive: (v) => {
|
|
1379
|
+
void store.remove(v.rowId).then(refresh).then(() => setStatus({ tone: "info", text: `Archived \u201C${v.name}\u201D v${v.version}` })).catch((e) => setStatus({ tone: "error", text: `Archive failed: ${e instanceof Error ? e.message : String(e)}` }));
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
),
|
|
1383
|
+
/* @__PURE__ */ jsxs2("aside", { className: "flex min-h-0 min-w-0 flex-col overflow-x-hidden overflow-y-auto border-l border-border", children: [
|
|
1384
|
+
/* @__PURE__ */ jsx3(Stage, { design: design2, selected, onSelect: setSelected }),
|
|
1385
|
+
selectedNode ? /* @__PURE__ */ jsx3(
|
|
1386
|
+
Inspector,
|
|
1387
|
+
{
|
|
1388
|
+
node: selectedNode,
|
|
1389
|
+
onChange: (patch) => set((d) => ({ ...d, nodes: updateNode(d.nodes, selectedNode.id, patch) }))
|
|
1390
|
+
},
|
|
1391
|
+
selectedNode.id
|
|
1392
|
+
) : /* @__PURE__ */ jsx3(Panel, { title: "Notes for the build", children: /* @__PURE__ */ jsx3(
|
|
1393
|
+
Textarea,
|
|
1394
|
+
{
|
|
1395
|
+
"aria-label": "Notes for the build",
|
|
1396
|
+
value: design2.notes,
|
|
1397
|
+
onChange: (e) => set((d) => ({ ...d, notes: e.target.value })),
|
|
1398
|
+
rows: 4,
|
|
1399
|
+
autoGrow: true,
|
|
1400
|
+
placeholder: "Anything the builder of this menu should know"
|
|
1401
|
+
}
|
|
1402
|
+
) }),
|
|
1403
|
+
/* @__PURE__ */ jsx3(Look, { design: design2, onChange: (look) => set((d) => ({ ...d, look })) })
|
|
1404
|
+
] })
|
|
1405
|
+
] }),
|
|
1406
|
+
dnd.ghost && /* @__PURE__ */ jsx3(
|
|
1407
|
+
"div",
|
|
1408
|
+
{
|
|
1409
|
+
className: "pointer-events-none fixed z-50 rounded-md border border-primary bg-popover px-2 py-1 text-sm shadow-lg",
|
|
1410
|
+
style: { left: dnd.ghost.x + 12, top: dnd.ghost.y + 8 },
|
|
1411
|
+
children: dnd.ghost.label
|
|
1412
|
+
}
|
|
1413
|
+
)
|
|
1414
|
+
] }) });
|
|
1415
|
+
}
|
|
1416
|
+
function Panel({ title, children, right }) {
|
|
1417
|
+
return /* @__PURE__ */ jsxs2("section", { className: "border-b border-border p-3", children: [
|
|
1418
|
+
/* @__PURE__ */ jsxs2("div", { className: "mb-2 flex items-center justify-between", children: [
|
|
1419
|
+
/* @__PURE__ */ jsx3("h2", { className: "type-meta uppercase", children: title }),
|
|
1420
|
+
right
|
|
1421
|
+
] }),
|
|
1422
|
+
children
|
|
1423
|
+
] });
|
|
1424
|
+
}
|
|
1425
|
+
function Palette({ onAdd, startDrag, click }) {
|
|
1426
|
+
const [q, setQ] = useState2("");
|
|
1427
|
+
const query = q.trim().toLowerCase();
|
|
1428
|
+
const shown = (c) => !query || c.label.toLowerCase().includes(query) || c.id.toLowerCase().includes(query) || c.group.toLowerCase().includes(query);
|
|
1429
|
+
const add = (p) => {
|
|
1430
|
+
const n = nodeFromPayload(p);
|
|
1431
|
+
if (n) onAdd(n);
|
|
1432
|
+
};
|
|
1433
|
+
const design2 = useContext(DesignContext);
|
|
1434
|
+
const uses = useMemo2(() => catalogUseCounts(design2?.nodes ?? []), [design2?.nodes]);
|
|
1435
|
+
const Option = ({ p, icon, label, end, dimmed }) => /* @__PURE__ */ jsx3(
|
|
1436
|
+
Tile2,
|
|
1437
|
+
{
|
|
1438
|
+
layout: "row",
|
|
1439
|
+
variant: "quiet",
|
|
1440
|
+
icon: renderLucide(icon),
|
|
1441
|
+
title: label,
|
|
1442
|
+
end,
|
|
1443
|
+
dimmed: dimmed === true,
|
|
1444
|
+
onPointerDown: (e) => startDrag(e, p, label),
|
|
1445
|
+
onClick: click(() => add(p))
|
|
1446
|
+
}
|
|
1447
|
+
);
|
|
1448
|
+
return /* @__PURE__ */ jsxs2("aside", { className: "flex min-h-0 flex-col border-r border-border", children: [
|
|
1449
|
+
/* @__PURE__ */ jsx3("div", { className: "border-b border-border p-2", children: /* @__PURE__ */ jsx3(SearchField, { "aria-label": "Search options", placeholder: "Search options", value: q, onChange: (e) => setQ(e.target.value) }) }),
|
|
1450
|
+
/* @__PURE__ */ jsxs2("div", { className: "min-h-0 flex-1 overflow-y-auto p-2", children: [
|
|
1451
|
+
!query && /* @__PURE__ */ jsxs2(Fragment2, { children: [
|
|
1452
|
+
/* @__PURE__ */ jsxs2(RowGroup, { title: "Building blocks", children: [
|
|
1453
|
+
/* @__PURE__ */ jsx3(Option, { p: { type: "new", kind: "section" }, icon: "Rows3", label: "Section" }),
|
|
1454
|
+
/* @__PURE__ */ jsx3(Option, { p: { type: "new", kind: "submenu" }, icon: "ChevronsRight", label: "Submenu" }),
|
|
1455
|
+
/* @__PURE__ */ jsx3(Option, { p: { type: "new", kind: "divider" }, icon: "Minus", label: "Divider" }),
|
|
1456
|
+
/* @__PURE__ */ jsx3(Option, { p: { type: "new", kind: "item" }, icon: "Plus", label: "Custom option" }),
|
|
1457
|
+
/* @__PURE__ */ jsx3(Option, { p: { type: "new", kind: "slot-items" }, icon: "ListPlus", label: "Placeholder \u2014 items" }),
|
|
1458
|
+
/* @__PURE__ */ jsx3(Option, { p: { type: "new", kind: "slot-menu" }, icon: "FolderPlus", label: "Placeholder \u2014 submenu" })
|
|
1459
|
+
] }),
|
|
1460
|
+
/* @__PURE__ */ jsx3(RowGroup, { title: "Ready-made", children: TEMPLATES.map((t) => /* @__PURE__ */ jsx3(Option, { p: { type: "template", id: t.id }, icon: t.icon, label: t.label }, t.id)) })
|
|
1461
|
+
] }),
|
|
1462
|
+
CATALOG_GROUPS.map((g) => {
|
|
1463
|
+
const matching = CATALOG.filter((c) => c.group === g && shown(c));
|
|
1464
|
+
const items = [...matching.filter((c) => !uses.has(c.id)), ...matching.filter((c) => uses.has(c.id))];
|
|
1465
|
+
if (items.length === 0) return null;
|
|
1466
|
+
return /* @__PURE__ */ jsx3(RowGroup, { title: g, children: items.map((c) => /* @__PURE__ */ jsx3(
|
|
1467
|
+
Option,
|
|
1468
|
+
{
|
|
1469
|
+
p: { type: "catalog", id: c.id },
|
|
1470
|
+
icon: c.icon,
|
|
1471
|
+
label: c.label,
|
|
1472
|
+
dimmed: uses.has(c.id),
|
|
1473
|
+
...uses.has(c.id) ? { end: /* @__PURE__ */ jsx3(Badge, { children: (uses.get(c.id) ?? 0) > 1 ? `used \xD7${uses.get(c.id)}` : "used" }) } : c.status === "proposed" ? { end: /* @__PURE__ */ jsx3(Badge, { children: "new" }) } : {}
|
|
1474
|
+
},
|
|
1475
|
+
c.id
|
|
1476
|
+
)) }, g);
|
|
1477
|
+
})
|
|
1478
|
+
] })
|
|
1479
|
+
] });
|
|
1480
|
+
}
|
|
1481
|
+
function Structure(props) {
|
|
1482
|
+
const rootOver = props.over !== null && props.over.id === null;
|
|
1483
|
+
return /* @__PURE__ */ jsxs2("main", { className: "flex min-h-0 flex-col overflow-hidden", children: [
|
|
1484
|
+
/* @__PURE__ */ jsxs2("div", { className: "flex items-center justify-between gap-3 border-b border-border px-3 py-2", children: [
|
|
1485
|
+
/* @__PURE__ */ jsx3("h2", { className: "type-meta shrink-0 uppercase", children: "Structure" }),
|
|
1486
|
+
/* @__PURE__ */ jsx3("span", { className: "type-meta min-w-0 truncate", children: "Drag to reorder \xB7 drop on a section or submenu to nest" })
|
|
1487
|
+
] }),
|
|
1488
|
+
/* @__PURE__ */ jsxs2("div", { className: "min-h-0 flex-1 overflow-y-auto p-2", onClick: (e) => e.target === e.currentTarget && props.onSelect(null), children: [
|
|
1489
|
+
/* @__PURE__ */ jsx3(Rows, { ...props, list: props.nodes, depth: 0 }),
|
|
1490
|
+
/* @__PURE__ */ jsx3(
|
|
1491
|
+
"div",
|
|
1492
|
+
{
|
|
1493
|
+
"data-drop": "root",
|
|
1494
|
+
className: `mt-1 flex h-14 items-center justify-center rounded-md border border-dashed text-xs text-muted-foreground ${rootOver ? "border-primary bg-primary/5" : "border-border"}`,
|
|
1495
|
+
children: props.nodes.length === 0 ? "Drag options here to start the menu" : "Drop here to add at the end"
|
|
1496
|
+
}
|
|
1497
|
+
)
|
|
1498
|
+
] }),
|
|
1499
|
+
/* @__PURE__ */ jsx3(Versions, { versions: props.versions, currentDesignId: props.currentDesignId, onOpen: props.onOpen, onArchive: props.onArchive, onStart: props.onStart })
|
|
1500
|
+
] });
|
|
1501
|
+
}
|
|
1502
|
+
function Rows(props) {
|
|
1503
|
+
const { list, depth, over, selected, onSelect, startDrag, click } = props;
|
|
1504
|
+
return /* @__PURE__ */ jsx3(Fragment2, { children: list.map((node) => {
|
|
1505
|
+
const container = isContainer(node);
|
|
1506
|
+
const isOver = over?.id === node.id;
|
|
1507
|
+
const label = node.kind === "divider" ? "\u2500\u2500\u2500\u2500 divider \u2500\u2500\u2500\u2500" : node.label || (node.kind === "section" ? "(unlabelled section)" : "(no label)");
|
|
1508
|
+
const icon = node.kind === "item" || node.kind === "submenu" ? node.icon : node.kind === "slot" ? node.icon ?? slotSource(node.fills)?.icon ?? "Ellipsis" : node.kind === "section" ? "Rows3" : "Minus";
|
|
1509
|
+
return /* @__PURE__ */ jsxs2("div", { children: [
|
|
1510
|
+
/* @__PURE__ */ jsxs2(
|
|
1511
|
+
"div",
|
|
1512
|
+
{
|
|
1513
|
+
"data-drop": node.id,
|
|
1514
|
+
...container ? { "data-container": "1" } : {},
|
|
1515
|
+
onPointerDown: (e) => {
|
|
1516
|
+
if (e.target.closest("button")) return;
|
|
1517
|
+
startDrag(e, { type: "move", id: node.id }, label);
|
|
1518
|
+
},
|
|
1519
|
+
onClick: click(() => onSelect(node.id)),
|
|
1520
|
+
style: { marginLeft: depth * 18 },
|
|
1521
|
+
className: [
|
|
1522
|
+
"group relative flex h-8 cursor-grab touch-none items-center gap-2 rounded-md border px-2 text-sm",
|
|
1523
|
+
selected === node.id ? "border-primary bg-primary/5" : "border-transparent hover:bg-accent",
|
|
1524
|
+
isOver && over?.pos === "inside" ? "ring-2 ring-primary" : "",
|
|
1525
|
+
container ? "font-medium" : ""
|
|
1526
|
+
].join(" "),
|
|
1527
|
+
children: [
|
|
1528
|
+
isOver && over?.pos === "before" && /* @__PURE__ */ jsx3("span", { className: "pointer-events-none absolute inset-x-0 -top-px h-0.5 rounded bg-primary" }),
|
|
1529
|
+
isOver && over?.pos === "after" && /* @__PURE__ */ jsx3("span", { className: "pointer-events-none absolute inset-x-0 -bottom-px h-0.5 rounded bg-primary" }),
|
|
1530
|
+
/* @__PURE__ */ jsx3("span", { className: "text-muted-foreground", children: "\u22EE\u22EE" }),
|
|
1531
|
+
icon && /* @__PURE__ */ jsx3("span", { className: "text-muted-foreground", children: renderLucide(icon, "size-4") }),
|
|
1532
|
+
/* @__PURE__ */ jsx3("span", { className: `min-w-0 flex-1 truncate ${node.kind === "divider" ? "text-xs text-muted-foreground" : ""}`, children: label }),
|
|
1533
|
+
node.kind === "submenu" && /* @__PURE__ */ jsx3("span", { className: "text-[10px] text-muted-foreground", children: "submenu" }),
|
|
1534
|
+
node.kind === "slot" && /* @__PURE__ */ jsx3(Badge, { children: node.shape === "submenu" ? "placeholder submenu" : "placeholder" }),
|
|
1535
|
+
node.kind === "section" && /* @__PURE__ */ jsx3("span", { className: "text-[10px] text-muted-foreground", children: node.layout && node.layout !== "list" ? `${node.layout} section` : "section" }),
|
|
1536
|
+
node.kind === "item" && node.shortcut && /* @__PURE__ */ jsx3("kbd", { className: "text-[10px] text-muted-foreground", children: node.shortcut }),
|
|
1537
|
+
/* @__PURE__ */ jsxs2("span", { className: "hidden items-center gap-0.5 group-hover:flex", children: [
|
|
1538
|
+
/* @__PURE__ */ jsx3(RowAction, { title: "Duplicate", onClick: () => props.onDuplicate(node.id), icon: "CopyPlus" }),
|
|
1539
|
+
container ? /* @__PURE__ */ jsx3(RowAction, { title: "Unwrap (keep its options)", onClick: () => props.onUnwrap(node.id), icon: "Ungroup" }) : /* @__PURE__ */ jsxs2(Fragment2, { children: [
|
|
1540
|
+
/* @__PURE__ */ jsx3(RowAction, { title: "Put in a new section", onClick: () => props.onWrap(node.id, "section"), icon: "Rows3" }),
|
|
1541
|
+
/* @__PURE__ */ jsx3(RowAction, { title: "Put in a new submenu", onClick: () => props.onWrap(node.id, "submenu"), icon: "ChevronsRight" })
|
|
1542
|
+
] }),
|
|
1543
|
+
/* @__PURE__ */ jsx3(RowAction, { title: "Delete", onClick: () => props.onRemove(node.id), icon: "Trash2" })
|
|
1544
|
+
] })
|
|
1545
|
+
]
|
|
1546
|
+
}
|
|
1547
|
+
),
|
|
1548
|
+
container && /* @__PURE__ */ jsx3(Fragment2, { children: node.children.length === 0 ? /* @__PURE__ */ jsxs2("div", { style: { marginLeft: (depth + 1) * 18 }, className: "my-0.5 rounded-md border border-dashed border-border px-2 py-1 text-[11px] text-muted-foreground", children: [
|
|
1549
|
+
"Empty \u2014 drop options onto \u201C",
|
|
1550
|
+
node.label || "section",
|
|
1551
|
+
"\u201D"
|
|
1552
|
+
] }) : /* @__PURE__ */ jsx3(Rows, { ...props, list: node.children, depth: depth + 1 }) })
|
|
1553
|
+
] }, node.id);
|
|
1554
|
+
}) });
|
|
1555
|
+
}
|
|
1556
|
+
function RowAction({ title, onClick, icon }) {
|
|
1557
|
+
return /* @__PURE__ */ jsx3(
|
|
1558
|
+
Button,
|
|
1559
|
+
{
|
|
1560
|
+
variant: "quiet",
|
|
1561
|
+
icon: renderLucide(icon),
|
|
1562
|
+
"aria-label": title,
|
|
1563
|
+
title,
|
|
1564
|
+
onClick: (e) => {
|
|
1565
|
+
e.stopPropagation();
|
|
1566
|
+
onClick();
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1569
|
+
);
|
|
1570
|
+
}
|
|
1571
|
+
function Versions({
|
|
1572
|
+
versions,
|
|
1573
|
+
currentDesignId,
|
|
1574
|
+
onOpen,
|
|
1575
|
+
onArchive,
|
|
1576
|
+
onStart
|
|
1577
|
+
}) {
|
|
1578
|
+
const [openId, setOpenId] = useState2(null);
|
|
1579
|
+
const [startersOpen, setStartersOpen] = useState2(false);
|
|
1580
|
+
return /* @__PURE__ */ jsxs2("div", { className: "max-h-[45%] min-h-0 overflow-y-auto border-t border-border p-2", children: [
|
|
1581
|
+
/* @__PURE__ */ jsx3(DisclosureHeader, { open: startersOpen, onClick: () => setStartersOpen((o2) => !o2), title: "Starting points", meta: `${STARTER_DESIGNS.length}` }),
|
|
1582
|
+
startersOpen && ["Live today", "Proposal"].map((group) => /* @__PURE__ */ jsx3(RowGroup, { title: group, children: STARTER_DESIGNS.filter((st) => st.group === group).map((st) => /* @__PURE__ */ jsx3(Tile2, { layout: "row", variant: "quiet", icon: renderLucide(group === "Live today" ? "Radio" : "Lightbulb"), title: st.design.name, line: st.source, onClick: () => {
|
|
1583
|
+
onStart(st);
|
|
1584
|
+
setStartersOpen(false);
|
|
1585
|
+
} }, st.id)) }, group)),
|
|
1586
|
+
/* @__PURE__ */ jsxs2(RowGroup, { title: "Saved designs", children: [
|
|
1587
|
+
versions === null && /* @__PURE__ */ jsx3(RegionSkeleton, { shape: "rows" }),
|
|
1588
|
+
versions?.length === 0 && /* @__PURE__ */ jsx3(EmptyState, { icon: renderLucide("Save"), title: "Nothing saved yet", line: "\u2318S saves a version" }),
|
|
1589
|
+
(versions ?? []).map((group) => {
|
|
1590
|
+
const latest = group[0];
|
|
1591
|
+
const expanded = openId === latest.designId;
|
|
1592
|
+
return /* @__PURE__ */ jsxs2("div", { children: [
|
|
1593
|
+
/* @__PURE__ */ jsx3(
|
|
1594
|
+
DisclosureHeader,
|
|
1595
|
+
{
|
|
1596
|
+
open: expanded,
|
|
1597
|
+
onClick: () => setOpenId(expanded ? null : latest.designId),
|
|
1598
|
+
title: latest.name,
|
|
1599
|
+
meta: `${group.length} version${group.length === 1 ? "" : "s"}${latest.designId === currentDesignId ? " \xB7 open" : ""}`
|
|
1600
|
+
}
|
|
1601
|
+
),
|
|
1602
|
+
expanded && group.map((v) => /* @__PURE__ */ jsxs2("div", { className: "flex items-center pl-5", children: [
|
|
1603
|
+
/* @__PURE__ */ jsx3(
|
|
1604
|
+
Tile2,
|
|
1605
|
+
{
|
|
1606
|
+
className: "min-w-0 flex-1",
|
|
1607
|
+
layout: "row",
|
|
1608
|
+
variant: "quiet",
|
|
1609
|
+
title: `v${v.version}${v.note ? ` \u2014 ${v.note}` : ""}`,
|
|
1610
|
+
line: v.savedAt ? new Date(v.savedAt).toLocaleString() : void 0,
|
|
1611
|
+
onClick: () => onOpen(v)
|
|
1612
|
+
}
|
|
1613
|
+
),
|
|
1614
|
+
/* @__PURE__ */ jsx3(RowAction, { title: "Archive this version", onClick: () => onArchive(v), icon: "Archive" })
|
|
1615
|
+
] }, v.rowId))
|
|
1616
|
+
] }, latest.designId);
|
|
1617
|
+
})
|
|
1618
|
+
] })
|
|
1619
|
+
] });
|
|
1620
|
+
}
|
|
1621
|
+
function Stage({ design: design2, selected, onSelect }) {
|
|
1622
|
+
const [at, setAt] = useState2(null);
|
|
1623
|
+
const [picked, setPicked] = useState2(null);
|
|
1624
|
+
const ref = useRef(null);
|
|
1625
|
+
return /* @__PURE__ */ jsx3(Panel, { title: "Live menu", right: /* @__PURE__ */ jsx3("span", { className: "text-[11px] text-muted-foreground", children: "Right-click the grey area to open it there" }), children: /* @__PURE__ */ jsxs2(
|
|
1626
|
+
"div",
|
|
1627
|
+
{
|
|
1628
|
+
ref,
|
|
1629
|
+
onContextMenu: (e) => {
|
|
1630
|
+
e.preventDefault();
|
|
1631
|
+
const r = ref.current?.getBoundingClientRect();
|
|
1632
|
+
if (r) setAt({ x: Math.max(8, Math.min(e.clientX - r.left, r.width - design2.look.width - 8)), y: Math.max(8, Math.min(e.clientY - r.top, r.height - (ref.current?.querySelector(".cm-menu")?.getBoundingClientRect().height ?? 160) - 8)) });
|
|
1633
|
+
},
|
|
1634
|
+
onClick: (e) => {
|
|
1635
|
+
if (e.target === e.currentTarget) setAt(null);
|
|
1636
|
+
},
|
|
1637
|
+
"data-menu-bounds": "",
|
|
1638
|
+
className: "relative min-h-[300px] overflow-visible rounded-lg bg-muted/60 p-4",
|
|
1639
|
+
children: [
|
|
1640
|
+
at ? /* @__PURE__ */ jsx3(
|
|
1641
|
+
MenuPreview,
|
|
1642
|
+
{
|
|
1643
|
+
design: design2,
|
|
1644
|
+
renderIcon: renderLucide,
|
|
1645
|
+
highlightId: selected,
|
|
1646
|
+
onSelectNode: onSelect,
|
|
1647
|
+
onPick: (i) => setPicked(i.label),
|
|
1648
|
+
style: { position: "absolute", left: at.x, top: at.y, zIndex: 20 }
|
|
1649
|
+
}
|
|
1650
|
+
) : /* @__PURE__ */ jsx3(MenuPreview, { design: design2, renderIcon: renderLucide, highlightId: selected, onSelectNode: onSelect, onPick: (i) => setPicked(i.label) }),
|
|
1651
|
+
picked && /* @__PURE__ */ jsxs2("div", { className: "absolute right-3 bottom-2 text-xs text-muted-foreground", children: [
|
|
1652
|
+
"Picked: ",
|
|
1653
|
+
picked
|
|
1654
|
+
] })
|
|
1655
|
+
]
|
|
1656
|
+
}
|
|
1657
|
+
) });
|
|
1658
|
+
}
|
|
1659
|
+
var DENSITY_OPTIONS = [
|
|
1660
|
+
{ value: "comfortable", label: "Comfortable" },
|
|
1661
|
+
{ value: "compact", label: "Compact" }
|
|
1662
|
+
];
|
|
1663
|
+
function Look({ design: design2, onChange }) {
|
|
1664
|
+
const look = design2.look;
|
|
1665
|
+
const toggle = (k, label) => /* @__PURE__ */ jsx3(SettingRow, { label, children: /* @__PURE__ */ jsx3(Switch, { "aria-label": label, checked: look[k], onCheckedChange: (v) => onChange({ ...look, [k]: v }) }) });
|
|
1666
|
+
return /* @__PURE__ */ jsxs2(Panel, { title: "Look", right: /* @__PURE__ */ jsx3(Button, { variant: "quiet", onClick: () => onChange({ ...DEFAULT_LOOK }), children: "Reset" }), children: [
|
|
1667
|
+
/* @__PURE__ */ jsx3(SettingRow, { label: "Density", children: /* @__PURE__ */ jsx3(SegmentedControl, { "aria-label": "Density", value: look.density, data: DENSITY_OPTIONS, onValueChange: (density) => onChange({ ...look, density }) }) }),
|
|
1668
|
+
/* @__PURE__ */ jsx3(SettingRow, { label: "Width", children: /* @__PURE__ */ jsx3(NumberStepper, { value: look.width, min: 180, max: 420, step: 10, onChange: (width) => onChange({ ...look, width }) }) }),
|
|
1669
|
+
toggle("showIcons", "Icons"),
|
|
1670
|
+
toggle("showShortcuts", "Shortcuts"),
|
|
1671
|
+
toggle("showHints", "Hints"),
|
|
1672
|
+
toggle("search", "Search box")
|
|
1673
|
+
] });
|
|
1674
|
+
}
|
|
1675
|
+
var LAYOUT_OPTIONS = [
|
|
1676
|
+
{ value: "list", label: "List" },
|
|
1677
|
+
{ value: "row", label: "Icon row" },
|
|
1678
|
+
{ value: "grid", label: "Icon grid" }
|
|
1679
|
+
];
|
|
1680
|
+
var FLAGS = [
|
|
1681
|
+
["ai", "AI"],
|
|
1682
|
+
["danger", "Destructive"],
|
|
1683
|
+
["toggle", "Checkbox"],
|
|
1684
|
+
["disabled", "Disabled"]
|
|
1685
|
+
];
|
|
1686
|
+
function Inspector({ node, onChange }) {
|
|
1687
|
+
const catalog = node.catalogId ? catalogOption(node.catalogId) : void 0;
|
|
1688
|
+
return /* @__PURE__ */ jsxs2(Panel, { title: `Selected ${node.kind === "slot" ? "placeholder" : node.kind}`, right: catalog ? /* @__PURE__ */ jsx3(Badge, { children: catalog.id }) : void 0, children: [
|
|
1689
|
+
node.kind === "divider" ? /* @__PURE__ */ jsx3("p", { className: "type-secondary", children: "A divider has no settings." }) : /* @__PURE__ */ jsxs2(Fragment2, { children: [
|
|
1690
|
+
/* @__PURE__ */ jsx3(Labelled, { label: "Label", children: /* @__PURE__ */ jsx3(Field, { "aria-label": "Label", value: node.label, onChange: (e) => onChange({ label: e.target.value }) }) }),
|
|
1691
|
+
node.kind === "slot" && /* @__PURE__ */ jsx3(SlotSettings, { node, onChange }),
|
|
1692
|
+
(node.kind === "item" || node.kind === "submenu") && /* @__PURE__ */ jsxs2(Fragment2, { children: [
|
|
1693
|
+
/* @__PURE__ */ jsx3(Labelled, { label: "Icon", children: /* @__PURE__ */ jsx3(IconPicker, { value: node.icon ?? "", onChange: (icon) => onChange({ icon }) }) }),
|
|
1694
|
+
/* @__PURE__ */ jsx3(Labelled, { label: "Hint", children: /* @__PURE__ */ jsx3(Field, { "aria-label": "Hint", value: node.hint ?? "", onChange: (e) => onChange({ hint: e.target.value }) }) })
|
|
1695
|
+
] }),
|
|
1696
|
+
node.kind === "item" && /* @__PURE__ */ jsxs2(Fragment2, { children: [
|
|
1697
|
+
/* @__PURE__ */ jsx3(Labelled, { label: "Shortcut", children: /* @__PURE__ */ jsx3(Field, { "aria-label": "Shortcut", value: node.shortcut ?? "", placeholder: "\u2318K", onChange: (e) => onChange({ shortcut: e.target.value }) }) }),
|
|
1698
|
+
FLAGS.map(([k, label]) => /* @__PURE__ */ jsx3(SettingRow, { label, children: /* @__PURE__ */ jsx3(Switch, { "aria-label": label, checked: Boolean(node[k]), onCheckedChange: (v) => onChange({ [k]: v }) }) }, k))
|
|
1699
|
+
] }),
|
|
1700
|
+
node.kind === "section" && /* @__PURE__ */ jsxs2(Fragment2, { children: [
|
|
1701
|
+
/* @__PURE__ */ jsx3(SettingRow, { label: "Layout", children: /* @__PURE__ */ jsx3(SegmentedControl, { "aria-label": "Layout", value: node.layout ?? "list", data: LAYOUT_OPTIONS, onValueChange: (layout) => onChange({ layout }) }) }),
|
|
1702
|
+
/* @__PURE__ */ jsx3(SettingRow, { label: "Starts folded", children: /* @__PURE__ */ jsx3(Switch, { "aria-label": "Starts folded", checked: Boolean(node.collapsible), onCheckedChange: (v) => onChange({ collapsible: v }) }) })
|
|
1703
|
+
] })
|
|
1704
|
+
] }),
|
|
1705
|
+
/* @__PURE__ */ jsx3(Labelled, { label: "Note for the build", children: /* @__PURE__ */ jsx3(Textarea, { "aria-label": "Note for the build", rows: 2, autoGrow: true, value: node.note ?? "", onChange: (e) => onChange({ note: e.target.value }) }) })
|
|
1706
|
+
] });
|
|
1707
|
+
}
|
|
1708
|
+
var SLOT_SHAPES = [
|
|
1709
|
+
{ value: "items", label: "Rows here" },
|
|
1710
|
+
{ value: "submenu", label: "One submenu" }
|
|
1711
|
+
];
|
|
1712
|
+
var SLOT_OPTIONS = SLOT_SOURCES.map((src) => ({ value: src.id, label: src.label }));
|
|
1713
|
+
function SlotSettings({ node, onChange }) {
|
|
1714
|
+
const source = slotSource(node.fills);
|
|
1715
|
+
return /* @__PURE__ */ jsxs2(Fragment2, { children: [
|
|
1716
|
+
/* @__PURE__ */ jsx3(Labelled, { label: "Holds", children: /* @__PURE__ */ jsx3(
|
|
1717
|
+
Select,
|
|
1718
|
+
{
|
|
1719
|
+
"aria-label": "What this placeholder holds",
|
|
1720
|
+
value: node.fills,
|
|
1721
|
+
options: SLOT_OPTIONS,
|
|
1722
|
+
onValueChange: (fills) => {
|
|
1723
|
+
const relabel = !node.label || node.label === source?.phrase || node.label === "Everything else" || node.label === "More";
|
|
1724
|
+
onChange({ fills, ...relabel ? { label: slotSource(fills)?.phrase ?? node.label } : {} });
|
|
1725
|
+
}
|
|
1726
|
+
}
|
|
1727
|
+
) }),
|
|
1728
|
+
/* @__PURE__ */ jsx3(SettingRow, { label: "Shows as", children: /* @__PURE__ */ jsx3(SegmentedControl, { "aria-label": "Shows as", value: node.shape, data: SLOT_SHAPES, onValueChange: (shape) => onChange({ shape }) }) }),
|
|
1729
|
+
/* @__PURE__ */ jsx3(SlotContents, { fills: node.fills })
|
|
1730
|
+
] });
|
|
1731
|
+
}
|
|
1732
|
+
function SlotContents({ fills }) {
|
|
1733
|
+
const source = slotSource(fills);
|
|
1734
|
+
const design2 = useContext(DesignContext);
|
|
1735
|
+
const left = useMemo2(() => design2 ? slotOptions(fills, placedCatalogIds(design2.nodes)) : [], [design2, fills]);
|
|
1736
|
+
if (source?.runtime) return /* @__PURE__ */ jsx3("p", { className: "type-secondary mt-2", children: "Filled by the live menu at runtime." });
|
|
1737
|
+
return /* @__PURE__ */ jsx3("p", { className: "type-secondary mt-2", children: left.length === 0 ? "Nothing left \u2014 every option of this kind is already placed." : `Holds ${left.length}: ${left.slice(0, 6).map((o2) => o2.label).join(", ")}${left.length > 6 ? "\u2026" : ""}` });
|
|
1738
|
+
}
|
|
1739
|
+
function Labelled({ label, children }) {
|
|
1740
|
+
return /* @__PURE__ */ jsxs2("div", { className: "mt-2 grid gap-1", children: [
|
|
1741
|
+
/* @__PURE__ */ jsx3("span", { className: "type-meta", children: label }),
|
|
1742
|
+
children
|
|
1743
|
+
] });
|
|
1744
|
+
}
|
|
1745
|
+
function IconPicker({ value, onChange }) {
|
|
1746
|
+
const [q, setQ] = useState2("");
|
|
1747
|
+
const [open, setOpen] = useState2(false);
|
|
1748
|
+
const shown = useMemo2(() => {
|
|
1749
|
+
const s = q.trim().toLowerCase();
|
|
1750
|
+
return (s ? ICON_NAMES.filter((n) => n.toLowerCase().includes(s)) : ICON_NAMES).slice(0, 96);
|
|
1751
|
+
}, [q]);
|
|
1752
|
+
return /* @__PURE__ */ jsxs2("div", { className: "relative", children: [
|
|
1753
|
+
/* @__PURE__ */ jsx3(
|
|
1754
|
+
Button,
|
|
1755
|
+
{
|
|
1756
|
+
variant: "outline",
|
|
1757
|
+
icon: value ? renderLucide(value) : void 0,
|
|
1758
|
+
iconEnd: renderLucide("ChevronDown"),
|
|
1759
|
+
"aria-expanded": open,
|
|
1760
|
+
onClick: () => setOpen((o2) => !o2),
|
|
1761
|
+
children: value || "No icon"
|
|
1762
|
+
}
|
|
1763
|
+
),
|
|
1764
|
+
open && /* @__PURE__ */ jsxs2("div", { className: "absolute z-30 mt-1 w-full rounded-md border border-border bg-popover p-2 shadow-lg", children: [
|
|
1765
|
+
/* @__PURE__ */ jsx3(SearchField, { autoFocus: true, "aria-label": "Search icons", placeholder: `Search ${ICON_NAMES.length} icons`, value: q, onChange: (e) => setQ(e.target.value) }),
|
|
1766
|
+
/* @__PURE__ */ jsxs2("div", { className: "mt-2 grid max-h-56 grid-cols-8 overflow-y-auto", children: [
|
|
1767
|
+
/* @__PURE__ */ jsx3(Button, { variant: "quiet", icon: renderLucide("Ban"), "aria-label": "No icon", title: "No icon", pressed: !value, onClick: () => {
|
|
1768
|
+
onChange("");
|
|
1769
|
+
setOpen(false);
|
|
1770
|
+
} }),
|
|
1771
|
+
shown.map((n) => /* @__PURE__ */ jsx3(
|
|
1772
|
+
Button,
|
|
1773
|
+
{
|
|
1774
|
+
variant: "quiet",
|
|
1775
|
+
icon: renderLucide(n),
|
|
1776
|
+
"aria-label": n,
|
|
1777
|
+
title: n,
|
|
1778
|
+
pressed: n === value,
|
|
1779
|
+
onClick: () => {
|
|
1780
|
+
onChange(n);
|
|
1781
|
+
setOpen(false);
|
|
1782
|
+
}
|
|
1783
|
+
},
|
|
1784
|
+
n
|
|
1785
|
+
))
|
|
1786
|
+
] })
|
|
1787
|
+
] })
|
|
1788
|
+
] });
|
|
1789
|
+
}
|
|
1790
|
+
export {
|
|
1791
|
+
ICON_NAMES,
|
|
1792
|
+
MenuBuilder,
|
|
1793
|
+
renderLucide
|
|
1794
|
+
};
|
|
1795
|
+
//# sourceMappingURL=builder.js.map
|