@djangocfg/ui-tools 2.1.431 → 2.1.432

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 (171) hide show
  1. package/package.json +28 -14
  2. package/src/common/CodePanelHeader/CodePanelHeader.tsx +121 -0
  3. package/src/common/CodePanelHeader/index.ts +1 -0
  4. package/src/common/blocks/BlockError.tsx +46 -0
  5. package/src/common/blocks/index.ts +34 -0
  6. package/src/common/blocks/schemas.ts +143 -0
  7. package/src/common/blocks/types.ts +76 -0
  8. package/src/common/chips/README.md +67 -0
  9. package/src/common/chips/UrlChip.tsx +105 -0
  10. package/src/common/chips/index.ts +20 -0
  11. package/src/common/link-preview/LinkPreviewCard.tsx +178 -0
  12. package/src/common/link-preview/index.ts +12 -0
  13. package/src/common/link-preview/types.ts +44 -0
  14. package/src/common/tiptap/index.ts +10 -0
  15. package/src/common/tiptap/useNodeAttrs.ts +128 -0
  16. package/src/lib/page-snapshot/react/PageSnapshotChip.tsx +1 -1
  17. package/src/tools/chat/README.md +88 -2
  18. package/src/tools/chat/composer/Composer.tsx +45 -12
  19. package/src/tools/chat/composer/ComposerRichTextarea.tsx +16 -4
  20. package/src/tools/chat/constants.ts +9 -2
  21. package/src/tools/chat/core/extractFirstUrl.ts +45 -0
  22. package/src/tools/chat/core/reducer.ts +32 -8
  23. package/src/tools/chat/core/transport/mappers/pydantic-ai.ts +1 -1
  24. package/src/tools/chat/core/transport/mock.ts +4 -1
  25. package/src/tools/chat/hooks/useChat.ts +20 -20
  26. package/src/tools/chat/lazy.tsx +10 -0
  27. package/src/tools/chat/messages/Attachments.tsx +267 -42
  28. package/src/tools/chat/messages/BubbleContextMenu.tsx +203 -0
  29. package/src/tools/chat/messages/MessageBubble.tsx +175 -31
  30. package/src/tools/chat/messages/MessageList.tsx +107 -21
  31. package/src/tools/chat/messages/ToolCalls.tsx +28 -13
  32. package/src/tools/chat/messages/blocks/MessageBlocks.tsx +70 -17
  33. package/src/tools/chat/messages/blocks/builtin.tsx +6 -3
  34. package/src/tools/chat/messages/blocks/renderers/AudioBlock.tsx +6 -0
  35. package/src/tools/chat/messages/blocks/renderers/ChatImageAlbum.tsx +154 -0
  36. package/src/tools/chat/messages/blocks/renderers/CodeBlock.tsx +94 -6
  37. package/src/tools/chat/messages/blocks/renderers/DiffBlock.tsx +29 -0
  38. package/src/tools/chat/messages/blocks/renderers/GalleryBlock.tsx +17 -10
  39. package/src/tools/chat/messages/blocks/renderers/ImageBlock.tsx +37 -16
  40. package/src/tools/chat/messages/blocks/renderers/JsonBlock.tsx +11 -6
  41. package/src/tools/chat/messages/blocks/renderers/LinkBlock.tsx +32 -0
  42. package/src/tools/chat/messages/blocks/renderers/MapBlock.tsx +181 -19
  43. package/src/tools/chat/messages/blocks/renderers/VideoBlock.tsx +2 -5
  44. package/src/tools/chat/messages/index.ts +13 -0
  45. package/src/tools/chat/public.ts +5 -1
  46. package/src/tools/chat/shell/ChatRoot.tsx +29 -0
  47. package/src/tools/chat/styles/bubbleTokens.ts +18 -0
  48. package/src/tools/chat/styles/index.ts +1 -0
  49. package/src/tools/chat/types/attachment.ts +26 -2
  50. package/src/tools/chat/types/block.ts +56 -9
  51. package/src/tools/chat/types/blockSchemas.ts +197 -0
  52. package/src/tools/chat/types/config.ts +33 -0
  53. package/src/tools/chat/types/events.ts +14 -7
  54. package/src/tools/chat/types/index.ts +4 -2
  55. package/src/tools/chat/types/message.ts +7 -0
  56. package/src/tools/data/JsonTree/JsonViewer.tsx +7 -7
  57. package/src/tools/dev/Map/README.md +30 -345
  58. package/src/tools/dev/Map/cards/index.ts +6 -0
  59. package/src/tools/dev/Map/cards/types.ts +67 -0
  60. package/src/tools/dev/Map/components/CompassChip.tsx +67 -0
  61. package/src/tools/dev/Map/components/DraggableMarkers.tsx +79 -0
  62. package/src/tools/dev/Map/components/GeocoderControl.tsx +148 -63
  63. package/src/tools/dev/Map/components/GeocoderInput.tsx +181 -0
  64. package/src/tools/dev/Map/components/MapContainer/FullscreenDialog.tsx +140 -0
  65. package/src/tools/dev/Map/components/MapContainer/GeolocateHandle.tsx +70 -0
  66. package/src/tools/dev/Map/components/MapContainer/MapInner.tsx +326 -0
  67. package/src/tools/dev/Map/components/MapContainer/MapToolbar.tsx +286 -0
  68. package/src/tools/dev/Map/components/MapContainer/RotateHint.tsx +25 -0
  69. package/src/tools/dev/Map/components/MapContainer/ScrollLockOverlay.tsx +52 -0
  70. package/src/tools/dev/Map/components/MapContainer/constants.ts +15 -0
  71. package/src/tools/dev/Map/components/MapContainer/hooks/useAutoReset.ts +51 -0
  72. package/src/tools/dev/Map/components/MapContainer/hooks/useBasemapState.ts +48 -0
  73. package/src/tools/dev/Map/components/MapContainer/hooks/useExternalMapsUrl.ts +29 -0
  74. package/src/tools/dev/Map/components/MapContainer/index.tsx +29 -0
  75. package/src/tools/dev/Map/components/MapContainer/types.ts +198 -0
  76. package/src/tools/dev/Map/components/MapControls.tsx +27 -0
  77. package/src/tools/dev/Map/components/MapMarkers.tsx +94 -0
  78. package/src/tools/dev/Map/components/MarkerCard.tsx +102 -0
  79. package/src/tools/dev/Map/components/MeasureControl.tsx +219 -0
  80. package/src/tools/dev/Map/components/index.ts +35 -1
  81. package/src/tools/dev/Map/geocode/index.ts +2 -0
  82. package/src/tools/dev/Map/geocode/photon.ts +265 -0
  83. package/src/tools/dev/Map/hooks/index.ts +37 -0
  84. package/src/tools/dev/Map/hooks/useCompass.ts +44 -0
  85. package/src/tools/dev/Map/hooks/useDraggableMarkers.ts +112 -0
  86. package/src/tools/dev/Map/hooks/useGeocoder.ts +155 -0
  87. package/src/tools/dev/Map/hooks/useGeolocation.ts +174 -0
  88. package/src/tools/dev/Map/hooks/useMapControl.ts +5 -1
  89. package/src/tools/dev/Map/hooks/useMapImages.ts +112 -0
  90. package/src/tools/dev/Map/hooks/useMapScrollProtection.ts +155 -0
  91. package/src/tools/dev/Map/hooks/useMapTouchMode.ts +21 -0
  92. package/src/tools/dev/Map/hooks/useMarkerCard.ts +39 -0
  93. package/src/tools/dev/Map/hooks/useMarkers.ts +9 -3
  94. package/src/tools/dev/Map/hooks/useMeasure.ts +169 -0
  95. package/src/tools/dev/Map/hooks/useTerrain.ts +123 -0
  96. package/src/tools/dev/Map/index.ts +59 -2
  97. package/src/tools/dev/Map/layers/index.ts +1 -0
  98. package/src/tools/dev/Map/layers/symbol.ts +93 -0
  99. package/src/tools/dev/Map/lazy.tsx +120 -0
  100. package/src/tools/dev/Map/protocols/pmtiles.ts +73 -0
  101. package/src/tools/dev/Map/styles/index.ts +62 -0
  102. package/src/tools/dev/Map/styles/terrain.ts +29 -0
  103. package/src/tools/dev/Map/types.ts +31 -3
  104. package/src/tools/dev/api/OpenapiViewer/components/DocsLayout/EndpointDoc/Header/MetaActions.tsx +1 -1
  105. package/src/tools/dev/api/OpenapiViewer/components/DocsLayout/EndpointDoc/Responses/StatusTag.tsx +5 -12
  106. package/src/tools/dev/api/OpenapiViewer/components/DocsLayout/EndpointDoc/Section/SectionHeader.tsx +1 -1
  107. package/src/tools/dev/api/OpenapiViewer/components/DocsLayout/SchemaCopyMenu.tsx +1 -1
  108. package/src/tools/dev/api/OpenapiViewer/components/shared/SendButton.tsx +1 -1
  109. package/src/tools/dev/api/OpenapiViewer/components/shared/http-colors.ts +61 -0
  110. package/src/tools/dev/api/OpenapiViewer/components/shared/ui.tsx +4 -21
  111. package/src/tools/dev/code/MarkdownMessage/MarkdownMessage.tsx +150 -58
  112. package/src/tools/dev/code/MarkdownMessage/README.md +38 -0
  113. package/src/tools/dev/code/MarkdownMessage/builtinRules.tsx +92 -0
  114. package/src/tools/dev/code/MarkdownMessage/index.ts +3 -0
  115. package/src/tools/dev/code/MarkdownMessage/streamingBlocks.ts +164 -0
  116. package/src/tools/dev/code/MarkdownMessage/types.ts +20 -0
  117. package/src/tools/dev/code/PrettyCode/PrettyCode.client.tsx +27 -30
  118. package/src/tools/dev/code/PrettyCode/index.tsx +4 -1
  119. package/src/tools/dev/code/PrettyCode/lazy.tsx +3 -1
  120. package/src/tools/forms/MarkdownEditor/MarkdownEditor.tsx +168 -3
  121. package/src/tools/forms/MarkdownEditor/README.md +84 -0
  122. package/src/tools/forms/MarkdownEditor/chip/ChipNode.ts +454 -0
  123. package/src/tools/forms/MarkdownEditor/chip/syncChips.ts +80 -0
  124. package/src/tools/forms/MarkdownEditor/filePath/FilePathChip.tsx +68 -0
  125. package/src/tools/forms/MarkdownEditor/filePath/__tests__/detect.test.ts +161 -0
  126. package/src/tools/forms/MarkdownEditor/filePath/detect.ts +289 -0
  127. package/src/tools/forms/MarkdownEditor/filePath/index.ts +15 -0
  128. package/src/tools/forms/MarkdownEditor/index.ts +19 -0
  129. package/src/tools/forms/MarkdownEditor/lazy.tsx +10 -0
  130. package/src/tools/forms/MarkdownEditor/styles.css +134 -2
  131. package/src/tools/forms/MarkdownEditor/url/__tests__/detect.test.ts +121 -0
  132. package/src/tools/forms/MarkdownEditor/url/detect.ts +227 -0
  133. package/src/tools/forms/NotionEditor/BookmarkNode.ts +158 -0
  134. package/src/tools/forms/NotionEditor/BookmarkView.tsx +45 -0
  135. package/src/tools/forms/NotionEditor/MapEditLayer.tsx +199 -0
  136. package/src/tools/forms/NotionEditor/MapNode.ts +218 -0
  137. package/src/tools/forms/NotionEditor/MapView.tsx +103 -0
  138. package/src/tools/forms/NotionEditor/NotionEditor.tsx +7 -2
  139. package/src/tools/forms/NotionEditor/README.md +109 -188
  140. package/src/tools/forms/NotionEditor/extensions.ts +15 -0
  141. package/src/tools/forms/NotionEditor/slashItems.ts +19 -0
  142. package/src/tools/forms/NotionEditor/styles.css +25 -0
  143. package/src/tools/forms/NotionEditor/types.ts +9 -0
  144. package/src/tools/input/SpeechRecognition/core/engine/external.ts +5 -0
  145. package/src/tools/input/SpeechRecognition/types.ts +10 -0
  146. package/src/tools/media/Gallery/components/lightbox/GalleryLightbox.tsx +39 -22
  147. package/src/tools/media/ImageViewer/README.md +9 -0
  148. package/src/tools/media/ImageViewer/components/ImageViewer.tsx +165 -79
  149. package/src/tools/media/ImageViewer/components/LightboxChrome.tsx +137 -0
  150. package/src/tools/media/ImageViewer/components/MiddleEllipsis.tsx +46 -0
  151. package/src/tools/media/ImageViewer/components/index.ts +2 -0
  152. package/src/tools/media/ImageViewer/hooks/index.ts +3 -0
  153. package/src/tools/media/ImageViewer/hooks/useIdleChrome.ts +73 -0
  154. package/src/tools/media/ImageViewer/types.ts +10 -0
  155. package/src/tools/media/ImageViewer/utils/constants.ts +28 -0
  156. package/src/tools/media/ImageViewer/utils/index.ts +4 -0
  157. package/src/tools/media/VideoPlayer/VideoPlayer.tsx +34 -40
  158. package/src/tools/media/VideoPlayer/canvas/youtube-canvas.tsx +13 -13
  159. package/src/tools/media/VideoPlayer/parts/center-play.tsx +26 -0
  160. package/src/tools/media/VideoPlayer/parts/index.ts +2 -0
  161. package/src/tools/media/VideoPlayer/styles/video-player.css +102 -53
  162. package/src/tools/visual/charts/SmoothLine/README.md +97 -0
  163. package/src/tools/visual/charts/SmoothLine/SmoothLine.tsx +177 -0
  164. package/src/tools/visual/charts/SmoothLine/index.ts +21 -0
  165. package/src/tools/visual/charts/SmoothLine/lazy.tsx +23 -0
  166. package/src/tools/visual/charts/SmoothLine/smoothPath.ts +209 -0
  167. package/src/tools/visual/charts/SmoothLine/types.ts +98 -0
  168. package/src/tools/visual/charts/SmoothLine/useSmoothSeries.ts +429 -0
  169. package/src/tools/visual/indicators/Fps/types.ts +1 -1
  170. package/src/tools/chat/messages/blocks/renderers/LottieBlock.tsx +0 -11
  171. package/src/tools/dev/Map/components/MapContainer.tsx +0 -198
