@ai-matrx/context-menu 0.1.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 +7 -0
- package/dist/builder.cjs +158 -13
- package/dist/builder.cjs.map +1 -1
- package/dist/builder.d.cts +1 -1
- package/dist/builder.d.ts +1 -1
- package/dist/builder.js +159 -14
- package/dist/builder.js.map +1 -1
- package/dist/design-store.cjs.map +1 -1
- package/dist/design-store.d.cts +1 -1
- package/dist/design-store.d.ts +1 -1
- package/dist/design-store.js.map +1 -1
- package/dist/index.cjs +102 -5
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +17 -3
- package/dist/index.d.ts +17 -3
- package/dist/index.js +102 -5
- package/dist/index.js.map +1 -1
- package/dist/{model-BdF0TqZ3.d.cts → model-CkLCR6WY.d.cts} +19 -3
- package/dist/{model-BdF0TqZ3.d.ts → model-CkLCR6WY.d.ts} +19 -3
- package/package.json +6 -6
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/design/store.ts","../src/design/model.ts"],"sourcesContent":["// src/design/store.ts — @ai-matrx/context-menu/design-store: WHERE MENU DESIGNS ARE SAVED.\n//\n// Every Save writes a NEW VERSION of a design; nothing is overwritten, so any earlier version can be\n// opened again. Two stores answer the same interface:\n// - recordsDesignStore: a typed table in the record store (`custom.*`), so a saved design is real\n// shared data the builder's author, Claude, and the app can all read — no hand-made schema;\n// - localDesignStore: this browser's storage, for a playground with no sign-in; it says so.\n\nimport { listAppRows, removeAppRow, upsertAppRow, defineTypedTable, f } from \"@ai-matrx/records/typed-table\";\nimport type { RecordsClient } from \"@ai-matrx/records/core\";\nimport { parseDesign, serializeDesign, type MenuDesign } from \"./model\";\n\nexport const menuDesignTable = defineTypedTable({\n name: \"Menu designs\",\n slug: \"menu_designs\",\n kept_for: \"context_menu\",\n scope: \"organization\",\n owner: \"lane:CONTEXT-MENU\",\n key: \"version_key\",\n declared_in: \"aidream:apps/shared/context-menu/src/design/store.ts\",\n fields: {\n version_key: f.text({ label: \"Version key\", unique: true, required: true }),\n design_id: f.text({ label: \"Design id\", required: true }),\n name: f.text({ label: \"Name\", required: true }),\n version: f.integer({ label: \"Version\", required: true }),\n doc: f.longText({ label: \"Menu\", required: true }),\n note: f.text({ label: \"Note\" }),\n saved_by: f.text({ label: \"Saved by\" }),\n saved_at: f.datetime({ label: \"Saved\" }),\n },\n});\n\nexport interface SavedDesignVersion {\n /** The stored row (record id, or a local key). */\n rowId: string;\n designId: string;\n name: string;\n version: number;\n note: string;\n savedAt: string;\n savedBy: string;\n design: MenuDesign;\n}\n\nexport interface DesignStore {\n /** \"live\" saves to shared data; \"local\" saves to this browser only. */\n readonly kind: \"live\" | \"local\";\n /** Every saved version of every design, newest first. */\n list(): Promise<SavedDesignVersion[]>;\n /** Save `design` as its next version. Answers the saved version. */\n save(design: MenuDesign, note?: string): Promise<SavedDesignVersion>;\n /** Archive one version (restorable from the record store's archive). */\n remove(rowId: string): Promise<void>;\n}\n\nconst byNewest = (a: SavedDesignVersion, b: SavedDesignVersion) =>\n b.savedAt.localeCompare(a.savedAt) || b.version - a.version;\n\nfunction nextVersion(all: SavedDesignVersion[], designId: string): number {\n return all.filter((v) => v.designId === designId).reduce((m, v) => Math.max(m, v.version), 0) + 1;\n}\n\nexport function recordsDesignStore(client: RecordsClient, options: { organizationId: string; savedBy?: string }): DesignStore {\n const list = async (): Promise<SavedDesignVersion[]> => {\n const read = await listAppRows(client, menuDesignTable);\n if (!read.ok) throw new Error(read.error.message);\n return read.data.rows\n .map((r) => ({\n rowId: String(r._id),\n designId: String(r.design_id),\n name: String(r.name),\n version: Number(r.version),\n note: String(r.note ?? \"\"),\n savedAt: String(r.saved_at ?? \"\"),\n savedBy: String(r.saved_by ?? \"\"),\n design: parseDesign(r.doc),\n }))\n .sort(byNewest);\n };\n return {\n kind: \"live\",\n list,\n async save(design, note = \"\") {\n const all = await list();\n const version = nextVersion(all, design.designId);\n const savedAt = new Date().toISOString();\n const savedBy = options.savedBy ?? \"\";\n const wrote = await upsertAppRow(\n client,\n menuDesignTable,\n {\n version_key: `${design.designId}@${version}`,\n design_id: design.designId,\n name: design.name,\n version,\n doc: serializeDesign(design),\n note,\n saved_by: savedBy,\n saved_at: savedAt,\n },\n { organizationId: options.organizationId },\n );\n if (!wrote.ok) throw new Error(wrote.error.message);\n return { rowId: wrote.data, designId: design.designId, name: design.name, version, note, savedAt, savedBy, design };\n },\n async remove(rowId) {\n const gone = await removeAppRow(client, menuDesignTable, rowId, { organizationId: options.organizationId });\n if (!gone.ok) throw new Error(gone.error.message);\n },\n };\n}\n\nconst LOCAL_KEY = \"matrx.context-menu.designs.v1\";\n\nexport function localDesignStore(): DesignStore {\n const read = (): SavedDesignVersion[] => {\n try {\n const raw = globalThis.localStorage?.getItem(LOCAL_KEY);\n const rows = raw ? (JSON.parse(raw) as SavedDesignVersion[]) : [];\n return rows.map((r) => ({ ...r, design: parseDesign(r.design) }));\n } catch {\n return [];\n }\n };\n const write = (rows: SavedDesignVersion[]) => {\n try {\n globalThis.localStorage?.setItem(LOCAL_KEY, JSON.stringify(rows));\n } catch {\n /* storage blocked: the save still answers; the next reload will not have it */\n }\n };\n return {\n kind: \"local\",\n async list() {\n return read().sort(byNewest);\n },\n async save(design, note = \"\") {\n const all = read();\n const version = nextVersion(all, design.designId);\n const saved: SavedDesignVersion = {\n rowId: `${design.designId}@${version}`,\n designId: design.designId,\n name: design.name,\n version,\n note,\n savedAt: new Date().toISOString(),\n savedBy: \"this browser\",\n design,\n };\n write([saved, ...all]);\n return saved;\n },\n async remove(rowId) {\n write(read().filter((r) => r.rowId !== rowId));\n },\n };\n}\n","// src/design/model.ts — @ai-matrx/context-menu: THE MENU DESIGN.\n//\n// A design is the whole menu a person drew in the builder: an ordered tree of items, sections,\n// submenus and dividers, plus the few presentation choices that change how it reads. It is plain\n// JSON (no functions, no React) so it saves as one record, travels in a URL or a chat, and is the\n// exact thing the real menu is later built from. Every edit is a pure function returning a new\n// design; nothing here mutates.\n\nexport type MenuNodeKind = \"item\" | \"section\" | \"submenu\" | \"divider\";\n\ninterface NodeBase {\n /** Stable within the design. */\n id: string;\n /** The catalog option this was dragged from, when it came from one. */\n catalogId?: string;\n /** A designer's note on this one entry (\"only on a table row\", \"merge with Copy\"). */\n note?: string;\n}\n\nexport interface MenuItemNode extends NodeBase {\n kind: \"item\";\n label: string;\n /** A lucide icon name (`Sparkles`, `Copy`). */\n icon?: string;\n /** Shown right-aligned (`⌘C`). */\n shortcut?: string;\n /** One short line under the label. */\n hint?: string;\n /** Shown greyed out — the design says \"this exists here but is unavailable now\". */\n disabled?: boolean;\n /** Red, for actions that remove or discard. */\n danger?: boolean;\n /** A checkbox-style toggle item. */\n toggle?: boolean;\n /** Drawn as a highlighted, AI-flavoured item. */\n ai?: boolean;\n}\n\nexport interface MenuSectionNode extends NodeBase {\n kind: \"section\";\n /** The small heading. Empty = an unlabelled group. */\n label: string;\n children: MenuNode[];\n /** Starts folded; a click on the heading opens it. */\n collapsible?: boolean;\n /** How the section's items are laid out. */\n layout?: \"list\" | \"grid\" | \"row\";\n}\n\nexport interface MenuSubmenuNode extends NodeBase {\n kind: \"submenu\";\n label: string;\n icon?: string;\n hint?: string;\n children: MenuNode[];\n}\n\nexport interface MenuDividerNode extends NodeBase {\n kind: \"divider\";\n}\n\nexport type MenuNode = MenuItemNode | MenuSectionNode | MenuSubmenuNode | MenuDividerNode;\nexport type MenuContainerNode = MenuSectionNode | MenuSubmenuNode;\n\n/** Where the menu opens; the builder previews each the way it appears there. */\nexport const MENU_SURFACES = [\n \"text-selection\",\n \"editable-field\",\n \"table-row\",\n \"page\",\n \"chat-message\",\n \"file\",\n \"anywhere\",\n] as const;\nexport type MenuSurface = (typeof MENU_SURFACES)[number];\n\nexport interface MenuLook {\n density: \"compact\" | \"comfortable\";\n width: number;\n showIcons: boolean;\n showShortcuts: boolean;\n showHints: boolean;\n /** A search box at the top of the menu. */\n search: boolean;\n}\n\nexport const DEFAULT_LOOK: MenuLook = {\n density: \"comfortable\",\n width: 280,\n showIcons: true,\n showShortcuts: true,\n showHints: false,\n search: false,\n};\n\nexport interface MenuDesign {\n /** The design's identity; every version shares it. */\n designId: string;\n name: string;\n surface: MenuSurface;\n look: MenuLook;\n nodes: MenuNode[];\n /** Free notes for whoever builds it. */\n notes: string;\n}\n\nexport const DESIGN_FORMAT = 1;\n\n// ── ids ──────────────────────────────────────────────────────────────────────\n\nexport function newId(prefix = \"n\"): string {\n const rnd =\n typeof crypto !== \"undefined\" && \"randomUUID\" in crypto\n ? crypto.randomUUID().slice(0, 8)\n : Math.random().toString(36).slice(2, 10);\n return `${prefix}_${rnd}`;\n}\n\nexport function emptyDesign(name = \"Untitled menu\"): MenuDesign {\n return { designId: newId(\"d\"), name, surface: \"anywhere\", look: { ...DEFAULT_LOOK }, nodes: [], notes: \"\" };\n}\n\nexport function isContainer(node: MenuNode): node is MenuContainerNode {\n return node.kind === \"section\" || node.kind === \"submenu\";\n}\n\n/** A deep copy with every id renewed — what a duplicate or a second drop of a template gets. */\nexport function cloneNode<N extends MenuNode>(node: N): N {\n const copy = { ...node, id: newId(node.kind[0]) } as N;\n if (isContainer(copy)) (copy as MenuContainerNode).children = (copy as MenuContainerNode).children.map(cloneNode);\n return copy;\n}\n\n// ── reading the tree ─────────────────────────────────────────────────────────\n\nexport interface Located {\n node: MenuNode;\n /** The container's id, or null at the top. */\n parentId: string | null;\n index: number;\n depth: number;\n}\n\nexport function locate(nodes: readonly MenuNode[], id: string, parentId: string | null = null, depth = 0): Located | null {\n for (let i = 0; i < nodes.length; i++) {\n const node = nodes[i] as MenuNode;\n if (node.id === id) return { node, parentId, index: i, depth };\n if (isContainer(node)) {\n const inner = locate(node.children, id, node.id, depth + 1);\n if (inner) return inner;\n }\n }\n return null;\n}\n\nexport function contains(node: MenuNode, id: string): boolean {\n if (node.id === id) return true;\n return isContainer(node) && node.children.some((c) => contains(c, id));\n}\n\nexport function countItems(nodes: readonly MenuNode[]): number {\n let n = 0;\n for (const node of nodes) {\n if (node.kind === \"item\") n += 1;\n else if (isContainer(node)) n += countItems(node.children) + (node.kind === \"submenu\" ? 1 : 0);\n }\n return n;\n}\n\n// ── editing the tree (pure) ──────────────────────────────────────────────────\n\nfunction mapChildren(nodes: readonly MenuNode[], parentId: string | null, fn: (list: MenuNode[]) => MenuNode[]): MenuNode[] {\n if (parentId === null) return fn([...nodes]);\n return nodes.map((node) => {\n if (!isContainer(node)) return node;\n if (node.id === parentId) return { ...node, children: fn([...node.children]) };\n return { ...node, children: mapChildren(node.children, parentId, fn) };\n });\n}\n\nexport function insertNode(nodes: readonly MenuNode[], parentId: string | null, index: number, node: MenuNode): MenuNode[] {\n return mapChildren(nodes, parentId, (list) => {\n list.splice(Math.max(0, Math.min(index, list.length)), 0, node);\n return list;\n });\n}\n\nexport function removeNode(nodes: readonly MenuNode[], id: string): MenuNode[] {\n return nodes\n .filter((n) => n.id !== id)\n .map((n) => (isContainer(n) ? { ...n, children: removeNode(n.children, id) } : n));\n}\n\nexport function updateNode(nodes: readonly MenuNode[], id: string, patch: Partial<MenuNode>): MenuNode[] {\n return nodes.map((n) => {\n if (n.id === id) return { ...n, ...patch, id: n.id, kind: n.kind } as MenuNode;\n if (isContainer(n)) return { ...n, children: updateNode(n.children, id, patch) };\n return n;\n });\n}\n\n/**\n * Move `id` to `index` inside `parentId`. Refuses (returns the tree unchanged) a move into itself\n * or its own descendant. `index` is in the destination list as it reads BEFORE the move.\n */\nexport function canMove(nodes: readonly MenuNode[], id: string, parentId: string | null): boolean {\n const from = locate(nodes, id);\n return Boolean(from) && (parentId === null || !contains((from as Located).node, parentId));\n}\n\nexport function moveNode(nodes: readonly MenuNode[], id: string, parentId: string | null, index: number): MenuNode[] {\n const from = locate(nodes, id);\n // A refused move answers the SAME array, so a caller can tell \"nothing happened\" by identity.\n if (!from) return nodes as MenuNode[];\n if (parentId !== null && contains(from.node, parentId)) return nodes as MenuNode[];\n let target = index;\n if (from.parentId === parentId && from.index < index) target -= 1;\n return insertNode(removeNode(nodes, id), parentId, target, from.node);\n}\n\nexport function duplicateNode(nodes: readonly MenuNode[], id: string): MenuNode[] {\n const at = locate(nodes, id);\n if (!at) return [...nodes];\n return insertNode(nodes, at.parentId, at.index + 1, cloneNode(at.node));\n}\n\n/** Wrap a node into a new section (or submenu) in its place. */\nexport function wrapNode(nodes: readonly MenuNode[], id: string, kind: \"section\" | \"submenu\"): MenuNode[] {\n const at = locate(nodes, id);\n if (!at) return [...nodes];\n const wrapper: MenuContainerNode =\n kind === \"section\"\n ? { id: newId(\"s\"), kind: \"section\", label: \"New section\", children: [at.node] }\n : { id: newId(\"m\"), kind: \"submenu\", label: \"More\", icon: \"ChevronRight\", children: [at.node] };\n return insertNode(removeNode(nodes, id), at.parentId, at.index, wrapper);\n}\n\n/** Replace a container by its children. */\nexport function unwrapNode(nodes: readonly MenuNode[], id: string): MenuNode[] {\n const at = locate(nodes, id);\n if (!at || !isContainer(at.node)) return [...nodes];\n let out = removeNode(nodes, id);\n at.node.children.forEach((child, i) => {\n out = insertNode(out, at.parentId, at.index + i, child);\n });\n return out;\n}\n\n// ── reading a saved design defensively ──────────────────────────────────────\n\n/** A saved document back into a design; anything unreadable becomes an empty design, never a throw. */\nexport function parseDesign(raw: unknown): MenuDesign {\n let value: unknown = raw;\n if (typeof raw === \"string\") {\n try {\n value = JSON.parse(raw);\n } catch {\n return emptyDesign(\"Unreadable design\");\n }\n }\n const v = (value ?? {}) as Partial<MenuDesign> & { design?: MenuDesign };\n const d = v.design ?? v;\n return {\n designId: typeof d.designId === \"string\" ? d.designId : newId(\"d\"),\n name: typeof d.name === \"string\" ? d.name : \"Untitled menu\",\n surface: MENU_SURFACES.includes(d.surface as MenuSurface) ? (d.surface as MenuSurface) : \"anywhere\",\n look: { ...DEFAULT_LOOK, ...(d.look ?? {}) },\n nodes: Array.isArray(d.nodes) ? (d.nodes as MenuNode[]) : [],\n notes: typeof d.notes === \"string\" ? d.notes : \"\",\n };\n}\n\nexport function serializeDesign(design: MenuDesign): string {\n return JSON.stringify({ format: DESIGN_FORMAT, design });\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/design/store.ts","../src/design/model.ts"],"sourcesContent":["// src/design/store.ts — @ai-matrx/context-menu/design-store: WHERE MENU DESIGNS ARE SAVED.\n//\n// Every Save writes a NEW VERSION of a design; nothing is overwritten, so any earlier version can be\n// opened again. Two stores answer the same interface:\n// - recordsDesignStore: a typed table in the record store (`custom.*`), so a saved design is real\n// shared data the builder's author, Claude, and the app can all read — no hand-made schema;\n// - localDesignStore: this browser's storage, for a playground with no sign-in; it says so.\n\nimport { listAppRows, removeAppRow, upsertAppRow, defineTypedTable, f } from \"@ai-matrx/records/typed-table\";\nimport type { RecordsClient } from \"@ai-matrx/records/core\";\nimport { parseDesign, serializeDesign, type MenuDesign } from \"./model\";\n\nexport const menuDesignTable = defineTypedTable({\n name: \"Menu designs\",\n slug: \"menu_designs\",\n kept_for: \"context_menu\",\n scope: \"organization\",\n owner: \"lane:CONTEXT-MENU\",\n key: \"version_key\",\n declared_in: \"aidream:apps/shared/context-menu/src/design/store.ts\",\n fields: {\n version_key: f.text({ label: \"Version key\", unique: true, required: true }),\n design_id: f.text({ label: \"Design id\", required: true }),\n name: f.text({ label: \"Name\", required: true }),\n version: f.integer({ label: \"Version\", required: true }),\n doc: f.longText({ label: \"Menu\", required: true }),\n note: f.text({ label: \"Note\" }),\n saved_by: f.text({ label: \"Saved by\" }),\n saved_at: f.datetime({ label: \"Saved\" }),\n },\n});\n\nexport interface SavedDesignVersion {\n /** The stored row (record id, or a local key). */\n rowId: string;\n designId: string;\n name: string;\n version: number;\n note: string;\n savedAt: string;\n savedBy: string;\n design: MenuDesign;\n}\n\nexport interface DesignStore {\n /** \"live\" saves to shared data; \"local\" saves to this browser only. */\n readonly kind: \"live\" | \"local\";\n /** Every saved version of every design, newest first. */\n list(): Promise<SavedDesignVersion[]>;\n /** Save `design` as its next version. Answers the saved version. */\n save(design: MenuDesign, note?: string): Promise<SavedDesignVersion>;\n /** Archive one version (restorable from the record store's archive). */\n remove(rowId: string): Promise<void>;\n}\n\nconst byNewest = (a: SavedDesignVersion, b: SavedDesignVersion) =>\n b.savedAt.localeCompare(a.savedAt) || b.version - a.version;\n\nfunction nextVersion(all: SavedDesignVersion[], designId: string): number {\n return all.filter((v) => v.designId === designId).reduce((m, v) => Math.max(m, v.version), 0) + 1;\n}\n\nexport function recordsDesignStore(client: RecordsClient, options: { organizationId: string; savedBy?: string }): DesignStore {\n const list = async (): Promise<SavedDesignVersion[]> => {\n const read = await listAppRows(client, menuDesignTable);\n if (!read.ok) throw new Error(read.error.message);\n return read.data.rows\n .map((r) => ({\n rowId: String(r._id),\n designId: String(r.design_id),\n name: String(r.name),\n version: Number(r.version),\n note: String(r.note ?? \"\"),\n savedAt: String(r.saved_at ?? \"\"),\n savedBy: String(r.saved_by ?? \"\"),\n design: parseDesign(r.doc),\n }))\n .sort(byNewest);\n };\n return {\n kind: \"live\",\n list,\n async save(design, note = \"\") {\n const all = await list();\n const version = nextVersion(all, design.designId);\n const savedAt = new Date().toISOString();\n const savedBy = options.savedBy ?? \"\";\n const wrote = await upsertAppRow(\n client,\n menuDesignTable,\n {\n version_key: `${design.designId}@${version}`,\n design_id: design.designId,\n name: design.name,\n version,\n doc: serializeDesign(design),\n note,\n saved_by: savedBy,\n saved_at: savedAt,\n },\n { organizationId: options.organizationId },\n );\n if (!wrote.ok) throw new Error(wrote.error.message);\n return { rowId: wrote.data, designId: design.designId, name: design.name, version, note, savedAt, savedBy, design };\n },\n async remove(rowId) {\n const gone = await removeAppRow(client, menuDesignTable, rowId, { organizationId: options.organizationId });\n if (!gone.ok) throw new Error(gone.error.message);\n },\n };\n}\n\nconst LOCAL_KEY = \"matrx.context-menu.designs.v1\";\n\nexport function localDesignStore(): DesignStore {\n const read = (): SavedDesignVersion[] => {\n try {\n const raw = globalThis.localStorage?.getItem(LOCAL_KEY);\n const rows = raw ? (JSON.parse(raw) as SavedDesignVersion[]) : [];\n return rows.map((r) => ({ ...r, design: parseDesign(r.design) }));\n } catch {\n return [];\n }\n };\n const write = (rows: SavedDesignVersion[]) => {\n try {\n globalThis.localStorage?.setItem(LOCAL_KEY, JSON.stringify(rows));\n } catch {\n /* storage blocked: the save still answers; the next reload will not have it */\n }\n };\n return {\n kind: \"local\",\n async list() {\n return read().sort(byNewest);\n },\n async save(design, note = \"\") {\n const all = read();\n const version = nextVersion(all, design.designId);\n const saved: SavedDesignVersion = {\n rowId: `${design.designId}@${version}`,\n designId: design.designId,\n name: design.name,\n version,\n note,\n savedAt: new Date().toISOString(),\n savedBy: \"this browser\",\n design,\n };\n write([saved, ...all]);\n return saved;\n },\n async remove(rowId) {\n write(read().filter((r) => r.rowId !== rowId));\n },\n };\n}\n","// src/design/model.ts — @ai-matrx/context-menu: THE MENU DESIGN.\n//\n// A design is the whole menu a person drew in the builder: an ordered tree of items, sections,\n// submenus and dividers, plus the few presentation choices that change how it reads. It is plain\n// JSON (no functions, no React) so it saves as one record, travels in a URL or a chat, and is the\n// exact thing the real menu is later built from. Every edit is a pure function returning a new\n// design; nothing here mutates.\n\nexport type MenuNodeKind = \"item\" | \"section\" | \"submenu\" | \"divider\" | \"slot\";\n\ninterface NodeBase {\n /** Stable within the design. */\n id: string;\n /** The catalog option this was dragged from, when it came from one. */\n catalogId?: string;\n /** A designer's note on this one entry (\"only on a table row\", \"merge with Copy\"). */\n note?: string;\n}\n\nexport interface MenuItemNode extends NodeBase {\n kind: \"item\";\n label: string;\n /** A lucide icon name (`Sparkles`, `Copy`). */\n icon?: string;\n /** Shown right-aligned (`⌘C`). */\n shortcut?: string;\n /** One short line under the label. */\n hint?: string;\n /** Shown greyed out — the design says \"this exists here but is unavailable now\". */\n disabled?: boolean;\n /** Red, for actions that remove or discard. */\n danger?: boolean;\n /** A checkbox-style toggle item. */\n toggle?: boolean;\n /** Drawn as a highlighted, AI-flavoured item. */\n ai?: boolean;\n}\n\nexport interface MenuSectionNode extends NodeBase {\n kind: \"section\";\n /** The small heading. Empty = an unlabelled group. */\n label: string;\n children: MenuNode[];\n /** Starts folded; a click on the heading opens it. */\n collapsible?: boolean;\n /** How the section's items are laid out. */\n layout?: \"list\" | \"grid\" | \"row\";\n}\n\nexport interface MenuSubmenuNode extends NodeBase {\n kind: \"submenu\";\n label: string;\n icon?: string;\n hint?: string;\n children: MenuNode[];\n}\n\nexport interface MenuDividerNode extends NodeBase {\n kind: \"divider\";\n}\n\n/**\n * A PLACEHOLDER: \"every other option of this kind goes here\". `shape: \"items\"` lays them out as rows\n * where the placeholder sits; `shape: \"submenu\"` gathers them into one submenu. `fills` names the\n * kind (a catalog group, the agent library, the page's own rows, or everything not placed).\n */\nexport interface MenuSlotNode extends NodeBase {\n kind: \"slot\";\n label: string;\n fills: string;\n shape: \"items\" | \"submenu\";\n icon?: string;\n}\n\nexport type MenuNode = MenuItemNode | MenuSectionNode | MenuSubmenuNode | MenuDividerNode | MenuSlotNode;\nexport type MenuContainerNode = MenuSectionNode | MenuSubmenuNode;\n\n/** Where the menu opens; the builder previews each the way it appears there. */\nexport const MENU_SURFACES = [\n \"text-selection\",\n \"editable-field\",\n \"table-row\",\n \"page\",\n \"chat-message\",\n \"file\",\n \"anywhere\",\n] as const;\nexport type MenuSurface = (typeof MENU_SURFACES)[number];\n\nexport interface MenuLook {\n density: \"compact\" | \"comfortable\";\n width: number;\n showIcons: boolean;\n showShortcuts: boolean;\n showHints: boolean;\n /** A search box at the top of the menu. */\n search: boolean;\n}\n\nexport const DEFAULT_LOOK: MenuLook = {\n density: \"comfortable\",\n width: 280,\n showIcons: true,\n showShortcuts: true,\n showHints: false,\n search: false,\n};\n\nexport interface MenuDesign {\n /** The design's identity; every version shares it. */\n designId: string;\n name: string;\n surface: MenuSurface;\n look: MenuLook;\n nodes: MenuNode[];\n /** Free notes for whoever builds it. */\n notes: string;\n}\n\nexport const DESIGN_FORMAT = 1;\n\n// ── ids ──────────────────────────────────────────────────────────────────────\n\nexport function newId(prefix = \"n\"): string {\n const rnd =\n typeof crypto !== \"undefined\" && \"randomUUID\" in crypto\n ? crypto.randomUUID().slice(0, 8)\n : Math.random().toString(36).slice(2, 10);\n return `${prefix}_${rnd}`;\n}\n\nexport function emptyDesign(name = \"Untitled menu\"): MenuDesign {\n return { designId: newId(\"d\"), name, surface: \"anywhere\", look: { ...DEFAULT_LOOK }, nodes: [], notes: \"\" };\n}\n\nexport function isContainer(node: MenuNode): node is MenuContainerNode {\n return node.kind === \"section\" || node.kind === \"submenu\";\n}\n\n/** Every catalog option the design already places (so a placeholder holds only the rest). */\nexport function placedCatalogIds(nodes: readonly MenuNode[], out = new Set<string>()): Set<string> {\n for (const n of nodes) {\n if (n.catalogId) out.add(n.catalogId);\n if (isContainer(n)) placedCatalogIds(n.children, out);\n }\n return out;\n}\n\n/** How many times each catalog option appears in the design (a duplicate counts twice). */\nexport function catalogUseCounts(nodes: readonly MenuNode[], out = new Map<string, number>()): Map<string, number> {\n for (const n of nodes) {\n if (n.catalogId) out.set(n.catalogId, (out.get(n.catalogId) ?? 0) + 1);\n if (isContainer(n)) catalogUseCounts(n.children, out);\n }\n return out;\n}\n\n/** A deep copy with every id renewed — what a duplicate or a second drop of a template gets. */\nexport function cloneNode<N extends MenuNode>(node: N): N {\n const copy = { ...node, id: newId(node.kind[0]) } as N;\n if (isContainer(copy)) (copy as MenuContainerNode).children = (copy as MenuContainerNode).children.map(cloneNode);\n return copy;\n}\n\n// ── reading the tree ─────────────────────────────────────────────────────────\n\nexport interface Located {\n node: MenuNode;\n /** The container's id, or null at the top. */\n parentId: string | null;\n index: number;\n depth: number;\n}\n\nexport function locate(nodes: readonly MenuNode[], id: string, parentId: string | null = null, depth = 0): Located | null {\n for (let i = 0; i < nodes.length; i++) {\n const node = nodes[i] as MenuNode;\n if (node.id === id) return { node, parentId, index: i, depth };\n if (isContainer(node)) {\n const inner = locate(node.children, id, node.id, depth + 1);\n if (inner) return inner;\n }\n }\n return null;\n}\n\nexport function contains(node: MenuNode, id: string): boolean {\n if (node.id === id) return true;\n return isContainer(node) && node.children.some((c) => contains(c, id));\n}\n\nexport function countItems(nodes: readonly MenuNode[]): number {\n let n = 0;\n for (const node of nodes) {\n if (node.kind === \"item\" || node.kind === \"slot\") n += 1;\n else if (isContainer(node)) n += countItems(node.children) + (node.kind === \"submenu\" ? 1 : 0);\n }\n return n;\n}\n\n// ── editing the tree (pure) ──────────────────────────────────────────────────\n\nfunction mapChildren(nodes: readonly MenuNode[], parentId: string | null, fn: (list: MenuNode[]) => MenuNode[]): MenuNode[] {\n if (parentId === null) return fn([...nodes]);\n return nodes.map((node) => {\n if (!isContainer(node)) return node;\n if (node.id === parentId) return { ...node, children: fn([...node.children]) };\n return { ...node, children: mapChildren(node.children, parentId, fn) };\n });\n}\n\nexport function insertNode(nodes: readonly MenuNode[], parentId: string | null, index: number, node: MenuNode): MenuNode[] {\n return mapChildren(nodes, parentId, (list) => {\n list.splice(Math.max(0, Math.min(index, list.length)), 0, node);\n return list;\n });\n}\n\nexport function removeNode(nodes: readonly MenuNode[], id: string): MenuNode[] {\n return nodes\n .filter((n) => n.id !== id)\n .map((n) => (isContainer(n) ? { ...n, children: removeNode(n.children, id) } : n));\n}\n\nexport function updateNode(nodes: readonly MenuNode[], id: string, patch: Partial<MenuNode>): MenuNode[] {\n return nodes.map((n) => {\n if (n.id === id) return { ...n, ...patch, id: n.id, kind: n.kind } as MenuNode;\n if (isContainer(n)) return { ...n, children: updateNode(n.children, id, patch) };\n return n;\n });\n}\n\n/**\n * Move `id` to `index` inside `parentId`. Refuses (returns the tree unchanged) a move into itself\n * or its own descendant. `index` is in the destination list as it reads BEFORE the move.\n */\nexport function canMove(nodes: readonly MenuNode[], id: string, parentId: string | null): boolean {\n const from = locate(nodes, id);\n return Boolean(from) && (parentId === null || !contains((from as Located).node, parentId));\n}\n\nexport function moveNode(nodes: readonly MenuNode[], id: string, parentId: string | null, index: number): MenuNode[] {\n const from = locate(nodes, id);\n // A refused move answers the SAME array, so a caller can tell \"nothing happened\" by identity.\n if (!from) return nodes as MenuNode[];\n if (parentId !== null && contains(from.node, parentId)) return nodes as MenuNode[];\n let target = index;\n if (from.parentId === parentId && from.index < index) target -= 1;\n return insertNode(removeNode(nodes, id), parentId, target, from.node);\n}\n\nexport function duplicateNode(nodes: readonly MenuNode[], id: string): MenuNode[] {\n const at = locate(nodes, id);\n if (!at) return [...nodes];\n return insertNode(nodes, at.parentId, at.index + 1, cloneNode(at.node));\n}\n\n/** Wrap a node into a new section (or submenu) in its place. */\nexport function wrapNode(nodes: readonly MenuNode[], id: string, kind: \"section\" | \"submenu\"): MenuNode[] {\n const at = locate(nodes, id);\n if (!at) return [...nodes];\n const wrapper: MenuContainerNode =\n kind === \"section\"\n ? { id: newId(\"s\"), kind: \"section\", label: \"New section\", children: [at.node] }\n : { id: newId(\"m\"), kind: \"submenu\", label: \"More\", icon: \"ChevronRight\", children: [at.node] };\n return insertNode(removeNode(nodes, id), at.parentId, at.index, wrapper);\n}\n\n/** Replace a container by its children. */\nexport function unwrapNode(nodes: readonly MenuNode[], id: string): MenuNode[] {\n const at = locate(nodes, id);\n if (!at || !isContainer(at.node)) return [...nodes];\n let out = removeNode(nodes, id);\n at.node.children.forEach((child, i) => {\n out = insertNode(out, at.parentId, at.index + i, child);\n });\n return out;\n}\n\n// ── reading a saved design defensively ──────────────────────────────────────\n\n/** A saved document back into a design; anything unreadable becomes an empty design, never a throw. */\nexport function parseDesign(raw: unknown): MenuDesign {\n let value: unknown = raw;\n if (typeof raw === \"string\") {\n try {\n value = JSON.parse(raw);\n } catch {\n return emptyDesign(\"Unreadable design\");\n }\n }\n const v = (value ?? {}) as Partial<MenuDesign> & { design?: MenuDesign };\n const d = v.design ?? v;\n return {\n designId: typeof d.designId === \"string\" ? d.designId : newId(\"d\"),\n name: typeof d.name === \"string\" ? d.name : \"Untitled menu\",\n surface: MENU_SURFACES.includes(d.surface as MenuSurface) ? (d.surface as MenuSurface) : \"anywhere\",\n look: { ...DEFAULT_LOOK, ...(d.look ?? {}) },\n nodes: Array.isArray(d.nodes) ? (d.nodes as MenuNode[]) : [],\n notes: typeof d.notes === \"string\" ? d.notes : \"\",\n };\n}\n\nexport function serializeDesign(design: MenuDesign): string {\n return JSON.stringify({ format: DESIGN_FORMAT, design });\n}\n"],"mappings":"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","names":[]}
|
package/dist/design-store.d.cts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as _ai_matrx_records_typed_table from '@ai-matrx/records/typed-table';
|
|
2
2
|
import { RecordsClient } from '@ai-matrx/records/core';
|
|
3
|
-
import { M as MenuDesign } from './model-
|
|
3
|
+
import { M as MenuDesign } from './model-CkLCR6WY.cjs';
|
|
4
4
|
|
|
5
5
|
declare const menuDesignTable: _ai_matrx_records_typed_table.TypedTableDef<{
|
|
6
6
|
version_key: _ai_matrx_records_typed_table.AppField<string, true, false, true>;
|
package/dist/design-store.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as _ai_matrx_records_typed_table from '@ai-matrx/records/typed-table';
|
|
2
2
|
import { RecordsClient } from '@ai-matrx/records/core';
|
|
3
|
-
import { M as MenuDesign } from './model-
|
|
3
|
+
import { M as MenuDesign } from './model-CkLCR6WY.js';
|
|
4
4
|
|
|
5
5
|
declare const menuDesignTable: _ai_matrx_records_typed_table.TypedTableDef<{
|
|
6
6
|
version_key: _ai_matrx_records_typed_table.AppField<string, true, false, true>;
|
package/dist/design-store.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/design/store.ts","../src/design/model.ts"],"sourcesContent":["// src/design/store.ts — @ai-matrx/context-menu/design-store: WHERE MENU DESIGNS ARE SAVED.\n//\n// Every Save writes a NEW VERSION of a design; nothing is overwritten, so any earlier version can be\n// opened again. Two stores answer the same interface:\n// - recordsDesignStore: a typed table in the record store (`custom.*`), so a saved design is real\n// shared data the builder's author, Claude, and the app can all read — no hand-made schema;\n// - localDesignStore: this browser's storage, for a playground with no sign-in; it says so.\n\nimport { listAppRows, removeAppRow, upsertAppRow, defineTypedTable, f } from \"@ai-matrx/records/typed-table\";\nimport type { RecordsClient } from \"@ai-matrx/records/core\";\nimport { parseDesign, serializeDesign, type MenuDesign } from \"./model\";\n\nexport const menuDesignTable = defineTypedTable({\n name: \"Menu designs\",\n slug: \"menu_designs\",\n kept_for: \"context_menu\",\n scope: \"organization\",\n owner: \"lane:CONTEXT-MENU\",\n key: \"version_key\",\n declared_in: \"aidream:apps/shared/context-menu/src/design/store.ts\",\n fields: {\n version_key: f.text({ label: \"Version key\", unique: true, required: true }),\n design_id: f.text({ label: \"Design id\", required: true }),\n name: f.text({ label: \"Name\", required: true }),\n version: f.integer({ label: \"Version\", required: true }),\n doc: f.longText({ label: \"Menu\", required: true }),\n note: f.text({ label: \"Note\" }),\n saved_by: f.text({ label: \"Saved by\" }),\n saved_at: f.datetime({ label: \"Saved\" }),\n },\n});\n\nexport interface SavedDesignVersion {\n /** The stored row (record id, or a local key). */\n rowId: string;\n designId: string;\n name: string;\n version: number;\n note: string;\n savedAt: string;\n savedBy: string;\n design: MenuDesign;\n}\n\nexport interface DesignStore {\n /** \"live\" saves to shared data; \"local\" saves to this browser only. */\n readonly kind: \"live\" | \"local\";\n /** Every saved version of every design, newest first. */\n list(): Promise<SavedDesignVersion[]>;\n /** Save `design` as its next version. Answers the saved version. */\n save(design: MenuDesign, note?: string): Promise<SavedDesignVersion>;\n /** Archive one version (restorable from the record store's archive). */\n remove(rowId: string): Promise<void>;\n}\n\nconst byNewest = (a: SavedDesignVersion, b: SavedDesignVersion) =>\n b.savedAt.localeCompare(a.savedAt) || b.version - a.version;\n\nfunction nextVersion(all: SavedDesignVersion[], designId: string): number {\n return all.filter((v) => v.designId === designId).reduce((m, v) => Math.max(m, v.version), 0) + 1;\n}\n\nexport function recordsDesignStore(client: RecordsClient, options: { organizationId: string; savedBy?: string }): DesignStore {\n const list = async (): Promise<SavedDesignVersion[]> => {\n const read = await listAppRows(client, menuDesignTable);\n if (!read.ok) throw new Error(read.error.message);\n return read.data.rows\n .map((r) => ({\n rowId: String(r._id),\n designId: String(r.design_id),\n name: String(r.name),\n version: Number(r.version),\n note: String(r.note ?? \"\"),\n savedAt: String(r.saved_at ?? \"\"),\n savedBy: String(r.saved_by ?? \"\"),\n design: parseDesign(r.doc),\n }))\n .sort(byNewest);\n };\n return {\n kind: \"live\",\n list,\n async save(design, note = \"\") {\n const all = await list();\n const version = nextVersion(all, design.designId);\n const savedAt = new Date().toISOString();\n const savedBy = options.savedBy ?? \"\";\n const wrote = await upsertAppRow(\n client,\n menuDesignTable,\n {\n version_key: `${design.designId}@${version}`,\n design_id: design.designId,\n name: design.name,\n version,\n doc: serializeDesign(design),\n note,\n saved_by: savedBy,\n saved_at: savedAt,\n },\n { organizationId: options.organizationId },\n );\n if (!wrote.ok) throw new Error(wrote.error.message);\n return { rowId: wrote.data, designId: design.designId, name: design.name, version, note, savedAt, savedBy, design };\n },\n async remove(rowId) {\n const gone = await removeAppRow(client, menuDesignTable, rowId, { organizationId: options.organizationId });\n if (!gone.ok) throw new Error(gone.error.message);\n },\n };\n}\n\nconst LOCAL_KEY = \"matrx.context-menu.designs.v1\";\n\nexport function localDesignStore(): DesignStore {\n const read = (): SavedDesignVersion[] => {\n try {\n const raw = globalThis.localStorage?.getItem(LOCAL_KEY);\n const rows = raw ? (JSON.parse(raw) as SavedDesignVersion[]) : [];\n return rows.map((r) => ({ ...r, design: parseDesign(r.design) }));\n } catch {\n return [];\n }\n };\n const write = (rows: SavedDesignVersion[]) => {\n try {\n globalThis.localStorage?.setItem(LOCAL_KEY, JSON.stringify(rows));\n } catch {\n /* storage blocked: the save still answers; the next reload will not have it */\n }\n };\n return {\n kind: \"local\",\n async list() {\n return read().sort(byNewest);\n },\n async save(design, note = \"\") {\n const all = read();\n const version = nextVersion(all, design.designId);\n const saved: SavedDesignVersion = {\n rowId: `${design.designId}@${version}`,\n designId: design.designId,\n name: design.name,\n version,\n note,\n savedAt: new Date().toISOString(),\n savedBy: \"this browser\",\n design,\n };\n write([saved, ...all]);\n return saved;\n },\n async remove(rowId) {\n write(read().filter((r) => r.rowId !== rowId));\n },\n };\n}\n","// src/design/model.ts — @ai-matrx/context-menu: THE MENU DESIGN.\n//\n// A design is the whole menu a person drew in the builder: an ordered tree of items, sections,\n// submenus and dividers, plus the few presentation choices that change how it reads. It is plain\n// JSON (no functions, no React) so it saves as one record, travels in a URL or a chat, and is the\n// exact thing the real menu is later built from. Every edit is a pure function returning a new\n// design; nothing here mutates.\n\nexport type MenuNodeKind = \"item\" | \"section\" | \"submenu\" | \"divider\";\n\ninterface NodeBase {\n /** Stable within the design. */\n id: string;\n /** The catalog option this was dragged from, when it came from one. */\n catalogId?: string;\n /** A designer's note on this one entry (\"only on a table row\", \"merge with Copy\"). */\n note?: string;\n}\n\nexport interface MenuItemNode extends NodeBase {\n kind: \"item\";\n label: string;\n /** A lucide icon name (`Sparkles`, `Copy`). */\n icon?: string;\n /** Shown right-aligned (`⌘C`). */\n shortcut?: string;\n /** One short line under the label. */\n hint?: string;\n /** Shown greyed out — the design says \"this exists here but is unavailable now\". */\n disabled?: boolean;\n /** Red, for actions that remove or discard. */\n danger?: boolean;\n /** A checkbox-style toggle item. */\n toggle?: boolean;\n /** Drawn as a highlighted, AI-flavoured item. */\n ai?: boolean;\n}\n\nexport interface MenuSectionNode extends NodeBase {\n kind: \"section\";\n /** The small heading. Empty = an unlabelled group. */\n label: string;\n children: MenuNode[];\n /** Starts folded; a click on the heading opens it. */\n collapsible?: boolean;\n /** How the section's items are laid out. */\n layout?: \"list\" | \"grid\" | \"row\";\n}\n\nexport interface MenuSubmenuNode extends NodeBase {\n kind: \"submenu\";\n label: string;\n icon?: string;\n hint?: string;\n children: MenuNode[];\n}\n\nexport interface MenuDividerNode extends NodeBase {\n kind: \"divider\";\n}\n\nexport type MenuNode = MenuItemNode | MenuSectionNode | MenuSubmenuNode | MenuDividerNode;\nexport type MenuContainerNode = MenuSectionNode | MenuSubmenuNode;\n\n/** Where the menu opens; the builder previews each the way it appears there. */\nexport const MENU_SURFACES = [\n \"text-selection\",\n \"editable-field\",\n \"table-row\",\n \"page\",\n \"chat-message\",\n \"file\",\n \"anywhere\",\n] as const;\nexport type MenuSurface = (typeof MENU_SURFACES)[number];\n\nexport interface MenuLook {\n density: \"compact\" | \"comfortable\";\n width: number;\n showIcons: boolean;\n showShortcuts: boolean;\n showHints: boolean;\n /** A search box at the top of the menu. */\n search: boolean;\n}\n\nexport const DEFAULT_LOOK: MenuLook = {\n density: \"comfortable\",\n width: 280,\n showIcons: true,\n showShortcuts: true,\n showHints: false,\n search: false,\n};\n\nexport interface MenuDesign {\n /** The design's identity; every version shares it. */\n designId: string;\n name: string;\n surface: MenuSurface;\n look: MenuLook;\n nodes: MenuNode[];\n /** Free notes for whoever builds it. */\n notes: string;\n}\n\nexport const DESIGN_FORMAT = 1;\n\n// ── ids ──────────────────────────────────────────────────────────────────────\n\nexport function newId(prefix = \"n\"): string {\n const rnd =\n typeof crypto !== \"undefined\" && \"randomUUID\" in crypto\n ? crypto.randomUUID().slice(0, 8)\n : Math.random().toString(36).slice(2, 10);\n return `${prefix}_${rnd}`;\n}\n\nexport function emptyDesign(name = \"Untitled menu\"): MenuDesign {\n return { designId: newId(\"d\"), name, surface: \"anywhere\", look: { ...DEFAULT_LOOK }, nodes: [], notes: \"\" };\n}\n\nexport function isContainer(node: MenuNode): node is MenuContainerNode {\n return node.kind === \"section\" || node.kind === \"submenu\";\n}\n\n/** A deep copy with every id renewed — what a duplicate or a second drop of a template gets. */\nexport function cloneNode<N extends MenuNode>(node: N): N {\n const copy = { ...node, id: newId(node.kind[0]) } as N;\n if (isContainer(copy)) (copy as MenuContainerNode).children = (copy as MenuContainerNode).children.map(cloneNode);\n return copy;\n}\n\n// ── reading the tree ─────────────────────────────────────────────────────────\n\nexport interface Located {\n node: MenuNode;\n /** The container's id, or null at the top. */\n parentId: string | null;\n index: number;\n depth: number;\n}\n\nexport function locate(nodes: readonly MenuNode[], id: string, parentId: string | null = null, depth = 0): Located | null {\n for (let i = 0; i < nodes.length; i++) {\n const node = nodes[i] as MenuNode;\n if (node.id === id) return { node, parentId, index: i, depth };\n if (isContainer(node)) {\n const inner = locate(node.children, id, node.id, depth + 1);\n if (inner) return inner;\n }\n }\n return null;\n}\n\nexport function contains(node: MenuNode, id: string): boolean {\n if (node.id === id) return true;\n return isContainer(node) && node.children.some((c) => contains(c, id));\n}\n\nexport function countItems(nodes: readonly MenuNode[]): number {\n let n = 0;\n for (const node of nodes) {\n if (node.kind === \"item\") n += 1;\n else if (isContainer(node)) n += countItems(node.children) + (node.kind === \"submenu\" ? 1 : 0);\n }\n return n;\n}\n\n// ── editing the tree (pure) ──────────────────────────────────────────────────\n\nfunction mapChildren(nodes: readonly MenuNode[], parentId: string | null, fn: (list: MenuNode[]) => MenuNode[]): MenuNode[] {\n if (parentId === null) return fn([...nodes]);\n return nodes.map((node) => {\n if (!isContainer(node)) return node;\n if (node.id === parentId) return { ...node, children: fn([...node.children]) };\n return { ...node, children: mapChildren(node.children, parentId, fn) };\n });\n}\n\nexport function insertNode(nodes: readonly MenuNode[], parentId: string | null, index: number, node: MenuNode): MenuNode[] {\n return mapChildren(nodes, parentId, (list) => {\n list.splice(Math.max(0, Math.min(index, list.length)), 0, node);\n return list;\n });\n}\n\nexport function removeNode(nodes: readonly MenuNode[], id: string): MenuNode[] {\n return nodes\n .filter((n) => n.id !== id)\n .map((n) => (isContainer(n) ? { ...n, children: removeNode(n.children, id) } : n));\n}\n\nexport function updateNode(nodes: readonly MenuNode[], id: string, patch: Partial<MenuNode>): MenuNode[] {\n return nodes.map((n) => {\n if (n.id === id) return { ...n, ...patch, id: n.id, kind: n.kind } as MenuNode;\n if (isContainer(n)) return { ...n, children: updateNode(n.children, id, patch) };\n return n;\n });\n}\n\n/**\n * Move `id` to `index` inside `parentId`. Refuses (returns the tree unchanged) a move into itself\n * or its own descendant. `index` is in the destination list as it reads BEFORE the move.\n */\nexport function canMove(nodes: readonly MenuNode[], id: string, parentId: string | null): boolean {\n const from = locate(nodes, id);\n return Boolean(from) && (parentId === null || !contains((from as Located).node, parentId));\n}\n\nexport function moveNode(nodes: readonly MenuNode[], id: string, parentId: string | null, index: number): MenuNode[] {\n const from = locate(nodes, id);\n // A refused move answers the SAME array, so a caller can tell \"nothing happened\" by identity.\n if (!from) return nodes as MenuNode[];\n if (parentId !== null && contains(from.node, parentId)) return nodes as MenuNode[];\n let target = index;\n if (from.parentId === parentId && from.index < index) target -= 1;\n return insertNode(removeNode(nodes, id), parentId, target, from.node);\n}\n\nexport function duplicateNode(nodes: readonly MenuNode[], id: string): MenuNode[] {\n const at = locate(nodes, id);\n if (!at) return [...nodes];\n return insertNode(nodes, at.parentId, at.index + 1, cloneNode(at.node));\n}\n\n/** Wrap a node into a new section (or submenu) in its place. */\nexport function wrapNode(nodes: readonly MenuNode[], id: string, kind: \"section\" | \"submenu\"): MenuNode[] {\n const at = locate(nodes, id);\n if (!at) return [...nodes];\n const wrapper: MenuContainerNode =\n kind === \"section\"\n ? { id: newId(\"s\"), kind: \"section\", label: \"New section\", children: [at.node] }\n : { id: newId(\"m\"), kind: \"submenu\", label: \"More\", icon: \"ChevronRight\", children: [at.node] };\n return insertNode(removeNode(nodes, id), at.parentId, at.index, wrapper);\n}\n\n/** Replace a container by its children. */\nexport function unwrapNode(nodes: readonly MenuNode[], id: string): MenuNode[] {\n const at = locate(nodes, id);\n if (!at || !isContainer(at.node)) return [...nodes];\n let out = removeNode(nodes, id);\n at.node.children.forEach((child, i) => {\n out = insertNode(out, at.parentId, at.index + i, child);\n });\n return out;\n}\n\n// ── reading a saved design defensively ──────────────────────────────────────\n\n/** A saved document back into a design; anything unreadable becomes an empty design, never a throw. */\nexport function parseDesign(raw: unknown): MenuDesign {\n let value: unknown = raw;\n if (typeof raw === \"string\") {\n try {\n value = JSON.parse(raw);\n } catch {\n return emptyDesign(\"Unreadable design\");\n }\n }\n const v = (value ?? {}) as Partial<MenuDesign> & { design?: MenuDesign };\n const d = v.design ?? v;\n return {\n designId: typeof d.designId === \"string\" ? d.designId : newId(\"d\"),\n name: typeof d.name === \"string\" ? d.name : \"Untitled menu\",\n surface: MENU_SURFACES.includes(d.surface as MenuSurface) ? (d.surface as MenuSurface) : \"anywhere\",\n look: { ...DEFAULT_LOOK, ...(d.look ?? {}) },\n nodes: Array.isArray(d.nodes) ? (d.nodes as MenuNode[]) : [],\n notes: typeof d.notes === \"string\" ? d.notes : \"\",\n };\n}\n\nexport function serializeDesign(design: MenuDesign): string {\n return JSON.stringify({ format: DESIGN_FORMAT, design });\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/design/store.ts","../src/design/model.ts"],"sourcesContent":["// src/design/store.ts — @ai-matrx/context-menu/design-store: WHERE MENU DESIGNS ARE SAVED.\n//\n// Every Save writes a NEW VERSION of a design; nothing is overwritten, so any earlier version can be\n// opened again. Two stores answer the same interface:\n// - recordsDesignStore: a typed table in the record store (`custom.*`), so a saved design is real\n// shared data the builder's author, Claude, and the app can all read — no hand-made schema;\n// - localDesignStore: this browser's storage, for a playground with no sign-in; it says so.\n\nimport { listAppRows, removeAppRow, upsertAppRow, defineTypedTable, f } from \"@ai-matrx/records/typed-table\";\nimport type { RecordsClient } from \"@ai-matrx/records/core\";\nimport { parseDesign, serializeDesign, type MenuDesign } from \"./model\";\n\nexport const menuDesignTable = defineTypedTable({\n name: \"Menu designs\",\n slug: \"menu_designs\",\n kept_for: \"context_menu\",\n scope: \"organization\",\n owner: \"lane:CONTEXT-MENU\",\n key: \"version_key\",\n declared_in: \"aidream:apps/shared/context-menu/src/design/store.ts\",\n fields: {\n version_key: f.text({ label: \"Version key\", unique: true, required: true }),\n design_id: f.text({ label: \"Design id\", required: true }),\n name: f.text({ label: \"Name\", required: true }),\n version: f.integer({ label: \"Version\", required: true }),\n doc: f.longText({ label: \"Menu\", required: true }),\n note: f.text({ label: \"Note\" }),\n saved_by: f.text({ label: \"Saved by\" }),\n saved_at: f.datetime({ label: \"Saved\" }),\n },\n});\n\nexport interface SavedDesignVersion {\n /** The stored row (record id, or a local key). */\n rowId: string;\n designId: string;\n name: string;\n version: number;\n note: string;\n savedAt: string;\n savedBy: string;\n design: MenuDesign;\n}\n\nexport interface DesignStore {\n /** \"live\" saves to shared data; \"local\" saves to this browser only. */\n readonly kind: \"live\" | \"local\";\n /** Every saved version of every design, newest first. */\n list(): Promise<SavedDesignVersion[]>;\n /** Save `design` as its next version. Answers the saved version. */\n save(design: MenuDesign, note?: string): Promise<SavedDesignVersion>;\n /** Archive one version (restorable from the record store's archive). */\n remove(rowId: string): Promise<void>;\n}\n\nconst byNewest = (a: SavedDesignVersion, b: SavedDesignVersion) =>\n b.savedAt.localeCompare(a.savedAt) || b.version - a.version;\n\nfunction nextVersion(all: SavedDesignVersion[], designId: string): number {\n return all.filter((v) => v.designId === designId).reduce((m, v) => Math.max(m, v.version), 0) + 1;\n}\n\nexport function recordsDesignStore(client: RecordsClient, options: { organizationId: string; savedBy?: string }): DesignStore {\n const list = async (): Promise<SavedDesignVersion[]> => {\n const read = await listAppRows(client, menuDesignTable);\n if (!read.ok) throw new Error(read.error.message);\n return read.data.rows\n .map((r) => ({\n rowId: String(r._id),\n designId: String(r.design_id),\n name: String(r.name),\n version: Number(r.version),\n note: String(r.note ?? \"\"),\n savedAt: String(r.saved_at ?? \"\"),\n savedBy: String(r.saved_by ?? \"\"),\n design: parseDesign(r.doc),\n }))\n .sort(byNewest);\n };\n return {\n kind: \"live\",\n list,\n async save(design, note = \"\") {\n const all = await list();\n const version = nextVersion(all, design.designId);\n const savedAt = new Date().toISOString();\n const savedBy = options.savedBy ?? \"\";\n const wrote = await upsertAppRow(\n client,\n menuDesignTable,\n {\n version_key: `${design.designId}@${version}`,\n design_id: design.designId,\n name: design.name,\n version,\n doc: serializeDesign(design),\n note,\n saved_by: savedBy,\n saved_at: savedAt,\n },\n { organizationId: options.organizationId },\n );\n if (!wrote.ok) throw new Error(wrote.error.message);\n return { rowId: wrote.data, designId: design.designId, name: design.name, version, note, savedAt, savedBy, design };\n },\n async remove(rowId) {\n const gone = await removeAppRow(client, menuDesignTable, rowId, { organizationId: options.organizationId });\n if (!gone.ok) throw new Error(gone.error.message);\n },\n };\n}\n\nconst LOCAL_KEY = \"matrx.context-menu.designs.v1\";\n\nexport function localDesignStore(): DesignStore {\n const read = (): SavedDesignVersion[] => {\n try {\n const raw = globalThis.localStorage?.getItem(LOCAL_KEY);\n const rows = raw ? (JSON.parse(raw) as SavedDesignVersion[]) : [];\n return rows.map((r) => ({ ...r, design: parseDesign(r.design) }));\n } catch {\n return [];\n }\n };\n const write = (rows: SavedDesignVersion[]) => {\n try {\n globalThis.localStorage?.setItem(LOCAL_KEY, JSON.stringify(rows));\n } catch {\n /* storage blocked: the save still answers; the next reload will not have it */\n }\n };\n return {\n kind: \"local\",\n async list() {\n return read().sort(byNewest);\n },\n async save(design, note = \"\") {\n const all = read();\n const version = nextVersion(all, design.designId);\n const saved: SavedDesignVersion = {\n rowId: `${design.designId}@${version}`,\n designId: design.designId,\n name: design.name,\n version,\n note,\n savedAt: new Date().toISOString(),\n savedBy: \"this browser\",\n design,\n };\n write([saved, ...all]);\n return saved;\n },\n async remove(rowId) {\n write(read().filter((r) => r.rowId !== rowId));\n },\n };\n}\n","// src/design/model.ts — @ai-matrx/context-menu: THE MENU DESIGN.\n//\n// A design is the whole menu a person drew in the builder: an ordered tree of items, sections,\n// submenus and dividers, plus the few presentation choices that change how it reads. It is plain\n// JSON (no functions, no React) so it saves as one record, travels in a URL or a chat, and is the\n// exact thing the real menu is later built from. Every edit is a pure function returning a new\n// design; nothing here mutates.\n\nexport type MenuNodeKind = \"item\" | \"section\" | \"submenu\" | \"divider\" | \"slot\";\n\ninterface NodeBase {\n /** Stable within the design. */\n id: string;\n /** The catalog option this was dragged from, when it came from one. */\n catalogId?: string;\n /** A designer's note on this one entry (\"only on a table row\", \"merge with Copy\"). */\n note?: string;\n}\n\nexport interface MenuItemNode extends NodeBase {\n kind: \"item\";\n label: string;\n /** A lucide icon name (`Sparkles`, `Copy`). */\n icon?: string;\n /** Shown right-aligned (`⌘C`). */\n shortcut?: string;\n /** One short line under the label. */\n hint?: string;\n /** Shown greyed out — the design says \"this exists here but is unavailable now\". */\n disabled?: boolean;\n /** Red, for actions that remove or discard. */\n danger?: boolean;\n /** A checkbox-style toggle item. */\n toggle?: boolean;\n /** Drawn as a highlighted, AI-flavoured item. */\n ai?: boolean;\n}\n\nexport interface MenuSectionNode extends NodeBase {\n kind: \"section\";\n /** The small heading. Empty = an unlabelled group. */\n label: string;\n children: MenuNode[];\n /** Starts folded; a click on the heading opens it. */\n collapsible?: boolean;\n /** How the section's items are laid out. */\n layout?: \"list\" | \"grid\" | \"row\";\n}\n\nexport interface MenuSubmenuNode extends NodeBase {\n kind: \"submenu\";\n label: string;\n icon?: string;\n hint?: string;\n children: MenuNode[];\n}\n\nexport interface MenuDividerNode extends NodeBase {\n kind: \"divider\";\n}\n\n/**\n * A PLACEHOLDER: \"every other option of this kind goes here\". `shape: \"items\"` lays them out as rows\n * where the placeholder sits; `shape: \"submenu\"` gathers them into one submenu. `fills` names the\n * kind (a catalog group, the agent library, the page's own rows, or everything not placed).\n */\nexport interface MenuSlotNode extends NodeBase {\n kind: \"slot\";\n label: string;\n fills: string;\n shape: \"items\" | \"submenu\";\n icon?: string;\n}\n\nexport type MenuNode = MenuItemNode | MenuSectionNode | MenuSubmenuNode | MenuDividerNode | MenuSlotNode;\nexport type MenuContainerNode = MenuSectionNode | MenuSubmenuNode;\n\n/** Where the menu opens; the builder previews each the way it appears there. */\nexport const MENU_SURFACES = [\n \"text-selection\",\n \"editable-field\",\n \"table-row\",\n \"page\",\n \"chat-message\",\n \"file\",\n \"anywhere\",\n] as const;\nexport type MenuSurface = (typeof MENU_SURFACES)[number];\n\nexport interface MenuLook {\n density: \"compact\" | \"comfortable\";\n width: number;\n showIcons: boolean;\n showShortcuts: boolean;\n showHints: boolean;\n /** A search box at the top of the menu. */\n search: boolean;\n}\n\nexport const DEFAULT_LOOK: MenuLook = {\n density: \"comfortable\",\n width: 280,\n showIcons: true,\n showShortcuts: true,\n showHints: false,\n search: false,\n};\n\nexport interface MenuDesign {\n /** The design's identity; every version shares it. */\n designId: string;\n name: string;\n surface: MenuSurface;\n look: MenuLook;\n nodes: MenuNode[];\n /** Free notes for whoever builds it. */\n notes: string;\n}\n\nexport const DESIGN_FORMAT = 1;\n\n// ── ids ──────────────────────────────────────────────────────────────────────\n\nexport function newId(prefix = \"n\"): string {\n const rnd =\n typeof crypto !== \"undefined\" && \"randomUUID\" in crypto\n ? crypto.randomUUID().slice(0, 8)\n : Math.random().toString(36).slice(2, 10);\n return `${prefix}_${rnd}`;\n}\n\nexport function emptyDesign(name = \"Untitled menu\"): MenuDesign {\n return { designId: newId(\"d\"), name, surface: \"anywhere\", look: { ...DEFAULT_LOOK }, nodes: [], notes: \"\" };\n}\n\nexport function isContainer(node: MenuNode): node is MenuContainerNode {\n return node.kind === \"section\" || node.kind === \"submenu\";\n}\n\n/** Every catalog option the design already places (so a placeholder holds only the rest). */\nexport function placedCatalogIds(nodes: readonly MenuNode[], out = new Set<string>()): Set<string> {\n for (const n of nodes) {\n if (n.catalogId) out.add(n.catalogId);\n if (isContainer(n)) placedCatalogIds(n.children, out);\n }\n return out;\n}\n\n/** How many times each catalog option appears in the design (a duplicate counts twice). */\nexport function catalogUseCounts(nodes: readonly MenuNode[], out = new Map<string, number>()): Map<string, number> {\n for (const n of nodes) {\n if (n.catalogId) out.set(n.catalogId, (out.get(n.catalogId) ?? 0) + 1);\n if (isContainer(n)) catalogUseCounts(n.children, out);\n }\n return out;\n}\n\n/** A deep copy with every id renewed — what a duplicate or a second drop of a template gets. */\nexport function cloneNode<N extends MenuNode>(node: N): N {\n const copy = { ...node, id: newId(node.kind[0]) } as N;\n if (isContainer(copy)) (copy as MenuContainerNode).children = (copy as MenuContainerNode).children.map(cloneNode);\n return copy;\n}\n\n// ── reading the tree ─────────────────────────────────────────────────────────\n\nexport interface Located {\n node: MenuNode;\n /** The container's id, or null at the top. */\n parentId: string | null;\n index: number;\n depth: number;\n}\n\nexport function locate(nodes: readonly MenuNode[], id: string, parentId: string | null = null, depth = 0): Located | null {\n for (let i = 0; i < nodes.length; i++) {\n const node = nodes[i] as MenuNode;\n if (node.id === id) return { node, parentId, index: i, depth };\n if (isContainer(node)) {\n const inner = locate(node.children, id, node.id, depth + 1);\n if (inner) return inner;\n }\n }\n return null;\n}\n\nexport function contains(node: MenuNode, id: string): boolean {\n if (node.id === id) return true;\n return isContainer(node) && node.children.some((c) => contains(c, id));\n}\n\nexport function countItems(nodes: readonly MenuNode[]): number {\n let n = 0;\n for (const node of nodes) {\n if (node.kind === \"item\" || node.kind === \"slot\") n += 1;\n else if (isContainer(node)) n += countItems(node.children) + (node.kind === \"submenu\" ? 1 : 0);\n }\n return n;\n}\n\n// ── editing the tree (pure) ──────────────────────────────────────────────────\n\nfunction mapChildren(nodes: readonly MenuNode[], parentId: string | null, fn: (list: MenuNode[]) => MenuNode[]): MenuNode[] {\n if (parentId === null) return fn([...nodes]);\n return nodes.map((node) => {\n if (!isContainer(node)) return node;\n if (node.id === parentId) return { ...node, children: fn([...node.children]) };\n return { ...node, children: mapChildren(node.children, parentId, fn) };\n });\n}\n\nexport function insertNode(nodes: readonly MenuNode[], parentId: string | null, index: number, node: MenuNode): MenuNode[] {\n return mapChildren(nodes, parentId, (list) => {\n list.splice(Math.max(0, Math.min(index, list.length)), 0, node);\n return list;\n });\n}\n\nexport function removeNode(nodes: readonly MenuNode[], id: string): MenuNode[] {\n return nodes\n .filter((n) => n.id !== id)\n .map((n) => (isContainer(n) ? { ...n, children: removeNode(n.children, id) } : n));\n}\n\nexport function updateNode(nodes: readonly MenuNode[], id: string, patch: Partial<MenuNode>): MenuNode[] {\n return nodes.map((n) => {\n if (n.id === id) return { ...n, ...patch, id: n.id, kind: n.kind } as MenuNode;\n if (isContainer(n)) return { ...n, children: updateNode(n.children, id, patch) };\n return n;\n });\n}\n\n/**\n * Move `id` to `index` inside `parentId`. Refuses (returns the tree unchanged) a move into itself\n * or its own descendant. `index` is in the destination list as it reads BEFORE the move.\n */\nexport function canMove(nodes: readonly MenuNode[], id: string, parentId: string | null): boolean {\n const from = locate(nodes, id);\n return Boolean(from) && (parentId === null || !contains((from as Located).node, parentId));\n}\n\nexport function moveNode(nodes: readonly MenuNode[], id: string, parentId: string | null, index: number): MenuNode[] {\n const from = locate(nodes, id);\n // A refused move answers the SAME array, so a caller can tell \"nothing happened\" by identity.\n if (!from) return nodes as MenuNode[];\n if (parentId !== null && contains(from.node, parentId)) return nodes as MenuNode[];\n let target = index;\n if (from.parentId === parentId && from.index < index) target -= 1;\n return insertNode(removeNode(nodes, id), parentId, target, from.node);\n}\n\nexport function duplicateNode(nodes: readonly MenuNode[], id: string): MenuNode[] {\n const at = locate(nodes, id);\n if (!at) return [...nodes];\n return insertNode(nodes, at.parentId, at.index + 1, cloneNode(at.node));\n}\n\n/** Wrap a node into a new section (or submenu) in its place. */\nexport function wrapNode(nodes: readonly MenuNode[], id: string, kind: \"section\" | \"submenu\"): MenuNode[] {\n const at = locate(nodes, id);\n if (!at) return [...nodes];\n const wrapper: MenuContainerNode =\n kind === \"section\"\n ? { id: newId(\"s\"), kind: \"section\", label: \"New section\", children: [at.node] }\n : { id: newId(\"m\"), kind: \"submenu\", label: \"More\", icon: \"ChevronRight\", children: [at.node] };\n return insertNode(removeNode(nodes, id), at.parentId, at.index, wrapper);\n}\n\n/** Replace a container by its children. */\nexport function unwrapNode(nodes: readonly MenuNode[], id: string): MenuNode[] {\n const at = locate(nodes, id);\n if (!at || !isContainer(at.node)) return [...nodes];\n let out = removeNode(nodes, id);\n at.node.children.forEach((child, i) => {\n out = insertNode(out, at.parentId, at.index + i, child);\n });\n return out;\n}\n\n// ── reading a saved design defensively ──────────────────────────────────────\n\n/** A saved document back into a design; anything unreadable becomes an empty design, never a throw. */\nexport function parseDesign(raw: unknown): MenuDesign {\n let value: unknown = raw;\n if (typeof raw === \"string\") {\n try {\n value = JSON.parse(raw);\n } catch {\n return emptyDesign(\"Unreadable design\");\n }\n }\n const v = (value ?? {}) as Partial<MenuDesign> & { design?: MenuDesign };\n const d = v.design ?? v;\n return {\n designId: typeof d.designId === \"string\" ? d.designId : newId(\"d\"),\n name: typeof d.name === \"string\" ? d.name : \"Untitled menu\",\n surface: MENU_SURFACES.includes(d.surface as MenuSurface) ? (d.surface as MenuSurface) : \"anywhere\",\n look: { ...DEFAULT_LOOK, ...(d.look ?? {}) },\n nodes: Array.isArray(d.nodes) ? (d.nodes as MenuNode[]) : [],\n notes: typeof d.notes === \"string\" ? d.notes : \"\",\n };\n}\n\nexport function serializeDesign(design: MenuDesign): string {\n return JSON.stringify({ format: DESIGN_FORMAT, design });\n}\n"],"mappings":"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","names":[]}
|
package/dist/index.cjs
CHANGED
|
@@ -27,9 +27,11 @@ __export(src_exports, {
|
|
|
27
27
|
DESIGN_FORMAT: () => DESIGN_FORMAT,
|
|
28
28
|
MENU_SURFACES: () => MENU_SURFACES,
|
|
29
29
|
MenuPreview: () => MenuPreview,
|
|
30
|
+
SLOT_SOURCES: () => SLOT_SOURCES,
|
|
30
31
|
TEMPLATES: () => TEMPLATES,
|
|
31
32
|
canMove: () => canMove,
|
|
32
33
|
catalogOption: () => catalogOption,
|
|
34
|
+
catalogUseCounts: () => catalogUseCounts,
|
|
33
35
|
cloneNode: () => cloneNode,
|
|
34
36
|
contains: () => contains,
|
|
35
37
|
countItems: () => countItems,
|
|
@@ -41,8 +43,11 @@ __export(src_exports, {
|
|
|
41
43
|
moveNode: () => moveNode,
|
|
42
44
|
newId: () => newId,
|
|
43
45
|
parseDesign: () => parseDesign,
|
|
46
|
+
placedCatalogIds: () => placedCatalogIds,
|
|
44
47
|
removeNode: () => removeNode,
|
|
45
48
|
serializeDesign: () => serializeDesign,
|
|
49
|
+
slotOptions: () => slotOptions,
|
|
50
|
+
slotSource: () => slotSource,
|
|
46
51
|
unwrapNode: () => unwrapNode,
|
|
47
52
|
updateNode: () => updateNode,
|
|
48
53
|
wrapNode: () => wrapNode
|
|
@@ -78,6 +83,20 @@ function emptyDesign(name = "Untitled menu") {
|
|
|
78
83
|
function isContainer(node) {
|
|
79
84
|
return node.kind === "section" || node.kind === "submenu";
|
|
80
85
|
}
|
|
86
|
+
function placedCatalogIds(nodes, out = /* @__PURE__ */ new Set()) {
|
|
87
|
+
for (const n of nodes) {
|
|
88
|
+
if (n.catalogId) out.add(n.catalogId);
|
|
89
|
+
if (isContainer(n)) placedCatalogIds(n.children, out);
|
|
90
|
+
}
|
|
91
|
+
return out;
|
|
92
|
+
}
|
|
93
|
+
function catalogUseCounts(nodes, out = /* @__PURE__ */ new Map()) {
|
|
94
|
+
for (const n of nodes) {
|
|
95
|
+
if (n.catalogId) out.set(n.catalogId, (out.get(n.catalogId) ?? 0) + 1);
|
|
96
|
+
if (isContainer(n)) catalogUseCounts(n.children, out);
|
|
97
|
+
}
|
|
98
|
+
return out;
|
|
99
|
+
}
|
|
81
100
|
function cloneNode(node) {
|
|
82
101
|
const copy = { ...node, id: newId(node.kind[0]) };
|
|
83
102
|
if (isContainer(copy)) copy.children = copy.children.map(cloneNode);
|
|
@@ -101,7 +120,7 @@ function contains(node, id) {
|
|
|
101
120
|
function countItems(nodes) {
|
|
102
121
|
let n = 0;
|
|
103
122
|
for (const node of nodes) {
|
|
104
|
-
if (node.kind === "item") n += 1;
|
|
123
|
+
if (node.kind === "item" || node.kind === "slot") n += 1;
|
|
105
124
|
else if (isContainer(node)) n += countItems(node.children) + (node.kind === "submenu" ? 1 : 0);
|
|
106
125
|
}
|
|
107
126
|
return n;
|
|
@@ -326,6 +345,25 @@ var TEMPLATES = [
|
|
|
326
345
|
{ id: "sub-organize", label: "Organize \u25B8", icon: "Tags", build: () => sub("Organize", "Tags", ["cm:attach", "pin-message", "cm:view-history", "cm:compare"]) },
|
|
327
346
|
{ id: "sub-tools", label: "Tools \u25B8", icon: "Wrench", build: () => sub("Tools", "Wrench", ["cm:speak", "cm:quick-actions", "feedback", "cm:x:surface", "cm:admin"]) }
|
|
328
347
|
];
|
|
348
|
+
var SLOT_SOURCES = [
|
|
349
|
+
{ id: "any", label: "Everything not placed", phrase: "Everything else", icon: "Ellipsis" },
|
|
350
|
+
{ id: "agents", label: "Agents & shortcuts (library)", phrase: "All other agents", icon: "Bot", runtime: true },
|
|
351
|
+
{ id: "page-rows", label: "This page's own rows", phrase: "All of this page's actions", icon: "PanelTop", runtime: true },
|
|
352
|
+
...CATALOG_GROUPS.map((g) => ({
|
|
353
|
+
id: `group:${g}`,
|
|
354
|
+
label: g,
|
|
355
|
+
phrase: g === "AI" ? "All other AI items" : `All other ${g.toLowerCase()} options`,
|
|
356
|
+
icon: CATALOG.find((c) => c.group === g)?.icon ?? "Circle"
|
|
357
|
+
}))
|
|
358
|
+
];
|
|
359
|
+
function slotSource(id) {
|
|
360
|
+
return SLOT_SOURCES.find((s) => s.id === id);
|
|
361
|
+
}
|
|
362
|
+
function slotOptions(fills, placed) {
|
|
363
|
+
if (fills === "agents" || fills === "page-rows") return [];
|
|
364
|
+
const pool = fills === "any" ? CATALOG : CATALOG.filter((c) => `group:${c.group}` === fills);
|
|
365
|
+
return pool.filter((c) => !placed.has(c.id));
|
|
366
|
+
}
|
|
329
367
|
|
|
330
368
|
// src/design/MenuPreview.tsx
|
|
331
369
|
var import_react = require("react");
|
|
@@ -333,7 +371,7 @@ var import_jsx_runtime = require("react/jsx-runtime");
|
|
|
333
371
|
function matches(node, q) {
|
|
334
372
|
if (!q) return true;
|
|
335
373
|
if (node.kind === "divider") return false;
|
|
336
|
-
if (node.kind === "item") return node.label.toLowerCase().includes(q);
|
|
374
|
+
if (node.kind === "item" || node.kind === "slot") return node.label.toLowerCase().includes(q);
|
|
337
375
|
return node.label.toLowerCase().includes(q) || node.children.some((c) => matches(c, q));
|
|
338
376
|
}
|
|
339
377
|
function flatten(nodes, q, out = []) {
|
|
@@ -348,6 +386,8 @@ function MenuPreview({ design, renderIcon, highlightId, onSelectNode, onPick, cl
|
|
|
348
386
|
const q = query.trim().toLowerCase();
|
|
349
387
|
const { look } = design;
|
|
350
388
|
const results = (0, import_react.useMemo)(() => q ? flatten(design.nodes, q) : null, [design.nodes, q]);
|
|
389
|
+
const placed = (0, import_react.useMemo)(() => placedCatalogIds(design.nodes), [design.nodes]);
|
|
390
|
+
const ctx = { design, renderIcon, highlightId, onSelectNode, onPick, placed };
|
|
351
391
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
352
392
|
"div",
|
|
353
393
|
{
|
|
@@ -360,7 +400,7 @@ function MenuPreview({ design, renderIcon, highlightId, onSelectNode, onPick, cl
|
|
|
360
400
|
renderIcon("Search", "cm-icon"),
|
|
361
401
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("input", { value: query, onChange: (e) => setQuery(e.target.value), placeholder: "Type to filter\u2026", "aria-label": "Filter menu" })
|
|
362
402
|
] }),
|
|
363
|
-
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
|
|
403
|
+
results ? results.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "cm-empty", children: "No matches" }) : results.map((n) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Item, { node: n, ctx }, n.id)) : design.nodes.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "cm-empty", children: "Empty menu \u2014 drag options in" }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Nodes, { nodes: design.nodes, ctx })
|
|
364
404
|
]
|
|
365
405
|
}
|
|
366
406
|
);
|
|
@@ -370,6 +410,7 @@ function Nodes({ nodes, ctx }) {
|
|
|
370
410
|
if (n.kind === "divider") return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { role: "separator", className: "cm-divider" }, n.id);
|
|
371
411
|
if (n.kind === "item") return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Item, { node: n, ctx }, n.id);
|
|
372
412
|
if (n.kind === "submenu") return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Submenu, { node: n, ctx }, n.id);
|
|
413
|
+
if (n.kind === "slot") return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Slot, { node: n, ctx }, n.id);
|
|
373
414
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Section, { node: n, ctx }, n.id);
|
|
374
415
|
}) });
|
|
375
416
|
}
|
|
@@ -453,8 +494,7 @@ function Submenu({ node, ctx }) {
|
|
|
453
494
|
const [open, setOpen] = (0, import_react.useState)(false);
|
|
454
495
|
const [side, setSide] = (0, import_react.useState)("right");
|
|
455
496
|
const show = (el) => {
|
|
456
|
-
|
|
457
|
-
setSide(r.right + look.width + 8 > window.innerWidth ? "left" : "right");
|
|
497
|
+
setSide(flyoutSide(el, look.width));
|
|
458
498
|
setOpen(true);
|
|
459
499
|
};
|
|
460
500
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "cm-sub", onMouseEnter: (e) => show(e.currentTarget), onMouseLeave: () => setOpen(false), children: [
|
|
@@ -483,4 +523,61 @@ function Submenu({ node, ctx }) {
|
|
|
483
523
|
open && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { role: "menu", className: "cm-menu cm-flyout", "data-side": side, "data-density": look.density, style: { width: look.width }, children: node.children.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "cm-empty", children: "Empty submenu" }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Nodes, { nodes: node.children, ctx }) })
|
|
484
524
|
] });
|
|
485
525
|
}
|
|
526
|
+
function flyoutSide(row, width) {
|
|
527
|
+
const r = row.getBoundingClientRect();
|
|
528
|
+
const bounds = row.closest("[data-menu-bounds]")?.getBoundingClientRect() ?? { left: 0, right: window.innerWidth };
|
|
529
|
+
if (r.right + width + 8 <= bounds.right) return "right";
|
|
530
|
+
if (r.left - width - 8 >= bounds.left) return "left";
|
|
531
|
+
return "below";
|
|
532
|
+
}
|
|
533
|
+
function Slot({ node, ctx }) {
|
|
534
|
+
const { look } = ctx.design;
|
|
535
|
+
const source = slotSource(node.fills);
|
|
536
|
+
const options = slotOptions(node.fills, ctx.placed);
|
|
537
|
+
const [open, setOpen] = (0, import_react.useState)(false);
|
|
538
|
+
const [side, setSide] = (0, import_react.useState)("right");
|
|
539
|
+
const count = source?.runtime ? "live" : String(options.length);
|
|
540
|
+
const row = (
|
|
541
|
+
// ui-exception: a menu row of the menu component itself
|
|
542
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
543
|
+
"button",
|
|
544
|
+
{
|
|
545
|
+
type: "button",
|
|
546
|
+
role: "menuitem",
|
|
547
|
+
"aria-haspopup": node.shape === "submenu" ? "menu" : void 0,
|
|
548
|
+
title: source ? `Placeholder: ${source.label}` : "Placeholder",
|
|
549
|
+
className: `cm-item cm-slot ${ctx.highlightId === node.id ? "cm-hl" : ""}`,
|
|
550
|
+
onClick: () => {
|
|
551
|
+
ctx.onSelectNode?.(node.id);
|
|
552
|
+
if (node.shape === "submenu") setOpen(true);
|
|
553
|
+
},
|
|
554
|
+
children: [
|
|
555
|
+
look.showIcons && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "cm-icon-slot", children: ctx.renderIcon(node.icon ?? source?.icon ?? "Ellipsis", "cm-icon") }),
|
|
556
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "cm-text", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "cm-label", children: node.label }) }),
|
|
557
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "cm-count", children: count }),
|
|
558
|
+
node.shape === "submenu" && ctx.renderIcon("ChevronRight", "cm-icon cm-chev")
|
|
559
|
+
]
|
|
560
|
+
}
|
|
561
|
+
)
|
|
562
|
+
);
|
|
563
|
+
if (node.shape === "items") return row;
|
|
564
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
565
|
+
"div",
|
|
566
|
+
{
|
|
567
|
+
className: "cm-sub",
|
|
568
|
+
onMouseEnter: (e) => {
|
|
569
|
+
setSide(flyoutSide(e.currentTarget, look.width));
|
|
570
|
+
setOpen(true);
|
|
571
|
+
},
|
|
572
|
+
onMouseLeave: () => setOpen(false),
|
|
573
|
+
children: [
|
|
574
|
+
row,
|
|
575
|
+
open && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { role: "menu", className: "cm-menu cm-flyout", "data-side": side, "data-density": look.density, style: { width: look.width }, children: source?.runtime ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "cm-runtime", children: [
|
|
576
|
+
"Filled at runtime: ",
|
|
577
|
+
source.label
|
|
578
|
+
] }) : options.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "cm-empty", children: "Nothing left \u2014 every option of this kind is placed" }) : options.map((o2) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Item, { node: { id: `slot-${node.id}-${o2.id}`, kind: "item", label: o2.label, icon: o2.icon, catalogId: o2.id, ...o2.shortcut ? { shortcut: o2.shortcut } : {}, ...o2.ai ? { ai: true } : {}, ...o2.danger ? { danger: true } : {} }, ctx }, o2.id)) })
|
|
579
|
+
]
|
|
580
|
+
}
|
|
581
|
+
);
|
|
582
|
+
}
|
|
486
583
|
//# sourceMappingURL=index.cjs.map
|