@agent-native/core 0.41.1 → 0.43.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (181) hide show
  1. package/README.md +17 -56
  2. package/dist/action.d.ts +13 -1
  3. package/dist/action.d.ts.map +1 -1
  4. package/dist/action.js.map +1 -1
  5. package/dist/agent/production-agent.d.ts +8 -0
  6. package/dist/agent/production-agent.d.ts.map +1 -1
  7. package/dist/agent/production-agent.js +93 -0
  8. package/dist/agent/production-agent.js.map +1 -1
  9. package/dist/cli/app-skill.d.ts +16 -0
  10. package/dist/cli/app-skill.d.ts.map +1 -1
  11. package/dist/cli/app-skill.js +33 -3
  12. package/dist/cli/app-skill.js.map +1 -1
  13. package/dist/cli/pr-visual-recap-workflow.d.ts +1 -1
  14. package/dist/cli/pr-visual-recap-workflow.d.ts.map +1 -1
  15. package/dist/cli/pr-visual-recap-workflow.js +1 -1
  16. package/dist/cli/pr-visual-recap-workflow.js.map +1 -1
  17. package/dist/cli/recap.d.ts.map +1 -1
  18. package/dist/cli/recap.js +38 -16
  19. package/dist/cli/recap.js.map +1 -1
  20. package/dist/cli/skills.d.ts +30 -3
  21. package/dist/cli/skills.d.ts.map +1 -1
  22. package/dist/cli/skills.js +180 -114
  23. package/dist/cli/skills.js.map +1 -1
  24. package/dist/client/AssistantChat.d.ts.map +1 -1
  25. package/dist/client/AssistantChat.js +2 -2
  26. package/dist/client/AssistantChat.js.map +1 -1
  27. package/dist/client/agent-chat-adapter.d.ts.map +1 -1
  28. package/dist/client/agent-chat-adapter.js +172 -5
  29. package/dist/client/agent-chat-adapter.js.map +1 -1
  30. package/dist/client/blocks/index.d.ts +11 -0
  31. package/dist/client/blocks/index.d.ts.map +1 -1
  32. package/dist/client/blocks/index.js +11 -0
  33. package/dist/client/blocks/index.js.map +1 -1
  34. package/dist/client/blocks/library/AnnotatedCodeBlock.d.ts +19 -0
  35. package/dist/client/blocks/library/AnnotatedCodeBlock.d.ts.map +1 -1
  36. package/dist/client/blocks/library/AnnotatedCodeBlock.js +6 -58
  37. package/dist/client/blocks/library/AnnotatedCodeBlock.js.map +1 -1
  38. package/dist/client/blocks/library/ApiEndpointBlock.d.ts.map +1 -1
  39. package/dist/client/blocks/library/ApiEndpointBlock.js +116 -7
  40. package/dist/client/blocks/library/ApiEndpointBlock.js.map +1 -1
  41. package/dist/client/blocks/library/DataModelBlock.d.ts.map +1 -1
  42. package/dist/client/blocks/library/DataModelBlock.js +75 -9
  43. package/dist/client/blocks/library/DataModelBlock.js.map +1 -1
  44. package/dist/client/blocks/library/DiffBlock.d.ts +1 -1
  45. package/dist/client/blocks/library/DiffBlock.d.ts.map +1 -1
  46. package/dist/client/blocks/library/DiffBlock.js +265 -39
  47. package/dist/client/blocks/library/DiffBlock.js.map +1 -1
  48. package/dist/client/blocks/library/FileTreeBlock.d.ts.map +1 -1
  49. package/dist/client/blocks/library/FileTreeBlock.js +27 -4
  50. package/dist/client/blocks/library/FileTreeBlock.js.map +1 -1
  51. package/dist/client/blocks/library/HighlightedCode.d.ts +1 -1
  52. package/dist/client/blocks/library/HighlightedCode.js +1 -1
  53. package/dist/client/blocks/library/HighlightedCode.js.map +1 -1
  54. package/dist/client/blocks/library/JsonExplorerBlock.js +1 -1
  55. package/dist/client/blocks/library/JsonExplorerBlock.js.map +1 -1
  56. package/dist/client/blocks/library/MermaidBlock.js +1 -1
  57. package/dist/client/blocks/library/MermaidBlock.js.map +1 -1
  58. package/dist/client/blocks/library/annotation-rail.d.ts +115 -0
  59. package/dist/client/blocks/library/annotation-rail.d.ts.map +1 -0
  60. package/dist/client/blocks/library/annotation-rail.js +139 -0
  61. package/dist/client/blocks/library/annotation-rail.js.map +1 -0
  62. package/dist/client/blocks/library/api-endpoint.config.d.ts +31 -6
  63. package/dist/client/blocks/library/api-endpoint.config.d.ts.map +1 -1
  64. package/dist/client/blocks/library/api-endpoint.config.js +30 -6
  65. package/dist/client/blocks/library/api-endpoint.config.js.map +1 -1
  66. package/dist/client/blocks/library/callout.config.d.ts +29 -0
  67. package/dist/client/blocks/library/callout.config.d.ts.map +1 -0
  68. package/dist/client/blocks/library/callout.config.js +33 -0
  69. package/dist/client/blocks/library/callout.config.js.map +1 -0
  70. package/dist/client/blocks/library/callout.d.ts +20 -0
  71. package/dist/client/blocks/library/callout.d.ts.map +1 -0
  72. package/dist/client/blocks/library/callout.js +61 -0
  73. package/dist/client/blocks/library/callout.js.map +1 -0
  74. package/dist/client/blocks/library/checklist.d.ts.map +1 -1
  75. package/dist/client/blocks/library/checklist.js +3 -3
  76. package/dist/client/blocks/library/checklist.js.map +1 -1
  77. package/dist/client/blocks/library/code.d.ts.map +1 -1
  78. package/dist/client/blocks/library/code.js +32 -15
  79. package/dist/client/blocks/library/code.js.map +1 -1
  80. package/dist/client/blocks/library/columns.d.ts.map +1 -1
  81. package/dist/client/blocks/library/columns.js +56 -35
  82. package/dist/client/blocks/library/columns.js.map +1 -1
  83. package/dist/client/blocks/library/data-model.config.d.ts +17 -0
  84. package/dist/client/blocks/library/data-model.config.d.ts.map +1 -1
  85. package/dist/client/blocks/library/data-model.config.js +15 -0
  86. package/dist/client/blocks/library/data-model.config.js.map +1 -1
  87. package/dist/client/blocks/library/decision.config.d.ts +37 -0
  88. package/dist/client/blocks/library/decision.config.d.ts.map +1 -0
  89. package/dist/client/blocks/library/decision.config.js +32 -0
  90. package/dist/client/blocks/library/decision.config.js.map +1 -0
  91. package/dist/client/blocks/library/decision.d.ts +19 -0
  92. package/dist/client/blocks/library/decision.d.ts.map +1 -0
  93. package/dist/client/blocks/library/decision.js +119 -0
  94. package/dist/client/blocks/library/decision.js.map +1 -0
  95. package/dist/client/blocks/library/diagram.config.d.ts +64 -0
  96. package/dist/client/blocks/library/diagram.config.d.ts.map +1 -0
  97. package/dist/client/blocks/library/diagram.config.js +111 -0
  98. package/dist/client/blocks/library/diagram.config.js.map +1 -0
  99. package/dist/client/blocks/library/diagram.d.ts +16 -0
  100. package/dist/client/blocks/library/diagram.d.ts.map +1 -0
  101. package/dist/client/blocks/library/diagram.js +261 -0
  102. package/dist/client/blocks/library/diagram.js.map +1 -0
  103. package/dist/client/blocks/library/diff.config.d.ts +28 -6
  104. package/dist/client/blocks/library/diff.config.d.ts.map +1 -1
  105. package/dist/client/blocks/library/diff.config.js +30 -6
  106. package/dist/client/blocks/library/diff.config.js.map +1 -1
  107. package/dist/client/blocks/library/question-form.config.d.ts +69 -0
  108. package/dist/client/blocks/library/question-form.config.d.ts.map +1 -0
  109. package/dist/client/blocks/library/question-form.config.js +58 -0
  110. package/dist/client/blocks/library/question-form.config.js.map +1 -0
  111. package/dist/client/blocks/library/question-form.d.ts +20 -0
  112. package/dist/client/blocks/library/question-form.d.ts.map +1 -0
  113. package/dist/client/blocks/library/question-form.js +286 -0
  114. package/dist/client/blocks/library/question-form.js.map +1 -0
  115. package/dist/client/blocks/library/sanitize-html.d.ts +5 -0
  116. package/dist/client/blocks/library/sanitize-html.d.ts.map +1 -0
  117. package/dist/client/blocks/library/sanitize-html.js +240 -0
  118. package/dist/client/blocks/library/sanitize-html.js.map +1 -0
  119. package/dist/client/blocks/library/server-specs.d.ts.map +1 -1
  120. package/dist/client/blocks/library/server-specs.js +59 -0
  121. package/dist/client/blocks/library/server-specs.js.map +1 -1
  122. package/dist/client/blocks/library/specs.d.ts.map +1 -1
  123. package/dist/client/blocks/library/specs.js +11 -0
  124. package/dist/client/blocks/library/specs.js.map +1 -1
  125. package/dist/client/blocks/library/tabs.d.ts.map +1 -1
  126. package/dist/client/blocks/library/tabs.js +12 -12
  127. package/dist/client/blocks/library/tabs.js.map +1 -1
  128. package/dist/client/blocks/library/wireframe-kit.d.ts +260 -0
  129. package/dist/client/blocks/library/wireframe-kit.d.ts.map +1 -0
  130. package/dist/client/blocks/library/wireframe-kit.js +920 -0
  131. package/dist/client/blocks/library/wireframe-kit.js.map +1 -0
  132. package/dist/client/blocks/library/wireframe.config.d.ts +123 -0
  133. package/dist/client/blocks/library/wireframe.config.d.ts.map +1 -0
  134. package/dist/client/blocks/library/wireframe.config.js +294 -0
  135. package/dist/client/blocks/library/wireframe.config.js.map +1 -0
  136. package/dist/client/blocks/library/wireframe.d.ts +15 -0
  137. package/dist/client/blocks/library/wireframe.d.ts.map +1 -0
  138. package/dist/client/blocks/library/wireframe.js +206 -0
  139. package/dist/client/blocks/library/wireframe.js.map +1 -0
  140. package/dist/client/blocks/registry.d.ts +9 -0
  141. package/dist/client/blocks/registry.d.ts.map +1 -1
  142. package/dist/client/blocks/registry.js +12 -5
  143. package/dist/client/blocks/registry.js.map +1 -1
  144. package/dist/client/blocks/server.d.ts +1 -0
  145. package/dist/client/blocks/server.d.ts.map +1 -1
  146. package/dist/client/blocks/server.js +1 -0
  147. package/dist/client/blocks/server.js.map +1 -1
  148. package/dist/client/blocks/types.d.ts +10 -2
  149. package/dist/client/blocks/types.d.ts.map +1 -1
  150. package/dist/client/blocks/types.js.map +1 -1
  151. package/dist/client/rich-markdown-editor/DragHandle.d.ts.map +1 -1
  152. package/dist/client/rich-markdown-editor/DragHandle.js +152 -21
  153. package/dist/client/rich-markdown-editor/DragHandle.js.map +1 -1
  154. package/dist/client/rich-markdown-editor/RegistryBlockNode.d.ts +25 -1
  155. package/dist/client/rich-markdown-editor/RegistryBlockNode.d.ts.map +1 -1
  156. package/dist/client/rich-markdown-editor/RegistryBlockNode.js +29 -6
  157. package/dist/client/rich-markdown-editor/RegistryBlockNode.js.map +1 -1
  158. package/dist/client/rich-markdown-editor/SharedRichEditor.d.ts +8 -1
  159. package/dist/client/rich-markdown-editor/SharedRichEditor.d.ts.map +1 -1
  160. package/dist/client/rich-markdown-editor/SharedRichEditor.js +5 -1
  161. package/dist/client/rich-markdown-editor/SharedRichEditor.js.map +1 -1
  162. package/dist/extensions/actions.d.ts.map +1 -1
  163. package/dist/extensions/actions.js +159 -12
  164. package/dist/extensions/actions.js.map +1 -1
  165. package/dist/extensions/store.d.ts +21 -0
  166. package/dist/extensions/store.d.ts.map +1 -1
  167. package/dist/extensions/store.js +33 -1
  168. package/dist/extensions/store.js.map +1 -1
  169. package/dist/server/recap-image-route.d.ts.map +1 -1
  170. package/dist/server/recap-image-route.js +12 -3
  171. package/dist/server/recap-image-route.js.map +1 -1
  172. package/dist/styles/agent-native.css +1 -0
  173. package/dist/styles/blocks.css +1380 -0
  174. package/dist/templates/workspace-core/.agents/skills/extensions/SKILL.md +30 -5
  175. package/docs/content/plan-plugin.md +107 -0
  176. package/docs/content/pr-visual-recap.md +2 -2
  177. package/docs/content/skills-guide.md +8 -0
  178. package/docs/content/template-plan.md +94 -17
  179. package/package.json +2 -1
  180. package/src/templates/workspace-core/.agents/skills/extensions/SKILL.md +30 -5
  181. package/docs/content/visual-plans.md +0 -80
