@ai-matrx/context-menu 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.2
4
+
5
+ Automatic changed-only republish (docs/metadata drift since the last tag — see
6
+ `git diff npm/context-menu/v0.1.1..npm/context-menu/v0.1.2 -- apps/shared/context-menu`).
7
+ No source changes intended and no consumer action required.
8
+
9
+ ## 0.1.1
10
+
11
+ Automatic changed-only republish (docs/metadata drift since the last tag — see
12
+ `git diff npm/context-menu/v0.1.0..npm/context-menu/v0.1.1 -- apps/shared/context-menu`).
13
+ No source changes intended and no consumer action required.
14
+
3
15
  ## 0.1.0
4
16
 
5
17
  - The menu design model, option catalog, renderer, versioned design store and drag-and-drop builder.
@@ -14,3 +26,4 @@
14
26
  - Repoint `@/features/context-menu-v3/*` imports: components and hooks → `@ai-matrx/context-menu/react`,
15
27
  everything else → `@ai-matrx/context-menu/engine`; then delete `features/context-menu-v3/`.
16
28
 
29
+
package/dist/builder.cjs CHANGED
@@ -180,6 +180,25 @@ var TEMPLATES = [
180
180
  { id: "sub-organize", label: "Organize \u25B8", icon: "Tags", build: () => sub("Organize", "Tags", ["cm:attach", "pin-message", "cm:view-history", "cm:compare"]) },
181
181
  { id: "sub-tools", label: "Tools \u25B8", icon: "Wrench", build: () => sub("Tools", "Wrench", ["cm:speak", "cm:quick-actions", "feedback", "cm:x:surface", "cm:admin"]) }
182
182
  ];
183
+ var SLOT_SOURCES = [
184
+ { id: "any", label: "Everything not placed", phrase: "Everything else", icon: "Ellipsis" },
185
+ { id: "agents", label: "Agents & shortcuts (library)", phrase: "All other agents", icon: "Bot", runtime: true },
186
+ { id: "page-rows", label: "This page's own rows", phrase: "All of this page's actions", icon: "PanelTop", runtime: true },
187
+ ...CATALOG_GROUPS.map((g) => ({
188
+ id: `group:${g}`,
189
+ label: g,
190
+ phrase: g === "AI" ? "All other AI items" : `All other ${g.toLowerCase()} options`,
191
+ icon: CATALOG.find((c) => c.group === g)?.icon ?? "Circle"
192
+ }))
193
+ ];
194
+ function slotSource(id) {
195
+ return SLOT_SOURCES.find((s) => s.id === id);
196
+ }
197
+ function slotOptions(fills, placed) {
198
+ if (fills === "agents" || fills === "page-rows") return [];
199
+ const pool = fills === "any" ? CATALOG : CATALOG.filter((c) => `group:${c.group}` === fills);
200
+ return pool.filter((c) => !placed.has(c.id));
201
+ }
183
202
 
184
203
  // src/design/MenuPreview.tsx
185
204
  var import_react = require("react");
