@agent-native/core 0.42.0 → 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 (110) hide show
  1. package/README.md +17 -56
  2. package/dist/cli/recap.d.ts.map +1 -1
  3. package/dist/cli/recap.js +24 -13
  4. package/dist/cli/recap.js.map +1 -1
  5. package/dist/cli/skills.d.ts +2 -6
  6. package/dist/cli/skills.d.ts.map +1 -1
  7. package/dist/cli/skills.js +8 -66
  8. package/dist/cli/skills.js.map +1 -1
  9. package/dist/client/blocks/index.d.ts +11 -0
  10. package/dist/client/blocks/index.d.ts.map +1 -1
  11. package/dist/client/blocks/index.js +11 -0
  12. package/dist/client/blocks/index.js.map +1 -1
  13. package/dist/client/blocks/library/AnnotatedCodeBlock.d.ts.map +1 -1
  14. package/dist/client/blocks/library/AnnotatedCodeBlock.js +2 -2
  15. package/dist/client/blocks/library/AnnotatedCodeBlock.js.map +1 -1
  16. package/dist/client/blocks/library/DiffBlock.d.ts.map +1 -1
  17. package/dist/client/blocks/library/DiffBlock.js +86 -21
  18. package/dist/client/blocks/library/DiffBlock.js.map +1 -1
  19. package/dist/client/blocks/library/FileTreeBlock.d.ts.map +1 -1
  20. package/dist/client/blocks/library/FileTreeBlock.js +27 -4
  21. package/dist/client/blocks/library/FileTreeBlock.js.map +1 -1
  22. package/dist/client/blocks/library/JsonExplorerBlock.js +1 -1
  23. package/dist/client/blocks/library/JsonExplorerBlock.js.map +1 -1
  24. package/dist/client/blocks/library/MermaidBlock.js +1 -1
  25. package/dist/client/blocks/library/MermaidBlock.js.map +1 -1
  26. package/dist/client/blocks/library/annotation-rail.d.ts +19 -0
  27. package/dist/client/blocks/library/annotation-rail.d.ts.map +1 -1
  28. package/dist/client/blocks/library/annotation-rail.js +19 -0
  29. package/dist/client/blocks/library/annotation-rail.js.map +1 -1
  30. package/dist/client/blocks/library/callout.config.d.ts +29 -0
  31. package/dist/client/blocks/library/callout.config.d.ts.map +1 -0
  32. package/dist/client/blocks/library/callout.config.js +33 -0
  33. package/dist/client/blocks/library/callout.config.js.map +1 -0
  34. package/dist/client/blocks/library/callout.d.ts +20 -0
  35. package/dist/client/blocks/library/callout.d.ts.map +1 -0
  36. package/dist/client/blocks/library/callout.js +61 -0
  37. package/dist/client/blocks/library/callout.js.map +1 -0
  38. package/dist/client/blocks/library/checklist.d.ts.map +1 -1
  39. package/dist/client/blocks/library/checklist.js +3 -3
  40. package/dist/client/blocks/library/checklist.js.map +1 -1
  41. package/dist/client/blocks/library/decision.config.d.ts +37 -0
  42. package/dist/client/blocks/library/decision.config.d.ts.map +1 -0
  43. package/dist/client/blocks/library/decision.config.js +32 -0
  44. package/dist/client/blocks/library/decision.config.js.map +1 -0
  45. package/dist/client/blocks/library/decision.d.ts +19 -0
  46. package/dist/client/blocks/library/decision.d.ts.map +1 -0
  47. package/dist/client/blocks/library/decision.js +119 -0
  48. package/dist/client/blocks/library/decision.js.map +1 -0
  49. package/dist/client/blocks/library/diagram.config.d.ts +64 -0
  50. package/dist/client/blocks/library/diagram.config.d.ts.map +1 -0
  51. package/dist/client/blocks/library/diagram.config.js +111 -0
  52. package/dist/client/blocks/library/diagram.config.js.map +1 -0
  53. package/dist/client/blocks/library/diagram.d.ts +16 -0
  54. package/dist/client/blocks/library/diagram.d.ts.map +1 -0
  55. package/dist/client/blocks/library/diagram.js +261 -0
  56. package/dist/client/blocks/library/diagram.js.map +1 -0
  57. package/dist/client/blocks/library/question-form.config.d.ts +69 -0
  58. package/dist/client/blocks/library/question-form.config.d.ts.map +1 -0
  59. package/dist/client/blocks/library/question-form.config.js +58 -0
  60. package/dist/client/blocks/library/question-form.config.js.map +1 -0
  61. package/dist/client/blocks/library/question-form.d.ts +20 -0
  62. package/dist/client/blocks/library/question-form.d.ts.map +1 -0
  63. package/dist/client/blocks/library/question-form.js +286 -0
  64. package/dist/client/blocks/library/question-form.js.map +1 -0
  65. package/dist/client/blocks/library/sanitize-html.d.ts +5 -0
  66. package/dist/client/blocks/library/sanitize-html.d.ts.map +1 -0
  67. package/dist/client/blocks/library/sanitize-html.js +240 -0
  68. package/dist/client/blocks/library/sanitize-html.js.map +1 -0
  69. package/dist/client/blocks/library/server-specs.d.ts.map +1 -1
  70. package/dist/client/blocks/library/server-specs.js +59 -0
  71. package/dist/client/blocks/library/server-specs.js.map +1 -1
  72. package/dist/client/blocks/library/specs.d.ts.map +1 -1
  73. package/dist/client/blocks/library/specs.js +11 -0
  74. package/dist/client/blocks/library/specs.js.map +1 -1
  75. package/dist/client/blocks/library/tabs.d.ts.map +1 -1
  76. package/dist/client/blocks/library/tabs.js +12 -12
  77. package/dist/client/blocks/library/tabs.js.map +1 -1
  78. package/dist/client/blocks/library/wireframe-kit.d.ts +260 -0
  79. package/dist/client/blocks/library/wireframe-kit.d.ts.map +1 -0
  80. package/dist/client/blocks/library/wireframe-kit.js +920 -0
  81. package/dist/client/blocks/library/wireframe-kit.js.map +1 -0
  82. package/dist/client/blocks/library/wireframe.config.d.ts +123 -0
  83. package/dist/client/blocks/library/wireframe.config.d.ts.map +1 -0
  84. package/dist/client/blocks/library/wireframe.config.js +294 -0
  85. package/dist/client/blocks/library/wireframe.config.js.map +1 -0
  86. package/dist/client/blocks/library/wireframe.d.ts +15 -0
  87. package/dist/client/blocks/library/wireframe.d.ts.map +1 -0
  88. package/dist/client/blocks/library/wireframe.js +206 -0
  89. package/dist/client/blocks/library/wireframe.js.map +1 -0
  90. package/dist/client/blocks/registry.d.ts +9 -0
  91. package/dist/client/blocks/registry.d.ts.map +1 -1
  92. package/dist/client/blocks/registry.js +12 -5
  93. package/dist/client/blocks/registry.js.map +1 -1
  94. package/dist/client/blocks/server.d.ts +1 -0
  95. package/dist/client/blocks/server.d.ts.map +1 -1
  96. package/dist/client/blocks/server.js +1 -0
  97. package/dist/client/blocks/server.js.map +1 -1
  98. package/dist/client/blocks/types.d.ts +8 -0
  99. package/dist/client/blocks/types.d.ts.map +1 -1
  100. package/dist/client/blocks/types.js.map +1 -1
  101. package/dist/client/rich-markdown-editor/DragHandle.d.ts.map +1 -1
  102. package/dist/client/rich-markdown-editor/DragHandle.js +77 -12
  103. package/dist/client/rich-markdown-editor/DragHandle.js.map +1 -1
  104. package/dist/styles/agent-native.css +1 -0
  105. package/dist/styles/blocks.css +1380 -0
  106. package/docs/content/plan-plugin.md +8 -8
  107. package/docs/content/pr-visual-recap.md +2 -2
  108. package/docs/content/template-plan.md +94 -17
  109. package/package.json +2 -1
  110. package/docs/content/visual-plans.md +0 -82