@@ -1 +1 @@
1
- {"version":3,"file":"types.js","sourceRoot":"","sources":["../../../src/client/blocks/types.ts"],"names":[],"mappings":"AAmWA,qEAAqE;AACrE,MAAM,UAAU,WAAW,CAAQ,IAAsB;IACvD,OAAO,IAAI,CAAC;AACd,CAAC","sourcesContent":["import type { FC } from \"react\";\nimport type { ZodType } from \"zod\";\n\n/**\n * Block-registry contract. A `BlockSpec` describes one document block end to end:\n * its data shape (`schema`), how it round-trips to MDX source (`mdx`), how it\n * renders read-only (`Read`) and how it is edited (`Edit`, or an auto-generated\n * schema-driven editor when omitted), where it can be placed (`placement`), and\n * metadata for menus / agent schema export.\n *\n * The registry runs ALONGSIDE existing per-block code (the plan `PlanBlockView`\n * switch + `serializeBlock`/`parseBlock`). Renderers check the registry first;\n * unregistered block types fall through to the legacy code path unchanged. The\n * MDX `tag` and attribute shape for a converted block MUST match the historical\n * encoding (e.g. `<Callout tone>…body…</Callout>`) so stored `.mdx` files still\n * parse byte-compatibly.\n */\n\n/** Where a block can be placed in a document. */\nexport type BlockPlacement = \"block\" | \"inline\";\n\n/**\n * A serialized MDX/NFM attribute value before the shared `prop()` encoder runs.\n * `prop()` decides string-vs-JSON encoding; this is just the value domain.\n */\nexport type MdxAttrValue =\n | string\n | number\n | boolean\n | unknown[]\n | Record<string, unknown>;\n\n/**\n * Type-narrowed reader over the resolved MDX attributes of a parsed block node.\n * The values are already estree/JSON-resolved by the shared attribute reader\n * (the same engine `plan-mdx.ts` uses), so a spec's `fromAttrs` never touches\n * the AST directly.\n */\nexport interface BlockAttrReader {\n string(name: string): string | undefined;\n number(name: string): number | undefined;\n bool(name: string): boolean | undefined;\n array<T = unknown>(name: string): T[] | undefined;\n object<T = unknown>(name: string): T | undefined;\n raw(name: string): unknown;\n}\n\n/**\n * Maps a block's validated data to/from its MDX component representation.\n * `tag` is the JSX component name in source (e.g. \"Callout\"). It MUST match the\n * historical name in `plan-mdx.ts` `BLOCK_COMPONENTS` / stored `.mdx` files or\n * existing plans break.\n */\nexport interface BlockMdxConfig<TData> {\n /** JSX component name in MDX source. Stable contract — never rename. */\n tag: string;\n /**\n * Encode `data` → a flat attribute bag. The registry serializer runs each\n * value through the shared `prop()` encoder (string-vs-JSON heuristic) and\n * preserves insertion order, so write the keys in the exact historical order.\n * Return `undefined` for a key (or omit it) to drop the attribute. When\n * `childrenField` is set, that field is excluded from the attribute bag.\n */\n toAttrs: (data: TData) => Record<string, MdxAttrValue | undefined>;\n /**\n * Decode resolved attributes (+ optional children markdown) → data. Must\n * tolerate missing/partial attributes for backward-compat (mirror today's\n * `?? []` / `?? \"\"` defaults).\n */\n fromAttrs: (attrs: BlockAttrReader, children: string) => TData;\n /**\n * When set, this data field is a markdown string serialized as MDX *children*\n * between the open/close tags (prose-bearing blocks: rich-text, callout)\n * rather than as a prop — so the body survives as real, inline-editable MDX\n * prose in source.\n */\n childrenField?: keyof TData & string;\n /**\n * Opt-in custom children serializer for blocks whose internals are nested MDX\n * components rather than a single markdown string (e.g. wireframe → Screen/kit\n * primitives). When present it overrides `childrenField`. `serializeChildren`\n * returns the raw inner MDX; `parseChildren` receives the child MDX AST nodes.\n */\n serializeChildren?: (data: TData) => string;\n parseChildren?: (childNodes: unknown[], idContext: string) => Partial<TData>;\n}\n\n/**\n * App-injected capabilities. Core blocks stay app-agnostic by taking these\n * rather than importing app services — mirroring `createImageExtension`'s\n * `onImageUpload` injection. Provided via `BlockRegistryProvider`.\n */\nexport interface BlockRenderContext {\n /** Markdown dialect for the auto-editor's rich-text field. */\n dialect?: \"gfm\" | \"nfm\";\n /** Resolve an asset id → displayable URL. */\n resolveAssetSrc?: (assetId: string) => string | undefined;\n /** Open the shared asset picker (returns the chosen asset). */\n pickAsset?: () => Promise<{ assetId: string; url?: string } | null>;\n /** Upload a local file, returns a hosted URL. */\n uploadFile?: (file: File) => Promise<{ url: string; assetId?: string }>;\n /** Call an app action by name (for blocks that fetch live data). */\n callAction?: (name: string, args: unknown) => Promise<unknown>;\n /** Sanitizer for HTML-bearing blocks. Provided by the app/core. */\n sanitizeHtml?: (html: string, css?: string) => string;\n /**\n * Render a markdown string with the app's read-only markdown renderer. Lets a\n * core block (whose `Read` lives in core) defer prose rendering to the app's\n * markdown reader (e.g. the plan `PlanMarkdownReader`) without importing it.\n */\n renderMarkdown?: (\n markdown: string,\n options?: { className?: string },\n ) => React.ReactNode;\n /**\n * Render an inline, editable rich-markdown field. The auto-editor calls this\n * for a `markdown()`-tagged field so the app owns the editor wiring (collab,\n * autosave debounce, dialect) rather than core hardcoding it.\n */\n renderMarkdownEditor?: (props: {\n value: string;\n onChange: (next: string) => void;\n editable: boolean;\n blockId?: string;\n className?: string;\n ariaLabel?: string;\n }) => React.ReactNode;\n /**\n * Render an app-owned \"Edit with AI\" affordance for a focused/editable block\n * field. Core block editors pass the current field value and nearby companion\n * fields; the host app decides how to collect the prompt and route it to the\n * agent sidebar. This keeps reusable core blocks from importing app-specific\n * popover/composer code while still exposing a generic AI edit hook.\n */\n renderAiFieldAction?: (props: BlockAiFieldActionProps) => React.ReactNode;\n /**\n * Render a nested child block through the app's own block dispatcher. Container\n * blocks whose `Read`/`Edit` live in core (e.g. tabs) call this to render each\n * child so the recursion keeps flowing through the SAME app renderer the\n * top-level document uses — registered children render via their spec, and\n * unregistered (not-yet-converted) children still fall through the app's legacy\n * switch. This is the coexistence seam: a core container never has to know\n * about app-specific child block types. Returns `null`/`undefined` when no\n * dispatcher is wired (read-only/SSR-only contexts can omit it).\n */\n renderBlock?: (props: {\n block: NestedBlock;\n /** Commit a replacement for this child block (edit mode only). */\n onChange?: (next: NestedBlock) => void;\n /** Whether the parent container is being edited. */\n editing?: boolean;\n /** Tighten embedded visuals in dense contexts (e.g. tab panes). */\n compactVisuals?: boolean;\n }) => React.ReactNode;\n /**\n * Render a nested editable block list through the host app's document editor.\n * Container blocks such as columns call this for each editable region so slash\n * commands, nested structured blocks, and ordinary prose behave like the\n * top-level document while the container still persists its normalized runtime\n * data. Source adapters may still expose a human-friendly nested MDX form\n * (for example `<Columns><Column>markdown</Column></Columns>`) and normalize it\n * into these block arrays at runtime.\n */\n renderBlocksEditor?: (props: {\n blocks: NestedBlock[];\n onChange: (blocks: NestedBlock[]) => void;\n editable: boolean;\n containerBlockId: string;\n regionId: string;\n regionLabel?: string;\n /** Tighten embedded visuals in dense regions such as tab panes. */\n compactVisuals?: boolean;\n }) => React.ReactNode;\n /**\n * Wrap a block's edit form in an app-provided \"panel\" surface (e.g. a shadcn\n * Popover anchored to the corner edit button) for `editSurface: \"panel\"`\n * blocks. Core renders the rendered `Read` view plus a corner trigger button\n * and the form, then hands them here so the app owns the overlay primitive\n * (core stays shadcn-free, mirroring `renderMarkdownEditor`). When omitted, a\n * panel-mode block falls back to inline editing. `title` is the block label.\n */\n renderEditSurface?: (props: {\n title: string;\n open?: boolean;\n onOpenChange?: (open: boolean) => void;\n trigger: React.ReactNode;\n children: React.ReactNode;\n /** Metadata for host-provided contextual controls such as \"Edit with AI\". */\n blockId?: string;\n blockType?: string;\n blockTitle?: string;\n blockSummary?: string;\n blockData?: unknown;\n }) => React.ReactNode;\n}\n\nexport interface BlockAiFieldActionProps {\n blockId: string;\n blockType: string;\n blockTitle?: string;\n blockSummary?: string;\n fieldLabel: string;\n fieldValue: string;\n draftScope: string;\n disabled?: boolean;\n /**\n * Human-readable instructions for the host agent prompt. Mention how to patch\n * the block and which sibling fields should be preserved.\n */\n instructions: string;\n companionFields?: Array<{\n label: string;\n value: string;\n language?: string;\n }>;\n}\n\n/**\n * The minimal shape of a nested child block passed to {@link\n * BlockRenderContext.renderBlock}. It mirrors the app's block union loosely (the\n * app casts it back to its own block type) — a discriminating `type`, a stable\n * `id`, optional heading/summary, and the type-specific `data`.\n */\nexport interface NestedBlock {\n type: string;\n id: string;\n title?: string;\n summary?: string;\n data: unknown;\n [key: string]: unknown;\n}\n\nexport interface BlockContainerRegion {\n id: string;\n label?: string;\n blocks: NestedBlock[];\n}\n\nexport interface BlockContainerSpec<TData> {\n regions: (data: TData) => BlockContainerRegion[];\n updateRegion: (data: TData, regionId: string, blocks: NestedBlock[]) => TData;\n addRegion?: (data: TData, afterRegionId?: string) => TData;\n removeRegion?: (data: TData, regionId: string) => TData;\n reorderRegion?: (\n data: TData,\n fromRegionId: string,\n toRegionId: string,\n ) => TData;\n}\n\nexport type BlockDataChangeMeta = {\n containerRegion?: {\n regionId: string;\n blocks: NestedBlock[];\n };\n};\n\n/** Props passed to a block's read-only renderer. */\nexport interface BlockReadProps<TData> {\n data: TData;\n /** Stable block id (for anchors, comment targeting, source patches). */\n blockId: string;\n /** Block heading, when present. */\n title?: string;\n /** Block trailing summary, when present. */\n summary?: string;\n /** Injected app capabilities. */\n ctx: BlockRenderContext;\n}\n\n/** Props passed to a block's editor (custom or schema-generated). */\nexport interface BlockEditProps<TData> {\n data: TData;\n onChange: (next: TData, meta?: BlockDataChangeMeta) => void;\n editable: boolean;\n blockId: string;\n title?: string;\n summary?: string;\n /** Injected app capabilities. */\n ctx: BlockRenderContext;\n}\n\nexport interface BlockSpec<TData = unknown> {\n /** Discriminator. Equals the runtime block `type`. */\n type: string;\n /** Zod schema for `data`. Drives validation AND the schema-auto-editor. */\n schema: ZodType<TData>;\n /** MDX round-trip config. */\n mdx: BlockMdxConfig<TData>;\n /** Read-only renderer (replaces a `PlanBlockView` switch branch / NodeView). */\n Read: FC<BlockReadProps<TData>>;\n /**\n * Optional editor. When omitted, the registry renders the schema-driven\n * `SchemaBlockEditor` generated from `schema`. Supply for full control\n * (wireframe canvas, diagram editor).\n */\n Edit?: FC<BlockEditProps<TData>>;\n /** Allowed placements: `[\"block\"]`, `[\"inline\"]`, or both. */\n placement: BlockPlacement[];\n /**\n * When `true`, this block's data maps to a Notion-Flavored-Markdown (NFM)\n * analog and therefore round-trips into a Notion page. Apps can derive\n * registry-backed Notion allowlists with\n * {@link BlockRegistry.notionCompatibleTypes} instead of hand-maintaining\n * per-app sets. Set it on registry-atom blocks with an NFM counterpart\n * (checklist, table); leave it `false`/undefined on dev-doc blocks\n * (api-endpoint, openapi-spec, data-model, diff, file-tree, json-explorer,\n * annotated-code, mermaid, custom-html, tabs, code-tabs) and visual/plan-only\n * blocks (wireframe, diagram). Prose blocks that aren't registry atoms\n * (rich-text, callout) carry their NFM analog through the prose path, not this\n * flag.\n */\n notionCompatible?: boolean;\n /**\n * How the block is edited in a `block`-placed document:\n * - `\"inline\"` — the `Edit`/auto-form renders in place for direct\n * manipulation of authored content (prose, checklist text, table cells,\n * code bodies). Schema-ish metadata such as tone/type, tab labels,\n * language, density, or structural settings should still be tucked behind a\n * contextual edit/settings affordance inside the custom `Edit`.\n * - `\"panel\"` — the block shows its rendered `Read` view with a corner edit\n * button that opens the `Edit`/auto-form in an app-provided panel (popover).\n * Best for config-driven blocks whose render differs from their props\n * (custom HTML, charts, any user-registered block).\n * - `\"container\"` — the block renders its `Edit` in place, and that editor\n * may call `ctx.renderBlocksEditor` for nested block regions with normal\n * slash commands and nested structured blocks.\n * Defaults to `\"inline\"` when a custom `Edit` is supplied, else `\"panel\"`\n * (auto-form blocks are property forms, ideal for a panel). The app must wire\n * `ctx.renderEditSurface` for `\"panel\"` to take effect; otherwise it falls\n * back to inline.\n */\n editSurface?: \"inline\" | \"panel\" | \"container\";\n /**\n * Optional generic contract for content-bearing container blocks. Keep this\n * runtime-oriented: it describes editable regions over normalized block arrays;\n * source formats can provide readable nested MDX adapters independently.\n */\n container?: BlockContainerSpec<TData>;\n /** Human label for menus + agent schema export. */\n label: string;\n /** Tabler icon component for UI menus (never emoji/robot/sparkle). */\n icon?: FC<{ size?: number; className?: string }>;\n /** One-line description for the agent schema export. */\n description: string;\n /** Optional default `data` factory for slash-menu insertion (an empty block). */\n empty?: () => TData;\n /**\n * Optional block-specific source-patch handlers, generalizing bespoke ops\n * like `update-custom-html`. Keyed by op name; the registry dispatches a\n * matching patch op here. Generic ops (`update-block` shallow-merge) need none.\n */\n patches?: Record<string, (data: TData, op: Record<string, unknown>) => TData>;\n}\n\n/** Identity helper for authoring a spec with full type inference. */\nexport function defineBlock<TData>(spec: BlockSpec<TData>): BlockSpec<TData> {\n return spec;\n}\n"]}
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../../src/client/blocks/types.ts"],"names":[],"mappings":"AA2WA,qEAAqE;AACrE,MAAM,UAAU,WAAW,CAAQ,IAAsB;IACvD,OAAO,IAAI,CAAC;AACd,CAAC","sourcesContent":["import type { FC } from \"react\";\nimport type { ZodType } from \"zod\";\n\n/**\n * Block-registry contract. A `BlockSpec` describes one document block end to end:\n * its data shape (`schema`), how it round-trips to MDX source (`mdx`), how it\n * renders read-only (`Read`) and how it is edited (`Edit`, or an auto-generated\n * schema-driven editor when omitted), where it can be placed (`placement`), and\n * metadata for menus / agent schema export.\n *\n * The registry runs ALONGSIDE existing per-block code (the plan `PlanBlockView`\n * switch + `serializeBlock`/`parseBlock`). Renderers check the registry first;\n * unregistered block types fall through to the legacy code path unchanged. The\n * MDX `tag` and attribute shape for a converted block MUST match the historical\n * encoding (e.g. `<Callout tone>…body…</Callout>`) so stored `.mdx` files still\n * parse byte-compatibly.\n */\n\n/** Where a block can be placed in a document. */\nexport type BlockPlacement = \"block\" | \"inline\";\n\n/**\n * A serialized MDX/NFM attribute value before the shared `prop()` encoder runs.\n * `prop()` decides string-vs-JSON encoding; this is just the value domain.\n */\nexport type MdxAttrValue =\n | string\n | number\n | boolean\n | unknown[]\n | Record<string, unknown>;\n\n/**\n * Type-narrowed reader over the resolved MDX attributes of a parsed block node.\n * The values are already estree/JSON-resolved by the shared attribute reader\n * (the same engine `plan-mdx.ts` uses), so a spec's `fromAttrs` never touches\n * the AST directly.\n */\nexport interface BlockAttrReader {\n string(name: string): string | undefined;\n number(name: string): number | undefined;\n bool(name: string): boolean | undefined;\n array<T = unknown>(name: string): T[] | undefined;\n object<T = unknown>(name: string): T | undefined;\n raw(name: string): unknown;\n}\n\n/**\n * Maps a block's validated data to/from its MDX component representation.\n * `tag` is the JSX component name in source (e.g. \"Callout\"). It MUST match the\n * historical name in `plan-mdx.ts` `BLOCK_COMPONENTS` / stored `.mdx` files or\n * existing plans break.\n */\nexport interface BlockMdxConfig<TData> {\n /** JSX component name in MDX source. Stable contract — never rename. */\n tag: string;\n /**\n * Encode `data` → a flat attribute bag. The registry serializer runs each\n * value through the shared `prop()` encoder (string-vs-JSON heuristic) and\n * preserves insertion order, so write the keys in the exact historical order.\n * Return `undefined` for a key (or omit it) to drop the attribute. When\n * `childrenField` is set, that field is excluded from the attribute bag.\n */\n toAttrs: (data: TData) => Record<string, MdxAttrValue | undefined>;\n /**\n * Decode resolved attributes (+ optional children markdown) → data. Must\n * tolerate missing/partial attributes for backward-compat (mirror today's\n * `?? []` / `?? \"\"` defaults).\n */\n fromAttrs: (attrs: BlockAttrReader, children: string) => TData;\n /**\n * When set, this data field is a markdown string serialized as MDX *children*\n * between the open/close tags (prose-bearing blocks: rich-text, callout)\n * rather than as a prop — so the body survives as real, inline-editable MDX\n * prose in source.\n */\n childrenField?: keyof TData & string;\n /**\n * Opt-in custom children serializer for blocks whose internals are nested MDX\n * components rather than a single markdown string (e.g. wireframe → Screen/kit\n * primitives). When present it overrides `childrenField`. `serializeChildren`\n * returns the raw inner MDX; `parseChildren` receives the child MDX AST nodes.\n */\n serializeChildren?: (data: TData) => string;\n parseChildren?: (childNodes: unknown[], idContext: string) => Partial<TData>;\n}\n\n/**\n * App-injected capabilities. Core blocks stay app-agnostic by taking these\n * rather than importing app services — mirroring `createImageExtension`'s\n * `onImageUpload` injection. Provided via `BlockRegistryProvider`.\n */\nexport interface BlockRenderContext {\n /** Markdown dialect for the auto-editor's rich-text field. */\n dialect?: \"gfm\" | \"nfm\";\n /** Resolve an asset id → displayable URL. */\n resolveAssetSrc?: (assetId: string) => string | undefined;\n /** Open the shared asset picker (returns the chosen asset). */\n pickAsset?: () => Promise<{ assetId: string; url?: string } | null>;\n /** Upload a local file, returns a hosted URL. */\n uploadFile?: (file: File) => Promise<{ url: string; assetId?: string }>;\n /** Call an app action by name (for blocks that fetch live data). */\n callAction?: (name: string, args: unknown) => Promise<unknown>;\n /** Sanitizer for HTML-bearing blocks. Provided by the app/core. */\n sanitizeHtml?: (html: string, css?: string) => string;\n /**\n * Render a markdown string with the app's read-only markdown renderer. Lets a\n * core block (whose `Read` lives in core) defer prose rendering to the app's\n * markdown reader (e.g. the plan `PlanMarkdownReader`) without importing it.\n */\n renderMarkdown?: (\n markdown: string,\n options?: { className?: string },\n ) => React.ReactNode;\n /**\n * Render an inline, editable rich-markdown field. The auto-editor calls this\n * for a `markdown()`-tagged field so the app owns the editor wiring (collab,\n * autosave debounce, dialect) rather than core hardcoding it.\n */\n renderMarkdownEditor?: (props: {\n value: string;\n onChange: (next: string) => void;\n editable: boolean;\n blockId?: string;\n className?: string;\n ariaLabel?: string;\n }) => React.ReactNode;\n /**\n * Render an app-owned edit-by-prompt affordance (\"Describe a change…\") for a focused/editable block\n * field. Core block editors pass the current field value and nearby companion\n * fields; the host app decides how to collect the prompt and route it to the\n * agent sidebar. This keeps reusable core blocks from importing app-specific\n * popover/composer code while still exposing a generic AI edit hook.\n */\n renderAiFieldAction?: (props: BlockAiFieldActionProps) => React.ReactNode;\n /**\n * Render a nested child block through the app's own block dispatcher. Container\n * blocks whose `Read`/`Edit` live in core (e.g. tabs) call this to render each\n * child so the recursion keeps flowing through the SAME app renderer the\n * top-level document uses — registered children render via their spec, and\n * unregistered (not-yet-converted) children still fall through the app's legacy\n * switch. This is the coexistence seam: a core container never has to know\n * about app-specific child block types. Returns `null`/`undefined` when no\n * dispatcher is wired (read-only/SSR-only contexts can omit it).\n */\n renderBlock?: (props: {\n block: NestedBlock;\n /** Commit a replacement for this child block (edit mode only). */\n onChange?: (next: NestedBlock) => void;\n /** Whether the parent container is being edited. */\n editing?: boolean;\n /** Tighten embedded visuals in dense contexts (e.g. tab panes). */\n compactVisuals?: boolean;\n }) => React.ReactNode;\n /**\n * Render a nested editable block list through the host app's document editor.\n * Container blocks such as columns call this for each editable region so slash\n * commands, nested structured blocks, and ordinary prose behave like the\n * top-level document while the container still persists its normalized runtime\n * data. Source adapters may still expose a human-friendly nested MDX form\n * (for example `<Columns><Column>markdown</Column></Columns>`) and normalize it\n * into these block arrays at runtime.\n */\n renderBlocksEditor?: (props: {\n blocks: NestedBlock[];\n onChange: (blocks: NestedBlock[]) => void;\n editable: boolean;\n containerBlockId: string;\n regionId: string;\n regionLabel?: string;\n /** Tighten embedded visuals in dense regions such as tab panes. */\n compactVisuals?: boolean;\n }) => React.ReactNode;\n /**\n * Wrap a block's edit form in an app-provided \"panel\" surface (e.g. a shadcn\n * Popover anchored to the corner edit button) for `editSurface: \"panel\"`\n * blocks. Core renders the rendered `Read` view plus a corner trigger button\n * and the form, then hands them here so the app owns the overlay primitive\n * (core stays shadcn-free, mirroring `renderMarkdownEditor`). When omitted, a\n * panel-mode block falls back to inline editing. `title` is the block label.\n */\n renderEditSurface?: (props: {\n title: string;\n open?: boolean;\n onOpenChange?: (open: boolean) => void;\n trigger: React.ReactNode;\n children: React.ReactNode;\n /** Metadata for host-provided contextual controls such as the edit-by-prompt CTA. */\n blockId?: string;\n blockType?: string;\n blockTitle?: string;\n blockSummary?: string;\n blockData?: unknown;\n }) => React.ReactNode;\n /**\n * Submit a respondent's answers from a `question-form` / `visual-questions`\n * block back to the host. The app decides how to route the summary (e.g. send\n * to the inline agent, copy to clipboard). Core blocks call this through the\n * context so they never import app-specific submit wiring; omit it and the\n * block degrades to a no-op submit.\n */\n onQuestionFormSubmit?: (summary: string) => void;\n}\n\nexport interface BlockAiFieldActionProps {\n blockId: string;\n blockType: string;\n blockTitle?: string;\n blockSummary?: string;\n fieldLabel: string;\n fieldValue: string;\n draftScope: string;\n disabled?: boolean;\n /**\n * Human-readable instructions for the host agent prompt. Mention how to patch\n * the block and which sibling fields should be preserved.\n */\n instructions: string;\n companionFields?: Array<{\n label: string;\n value: string;\n language?: string;\n }>;\n}\n\n/**\n * The minimal shape of a nested child block passed to {@link\n * BlockRenderContext.renderBlock}. It mirrors the app's block union loosely (the\n * app casts it back to its own block type) — a discriminating `type`, a stable\n * `id`, optional heading/summary, and the type-specific `data`.\n */\nexport interface NestedBlock {\n type: string;\n id: string;\n title?: string;\n summary?: string;\n data: unknown;\n [key: string]: unknown;\n}\n\nexport interface BlockContainerRegion {\n id: string;\n label?: string;\n blocks: NestedBlock[];\n}\n\nexport interface BlockContainerSpec<TData> {\n regions: (data: TData) => BlockContainerRegion[];\n updateRegion: (data: TData, regionId: string, blocks: NestedBlock[]) => TData;\n addRegion?: (data: TData, afterRegionId?: string) => TData;\n removeRegion?: (data: TData, regionId: string) => TData;\n reorderRegion?: (\n data: TData,\n fromRegionId: string,\n toRegionId: string,\n ) => TData;\n}\n\nexport type BlockDataChangeMeta = {\n containerRegion?: {\n regionId: string;\n blocks: NestedBlock[];\n };\n};\n\n/** Props passed to a block's read-only renderer. */\nexport interface BlockReadProps<TData> {\n data: TData;\n /** Stable block id (for anchors, comment targeting, source patches). */\n blockId: string;\n /** Block heading, when present. */\n title?: string;\n /** Block trailing summary, when present. */\n summary?: string;\n /** Injected app capabilities. */\n ctx: BlockRenderContext;\n}\n\n/** Props passed to a block's editor (custom or schema-generated). */\nexport interface BlockEditProps<TData> {\n data: TData;\n onChange: (next: TData, meta?: BlockDataChangeMeta) => void;\n editable: boolean;\n blockId: string;\n title?: string;\n summary?: string;\n /** Injected app capabilities. */\n ctx: BlockRenderContext;\n}\n\nexport interface BlockSpec<TData = unknown> {\n /** Discriminator. Equals the runtime block `type`. */\n type: string;\n /** Zod schema for `data`. Drives validation AND the schema-auto-editor. */\n schema: ZodType<TData>;\n /** MDX round-trip config. */\n mdx: BlockMdxConfig<TData>;\n /** Read-only renderer (replaces a `PlanBlockView` switch branch / NodeView). */\n Read: FC<BlockReadProps<TData>>;\n /**\n * Optional editor. When omitted, the registry renders the schema-driven\n * `SchemaBlockEditor` generated from `schema`. Supply for full control\n * (wireframe canvas, diagram editor).\n */\n Edit?: FC<BlockEditProps<TData>>;\n /** Allowed placements: `[\"block\"]`, `[\"inline\"]`, or both. */\n placement: BlockPlacement[];\n /**\n * When `true`, this block's data maps to a Notion-Flavored-Markdown (NFM)\n * analog and therefore round-trips into a Notion page. Apps can derive\n * registry-backed Notion allowlists with\n * {@link BlockRegistry.notionCompatibleTypes} instead of hand-maintaining\n * per-app sets. Set it on registry-atom blocks with an NFM counterpart\n * (checklist, table); leave it `false`/undefined on dev-doc blocks\n * (api-endpoint, openapi-spec, data-model, diff, file-tree, json-explorer,\n * annotated-code, mermaid, custom-html, tabs, code-tabs) and visual/plan-only\n * blocks (wireframe, diagram). Prose blocks that aren't registry atoms\n * (rich-text, callout) carry their NFM analog through the prose path, not this\n * flag.\n */\n notionCompatible?: boolean;\n /**\n * How the block is edited in a `block`-placed document:\n * - `\"inline\"` — the `Edit`/auto-form renders in place for direct\n * manipulation of authored content (prose, checklist text, table cells,\n * code bodies). Schema-ish metadata such as tone/type, tab labels,\n * language, density, or structural settings should still be tucked behind a\n * contextual edit/settings affordance inside the custom `Edit`.\n * - `\"panel\"` — the block shows its rendered `Read` view with a corner edit\n * button that opens the `Edit`/auto-form in an app-provided panel (popover).\n * Best for config-driven blocks whose render differs from their props\n * (custom HTML, charts, any user-registered block).\n * - `\"container\"` — the block renders its `Edit` in place, and that editor\n * may call `ctx.renderBlocksEditor` for nested block regions with normal\n * slash commands and nested structured blocks.\n * Defaults to `\"inline\"` when a custom `Edit` is supplied, else `\"panel\"`\n * (auto-form blocks are property forms, ideal for a panel). The app must wire\n * `ctx.renderEditSurface` for `\"panel\"` to take effect; otherwise it falls\n * back to inline.\n */\n editSurface?: \"inline\" | \"panel\" | \"container\";\n /**\n * Optional generic contract for content-bearing container blocks. Keep this\n * runtime-oriented: it describes editable regions over normalized block arrays;\n * source formats can provide readable nested MDX adapters independently.\n */\n container?: BlockContainerSpec<TData>;\n /** Human label for menus + agent schema export. */\n label: string;\n /** Tabler icon component for UI menus (never emoji/robot/sparkle). */\n icon?: FC<{ size?: number; className?: string }>;\n /** One-line description for the agent schema export. */\n description: string;\n /** Optional default `data` factory for slash-menu insertion (an empty block). */\n empty?: () => TData;\n /**\n * Optional block-specific source-patch handlers, generalizing bespoke ops\n * like `update-custom-html`. Keyed by op name; the registry dispatches a\n * matching patch op here. Generic ops (`update-block` shallow-merge) need none.\n */\n patches?: Record<string, (data: TData, op: Record<string, unknown>) => TData>;\n}\n\n/** Identity helper for authoring a spec with full type inference. */\nexport function defineBlock<TData>(spec: BlockSpec<TData>): BlockSpec<TData> {\n return spec;\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"DragHandle.d.ts","sourceRoot":"","sources":["../../../src/client/rich-markdown-editor/DragHandle.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAQzC,OAAO,KAAK,EAAE,IAAI,IAAI,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAChE,OAAO,EAAE,KAAK,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAElD;;;;;;;;GAQG;AACH,eAAO,MAAM,oCAAoC,2BAA2B,CAAC;AAE7E,MAAM,WAAW,iBAAiB;IAChC;;;;;;;;OAQG;IACH,eAAe,EAAE,MAAM,CAAC;IACxB;;;;;OAKG;IACH,mBAAmB,CAAC,EAAE,CAAC,OAAO,EAAE;QAC9B,IAAI,EAAE,UAAU,CAAC;QACjB,IAAI,EAAE,eAAe,CAAC;QACtB,GAAG,EAAE,MAAM,CAAC;KACb,KAAK,OAAO,CAAC;IACd;;;;OAIG;IACH,uBAAuB,CAAC,EAAE,CACxB,IAAI,EAAE,OAAO,EACb,OAAO,EAAE;QACP,IAAI,EAAE,UAAU,CAAC;QACjB,IAAI,EAAE,eAAe,CAAC;QACtB,GAAG,EAAE,MAAM,CAAC;QACZ,UAAU,EAAE,UAAU,CAAC;KACxB,KACE,IAAI,CAAC;IACV;;;;;;OAMG;IACH,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,OAAO,EAAE,OAAO,EAAE,qBAAqB,KAAK,OAAO,CAAC;CACzE;AAuBD,MAAM,MAAM,uBAAuB,GAAG,QAAQ,GAAG,OAAO,GAAG,MAAM,GAAG,OAAO,CAAC;AAE5E,MAAM,MAAM,qBAAqB,GAAG;IAClC,IAAI,EAAE,UAAU,CAAC;IACjB,UAAU,EAAE,UAAU,CAAC;IACvB,UAAU,EAAE,eAAe,CAAC;IAC5B,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc,EAAE,MAAM,CAAC;IACvB,UAAU,EAAE,eAAe,CAAC;IAC5B,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc,EAAE,MAAM,CAAC;IACvB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,uBAAuB,CAAC;CACpC,CAAC;AA4PF;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,eAAO,MAAM,UAAU,mCAi5BrB,CAAC"}
1
+ {"version":3,"file":"DragHandle.d.ts","sourceRoot":"","sources":["../../../src/client/rich-markdown-editor/DragHandle.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAQzC,OAAO,KAAK,EAAE,IAAI,IAAI,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAChE,OAAO,EAAE,KAAK,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAElD;;;;;;;;GAQG;AACH,eAAO,MAAM,oCAAoC,2BAA2B,CAAC;AAE7E,MAAM,WAAW,iBAAiB;IAChC;;;;;;;;OAQG;IACH,eAAe,EAAE,MAAM,CAAC;IACxB;;;;;OAKG;IACH,mBAAmB,CAAC,EAAE,CAAC,OAAO,EAAE;QAC9B,IAAI,EAAE,UAAU,CAAC;QACjB,IAAI,EAAE,eAAe,CAAC;QACtB,GAAG,EAAE,MAAM,CAAC;KACb,KAAK,OAAO,CAAC;IACd;;;;OAIG;IACH,uBAAuB,CAAC,EAAE,CACxB,IAAI,EAAE,OAAO,EACb,OAAO,EAAE;QACP,IAAI,EAAE,UAAU,CAAC;QACjB,IAAI,EAAE,eAAe,CAAC;QACtB,GAAG,EAAE,MAAM,CAAC;QACZ,UAAU,EAAE,UAAU,CAAC;KACxB,KACE,IAAI,CAAC;IACV;;;;;;OAMG;IACH,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,OAAO,EAAE,OAAO,EAAE,qBAAqB,KAAK,OAAO,CAAC;CACzE;AAqCD,MAAM,MAAM,uBAAuB,GAAG,QAAQ,GAAG,OAAO,GAAG,MAAM,GAAG,OAAO,CAAC;AAE5E,MAAM,MAAM,qBAAqB,GAAG;IAClC,IAAI,EAAE,UAAU,CAAC;IACjB,UAAU,EAAE,UAAU,CAAC;IACvB,UAAU,EAAE,eAAe,CAAC;IAC5B,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc,EAAE,MAAM,CAAC;IACvB,UAAU,EAAE,eAAe,CAAC;IAC5B,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc,EAAE,MAAM,CAAC;IACvB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,uBAAuB,CAAC;CACpC,CAAC;AAiWF;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,eAAO,MAAM,UAAU,mCA67BrB,CAAC"}
@@ -12,9 +12,23 @@ import { Plugin, PluginKey, NodeSelection, TextSelection, } from "@tiptap/pm/sta
12
12
  export const DEFAULT_DRAG_HANDLE_WRAPPER_SELECTOR = ".visual-editor-wrapper";
13
13
  const dragHandleKey = new PluginKey("dragHandle");
14
14
  const HOVER_SIDE_OUTSET_REM = 8;
15
- const SIDE_DROP_ZONE_RATIO = 0.28;
16
- const SIDE_DROP_ZONE_MIN_PX = 48;
17
- const SIDE_DROP_ZONE_MAX_PX = 140;
15
+ // Notion-style side drop: drag a block to a neighbour's LEFT/RIGHT region and it
16
+ // builds (or joins) a column layout instead of reordering. The activation region
17
+ // has to be GENEROUS or the gesture is dead for a real human — a natural drag
18
+ // releases somewhere over the block's body, nowhere near a thin edge sliver. The
19
+ // old values (28% of width, capped at 140px, AND only the vertical middle 60%)
20
+ // left a wide ~820px plan block with two ~17%-of-width edge slivers in a 35px-tall
21
+ // band as the ONLY column targets — ~66% of the block (the whole centre) plus the
22
+ // top/bottom only ever reordered, so "drag side by side" essentially never made
23
+ // columns. Now each side claims ~a third of the width across the FULL block
24
+ // height, with a middle band always preserved for before/after reorder.
25
+ const SIDE_DROP_ZONE_RATIO = 0.33;
26
+ const SIDE_DROP_ZONE_MIN_PX = 56;
27
+ const SIDE_DROP_ZONE_MAX_PX = 320;
28
+ // Never let the two side zones swallow the whole block: keep at least the middle
29
+ // ~10% of the width as the before/after reorder band so dropping over the centre
30
+ // still moves the block above/below the target (Notion keeps reorder reachable).
31
+ const SIDE_DROP_ZONE_MAX_WIDTH_FRACTION = 0.45;
18
32
  const DRAG_HANDLE_MENU_STYLE_ID = "an-rich-md-drag-menu-styles";
19
33
  const DRAG_HANDLE_MENU_WIDTH = 220;
20
34
  const DRAG_HANDLE_MENU_GAP = 6;
@@ -22,6 +36,12 @@ const DRAG_HANDLE_MENU_VIEWPORT_PADDING = 8;
22
36
  const dragHandleRegistrations = new Set();
23
37
  let dragHandleGlobalHoverListeners = 0;
24
38
  let activeDragRegistration = null;
39
+ // The registration whose grip is currently shown. Used to keep that grip alive
40
+ // while the cursor travels from a block's body to its grip, even when the grip
41
+ // sits in a contested gap (an inter-column gap or a tab body's left offset)
42
+ // where another editor's wide forgiving zone would otherwise re-win the hover
43
+ // and hide the grip out from under the approaching cursor.
44
+ let activeHoverRegistration = null;
25
45
  const clamp = (value, min, max) => Math.min(Math.max(value, min), max);
26
46
  const editorArea = (registration) => {
27
47
  const rect = registration.view.dom.getBoundingClientRect();
@@ -32,6 +52,7 @@ const updateRegisteredHover = (clientX, clientY) => {
32
52
  for (const registration of dragHandleRegistrations) {
33
53
  registration.hideHover?.();
34
54
  }
55
+ activeHoverRegistration = null;
35
56
  return;
36
57
  }
37
58
  const candidates = [];
@@ -48,13 +69,88 @@ const updateRegisteredHover = (clientX, clientY) => {
48
69
  registration.hideHover?.();
49
70
  }
50
71
  }
51
- candidates.sort((a, b) => editorArea(a.registration) - editorArea(b.registration));
52
- const active = candidates[0] ?? null;
72
+ // Grip keepalive. Once a block's grip is showing, hold it while the cursor
73
+ // travels LEFT of that block's content toward its grip glyph — within the
74
+ // block's own vertical row and no further left than the glyph itself. This is
75
+ // what makes grips grabbable for blocks that are NOT flush with the page's
76
+ // left gutter (a right column, a tab body): their grip sits in a gap that the
77
+ // neighbour's wide forgiving zone also claims, so the normal picker would flip
78
+ // hover to the neighbour mid-approach and the grip would vanish before the
79
+ // cursor reaches it. The keepalive only bridges the body→grip gap — it does
80
+ // NOT fire while the cursor is over content (so the innermost/nested picking
81
+ // and gutter-grab rules below still decide there) and the row guard stops it
82
+ // from sticking the grip across vertical moves to another block's row.
83
+ if (activeHoverRegistration) {
84
+ const held = candidates.find((candidate) => candidate.registration === activeHoverRegistration);
85
+ const grip = activeHoverRegistration.gripRect?.();
86
+ if (held &&
87
+ grip &&
88
+ clientY >= held.block.rect.top &&
89
+ clientY < held.block.rect.bottom &&
90
+ clientX >= grip.left - 4 &&
91
+ clientX < held.block.rect.left) {
92
+ for (const registration of dragHandleRegistrations) {
93
+ if (registration !== held.registration)
94
+ registration.hideHover?.();
95
+ }
96
+ held.registration.showHoverBlock?.(held.block);
97
+ return;
98
+ }
99
+ }
100
+ // Pick which editor owns the grip when several register a hover block at this
101
+ // point. Nested region editors (e.g. each column inside a `columns` block) tile
102
+ // their container's whole footprint AND extend a wide forgiving zone
103
+ // (HOVER_SIDE_OUTSET_REM) into its left-margin gutter, so a pure
104
+ // "smallest editor wins" rule lets an inner block beat the container everywhere
105
+ // and leaves the container itself impossible to grab. Split candidates by where
106
+ // the cursor sits relative to each block:
107
+ // - Over a block's body (clientX at/after its left edge) the innermost
108
+ // (smallest) editor wins, so nested blocks stay grabbable from their content.
109
+ // - In the shared left-margin gutter (clientX left of every candidate's
110
+ // content, where the grip lives) the outermost (largest) editor wins, so the
111
+ // container block can be picked up and reordered.
112
+ // Prefer the candidate whose block actually sits UNDER the cursor
113
+ // horizontally. Without this, a left column's forgiving side zone reaches
114
+ // across the inter-column gap, ties the right column's editor on area, and
115
+ // wins — so hovering a right-column block shows the grip for the LEFT block
116
+ // (and right-column blocks appear to have no grip at all). `overContent`
117
+ // restricts to blocks the cursor is genuinely within; `rightOfLeftEdge` keeps
118
+ // the gutter-grab behaviour; fully left of every block → the container wins.
119
+ const overContent = candidates.filter((candidate) => clientX >= candidate.block.rect.left &&
120
+ clientX <= candidate.block.rect.right);
121
+ // The grip renders in a narrow band just LEFT of each block (≈24px). A block
122
+ // must OWN that band so moving the cursor onto its grip keeps showing (and
123
+ // lets you press) that block's grip — otherwise, for a column block whose grip
124
+ // sits in the gutter/inter-column gap, the "gutter → largest editor" rule
125
+ // below would flip the hover to the columns container and the grip would
126
+ // vanish out from under the cursor, making inner column blocks impossible to
127
+ // drag. The band is narrow, so it does not collide with the neighbouring
128
+ // column's content (the right column's grip lives in the inter-column gap,
129
+ // left of its own content but right of the left column's content).
130
+ const GRIP_HOVER_ZONE_PX = 28;
131
+ const overGrip = candidates.filter((candidate) => clientX >= candidate.block.rect.left - GRIP_HOVER_ZONE_PX &&
132
+ clientX < candidate.block.rect.left);
133
+ const rightOfLeftEdge = candidates.filter((candidate) => clientX >= candidate.block.rect.left);
134
+ let active;
135
+ const innerPool = overContent.length > 0
136
+ ? overContent
137
+ : overGrip.length > 0
138
+ ? overGrip
139
+ : rightOfLeftEdge;
140
+ if (innerPool.length > 0) {
141
+ innerPool.sort((a, b) => editorArea(a.registration) - editorArea(b.registration));
142
+ active = innerPool[0];
143
+ }
144
+ else {
145
+ candidates.sort((a, b) => editorArea(b.registration) - editorArea(a.registration));
146
+ active = candidates[0] ?? null;
147
+ }
53
148
  for (const registration of dragHandleRegistrations) {
54
149
  if (registration !== active?.registration)
55
150
  registration.hideHover?.();
56
151
  }
57
152
  active?.registration.showHoverBlock?.(active.block);
153
+ activeHoverRegistration = active?.registration ?? null;
58
154
  };