@@ -213,6 +232,20 @@ function emptyDesign(name = "Untitled menu") {
213
232
  function isContainer(node) {
214
233
  return node.kind === "section" || node.kind === "submenu";
215
234
  }
235
+ function placedCatalogIds(nodes, out = /* @__PURE__ */ new Set()) {
236
+ for (const n of nodes) {
237
+ if (n.catalogId) out.add(n.catalogId);
238
+ if (isContainer(n)) placedCatalogIds(n.children, out);
239
+ }
240
+ return out;
241
+ }
242
+ function catalogUseCounts(nodes, out = /* @__PURE__ */ new Map()) {
243
+ for (const n of nodes) {
244
+ if (n.catalogId) out.set(n.catalogId, (out.get(n.catalogId) ?? 0) + 1);
245
+ if (isContainer(n)) catalogUseCounts(n.children, out);
246
+ }
247
+ return out;
248
+ }
216
249
  function cloneNode(node) {
217
250
  const copy = { ...node, id: newId(node.kind[0]) };
218
251
  if (isContainer(copy)) copy.children = copy.children.map(cloneNode);
@@ -236,7 +269,7 @@ function contains(node, id) {
236
269
  function countItems(nodes) {
237
270
  let n = 0;
238
271
  for (const node of nodes) {
239
- if (node.kind === "item") n += 1;
272
+ if (node.kind === "item" || node.kind === "slot") n += 1;
240
273
  else if (isContainer(node)) n += countItems(node.children) + (node.kind === "submenu" ? 1 : 0);
241
274
  }
242
275
  return n;
@@ -326,7 +359,7 @@ var import_jsx_runtime = require("react/jsx-runtime");
326
359
  function matches(node, q) {
327
360
  if (!q) return true;
328
361
  if (node.kind === "divider") return false;
329
- if (node.kind === "item") return node.label.toLowerCase().includes(q);
362
+ if (node.kind === "item" || node.kind === "slot") return node.label.toLowerCase().includes(q);
330
363
  return node.label.toLowerCase().includes(q) || node.children.some((c) => matches(c, q));
331
364
  }
332
365
  function flatten(nodes, q, out = []) {
@@ -341,6 +374,8 @@ function MenuPreview({ design: design2, renderIcon, highlightId, onSelectNode, o
341
374
  const q = query.trim().toLowerCase();
342
375
  const { look } = design2;
343
376
  const results = (0, import_react.useMemo)(() => q ? flatten(design2.nodes, q) : null, [design2.nodes, q]);
377
+ const placed = (0, import_react.useMemo)(() => placedCatalogIds(design2.nodes), [design2.nodes]);
378
+ const ctx = { design: design2, renderIcon, highlightId, onSelectNode, onPick, placed };
344
379
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
345
380
  "div",
346
381
  {
@@ -353,7 +388,7 @@ function MenuPreview({ design: design2, renderIcon, highlightId, onSelectNode, o
353
388
  renderIcon("Search", "cm-icon"),
354
389
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("input", { value: query, onChange: (e) => setQuery(e.target.value), placeholder: "Type to filter\u2026", "aria-label": "Filter menu" })
355
390
  ] }),
356
- results ? results.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "cm-empty", children: "No matches" }) : results.map((n) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Item, { node: n, ctx: { design: design2, renderIcon, highlightId, onSelectNode, onPick } }, n.id)) : design2.nodes.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "cm-empty", children: "Empty menu \u2014 drag options in" }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Nodes, { nodes: design2.nodes, ctx: { design: design2, renderIcon, highlightId, onSelectNode, onPick } })
391
+ results ? results.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "cm-empty", children: "No matches" }) : results.map((n) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Item, { node: n, ctx }, n.id)) : design2.nodes.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "cm-empty", children: "Empty menu \u2014 drag options in" }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Nodes, { nodes: design2.nodes, ctx })
357
392
  ]
358
393
  }
359
394
  );
@@ -363,6 +398,7 @@ function Nodes({ nodes, ctx }) {
363
398
  if (n.kind === "divider") return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { role: "separator", className: "cm-divider" }, n.id);
364
399
  if (n.kind === "item") return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Item, { node: n, ctx }, n.id);
365
400
  if (n.kind === "submenu") return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Submenu, { node: n, ctx }, n.id);
401
+ if (n.kind === "slot") return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Slot, { node: n, ctx }, n.id);
366
402
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Section, { node: n, ctx }, n.id);
367
403
  }) });
368
404
  }
@@ -446,8 +482,7 @@ function Submenu({ node, ctx }) {
446
482
  const [open, setOpen] = (0, import_react.useState)(false);
447
483
  const [side, setSide] = (0, import_react.useState)("right");
448
484
  const show = (el) => {
449
- const r = el.getBoundingClientRect();
450
- setSide(r.right + look.width + 8 > window.innerWidth ? "left" : "right");
485
+ setSide(flyoutSide(el, look.width));
451
486
  setOpen(true);
452
487
  };
453
488
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "cm-sub", onMouseEnter: (e) => show(e.currentTarget), onMouseLeave: () => setOpen(false), children: [
@@ -476,6 +511,63 @@ function Submenu({ node, ctx }) {
476
511
  open && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { role: "menu", className: "cm-menu cm-flyout", "data-side": side, "data-density": look.density, style: { width: look.width }, children: node.children.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "cm-empty", children: "Empty submenu" }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Nodes, { nodes: node.children, ctx }) })