@@ -3,9 +3,10 @@
3
3
  Notion-style WYSIWYG markdown editor built on **TipTap v3**.
4
4
 
5
5
  Lazy-loaded sibling of [`MarkdownEditor`](../MarkdownEditor/) — same
6
- markdown round-trip, but with a heavier extension stack and the
7
- floating menus (slash / bubble / drag handle) that users expect from a
8
- "document" editor.
6
+ markdown round-trip, but with a heavier extension stack, the floating
7
+ menus (slash / bubble / drag handle) users expect from a "document"
8
+ editor, **and serializable block nodes** (a link-preview *bookmark*
9
+ card and an interactive *map* block) that round-trip through markdown.
9
10
 
10
11
  ```tsx
11
12
  import { NotionEditor } from '@djangocfg/ui-tools/notion-editor';
@@ -16,84 +17,96 @@ import { NotionEditor } from '@djangocfg/ui-tools/notion-editor';
16
17
  onSave={(md) => writeFile(md)}
17
18
  autoFocus
18
19
  placeholder="Type '/' for commands…"
20
+ resolveLinkPreview={unfurl} // optional — powers bookmark cards
19
21
  />
20
22
  ```
21
23
 
24
+ > **Bundle.** The full stack (lowlight common pack + tables + drag
25
+ > handle, plus the lazy MapLibre chunk when a map block mounts) lands
26
+ > around ~350 KB minified. `@djangocfg/ui-tools/notion-editor` is the
27
+ > **lazy** entry (`lazy.tsx`) — pages that never render the editor pay
28
+ > nothing. All of it is `'use client'`.
29
+
22
30
  ## When to use this vs `MarkdownEditor`