@@ -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-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\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;AAuTF;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,eAAO,MAAM,UAAU,mCAu6BrB,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,6 +69,34 @@ const updateRegisteredHover = (clientX, clientY) => {
48
69
  registration.hideHover?.();
49
70
  }
50
71
  }
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
+ }
51
100
  // Pick which editor owns the grip when several register a hover block at this
52
101
  // point. Nested region editors (e.g. each column inside a `columns` block) tile
53
102
  // their container's whole footprint AND extend a wide forgiving zone
@@ -101,6 +150,7 @@ const updateRegisteredHover = (clientX, clientY) => {
101
150
  registration.hideHover?.();
102
151
  }
103
152
  active?.registration.showHoverBlock?.(active.block);
153
+ activeHoverRegistration = active?.registration ?? null;
104
154
  };
105
155
  const handleGlobalHoverMove = (event) => {
106
156
  updateRegisteredHover(event.clientX, event.clientY);
@@ -613,18 +663,17 @@ export const DragHandle = Extension.create({
613
663
  return null;
614
664
  let placement;
615
665
  const withinBlockY = clientY >= block.rect.top && clientY <= block.rect.bottom;
616
- const withinSideDropBand = clientY >= block.rect.top + block.rect.height * 0.2 &&
617
- clientY <= block.rect.bottom - block.rect.height * 0.2;
618
- 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);
619
670
  if (registration.handleDrop &&
620
671
  withinBlockY &&
621
- withinSideDropBand &&
622
672
  clientX <= block.rect.left + sideZoneWidth) {
623
673
  placement = "left";
624
674
  }
625
675
  else if (registration.handleDrop &&
626
676
  withinBlockY &&
627
- withinSideDropBand &&
628
677
  clientX >= block.rect.right - sideZoneWidth) {
629
678
  placement = "right";
630
679
  }
@@ -706,11 +755,21 @@ export const DragHandle = Extension.create({
706
755
  const wrapperRect = wrapper.getBoundingClientRect();
707
756
  const editorRect = target.view.dom.getBoundingClientRect();
708
757
  session.dropTarget = target;
709
- if (target.placement === "left" || target.placement === "right") {
710
- const left = target.placement === "left" ? target.rect.left : target.rect.right;
711
- 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`;
712
771
  session.dropLine.style.top = `${target.rect.top - wrapperRect.top}px`;
713
- session.dropLine.style.width = "3px";
772
+ session.dropLine.style.width = `${SIDE_BAR_WIDTH}px`;
714
773
  session.dropLine.style.height = `${target.rect.height}px`;
715
774
  return;
716
775
  }
@@ -939,6 +998,9 @@ export const DragHandle = Extension.create({
939
998
  findHoverBlock: (clientX, clientY) => findForgivingBlock(editorView, clientX, clientY),
940
999
  showHoverBlock: (block) => showHandleForBlock(editorView, block),
941
1000
  hideHover: () => hideHandle(),
1001
+ gripRect: () => handle && handle.style.display !== "none"
1002
+ ? handle.getBoundingClientRect()
1003
+ : null,
942
1004
  };
943
1005
  currentRegistration = registration;
944
1006
  dragHandleRegistrations.add(registration);
@@ -1010,6 +1072,9 @@ export const DragHandle = Extension.create({
1010
1072
  if (activeDragRegistration === registration) {
1011
1073
  activeDragRegistration = null;
1012
1074
  }
1075
+ if (activeHoverRegistration === registration) {
1076
+ activeHoverRegistration = null;
1077
+ }
1013
1078
  handle?.remove();
1014
1079
  handle = null;
1015
1080
  currentRegistration = null;