477
512
  ] });
478
513
  }
514
+ function flyoutSide(row, width) {
515
+ const r = row.getBoundingClientRect();
516
+ const bounds = row.closest("[data-menu-bounds]")?.getBoundingClientRect() ?? { left: 0, right: window.innerWidth };
517
+ if (r.right + width + 8 <= bounds.right) return "right";
518
+ if (r.left - width - 8 >= bounds.left) return "left";
519
+ return "below";
520
+ }
521
+ function Slot({ node, ctx }) {
522
+ const { look } = ctx.design;
523
+ const source = slotSource(node.fills);
524
+ const options = slotOptions(node.fills, ctx.placed);
525
+ const [open, setOpen] = (0, import_react.useState)(false);
526
+ const [side, setSide] = (0, import_react.useState)("right");
527
+ const count = source?.runtime ? "live" : String(options.length);
528
+ const row = (
529
+ // ui-exception: a menu row of the menu component itself
530
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
531
+ "button",
532
+ {
533
+ type: "button",
534
+ role: "menuitem",
535
+ "aria-haspopup": node.shape === "submenu" ? "menu" : void 0,
536
+ title: source ? `Placeholder: ${source.label}` : "Placeholder",
537
+ className: `cm-item cm-slot ${ctx.highlightId === node.id ? "cm-hl" : ""}`,
538
+ onClick: () => {
539
+ ctx.onSelectNode?.(node.id);
540
+ if (node.shape === "submenu") setOpen(true);
541
+ },
542
+ children: [
543
+ look.showIcons && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "cm-icon-slot", children: ctx.renderIcon(node.icon ?? source?.icon ?? "Ellipsis", "cm-icon") }),
544
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "cm-text", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "cm-label", children: node.label }) }),
545
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "cm-count", children: count }),
546
+ node.shape === "submenu" && ctx.renderIcon("ChevronRight", "cm-icon cm-chev")
547
+ ]
548
+ }
549
+ )
550
+ );
551
+ if (node.shape === "items") return row;
552
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
553
+ "div",
554
+ {
555
+ className: "cm-sub",
556
+ onMouseEnter: (e) => {
557
+ setSide(flyoutSide(e.currentTarget, look.width));
558
+ setOpen(true);
559
+ },
560
+ onMouseLeave: () => setOpen(false),
561
+ children: [
562
+ row,
563
+ open && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { role: "menu", className: "cm-menu cm-flyout", "data-side": side, "data-density": look.density, style: { width: look.width }, children: source?.runtime ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "cm-runtime", children: [
564
+ "Filled at runtime: ",
565
+ source.label
566
+ ] }) : options.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "cm-empty", children: "Nothing left \u2014 every option of this kind is placed" }) : options.map((o2) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Item, { node: { id: `slot-${node.id}-${o2.id}`, kind: "item", label: o2.label, icon: o2.icon, catalogId: o2.id, ...o2.shortcut ? { shortcut: o2.shortcut } : {}, ...o2.ai ? { ai: true } : {}, ...o2.danger ? { danger: true } : {} }, ctx }, o2.id)) })
567
+ ]
568
+ }
569
+ );
570
+ }
479
571
 
480
572
  // src/design/starters.ts