23
31
 
24
- | Use case | Pick |
25
- | --------------------------------------- | ------------------- |
26
- | Chat composer, single-paragraph input | `MarkdownEditor` |
27
- | Note / doc surface, multi-paragraph | `NotionEditor` |
28
- | `.md` file viewer with full editing | `NotionEditor` |
29
- | Slim mention dropdown, no tables/tasks | `MarkdownEditor` |
30
- | Slash menu, drag handle, tables, hljs | `NotionEditor` |
31
-
32
- The two coexist intentionally — `NotionEditor` is ~350 KB minified
33
- (lowlight common pack + tables + drag handle); `MarkdownEditor` is
34
- ~200 KB. Don't mount NotionEditor inside a chat composer just to get
35
- features the user will never use.
36
-
37
- ## Features
38
-
39
- - **Slash menu (`/`)** — 11 commands: Text, H1-H3, Bullet/Numbered/Todo
40
- lists, Quote, Code block, Divider, Table. Each item shows its
41
- markdown shorthand (`#`, `- [ ]`, etc.) as a kbd hint. Filtering by
42
- title + aliases. `Esc` / click-outside close.
43
- - **Bubble menu**floating selection toolbar with Bold, Italic,
44
- Underline, Strike, Code, Highlight, Link. Each button has a Tooltip
45
- with the keyboard shortcut. Auto-hides inside code blocks and on
46
- empty selections.
47
- - **Cmd+K link prompt**opens a Radix Dialog with URL input and
48
- Save/Remove/Cancel. Snapshots and restores selection across the
49
- dialog mount. Rejects unsafe schemes (`javascript:`, `data:`).
50
- - **Drag handle** Notion-style grip appears on the left of every
51
- block on hover (powered by `tiptap-extension-global-drag-handle`).
52
- The icon is rendered as a `mask-image` of `lucide`'s `GripVertical`
53
- so it matches the rest of the app at the pixel level.
54
- - **Smart Cmd+A** first press selects the current text block, second
55
- press selects the whole document (Notion convention).
56
- - **Heading-aware placeholder** — empty H1/H2/H3 show "Heading 1/2/3";
57
- empty paragraphs show the supplied placeholder.
58
- - **Forced-dark code blocks** — code blocks render on a fixed One Dark
59
- Pro surface regardless of host theme. Avoids the washed-out
60
- "light syntax on light page" look every editor hits.
61
- - **Task list with ui-core `<Checkbox>`** — `taskItem` uses a React
62
- `NodeView` so the checkbox is the same component used everywhere
63
- else in the app (theme-aware, focus ring, etc.). Aligned to the
64
- text cap-height via `align-items: baseline`.
65
- - **Cmd+S save hook** — `onSave?: (md: string) => void` bound to
66
- Cmd/Ctrl+S via `useHotkey`, scoped to the editor DOM so it doesn't
67
- collide with global palettes higher up.
68
- - **autoFocus** declarative prop. Pair with `key={path}` upstream
69
- when the parent wants a fresh focus per file change.
70
- - **Markdown shortcuts** StarterKit's input rules cover `#`, `>`,
71
- `-`, `1.`, `` ``` ``, `---` → matching block on type.
72
-
73
- ## Extension stack
74
-
75
- Built once per editor instance (see `extensions.ts`). Order matters
76
- for markdown serialisation`Markdown` is registered before
77
- `Highlight`/`TaskList` so the latter use the markdown extension's
78
- node spec when serialising.
79
-
80
- - `StarterKit` paragraph, headings 1-4, bold/italic/code/strike,
81
- blockquote, ordered+unordered list, link, underline, horizontal
82
- rule, history. CodeBlock is **disabled** here replaced by
83
- `CodeBlockLowlight` for syntax highlighting.
84
- - `Placeholder` heading-aware ghost text.
85
- - `Markdown`bidirectional serialisation via `@tiptap/markdown`.
86
- Handles tables, task lists (`- [ ]`), highlight (`==text==`).
87
- - `Highlight` `==text==` mark.
88
- - `TaskList` + `TaskItem.extend({ addNodeView: TaskItemView })`.
89
- - `Table` + `TableRow` + `TableHeader` + `TableCell`.
90
- - `CodeBlockLowlight` uses `lowlight`'s `common` language pack
91
- (~25 languages). Add more by importing them and registering on the
92
- shared `lowlight` instance.
93
- - `GlobalDragHandle` — block grabber. **One handle per page** — see
94
- the "Known limitations" section below.
95
- - `SlashExtension` wraps `@tiptap/suggestion` for the `/` menu.
96
- - `CustomKeymap` — smart Cmd+A.
32
+ | Use case | Pick |
33
+ | --------------------------------------- | ---------------- |
34
+ | Chat composer, single-paragraph input | `MarkdownEditor` |
35
+ | Note / doc surface, multi-paragraph | `NotionEditor` |
36
+ | `.md` file viewer with full editing | `NotionEditor` |
37
+ | Slim mention dropdown, no tables/tasks | `MarkdownEditor` |
38
+ | Slash menu, drag handle, tables, hljs | `NotionEditor` |
39
+ | Bookmark cards / embedded map blocks | `NotionEditor` |
40
+
41
+ The two coexist intentionally `NotionEditor` is heavier; don't mount
42
+ it inside a chat composer just to get features the user will never use.
43
+
44
+ ## Props
45
+
46
+ `NotionEditorProps` (see `types.ts`):
47
+
48
+ | Prop | Type | Default | Notes |
49
+ | -------------------- | -------------------------- | -------------------- | ----- |
50
+ | `value` | `string` | — | Markdown source (required). |
51
+ | `onChange` | `(md: string) => void` | | Fired on every keystroke with freshly-serialised markdown (required). |
52
+ | `placeholder` | `string` | `"Type '/' for commands…"` | Ghost text on an empty paragraph. |
53
+ | `disabled` | `boolean` | `false` | Read-only renderer (also makes map blocks static). |
54
+ | `autoFocus` | `boolean` | `false` | Focus on mount. Pair with `key={path}` upstream for fresh focus per file. |
55
+ | `onSave` | `(md: string) => void` | | Bound to Cmd/Ctrl+S, scoped to the editor DOM. |
56
+ | `className` | `string` | `''` | Extra class on the outer wrapper. |
57
+ | `minHeight` | `number` | `320` | Min height of the editor surface (px). |
58
+ | `resolveLinkPreview` | `ResolveLinkPreview` | `undefined` | **Host URL-unfurl resolver** threaded into the bookmark node. `undefined` cards render a hostname/favicon fallback. See [nodes.md](./@docs/nodes.md#bookmark-block). |
59
+
60
+ `resolveLinkPreview` is captured on first build (memoised on
61
+ `[placeholder, resolveLinkPreview]`) pass a **stable reference** (a
62
+ host method), not a new closure per render.
63
+
64
+ ### Imperative API
65
+
66
+ ```tsx
67
+ const ref = useRef<NotionEditorHandle>(null);
68
+
69
+ ref.current?.focus(); // focus the editor surface
70
+ ref.current?.moveCursorToEnd(); // focus + caret at end
71
+ ref.current?.getEditor(); // raw TipTap Editor (escape hatch)
72
+ ```
73
+
74
+ `NotionEditorHandle` is structurally compatible with `ComposerHandle`
75
+ from `@djangocfg/ui-tools/composer-registry` the chat suite can
76
+ register a NotionEditor as a composer if you ever want to swap them in.
77
+
78
+ ## Features at a glance
79
+
80
+ - **Slash menu (`/`)** — 12 commands: Text, H1-H3, Bullet/Numbered/Todo
81
+ lists, Quote, Code block, Divider, Table, and **Map**. Filtering by
82
+ title + aliases; each item shows its markdown shorthand as a hint.
83
+ See [slash.md](./@docs/slash.md).
84
+ - **Block nodes**a **bookmark** (link-preview) card from pasting a
85
+ bare URL on an empty line, and an interactive **map** block from
86
+ `/map`. Both round-trip through markdown. See [nodes.md](./@docs/nodes.md).
87
+ - **Interactive map editing** — drag a pin, click-to-add, remove,
88
+ switch basemap; every edit writes back into the document via
89
+ `useNodeAttrs`. See [interactivity.md](./@docs/interactivity.md).
90
+ - **Safe block rendering** — each block parses its attrs through a Zod
91
+ schema; a malformed payload renders `<BlockError>` instead of
92
+ crashing the editor. See [interactivity.md](./@docs/interactivity.md#safe-rendering).
93
+ - **Bubble menu** floating selection toolbar (Bold, Italic, Underline,
94
+ Strike, Code, Highlight, Link) with shortcut tooltips. Auto-hides in
95
+ code blocks and on empty selections.
96
+ - **Cmd+K link prompt**, **drag handle**, **smart Cmd+A**,
97
+ **heading-aware placeholder**, **forced-dark code blocks**,
98
+ **task list** (ui-core `<Checkbox>`), **Cmd+S save hook**,
99
+ **markdown input rules**. See [editor.md](./@docs/editor.md).
100
+
101
+ ## Docs
102
+
103
+ | Doc | Covers |
104
+ |---|---|
105
+ | [nodes.md](./@docs/nodes.md) | The **bookmark** + **map** block nodes — triggers, payload shapes, the `resolveLinkPreview` injection, and markdown serialisation / round-trip. |
106
+ | [interactivity.md](./@docs/interactivity.md) | The `useNodeAttrs` write-back seam (how map edits persist) and the Zod-validator / `<BlockError>` safety net. |
107
+ | [slash.md](./@docs/slash.md) | The `/` slash menu — the command list, filtering, and how to add a command (incl. `/map`). |
108
+ | [editor.md](./@docs/editor.md) | Base editor config — the TipTap extension stack, keymap, task items, drag handle, link dialog, markdown round-trip table, and known limitations. |
109
+ | [stories.md](./@docs/stories.md) | The Storybook stories list. |
97
110
 
98
111
  ## Files
99
112
 
@@ -101,8 +114,10 @@ node spec when serialising.
101
114
  | ----------------------------- | ----------------------------------------------------------- |
102
115
  | `lazy.tsx` | Subpath entry — `React.lazy` + `forwardRef` for the handle |
103
116
  | `NotionEditor.tsx` | Main component + `BubbleSelectionToolbar` |
104
- | `extensions.ts` | Assembled TipTap stack (factory `notionExtensions()`) |
117
+ | `extensions.ts` | Assembled TipTap stack (factory `notionExtensions()`) |
105
118
  | `types.ts` | `NotionEditorProps`, `NotionEditorHandle` |
119
+ | `BookmarkNode.ts` · `BookmarkView.tsx` | Bookmark (link-preview) block node + NodeView |
120
+ | `MapNode.ts` · `MapView.tsx` · `MapEditLayer.tsx` | Map block node, NodeView, editable layer |
106
121
  | `SlashExtension.ts` | TipTap extension wrapping `@tiptap/suggestion` |
107
122
  | `createSlashSuggestion.ts` | Suggestion config — floating-ui popup mount/update/teardown |
108
123
  | `SlashList.tsx` | Popover content (listbox + keyboard nav) |
@@ -112,126 +127,32 @@ node spec when serialising.
112
127
  | `LinkDialog.tsx` | Cmd+K link prompt (ui-core Dialog) |
113
128
  | `styles.css` | Typography + slash/bubble menu + drag handle + lowlight |
114
129
 
115
- ## Imperative API
116
-
117
- ```tsx
118
- const ref = useRef<NotionEditorHandle>(null);
119
-
120
- ref.current?.focus(); // focus the editor surface
121
- ref.current?.moveCursorToEnd(); // focus + caret at end
122
- ref.current?.getEditor(); // raw TipTap Editor (escape hatch)
123
- ```
124
-
125
- `NotionEditorHandle` is structurally compatible with `ComposerHandle`
126
- from `@djangocfg/ui-tools/composer-registry` — the chat suite can
127
- register a NotionEditor as a composer if you ever want to swap them in
128
- the chat input (unusual, but allowed).
130
+ Shared seams used by the block nodes live under `src/common/` (outside
131
+ this folder): `common/link-preview` (the `LinkPreviewCard` +
132
+ `ResolveLinkPreview` contract), `common/blocks` (payload types, Zod
133
+ schemas, `<BlockError>`), and `common/tiptap` (the `useNodeAttrs`
134
+ write-back hook).
129
135
 
130
136
  ## Saving — dirty / pristine handling
131
137
 
132
- The editor is **uncontrolled-ish**: it calls `onChange` on every
133
- keystroke with the freshly-serialised markdown. The host owns state
134
- and detects "dirty" however it likes.
135
-
136
- Two gotchas the host needs to know about:
138
+ The editor calls `onChange` on every keystroke with the freshly
139
+ serialised markdown; the host owns state and detects "dirty" however it
140
+ likes. Two gotchas:
137
141
 
138
- 1. **TipTap re-serialises markdown on mount** (trailing newline,
139
- CRLF → LF normalisation). The first `onChange` after
140
- `setContent(value)` is byte-different from `value` even though
141
- nobody typed. If you compare `draft !== savedContent` literally,
142
- the dirty indicator lights up on every file open. **Fix in the
143
- host**: normalise both sides before comparing.
142
+ 1. **TipTap re-serialises markdown on mount** (trailing newline, CRLF →
143
+ LF). The first `onChange` after `setContent(value)` is byte-different
144
+ from `value` even though nobody typed. Normalise both sides before a
145
+ dirty comparison:
144
146
 
145
147
  ```ts
