@ai-matrx/context-menu 0.1.9 → 0.1.11
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 +12 -0
- package/dist/builder.cjs +17 -18
- package/dist/builder.cjs.map +1 -1
- package/dist/builder.js +2 -3
- package/dist/builder.js.map +1 -1
- package/dist/{chunk-ECKA474B.js → chunk-2B37SXQW.js} +21 -12
- package/dist/chunk-2B37SXQW.js.map +1 -0
- package/dist/{chunk-TSOUI723.cjs → chunk-7TIZAANU.cjs} +26 -11
- package/dist/chunk-7TIZAANU.cjs.map +1 -0
- package/dist/{chunk-A24SBKSA.js → chunk-EG23Y2KW.js} +14 -13
- package/dist/{chunk-A24SBKSA.js.map → chunk-EG23Y2KW.js.map} +1 -1
- package/dist/{chunk-7VDSMZWI.cjs → chunk-ERFH2RZC.cjs} +18 -17
- package/dist/chunk-ERFH2RZC.cjs.map +1 -0
- package/dist/{chunk-EIP5YS4Y.js → chunk-HJU3RU2B.js} +27 -12
- package/dist/chunk-HJU3RU2B.js.map +1 -0
- package/dist/{chunk-SWEWOUHK.cjs → chunk-IH5WN5JP.cjs} +31 -26
- package/dist/chunk-IH5WN5JP.cjs.map +1 -0
- package/dist/{chunk-QXHLM7SS.cjs → chunk-LDMYNYFC.cjs} +42 -33
- package/dist/chunk-LDMYNYFC.cjs.map +1 -0
- package/dist/{chunk-ANJYKQAA.cjs → chunk-MBU4Y22O.cjs} +3 -3
- package/dist/{chunk-ANJYKQAA.cjs.map → chunk-MBU4Y22O.cjs.map} +1 -1
- package/dist/{chunk-L6DPISFT.js → chunk-NBEXJ25Z.js} +24 -19
- package/dist/{chunk-L6DPISFT.js.map → chunk-NBEXJ25Z.js.map} +1 -1
- package/dist/{chunk-LE6ZUVCO.js → chunk-PB3PMDKV.js} +2 -2
- package/dist/{chunk-NOYMFNSU.js → chunk-SJWKMZEJ.js} +33 -12
- package/dist/chunk-SJWKMZEJ.js.map +1 -0
- package/dist/{chunk-XERD2YCJ.cjs → chunk-WROBL3DR.cjs} +32 -11
- package/dist/chunk-WROBL3DR.cjs.map +1 -0
- package/dist/design-store.cjs +0 -1
- package/dist/design-store.cjs.map +1 -1
- package/dist/design-store.js +0 -1
- package/dist/design-store.js.map +1 -1
- package/dist/engine.cjs +64 -96
- package/dist/engine.cjs.map +1 -1
- package/dist/engine.d.cts +7 -7
- package/dist/engine.d.ts +7 -7
- package/dist/engine.js +72 -104
- package/dist/engine.js.map +1 -1
- package/dist/index.cjs +3 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +2 -3
- package/dist/menu-content.cjs +18 -19
- package/dist/menu-content.cjs.map +1 -1
- package/dist/menu-content.js +4 -5
- package/dist/menu-content.js.map +1 -1
- package/dist/menu-inspect.cjs +0 -1
- package/dist/menu-inspect.cjs.map +1 -1
- package/dist/menu-inspect.js +0 -1
- package/dist/menu-inspect.js.map +1 -1
- package/dist/model.cjs +4 -5
- package/dist/model.cjs.map +1 -1
- package/dist/model.js +3 -4
- package/dist/react.cjs +52 -44
- package/dist/react.cjs.map +1 -1
- package/dist/react.js +30 -22
- package/dist/react.js.map +1 -1
- package/package.json +6 -6
- package/dist/chunk-7VDSMZWI.cjs.map +0 -1
- package/dist/chunk-CUODNUAD.cjs +0 -24
- package/dist/chunk-CUODNUAD.cjs.map +0 -1
- package/dist/chunk-ECKA474B.js.map +0 -1
- package/dist/chunk-EIP5YS4Y.js.map +0 -1
- package/dist/chunk-NOYMFNSU.js.map +0 -1
- package/dist/chunk-PE3ZHHM5.js +0 -24
- package/dist/chunk-PE3ZHHM5.js.map +0 -1
- package/dist/chunk-QXHLM7SS.cjs.map +0 -1
- package/dist/chunk-SWEWOUHK.cjs.map +0 -1
- package/dist/chunk-TSOUI723.cjs.map +0 -1
- package/dist/chunk-XERD2YCJ.cjs.map +0 -1
- /package/dist/{chunk-LE6ZUVCO.js.map → chunk-PB3PMDKV.js.map} +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ai-matrx/context-menu",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.11",
|
|
4
4
|
"description": "The AI Matrx context menu: the menu design model, the option catalog, the drag-and-drop menu builder, and (in progress) the live context menu engine moved out of matrx-frontend.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
@@ -115,16 +115,16 @@
|
|
|
115
115
|
"@radix-ui/react-slot": "^1.3.3",
|
|
116
116
|
"lucide-react": "^1.47.0",
|
|
117
117
|
"@ai-matrx/alchemy": "latest",
|
|
118
|
-
"@ai-matrx/
|
|
119
|
-
"@ai-matrx/design-system": "latest",
|
|
118
|
+
"@ai-matrx/chat": "latest",
|
|
120
119
|
"@ai-matrx/icons": "latest",
|
|
121
|
-
"@ai-matrx/kit": "latest",
|
|
122
120
|
"@ai-matrx/media": "latest",
|
|
123
121
|
"@ai-matrx/records": "latest",
|
|
122
|
+
"@ai-matrx/content-ir": "latest",
|
|
124
123
|
"@ai-matrx/records-ui": "latest",
|
|
125
124
|
"@ai-matrx/rich-content": "latest",
|
|
126
125
|
"@ai-matrx/rich-editor": "latest",
|
|
127
|
-
"@ai-matrx/
|
|
126
|
+
"@ai-matrx/design-system": "latest",
|
|
127
|
+
"@ai-matrx/kit": "latest"
|
|
128
128
|
},
|
|
129
129
|
"peerDependencies": {
|
|
130
130
|
"@reduxjs/toolkit": ">=2.0.0",
|
|
@@ -187,5 +187,5 @@
|
|
|
187
187
|
"builder:build": "vite build --config demo/vite.config.ts",
|
|
188
188
|
"builder:preview": "vite preview --config demo/vite.config.ts"
|
|
189
189
|
},
|
|
190
|
-
"gitHead": "
|
|
190
|
+
"gitHead": "7ee2d68e156416d62a043d8f5053ff7f0b5792fc"
|
|
191
191
|
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["/home/runner/work/aidream/aidream/apps/shared/context-menu/dist/chunk-7VDSMZWI.cjs","../src/engine/table-row-item.ts","../src/engine/record-menu-registry.ts","../src/engine/utils/per-row-entity.ts","../src/engine/utils/join-extra-sections.ts","../src/engine/utils/widget-handle.ts","../src/engine/utils/table-at-target.ts","../src/engine/region-trigger-attrs.ts","../src/engine/item/itemMenuToV3.ts","../src/engine/selection-provider.ts"],"names":[],"mappings":"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AAA;AACT;AAGE;AAGW,EAAA;AACgC,IAAA;AAC9B,IAAA;AACD,IAAA;AACgC,IAAA;AAET,EAAA;AAGN,EAAA;AACL,EAAA;AACY,EAAA;AACK,IAAA;AACzC,EAAA;AACO,EAAA;AACT;ARsY4C;AACA;ASrgBjB;AAES;AACpC;AACE;AACA;AACA;AACA;AACA;AACK;AAEwC;AAQuB;AACZ,EAAA;AAClC,EAAA;AACxB;AAEuB;AACjB,EAAA;AACG,EAAA;AACM,EAAA;AACuB,EAAA;AAC1B,EAAA;AACH,EAAA;AACI,EAAA;AACQ,EAAA;AACY,EAAA;AACnB,EAAA;AACW,IAAA;AACV,IAAA;AAEyB,oBAAA;AAC1B,IAAA;AACZ,EAAA;AACF;AAE4D;AACtD,EAAA;AACE,EAAA;AACuB,EAAA;AACU,EAAA;AACzC;AAEyB;AT0fmB;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA","file":"/home/runner/work/aidream/aidream/apps/shared/context-menu/dist/chunk-7VDSMZWI.cjs","sourcesContent":[null,"/**\n * THE TABLE ROW IS A DECLARED ITEM (ALC-18, LIST.md D4).\n *\n * A canonical table's row is alchemy's `table_row` item type (`TABLE_ROW_ITEM_TYPE`, a baseline\n * item type of every surface — `features/surfaces/manifests/registry.ts`). Each mounted table\n * registers ONE item source with `@ai-matrx/alchemy/surface` `itemSources` under its table id; a\n * right-click resolves the row through that one path into the declared item (identity\n * `table_id` + `row_id`, the level, its values) plus the row's menu, which rides as the item's\n * opaque host data. There is no private registry here: this file only builds the row's menu and\n * reads the words the row shows.\n */\nimport { compact } from \"./utils/compact\";\nimport { CONTEXT_MENU_HEADING_KEY, type ContextMenuExtraSection, type ResolvedContextMenuContext } from \"./types\";\nimport { TABLE_ROW_ITEM } from \"@ai-matrx/alchemy/declare\";\nimport type { ResolvedItem } from \"@ai-matrx/alchemy/declare\";\nimport { itemSources } from \"@ai-matrx/alchemy/surface\";\nimport type { MatrxDataTableRecordControls } from \"@ai-matrx/design-system/data-table/types\";\nimport type {\n MatrxTableMenuLevel,\n MatrxTableMenuPayload,\n MatrxTableMenuTarget,\n} from \"@ai-matrx/design-system/data-table/menu-targets\";\n\nexport interface TableRowMenuDescriptor {\n context: ResolvedContextMenuContext;\n extraSections: ContextMenuExtraSection[];\n}\n\n/**\n * The row's menu as the table hands it back (the table types it `unknown` and never reads it). A\n * class, so a resolver answering anything else is told apart without a private lookup table.\n */\nclass TableRowMenu {\n constructor(readonly descriptor: TableRowMenuDescriptor) {}\n}\n\nexport function createTableRowMenuDescriptor(descriptor: TableRowMenuDescriptor): object {\n return new TableRowMenu(descriptor);\n}\n\n/** Exactly the row commands the default menu renders. Nothing else is read. */\nexport type TableRowEditCommands = Pick<\n MatrxDataTableRecordControls,\n \"beginEdit\" | \"saveEdits\" | \"cancelEdits\"\n>;\n\nfunction isCommand(value: unknown): value is () => void {\n return typeof value === \"function\";\n}\n\n/**\n * The table hands its host the row's controls as `unknown` — it never\n * interprets what the host builds from them — so they are READ here, one\n * command at a time. A command the table did not send, or sent as something\n * that cannot be called, is simply not offered rather than rendered dead.\n */\nexport function tableRowEditCommands(controls: unknown): TableRowEditCommands {\n if (typeof controls !== \"object\" || controls === null) return {};\n const read = (key: string): (() => void) | undefined => {\n const value: unknown = Reflect.get(controls, key);\n return isCommand(value) ? value : undefined;\n };\n return compact({\n beginEdit: read(\"beginEdit\"),\n saveEdits: read(\"saveEdits\"),\n cancelEdits: read(\"cancelEdits\"),\n });\n}\n\n/**\n * The host's generic menu model: complete current row data plus the\n * table-owned edit commands.\n *\n * The table asks this for whichever of its five right-click LEVELS was aimed\n * at. Only the row has a generic menu here, so every other level answers\n * `null` and the table leaves that level to the surface that owns it.\n */\nexport function createDefaultTableRowMenuDescriptor(\n payload: MatrxTableMenuPayload,\n): object | null {\n if (payload.level !== \"row\") return null;\n return createTableRowMenuDescriptor(\n buildDefaultTableRowMenuDescriptor(\n payload.row,\n tableRowEditCommands(payload.controls),\n ),\n );\n}\n\n/** Reusable base for a domain menu that keeps the shared row edit controls. */\nexport function buildDefaultTableRowMenuDescriptor(\n row: unknown,\n controls: TableRowEditCommands,\n): TableRowMenuDescriptor {\n const items = [\n ...(controls.beginEdit\n ? [{ kind: \"item\" as const, id: \"table-edit-row\", label: \"Edit row\", onSelect: controls.beginEdit }]\n : []),\n ...(controls.saveEdits\n ? [{ kind: \"item\" as const, id: \"table-save-row\", label: \"Save changes\", onSelect: () => void controls.saveEdits?.() }]\n : []),\n ...(controls.cancelEdits\n ? [{ kind: \"item\" as const, id: \"table-discard-row\", label: \"Discard changes\", onSelect: controls.cancelEdits }]\n : []),\n ];\n return {\n context: {\n content: JSON.stringify(row, null, 2) ?? String(row),\n context: row,\n __entity: null,\n },\n extraSections: items.length === 0\n ? []\n : [{ id: \"table-row\", label: \"Row\", primary: true, anchor: \"after-clipboard\", items }],\n };\n}\n\n/** The mounted row element of a table, read when the item's values are read. */\nfunction rowElement(tableId: string, rowId: string): HTMLElement | null {\n if (typeof document === \"undefined\") return null;\n const table = Array.from(document.querySelectorAll<HTMLElement>(\"[data-matrx-table-id]\")).find((el) => el.dataset.matrxTableId === tableId);\n return Array.from(table?.querySelectorAll<HTMLElement>(\"[data-row-id]\") ?? []).find((el) => el.dataset.rowId === rowId) ?? null;\n}\n\n/** A row is writable when its table offers the row's edit command. */\nfunction rowIsReadOnly(descriptor: TableRowMenuDescriptor): boolean {\n return !descriptor.extraSections.some((section) => section.items.some((item) => \"id\" in item && item.id === \"table-edit-row\"));\n}\n\n/**\n * The canonical table registers ONE item source per mounted instance, under its table id. The table\n * answers which of its right-click LEVELS was aimed at (`MatrxTableMenuTarget`); the source turns\n * that answer into the declared `table_row` item, with the row's menu as its host data.\n */\nexport function registerTableRowContextResolver(\n tableId: string,\n resolve: (target: MatrxTableMenuTarget) => unknown,\n) {\n return itemSources.register<TableRowMenuDescriptor>(tableId, (target) => {\n const token = resolve({\n tableId,\n level: target.level as MatrxTableMenuLevel,\n ...(target.itemId ? { rowId: target.itemId } : {}),\n ...(target.partId ? { columnId: target.partId } : {}),\n });\n if (!(token instanceof TableRowMenu)) return null;\n const descriptor = token.descriptor;\n const rowId = target.itemId ?? \"\";\n return {\n item: {\n itemType: TABLE_ROW_ITEM,\n identity: { table_id: tableId, row_id: rowId },\n level: target.level,\n readOnly: rowIsReadOnly(descriptor),\n readValues: async () => ({\n table_id: tableId,\n row_id: rowId,\n shown: shownWords(rowElement(tableId, rowId)),\n row: descriptor.context.context,\n }),\n },\n host: descriptor,\n };\n });\n}\n\n/** The clicked row as its declared item (raw-free) and the menu its table built for it. */\nexport interface TableRowItemHit {\n item: ResolvedItem;\n menu: TableRowMenuDescriptor;\n}\n\nexport function resolveTableRowItem(target: HTMLElement | null): TableRowItemHit | null {\n const row = target?.closest<HTMLElement>(\"[data-row-id]\");\n const tableId = row?.closest<HTMLElement>(\"[data-matrx-table-id]\")?.dataset.matrxTableId;\n const rowId = row?.dataset.rowId;\n if (!tableId || !rowId) return null;\n // This menu acts on the ROW it found in the DOM, so it asks for that level by name.\n const hit = itemSources.resolve<TableRowMenuDescriptor>({ containerId: tableId, level: \"row\", itemId: rowId });\n const descriptor = hit?.host ?? null;\n if (!hit || !descriptor) return null;\n const { readValues: _raw, ...item } = hit.item;\n return { item, menu: headedMenu(descriptor, target, row) };\n}\n\n/** The row's menu, or null when the click was not on a canonical table's row. */\nexport function resolveTableRowMenuDescriptor(target: HTMLElement | null): TableRowMenuDescriptor | null {\n return resolveTableRowItem(target)?.menu ?? null;\n}\n\nfunction headedMenu(\n descriptor: TableRowMenuDescriptor,\n target: HTMLElement | null,\n row: HTMLElement,\n): TableRowMenuDescriptor {\n // 🚨 THE HEADING IS WHAT THE PERSON RIGHT-CLICKED, IN THE WORDS ON SCREEN (merged-grid review\n // 2026-09-26): the heading read `Content: { \"id\": …, \"_choices\": …, \"level\": \"admin\",\n // \"hidden\": {} }` — the row's raw document. The clicked cell's shown text (\"Content: Priya\n // Nair\") is the heading; the row's words when the click was between cells. The full row stays\n // in `context.context` for the actions that read it.\n const cell = target?.closest<HTMLElement>(\"td, [role='gridcell']\");\n const shown = shownWords(cell ?? row);\n // THE HEADER NAMES THE ROW'S RECORD (admin pass 2026-09-27: \"Content: partial\" — the clicked\n // status cell). The descriptor's own `__heading` wins (a list row names \"Quiz: …\"); otherwise\n // the row's record name — its first data cell's primary line. What the actions act on\n // (`content`) stays the clicked cell's words.\n const ownHeading = (descriptor.context as Record<string, unknown>)[CONTEXT_MENU_HEADING_KEY];\n const recordName = ownHeading ? \"\" : rowRecordName(row);\n const context = {\n ...descriptor.context,\n ...(shown ? { content: shown } : {}),\n ...(recordName ? { [CONTEXT_MENU_HEADING_KEY]: { label: \"Row\", text: recordName } } : {}),\n };\n // THE ROW IS NAMED ONCE (admin final judge 2026-09-27: \"Row: Basic Editor\" then a \"Basic\n // Editor\" section heading right under it). A primary section labelled with the same name the\n // header already shows is headed \"Row\" instead — its rows stay first (an unnamed section\n // would lose its place; the package never draws a coined or empty heading).\n const headed = (ownHeading as { text?: unknown } | undefined)?.text ?? recordName;\n const same = (label: string | undefined) =>\n typeof headed === \"string\" && !!label && label.trim().toLowerCase() === headed.trim().toLowerCase();\n const extraSections = descriptor.extraSections.map((section) =>\n section.primary && same(section.label) ? { ...section, label: \"Row\" } : section,\n );\n const changedSections = extraSections.some((section, i) => section !== descriptor.extraSections[i]);\n return shown || recordName || changedSections ? { ...descriptor, context, extraSections } : descriptor;\n}\n\n/** The row's record name: the first data cell (not a tick box, star or button cell) with words. */\nexport function rowRecordName(row: HTMLElement | null | undefined): string {\n if (!row) return \"\";\n for (const cell of Array.from(row.querySelectorAll<HTMLElement>(\"td[data-matrx-table-column-id], [role='gridcell'][data-matrx-table-column-id]\"))) {\n const words = primaryLine(cell);\n if (words) return words.length > 120 ? `${words.slice(0, 119)}…` : words;\n }\n return \"\";\n}\n\n/** Elements that start a new LINE of what a cell shows (name, then its subtitle). */\nconst LINE_BREAKING = \"div, p, li, br, tr, h1, h2, h3, h4, h5, h6, section, article, header, footer, dd, dt\";\n\n/**\n * The PRIMARY line an element shows — its first line, without its controls.\n * `textContent` glues a cell's stacked lines together with no space\n * (\"Data Destruction, Inc.Company re…\", \"Notesmatrx-user/notes\"; page-pass\n * 2026-09-27), so each line-starting element is marked with a newline first and\n * only the first non-empty line is kept. Inline markup inside a line\n * (<b>, <a>, <span>) is untouched, so words are never split.\n */\nfunction primaryLine(element: Element): string {\n const copy = element.cloneNode(true) as HTMLElement;\n copy.querySelectorAll(\"button, [aria-hidden='true'], input, textarea, select\").forEach((node) => node.remove());\n copy.querySelectorAll(LINE_BREAKING).forEach((node) => node.before(\"\\n\"));\n const lines = (copy.textContent ?? \"\")\n .split(\"\\n\")\n .map((line) => line.replace(/\\s+/g, \" \").trim())\n .filter((line) => line && line !== \"—\");\n return lines[0] ?? \"\";\n}\n\n/** The words an element shows: one cell's primary line, or each cell's, joined with \" · \". */\nfunction shownWords(element: HTMLElement | null | undefined): string {\n if (!element) return \"\";\n const cells = element.querySelectorAll(\"td, [role='gridcell']\");\n const text = cells.length > 1\n ? Array.from(cells).map(primaryLine).filter(Boolean).join(\" · \")\n : primaryLine(element);\n return text.length > 200 ? `${text.slice(0, 199)}…` : text;\n}\n","/**\n * A RECORD'S ROWS, IN THE ONE MENU OF THE CONTENT THAT SHOWS IT (R26; ALC-15 round 5).\n *\n * A record's toolbar can live apart from the content it belongs to: on /notes the tab strip holds the\n * note's ⋯ (Save, Duplicate, Move, Delete, Close tab…) while the note's content is drawn in the pane\n * below. The ⋯ and a right-click on that content target the SAME thing — the note — so they must be\n * one menu (measured 2026-09-27: 27 rows from the tab's ⋯, 25 from the content, each missing the\n * other's rows).\n *\n * · The owner of the rows registers them under a key: `registerRecordMenu(key, get)`.\n * · The host marks the content's root with `data-record-menu=\"<key>\"`.\n * · Every menu opened inside that root carries the registered sections (and the record's entity,\n * when the menu has none of its own) — the shell reads them at open (ContextMenuV3).\n * · The record's ⋯ calls `openRecordMenu(key, button)`: it opens the content's OWN menu at the\n * button. `false` = that content is not on screen; the caller opens its own menu instead.\n *\n * Plain data, no React.\n */\nimport type { ContextMenuEntityRef, ContextMenuExtraSection } from \"./types\";\n\nexport const RECORD_MENU_ATTR = \"data-record-menu\";\n\nexport interface RecordMenuRows {\n entity?: ContextMenuEntityRef | null | undefined;\n extraSections: ContextMenuExtraSection[];\n /**\n * The record's own name for the menu header (\"Note · Clinic intake\n * checklist\") — used when the clicked content names nothing itself, so the\n * header names the record instead of quoting its body. A selection still wins.\n */\n heading?: { label: string; text: string } | null;\n}\n\nconst registry = new Map<string, () => RecordMenuRows>();\n\n/**\n * A record's rows are pulled at open, but its NAME can change while a menu is\n * open or mounted (the note auto-labels from its first words 3 s after typing;\n * a person renames it). The owner calls `recordMenuChanged()` when what it\n * registers changes, and an open menu re-reads the heading (G6B review,\n * 2026-10-02: \"Note: New Note\" after the note was renamed).\n */\nlet revision = 0;\nconst listeners = new Set<() => void>();\n\n/** The owner of registered rows says they changed (its record was renamed). */\nexport function recordMenuChanged(): void {\n revision += 1;\n for (const listener of listeners) listener();\n}\n\n/** For `useSyncExternalStore`: re-render when any registered record changes. */\nexport function subscribeRecordMenus(listener: () => void): () => void {\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n}\n\nexport function recordMenusRevision(): number {\n return revision;\n}\n\n/** Register the rows a record contributes; returns the unregister. Read at every open, so always current. */\nexport function registerRecordMenu(key: string, get: () => RecordMenuRows): () => void {\n registry.set(key, get);\n return () => {\n if (registry.get(key) === get) registry.delete(key);\n };\n}\n\n/** The rows of the record whose content holds `target` (nearest marked root), or null. */\nexport function resolveRecordMenu(target: Element | null): RecordMenuRows | null {\n const key = target?.closest?.(`[${RECORD_MENU_ATTR}]`)?.getAttribute(RECORD_MENU_ATTR);\n if (!key) return null;\n return registry.get(key)?.() ?? null;\n}\n\nconst TRIGGER = '[data-alchemy-trigger=\"context\"]';\n\n/**\n * The right-click trigger a click on the record's content lands in: the first menu trigger inside\n * the marked root that carries content (`data-content-source`), else the root's own trigger.\n */\nfunction contentTrigger(root: HTMLElement): HTMLElement | null {\n const inner = root.querySelector<HTMLElement>(`${TRIGGER}[data-content-source]`);\n if (inner) return inner;\n if (root.matches(TRIGGER)) return root;\n return root.querySelector<HTMLElement>(TRIGGER) ?? root.closest<HTMLElement>(TRIGGER);\n}\n\n/**\n * Open the record content's own menu at `anchor` (a ⋯ button). Returns false when no content for\n * `key` is on screen, so the caller can open its own menu instead.\n */\nexport function openRecordMenu(key: string, anchor: HTMLElement): boolean {\n const doc = anchor.ownerDocument;\n const root = [...doc.querySelectorAll<HTMLElement>(`[${RECORD_MENU_ATTR}]`)].find(\n (el) => el.getAttribute(RECORD_MENU_ATTR) === key,\n );\n const trigger = root ? contentTrigger(root) : null;\n if (!trigger) return false;\n const rect = anchor.getBoundingClientRect();\n trigger.dispatchEvent(\n new MouseEvent(\"contextmenu\", {\n bubbles: true,\n cancelable: true,\n view: doc.defaultView ?? window,\n button: 2,\n buttons: 2,\n clientX: rect.left + rect.width / 2,\n clientY: rect.bottom,\n }),\n );\n return true;\n}\n","// features/context-menu-v3/utils/per-row-entity.ts\n//\n// THE PER-ROW ENTITY — the delegated-menu half of the entity contract.\n//\n// A table wires ONE menu for the whole pane (`resolveContextOnOpen`), so the\n// menu-level `entity` prop can never name the row that was actually\n// right-clicked: before 2026-08-24 Attach To / Share on a table menu silently\n// targeted the pane's entity, or rendered nothing at all. `resolveContextOnOpen`\n// may now return the reserved `CONTEXT_MENU_ENTITY_KEY` and the shell rebuilds\n// the entity-bound actions from it.\n//\n// Kept in its own module (not `value-resolution.ts`) on purpose: the INERT\n// SHELL imports this, and value-resolution pulls the surface-manifest registry.\n// Nothing here may import anything heavier than types.\n\nimport {\n CONTEXT_MENU_ENTITY_KEY,\n type ContextMenuEntityRef,\n type ResolvedContextMenuContext,\n} from \"../types\";\n\nconst isDev = process.env.NODE_ENV !== \"production\";\n\n/**\n * The effective entity for ONE menu open.\n *\n * key absent → the menu-level prop stands (single-entity surfaces unchanged)\n * key present → that row's entity wins\n * key `null` → this target has no entity, so the entity-bound actions hide\n * rather than target the wrong record\n *\n * A malformed value (no string `type` + `id`) is treated as absent and SCREAMS\n * in dev — a half-built entity would render an Attach that writes a broken edge.\n */\nexport function resolveEffectiveEntity(\n entityProp: ContextMenuEntityRef | undefined,\n resolved: ResolvedContextMenuContext | null,\n): ContextMenuEntityRef | undefined {\n if (!resolved || !(CONTEXT_MENU_ENTITY_KEY in resolved)) return entityProp;\n const perRow = resolved[CONTEXT_MENU_ENTITY_KEY];\n if (perRow === null || perRow === undefined) return undefined;\n const candidate = perRow as ContextMenuEntityRef;\n if (\n typeof perRow !== \"object\" ||\n typeof candidate.type !== \"string\" ||\n !candidate.type ||\n typeof candidate.id !== \"string\" ||\n !candidate.id\n ) {\n if (isDev) {\n console.error(\n `%c[ContextMenuV3] MALFORMED PER-ROW ENTITY%c — resolveContextOnOpen returned \"${CONTEXT_MENU_ENTITY_KEY}\" without a string type + id, so Attach To / Share would target nothing. Falling back to the menu-level entity.`,\n \"color:#ef4444;font-weight:bold\",\n \"color:inherit\",\n { received: perRow },\n );\n }\n return entityProp;\n }\n return candidate;\n}\n\n/**\n * The effective `contextData` for one open: static payload + per-target merge,\n * with the reserved entity key stripped so it never lands in the\n * `ApplicationScope` as a value.\n */\nexport function mergeResolvedContextData(\n contextData: Record<string, unknown> | undefined,\n resolved: ResolvedContextMenuContext | null,\n): Record<string, unknown> {\n const merged: Record<string, unknown> = {\n ...(contextData ?? {}),\n ...(resolved ?? {}),\n };\n delete merged[CONTEXT_MENU_ENTITY_KEY];\n return merged;\n}\n\n// ---------------------------------------------------------------------------\n// A TABLE ROW HAS TWO OWNERS — the table and the surface that mounted it\n// ---------------------------------------------------------------------------\n\n/**\n * The context for ONE open on a table row: the table's own row descriptor\n * (table-row-item.ts) JOINED with the surface's `resolveContextOnOpen` answer.\n *\n * 🚨 WHY (2026-10-03, CHAIR-REACH). The shell used to read `rowMenu?.context ?? surface(target)`:\n * once a canonical table registered its default row descriptor — which every MatrxDataTable does,\n * with `__entity: null` — the surface's resolver was never called on a row. So a list that names\n * its row's record there (CRM `useCrmRowMenu`, the keyword tables) lost Attach To, Share and\n * \"Link a record…\" on every row, and its own row doors with them (they are built from the state\n * that resolver sets).\n *\n * · The row descriptor still owns the row's VALUES (content, heading, the full row): on a key\n * both name, the descriptor stands (the 2026-09-26/27 heading rulings are unchanged).\n * · The ENTITY is the surface's to name when the descriptor names none: the table knows a row,\n * only the surface knows which record that row is. A descriptor that names its own entity\n * keeps it.\n * · Anything only the surface says is kept.\n */\nexport function joinRowAndSurfaceContext(\n row: ResolvedContextMenuContext | null | undefined,\n surface: ResolvedContextMenuContext | null | undefined,\n): ResolvedContextMenuContext | null {\n if (!row) return surface ?? null;\n if (!surface) return row;\n const rowNamesEntity = row[CONTEXT_MENU_ENTITY_KEY] !== null && row[CONTEXT_MENU_ENTITY_KEY] !== undefined;\n return {\n ...surface,\n ...row,\n ...(!rowNamesEntity && CONTEXT_MENU_ENTITY_KEY in surface\n ? { [CONTEXT_MENU_ENTITY_KEY]: surface[CONTEXT_MENU_ENTITY_KEY] }\n : {}),\n };\n}\n\n// ---------------------------------------------------------------------------\n// THE DOM SNIFFER — per-row identity with no resolver code\n// ---------------------------------------------------------------------------\n\n/**\n * A token SHAPE check, deliberately not a registry lookup.\n *\n * `isEntityTypeToken` would be the precise guard, but it lives in the 2,500-line\n * generated entity module and this file loads in the INERT SHELL (see the header\n * law). A shape check catches the realistic failure — a typo, a label, a raw\n * uuid in the wrong attribute — at zero weight; a well-formed but unregistered\n * token is caught downstream by Attach To's own write path.\n */\nconst TOKEN_SHAPE = /^[a-z][a-z0-9_]*$/;\n\n/**\n * Read the right-clicked row's entity straight off the DOM.\n *\n * 🚨 WHY THIS EXISTS (Phase 0, 2026-08-25). `resolveContextOnOpen` is the\n * precise path and remains it — but it made per-row identity cost a\n * hand-written resolver on EVERY table. With ~500 surfaces still to wire that\n * is ~500 bespoke `rowFor(target)` functions, each one a place to return the\n * wrong row. A row that already knows what it is can now simply SAY so:\n *\n * <tr data-entity-type=\"seo_keyword\" data-entity-id={k.id} data-entity-title={k.phrase}>\n *\n * and Attach To / Share target that record with no resolver at all.\n *\n * PRECEDENCE — THE SURFACE ALWAYS WINS. This runs ONLY when\n * `resolveContextOnOpen` did not speak about the entity at all (key absent).\n * An explicit `null` from the surface means \"this target has no entity\" and is\n * honoured; the DOM never overrides a deliberate answer.\n */\nexport function sniffEntityFromDom(\n target: HTMLElement | null,\n): ContextMenuEntityRef | null {\n const host = target?.closest?.(\"[data-entity-type][data-entity-id]\");\n if (!(host instanceof HTMLElement)) return null;\n\n const type = host.getAttribute(\"data-entity-type\")?.trim() ?? \"\";\n const id = host.getAttribute(\"data-entity-id\")?.trim() ?? \"\";\n if (!type || !id) return null;\n\n if (!TOKEN_SHAPE.test(type)) {\n if (isDev) {\n console.error(\n `%c[ContextMenuV3] MALFORMED data-entity-type%c — \"${type}\" is not an entity token (expected snake_case, e.g. \"seo_keyword\"), so Attach To / Share would target nothing. Fix the attribute or drop it.`,\n \"color:#ef4444;font-weight:bold\",\n \"color:inherit\",\n { element: host },\n );\n }\n return null;\n }\n\n // Title is what the user sees the row called; the row's own text is the\n // honest fallback so an Attach is never labelled with a raw uuid.\n const title =\n host.getAttribute(\"data-entity-title\")?.trim() ||\n (host.textContent ?? \"\").trim().slice(0, 120) ||\n id;\n\n const resourceType = host.getAttribute(\"data-entity-resource\")?.trim();\n\n return {\n type: type as ContextMenuEntityRef[\"type\"],\n id,\n title,\n ...(resourceType\n ? { resourceType: resourceType as NonNullable<ContextMenuEntityRef[\"resourceType\"]> }\n : {}),\n };\n}\n\n/**\n * The effective entity for one open, DOM sniff included. Surface answer wins;\n * the sniff only fills a silence.\n */\nexport function resolveEffectiveEntityWithDom(\n entityProp: ContextMenuEntityRef | undefined,\n resolved: ResolvedContextMenuContext | null,\n target: HTMLElement | null,\n): ContextMenuEntityRef | undefined {\n const surfaceSpoke = !!resolved && CONTEXT_MENU_ENTITY_KEY in resolved;\n if (surfaceSpoke) return resolveEffectiveEntity(entityProp, resolved);\n return sniffEntityFromDom(target) ?? entityProp;\n}\n","/**\n * ONE MENU, SEVERAL SOURCES OF SURFACE ROWS — joined so every row id is unique.\n *\n * A menu's surface sections can come from more than one owner for one open: a table row's\n * descriptor plus the grid's per-target sections, or the content's own sections plus the RECORD\n * it belongs to (record-menu-registry.ts — a note's tab rows join the menu opened on the note's\n * content). Each owner keeps its own ids unique, but not across owners, and the menu model\n * namespaces every surface row as `x:<id>` (menu-model.ts) — so the same id from two owners\n * reached the one Alchemy registry twice and it refused the second:\n * `DuplicateActionError: Action \"cm:x:save\" is yielded twice by provider \"context-menu:…\"`\n * (/notes, 2026-09-30: the editor's \"Save\" and the tab's \"Save\", both saving the same note).\n *\n * Sources are passed in precedence order. Two owners describing one target with the same id\n * mean the same action on it, so the FIRST source's row stands and a later source's row with\n * that id is not drawn a second time. A duplicate inside ONE source is that source's own defect\n * and is left untouched, so the registry still refuses it loudly.\n */\nimport type { ContextMenuExtraItem, ContextMenuExtraSection } from \"../types\";\n\nfunction isRow(item: ContextMenuExtraItem): boolean {\n return item.kind !== \"separator\";\n}\n\n/** Drop leading, trailing and doubled separators left behind when rows are removed. */\nfunction tidySeparators(items: ContextMenuExtraItem[]): ContextMenuExtraItem[] {\n const out: ContextMenuExtraItem[] = [];\n for (const item of items) {\n if (!isRow(item) && (out.length === 0 || !isRow(out[out.length - 1]!))) continue;\n out.push(item);\n }\n while (out.length > 0 && !isRow(out[out.length - 1]!)) out.pop();\n return out;\n}\n\nexport function joinExtraSections(\n ...sources: Array<ContextMenuExtraSection[] | null | undefined>\n): ContextMenuExtraSection[] | undefined {\n const present = sources.filter((s): s is ContextMenuExtraSection[] => Array.isArray(s));\n if (present.length === 0) return undefined;\n if (present.length === 1) return present[0];\n const taken = new Set<string>();\n const out: ContextMenuExtraSection[] = [];\n for (const source of present) {\n const ownIds = new Set<string>();\n for (const section of source) {\n const kept = section.items.filter((item) => !isRow(item) || !taken.has(item.id));\n for (const item of kept) if (isRow(item)) ownIds.add(item.id);\n if (kept.length === section.items.length) {\n out.push(section);\n continue;\n }\n const items = tidySeparators(kept);\n if (items.some(isRow)) out.push({ ...section, items });\n }\n for (const id of ownIds) taken.add(id);\n }\n return out;\n}\n","// features/context-menu-v3/utils/widget-handle.ts\n//\n// Inline agent editing — the v1 promise, paid via the platform channel.\n//\n// Builds a `WidgetHandle` (the canonical client-tool contract — see\n// `features/agents/types/widget-handle.types.ts` and CLIENT_SIDE_TOOLS.md)\n// from the SAME props an editable surface already passes to\n// `EditableContextMenu` (`onTextReplace` / `onTextInsertBefore|After` /\n// `getTextarea`). The shell registers it via `useOptionalWidgetHandle` and the\n// launch handlers pass the id as `runtime.widgetHandleId`, so any agent\n// launched from the menu on an editable surface can stream `widget_text_*`\n// tool calls that edit the surface's content in place — zero extra wiring per\n// surface, no bespoke edit protocol (the old XML-in-stream idea stays dead).\n//\n// Precedence: a surface callback (controlled React state) always wins; the\n// fallback writes through `setFieldValue` (native setter + input event), which\n// works on BOTH controlled and uncontrolled textareas. Only methods this\n// surface can actually service are present — the per-turn assembler\n// (`deriveClientToolsFromHandle`) advertises exactly that subset.\n\nimport type { WidgetHandle } from \"@ai-matrx/chat/agents/types/widget-handle.types\";\nimport type { ApplicationScope } from \"@ai-matrx/chat/agents/types/scope.types\";\n\n/**\n * Write a value into a textarea so BOTH controlled and uncontrolled fields\n * take it: the native value setter bypasses React's value tracker, and the\n * bubbled `input` event makes a controlled component's onChange fire (state\n * updates instead of React reverting the DOM on the next render). A bare\n * `element.value = x` write silently loses the edit on controlled fields —\n * while the tool result still reports ok:true to the agent.\n */\nfunction setFieldValue(\n el: HTMLTextAreaElement,\n next: string,\n caret: number,\n): void {\n const setter = Object.getOwnPropertyDescriptor(\n HTMLTextAreaElement.prototype,\n \"value\",\n )?.set;\n if (setter) setter.call(el, next);\n else el.value = next;\n el.dispatchEvent(new Event(\"input\", { bubbles: true }));\n try {\n el.setSelectionRange(caret, caret);\n } catch {\n // non-focusable / detached — caret is best-effort\n }\n}\n\nexport interface BuildEditableWidgetHandleArgs {\n getTextarea?: (() => HTMLTextAreaElement | null) | undefined;\n onTextReplace?: ((newText: string) => void) | undefined;\n onTextInsertBefore?: ((text: string) => void) | undefined;\n onTextInsertAfter?: ((text: string) => void) | undefined;\n /** Live scope builder — read for `content` when there is no textarea. */\n getApplicationScope?: (() => ApplicationScope) | undefined;\n}\n\n/**\n * Build the widget handle for an editable surface, or `null` when the surface\n * exposes no way to write (nothing to register, no tools advertised).\n */\nexport function buildEditableWidgetHandle(\n args: BuildEditableWidgetHandleArgs,\n): WidgetHandle | null {\n const {\n getTextarea,\n onTextReplace,\n onTextInsertBefore,\n onTextInsertAfter,\n getApplicationScope,\n } = args;\n\n const field = (): HTMLTextAreaElement | null => getTextarea?.() ?? null;\n\n const readCurrent = (): string | null => {\n const el = field();\n if (el) return el.value;\n const content = getApplicationScope?.().content;\n return typeof content === \"string\" ? content : null;\n };\n\n // Full-content write. Surface callback wins; the field fallback goes\n // through setFieldValue so controlled AND uncontrolled textareas take it.\n const writeFull = (next: string): void => {\n if (onTextReplace) {\n onTextReplace(next);\n return;\n }\n const el = field();\n if (!el) throw new Error(\"Surface has no writable target\");\n setFieldValue(el, next, next.length);\n };\n\n // Capability PRESENCE only — this function runs in the shell's render body,\n // so it must never invoke a surface callback (getTextarea reads the DOM,\n // getApplicationScope may serialize the whole document; per-keystroke cost\n // + a React render-purity violation). The method bodies re-check liveness\n // (readCurrent() null checks) at call time, which is the only time it matters.\n const canWrite = Boolean(onTextReplace) || Boolean(getTextarea);\n const canRead = Boolean(getTextarea) || Boolean(getApplicationScope);\n if (!canWrite) return null;\n\n // No `applyPolicy`: an agent's write into the person's own text is `ask` —\n // the approval card shows `readText()` as the \"before\".\n const handle: WidgetHandle = {\n onTextReplace: ({ text }) => writeFull(text),\n ...(canRead ? { readText: readCurrent } : {}),\n };\n\n // Insert relative to the cursor/selection. Surface callbacks carry their\n // own semantics; the fallback splices at the field's live cursor.\n if (onTextInsertBefore || getTextarea) {\n handle.onTextInsertBefore = ({ text }) => {\n if (onTextInsertBefore) return onTextInsertBefore(text);\n const el = field();\n if (!el) throw new Error(\"Surface has no writable target\");\n const at = el.selectionStart ?? 0;\n setFieldValue(\n el,\n el.value.slice(0, at) + text + el.value.slice(at),\n at + text.length,\n );\n };\n }\n if (onTextInsertAfter || getTextarea) {\n handle.onTextInsertAfter = ({ text }) => {\n if (onTextInsertAfter) return onTextInsertAfter(text);\n const el = field();\n if (!el) throw new Error(\"Surface has no writable target\");\n const at = el.selectionEnd ?? el.value.length;\n setFieldValue(\n el,\n el.value.slice(0, at) + text + el.value.slice(at),\n at + text.length,\n );\n };\n }\n\n // Whole-content operations need read + write.\n if (canRead) {\n handle.onTextPrepend = ({ text }) => {\n const current = readCurrent();\n if (current === null) throw new Error(\"Surface content is unreadable\");\n writeFull(text + current);\n };\n handle.onTextAppend = ({ text }) => {\n const current = readCurrent();\n if (current === null) throw new Error(\"Surface content is unreadable\");\n writeFull(current + text);\n };\n handle.onTextPatch = ({ search_text, replacement_text }) => {\n const current = readCurrent();\n if (current === null) throw new Error(\"Surface content is unreadable\");\n const idx = current.indexOf(search_text);\n if (idx === -1)\n // access-errors: ok — in-memory text-patch match against the surface's own content, no record read\n throw new Error(\"search_text not found in surface content\");\n writeFull(\n current.slice(0, idx) +\n replacement_text +\n current.slice(idx + search_text.length),\n );\n };\n }\n\n return handle;\n}\n","// features/context-menu-v3/utils/table-at-target.ts\n//\n// THE TABLE A RIGHT-CLICK LANDED ON, with its shape kept (header + rows). A chat answer's menu is\n// one menu over the whole answer, so \"Save to a table\" read the answer's text and opened on its\n// FIRST shape — a bullet list above the table won, and the table the person right-clicked was\n// never offered first. The shell hands the rich-document registry this reader (callbacks\n// `tableAtTarget`); the one shape reader (`shapeTextOfNode`, records-ui) turns the clicked\n// <table> back into table text, streamed or finished — it reads the rendered DOM.\n\n// The shape reader is imported only when a right-click lands on a <table> (the shell calls\n// `preloadTableShape` from its open path), never with the page: a menu is opened on ~1 in 149\n// page loads and most opens are not on a table.\ntype ShapeReader = (node: Node) => string;\nlet shapeReader: ShapeReader | null = null;\nlet shapeLoad: Promise<unknown> | null = null;\n\nexport function preloadTableShape(): Promise<unknown> {\n shapeLoad ??= import(\"@ai-matrx/records-ui/table-shape\")\n .then((m) => {\n shapeReader = m.shapeTextOfNode as ShapeReader;\n })\n .catch((error: unknown) => {\n shapeLoad = null;\n console.error(\"[ContextMenuV3] could not load the table shape reader\", error);\n });\n return shapeLoad;\n}\n\n/** Sync; null until the reader has loaded (a click on a table starts the load at open). */\nexport function tableTextAtTarget(target: Element | null): string | null {\n const table = target?.closest(\"table\");\n if (!table || !shapeReader) return null;\n const text = shapeReader(table.cloneNode(true));\n return text.trim() ? text : null;\n}\n","/**\n * The attributes ContextMenuV3 stamps onto every right-click REGION it wraps\n * (the chat composer textarea, every sidebar chat row, messages, table rows).\n *\n * ONE source: `ContextMenuV3.tsx` spreads this onto its trigger, and the real-\n * browser layout gate (`features/shell/layout-gate/shipped-css-region-triggers.spec.ts`)\n * renders its fixtures with the same object — so a stylesheet the app ships\n * that styles these attributes (the 2026-09-26 `[data-alchemy-trigger] {\n * width: 2rem }` collapse) is measured against exactly what production emits.\n *\n * Plain data, no React: the Playwright spec imports it directly.\n */\nexport const CONTEXT_REGION_TRIGGER_ATTRS = {\n \"data-alchemy-trigger\": \"context\",\n} as const;\n","/**\n * features/context-menu-v3/item/itemMenuToV3.ts\n *\n * ItemMenuConfig → v3 `extraSections` converter — the bridge that lets\n * `ItemContextMenu` render the ONE universal context menu instead of its own\n * Radix tree. Command/toggle execution reuses `run-entry.ts` (toast.promise\n * parity with the kebab dropdown), checkbox/link map to the dedicated v3 item\n * kinds, and section boundaries/labels carry over. Deliberate deltas from the\n * old bespoke render (documented in FEATURE.md): no in-menu single-key\n * shortcut execution (hints still display) and the header renders as a\n * leading section label instead of a Radix label block.\n */\n\nimport { compact } from \"../utils/compact\";\nimport type {\n ContextMenuExtraItem,\n ContextMenuExtraSection,\n} from \"../types\";\nimport {\n isCheckbox,\n isLink,\n isSubmenu,\n runCommand as runItemCommand,\n runToggle,\n type ItemMenuCommand,\n type ItemMenuConfig,\n type ItemMenuEntry,\n type ItemMenuSection,\n} from \"@ai-matrx/design-system/item\";\nimport { toast } from \"@ai-matrx/chat/host/notify\";\n\n// design-system's item icon is any component taking a className; the v3 menu's icon slot takes the same.\nconst iconOf = (entry: ItemMenuEntry) => entry.icon;\n\nconst toastPromise = (promise: Promise<unknown>, messages: { loading: string; success: string; error: (e: unknown) => string }) => {\n toast.promise(promise, messages);\n};\n\nfunction runCommand(entry: ItemMenuCommand) {\n runItemCommand(entry, toastPromise);\n}\n\nexport { toastPromise as itemToastPromise };\n\nfunction entryToExtraItem(entry: ItemMenuEntry): ContextMenuExtraItem {\n const description =\n entry.disabled && entry.disabledReason\n ? entry.disabledReason\n : entry.description;\n\n if (isSubmenu(entry)) {\n return compact({\n kind: \"submenu\" as const,\n id: entry.id,\n label: entry.label,\n icon: iconOf(entry),\n disabled: entry.disabled,\n children: submenuChildren(entry.sections),\n }) as ContextMenuExtraItem;\n }\n if (isCheckbox(entry)) {\n return compact({\n kind: \"checkbox\" as const,\n id: entry.id,\n label: entry.label,\n icon: iconOf(entry),\n description,\n checked: entry.checked,\n onCheckedChange: (next: boolean) => runToggle(entry, next),\n disabled: entry.disabled,\n hint: entry.shortcut ?? entry.shortcutKey?.toUpperCase(),\n }) as ContextMenuExtraItem;\n }\n if (isLink(entry)) {\n return compact({\n kind: \"link\" as const,\n id: entry.id,\n label: entry.label,\n icon: iconOf(entry),\n description,\n href: entry.href,\n target: entry.target,\n disabled: entry.disabled,\n }) as ContextMenuExtraItem;\n }\n // command\n return compact({\n kind: \"item\" as const,\n id: entry.id,\n label: entry.label,\n icon: iconOf(entry),\n description,\n destructive: entry.tone === \"destructive\",\n disabled: entry.disabled,\n hint: entry.shortcut ?? entry.shortcutKey?.toUpperCase(),\n onSelect: () => runCommand(entry),\n }) as ContextMenuExtraItem;\n}\n\n/** Flatten submenu sections into children with separators at boundaries. */\nfunction submenuChildren(sections: ItemMenuSection[]): ContextMenuExtraItem[] {\n const visible = sections\n .map((s) => ({ ...s, items: s.items.filter((e) => !e.hidden) }))\n .filter((s) => s.items.length > 0);\n const out: ContextMenuExtraItem[] = [];\n visible.forEach((section, i) => {\n if (i > 0)\n out.push({\n kind: \"separator\",\n id: `sep-${section.id ?? section.label ?? i}`,\n });\n out.push(...section.items.map(entryToExtraItem));\n });\n return out;\n}\n\nexport function itemMenuConfigToExtraSections(\n config: ItemMenuConfig,\n): ContextMenuExtraSection[] {\n const sections: ContextMenuExtraSection[] = config.sections\n .map((s, i) => compact({\n id: String(s.id ?? s.label ?? `im-${i}`),\n label: s.label,\n anchor: \"after-clipboard\" as const,\n items: s.items.filter((e) => !e.hidden).map(entryToExtraItem),\n }))\n .filter((s) => s.items.length > 0);\n\n // Header → leading label on the first section (or its own labeled block).\n const title = config.header?.title;\n const first = sections[0];\n if (title && first && !first.label) {\n sections[0] = { ...first, label: title };\n }\n return sections;\n}\n","// features/context-menu-v3/selection-provider.ts\n//\n// The context menu's one entry in the ONE selection toolbar\n// (components/selection-toolbar): \"Ask AI\" — it opens the same Alchemy menu a\n// right-click opens, over the selected text (the AI actions, the agent\n// libraries, copy / export / save), on desktop and as the sheet on a phone.\n// It replaces the old floating selection icon: the menu has one door on a\n// selection, and it sits in the toolbar with every other passage action.\n\nimport { AGENT_ICON } from \"@ai-matrx/icons/domain\";\nimport type { Action, ActionProvider, ClickTarget } from \"@ai-matrx/alchemy/actions\";\nimport { registerAlchemyIcon } from \"@ai-matrx/rich-content/utils/alchemy-icon-keys\";\nimport {\n declareSelectionProvider,\n placeSelectionActions,\n hostHalf,\n selectionToolbarHostOf,\n shownInSelectionMode,\n} from \"@ai-matrx/rich-content/selection-toolbar/selection-actions\";\n\nexport const CONTEXT_MENU_SELECTION_HOST_KEY = \"contextMenuSelection\";\n\nexport interface ContextMenuSelectionHost {\n kind: \"context-menu-selection\";\n /** Open this menu instance over the current selection. */\n open(): void;\n}\n\nfunction menuOf(target: ClickTarget): ContextMenuSelectionHost | null {\n const half = hostHalf<ContextMenuSelectionHost>(target, CONTEXT_MENU_SELECTION_HOST_KEY);\n return half?.kind === \"context-menu-selection\" ? half : null;\n}\n\nconst ASK_AI: Action = {\n id: \"selection:ai\",\n label: \"Ask AI\",\n description: \"AI actions, agents, copy and export for the selected text\",\n icon: registerAlchemyIcon(AGENT_ICON),\n category: \"ai\",\n order: 0,\n placement: \"primary\",\n preserveSelection: true,\n eligible: (t) => (menuOf(t) && shownInSelectionMode(ASK_AI.id, t) ? { status: \"available\" } : { status: \"absent\" }),\n run: (t) => {\n const menu = menuOf(t);\n if (!menu) return;\n // The toolbar steps aside; the selection stays for the menu to act on.\n selectionToolbarHostOf(t)?.ui.close();\n menu.open();\n },\n};\n\nexport const contextMenuSelectionProvider: ActionProvider = {\n id: \"context-menu-selection\",\n tier: \"T0\",\n declaredIds: () => [ASK_AI.id],\n actions: (target) => (menuOf(target) ? placeSelectionActions([ASK_AI], target) : []),\n};\n\ndeclareSelectionProvider(contextMenuSelectionProvider);\n"]}
|
package/dist/chunk-CUODNUAD.cjs
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports, "__esModule", {value: true});"use client";
|
|
2
|
-
var __defProp = Object.defineProperty;
|
|
3
|
-
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
-
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
-
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
-
var __export = (target, all) => {
|
|
7
|
-
for (var name in all)
|
|
8
|
-
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
-
};
|
|
10
|
-
var __copyProps = (to, from, except, desc) => {
|
|
11
|
-
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
-
for (let key of __getOwnPropNames(from))
|
|
13
|
-
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
-
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
-
}
|
|
16
|
-
return to;
|
|
17
|
-
};
|
|
18
|
-
var __reExport = (target, mod, secondTarget) => (__copyProps(target, mod, "default"), secondTarget && __copyProps(secondTarget, mod, "default"));
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
exports.__export = __export; exports.__reExport = __reExport;
|
|
24
|
-
//# sourceMappingURL=chunk-CUODNUAD.cjs.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["/home/runner/work/aidream/aidream/apps/shared/context-menu/dist/chunk-CUODNUAD.cjs"],"names":[],"mappings":"AAAA,qFAAY;AACZ,IAAI,UAAU,EAAE,MAAM,CAAC,cAAc;AACrC,IAAI,iBAAiB,EAAE,MAAM,CAAC,wBAAwB;AACtD,IAAI,kBAAkB,EAAE,MAAM,CAAC,mBAAmB;AAClD,IAAI,aAAa,EAAE,MAAM,CAAC,SAAS,CAAC,cAAc;AAClD,IAAI,SAAS,EAAE,CAAC,MAAM,EAAE,GAAG,EAAE,GAAG;AAChC,EAAE,IAAI,CAAC,IAAI,KAAK,GAAG,GAAG;AACtB,IAAI,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,EAAE,GAAG,EAAE,GAAG,CAAC,IAAI,CAAC,EAAE,UAAU,EAAE,KAAK,CAAC,CAAC;AACjE,CAAC;AACD,IAAI,YAAY,EAAE,CAAC,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG;AAC9C,EAAE,GAAG,CAAC,KAAK,GAAG,OAAO,KAAK,IAAI,SAAS,GAAG,OAAO,KAAK,IAAI,UAAU,EAAE;AACtE,IAAI,IAAI,CAAC,IAAI,IAAI,GAAG,iBAAiB,CAAC,IAAI,CAAC;AAC3C,MAAM,GAAG,CAAC,CAAC,YAAY,CAAC,IAAI,CAAC,EAAE,EAAE,GAAG,EAAE,GAAG,IAAI,IAAI,MAAM;AACvD,QAAQ,SAAS,CAAC,EAAE,EAAE,GAAG,EAAE,EAAE,GAAG,EAAE,CAAC,EAAE,GAAG,IAAI,CAAC,GAAG,CAAC,EAAE,UAAU,EAAE,CAAC,CAAC,KAAK,EAAE,gBAAgB,CAAC,IAAI,EAAE,GAAG,CAAC,EAAE,GAAG,IAAI,CAAC,WAAW,CAAC,CAAC;AAC1H,EAAE;AACF,EAAE,OAAO,EAAE;AACX,CAAC;AACD,IAAI,WAAW,EAAE,CAAC,MAAM,EAAE,GAAG,EAAE,YAAY,EAAE,GAAG,CAAC,WAAW,CAAC,MAAM,EAAE,GAAG,EAAE,SAAS,CAAC,EAAE,aAAa,GAAG,WAAW,CAAC,YAAY,EAAE,GAAG,EAAE,SAAS,CAAC,CAAC;AAChJ;AACA;AACE;AACA;AACF,6DAAC","file":"/home/runner/work/aidream/aidream/apps/shared/context-menu/dist/chunk-CUODNUAD.cjs"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/designed/MenuPanel.tsx"],"sourcesContent":["// src/designed/MenuPanel.tsx — @ai-matrx/context-menu: THE ONE MENU RENDERER.\n//\n// Draws a resolved menu (ResolvedNode[]) — the builder's preview and the live right-click menu are\n// both this component, so what the designer sees is what opens. It owns the menu's own behaviour:\n// submenus on hover with an intent delay and a safe triangle toward the open flyout, flyouts that\n// flip at the screen's edge, the keyboard (arrows, Enter, Esc / Left closes a submenu, Right opens\n// one, typeahead), the search box, and the status marks (partial / unmatched / needs-arman).\n// This file IS the menu component: its rows are the menu's own rows (hence the ui-exception marks).\n\nimport {\n createContext,\n useCallback,\n useContext,\n useEffect,\n useLayoutEffect,\n useRef,\n useState,\n type CSSProperties,\n type KeyboardEvent as ReactKeyboardEvent,\n type PointerEvent as ReactPointerEvent,\n type ReactNode,\n} from \"react\";\nimport type { MenuLook, MenuTone } from \"../design/model\";\nimport type { IconRef, ResolvedItem, ResolvedNode, ResolvedSearch, ResolvedSection, ResolvedSubmenu } from \"./types\";\nimport type { ResolvedHit } from \"./bindings\";\n\nexport interface MenuPanelEnv {\n look: MenuLook;\n /** Draws a lucide icon by name (the design's icons). */\n renderIcon: (name: string, className?: string) => ReactNode;\n /** A row was chosen (live: run it and close; builder: the designer's pick). */\n onActivate: (item: ResolvedItem) => void;\n /** Builder: a click on any entry selects its design node. */\n onSelectNode?: ((designId: string) => void) | undefined;\n highlightId?: string | null | undefined;\n /** absolute: flyouts sit beside their row (the builder) · fixed: measured against the screen (live). */\n flyout: \"absolute\" | \"fixed\";\n query: string;\n setQuery: (q: string) => void;\n /** The search, over the bound entries. */\n search: (q: string) => ResolvedHit[];\n /** Esc at the top: close the whole menu. */\n onEscape?: (() => void) | undefined;\n}\n\nconst Env = createContext<MenuPanelEnv | null>(null);\nfunction useEnv(): MenuPanelEnv {\n const env = useContext(Env);\n if (!env) throw new Error(\"MenuPanel: rendered outside its menu\");\n return env;\n}\n\n// ── rows: focus, keyboard ────────────────────────────────────────────────────\n\nconst ROW = \"[data-cm-row]\";\n\n/** The rows of THIS panel (not those of a flyout inside it), in order. */\nfunction rowsOf(panel: HTMLElement): HTMLElement[] {\n return [...panel.querySelectorAll<HTMLElement>(ROW)].filter((el) => el.closest('[role=\"menu\"]') === panel && el.getAttribute(\"aria-disabled\") !== \"true\");\n}\n\nfunction labelOf(el: HTMLElement): string {\n return (el.getAttribute(\"data-cm-label\") ?? el.textContent ?? \"\").trim().toLowerCase();\n}\n\nfunction reasonOf(n: ResolvedItem | ResolvedSubmenu): string | undefined {\n const parts = [n.statusReason, n.disabled ? n.disabledReason : undefined].filter(Boolean);\n return parts.length ? parts.join(\" — \") : undefined;\n}\n\nfunction toneAttrs(tone: MenuTone | undefined, toneLabel: boolean | undefined) {\n return tone && tone !== \"default\" ? { \"data-tone\": tone, ...(toneLabel ? { \"data-tone-label\": \"\" } : {}) } : {};\n}\n\nfunction Icon({ icon, fallback }: { icon?: IconRef | undefined; fallback?: string }) {\n const env = useEnv();\n if (icon?.component) {\n const C = icon.component;\n return <C className={`cm-icon ${icon.className ?? \"\"}`} {...(icon.style ? { style: icon.style } : {})} />;\n }\n const name = icon?.name ?? fallback;\n return name ? <>{env.renderIcon(name, \"cm-icon\")}</> : null;\n}\n\n// ── submenu intent: open on hover after a beat, never steal a flyout the pointer is heading to ──\n\ninterface Intent {\n openId: string | null;\n viaKey: boolean;\n enter: (id: string | null, el: HTMLElement) => void;\n openNow: (id: string, viaKey: boolean) => void;\n close: () => void;\n track: (e: ReactPointerEvent) => void;\n}\n\nfunction useIntent(): Intent {\n const [state, setState] = useState<{ openId: string | null; viaKey: boolean }>({ openId: null, viaKey: false });\n const timer = useRef<ReturnType<typeof setTimeout> | null>(null);\n const trail = useRef<{ x: number; y: number }[]>([]);\n const flyoutRect = useRef<DOMRect | null>(null);\n const stateRef = useRef(state);\n stateRef.current = state;\n useEffect(() => () => void (timer.current && clearTimeout(timer.current)), []);\n\n const heading = (): boolean => {\n const r = flyoutRect.current;\n const [a, b] = trail.current.slice(-2);\n if (!r || !a || !b) return false;\n // Is the pointer inside the triangle from where it was to the flyout's near edge?\n const towardRight = r.left >= a.x;\n const ex = towardRight ? r.left : r.right;\n const p = b;\n const t1 = { x: ex, y: r.top - 8 };\n const t2 = { x: ex, y: r.bottom + 8 };\n const sign = (p1: { x: number; y: number }, p2: { x: number; y: number }, p3: { x: number; y: number }) =>\n (p1.x - p3.x) * (p2.y - p3.y) - (p2.x - p3.x) * (p1.y - p3.y);\n const d1 = sign(p, a, t1);\n const d2 = sign(p, t1, t2);\n const d3 = sign(p, t2, a);\n const neg = d1 < 0 || d2 < 0 || d3 < 0;\n const pos = d1 > 0 || d2 > 0 || d3 > 0;\n return !(neg && pos);\n };\n\n const schedule = (fn: () => void, ms: number) => {\n if (timer.current) clearTimeout(timer.current);\n timer.current = setTimeout(fn, ms);\n };\n\n return {\n openId: state.openId,\n viaKey: state.viaKey,\n track: (e) => {\n trail.current.push({ x: e.clientX, y: e.clientY });\n if (trail.current.length > 3) trail.current.shift();\n },\n enter: (id, el) => {\n const current = stateRef.current.openId;\n if (id === current) {\n if (timer.current) clearTimeout(timer.current);\n return;\n }\n const flyout = current ? el.closest('[role=\"menu\"]')?.querySelector<HTMLElement>(`[data-cm-flyout=\"${CSS.escape(current)}\"]`) : null;\n flyoutRect.current = flyout?.getBoundingClientRect() ?? null;\n const go = () => setState({ openId: id, viaKey: false });\n if (current && heading()) {\n // On the way to the open flyout: wait, then look again.\n schedule(() => {\n if (el.matches(\":hover\")) go();\n }, 300);\n } else schedule(go, current ? 60 : 90);\n },\n openNow: (id, viaKey) => {\n if (timer.current) clearTimeout(timer.current);\n setState({ openId: id, viaKey });\n },\n close: () => {\n if (timer.current) clearTimeout(timer.current);\n setState({ openId: null, viaKey: false });\n },\n };\n}\n\nconst PanelIntent = createContext<Intent | null>(null);\n\n// ── the panel ────────────────────────────────────────────────────────────────\n\nexport interface MenuPanelProps {\n nodes: readonly ResolvedNode[];\n env: MenuPanelEnv;\n className?: string;\n style?: CSSProperties;\n /** Props for the root element (position, data attributes). */\n rootProps?: Record<string, unknown>;\n /** The root element, for the owner's own measuring and outside-click test. */\n panelRef?: (el: HTMLDivElement | null) => void;\n}\n\n/** THE MENU: the root panel (with the search box), drawn from resolved nodes. */\nexport function MenuPanel({ nodes, env, className, style, rootProps, panelRef }: MenuPanelProps) {\n const q = env.query.trim().toLowerCase();\n const searchAt = nodes.findIndex((n) => n.kind === \"search\");\n const hits = q ? env.search(q) : null;\n const above = searchAt >= 0 ? nodes.slice(0, searchAt) : [];\n return (\n <Env.Provider value={env}>\n <Panel className={`cm-menu ${className ?? \"\"}`} style={{ width: env.look.width, ...style }} root rootProps={rootProps} panelRef={panelRef}>\n {nodes.length === 0 ? (\n <div className=\"cm-empty\">Empty menu — drag options in</div>\n ) : hits ? (\n <>\n <Nodes nodes={above} />\n {searchAt >= 0 && <SearchBox node={nodes[searchAt] as ResolvedSearch} />}\n {hits.length === 0 ? (\n <div className=\"cm-empty\">No matches</div>\n ) : (\n hits.map((h) =>\n h.node.kind === \"item\" ? <Item key={`hit:${h.node.id}`} node={h.node} path={h.path} /> : null,\n )\n )}\n </>\n ) : (\n <Nodes nodes={nodes} />\n )}\n </Panel>\n </Env.Provider>\n );\n}\n\nfunction Panel({\n children,\n className,\n style,\n root,\n rootProps,\n onClose,\n flyoutId,\n panelRef,\n}: {\n panelRef?: ((el: HTMLDivElement | null) => void) | undefined;\n children: ReactNode;\n className: string;\n style?: CSSProperties | undefined;\n root?: boolean;\n rootProps?: Record<string, unknown> | undefined;\n /** A flyout: Esc / Left closes it (the parent focuses its row again). */\n onClose?: (() => void) | undefined;\n flyoutId?: string | undefined;\n}) {\n const env = useEnv();\n const intent = useIntent();\n const ref = useRef<HTMLDivElement>(null);\n const typed = useRef({ text: \"\", at: 0 });\n\n const onKeyDown = (e: ReactKeyboardEvent<HTMLDivElement>) => {\n const panel = ref.current;\n if (!panel) return;\n // A key that started in a flyout inside this panel belongs to that flyout.\n if ((e.target as HTMLElement).closest('[role=\"menu\"]') !== panel) return;\n const inSearch = (e.target as HTMLElement).tagName === \"INPUT\";\n const rows = rowsOf(panel);\n const at = rows.indexOf(document.activeElement as HTMLElement);\n const focus = (i: number) => rows[(i + rows.length) % rows.length]?.focus();\n switch (e.key) {\n case \"ArrowDown\":\n e.preventDefault();\n focus(inSearch || at < 0 ? 0 : at + 1);\n return;\n case \"ArrowUp\":\n e.preventDefault();\n if (inSearch) return;\n if (at <= 0 && root) {\n const search = panel.querySelector<HTMLInputElement>(\".cm-search input\");\n if (search) return search.focus();\n }\n focus(at < 0 ? rows.length - 1 : at - 1);\n return;\n case \"Home\":\n case \"End\":\n if (inSearch) return;\n e.preventDefault();\n focus(e.key === \"Home\" ? 0 : rows.length - 1);\n return;\n case \"ArrowRight\": {\n if (inSearch) return;\n const row = document.activeElement as HTMLElement | null;\n const sub = row?.getAttribute(\"data-cm-sub\");\n if (sub) {\n e.preventDefault();\n intent.openNow(sub, true);\n }\n return;\n }\n case \"ArrowLeft\":\n if (inSearch || !onClose) return;\n e.preventDefault();\n e.stopPropagation();\n onClose();\n return;\n case \"Escape\":\n e.preventDefault();\n e.stopPropagation();\n if (inSearch && env.query) return env.setQuery(\"\");\n if (onClose) onClose();\n else env.onEscape?.();\n return;\n case \"Enter\":\n if (inSearch) {\n e.preventDefault();\n rows[0]?.click();\n }\n return;\n default:\n break;\n }\n // Typeahead: a printable key jumps to the next row starting with what was typed. At the top\n // of a menu with a search box, it types into the search instead.\n if (e.key.length !== 1 || e.metaKey || e.ctrlKey || e.altKey || inSearch) return;\n const search = root ? panel.querySelector<HTMLInputElement>(\".cm-search input\") : null;\n if (search) {\n e.preventDefault();\n search.focus();\n env.setQuery(env.query + e.key);\n return;\n }\n const now = Date.now();\n typed.current = { text: now - typed.current.at > 700 ? e.key.toLowerCase() : typed.current.text + e.key.toLowerCase(), at: now };\n const start = at < 0 ? 0 : typed.current.text.length > 1 ? at : at + 1;\n for (let i = 0; i < rows.length; i++) {\n const row = rows[(start + i) % rows.length];\n if (row && labelOf(row).startsWith(typed.current.text)) {\n e.preventDefault();\n row.focus();\n return;\n }\n }\n };\n\n return (\n <PanelIntent.Provider value={intent}>\n <div\n ref={(el) => {\n ref.current = el;\n panelRef?.(el);\n }}\n role=\"menu\"\n tabIndex={-1}\n data-density={env.look.density}\n {...(flyoutId ? { \"data-cm-flyout\": flyoutId } : {})}\n className={className}\n style={style}\n onKeyDown={onKeyDown}\n onPointerMove={intent.track}\n {...(rootProps ?? {})}\n >\n {children}\n </div>\n </PanelIntent.Provider>\n );\n}\n\nfunction Nodes({ nodes }: { nodes: readonly ResolvedNode[] }) {\n return (\n <>\n {nodes.map((n) => {\n if (n.kind === \"divider\") return <div key={n.id} role=\"separator\" className=\"cm-divider\" />;\n if (n.kind === \"item\") return <Item key={n.id} node={n} />;\n if (n.kind === \"submenu\") return <Submenu key={n.id} node={n} />;\n if (n.kind === \"search\") return <SearchBox key={n.id} node={n} />;\n if (n.kind === \"note\") return <div key={n.id} className={n.tone === \"runtime\" ? \"cm-runtime\" : \"cm-empty\"}>{n.text}</div>;\n return <Section key={n.id} node={n} />;\n })}\n </>\n );\n}\n\nfunction SearchBox({ node }: { node: ResolvedSearch }) {\n const env = useEnv();\n const ref = useRef<HTMLInputElement>(null);\n useEffect(() => {\n if (node.autofocus) ref.current?.focus({ preventScroll: true });\n }, [node.autofocus]);\n return (\n <div className={`cm-search ${env.highlightId === node.designId ? \"cm-hl\" : \"\"}`} onClick={() => node.designId && env.onSelectNode?.(node.designId)}>\n {env.renderIcon(\"Search\", \"cm-icon\")}\n {/* ui-exception: the menu's own filter row — this file IS the menu component */}\n <input ref={ref} value={env.query} onChange={(e) => env.setQuery(e.target.value)} placeholder={node.placeholder || \"Type to filter…\"} aria-label=\"Filter menu\" />\n </div>\n );\n}\n\nfunction Section({ node }: { node: ResolvedSection }) {\n const env = useEnv();\n const [open, setOpen] = useState(!node.collapsible);\n const layout = node.layout;\n return (\n <div className={`cm-section ${env.highlightId && env.highlightId === node.designId ? \"cm-hl\" : \"\"}`} data-layout={layout} {...(node.feature ? { \"data-feature\": \"\" } : {})}>\n {node.label && (\n // ui-exception: a menu row of the menu component itself\n <button\n type=\"button\"\n tabIndex={-1}\n className={`cm-section-label ${node.placeholder ? \"cm-slot-label\" : \"\"}`}\n onClick={() => {\n if (node.designId) env.onSelectNode?.(node.designId);\n if (node.collapsible) setOpen((o) => !o);\n }}\n >\n {node.label}\n {node.collapsible && <span className=\"cm-caret\">{open ? \"▾\" : \"▸\"}</span>}\n </button>\n )}\n {open && (\n <div className=\"cm-section-body\">\n {layout === \"list\" ? (\n <Nodes nodes={node.children} />\n ) : (\n node.children.map((c) => (c.kind === \"item\" ? <Tile key={c.id} node={c} /> : <Nodes key={c.id} nodes={[c]} />))\n )}\n </div>\n )}\n </div>\n );\n}\n\nfunction statusAttrs(n: ResolvedItem | ResolvedSubmenu) {\n const status = n.missingRequired ? \"required-missing\" : n.status && n.status !== \"works\" ? n.status : undefined;\n return status ? { \"data-status\": status } : {};\n}\n\nfunction Tile({ node }: { node: ResolvedItem }) {\n const env = useEnv();\n const reason = reasonOf(node);\n return (\n // ui-exception: a menu row of the menu component itself\n <button\n type=\"button\"\n role=\"menuitem\"\n tabIndex={-1}\n data-cm-row=\"\"\n data-cm-label={node.label}\n title={[node.label + (node.shortcut ? ` ${node.shortcut}` : \"\"), reason].filter(Boolean).join(\"\\n\")}\n aria-label={node.label}\n aria-disabled={node.disabled || undefined}\n {...toneAttrs(node.tone, node.toneLabel)}\n {...statusAttrs(node)}\n className={`cm-tile ${node.danger ? \"cm-danger\" : \"\"} ${node.ai ? \"cm-ai\" : \"\"} ${env.highlightId && env.highlightId === node.designId ? \"cm-hl\" : \"\"}`}\n onClick={() => {\n if (node.designId) env.onSelectNode?.(node.designId);\n if (!node.disabled) env.onActivate(node);\n }}\n >\n <Icon icon={node.icon} fallback=\"Circle\" />\n </button>\n );\n}\n\nfunction Item({ node, path }: { node: ResolvedItem; path?: string }) {\n const env = useEnv();\n const { look } = env;\n const [checked, setChecked] = useState(Boolean(node.checked));\n // A changed default shows at once (the designer flips \"On by default\" while looking at the row).\n useEffect(() => setChecked(Boolean(node.checked)), [node.checked]);\n const settable = node.toggle || node.switch;\n const hint = path || (look.showHints ? node.hint : undefined);\n const reason = reasonOf(node);\n const intent = useContext(PanelIntent);\n return (\n // ui-exception: a menu row of the menu component itself\n <button\n type=\"button\"\n role={settable ? \"menuitemcheckbox\" : \"menuitem\"}\n tabIndex={-1}\n data-cm-row=\"\"\n data-cm-label={node.label}\n aria-checked={settable ? checked : undefined}\n aria-disabled={node.disabled || undefined}\n {...(reason ? { title: reason, \"aria-description\": reason } : {})}\n {...toneAttrs(node.tone, node.toneLabel)}\n {...statusAttrs(node)}\n className={`cm-item ${node.placeholder ? \"cm-slot\" : \"\"} ${node.danger ? \"cm-danger\" : \"\"} ${node.ai ? \"cm-ai\" : \"\"} ${env.highlightId && env.highlightId === node.designId ? \"cm-hl\" : \"\"}`}\n onPointerEnter={(e) => intent?.enter(null, e.currentTarget)}\n onClick={() => {\n if (node.designId) env.onSelectNode?.(node.designId);\n if (node.disabled) return;\n if (settable) setChecked((c) => !c);\n env.onActivate(node);\n }}\n >\n {look.showIcons && (\n <span className=\"cm-icon-slot\">{node.toggle ? (checked ? env.renderIcon(\"Check\", \"cm-icon\") : null) : <Icon icon={node.icon} />}</span>\n )}\n <span className=\"cm-text\">\n <span className=\"cm-label\">{node.label}</span>\n {hint && <span className=\"cm-hint\">{hint}</span>}\n </span>\n {node.count && <span className=\"cm-count\">{node.count}</span>}\n {look.showShortcuts && node.shortcut && !node.switch && <kbd className=\"cm-shortcut\">{node.shortcut}</kbd>}\n {node.switch && <span className=\"cm-switch\" data-on={checked ? \"\" : undefined} aria-hidden=\"true\" />}\n </button>\n );\n}\n\ntype FlyoutSide = \"right\" | \"left\" | \"below\";\n\n/**\n * Builder: where a submenu opens beside its row — right when it fits, else left, else just under it.\n * \"Fits\" is measured against the nearest `[data-menu-bounds]` box (a panel that clips), else the window.\n */\nfunction flyoutSide(row: HTMLElement, width: number): FlyoutSide {\n const r = row.getBoundingClientRect();\n const bounds = row.closest<HTMLElement>(\"[data-menu-bounds]\")?.getBoundingClientRect() ?? { left: 0, right: window.innerWidth };\n if (r.right + width + 8 <= bounds.right) return \"right\";\n if (r.left - width - 8 >= bounds.left) return \"left\";\n return \"below\";\n}\n\n/** Live: the flyout's place on the screen — beside its row, flipped and clamped to stay on screen. */\nexport function placeFlyout(row: DOMRect, size: { width: number; height: number }, view: { width: number; height: number }) {\n const gap = 2;\n const margin = 8;\n let left = row.right + gap;\n let side: \"right\" | \"left\" = \"right\";\n if (left + size.width > view.width - margin) {\n const flipped = row.left - gap - size.width;\n if (flipped >= margin) {\n left = flipped;\n side = \"left\";\n } else left = Math.max(margin, view.width - margin - size.width);\n }\n let top = row.top - 5;\n if (top + size.height > view.height - margin) top = Math.max(margin, view.height - margin - size.height);\n return { left, top, side };\n}\n\nfunction Submenu({ node }: { node: ResolvedSubmenu }) {\n const env = useEnv();\n const intent = useContext(PanelIntent);\n const { look } = env;\n const open = intent?.openId === node.id;\n const [side, setSide] = useState<FlyoutSide>(\"right\");\n const [fixed, setFixed] = useState<{ left: number; top: number } | null>(null);\n const rowRef = useRef<HTMLButtonElement>(null);\n const flyRef = useRef<HTMLDivElement>(null);\n const reason = reasonOf(node);\n const greyed = node.disabled;\n\n useLayoutEffect(() => {\n if (!open || env.flyout !== \"fixed\" || !rowRef.current || !flyRef.current) return;\n const r = rowRef.current.getBoundingClientRect();\n const f = flyRef.current.getBoundingClientRect();\n const at = placeFlyout(r, { width: f.width || look.width, height: f.height }, { width: window.innerWidth, height: window.innerHeight });\n setFixed({ left: at.left, top: at.top });\n setSide(at.side);\n }, [open, env.flyout, look.width]);\n useEffect(() => {\n if (!open) setFixed(null);\n if (open && intent?.viaKey && flyRef.current) {\n const first = rowsOf(flyRef.current)[0];\n (first ?? flyRef.current).focus();\n }\n }, [open, intent?.viaKey]);\n\n const closeFlyout = useCallback(() => {\n intent?.close();\n rowRef.current?.focus();\n }, [intent]);\n\n const flyStyle: CSSProperties =\n env.flyout === \"fixed\"\n ? { position: \"fixed\", width: look.width, maxHeight: \"calc(100vh - 16px)\", overflowY: \"auto\", left: fixed?.left ?? -9999, top: fixed?.top ?? -9999, visibility: fixed ? \"visible\" : \"hidden\" }\n : { width: look.width };\n\n return (\n <div className=\"cm-sub\">\n {/* ui-exception: a menu row of the menu component itself */}\n <button\n ref={rowRef}\n type=\"button\"\n role=\"menuitem\"\n tabIndex={-1}\n data-cm-row=\"\"\n data-cm-label={node.label}\n {...(greyed ? {} : { \"data-cm-sub\": node.id })}\n aria-haspopup=\"menu\"\n aria-expanded={open}\n aria-disabled={greyed || undefined}\n {...(reason ? { title: reason, \"aria-description\": reason } : {})}\n {...toneAttrs(node.tone, node.toneLabel)}\n {...statusAttrs(node)}\n className={`cm-item ${node.placeholder ? \"cm-slot\" : \"\"} ${open ? \"cm-open\" : \"\"} ${env.highlightId && env.highlightId === node.designId ? \"cm-hl\" : \"\"}`}\n onPointerEnter={(e) => {\n if (env.flyout === \"absolute\") setSide(flyoutSide(e.currentTarget, look.width));\n if (greyed) intent?.enter(null, e.currentTarget);\n else intent?.enter(node.id, e.currentTarget);\n }}\n onClick={(e) => {\n if (node.designId) env.onSelectNode?.(node.designId);\n if (greyed) return;\n if (env.flyout === \"absolute\") setSide(flyoutSide(e.currentTarget, look.width));\n intent?.openNow(node.id, e.detail === 0);\n }}\n >\n {look.showIcons && <span className=\"cm-icon-slot\"><Icon icon={node.icon} /></span>}\n <span className=\"cm-text\">\n <span className=\"cm-label\">{node.label}</span>\n {look.showHints && node.hint && <span className=\"cm-hint\">{node.hint}</span>}\n </span>\n {node.loading && <span className=\"cm-count\">…</span>}\n {node.count && <span className=\"cm-count\">{node.count}</span>}\n {env.renderIcon(\"ChevronRight\", \"cm-icon cm-chev\")}\n </button>\n {open && (\n <Panel className=\"cm-menu cm-flyout\" flyoutId={node.id} style={flyStyle} onClose={closeFlyout}>\n <FlyoutBody node={node} flyRef={flyRef} side={env.flyout === \"absolute\" ? side : undefined} />\n </Panel>\n )}\n </div>\n );\n}\n\n/** The flyout's rows (kept apart so the panel element itself is what `flyRef` measures). */\nfunction FlyoutBody({ node, flyRef, side }: { node: ResolvedSubmenu; flyRef: React.RefObject<HTMLDivElement | null>; side: FlyoutSide | undefined }) {\n const ref = useRef<HTMLSpanElement>(null);\n useLayoutEffect(() => {\n const panel = ref.current?.closest<HTMLDivElement>('[role=\"menu\"]') ?? null;\n flyRef.current = panel;\n if (panel && side) panel.setAttribute(\"data-side\", side);\n });\n return (\n <>\n <span ref={ref} hidden />\n {node.loading && node.children.length === 0 ? (\n <div className=\"cm-empty\">Loading…</div>\n ) : node.children.length === 0 ? (\n <div className=\"cm-empty\">{node.emptyLabel ?? \"Empty submenu\"}</div>\n ) : (\n <Nodes nodes={node.children} />\n )}\n </>\n );\n}\n"],"mappings":"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","names":["search"]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/engine/utils/menu-deadline.ts","../src/engine/model/requirement-gate.ts","../src/engine/utils/already-here.ts","../src/engine/utils/json-menu-actions.ts","../src/engine/utils/field-menu-header.ts","../src/engine/managed-agent-launch.ts"],"sourcesContent":["export * from \"@ai-matrx/chat/context-menu/utils/menu-deadline\";\n","export * from \"@ai-matrx/chat/context-menu/model/requirement-gate\";\n","// features/context-menu-v3/utils/already-here.ts\n//\n// ACTIONS THAT TARGET THE THING YOU ARE ALREADY IN ARE ABSENT (page-pass\n// 2026-09-27, /notes/<id>): the menu inside a note offered \"Save to Notes\", and\n// the menu inside the editor offered \"Edit content\" and \"Open in full-screen\n// editor\". Each rich-document action id listed here is excluded for the menu\n// instance that is already that place.\n\nexport interface WhereTheMenuIs {\n /** The rich-document source type (`note`, `task`, `chat-message`, `raw`, …). */\n sourceType: string;\n /** The registry surface the menu belongs to. */\n surfaceName?: string | null | undefined;\n /** The menu wraps an editor (EditableContextMenu). */\n isEditable: boolean;\n}\n\n/** \"Save to X\" where the menu already IS an X. */\nconst SAVE_INTO_WHERE_YOU_ARE: ReadonlyArray<{ sourceType: string; surface: string; actionId: string }> = [\n { sourceType: \"note\", surface: \"matrx-user/notes\", actionId: \"save-to-notes\" },\n { sourceType: \"task\", surface: \"matrx-user/tasks\", actionId: \"save-to-task\" },\n];\n\n/** Opening a window from inside that same window (surface-only rules). */\nconst OPEN_WHERE_YOU_ARE: ReadonlyArray<{ surface: string; actionId: string }> = [\n // The Feedback window's menus offered \"Submit feedback\" — reopening itself.\n { surface: \"matrx-user/feedback\", actionId: \"submit-feedback\" },\n];\n\n/** Opening an editor from inside the editor. */\nconst EDITOR_DOORS = [\"edit\", \"open-fullscreen-editor\"] as const;\n\nexport function actionsAlreadyHere(where: WhereTheMenuIs): string[] {\n const out: string[] = [];\n for (const rule of SAVE_INTO_WHERE_YOU_ARE) {\n if (where.sourceType === rule.sourceType || where.surfaceName === rule.surface) out.push(rule.actionId);\n }\n for (const rule of OPEN_WHERE_YOU_ARE) {\n if (where.surfaceName === rule.surface) out.push(rule.actionId);\n }\n // On a record's OWN page (a note's source on the notes surface) the page is\n // its editor — its Write / Read toggle is the door — so the preview offers no\n // second one (live 2026-09-27, /notes/<id> in Read mode). Editor and preview\n // menus now carry the same set.\n // Matched by the SOURCE as well as the surface: live, the Read-mode menu on\n // /notes/<id> carried the note source under a surface name other than the\n // notes one, so a surface-only match missed it.\n const ownPage = SAVE_INTO_WHERE_YOU_ARE.some(\n (rule) => where.sourceType === rule.sourceType || where.surfaceName === rule.surface,\n );\n if (where.isEditable || ownPage) out.push(...EDITOR_DOORS);\n return [...new Set(out)];\n}\n","// features/context-menu-v3/utils/json-menu-actions.ts\n//\n// The JSON section of the universal context menu. Highlight something that\n// looks like JSON — fenced or bare — and the menu offers the things you\n// actually want for JSON: condense it, squeeze it onto one line, open it back\n// up, sort its keys, add or strip the code fence.\n//\n// Design rules:\n// - The menu ASKS the engine, it does not sniff. Detection and formatting\n// both come from `lib/json-format` (the shared primitive), so the notes\n// cleanup pass and this menu can never disagree about what \"condensed\"\n// means.\n// - Every action carries a REAL before/after hint (\"13 lines -> 3 lines\"),\n// computed from the actual formatted output. No action promises a change\n// it will not make: one that would be a no-op is dropped.\n// - On an EDITABLE surface the action rewrites the text in place (the exact\n// selection when there is one, the whole field otherwise). On a read-only\n// surface it copies the formatted text instead — the same intent, the only\n// verb available.\n// - Unparseable-but-JSON-shaped text still produces a section: one disabled\n// row carrying the parse error. Silently showing nothing on a selection\n// the user clearly believes is JSON is the \"fake menu\" failure this\n// codebase kills on sight.\n//\n// Pure except for the two side-effect callbacks handed in by the hook.\n\nimport { detectJson } from \"@ai-matrx/kit/json-format\";\nimport { formatJsonText } from \"@ai-matrx/kit/json-format\";\nimport type {\n JsonDetection,\n JsonFenceMode,\n JsonFormatStyle,\n} from \"@ai-matrx/kit/json-format\";\n\nexport interface JsonMenuAction {\n id: string;\n label: string;\n /** Real before -> after size, e.g. \"13 lines -> 3 lines\". */\n hint?: string | undefined;\n disabled?: boolean | undefined;\n run: () => void | Promise<void>;\n}\n\nexport interface JsonMenuSection {\n /** The detection that produced this section (null when the text isn't JSON-ish). */\n detection: JsonDetection;\n /** True when the surface will rewrite text; false when actions copy instead. */\n writes: boolean;\n actions: JsonMenuAction[];\n}\n\nexport interface BuildJsonMenuActionsParams {\n /** The text the menu is acting on (selection, else resolved content). */\n text: string;\n /** True when the action should REWRITE the text rather than copy it. */\n canWrite: boolean;\n /** Rewrite the acted-on text with `next`. Only called when `canWrite`. */\n onReplace: (next: string) => void;\n /** Copy `next` to the clipboard. Used on read-only surfaces. */\n onCopy: (next: string) => void | Promise<void>;\n}\n\nfunction lineCount(text: string): number {\n let n = text === \"\" ? 0 : 1;\n for (let i = 0; i < text.length; i++) if (text[i] === \"\\n\") n++;\n return n;\n}\n\nfunction sizeHint(before: string, after: string): string {\n const b = lineCount(before);\n const a = lineCount(after);\n if (b !== a) {\n return `${b} line${b !== 1 ? \"s\" : \"\"} -> ${a} line${a !== 1 ? \"s\" : \"\"}`;\n }\n return `${before.length} -> ${after.length} chars`;\n}\n\n/**\n * The style that preserves the user's rough layout — used by actions whose\n * point is NOT re-layout (sort keys, fence toggling). Re-printing a hand-\n * formatted blob as something else because the user asked to sort its keys is\n * a change they did not request.\n */\nfunction shapePreservingStyle(detection: JsonDetection): JsonFormatStyle {\n return detection.payload.includes(\"\\n\") ? \"pretty\" : \"minify\";\n}\n\n/**\n * Build the JSON section for the given text, or `null` when the text is not\n * JSON-shaped at all (the overwhelmingly common case — the section simply\n * does not render).\n */\nexport function buildJsonMenuSection(\n params: BuildJsonMenuActionsParams,\n): JsonMenuSection | null {\n const { text, canWrite, onReplace, onCopy } = params;\n if (!text.trim()) return null;\n\n const detection = detectJson(text);\n if (!detection.looksLikeJson) return null;\n\n // JSON-shaped but broken: one honest, disabled row.\n if (!detection.ok) {\n return {\n detection,\n writes: false,\n actions: [\n {\n id: \"json-invalid\",\n label: \"Invalid JSON\",\n hint: detection.error,\n disabled: true,\n run: () => {},\n },\n ],\n };\n }\n\n const apply = (next: string) => {\n if (canWrite) onReplace(next);\n else void onCopy(next);\n };\n\n const verb = canWrite ? \"\" : \"Copy \";\n const actions: JsonMenuAction[] = [];\n\n const addFormat = (\n id: string,\n label: string,\n style: JsonFormatStyle,\n fence: JsonFenceMode = \"preserve\",\n sortKeys = false,\n ) => {\n const result = formatJsonText(text, { style, fence, sortKeys });\n if (!result.ok) return;\n // Copying an identical string is still useful; rewriting it is not.\n if (canWrite && !result.changed) return;\n actions.push({\n id,\n label,\n hint: result.changed ? sizeHint(text, result.text) : undefined,\n run: () => apply(result.text),\n });\n };\n\n addFormat(\"json-condense\", `${verb}Condense`, \"compact\");\n addFormat(\"json-minify\", `${verb}Minify (one line)`, \"minify\");\n addFormat(\"json-expand\", `${verb}Expand`, \"pretty\");\n addFormat(\n \"json-sort-keys\",\n `${verb}Sort keys A-Z`,\n shapePreservingStyle(detection),\n \"preserve\",\n true,\n );\n\n // Fence toggle — only the direction that applies.\n if (detection.fence) {\n addFormat(\n \"json-strip-fence\",\n `${verb}Remove code fence`,\n shapePreservingStyle(detection),\n \"strip\",\n );\n } else {\n addFormat(\n \"json-add-fence\",\n `${verb}Wrap in json code fence`,\n shapePreservingStyle(detection),\n \"add\",\n );\n }\n\n // An editable surface still benefits from a plain \"give me the one-liner on\n // the clipboard\" that does NOT touch the document.\n if (canWrite) {\n const minified = formatJsonText(text, { style: \"minify\", fence: \"strip\" });\n if (minified.ok) {\n actions.push({\n id: \"json-copy-minified\",\n label: \"Copy minified\",\n hint: sizeHint(text, minified.text),\n run: () => void onCopy(minified.text),\n });\n }\n }\n\n if (actions.length === 0) return null;\n return { detection, writes: canWrite, actions };\n}\n\n/** Short label for the submenu trigger, e.g. \"JSON · 13 lines\". */\nexport function jsonSectionLabel(section: JsonMenuSection): string {\n const { detection } = section;\n if (!detection.ok) return \"JSON (invalid)\";\n const n = detection.lineCount;\n const kind = detection.root === \"array\" ? \"array\" : \"object\";\n return `JSON ${kind} · ${n} line${n !== 1 ? \"s\" : \"\"}`;\n}\n","/**\n * THE MENU OPENED IN A FIELD NAMES THE FIELD (page-pass 2026-09-27,\n * /chat/message-templates/<id>). Right-clicking the Body box headed the menu\n * \"Content: {{reply.body}}\" — the field's raw text. The header is now the\n * field's own name (\"Body\"), with a short plain preview only when it helps:\n * merge fields read as their names (\"Reply body\"), and text that is ONLY merge\n * fields gets no preview (the name already says it). Pure, so it is tested\n * without a DOM menu.\n */\n\n/** A field's name, the way a person reads it on screen. `null` when it has none. */\nexport function fieldLabelOf(element: Element | null | undefined): string | null {\n if (!element) return null;\n const clean = (s: string | null | undefined) => (s ?? \"\").replace(/\\s+/g, \" \").replace(/\\s*\\*$/, \"\").trim() || null;\n const aria = clean(element.getAttribute(\"aria-label\"));\n if (aria) return aria;\n const labelledBy = element.getAttribute(\"aria-labelledby\");\n if (labelledBy) {\n const text = labelledBy\n .split(/\\s+/)\n .map((id) => element.ownerDocument?.getElementById(id)?.textContent ?? \"\")\n .join(\" \");\n if (clean(text)) return clean(text);\n }\n const labels = (element as HTMLInputElement).labels;\n if (labels && labels.length > 0) {\n const text = clean(labels[0]?.textContent);\n if (text) return text;\n }\n const id = element.getAttribute(\"id\");\n if (id && element.ownerDocument) {\n const label = element.ownerDocument.querySelector(`label[for=\"${CSS.escape(id)}\"]`);\n if (clean(label?.textContent)) return clean(label?.textContent);\n }\n return null;\n}\n\nconst MERGE_FIELD = /\\{\\{\\s*([^{}]+?)\\s*\\}\\}/g;\n\n/** \"reply.body\" / \"reply_body\" → \"Reply body\". */\nexport function mergeFieldName(token: string): string {\n const words = token.replace(/[._-]+/g, \" \").replace(/([a-z])([A-Z])/g, \"$1 $2\").trim().toLowerCase();\n return words ? words[0]!.toUpperCase() + words.slice(1) : token;\n}\n\nexport const FIELD_PREVIEW_MAX = 60;\n\n/**\n * The preview beside the field's name: plain words with merge fields named,\n * clipped; `\"\"` when the text holds nothing but merge fields (or nothing).\n */\nexport function fieldPreview(text: string): string {\n const withoutFields = text.replace(MERGE_FIELD, \"\").replace(/[\\s\\p{P}]+/gu, \"\");\n if (!withoutFields) return \"\";\n const readable = text.replace(MERGE_FIELD, (_, name: string) => mergeFieldName(name)).replace(/\\s+/g, \" \").trim();\n return readable.length > FIELD_PREVIEW_MAX ? `${readable.slice(0, FIELD_PREVIEW_MAX - 1).trimEnd()}…` : readable;\n}\n","import type { AgentExecutionConfig } from \"@ai-matrx/chat/agents/types/agent-execution-config.types\";\n\n/**\n * Framework-owned launch defaults for agents listed in the managed \"Agents\"\n * context-menu section. Shortcuts do not use this config: their persisted\n * execution definitions remain authoritative.\n */\nexport const MANAGED_CONTEXT_MENU_AGENT_CONFIG = {\n displayMode: \"flexible-panel\",\n allowChat: true,\n showVariablePanel: true,\n} as const satisfies Pick<\n AgentExecutionConfig,\n \"displayMode\" | \"allowChat\" | \"showVariablePanel\"\n>;\n"],"mappings":"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","names":["id"]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/engine/types.ts","../src/engine/utils/selection-tracking.ts","../src/engine/utils/key-combo.ts","../src/engine/utils/compact.ts"],"sourcesContent":["import \"./host-types\";\nimport \"../designed/menu-design-prop\";\nexport * from \"@ai-matrx/chat/context-menu/types\";\n","export * from \"@ai-matrx/chat/context-menu/utils/selection-tracking\";\n","// features/context-menu-v3/utils/key-combo.ts\n//\n// THE ONE READER OF A SHORTCUT'S ADVERTISED KEY COMBO (\"Alt+Shift+S\").\n//\n// The menu prints `keyboard_shortcut` beside an item; the shell's key listener\n// runs it. Both read the combo HERE, so an advertised combo either runs or is\n// not shown (`parseKeyCombo` returns null → the menu draws no hint).\n//\n// MATCH ON `code`, NEVER `key`: on macOS Option rewrites the character —\n// Alt+Shift+S arrives as `key: \"Í\"`, `code: \"KeyS\"` (2026-10-02 → 10-05, the\n// advertised combo did nothing on /notes).\n\nexport interface KeyCombo {\n alt: boolean;\n shift: boolean;\n ctrl: boolean;\n meta: boolean;\n /** `KeyboardEvent.code` for letters/digits (\"KeyS\", \"Digit1\"), else null. */\n code: string | null;\n /** Lowercased `KeyboardEvent.key` for named keys (\"enter\", \"/\"), else null. */\n key: string | null;\n}\n\nconst MOD_ALIASES: Record<string, keyof Pick<KeyCombo, \"alt\" | \"shift\" | \"ctrl\" | \"meta\"> | \"mod\"> = {\n alt: \"alt\",\n option: \"alt\",\n opt: \"alt\",\n \"⌥\": \"alt\",\n shift: \"shift\",\n \"⇧\": \"shift\",\n ctrl: \"ctrl\",\n control: \"ctrl\",\n \"⌃\": \"ctrl\",\n cmd: \"meta\",\n command: \"meta\",\n meta: \"meta\",\n \"⌘\": \"meta\",\n win: \"meta\",\n mod: \"mod\",\n};\n\nfunction isApplePlatform(): boolean {\n if (typeof navigator === \"undefined\") return false;\n return /mac|iphone|ipad|ipod/i.test(navigator.platform || navigator.userAgent || \"\");\n}\n\n/**\n * Parse an advertised combo. Null when it cannot be a combo this listener can\n * honour: no non-Shift modifier (plain typing), or no key.\n */\nexport function parseKeyCombo(raw: string | null | undefined): KeyCombo | null {\n if (!raw) return null;\n const parts = raw\n .split(/\\s*\\+\\s*/)\n .map((p) => p.trim())\n .filter(Boolean);\n if (parts.length < 2) return null;\n const combo: KeyCombo = { alt: false, shift: false, ctrl: false, meta: false, code: null, key: null };\n const last = parts[parts.length - 1]!;\n for (const part of parts.slice(0, -1)) {\n const mod = MOD_ALIASES[part.toLowerCase()];\n if (!mod) return null;\n if (mod === \"mod\") combo[isApplePlatform() ? \"meta\" : \"ctrl\"] = true;\n else combo[mod] = true;\n }\n if (/^[a-z]$/i.test(last)) combo.code = `Key${last.toUpperCase()}`;\n else if (/^[0-9]$/.test(last)) combo.code = `Digit${last}`;\n else combo.key = last.toLowerCase();\n if (!combo.alt && !combo.ctrl && !combo.meta) return null;\n // ⌘/Ctrl+Shift+K opens the palette on every surface (ContextMenuV3); a\n // shortcut cannot take the platform's key, so it is not advertised either.\n if ((combo.ctrl || combo.meta) && combo.shift && !combo.alt && combo.code === \"KeyK\") return null;\n return combo;\n}\n\ntype KeyEventLike = Pick<KeyboardEvent, \"altKey\" | \"shiftKey\" | \"ctrlKey\" | \"metaKey\" | \"code\" | \"key\">;\n\nexport function eventMatchesCombo(e: KeyEventLike, combo: KeyCombo): boolean {\n if (!!e.altKey !== combo.alt || !!e.shiftKey !== combo.shift) return false;\n if (!!e.ctrlKey !== combo.ctrl || !!e.metaKey !== combo.meta) return false;\n if (combo.code) return e.code === combo.code;\n return !!combo.key && (e.key ?? \"\").toLowerCase() === combo.key;\n}\n\n/** First item whose advertised combo this key event presses. */\nexport function findComboMatch<T>(\n e: KeyEventLike,\n items: readonly T[],\n comboOf: (item: T) => string | null | undefined,\n): T | null {\n if (!e.altKey && !e.ctrlKey && !e.metaKey) return null;\n for (const item of items) {\n const combo = parseKeyCombo(comboOf(item));\n if (combo && eventMatchesCombo(e, combo)) return item;\n }\n return null;\n}\n","/**\n * Drops the keys whose value is `undefined`, so an object assembled from\n * maybe-present values satisfies a target whose optional properties do not\n * accept `undefined` (`exactOptionalPropertyTypes`). Reading any dropped key\n * still yields `undefined` — the only difference is `\"key\" in obj`.\n */\ntype Present<T> = { [K in keyof T as undefined extends T[K] ? never : K]: T[K] };\ntype Maybe<T> = { [K in keyof T as undefined extends T[K] ? K : never]?: Exclude<T[K], undefined> };\nexport type Compact<T> = Present<T> & Maybe<T>;\n\nexport function compact<T extends object>(value: T): Compact<T> {\n const out: Record<string, unknown> = {};\n for (const [key, v] of Object.entries(value)) if (v !== undefined) out[key] = v;\n return out as Compact<T>;\n}\n"],"mappings":";;;;;;AAAA;AAEA;AAAA,4BAAc;;;ACFd;AAAA;AAAA,yCAAc;;;ACuBd,IAAM,cAA+F;AAAA,EACnG,KAAK;AAAA,EACL,QAAQ;AAAA,EACR,KAAK;AAAA,EACL,UAAK;AAAA,EACL,OAAO;AAAA,EACP,UAAK;AAAA,EACL,MAAM;AAAA,EACN,SAAS;AAAA,EACT,UAAK;AAAA,EACL,KAAK;AAAA,EACL,SAAS;AAAA,EACT,MAAM;AAAA,EACN,UAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AACP;AAEA,SAAS,kBAA2B;AAClC,MAAI,OAAO,cAAc,YAAa,QAAO;AAC7C,SAAO,wBAAwB,KAAK,UAAU,YAAY,UAAU,aAAa,EAAE;AACrF;AAMO,SAAS,cAAc,KAAiD;AAC7E,MAAI,CAAC,IAAK,QAAO;AACjB,QAAM,QAAQ,IACX,MAAM,UAAU,EAChB,IAAI,CAAC,MAAM,EAAE,KAAK,CAAC,EACnB,OAAO,OAAO;AACjB,MAAI,MAAM,SAAS,EAAG,QAAO;AAC7B,QAAM,QAAkB,EAAE,KAAK,OAAO,OAAO,OAAO,MAAM,OAAO,MAAM,OAAO,MAAM,MAAM,KAAK,KAAK;AACpG,QAAM,OAAO,MAAM,MAAM,SAAS,CAAC;AACnC,aAAW,QAAQ,MAAM,MAAM,GAAG,EAAE,GAAG;AACrC,UAAM,MAAM,YAAY,KAAK,YAAY,CAAC;AAC1C,QAAI,CAAC,IAAK,QAAO;AACjB,QAAI,QAAQ,MAAO,OAAM,gBAAgB,IAAI,SAAS,MAAM,IAAI;AAAA,QAC3D,OAAM,GAAG,IAAI;AAAA,EACpB;AACA,MAAI,WAAW,KAAK,IAAI,EAAG,OAAM,OAAO,MAAM,KAAK,YAAY,CAAC;AAAA,WACvD,UAAU,KAAK,IAAI,EAAG,OAAM,OAAO,QAAQ,IAAI;AAAA,MACnD,OAAM,MAAM,KAAK,YAAY;AAClC,MAAI,CAAC,MAAM,OAAO,CAAC,MAAM,QAAQ,CAAC,MAAM,KAAM,QAAO;AAGrD,OAAK,MAAM,QAAQ,MAAM,SAAS,MAAM,SAAS,CAAC,MAAM,OAAO,MAAM,SAAS,OAAQ,QAAO;AAC7F,SAAO;AACT;AAIO,SAAS,kBAAkB,GAAiB,OAA0B;AAC3E,MAAI,CAAC,CAAC,EAAE,WAAW,MAAM,OAAO,CAAC,CAAC,EAAE,aAAa,MAAM,MAAO,QAAO;AACrE,MAAI,CAAC,CAAC,EAAE,YAAY,MAAM,QAAQ,CAAC,CAAC,EAAE,YAAY,MAAM,KAAM,QAAO;AACrE,MAAI,MAAM,KAAM,QAAO,EAAE,SAAS,MAAM;AACxC,SAAO,CAAC,CAAC,MAAM,QAAQ,EAAE,OAAO,IAAI,YAAY,MAAM,MAAM;AAC9D;AAGO,SAAS,eACd,GACA,OACA,SACU;AACV,MAAI,CAAC,EAAE,UAAU,CAAC,EAAE,WAAW,CAAC,EAAE,QAAS,QAAO;AAClD,aAAW,QAAQ,OAAO;AACxB,UAAM,QAAQ,cAAc,QAAQ,IAAI,CAAC;AACzC,QAAI,SAAS,kBAAkB,GAAG,KAAK,EAAG,QAAO;AAAA,EACnD;AACA,SAAO;AACT;;;ACtFO,SAAS,QAA0B,OAAsB;AAC9D,QAAM,MAA+B,CAAC;AACtC,aAAW,CAAC,KAAK,CAAC,KAAK,OAAO,QAAQ,KAAK,EAAG,KAAI,MAAM,OAAW,KAAI,GAAG,IAAI;AAC9E,SAAO;AACT;","names":[]}
|
package/dist/chunk-PE3ZHHM5.js
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
var __defProp = Object.defineProperty;
|
|
3
|
-
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
-
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
-
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
-
var __export = (target, all) => {
|
|
7
|
-
for (var name in all)
|
|
8
|
-
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
-
};
|
|
10
|
-
var __copyProps = (to, from, except, desc) => {
|
|
11
|
-
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
-
for (let key of __getOwnPropNames(from))
|
|
13
|
-
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
-
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
-
}
|
|
16
|
-
return to;
|
|
17
|
-
};
|
|
18
|
-
var __reExport = (target, mod, secondTarget) => (__copyProps(target, mod, "default"), secondTarget && __copyProps(secondTarget, mod, "default"));
|
|
19
|
-
|
|
20
|
-
export {
|
|
21
|
-
__export,
|
|
22
|
-
__reExport
|
|
23
|
-
};
|
|
24
|
-
//# sourceMappingURL=chunk-PE3ZHHM5.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["/home/runner/work/aidream/aidream/apps/shared/context-menu/dist/chunk-QXHLM7SS.cjs","../src/designed/MenuPanel.tsx"],"names":[],"mappings":"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","file":"/home/runner/work/aidream/aidream/apps/shared/context-menu/dist/chunk-QXHLM7SS.cjs","sourcesContent":[null,"// src/designed/MenuPanel.tsx — @ai-matrx/context-menu: THE ONE MENU RENDERER.\n//\n// Draws a resolved menu (ResolvedNode[]) — the builder's preview and the live right-click menu are\n// both this component, so what the designer sees is what opens. It owns the menu's own behaviour:\n// submenus on hover with an intent delay and a safe triangle toward the open flyout, flyouts that\n// flip at the screen's edge, the keyboard (arrows, Enter, Esc / Left closes a submenu, Right opens\n// one, typeahead), the search box, and the status marks (partial / unmatched / needs-arman).\n// This file IS the menu component: its rows are the menu's own rows (hence the ui-exception marks).\n\nimport {\n createContext,\n useCallback,\n useContext,\n useEffect,\n useLayoutEffect,\n useRef,\n useState,\n type CSSProperties,\n type KeyboardEvent as ReactKeyboardEvent,\n type PointerEvent as ReactPointerEvent,\n type ReactNode,\n} from \"react\";\nimport type { MenuLook, MenuTone } from \"../design/model\";\nimport type { IconRef, ResolvedItem, ResolvedNode, ResolvedSearch, ResolvedSection, ResolvedSubmenu } from \"./types\";\nimport type { ResolvedHit } from \"./bindings\";\n\nexport interface MenuPanelEnv {\n look: MenuLook;\n /** Draws a lucide icon by name (the design's icons). */\n renderIcon: (name: string, className?: string) => ReactNode;\n /** A row was chosen (live: run it and close; builder: the designer's pick). */\n onActivate: (item: ResolvedItem) => void;\n /** Builder: a click on any entry selects its design node. */\n onSelectNode?: ((designId: string) => void) | undefined;\n highlightId?: string | null | undefined;\n /** absolute: flyouts sit beside their row (the builder) · fixed: measured against the screen (live). */\n flyout: \"absolute\" | \"fixed\";\n query: string;\n setQuery: (q: string) => void;\n /** The search, over the bound entries. */\n search: (q: string) => ResolvedHit[];\n /** Esc at the top: close the whole menu. */\n onEscape?: (() => void) | undefined;\n}\n\nconst Env = createContext<MenuPanelEnv | null>(null);\nfunction useEnv(): MenuPanelEnv {\n const env = useContext(Env);\n if (!env) throw new Error(\"MenuPanel: rendered outside its menu\");\n return env;\n}\n\n// ── rows: focus, keyboard ────────────────────────────────────────────────────\n\nconst ROW = \"[data-cm-row]\";\n\n/** The rows of THIS panel (not those of a flyout inside it), in order. */\nfunction rowsOf(panel: HTMLElement): HTMLElement[] {\n return [...panel.querySelectorAll<HTMLElement>(ROW)].filter((el) => el.closest('[role=\"menu\"]') === panel && el.getAttribute(\"aria-disabled\") !== \"true\");\n}\n\nfunction labelOf(el: HTMLElement): string {\n return (el.getAttribute(\"data-cm-label\") ?? el.textContent ?? \"\").trim().toLowerCase();\n}\n\nfunction reasonOf(n: ResolvedItem | ResolvedSubmenu): string | undefined {\n const parts = [n.statusReason, n.disabled ? n.disabledReason : undefined].filter(Boolean);\n return parts.length ? parts.join(\" — \") : undefined;\n}\n\nfunction toneAttrs(tone: MenuTone | undefined, toneLabel: boolean | undefined) {\n return tone && tone !== \"default\" ? { \"data-tone\": tone, ...(toneLabel ? { \"data-tone-label\": \"\" } : {}) } : {};\n}\n\nfunction Icon({ icon, fallback }: { icon?: IconRef | undefined; fallback?: string }) {\n const env = useEnv();\n if (icon?.component) {\n const C = icon.component;\n return <C className={`cm-icon ${icon.className ?? \"\"}`} {...(icon.style ? { style: icon.style } : {})} />;\n }\n const name = icon?.name ?? fallback;\n return name ? <>{env.renderIcon(name, \"cm-icon\")}</> : null;\n}\n\n// ── submenu intent: open on hover after a beat, never steal a flyout the pointer is heading to ──\n\ninterface Intent {\n openId: string | null;\n viaKey: boolean;\n enter: (id: string | null, el: HTMLElement) => void;\n openNow: (id: string, viaKey: boolean) => void;\n close: () => void;\n track: (e: ReactPointerEvent) => void;\n}\n\nfunction useIntent(): Intent {\n const [state, setState] = useState<{ openId: string | null; viaKey: boolean }>({ openId: null, viaKey: false });\n const timer = useRef<ReturnType<typeof setTimeout> | null>(null);\n const trail = useRef<{ x: number; y: number }[]>([]);\n const flyoutRect = useRef<DOMRect | null>(null);\n const stateRef = useRef(state);\n stateRef.current = state;\n useEffect(() => () => void (timer.current && clearTimeout(timer.current)), []);\n\n const heading = (): boolean => {\n const r = flyoutRect.current;\n const [a, b] = trail.current.slice(-2);\n if (!r || !a || !b) return false;\n // Is the pointer inside the triangle from where it was to the flyout's near edge?\n const towardRight = r.left >= a.x;\n const ex = towardRight ? r.left : r.right;\n const p = b;\n const t1 = { x: ex, y: r.top - 8 };\n const t2 = { x: ex, y: r.bottom + 8 };\n const sign = (p1: { x: number; y: number }, p2: { x: number; y: number }, p3: { x: number; y: number }) =>\n (p1.x - p3.x) * (p2.y - p3.y) - (p2.x - p3.x) * (p1.y - p3.y);\n const d1 = sign(p, a, t1);\n const d2 = sign(p, t1, t2);\n const d3 = sign(p, t2, a);\n const neg = d1 < 0 || d2 < 0 || d3 < 0;\n const pos = d1 > 0 || d2 > 0 || d3 > 0;\n return !(neg && pos);\n };\n\n const schedule = (fn: () => void, ms: number) => {\n if (timer.current) clearTimeout(timer.current);\n timer.current = setTimeout(fn, ms);\n };\n\n return {\n openId: state.openId,\n viaKey: state.viaKey,\n track: (e) => {\n trail.current.push({ x: e.clientX, y: e.clientY });\n if (trail.current.length > 3) trail.current.shift();\n },\n enter: (id, el) => {\n const current = stateRef.current.openId;\n if (id === current) {\n if (timer.current) clearTimeout(timer.current);\n return;\n }\n const flyout = current ? el.closest('[role=\"menu\"]')?.querySelector<HTMLElement>(`[data-cm-flyout=\"${CSS.escape(current)}\"]`) : null;\n flyoutRect.current = flyout?.getBoundingClientRect() ?? null;\n const go = () => setState({ openId: id, viaKey: false });\n if (current && heading()) {\n // On the way to the open flyout: wait, then look again.\n schedule(() => {\n if (el.matches(\":hover\")) go();\n }, 300);\n } else schedule(go, current ? 60 : 90);\n },\n openNow: (id, viaKey) => {\n if (timer.current) clearTimeout(timer.current);\n setState({ openId: id, viaKey });\n },\n close: () => {\n if (timer.current) clearTimeout(timer.current);\n setState({ openId: null, viaKey: false });\n },\n };\n}\n\nconst PanelIntent = createContext<Intent | null>(null);\n\n// ── the panel ────────────────────────────────────────────────────────────────\n\nexport interface MenuPanelProps {\n nodes: readonly ResolvedNode[];\n env: MenuPanelEnv;\n className?: string;\n style?: CSSProperties;\n /** Props for the root element (position, data attributes). */\n rootProps?: Record<string, unknown>;\n /** The root element, for the owner's own measuring and outside-click test. */\n panelRef?: (el: HTMLDivElement | null) => void;\n}\n\n/** THE MENU: the root panel (with the search box), drawn from resolved nodes. */\nexport function MenuPanel({ nodes, env, className, style, rootProps, panelRef }: MenuPanelProps) {\n const q = env.query.trim().toLowerCase();\n const searchAt = nodes.findIndex((n) => n.kind === \"search\");\n const hits = q ? env.search(q) : null;\n const above = searchAt >= 0 ? nodes.slice(0, searchAt) : [];\n return (\n <Env.Provider value={env}>\n <Panel className={`cm-menu ${className ?? \"\"}`} style={{ width: env.look.width, ...style }} root rootProps={rootProps} panelRef={panelRef}>\n {nodes.length === 0 ? (\n <div className=\"cm-empty\">Empty menu — drag options in</div>\n ) : hits ? (\n <>\n <Nodes nodes={above} />\n {searchAt >= 0 && <SearchBox node={nodes[searchAt] as ResolvedSearch} />}\n {hits.length === 0 ? (\n <div className=\"cm-empty\">No matches</div>\n ) : (\n hits.map((h) =>\n h.node.kind === \"item\" ? <Item key={`hit:${h.node.id}`} node={h.node} path={h.path} /> : null,\n )\n )}\n </>\n ) : (\n <Nodes nodes={nodes} />\n )}\n </Panel>\n </Env.Provider>\n );\n}\n\nfunction Panel({\n children,\n className,\n style,\n root,\n rootProps,\n onClose,\n flyoutId,\n panelRef,\n}: {\n panelRef?: ((el: HTMLDivElement | null) => void) | undefined;\n children: ReactNode;\n className: string;\n style?: CSSProperties | undefined;\n root?: boolean;\n rootProps?: Record<string, unknown> | undefined;\n /** A flyout: Esc / Left closes it (the parent focuses its row again). */\n onClose?: (() => void) | undefined;\n flyoutId?: string | undefined;\n}) {\n const env = useEnv();\n const intent = useIntent();\n const ref = useRef<HTMLDivElement>(null);\n const typed = useRef({ text: \"\", at: 0 });\n\n const onKeyDown = (e: ReactKeyboardEvent<HTMLDivElement>) => {\n const panel = ref.current;\n if (!panel) return;\n // A key that started in a flyout inside this panel belongs to that flyout.\n if ((e.target as HTMLElement).closest('[role=\"menu\"]') !== panel) return;\n const inSearch = (e.target as HTMLElement).tagName === \"INPUT\";\n const rows = rowsOf(panel);\n const at = rows.indexOf(document.activeElement as HTMLElement);\n const focus = (i: number) => rows[(i + rows.length) % rows.length]?.focus();\n switch (e.key) {\n case \"ArrowDown\":\n e.preventDefault();\n focus(inSearch || at < 0 ? 0 : at + 1);\n return;\n case \"ArrowUp\":\n e.preventDefault();\n if (inSearch) return;\n if (at <= 0 && root) {\n const search = panel.querySelector<HTMLInputElement>(\".cm-search input\");\n if (search) return search.focus();\n }\n focus(at < 0 ? rows.length - 1 : at - 1);\n return;\n case \"Home\":\n case \"End\":\n if (inSearch) return;\n e.preventDefault();\n focus(e.key === \"Home\" ? 0 : rows.length - 1);\n return;\n case \"ArrowRight\": {\n if (inSearch) return;\n const row = document.activeElement as HTMLElement | null;\n const sub = row?.getAttribute(\"data-cm-sub\");\n if (sub) {\n e.preventDefault();\n intent.openNow(sub, true);\n }\n return;\n }\n case \"ArrowLeft\":\n if (inSearch || !onClose) return;\n e.preventDefault();\n e.stopPropagation();\n onClose();\n return;\n case \"Escape\":\n e.preventDefault();\n e.stopPropagation();\n if (inSearch && env.query) return env.setQuery(\"\");\n if (onClose) onClose();\n else env.onEscape?.();\n return;\n case \"Enter\":\n if (inSearch) {\n e.preventDefault();\n rows[0]?.click();\n }\n return;\n default:\n break;\n }\n // Typeahead: a printable key jumps to the next row starting with what was typed. At the top\n // of a menu with a search box, it types into the search instead.\n if (e.key.length !== 1 || e.metaKey || e.ctrlKey || e.altKey || inSearch) return;\n const search = root ? panel.querySelector<HTMLInputElement>(\".cm-search input\") : null;\n if (search) {\n e.preventDefault();\n search.focus();\n env.setQuery(env.query + e.key);\n return;\n }\n const now = Date.now();\n typed.current = { text: now - typed.current.at > 700 ? e.key.toLowerCase() : typed.current.text + e.key.toLowerCase(), at: now };\n const start = at < 0 ? 0 : typed.current.text.length > 1 ? at : at + 1;\n for (let i = 0; i < rows.length; i++) {\n const row = rows[(start + i) % rows.length];\n if (row && labelOf(row).startsWith(typed.current.text)) {\n e.preventDefault();\n row.focus();\n return;\n }\n }\n };\n\n return (\n <PanelIntent.Provider value={intent}>\n <div\n ref={(el) => {\n ref.current = el;\n panelRef?.(el);\n }}\n role=\"menu\"\n tabIndex={-1}\n data-density={env.look.density}\n {...(flyoutId ? { \"data-cm-flyout\": flyoutId } : {})}\n className={className}\n style={style}\n onKeyDown={onKeyDown}\n onPointerMove={intent.track}\n {...(rootProps ?? {})}\n >\n {children}\n </div>\n </PanelIntent.Provider>\n );\n}\n\nfunction Nodes({ nodes }: { nodes: readonly ResolvedNode[] }) {\n return (\n <>\n {nodes.map((n) => {\n if (n.kind === \"divider\") return <div key={n.id} role=\"separator\" className=\"cm-divider\" />;\n if (n.kind === \"item\") return <Item key={n.id} node={n} />;\n if (n.kind === \"submenu\") return <Submenu key={n.id} node={n} />;\n if (n.kind === \"search\") return <SearchBox key={n.id} node={n} />;\n if (n.kind === \"note\") return <div key={n.id} className={n.tone === \"runtime\" ? \"cm-runtime\" : \"cm-empty\"}>{n.text}</div>;\n return <Section key={n.id} node={n} />;\n })}\n </>\n );\n}\n\nfunction SearchBox({ node }: { node: ResolvedSearch }) {\n const env = useEnv();\n const ref = useRef<HTMLInputElement>(null);\n useEffect(() => {\n if (node.autofocus) ref.current?.focus({ preventScroll: true });\n }, [node.autofocus]);\n return (\n <div className={`cm-search ${env.highlightId === node.designId ? \"cm-hl\" : \"\"}`} onClick={() => node.designId && env.onSelectNode?.(node.designId)}>\n {env.renderIcon(\"Search\", \"cm-icon\")}\n {/* ui-exception: the menu's own filter row — this file IS the menu component */}\n <input ref={ref} value={env.query} onChange={(e) => env.setQuery(e.target.value)} placeholder={node.placeholder || \"Type to filter…\"} aria-label=\"Filter menu\" />\n </div>\n );\n}\n\nfunction Section({ node }: { node: ResolvedSection }) {\n const env = useEnv();\n const [open, setOpen] = useState(!node.collapsible);\n const layout = node.layout;\n return (\n <div className={`cm-section ${env.highlightId && env.highlightId === node.designId ? \"cm-hl\" : \"\"}`} data-layout={layout} {...(node.feature ? { \"data-feature\": \"\" } : {})}>\n {node.label && (\n // ui-exception: a menu row of the menu component itself\n <button\n type=\"button\"\n tabIndex={-1}\n className={`cm-section-label ${node.placeholder ? \"cm-slot-label\" : \"\"}`}\n onClick={() => {\n if (node.designId) env.onSelectNode?.(node.designId);\n if (node.collapsible) setOpen((o) => !o);\n }}\n >\n {node.label}\n {node.collapsible && <span className=\"cm-caret\">{open ? \"▾\" : \"▸\"}</span>}\n </button>\n )}\n {open && (\n <div className=\"cm-section-body\">\n {layout === \"list\" ? (\n <Nodes nodes={node.children} />\n ) : (\n node.children.map((c) => (c.kind === \"item\" ? <Tile key={c.id} node={c} /> : <Nodes key={c.id} nodes={[c]} />))\n )}\n </div>\n )}\n </div>\n );\n}\n\nfunction statusAttrs(n: ResolvedItem | ResolvedSubmenu) {\n const status = n.missingRequired ? \"required-missing\" : n.status && n.status !== \"works\" ? n.status : undefined;\n return status ? { \"data-status\": status } : {};\n}\n\nfunction Tile({ node }: { node: ResolvedItem }) {\n const env = useEnv();\n const reason = reasonOf(node);\n return (\n // ui-exception: a menu row of the menu component itself\n <button\n type=\"button\"\n role=\"menuitem\"\n tabIndex={-1}\n data-cm-row=\"\"\n data-cm-label={node.label}\n title={[node.label + (node.shortcut ? ` ${node.shortcut}` : \"\"), reason].filter(Boolean).join(\"\\n\")}\n aria-label={node.label}\n aria-disabled={node.disabled || undefined}\n {...toneAttrs(node.tone, node.toneLabel)}\n {...statusAttrs(node)}\n className={`cm-tile ${node.danger ? \"cm-danger\" : \"\"} ${node.ai ? \"cm-ai\" : \"\"} ${env.highlightId && env.highlightId === node.designId ? \"cm-hl\" : \"\"}`}\n onClick={() => {\n if (node.designId) env.onSelectNode?.(node.designId);\n if (!node.disabled) env.onActivate(node);\n }}\n >\n <Icon icon={node.icon} fallback=\"Circle\" />\n </button>\n );\n}\n\nfunction Item({ node, path }: { node: ResolvedItem; path?: string }) {\n const env = useEnv();\n const { look } = env;\n const [checked, setChecked] = useState(Boolean(node.checked));\n // A changed default shows at once (the designer flips \"On by default\" while looking at the row).\n useEffect(() => setChecked(Boolean(node.checked)), [node.checked]);\n const settable = node.toggle || node.switch;\n const hint = path || (look.showHints ? node.hint : undefined);\n const reason = reasonOf(node);\n const intent = useContext(PanelIntent);\n return (\n // ui-exception: a menu row of the menu component itself\n <button\n type=\"button\"\n role={settable ? \"menuitemcheckbox\" : \"menuitem\"}\n tabIndex={-1}\n data-cm-row=\"\"\n data-cm-label={node.label}\n aria-checked={settable ? checked : undefined}\n aria-disabled={node.disabled || undefined}\n {...(reason ? { title: reason, \"aria-description\": reason } : {})}\n {...toneAttrs(node.tone, node.toneLabel)}\n {...statusAttrs(node)}\n className={`cm-item ${node.placeholder ? \"cm-slot\" : \"\"} ${node.danger ? \"cm-danger\" : \"\"} ${node.ai ? \"cm-ai\" : \"\"} ${env.highlightId && env.highlightId === node.designId ? \"cm-hl\" : \"\"}`}\n onPointerEnter={(e) => intent?.enter(null, e.currentTarget)}\n onClick={() => {\n if (node.designId) env.onSelectNode?.(node.designId);\n if (node.disabled) return;\n if (settable) setChecked((c) => !c);\n env.onActivate(node);\n }}\n >\n {look.showIcons && (\n <span className=\"cm-icon-slot\">{node.toggle ? (checked ? env.renderIcon(\"Check\", \"cm-icon\") : null) : <Icon icon={node.icon} />}</span>\n )}\n <span className=\"cm-text\">\n <span className=\"cm-label\">{node.label}</span>\n {hint && <span className=\"cm-hint\">{hint}</span>}\n </span>\n {node.count && <span className=\"cm-count\">{node.count}</span>}\n {look.showShortcuts && node.shortcut && !node.switch && <kbd className=\"cm-shortcut\">{node.shortcut}</kbd>}\n {node.switch && <span className=\"cm-switch\" data-on={checked ? \"\" : undefined} aria-hidden=\"true\" />}\n </button>\n );\n}\n\ntype FlyoutSide = \"right\" | \"left\" | \"below\";\n\n/**\n * Builder: where a submenu opens beside its row — right when it fits, else left, else just under it.\n * \"Fits\" is measured against the nearest `[data-menu-bounds]` box (a panel that clips), else the window.\n */\nfunction flyoutSide(row: HTMLElement, width: number): FlyoutSide {\n const r = row.getBoundingClientRect();\n const bounds = row.closest<HTMLElement>(\"[data-menu-bounds]\")?.getBoundingClientRect() ?? { left: 0, right: window.innerWidth };\n if (r.right + width + 8 <= bounds.right) return \"right\";\n if (r.left - width - 8 >= bounds.left) return \"left\";\n return \"below\";\n}\n\n/** Live: the flyout's place on the screen — beside its row, flipped and clamped to stay on screen. */\nexport function placeFlyout(row: DOMRect, size: { width: number; height: number }, view: { width: number; height: number }) {\n const gap = 2;\n const margin = 8;\n let left = row.right + gap;\n let side: \"right\" | \"left\" = \"right\";\n if (left + size.width > view.width - margin) {\n const flipped = row.left - gap - size.width;\n if (flipped >= margin) {\n left = flipped;\n side = \"left\";\n } else left = Math.max(margin, view.width - margin - size.width);\n }\n let top = row.top - 5;\n if (top + size.height > view.height - margin) top = Math.max(margin, view.height - margin - size.height);\n return { left, top, side };\n}\n\nfunction Submenu({ node }: { node: ResolvedSubmenu }) {\n const env = useEnv();\n const intent = useContext(PanelIntent);\n const { look } = env;\n const open = intent?.openId === node.id;\n const [side, setSide] = useState<FlyoutSide>(\"right\");\n const [fixed, setFixed] = useState<{ left: number; top: number } | null>(null);\n const rowRef = useRef<HTMLButtonElement>(null);\n const flyRef = useRef<HTMLDivElement>(null);\n const reason = reasonOf(node);\n const greyed = node.disabled;\n\n useLayoutEffect(() => {\n if (!open || env.flyout !== \"fixed\" || !rowRef.current || !flyRef.current) return;\n const r = rowRef.current.getBoundingClientRect();\n const f = flyRef.current.getBoundingClientRect();\n const at = placeFlyout(r, { width: f.width || look.width, height: f.height }, { width: window.innerWidth, height: window.innerHeight });\n setFixed({ left: at.left, top: at.top });\n setSide(at.side);\n }, [open, env.flyout, look.width]);\n useEffect(() => {\n if (!open) setFixed(null);\n if (open && intent?.viaKey && flyRef.current) {\n const first = rowsOf(flyRef.current)[0];\n (first ?? flyRef.current).focus();\n }\n }, [open, intent?.viaKey]);\n\n const closeFlyout = useCallback(() => {\n intent?.close();\n rowRef.current?.focus();\n }, [intent]);\n\n const flyStyle: CSSProperties =\n env.flyout === \"fixed\"\n ? { position: \"fixed\", width: look.width, maxHeight: \"calc(100vh - 16px)\", overflowY: \"auto\", left: fixed?.left ?? -9999, top: fixed?.top ?? -9999, visibility: fixed ? \"visible\" : \"hidden\" }\n : { width: look.width };\n\n return (\n <div className=\"cm-sub\">\n {/* ui-exception: a menu row of the menu component itself */}\n <button\n ref={rowRef}\n type=\"button\"\n role=\"menuitem\"\n tabIndex={-1}\n data-cm-row=\"\"\n data-cm-label={node.label}\n {...(greyed ? {} : { \"data-cm-sub\": node.id })}\n aria-haspopup=\"menu\"\n aria-expanded={open}\n aria-disabled={greyed || undefined}\n {...(reason ? { title: reason, \"aria-description\": reason } : {})}\n {...toneAttrs(node.tone, node.toneLabel)}\n {...statusAttrs(node)}\n className={`cm-item ${node.placeholder ? \"cm-slot\" : \"\"} ${open ? \"cm-open\" : \"\"} ${env.highlightId && env.highlightId === node.designId ? \"cm-hl\" : \"\"}`}\n onPointerEnter={(e) => {\n if (env.flyout === \"absolute\") setSide(flyoutSide(e.currentTarget, look.width));\n if (greyed) intent?.enter(null, e.currentTarget);\n else intent?.enter(node.id, e.currentTarget);\n }}\n onClick={(e) => {\n if (node.designId) env.onSelectNode?.(node.designId);\n if (greyed) return;\n if (env.flyout === \"absolute\") setSide(flyoutSide(e.currentTarget, look.width));\n intent?.openNow(node.id, e.detail === 0);\n }}\n >\n {look.showIcons && <span className=\"cm-icon-slot\"><Icon icon={node.icon} /></span>}\n <span className=\"cm-text\">\n <span className=\"cm-label\">{node.label}</span>\n {look.showHints && node.hint && <span className=\"cm-hint\">{node.hint}</span>}\n </span>\n {node.loading && <span className=\"cm-count\">…</span>}\n {node.count && <span className=\"cm-count\">{node.count}</span>}\n {env.renderIcon(\"ChevronRight\", \"cm-icon cm-chev\")}\n </button>\n {open && (\n <Panel className=\"cm-menu cm-flyout\" flyoutId={node.id} style={flyStyle} onClose={closeFlyout}>\n <FlyoutBody node={node} flyRef={flyRef} side={env.flyout === \"absolute\" ? side : undefined} />\n </Panel>\n )}\n </div>\n );\n}\n\n/** The flyout's rows (kept apart so the panel element itself is what `flyRef` measures). */\nfunction FlyoutBody({ node, flyRef, side }: { node: ResolvedSubmenu; flyRef: React.RefObject<HTMLDivElement | null>; side: FlyoutSide | undefined }) {\n const ref = useRef<HTMLSpanElement>(null);\n useLayoutEffect(() => {\n const panel = ref.current?.closest<HTMLDivElement>('[role=\"menu\"]') ?? null;\n flyRef.current = panel;\n if (panel && side) panel.setAttribute(\"data-side\", side);\n });\n return (\n <>\n <span ref={ref} hidden />\n {node.loading && node.children.length === 0 ? (\n <div className=\"cm-empty\">Loading…</div>\n ) : node.children.length === 0 ? (\n <div className=\"cm-empty\">{node.emptyLabel ?? \"Empty submenu\"}</div>\n ) : (\n <Nodes nodes={node.children} />\n )}\n </>\n );\n}\n"]}
|