481
573
  function maker(slug) {
@@ -985,6 +1077,7 @@ var SURFACE_LABELS = {
985
1077
  anywhere: "Opens anywhere"
986
1078
  };
987
1079
  var SURFACE_OPTIONS = MENU_SURFACES.map((value) => ({ value, label: SURFACE_LABELS[value] }));
1080
+ var DesignContext = (0, import_react2.createContext)(null);
988
1081
  var DRAFT_KEY = "matrx.context-menu.builder.draft.v1";
989
1082
  function usePointerDrag(onDrop) {
990
1083
  const [ghost, setGhost] = (0, import_react2.useState)(null);
@@ -1060,6 +1153,8 @@ function nodeFromPayload(p) {
1060
1153
  if (p.kind === "section") return { id: newId("s"), kind: "section", label: "New section", children: [] };
1061
1154
  if (p.kind === "submenu") return { id: newId("m"), kind: "submenu", label: "More", icon: "Ellipsis", children: [] };
1062
1155
  if (p.kind === "divider") return { id: newId("d"), kind: "divider" };
1156
+ if (p.kind === "slot-items") return { id: newId("p"), kind: "slot", shape: "items", fills: "any", label: "Everything else" };
1157
+ if (p.kind === "slot-menu") return { id: newId("p"), kind: "slot", shape: "submenu", fills: "any", label: "More", icon: "Ellipsis" };
1063
1158
  return { id: newId("i"), kind: "item", label: "New option", icon: "Circle" };
1064
1159
  }
1065
1160
  return null;
@@ -1213,7 +1308,7 @@ function MenuBuilder({ store, storeLabel, initialDesign, accountSlot }) {
1213
1308
  for (const v of versions ?? []) byId.set(v.designId, [...byId.get(v.designId) ?? [], v]);
1214
1309
  return [...byId.values()];
1215
1310
  }, [versions]);
1216
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "flex h-dvh flex-col bg-background text-foreground", children: [
1311
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(DesignContext.Provider, { value: design2, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "flex h-dvh flex-col bg-background text-foreground", children: [
1217
1312
  /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("header", { className: "flex flex-wrap items-center gap-y-1 border-b border-border px-3 py-2", children: [
1218
1313
  /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_controls.ControlRow, { className: "min-w-0 flex-1", children: [
1219
1314
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_controls.Field, { "aria-label": "Design name", width: "lg", value: design2.name, onChange: (e) => set((d) => ({ ...d, name: e.target.value })) }),
@@ -1336,7 +1431,7 @@ function MenuBuilder({ store, storeLabel, initialDesign, accountSlot }) {
1336
1431
  children: dnd.ghost.label
1337
1432
  }
1338
1433
  )
1339
- ] });
1434
+ ] }) });
1340
1435
  }
1341
1436
  function Panel({ title, children, right }) {
1342
1437
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("section", { className: "border-b border-border p-3", children: [
@@ -1355,7 +1450,9 @@ function Palette({ onAdd, startDrag, click }) {
1355
1450
  const n = nodeFromPayload(p);
1356
1451
  if (n) onAdd(n);
1357
1452
  };
1358
- const Option = ({ p, icon, label, end }) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1453
+ const design2 = (0, import_react2.useContext)(DesignContext);
1454
+ const uses = (0, import_react2.useMemo)(() => catalogUseCounts(design2?.nodes ?? []), [design2?.nodes]);
1455
+ const Option = ({ p, icon, label, end, dimmed }) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1359
1456
  import_controls.Tile,
1360
1457
  {
1361
1458
  layout: "row",
@@ -1363,6 +1460,7 @@ function Palette({ onAdd, startDrag, click }) {
1363
1460
  icon: renderLucide(icon),
1364
1461
  title: label,
1365
1462
  end,
1463
+ dimmed: dimmed === true,
1366
1464
  onPointerDown: (e) => startDrag(e, p, label),
1367
1465
  onClick: click(() => add(p))
1368
1466
  }
@@ -1375,14 +1473,27 @@ function Palette({ onAdd, startDrag, click }) {
1375
1473
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Option, { p: { type: "new", kind: "section" }, icon: "Rows3", label: "Section" }),
1376
1474
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Option, { p: { type: "new", kind: "submenu" }, icon: "ChevronsRight", label: "Submenu" }),
1377
1475
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Option, { p: { type: "new", kind: "divider" }, icon: "Minus", label: "Divider" }),
1378
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Option, { p: { type: "new", kind: "item" }, icon: "Plus", label: "Custom option" })
1476
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Option, { p: { type: "new", kind: "item" }, icon: "Plus", label: "Custom option" }),
1477
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Option, { p: { type: "new", kind: "slot-items" }, icon: "ListPlus", label: "Placeholder \u2014 items" }),
1478
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Option, { p: { type: "new", kind: "slot-menu" }, icon: "FolderPlus", label: "Placeholder \u2014 submenu" })
1379
1479
  ] }),