146
148
  const norm = (s: string) => s.replace(/\r\n/g, '\n').trimEnd();
147
149
  const isDirty = norm(draft) !== norm(saved);
148
150
  ```
149
151
 
150
- 2. **Cmd+S only fires when focus is inside the editor**. If the user
151
- clicked outside (header, sidebar) and presses Cmd+S, this hook
152
- doesn't fire. Add a window-level `useHotkey('mod+s', save)` in the
153
- host as a fallback — both paths call the same save fn.
152
+ 2. **Cmd+S only fires when focus is inside the editor.** Add a
153
+ window-level `useHotkey('mod+s', save)` in the host as a fallback —
154
+ both paths call the same save fn.
154
155
 
155
156
  See `cmdop/.../document-preview/viewers/text-viewer.tsx` for the
156
- canonical wiring.
157
-
158
- ## Markdown round-trip
159
-
160
- | Input | Round-trips? | Notes |
161
- | ---------------- | ------------ | ---------------------------------- |
162
- | Headings | ✓ | H1-H4 only (StarterKit config) |
163
- | Bold/italic/etc. | ✓ | |
164
- | Inline code | ✓ | |
165
- | Code blocks | ✓ | Language fence preserved by hljs |
166
- | Bullet/ordered | ✓ | Nested too |
167
- | Task lists | ✓ | GFM `- [x]` / `- [ ]` |
168
- | Blockquote | ✓ | |
169
- | Horizontal rule | ✓ | `---` |
170
- | Tables | ✓ | GFM tables — alignment via `:--:` |
171
- | Links | ✓ | autolinks too |
172
- | Highlight | ✓ | `==text==` extension syntax |
173
- | Images | mostly | Round-trip URLs, no upload yet |
174
- | HTML passthrough | ✗ | Stripped on parse |
175
- | Footnotes | ✗ | Not in StarterKit |
176
-
177
- ## Known limitations
178
-
179
- - **One editor per page (drag handle).**
180
- `tiptap-extension-global-drag-handle` mints a single DOM grabber
181
- attached to `document.body`. Mounting two `NotionEditor` instances
182
- on the same page (e.g. a side-by-side diff) makes them fight over
183
- the handle. If you need multiple editors, render only one with
184
- drag-handle support.
185
-
186
- - **Slash popup is portal'd to `document.body`.**
187
- If the parent unmounts the editor mid-suggestion, the popup gets
188
- cleaned up by ProseMirror's `onExit` — but a hostile race (route
189
- change while menu is open) can leave an orphan div. Rare; visible
190
- only via the inspector.
191
-
192
- - **No image upload pipeline (yet).**
193
- `Image` extension is not registered. Pasting an image from the
194
- clipboard inserts the data-URL into the document (browser default)
195
- but there's no upload integration. Add `@tiptap/extension-image` +
196
- a custom paste handler when you have a target storage. See Novel's
197
- `plugins/upload-images.tsx` for the canonical pattern.
198
-
199
- - **Bubble menu uses `@tiptap/react/menus`'s `BubbleMenu`** (Tippy
200
- under the hood). Slash menu uses `floating-ui` directly. Two
201
- positioning libraries for one editor — kept this way because
202
- `BubbleMenu` is opinionated about anchor invalidation and Tippy
203
- handles it correctly; reimplementing on floating-ui would save
204
- ~5 KB but cost a week of edge cases.
205
-
206
- ## Storybook
207
-
208
- Stories live at `apps/storybook/stories/ui-tools/markdown/NotionEditor.stories.tsx`:
209
-
210
- - Default (empty editor with placeholder)
211
- - With sample content (every supported block + serialised output panel)
212
- - Auto-focus on mount
213
- - Read-only
214
- - Compact (240px min-height)
215
-
216
- Each story renders a right-side panel showing the current serialised
217
- markdown so you can watch the round-trip live as you type.
218
-
219
- ## Wiring inside a host
220
-
221
- The canonical host is `cmdop`'s document-preview text viewer:
222
-
223
- ```tsx
224
- // .md → NotionEditor only (it's already WYSIWYG, no Preview/Source split)
225
- <div className="mx-auto w-full max-w-[820px] px-6 py-6">
226
- <NotionEditor
227
- value={draft}
228
- onChange={handleChange}
229
- onSave={save}
230
- autoFocus
231
- minHeight={420}
232
- />
233
- </div>
234
- ```
235
-
236
- Use `key={path}` on the wrapper if you remount per file — that's what
237
- makes `autoFocus` fire on each file open.
157
+ canonical wiring. Use `key={path}` on the wrapper if you remount per
158
+ file — that's what makes `autoFocus` fire on each file open.
@@ -17,6 +17,9 @@ import { SlashExtension } from './SlashExtension';
17
17
  import { createSlashSuggestion } from './createSlashSuggestion';
18
18
  import { TaskItemView } from './TaskItemView';
19
19
  import { CustomKeymap } from './CustomKeymap';
20
+ import { BookmarkNode } from './BookmarkNode';
21
+ import { MapNode } from './MapNode';
22
+ import type { ResolveLinkPreview } from '../../../common/link-preview';
20
23
 
21
24
  // Lowlight bundle: "common" languages cover ts/js/json/py/go/rust/etc.
22
25
  // The full language pack pulls in ~300KB more — opt-in case by case.
@@ -24,6 +27,13 @@ const lowlight = createLowlight(common);
24
27
 
25
28
  export interface BuildExtensionsOptions {
26
29
  placeholder: string;
30
+ /**
31
+ * Host resolver for the `bookmark` (link-preview) node. Given a URL it
32
+ * returns unfurl metadata. Threaded into `BookmarkNode.options` and read by
33
+ * its NodeView. Default `undefined` → the card renders its own
34
+ * hostname/favicon fallback (same seam as chat's `config.linkPreview`).
35
+ */
36
+ resolveLinkPreview?: ResolveLinkPreview;
27
37
  }
28
38
 
29
39
  /**
@@ -97,6 +107,11 @@ export function notionExtensions(opts: BuildExtensionsOptions): AnyExtension[] {
97
107
  // ships — typo and all. Spelling it correctly is silently ignored.
98
108
  scrollTreshold: 100,
99
109
  }),
110
+ // Bookmark (link-preview) block — paste a bare URL on an empty line and
111
+ // it becomes a card. The host resolver is threaded through options.
112
+ BookmarkNode.configure({ resolveLinkPreview: opts.resolveLinkPreview }),
113
+ // Map block — inserted via the `/map` slash command (see slashItems).
114
+ MapNode,
100
115
  SlashExtension.configure({
101
116
  suggestion: createSlashSuggestion(),
102
117
  }),
@@ -6,6 +6,7 @@ import {
6
6
  Heading3,
7
7
  List,
8
8
  ListOrdered,
9
+ Map as MapIcon,
9
10
  Minus,
10
11
  Quote,
11
12
  Table as TableIcon,
@@ -13,6 +14,7 @@ import {
13
14
  type LucideIcon,
14
15
  } from 'lucide-react';
15
16
  import type { Editor, Range } from '@tiptap/react';
17
+ import { DEFAULT_MAP_PAYLOAD } from './MapNode';
16
18
 
17
19
  export interface SlashItem {
18
20
  title: string;
@@ -146,6 +148,23 @@ export const slashItems: SlashItem[] = [
146
148
  .run();
147
149
  },
148
150
  },
151
+ {
152
+ title: 'Map',
153
+ description: 'Interactive map block.',
154
+ icon: MapIcon,
155
+ aliases: ['map', 'location', 'geo', 'pin'],
156
+ command: ({ editor, range }) => {
157
+ // Drop the typed `/map`, then insert a map block seeded with a sensible
158
+ // default payload so the user sees a live map immediately. Attrs are
159
+ // plain JSON and round-trip through markdown (see MapNode).
160
+ editor
161
+ .chain()
162
+ .focus()
163
+ .deleteRange(range)
164
+ .setMapBlock({ ...DEFAULT_MAP_PAYLOAD })
165
+ .run();
166
+ },
167
+ },
149
168
  ];
150
169
 
151
170
  /** Case-insensitive filter over title + aliases. */
@@ -58,6 +58,15 @@
58
58
  text-underline-offset: 2px;
59
59
  }
60
60
 
61
+ /* …but block-card NodeViews (bookmark/map) own their styling — the card is
62
+ itself an <a>, and its inner title/description must NOT inherit the editor
63
+ link underline (it read as if the whole card text were a link). */
64
+ .notion-editor .tiptap .notion-bookmark a,
65
+ .notion-editor .tiptap a.notion-bookmark {
66
+ color: inherit;
67
+ text-decoration: none;
68
+ }
69
+
61
70
  /* Headings */
62
71
  .notion-editor .tiptap h1 {
63
72
  font-size: 2em;
@@ -167,6 +176,22 @@
167
176
  text-decoration: line-through;
168
177
  }
169
178
 
179
+ /* ── Bookmark (link-preview) + Map block nodes ────────────────────────────
180
+ * Block-level NodeViews rendering shared cards (LinkPreviewCard / the map).
181
+ * The card / map owns its own frame (radius, border, shadow); here we only
182
+ * own the block's vertical rhythm so it breathes like a paragraph and the
183
+ * ProseMirror-selectednode ring reads cleanly on the atom. */
184
+ .notion-editor .tiptap .notion-bookmark,
185
+ .notion-editor .tiptap .notion-map {
186
+ margin: 0.75em 0;
187
+ }
188
+ .notion-editor .tiptap .notion-bookmark.ProseMirror-selectednode,
189
+ .notion-editor .tiptap .notion-map.ProseMirror-selectednode {
190
+ outline: 2px solid var(--ring);
191
+ outline-offset: 2px;
192
+ border-radius: var(--radius, 0.5rem);
193
+ }
194
+
170
195
  /* Blockquote */
171
196
  .notion-editor .tiptap blockquote {
172
197
  border-left: 3px solid var(--color-border, var(--border));
@@ -1,4 +1,5 @@
1
1
  import type { Editor } from '@tiptap/react';
2
+ import type { ResolveLinkPreview } from '../../../common/link-preview';
2
3
 
3
4
  export interface NotionEditorProps {
4
5
  value: string;
@@ -17,6 +18,14 @@ export interface NotionEditorProps {
17
18
  className?: string;
18
19
  /** Minimum height of the editor surface in px. Default 320. */
19
20
  minHeight?: number;
21
+ /**
22
+ * Host resolver for the `bookmark` (link-preview) block. When a user pastes
23
+ * a bare URL on an empty line it becomes a bookmark card; this resolver
24
+ * unfurls the URL into title/description/image (a Wails/Go method, a
25
+ * Next.js API route). Default `undefined` → the card renders its own
26
+ * hostname/favicon fallback. Same seam as chat's `config.linkPreview`.
27
+ */
28
+ resolveLinkPreview?: ResolveLinkPreview;
20
29
  }
21
30
 
22
31
  /** Imperative handle exposed via `ref`. Compatible with `ComposerHandle`
@@ -182,6 +182,11 @@ export function createExternalEngine(
182
182
  return {
183
183
  id: opts.id ?? 'external',
184
184
  isSupported: opts.supported ?? true,
185
+ // Capture happens outside the browser (native / sidecar), so this
186
+ // engine must never be gated on `navigator.mediaDevices.getUserMedia`
187
+ // — that API is absent in many host shells (Wails WKWebView) and in
188
+ // headless / non-secure contexts, yet the engine works fine there.
189
+ usesMicrophone: false,
185
190
  on(event, cb): Unsub {
186
191
  return bus.on(event, cb);
187
192
  },
@@ -82,6 +82,16 @@ export type Unsub = () => void;
82
82
  export interface RecognitionEngine {
83
83
  readonly id: string;
84
84
  readonly isSupported: boolean;
85
+ /**
86
+ * Whether this engine captures audio through the browser microphone
87
+ * (`navigator.mediaDevices.getUserMedia`). `true` (default when
88
+ * omitted) for the browser-native engines — Web Speech, HTTP and
89
+ * WebSocket all open a `getUserMedia` stream. `false` for engines
90
+ * that own capture outside the browser (`createExternalEngine` —
91
+ * Wails / Tauri / native whisper sidecar), so consumers must NOT gate
92
+ * them on `getUserMedia` being present.
93
+ */
94
+ readonly usesMicrophone?: boolean;
85
95
  start(opts: EngineStartOptions): Promise<void>;
86
96
  stop(): Promise<void>;
87
97
  abort(): void;