59
155
  const handleGlobalHoverMove = (event) => {
60
156
  updateRegisteredHover(event.clientX, event.clientY);
@@ -567,18 +663,17 @@ export const DragHandle = Extension.create({
567
663
  return null;
568
664
  let placement;
569
665
  const withinBlockY = clientY >= block.rect.top && clientY <= block.rect.bottom;
570
- const withinSideDropBand = clientY >= block.rect.top + block.rect.height * 0.2 &&
571
- clientY <= block.rect.bottom - block.rect.height * 0.2;
572
- const sideZoneWidth = clamp(block.rect.width * SIDE_DROP_ZONE_RATIO, SIDE_DROP_ZONE_MIN_PX, SIDE_DROP_ZONE_MAX_PX);
666
+ // Side (column) zones span the FULL block height only the horizontal
667
+ // position decides column-vs-reorder. Restricting to the vertical middle
668
+ // (the old 0.2 band) made the already-tiny edge slivers nearly unhittable.
669
+ const sideZoneWidth = Math.min(clamp(block.rect.width * SIDE_DROP_ZONE_RATIO, SIDE_DROP_ZONE_MIN_PX, SIDE_DROP_ZONE_MAX_PX), block.rect.width * SIDE_DROP_ZONE_MAX_WIDTH_FRACTION);
573
670
  if (registration.handleDrop &&
574
671
  withinBlockY &&
575
- withinSideDropBand &&
576
672
  clientX <= block.rect.left + sideZoneWidth) {
577
673
  placement = "left";
578
674
  }
579
675
  else if (registration.handleDrop &&
580
676
  withinBlockY &&
581
- withinSideDropBand &&
582
677
  clientX >= block.rect.right - sideZoneWidth) {
583
678
  placement = "right";
584
679
  }
@@ -629,11 +724,20 @@ export const DragHandle = Extension.create({
629
724
  };
630
725
  const updateDropLine = (session, target) => {
631
726
  const sourceEnd = session.sourcePos + session.sourceNodeSize;
727
+ const isSideDrop = target?.placement === "left" || target?.placement === "right";
632
728
  if (!target ||
633
729
  (target.view === session.view &&
634
- (target.pos === session.sourcePos ||
635
- target.pos === sourceEnd ||
636
- (target.pos > session.sourcePos && target.pos < sourceEnd)))) {
730
+ (isSideDrop
731
+ ? // A side drop only ever builds columns; the ProseMirror seam
732
+ // position is irrelevant. The only no-op is dropping a block on
733
+ // ITS OWN side — adjacent *different* blocks must still form
734
+ // columns (otherwise dropping onto an immediate neighbour's
735
+ // facing edge silently does nothing, which reads as "side drop
736
+ // works sometimes").
737
+ target.targetPos === session.sourcePos
738
+ : target.pos === session.sourcePos ||
739
+ target.pos === sourceEnd ||
740
+ (target.pos > session.sourcePos && target.pos < sourceEnd)))) {
637
741
  session.dropTarget = null;
638
742
  session.dropLine?.remove();
639
743
  session.dropLine = null;
@@ -651,11 +755,21 @@ export const DragHandle = Extension.create({
651
755
  const wrapperRect = wrapper.getBoundingClientRect();
652
756
  const editorRect = target.view.dom.getBoundingClientRect();
653
757
  session.dropTarget = target;
654
- if (target.placement === "left" || target.placement === "right") {
655
- const left = target.placement === "left" ? target.rect.left : target.rect.right;
656
- session.dropLine.style.left = `${left - wrapperRect.left}px`;
758
+ // A column (side) drop and a reorder (before/after) drop both draw the
759
+ // `.notion-drop-indicator`, but they mean very different things, so the
760
+ // column case carries a modifier class apps style distinctly (a bolder,
761
+ // glowing vertical bar) — without a clear cue a human can't tell they've
762
+ // entered column-build mode before releasing.
763
+ const isColumnDrop = target.placement === "left" || target.placement === "right";
764
+ session.dropLine.classList.toggle("notion-drop-indicator--column", isColumnDrop);
765
+ if (isColumnDrop) {
766
+ // A vertical bar centred on the seam at the target's left/right edge,
767
+ // spanning the block's full height.
768
+ const SIDE_BAR_WIDTH = 4;
769
+ const seam = target.placement === "left" ? target.rect.left : target.rect.right;
770
+ session.dropLine.style.left = `${seam - wrapperRect.left - SIDE_BAR_WIDTH / 2}px`;
657
771
  session.dropLine.style.top = `${target.rect.top - wrapperRect.top}px`;
658
- session.dropLine.style.width = "3px";
772
+ session.dropLine.style.width = `${SIDE_BAR_WIDTH}px`;
659
773
  session.dropLine.style.height = `${target.rect.height}px`;
660
774
  return;
661
775
  }
@@ -676,7 +790,13 @@ export const DragHandle = Extension.create({
676
790
  el.setAttribute("aria-haspopup", "menu");
677
791
  el.setAttribute("aria-expanded", "false");
678
792
  el.title = "Open block menu or drag to reorder";
679
- el.innerHTML = `<svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor">
793
+ // The icon must not be its own hit target: a real mouse-down inside a
794
+ // nested editor (a column) lands on the SVG, and a container block's
795
+ // capture-phase block-select handler (RegistryBlockNode) only spares the
796
+ // grip DIV — so a press on the icon gets swallowed and the block can't be
797
+ // dragged out of / between columns. `pointer-events:none` makes every
798
+ // press in the grip area resolve to the DIV instead.
799
+ el.innerHTML = `<svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor" style="pointer-events:none">
680
800
  <circle cx="5.5" cy="3" r="1.5"/><circle cx="10.5" cy="3" r="1.5"/>
681
801
  <circle cx="5.5" cy="8" r="1.5"/><circle cx="10.5" cy="8" r="1.5"/>
682
802
  <circle cx="5.5" cy="13" r="1.5"/><circle cx="10.5" cy="13" r="1.5"/>
@@ -748,10 +868,15 @@ export const DragHandle = Extension.create({
748
868
  const sourceEnd = session.sourcePos + session.sourceNodeSize;
749
869
  const target = session.dropTarget;
750
870
  const dropPos = target.pos;
871
+ const isSideDrop = target.placement === "left" || target.placement === "right";
751
872
  if (target.view !== session.view ||
752
- (dropPos !== sourceStart &&
753
- dropPos !== sourceEnd &&
754
- !(dropPos > sourceStart && dropPos < sourceEnd))) {
873
+ (isSideDrop
874
+ ? // Side drop (column build): proceed for any block that isn't the
875
+ // source itself, including the source's immediate neighbour.
876
+ target.targetPos !== sourceStart
877
+ : dropPos !== sourceStart &&
878
+ dropPos !== sourceEnd &&
879
+ !(dropPos > sourceStart && dropPos < sourceEnd))) {
755
880
  const sourceNode = session.view.state.doc.nodeAt(sourceStart);
756
881
  if (sourceNode) {
757
882
  const sourceRegistration = registrationForView(session.view);
@@ -873,6 +998,9 @@ export const DragHandle = Extension.create({
873
998
  findHoverBlock: (clientX, clientY) => findForgivingBlock(editorView, clientX, clientY),
874
999
  showHoverBlock: (block) => showHandleForBlock(editorView, block),
875
1000
  hideHover: () => hideHandle(),
1001
+ gripRect: () => handle && handle.style.display !== "none"
1002
+ ? handle.getBoundingClientRect()
1003
+ : null,
876
1004
  };
877
1005
  currentRegistration = registration;
878
1006
  dragHandleRegistrations.add(registration);
@@ -944,6 +1072,9 @@ export const DragHandle = Extension.create({
944
1072
  if (activeDragRegistration === registration) {
945
1073
  activeDragRegistration = null;
946
1074
  }
1075
+ if (activeHoverRegistration === registration) {
1076
+ activeHoverRegistration = null;
1077
+ }
947
1078
  handle?.remove();
948
1079
  handle = null;
949
1080
  currentRegistration = null;