1380
1480
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_controls.RowGroup, { title: "Ready-made", children: TEMPLATES.map((t) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Option, { p: { type: "template", id: t.id }, icon: t.icon, label: t.label }, t.id)) })
1381
1481
  ] }),
1382
1482
  CATALOG_GROUPS.map((g) => {
1383
- const items = CATALOG.filter((c) => c.group === g && shown(c));
1483
+ const matching = CATALOG.filter((c) => c.group === g && shown(c));
1484
+ const items = [...matching.filter((c) => !uses.has(c.id)), ...matching.filter((c) => uses.has(c.id))];
1384
1485
  if (items.length === 0) return null;
1385
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_controls.RowGroup, { title: g, children: items.map((c) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Option, { p: { type: "catalog", id: c.id }, icon: c.icon, label: c.label, ...c.status === "proposed" ? { end: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_controls.Badge, { children: "new" }) } : {} }, c.id)) }, g);
1486
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_controls.RowGroup, { title: g, children: items.map((c) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1487
+ Option,
1488
+ {
1489
+ p: { type: "catalog", id: c.id },
1490
+ icon: c.icon,
1491
+ label: c.label,
1492
+ dimmed: uses.has(c.id),
1493
+ ...uses.has(c.id) ? { end: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_controls.Badge, { children: (uses.get(c.id) ?? 0) > 1 ? `used \xD7${uses.get(c.id)}` : "used" }) } : c.status === "proposed" ? { end: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_controls.Badge, { children: "new" }) } : {}
1494
+ },
1495
+ c.id
1496
+ )) }, g);
1386
1497
  })
1387
1498
  ] })
1388
1499
  ] });
@@ -1414,7 +1525,7 @@ function Rows(props) {
1414
1525
  const container = isContainer(node);
1415
1526
  const isOver = over?.id === node.id;
1416
1527
  const label = node.kind === "divider" ? "\u2500\u2500\u2500\u2500 divider \u2500\u2500\u2500\u2500" : node.label || (node.kind === "section" ? "(unlabelled section)" : "(no label)");
1417
- const icon = node.kind === "item" || node.kind === "submenu" ? node.icon : node.kind === "section" ? "Rows3" : "Minus";
1528
+ 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";
1418
1529
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { children: [
1419
1530
  /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
1420
1531
  "div",
@@ -1440,6 +1551,7 @@ function Rows(props) {
1440
1551
  icon && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "text-muted-foreground", children: renderLucide(icon, "size-4") }),
1441
1552
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: `min-w-0 flex-1 truncate ${node.kind === "divider" ? "text-xs text-muted-foreground" : ""}`, children: label }),
1442
1553
  node.kind === "submenu" && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "text-[10px] text-muted-foreground", children: "submenu" }),
1554
+ node.kind === "slot" && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_controls.Badge, { children: node.shape === "submenu" ? "placeholder submenu" : "placeholder" }),
1443
1555
  node.kind === "section" && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "text-[10px] text-muted-foreground", children: node.layout && node.layout !== "list" ? `${node.layout} section` : "section" }),
1444
1556
  node.kind === "item" && node.shortcut && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("kbd", { className: "text-[10px] text-muted-foreground", children: node.shortcut }),
1445
1557
  /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { className: "hidden items-center gap-0.5 group-hover:flex", children: [
@@ -1542,6 +1654,7 @@ function Stage({ design: design2, selected, onSelect }) {
1542
1654
  onClick: (e) => {
1543
1655
  if (e.target === e.currentTarget) setAt(null);
1544
1656
  },
1657
+ "data-menu-bounds": "",
1545
1658
  className: "relative min-h-[300px] overflow-visible rounded-lg bg-muted/60 p-4",
1546
1659
  children: [
1547
1660
  at ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
@@ -1592,9 +1705,10 @@ var FLAGS = [
1592
1705
  ];
1593
1706
  function Inspector({ node, onChange }) {
1594
1707
  const catalog = node.catalogId ? catalogOption(node.catalogId) : void 0;
1595
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(Panel, { title: `Selected ${node.kind}`, right: catalog ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_controls.Badge, { children: catalog.id }) : void 0, children: [
1708
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(Panel, { title: `Selected ${node.kind === "slot" ? "placeholder" : node.kind}`, right: catalog ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_controls.Badge, { children: catalog.id }) : void 0, children: [
1596
1709
  node.kind === "divider" ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "type-secondary", children: "A divider has no settings." }) : /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
1597
1710
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Labelled, { label: "Label", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_controls.Field, { "aria-label": "Label", value: node.label, onChange: (e) => onChange({ label: e.target.value }) }) }),
1711
+ node.kind === "slot" && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(SlotSettings, { node, onChange }),
1598
1712
  (node.kind === "item" || node.kind === "submenu") && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
1599
1713
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Labelled, { label: "Icon", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(IconPicker, { value: node.icon ?? "", onChange: (icon) => onChange({ icon }) }) }),
1600
1714
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Labelled, { label: "Hint", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_controls.Field, { "aria-label": "Hint", value: node.hint ?? "", onChange: (e) => onChange({ hint: e.target.value }) }) })
@@ -1611,6 +1725,37 @@ function Inspector({ node, onChange }) {
1611
1725
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Labelled, { label: "Note for the build", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_controls.Textarea, { "aria-label": "Note for the build", rows: 2, autoGrow: true, value: node.note ?? "", onChange: (e) => onChange({ note: e.target.value }) }) })
1612
1726
  ] });
1613
1727
  }
1728
+ var SLOT_SHAPES = [
1729
+ { value: "items", label: "Rows here" },
1730
+ { value: "submenu", label: "One submenu" }
1731
+ ];
1732
+ var SLOT_OPTIONS = SLOT_SOURCES.map((src) => ({ value: src.id, label: src.label }));
1733
+ function SlotSettings({ node, onChange }) {
1734
+ const source = slotSource(node.fills);
1735
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
1736
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Labelled, { label: "Holds", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1737
+ import_controls.Select,
1738
+ {
1739
+ "aria-label": "What this placeholder holds",
1740
+ value: node.fills,
1741
+ options: SLOT_OPTIONS,
1742
+ onValueChange: (fills) => {
1743
+ const relabel = !node.label || node.label === source?.phrase || node.label === "Everything else" || node.label === "More";
1744
+ onChange({ fills, ...relabel ? { label: slotSource(fills)?.phrase ?? node.label } : {} });
1745
+ }
1746
+ }
1747
+ ) }),
1748
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_controls.SettingRow, { label: "Shows as", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_controls.SegmentedControl, { "aria-label": "Shows as", value: node.shape, data: SLOT_SHAPES, onValueChange: (shape) => onChange({ shape }) }) }),
1749
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(SlotContents, { fills: node.fills })
1750
+ ] });
1751
+ }
1752
+ function SlotContents({ fills }) {
1753
+ const source = slotSource(fills);
1754
+ const design2 = (0, import_react2.useContext)(DesignContext);
1755
+ const left = (0, import_react2.useMemo)(() => design2 ? slotOptions(fills, placedCatalogIds(design2.nodes)) : [], [design2, fills]);
1756
+ if (source?.runtime) return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "type-secondary mt-2", children: "Filled by the live menu at runtime." });
1757
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("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" : ""}` });
1758
+ }
1614
1759
  function Labelled({ label, children }) {
1615
1760
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "mt-2 grid gap-1", children: [
1616
1761
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "type-meta", children: label }),