tiptap-markdown-react 0.17.1 → 0.19.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 (51) hide show
  1. package/README.md +171 -17
  2. package/THIRD_PARTY_LICENSES.md +45 -1
  3. package/dist/{ReportContent-MgbebxPV.js → ReportContent-B8jIzhy3.js} +8 -8
  4. package/dist/ReportContent-B8jIzhy3.js.map +1 -0
  5. package/dist/chart/columnMatching.d.ts +1 -1
  6. package/dist/chart/types.d.ts +1 -1
  7. package/dist/commentAnchor/commentMapper.d.ts +1 -1
  8. package/dist/components/EditorToolbar.d.ts +13 -2
  9. package/dist/components/EditorToolbar.dom.test.d.ts +1 -0
  10. package/dist/components/FindReplaceBar.d.ts +0 -7
  11. package/dist/components/ImageAlignTools.d.ts +19 -0
  12. package/dist/components/ImageUploadView.d.ts +10 -0
  13. package/dist/components/LinkPopover.d.ts +28 -0
  14. package/dist/components/LinkPopover.dom.test.d.ts +1 -0
  15. package/dist/components/MarkdownWysiwygEditor.d.ts +59 -1
  16. package/dist/components/TableHandles.d.ts +19 -0
  17. package/dist/components/TableHandles.dom.test.d.ts +1 -0
  18. package/dist/components/findEntry.dom.test.d.ts +1 -0
  19. package/dist/components/imageUpload.dom.test.d.ts +1 -0
  20. package/dist/extensions.d.ts +12 -0
  21. package/dist/{extractToc-BaR6LP1f.js → extractToc-CGrBMGvM.js} +433 -9
  22. package/dist/extractToc-CGrBMGvM.js.map +1 -0
  23. package/dist/findReplace.d.ts +18 -4
  24. package/dist/icons/index.d.ts +15 -0
  25. package/dist/imageAlign.d.ts +29 -0
  26. package/dist/imageAlign.dom.test.d.ts +1 -0
  27. package/dist/imageCaption.d.ts +12 -0
  28. package/dist/imageMarkdown.d.ts +86 -0
  29. package/dist/imageResize.dom.test.d.ts +1 -0
  30. package/dist/imageUpload.d.ts +63 -0
  31. package/dist/index.d.ts +11 -2
  32. package/dist/index.js +2354 -537
  33. package/dist/index.js.map +1 -1
  34. package/dist/labels.d.ts +93 -1
  35. package/dist/linkEditing.d.ts +45 -0
  36. package/dist/linkEditing.test.d.ts +1 -0
  37. package/dist/paragraphMarkdown.d.ts +19 -0
  38. package/dist/paragraphStyles.d.ts +38 -0
  39. package/dist/paragraphStyles.dom.test.d.ts +1 -0
  40. package/dist/reader.js +2 -2
  41. package/dist/resetImageSize.d.ts +12 -0
  42. package/dist/{scrollToTocHeading-BTQWajGD.js → scrollToTocHeading-B2QmL4wi.js} +2 -2
  43. package/dist/{scrollToTocHeading-BTQWajGD.js.map → scrollToTocHeading-B2QmL4wi.js.map} +1 -1
  44. package/dist/server.d.ts +1 -1
  45. package/dist/server.js +4 -4
  46. package/dist/style-inline.css +932 -180
  47. package/dist/style.css +932 -180
  48. package/dist/tableSelection.d.ts +25 -5
  49. package/package.json +5 -2
  50. package/dist/ReportContent-MgbebxPV.js.map +0 -1
  51. package/dist/extractToc-BaR6LP1f.js.map +0 -1
package/README.md CHANGED
@@ -1,13 +1,13 @@
1
1
  # tiptap-markdown-react
2
2
 
3
- A batteries-included, self-styled **Markdown WYSIWYG editor + reader** suite built on [Tiptap v3](https://tiptap.dev). Markdown in, markdown out — plus a table of contents, a client preview, and a server-side (RSC/SSR) renderer for SEO-friendly reading pages. No Ant Design, no icon library; themeable via CSS variables.
3
+ A batteries-included, self-styled **Markdown WYSIWYG editor + reader** suite built on [Tiptap v3](https://tiptap.dev). Markdown in, markdown out — plus a table of contents, a client preview, and a server-side (RSC/SSR) renderer for SEO-friendly reading pages. No UI framework dependency, no icon library; themeable via CSS variables.
4
4
 
5
5
  **[Live demo →](https://catsinjune.github.io/tiptap-markdown-react/)** — the docs site with runnable editor demos (source in [`site/`](./site)). Auto-deployed from `main`.
6
6
 
7
7
  - **Markdown-first**: content goes in and comes out as markdown (`getMarkdown()`), with `getHTML()` / `getJSON()` also exposed.
8
8
  - **Equations**: toolbar inserts inline / block math (KaTeX). Markdown round-trip uses `$$…$$` (inline) and newline-wrapped `$$` (block). Typing `$` / `$$` stays as text so dollar amounts are safe.
9
- - **Charts**: agentic-ui-compatible data charts (`<!-- {"chartType":…} -->` + GFM table). Chart.js renders in editor / preview / reader (SSR placeholder → client hydrate). MVP: line, bar, column, pie, donut, area.
10
- - **Own opinionated UI**: toolbar, color palette, code block, and table of contents ship styled out of the box. Zero `antd`. Dropdowns/popovers use [Radix](https://www.radix-ui.com/) primitives; icons are inline SVG.
9
+ - **Charts**: LLM-friendly data charts (`<!-- {"chartType":…} -->` + GFM table). Chart.js renders in editor / preview / reader (SSR placeholder → client hydrate). MVP: line, bar, column, pie, donut, area.
10
+ - **Own opinionated UI**: toolbar, color palette, code block, and table of contents ship styled out of the box. Dropdowns/popovers use [Radix](https://www.radix-ui.com/) primitives; icons are inline SVG.
11
11
  - **Editor + Preview + Static reader**: edit, live client-side preview, and a pure `renderReportHtml()` for server rendering (Next.js Server Components / ISR). Reading pages that only hydrate footnotes should import `tiptap-markdown-react/reader` so they do not load `TableKit`.
12
12
  - **Table of contents**: stable, shareable slug anchors that match between the editor preview and the published reading page.
13
13
  - **Themeable**: colors and fonts are exposed as `--tmr-*` CSS variables.
@@ -66,19 +66,21 @@ export function Composer() {
66
66
  return (
67
67
  <div>
68
68
  {editor && (
69
- <EditorToolbar
70
- editor={editor}
71
- onImageUpload={async (file) => {
72
- const url = await uploadToYourStorage(file); // return a public URL
73
- return url;
74
- }}
75
- onError={(err) => console.error(err)}
76
- />
69
+ <EditorToolbar editor={editor} onError={(err) => console.error(err)} />
77
70
  )}
78
71
  <MarkdownWysiwygEditor
79
72
  ref={ref}
80
73
  initialMarkdown={'# Hello\n\nStart writing…'}
81
74
  placeholder="Write something…"
75
+ // The toolbar's image button now inserts a drop/click upload block
76
+ // (official ImageUploadNode interaction); it swaps itself for the
77
+ // image when the upload finishes — see "Image upload" below.
78
+ imageUpload={{
79
+ upload: async (file, onProgress, signal) => {
80
+ return uploadToYourStorage(file, onProgress, signal); // public URL
81
+ },
82
+ maxSize: 5 * 1024 * 1024,
83
+ }}
82
84
  onEditorReady={setEditor}
83
85
  />
84
86
  <button onClick={() => console.log(ref.current?.getMarkdown())}>
@@ -161,7 +163,7 @@ Do not also import `tiptap-markdown-react` (the editor entry) from that same cli
161
163
 
162
164
  ### 3b. Charts (comment + table)
163
165
 
164
- Author / LLM form (matches agentic-ui / invret backend):
166
+ Author / LLM form:
165
167
 
166
168
  ```markdown
167
169
  <!-- {"chartType": "line", "x": "date", "y": "close", "title": "Price"} -->
@@ -287,22 +289,44 @@ Override any of these CSS variables on an ancestor (e.g. `:root` or the editor c
287
289
  | `onTocChange` | `(items: TocItem[]) => void` | Fires when headings change. |
288
290
  | `markdownPaste` | `boolean` | Auto-detect markdown in plain-text paste and convert it (Shift+paste keeps plain text). Default `true`. Init-only. |
289
291
  | `markdownFileDrop` | `boolean` | Drop or paste `.md` / `.markdown` files into the editor to insert their parsed content. Default `true`. Init-only. |
292
+ | `imageUpload` | `ImageUploadConfig` | Official `ImageUploadNode`-style upload (see [Image upload](#image-upload)). The toolbar's image button inserts a placeholder block you can drop a file on or click to pick; progress shows in place and the block is swapped for the image when every file finishes. `upload(file, onProgress?, signal?) => Promise<url>` is required; `accept` (`image/*`), `limit` (1), `maxSize` (0 = unlimited), `onError`, `onSuccess`, `labels` are optional. The block never reaches the saved markdown. Init-only. |
293
+ | `imageResize` | `boolean \| ImageResizeOptions` | Resizable images (default `true`, see [Image resize](#image-resize)). Drag a corner handle to resize; the size lands in the `width`/`height` node attributes and round-trips through markdown as `<img …>` (untouched images stay `![alt](url)`; `resetImageSize(editor)` clears a size). `false` disables; an object passes through to the official `resize` config (`directions`, `minWidth`, `minHeight`, `alwaysPreserveAspectRatio`). Init-only. |
290
294
  | `extraExtensions` | `AnyExtension[]` | Extra Tiptap extensions to register. |
291
295
  | `codeBlockLabels` | `Partial<CodeBlockLabels>` | Localize the code block UI. |
292
296
  | `findReplace` | `boolean` | Enable find & replace (default `true`): registers the official `@tiptap/extension-find-and-replace` and renders the floating bar. |
293
297
  | `findBar` | `boolean` | Render the floating bar inside the editor (default: same as `findReplace`). `false` hands **placement to the host** — the extension stays registered, the editor renders no bar, and you render `<FindReplaceBar editor={editor} />` wherever you want; `findShortcut` and `handle.openFind/closeFind` go quiet with it. |
294
- | `findBarContainer` | `HTMLElement \| (() => HTMLElement \| null)` | Mount the editor's bar into a container you own (the `getPopupContainer` idea): the library keeps open state and the shortcut, only the mount point changes. Use it when an `overflow: hidden` ancestor would clip the bar, or to park it in your own header / sidebar. Positioning is then yours (no absolute positioning is added), and if the container sits outside your themed subtree, bring `--tmr-*` along. Falls back to the in-editor bar when it resolves to `null`. |
298
+ | `findBarContainer` | `HTMLElement \| (() => HTMLElement \| null)` | Mount the editor's bar into a container you own (the popup-container pattern): the library keeps open state and the shortcut, only the mount point changes. Use it when an `overflow: hidden` ancestor would clip the bar, or to park it in your own header / sidebar. Positioning is then yours (no absolute positioning is added), and if the container sits outside your themed subtree, bring `--tmr-*` along. Falls back to the in-editor bar when it resolves to `null`. |
295
299
  | `findBarOffset` | `{ top?, right?, bottom?, left? } \| number = px` | Where the library-rendered bar sits inside its positioning context. Default `{ top: 4, right: 4 }` (top-right); pass e.g. `{ bottom: 8, left: 8 }` to dock it elsewhere. The context is the editor, or your `findBarContainer` element when you supply one — the library adds `position: relative` to that container if it is `static` (otherwise the bar would anchor to some unexpected ancestor). |
296
300
  | `findShortcut` | `boolean` | Take over <kbd>Cmd/Ctrl</kbd>+<kbd>F</kbd> while the editor has focus (default `true`; bound only when the editor owns a bar). `false` keeps the browser's native find — wire your own entry with `handle.openFind()`. |
297
301
  | `findLabels` | `Partial<FindLabels>` | Localize the find & replace bar. |
302
+ | `onFindOpenChange` | `(open: boolean) => void` | Fired when the bar actually shows or hides (Esc / × included). Use it to drive an external entry such as the toolbar magnifier. Fires only on a real change — never a `false` on mount — and stays `false` while `findBar={false}`, so a host-placed bar never lights up an entry you did not open. |
298
303
  | `className` | `string` | Class on the scroll container. |
299
304
 
300
305
  Ref handle (`MarkdownWysiwygEditorHandle`): `getMarkdown()`, `getHTML()`, `getJSON()`, `getEditor()`,
301
- `focusComment(id)`, `nextComment(dir?)`, `getCommentIds()`, `openFind()`, `closeFind()`.
306
+ `focusComment(id)`, `nextComment(dir?)`, `getCommentIds()`, `openFind()`, `closeFind()`, `toggleFind()`.
307
+ `openFind()` keeps the same "press again" feel as <kbd>Cmd/Ctrl</kbd>+<kbd>F</kbd> — refocus the search field and
308
+ select the query — while `toggleFind()` flips the bar and pairs with an entry that shows an active state
309
+ (`onSearch={() => handle.current?.toggleFind()}`).
302
310
 
303
311
  #### Find & replace
304
312
 
305
- <kbd>Cmd/Ctrl</kbd>+<kbd>F</kbd> (while focus is inside the editor) opens a floating bar: match counter inside the search field, wrap-around next/previous, match-case and whole-word toggles, replace and replace-all. `Esc` closes it, clears the highlights and returns focus to the editor. Read-only editors (`editable={false}`) can search but not replace.
313
+ <kbd>Cmd/Ctrl</kbd>+<kbd>F</kbd> (while focus is inside the editor) opens a floating bar: match counter inside the search field, wrap-around next/previous, match-case and whole-word toggles, replace and replace-all. Read-only editors (`editable={false}`) can search but not replace. <kbd>Enter</kbd> in the search field jumps to the next match — but an <kbd>Enter</kbd> that is committing an IME candidate (Chinese/Japanese input) is left alone, so confirming a word never jumps or replaces.
314
+
315
+ **Closing it.** `Esc` closes the bar, clears the highlights and returns focus to the editor; it works from anywhere that belongs to the editor (focus inside the bar, inside the document, or on the page while this is the only editor), and the keystroke is consumed so a host popover or drawer wrapping the bar does not close along with it. Other ways out: the bar's ×, `handle.closeFind()`, `toggleFind()`, and unmounting the bar. Deliberately **not** closing on: clicking into the document or another input, an empty query, zero results, after replace-all, or switching to read-only (search stays available then — only the replace row goes away). If an editor is swapped out (remounted for a new document), the library reports `onFindOpenChange(false)` so an entry elsewhere never stays lit.
316
+
317
+ `<EditorToolbar>` can carry an entry to the same bar. Pass `onSearch` (wired to the ref handle) and the magnifier appears just left of the More menu; omit it and the button does not render. Feed `searchActive` from `onFindOpenChange` and it lights up while the bar is open — pair it with `toggleFind()` so the lit button also turns the bar back off:
318
+
319
+ ```tsx
320
+ const find = useRef<MarkdownWysiwygEditorHandle>(null)
321
+ const [findOpen, setFindOpen] = useState(false)
322
+
323
+ <EditorToolbar
324
+ editor={editor}
325
+ onSearch={() => find.current?.toggleFind()}
326
+ searchActive={findOpen}
327
+ />
328
+ <MarkdownWysiwygEditor ref={find} onFindOpenChange={setFindOpen} />
329
+ ```
306
330
 
307
331
  Matching is done by the official extension, so the semantics are its semantics — worth knowing before you rely on them:
308
332
 
@@ -352,13 +376,15 @@ With `findBar={false}` the editor does not bind <kbd>Cmd/Ctrl</kbd>+<kbd>F</kbd>
352
376
 
353
377
  `FindReplaceBar` is exported if you'd rather place the bar yourself, `FindLabels` / `defaultFindLabels` for i18n, and `FindAndReplace` for hand-built pipelines. Two details to reuse when driving it yourself: keep `searchDebounceMs: 0` on the extension and debounce in your own UI, and route calls through `runFindCommand()` — it absorbs a Tiptap 3.31.3 transaction mismatch that fires on the first search when the document ends with a code block / table / chart (see the comment on `runFindCommand` for the mechanism).
354
378
 
379
+ Styling the bar from the outside: its internals use hashed CSS-module classes, so the bar carries stable hooks instead — `[data-find-bar]` on the root, `[data-find-field="search|replace"]` on the inputs, `[data-find-counter]` on the counter, `[data-find-option="match-case|whole-words"]` on the toggles and `[data-find-action="previous|next|close|replace|replace-all"]` on the buttons. Colors stay on the theme variables (`--tmr-find-*` for the highlights, `--tmr-toolbar-*` / `--tmr-accent` for the bar chrome).
380
+
355
381
  #### i18n (labels)
356
382
 
357
383
  There is no global locale and no provider: every visible string comes from a `Partial<XLabels>` prop that is merged over built-in English defaults (`{ ...defaultToolbarLabels, ...labels }`). A host ships one object per language and passes it down; switching languages is picking a different object. Override only the keys you care about — everything else falls back to English.
358
384
 
359
385
  | Component | Prop | Type |
360
386
  | --- | --- | --- |
361
- | `<EditorToolbar>` | `labels` | `Partial<ToolbarLabels>` (also covers the Import menu and the equation popover) |
387
+ | `<EditorToolbar>` | `labels` | `Partial<ToolbarLabels>` (also covers the Import menu, the equation popover and the link popover) |
362
388
  | `<MarkdownWysiwygEditor>` | `findLabels` | `Partial<FindLabels>` (replace / replace-all appear as icon buttons, so their labels are the tooltip + accessible name) |
363
389
  | `<MarkdownWysiwygEditor>` | `codeBlockLabels` | `Partial<CodeBlockLabels>` |
364
390
  | `<TocPanel>` | `labels` | `Partial<TocLabels>` |
@@ -384,21 +410,145 @@ Three ways to get markdown into the editor, all built in:
384
410
 
385
411
  The underlying extensions `MarkdownPaste` / `MarkdownFileDrop` (and the `looksLikeMarkdown` heuristic) are exported for custom pipelines.
386
412
 
413
+ #### Image upload
414
+
415
+ Configure `imageUpload` on the editor and the toolbar's image button stops opening the file picker directly. Instead it **inserts a placeholder block** in the document — the same interaction as the official `ImageUploadNode` UI component: drop a file on the block or click to pick one, the block lists each file with its size and a live progress bar, and once every queued file finishes it replaces itself with the matching image node(s) at the same position.
416
+
417
+ ```tsx
418
+ <MarkdownWysiwygEditor
419
+ imageUpload={{
420
+ upload: async (file, onProgress, signal) => {
421
+ return yourApi.upload(file, { onProgress, signal }); // public URL (or data URL)
422
+ },
423
+ accept: 'image/*', // default
424
+ limit: 3, // queue up to 3 per block; default 1
425
+ maxSize: 5 * 1024 * 1024, // per-file cap in bytes; 0 = unlimited (default 0)
426
+ onError: (err) => console.error(err),
427
+ }}
428
+ />
429
+ ```
430
+
431
+ - **Markdown stays clean** — the block is an atom node whose `renderMarkdown` returns `''`, so an in-flight upload can never leak into `getMarkdown()` / autosave; images appear only after the swap. On success `alt` / `title` are set to the file name (extension stripped).
432
+ - **Errors** — over-size / over-limit / failed uploads call `onError(error)`; the failed row stays in the block. Remove it (or Clear all) and try again.
433
+ - **Abort** — the `upload` callback receives an `AbortSignal` that fires when a row is removed, the queue is cleared, the block is deleted, or the editor unmounts. Hand it to `fetch` so abandoned uploads stop.
434
+ - **Keyboard** — <kbd>Cmd/Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>I</kbd> inserts a block (on Windows that combo also opens DevTools — the browser wins, same as the official component); <kbd>Enter</kbd> on a selected block opens the picker.
435
+ - The block's text comes from the `imageUpload.labels` (`ImageUploadLabels`); the `ImageUploadNode` extension and `hasImageUpload(editor)` are exported for hand-built pipelines.
436
+ - The legacy toolbar prop `onImageUpload` still works when the extension is not registered — see the `<EditorToolbar>` table.
437
+
438
+ #### Paragraph styles: alignment, indent, line height
439
+
440
+ Three dropdowns sit next to the font-size control: **alignment** (left / center / right / justify — the free official `@tiptap/extension-text-align`), **indent** (increase / decrease, one 2em step per level, max 8) and **line height** (default / 1 / 1.15 / 1.5 / 2 / 2.5 / 3). They apply to paragraphs and headings; commands are `setTextAlign` (official) plus `setIndent` / `increaseIndent` / `decreaseIndent` / `setLineHeight` from the exported `ParagraphStyles` extension.
441
+
442
+ Markdown has no paragraph-style syntax, so a styled paragraph serializes to inline HTML — the same bus as resized images, with the same "only when used" rule:
443
+
444
+ ```html
445
+ <p style="text-align: center; margin-left: 2em; line-height: 1.5">…</p>
446
+ <h2 style="text-align: right">…</h2>
447
+ ```
448
+
449
+ Untouched paragraphs stay native markdown. The editor, preview and server renderer share one tokenizer (it only claims `<p>` / `<hN>` carrying styles this library knows), so styles round-trip everywhere — including SSR. On other renderers (GitHub etc.) the `style` is sanitized away while the text renders normally: a decoration is lost, not content.
450
+
451
+ #### Image resize
452
+
453
+ Images are resizable out of the box (the official `@tiptap/extension-image` `resize` capability): hover an image — the handles appear on hover only — and drag a side handle. The size is committed as integer-pixel node attributes (`width` / `height`) — not inline styles — so it survives save and reload.
454
+
455
+ CommonMark has no size syntax, so **a resized image serializes to inline HTML**: `<img src="…" alt="…" width="440" height="330">`, while untouched images keep the standard `![alt](url)` form. The editor, the preview and the server renderer all parse that tag back through one shared tokenizer (it claims `<img …>` before the `window.DOMParser` path can run), so sizes round-trip in every pipeline — including SSR, where raw inline HTML would otherwise be escaped to literal text. Dragging writes `width`/`height`; export `resetImageSize(editor)` clears them and returns the image to the clean `![alt](url)` syntax (wire it to your own button — the library renders no UI for it).
456
+
457
+ ```tsx
458
+ <MarkdownWysiwygEditor
459
+ imageResize={{ minWidth: 80, minHeight: 80 }} // default: true
460
+ />
461
+ ```
462
+
463
+ Defaults: two side handles (`left` / `right`, matching the official demo, shown on hover only), aspect ratio always locked (`alwaysPreserveAspectRatio: true`), a **minimum of 80×80 px**, and a **maximum that follows the editor's content width** — images cannot be dragged past the layout; the bound is re-measured before every drag, so window resizes are picked up. Pin your own bounds with `minWidth` / `minHeight` / `maxWidth` / `maxHeight` (the library injects the max into the official `ResizableNodeView`, which has no max option of its own). Locking keeps the committed size consistent with how read-only rendering scales the image (CSS `height: auto`), so non-uniformly stretched images cannot "snap back" in previews; pass `alwaysPreserveAspectRatio: false` to allow free stretching anyway. Read-only editors never show handles, and `imageResize={false}` disables the feature entirely.
464
+
465
+ #### Image alignment
466
+
467
+ Hover an image and a small toolbar appears above it — left / center / right, the same hover-handle interaction as the table row/column handles. Alignment is stored as the `align` node attribute and rendered as **`data-align`** — the attribute name the official `image-align-button` uses — so existing `img[data-align="center"]` styles keep working. Keyboard shortcuts match the official component: <kbd>Alt</kbd>+<kbd>Shift</kbd>+<kbd>L</kbd> / <kbd>E</kbd> / <kbd>R</kbd> with an image selected (or the cursor right next to it).
468
+
469
+ Like size, alignment has no markdown syntax, so an aligned image serializes to `<img … data-align="center">` (untouched images stay `![alt](url)`) and the same tokenizer parses it back — editor, preview and server renderer all agree. For custom UIs the library exports `setImageAlign(editor, align)` / `isImageAlignActive(editor, align)` (selection-based) and `setImageAlignAt(editor, element, align)` (DOM-based); the hover toolbar's text comes from `labels.imageAlign*`.
470
+
471
+ #### Image caption
472
+
473
+ Hover an image, click the caption button in the hover toolbar and an input opens right there — <kbd>Enter</kbd> commits, <kbd>Esc</kbd> cancels, clicking away commits too. The caption renders under the image (small muted text, aligned with the image) and is stored as the `caption` node attribute.
474
+
475
+ In markdown a captioned image serializes to a `<figure>` block (standard HTML semantics — GitHub renders it the same way):
476
+
477
+ ```html
478
+ <figure>
479
+ <img src="…" alt="…">
480
+ <figcaption>Figure 1: something</figcaption>
481
+ </figure>
482
+ ```
483
+
484
+ Images without a caption keep the plain `![alt](url)` form. Editor, preview and server renderer share the same tokenizer, so captions round-trip everywhere — including SSR. Tooling for custom UIs: `setImageCaption(editor, text)` / `setImageCaptionAtPos(editor, pos, text)`; hover-toolbar labels via `labels.imageCaption` / `imageCaptionPlaceholder`. The official side has nothing for this (`@tiptap/extension-figure` does not exist on npm), so the whole path is native to this library.
485
+
387
486
  ### `<EditorToolbar>` (client)
388
487
 
488
+ The row stays centred and **scrolls horizontally** when the controls no longer fit — a single line, never wrapped (`justify-content: safe center` keeps both ends reachable once it overflows). The full set (search entry included) needs about 1045 px of container width; narrower containers scroll. `onSearch` renders the magnifier just left of the More menu.
489
+
389
490
  | Prop | Type | Description |
390
491
  | --- | --- | --- |
391
492
  | `editor` | `Editor` | The instance from `onEditorReady`. |
392
- | `onImageUpload` | `(file: File) => Promise<string>` | Upload handler returning a URL. Omit to hide the image button. |
493
+ | `onImageUpload` | `(file: File) => Promise<string>` | **Legacy** direct-upload path: picking a file inserts the returned URL. Ignored when the editor registers the `imageUpload` extension (configure that on `<MarkdownWysiwygEditor>` instead); omit both and no image button renders. |
393
494
  | `onError` | `(err, source?: 'image' \| 'markdown' \| 'import') => void` | Side-effect error callback (e.g. failed upload). |
394
495
  | `onImportDocument` | `(file, ctx) => Promise<ImportDocumentResult \| string>` | Converts a non-Markdown file to Markdown. `ctx.signal` aborts on cancel/unmount; `ctx.onProgress` reports upload/convert progress back. Omit and Import only takes `.md`. |
395
496
  | `importAccept` | `string` | Extra `accept` used by the default Document menu item when `importMenuItems` is omitted. |
396
497
  | `importMenuItems` | `ImportMenuItem[]` | Import dropdown options (`label` + `accept`). Hosts that want Markdown / Word / PDF pass three items. |
397
498
  | `showImport` | `boolean` | Show the Import dropdown (default `true`). |
398
499
  | `labels` | `Partial<ToolbarLabels>` | i18n labels. |
500
+ | `onSearch` | `() => void` | Search entry (magnifier) left of the More menu. Wire it to `handle.toggleFind()` (or `openFind()` if you'd rather it only ever open); omit it and no button renders. Worth passing only when the editor owns a bar (`findBar` on) — a host-placed bar has nothing for it to open. Search works read-only, so the button never greys out. |
501
+ | `searchActive` | `boolean` | Whether the bar is open; drives `aria-pressed` and the active highlight of the search entry (default `false`). Feed it from the editor's `onFindOpenChange`. |
399
502
  | `extraToolbarItems` | `ExtraToolbarItem[]` | Custom items appended to the "More" menu. |
400
503
  | `labels.inlineMath` / `blockMath` | `string` | More-menu equation items. |
401
504
  | `labels.mathPlaceholder` / `mathDone` / `mathNewInline` / `mathNewBlock` | `string` | Equation editor (empty chip/hint, input, Done). |
505
+ | `labels.tableRowMenu` / `tableColumnMenu` | `string` | Aria-labels of the table hover handles. |
506
+ | `labels.linkPrompt` / `linkApply` / `linkOpen` / `linkRemove` / `linkInvalid` | `string` | Link popover: field placeholder, apply, open in new window, remove, rejected-address hint. |
507
+ | `labels.imageAlign` / `imageAlignLeft` / `imageAlignCenter` / `imageAlignRight` | `string` | Hover alignment toolbar (rendered by the toolbar, appears over the hovered image): toolbar aria-label and the three button labels. |
508
+
509
+ #### Table editing (hover handles)
510
+
511
+ Hover any cell and four affordances appear: a `⋮` handle on the row's left edge, a `⋯` handle above the table aligned to the column, a `+` pill on the table's right edge (**append a column**) and a `+` pill below it (**append a row**). They are all sized to what they act on: the row handle spans the row's height, the column handle spans the column's width, and each `+` spans the table's edge it sits on. Clicking a handle selects that whole row / column and opens its menu: add before / after, delete, **Clear content**, **Reset cell styles**, plus **Delete table**. The handles ship with `<EditorToolbar>` (they are its table UI), reuse the same commands as the insert-table grid, and do not appear on read-only editors.
512
+
513
+ Right-click is still wired for exactly one case: a **multi-cell selection** (drag across cells). A handle can only express one row or one column, so batch add/delete of a rectangle keeps the context menu.
514
+
515
+ **Clear content** empties the selected cells but keeps them (a header row therefore never degrades into a data row), and **Reset cell styles** puts `colwidth` / `align` back to their schema defaults while leaving `colspan` / `rowspan` alone — those are structural, and the extension's `mergeCells` / `splitCell` are deliberately **not** exposed here: GFM has no `colspan`, so a merged cell exports as `| 1<br>2 | |` and cannot be restored on reload. Column width is a *column* property managed by the resize plugin, so clearing it needs the whole column selected (from the row handle it is restored from the widest cell in that column, which is the plugin's normal behavior).
516
+
517
+ Styling hooks (internals are hashed CSS-module classes): `[data-table-handle="row|col"]` on the handles, `[data-table-menu]` on the menu. Colors ride `--tmr-table-handle-bg` / `-fg` / `-border` (a light pill by default; hovering turns it into `--tmr-accent`).
518
+
519
+ **Not included: dragging a handle to reorder rows or columns.** That is precisely the part the official `table-node` component implements inside its own source — which is a **paid** Start-plan component (the registry answers 401 for `table-node`, `toc-node`, `drag-context-menu` while free ones answer 200), so it cannot be vendored into this MIT package. The free `@tiptap/extension-table` ships the add/delete/header/merge commands used here but no move commands; reordering would be our own transaction code on top of `TableMap`.
520
+
521
+ ### `<LinkPopover>` (client)
522
+
523
+ The toolbar's link button opens this popover — type an address, apply it, open it in a new window, or remove the link. With the caret inside a link it prefills that link's href, so editing a URL no longer means deleting and re-adding it (the old `window.prompt` also could not be edited, gave no feedback for a rejected address, and is unavailable in Electron and some WebViews).
524
+
525
+ ```tsx
526
+ <LinkPopover
527
+ editor={editor}
528
+ labels={{ field: 'Paste a link…' }}
529
+ trigger={<button type="button" className="my-btn"><LinkIcon /></button>}
530
+ />
531
+ ```
532
+
533
+ | Prop | Type | Description |
534
+ | --- | --- | --- |
535
+ | `editor` | `Editor` | The editor to operate on. |
536
+ | `trigger` | `ReactNode` | Your own trigger element. Radix's `asChild` takes over its click / ref / aria, so it must be a DOM element or a `forwardRef` component (the toolbar passes its `ToolbarButton`). |
537
+ | `labels` | `Partial<LinkPopoverLabels>` | i18n labels (`defaultLinkPopoverLabels` is exported). |
538
+ | `className` / `style` | `string` / `CSSProperties` | Extra class / inline style on the panel; placement stays with Radix. |
539
+
540
+ Styling hooks (internals are hashed CSS-module classes): `[data-link-popover]` on the panel, `[data-link-field]` on the input, `[data-link-action="apply|open|remove"]` on the buttons, `[data-link-invalid]` on the input while the editor rejects the address. Colors come from the theme variables (`--tmr-accent`, `--tmr-toolbar-border`, `--tmr-toolbar-muted`, `--tmr-text`, `--tmr-danger`).
541
+
542
+ The logic follows the official link-popover component (MIT) — ported, not vendored, so no Tailwind, no primitive design system and no extra runtime deps. It also fixes two flaws in that reference implementation, both caught by tests here: a rejected address used to leave the document modified (the insert-after-a-failed-`setLink` chain), and editing a link whose caret sits inside it replaced the link's text with the URL. What it does:
543
+
544
+ - applies over `extendMarkRange('link')`, so changes hit the whole link;
545
+ - inserts the address as its own text when the selection is empty and there is no link;
546
+ - removes without letting the autolink plugin re-add the link (`preventAutolink`);
547
+ - normalises bare domains and `host:port` with the extension's `defaultProtocol` (typing `example.com` stores `http://example.com`, matching what Tiptap's autolink does on paste — otherwise markdown would export a relative link);
548
+ - opens through a protocol allowlist with `noopener,noreferrer`;
549
+ - keeps its Enter IME-safe (the official panel's handler would apply the link while you commit a Chinese/Japanese candidate).
550
+
551
+ The commands are exported for headless or self-drawn UIs: `applyLink`, `removeLink`, `canSetLink`, `isLinkActive`, `readLinkHref`, `normalizeLinkHref`, `sanitizeLinkUrl`, `openLinkUrl`.
402
552
 
403
553
  ### `<MarkdownPreview>` (client)
404
554
 
@@ -439,3 +589,7 @@ The underlying extensions `MarkdownPaste` / `MarkdownFileDrop` (and the `looksLi
439
589
  ## License
440
590
 
441
591
  MIT © CatsInJune
592
+
593
+ Third-party content used or adapted by this project — Lucide icons, Tiptap UI Components
594
+ (ported component logic and icons), KaTeX styles/fonts, and the npm dependency licenses —
595
+ is credited in [THIRD_PARTY_LICENSES.md](./THIRD_PARTY_LICENSES.md).
@@ -52,7 +52,48 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
52
52
  SOFTWARE.
53
53
  ```
54
54
 
55
- ## 2. 运行时依赖(npm dependencies)
55
+ ## 2. Tiptap UI Components — MIT License
56
+
57
+ 本库部分组件的交互与实现参照(移植自)[tiptap-ui-components](https://github.com/ueberdosis/tiptap-ui-components)
58
+ (MIT 许可证),按本库的架构、命名与视觉体系重写,并在各源码文件头部标注来源:
59
+
60
+ | 官方组件 | 本库落点 | 移植范围 | 时间 |
61
+ | --- | --- | --- | --- |
62
+ | `image-upload-node` | `src/imageUpload.ts`、`src/components/ImageUploadView.tsx` | 上传占位块的交互与组件结构(拖拽区 / 文件队列 / 进度 / 中止)、默认文案与图标 path(文档形状、折角、云上传) | 2026-10-10 |
63
+ | `useLinkPopover` | `src/linkEditing.ts` | 链接编辑逻辑(整条链接的范围扩展、空选区插地址文本、`preventAutolink` 元数据、打开前的协议白名单);浮层外壳自绘 | 2026-10-09 |
64
+
65
+ 移植过程中修正了官方两处缺陷(`maxSize: 0` 被误判为超限、多文件时文件名与 URL 错位),
66
+ 并为 Markdown 往返补了空序列化等适配——这些属于本库的改动,不改变上述归属。
67
+ `src/findReplace.ts` 的 IME 组合守卫等交互判据与之同款(该文件的主逻辑由同为 MIT 的
68
+ `@tiptap/extension-find-and-replace` 包承担,见第 3 节)。
69
+
70
+ ### The MIT License
71
+
72
+ ```
73
+ MIT License
74
+
75
+ Copyright (c) 2025 Tiptap GmbH
76
+
77
+ Permission is hereby granted, free of charge, to any person obtaining a copy
78
+ of this software and associated documentation files (the "Software"), to deal
79
+ in the Software without restriction, including without limitation the rights
80
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
81
+ copies of the Software, and to permit persons to whom the Software is
82
+ furnished to do so, subject to the following conditions:
83
+
84
+ The above copyright notice and this permission notice shall be included in all
85
+ copies or substantial portions of the Software.
86
+
87
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
88
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
89
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
90
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
91
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
92
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
93
+ SOFTWARE.
94
+ ```
95
+
96
+ ## 3. 运行时依赖(npm dependencies)
56
97
 
57
98
  以下依赖通过 npm 以 `dependencies` 声明引入,构建时均 external(不打入 `dist/`),各自许可证随 `node_modules` 自动分发。此处仅做汇总。
58
99
 
@@ -65,14 +106,17 @@ SOFTWARE.
65
106
  | `@tiptap/extension-code-block-lowlight` | MIT |
66
107
  | `@tiptap/extension-file-handler` | MIT |
67
108
  | `@tiptap/extension-find-and-replace` | MIT |
109
+ | `@tiptap/extension-heading` | MIT |
68
110
  | `@tiptap/extension-highlight` | MIT |
69
111
  | `@tiptap/extension-image` | MIT |
112
+ | `@tiptap/extension-paragraph` | MIT |
70
113
  | `@tiptap/extension-subscript` | MIT |
71
114
  | `@tiptap/extension-superscript` | MIT |
72
115
  | `@tiptap/extension-table` | MIT |
73
116
  | `@tiptap/extension-table-of-contents` | MIT |
74
117
  | `@tiptap/extension-task-item` | MIT |
75
118
  | `@tiptap/extension-task-list` | MIT |
119
+ | `@tiptap/extension-text-align` | MIT |
76
120
  | `@tiptap/extension-text-style` | MIT |
77
121
  | `@tiptap/markdown` | MIT |
78
122
  | `@tiptap/pm` | MIT |
@@ -423,13 +423,13 @@ ${fenceBody}
423
423
  }
424
424
  return out;
425
425
  }
426
- const editorHost = "_editorHost_1teg4_6";
427
- const findBarAnchor = "_findBarAnchor_1teg4_13";
428
- const findBar = "_findBar_1teg4_13";
429
- const editorScroll = "_editorScroll_1teg4_5";
430
- const editorContent = "_editorContent_1teg4_30";
431
- const shortcutFab = "_shortcutFab_1teg4_391";
432
- const shortcutFabHosted = "_shortcutFabHosted_1teg4_419";
426
+ const editorHost = "_editorHost_yyqcs_6";
427
+ const findBarAnchor = "_findBarAnchor_yyqcs_13";
428
+ const findBar = "_findBar_yyqcs_13";
429
+ const editorScroll = "_editorScroll_yyqcs_5";
430
+ const editorContent = "_editorContent_yyqcs_30";
431
+ const shortcutFab = "_shortcutFab_yyqcs_570";
432
+ const shortcutFabHosted = "_shortcutFabHosted_yyqcs_598";
433
433
  const styles = {
434
434
  editorHost,
435
435
  findBarAnchor,
@@ -459,4 +459,4 @@ export {
459
459
  parseChartFenceBody as p,
460
460
  serializeChartPayload as s
461
461
  };
462
- //# sourceMappingURL=ReportContent-MgbebxPV.js.map
462
+ //# sourceMappingURL=ReportContent-B8jIzhy3.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ReportContent-B8jIzhy3.js","sources":["../src/citationUtils.ts","../src/chart/columnMatching.ts","../src/chart/types.ts","../src/chart/parse.ts","../src/ReportContent.tsx"],"sourcesContent":["import type { JSONContent } from '@tiptap/core';\n\nexport interface SourceRef {\n /** 与 markdown `[^n]` 中的 n 对齐(字符串,如 `\"1\"`)。 */\n index: string;\n url?: string;\n title?: string;\n /** 原文片段等;库本身不渲染 Popover,留给消费方。 */\n excerpt?: string;\n}\n\nfunction escapeAttr(value: string): string {\n return value\n .replace(/&/g, '&amp;')\n .replace(/\"/g, '&quot;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;');\n}\n\n/**\n * 将 markdown 中能匹配到 `sources` 的 `[^n]` 换成带 data-* 的\n * `<span class=\"citation-ref\">`(**不是** `<sup>`,避免被 Superscript 吃掉、\n * 也避免视觉上标)。未匹配的 `[^n]` 原样保留,留给 CitationRef tokenizer。\n *\n * 浏览器端 @tiptap/markdown 可解析内联 HTML;server 端无 DOMParser 时请改用\n * {@link applyCitationSources}(先 parse 再补 attrs)。\n */\nexport function enrichMarkdownCitations(\n markdown: string,\n sources: SourceRef[],\n): string {\n if (!sources.length) return markdown;\n\n return markdown.replace(/\\[\\^(\\d+)\\]/g, (match, num: string) => {\n const source = sources.find((s) => s.index === num);\n if (!source) return match;\n\n const attrs = [`class=\"citation-ref\"`, `data-index=\"${num}\"`];\n if (source.url) attrs.push(`data-url=\"${escapeAttr(source.url)}\"`);\n if (source.title) attrs.push(`data-title=\"${escapeAttr(source.title)}\"`);\n\n if (source.url) {\n return `<span ${attrs.join(' ')}><a href=\"${escapeAttr(source.url)}\" title=\"${escapeAttr(source.title || '')}\" target=\"_blank\" rel=\"noopener\">${num}</a></span>`;\n }\n return `<span ${attrs.join(' ')}>${num}</span>`;\n });\n}\n\nfunction patchCitationNode(\n node: JSONContent,\n byIndex: Map<string, SourceRef>,\n): JSONContent {\n if (node.type === 'citationRef') {\n const index = String(node.attrs?.index ?? '');\n const source = byIndex.get(index);\n if (!source) return node;\n return {\n ...node,\n attrs: {\n ...node.attrs,\n index,\n url: source.url ?? node.attrs?.url ?? null,\n title: source.title ?? node.attrs?.title ?? null,\n },\n };\n }\n\n if (!node.content?.length) return node;\n return {\n ...node,\n content: node.content.map((child) => patchCitationNode(child, byIndex)),\n };\n}\n\n/**\n * 在已 parse 的 Tiptap JSON 上,按 `sources` 给 `citationRef` 节点补 url/title。\n * SSR / 无 DOMParser 环境的首选路径。\n */\nexport function applyCitationSources(\n doc: JSONContent,\n sources: SourceRef[],\n): JSONContent {\n if (!sources.length) return doc;\n const byIndex = new Map(sources.map((s) => [s.index, s]));\n return patchCitationNode(doc, byIndex);\n}\n\n/** 单条脚注定义行:行首 0–3 空格 + `[^数字]:`。 */\nconst FOOTNOTE_DEF_RE = /^ {0,3}\\[\\^(\\d+)\\]:[ \\t]?(.*)$/;\n\n/** 内联链接:`[文本](url)`。URL 支持 `<...>` 包裹(可含空格)或一层配对括号(维基百科式)。 */\nconst MD_LINK_RE =\n /\\[([^\\]]*)\\]\\(\\s*(?:<([^<>]*)>|((?:[^()\\s]|\\([^()]*\\))+)(?:[ \\t]+\"[^\"]*\")?)\\s*\\)/;\nconst MD_LINK_RE_G = new RegExp(MD_LINK_RE.source, 'g');\n\n/** 把定义内容压成纯文本(链接只留文本),供 excerpt 与 title 兜底。 */\nfunction flattenFootnoteContent(content: string): string {\n return content\n .replace(\n MD_LINK_RE_G,\n (_match, text: string, angleUrl: string, plainUrl: string) =>\n text || angleUrl || plainUrl || '',\n )\n .trim();\n}\n\nexport interface ExtractedFootnotes {\n /** 剥离定义行后的正文(首尾与多余空行已收敛)。 */\n markdown: string;\n /** 解析出的来源列表,同一 label 重复定义取首次。 */\n sources: SourceRef[];\n}\n\n/**\n * 把 markdown 中的 GFM 脚注定义行(`[^n]: …`)抽成 {@link SourceRef} 并从正文剥离。\n *\n * 与 {@link enrichMarkdownCitations} / {@link applyCitationSources} 同属脚注一族:\n * 它们消费 `sources` 处理 `[^n]` 标记这一半,本函数生产 `sources`、处理定义行\n * 另一半——不剥离,定义行会以普通段落渲染;不抽取,标记没有 url/title 可补。\n * 典型用法(纯字符串变换,流式每帧重算也便宜):\n *\n * ```ts\n * const { markdown: body, sources } = extractFootnoteSources(raw);\n * // <MarkdownPreview markdown={body} sources={sources} … />\n * ```\n *\n * 规则与边界:\n * - label 仅支持数字,与 enrich / CitationRef tokenizer 的 `\\[\\^(\\d+)\\]` 一致;\n * 非数字定义行不识别、原样留在正文;\n * - 围栏代码块(``` / ~~~,判定与 {@link stabilizeMarkdown} 一致)内的定义行跳过;\n * 缩进代码块不识别(已知限制);\n * - 续行:紧跟定义的缩进(≥4 空格或 Tab)非空行并入内容,空行结束定义;\n * - 内容中第一个内联链接取作 url、链接文本作 title;excerpt 取纯文本全文;\n * 无链接时 title 取纯文本首行;\n * - 流式半行(如 `[^1]: [原文](htt`)按 best-effort 解析,下一帧自愈。\n */\nexport function extractFootnoteSources(markdown: string): ExtractedFootnotes {\n if (!markdown || !markdown.includes('[^')) {\n return { markdown, sources: [] };\n }\n\n const lines = markdown.split('\\n');\n const bodyLines: string[] = [];\n const sources: SourceRef[] = [];\n const seen = new Set<string>();\n let removed = false;\n\n let inFence = false;\n let fenceChar = '`';\n let fenceLen = 3;\n\n for (let i = 0; i < lines.length; i += 1) {\n const line = lines[i];\n\n const fence = line.match(/^(`{3,}|~{3,})/);\n if (fence) {\n const char = fence[1][0];\n if (!inFence) {\n inFence = true;\n fenceChar = char;\n fenceLen = fence[1].length;\n } else if (\n char === fenceChar &&\n fence[1].length >= fenceLen &&\n /^[`~]+\\s*$/.test(line)\n ) {\n inFence = false;\n }\n bodyLines.push(line);\n continue;\n }\n if (inFence) {\n bodyLines.push(line);\n continue;\n }\n\n const def = FOOTNOTE_DEF_RE.exec(line);\n if (!def) {\n bodyLines.push(line);\n continue;\n }\n removed = true;\n\n const parts = [def[2]];\n let j = i + 1;\n while (\n j < lines.length &&\n lines[j].trim() &&\n /^(?: {4,}|\\t)/.test(lines[j])\n ) {\n parts.push(lines[j].trim());\n j += 1;\n }\n i = j - 1;\n\n const index = def[1];\n if (seen.has(index)) continue;\n seen.add(index);\n\n const content = parts.join('\\n');\n const link = MD_LINK_RE.exec(content);\n const source: SourceRef = { index };\n if (link) {\n const url = link[2] ?? link[3];\n if (url) source.url = url;\n const text = link[1]?.trim();\n if (text) source.title = text;\n }\n const plain = flattenFootnoteContent(content);\n if (plain) {\n source.excerpt = plain;\n if (!source.title) source.title = plain.split('\\n')[0];\n }\n sources.push(source);\n }\n\n if (!removed) {\n return { markdown, sources: [] };\n }\n\n return {\n markdown: bodyLines\n .join('\\n')\n .replace(/\\n{3,}/g, '\\n\\n')\n .replace(/^\\n+/, '')\n .replace(/\\s+$/, ''),\n sources,\n };\n}\n","/**\n * Loose header ↔ config field matching for chart data tables.\n * Zero runtime deps beyond string/regex.\n */\n\nconst TRAILING_UNIT_SUFFIX_PATTERN = /^(\\s*[((][^))]+[))])+\\s*$/;\n\n/** Exact match, or column = field + parenthetical unit suffix. */\nexport function columnKeyMatchesConfiguredField(\n columnKey: string,\n configuredField: string,\n): boolean {\n const ck = (columnKey || '').trim();\n const f = (configuredField || '').trim();\n if (!ck || !f) return false;\n if (ck === f) return true;\n if (!ck.startsWith(f)) return false;\n const rest = ck.slice(f.length);\n return rest === '' || TRAILING_UNIT_SUFFIX_PATTERN.test(rest);\n}\n\n/**\n * Resolve configured x/y name to a real column dataIndex.\n * Returns original field when no match (caller decides degrade).\n */\nexport function resolveChartAxisFieldToColumnKey(\n configuredField: string | undefined,\n columnKeys: string[],\n): string | undefined {\n if (configuredField === undefined || configuredField === null) {\n return configuredField;\n }\n const f = configuredField.trim();\n if (!f) return configuredField;\n const keySet = new Set(columnKeys);\n if (keySet.has(f)) return f;\n const hit = columnKeys.find((k) => columnKeyMatchesConfiguredField(k, f));\n return hit ?? configuredField;\n}\n\nexport function normalizeChartConfigAxisFields<\n T extends { x?: string; y?: string; groupBy?: string; filterBy?: string; colorLegend?: string; sortBy?: string },\n>(cfg: T, columnKeys: string[]): T {\n return {\n ...cfg,\n ...(cfg.x !== undefined\n ? { x: resolveChartAxisFieldToColumnKey(cfg.x, columnKeys) }\n : {}),\n ...(cfg.y !== undefined\n ? { y: resolveChartAxisFieldToColumnKey(cfg.y, columnKeys) }\n : {}),\n ...(cfg.groupBy !== undefined\n ? { groupBy: resolveChartAxisFieldToColumnKey(cfg.groupBy, columnKeys) }\n : {}),\n ...(cfg.filterBy !== undefined\n ? {\n filterBy: resolveChartAxisFieldToColumnKey(cfg.filterBy, columnKeys),\n }\n : {}),\n ...(cfg.colorLegend !== undefined\n ? {\n colorLegend: resolveChartAxisFieldToColumnKey(\n cfg.colorLegend,\n columnKeys,\n ),\n }\n : {}),\n ...(cfg.sortBy !== undefined\n ? { sortBy: resolveChartAxisFieldToColumnKey(cfg.sortBy, columnKeys) }\n : {}),\n };\n}\n","/** MVP chart types (subset). */\nexport const MVP_CHART_TYPES = [\n 'line',\n 'bar',\n 'column',\n 'pie',\n 'donut',\n 'area',\n] as const;\n\nexport type MvpChartType = (typeof MVP_CHART_TYPES)[number];\n\nexport function isMvpChartType(value: unknown): value is MvpChartType {\n return (\n typeof value === 'string' &&\n (MVP_CHART_TYPES as readonly string[]).includes(value)\n );\n}\n\n/**\n * Flat chart config (author / LLM form).\n * `subgraphBy` is accepted but ignored in v1.\n */\nexport interface ChartConfig {\n chartType: string;\n x?: string;\n y?: string;\n title?: string;\n height?: number;\n dataTime?: string;\n groupBy?: string;\n filterBy?: string;\n colorLegend?: string;\n sortBy?: string;\n subgraphBy?: string;\n [key: string]: unknown;\n}\n\nexport interface ChartColumn {\n title: string;\n dataIndex: string;\n}\n\n/** Normalized payload stored on the TipTap chart node. */\nexport interface ChartPayload {\n config: ChartConfig[];\n columns: ChartColumn[];\n dataSource: Record<string, unknown>[];\n}\n\n/** Optional host override for Chart.js colors / fonts. */\nexport interface ChartTheme {\n backgroundColor?: string;\n textColor?: string;\n gridColor?: string;\n borderColor?: string;\n fontFamily?: string;\n palette?: string[];\n}\n","import { normalizeChartConfigAxisFields } from './columnMatching';\nimport {\n isMvpChartType,\n type ChartColumn,\n type ChartConfig,\n type ChartPayload,\n type MvpChartType,\n} from './types';\n\nconst CHART_COMMENT_RE = /^<!--\\s*(\\[[\\s\\S]*?\\]|\\{[\\s\\S]*?\\})\\s*-->\\s*$/;\n\n/** GFM table: header + separator + optional body rows. */\nconst GFM_TABLE_BLOCK_RE =\n /^\\|(.+)\\|\\s*\\n\\|([-:\\s|]+)\\|\\s*\\n((?:\\|.*\\|\\s*\\n?)*)/;\n\nfunction splitRow(line: string): string[] {\n const trimmed = line.trim();\n const inner =\n trimmed.startsWith('|') && trimmed.endsWith('|')\n ? trimmed.slice(1, -1)\n : trimmed;\n return inner.split('|').map((c) => c.trim());\n}\n\nfunction isSeparatorRow(line: string): boolean {\n const cells = splitRow(line);\n return cells.length > 0 && cells.every((c) => /^:?-+:?$/.test(c));\n}\n\nfunction coerceCell(raw: string): string | number {\n const t = raw.trim();\n if (t === '') return t;\n const normalized = t.replace(/,/g, '');\n if (/^[-+]?(\\d+(\\.\\d*)?|\\.\\d+)([eE][-+]?\\d+)?$/.test(normalized)) {\n const n = Number(normalized);\n if (!Number.isNaN(n)) return n;\n }\n return t;\n}\n\n/** Parse a GFM markdown table into columns + rows. */\nexport function parseGfmTable(markdown: string): {\n columns: ChartColumn[];\n dataSource: Record<string, unknown>[];\n rawLength: number;\n} | null {\n const match = GFM_TABLE_BLOCK_RE.exec(markdown);\n if (!match) return null;\n const headerLine = `|${match[1]}|`;\n const sepLine = `|${match[2]}|`;\n if (!isSeparatorRow(sepLine)) return null;\n\n const headers = splitRow(headerLine);\n if (headers.length === 0 || headers.every((h) => !h)) return null;\n\n const columns: ChartColumn[] = headers.map((h) => ({\n title: h,\n dataIndex: h,\n }));\n\n const body = match[3] ?? '';\n const dataSource: Record<string, unknown>[] = [];\n for (const line of body.split('\\n')) {\n if (!line.trim()) continue;\n if (!line.includes('|')) continue;\n const cells = splitRow(line);\n if (cells.length === 0) continue;\n const row: Record<string, unknown> = {};\n headers.forEach((h, i) => {\n row[h] = coerceCell(cells[i] ?? '');\n });\n dataSource.push(row);\n }\n\n return {\n columns,\n dataSource,\n rawLength: match[0].length,\n };\n}\n\nfunction looksLikeChartCommentJson(parsed: unknown): boolean {\n if (Array.isArray(parsed)) {\n return parsed.some(\n (c) =>\n c &&\n typeof c === 'object' &&\n 'chartType' in (c as object) &&\n typeof (c as ChartConfig).chartType === 'string',\n );\n }\n if (parsed && typeof parsed === 'object') {\n const o = parsed as Record<string, unknown>;\n if (Array.isArray(o.config)) return true;\n if (typeof o.chartType === 'string') return true;\n }\n return false;\n}\n\n/** Unwrap comment JSON into a flat config list (may include non-MVP types). */\nexport function unwrapChartConfigs(raw: unknown): ChartConfig[] | null {\n if (raw == null) return null;\n if (Array.isArray(raw)) {\n return raw.filter(\n (c): c is ChartConfig =>\n !!c && typeof c === 'object' && typeof (c as ChartConfig).chartType === 'string',\n );\n }\n if (typeof raw !== 'object') return null;\n const o = raw as Record<string, unknown>;\n if (Array.isArray(o.config)) {\n return unwrapChartConfigs(o.config);\n }\n if (typeof o.chartType === 'string') {\n return [o as ChartConfig];\n }\n return null;\n}\n\nfunction inferPieAxes(\n cfg: ChartConfig,\n columns: ChartColumn[],\n): ChartConfig {\n if (cfg.x && cfg.y) return cfg;\n if (columns.length < 2) return cfg;\n return {\n ...cfg,\n x: cfg.x ?? columns[0].dataIndex,\n y: cfg.y ?? columns[1].dataIndex,\n };\n}\n\nfunction axisExists(field: string | undefined, keys: string[]): boolean {\n if (!field) return false;\n return keys.includes(field);\n}\n\n/**\n * Keep only MVP chart configs that have resolvable axes.\n * Returns null when nothing left (caller should leave as plain table).\n */\nexport function normalizeChartPayload(\n configs: ChartConfig[],\n columns: ChartColumn[],\n dataSource: Record<string, unknown>[],\n): ChartPayload | null {\n const keys = columns.map((c) => c.dataIndex);\n const normalized: ChartConfig[] = [];\n\n for (const raw of configs) {\n if (!isMvpChartType(raw.chartType)) continue;\n let cfg = normalizeChartConfigAxisFields({ ...raw }, keys);\n const type = cfg.chartType as MvpChartType;\n if (type === 'pie' || type === 'donut') {\n cfg = inferPieAxes(cfg, columns);\n }\n if (!axisExists(cfg.x, keys) || !axisExists(cfg.y, keys)) continue;\n // v1: ignore subgraphBy\n if (cfg.subgraphBy) {\n const { subgraphBy: _drop, ...rest } = cfg;\n cfg = rest;\n }\n normalized.push(cfg);\n }\n\n if (normalized.length === 0) return null;\n return { config: normalized, columns, dataSource };\n}\n\n/** Parse ```chart / ```json-chart / ```tmr-chart fence body JSON. */\nexport function parseChartFenceBody(body: string): ChartPayload | null {\n let parsed: unknown;\n try {\n parsed = JSON.parse(body.trim());\n } catch {\n return null;\n }\n if (!parsed || typeof parsed !== 'object') return null;\n const o = parsed as Record<string, unknown>;\n\n // Full form\n if (Array.isArray(o.columns) || Array.isArray(o.dataSource)) {\n const configs = unwrapChartConfigs(o.config ?? o) ?? [];\n const columns = (o.columns as ChartColumn[]) ?? [];\n const dataSource =\n (o.dataSource as Record<string, unknown>[]) ??\n (o.data as Record<string, unknown>[]) ??\n [];\n if (!columns.length) {\n // Simple { chartType, x, y, data }\n if (Array.isArray(o.data) && typeof o.chartType === 'string') {\n const simpleCols: ChartColumn[] = [\n { title: String(o.x ?? 'x'), dataIndex: 'x' },\n { title: String(o.y ?? 'y'), dataIndex: 'y' },\n ];\n const rows = (o.data as unknown[]).map((item) => {\n if (item && typeof item === 'object') return item as Record<string, unknown>;\n return {};\n });\n return normalizeChartPayload(\n [{ chartType: String(o.chartType), x: 'x', y: 'y', title: o.title as string | undefined }],\n simpleCols,\n rows,\n );\n }\n return null;\n }\n const colObjs: ChartColumn[] = columns.map((c) =>\n typeof c === 'string'\n ? { title: c, dataIndex: c }\n : {\n title: String((c as ChartColumn).title ?? (c as ChartColumn).dataIndex),\n dataIndex: String((c as ChartColumn).dataIndex ?? (c as ChartColumn).title),\n },\n );\n return normalizeChartPayload(configs, colObjs, dataSource);\n }\n\n const configs = unwrapChartConfigs(parsed);\n if (!configs) return null;\n return null;\n}\n\n/**\n * Try to parse leading `<!-- chart json -->` + GFM table from `src`.\n * Returns payload + `raw`(`src` 里被消费掉的**精确前缀**,调用方按 `raw.length`\n * 跳过这一段), or null.\n */\nexport function tryParseCommentTableChart(\n src: string,\n): { payload: ChartPayload; raw: string } | null {\n const trimmedStart = src.replace(/^\\s*/, '');\n const offset = src.length - trimmedStart.length;\n if (!trimmedStart.startsWith('<!--')) return null;\n\n const endComment = trimmedStart.indexOf('-->');\n if (endComment < 0) return null;\n const commentBlock = trimmedStart.slice(0, endComment + 3);\n const cm = CHART_COMMENT_RE.exec(commentBlock.trim());\n if (!cm) return null;\n\n let json: unknown;\n try {\n json = JSON.parse(cm[1]);\n } catch {\n return null;\n }\n if (!looksLikeChartCommentJson(json)) return null;\n\n const configs = unwrapChartConfigs(json);\n if (!configs || configs.length === 0) return null;\n // All table-only → not a chart\n if (configs.every((c) => c.chartType === 'table')) return null;\n\n const afterCommentRaw = trimmedStart.slice(endComment + 3);\n // 注释与表格之间隔空行时折成一个 \\n 再解析(表格内容本身不受影响)。\n const afterComment = afterCommentRaw.replace(/^\\s*\\n/, '\\n');\n const tableStart = afterComment.search(/\\|/);\n if (tableStart < 0) return null;\n const beforeTable = afterComment.slice(0, tableStart);\n if (beforeTable.replace(/\\s/g, '') !== '') return null;\n\n const tableMd = afterComment.slice(tableStart);\n const table = parseGfmTable(tableMd);\n if (!table) return null;\n\n const payload = normalizeChartPayload(\n configs,\n table.columns,\n table.dataSource,\n );\n if (!payload) return null;\n\n // 表格起点要在**原文**里定位:折过的 afterComment 比原文短,拿它当基准会让\n // raw 短掉那几个空行字符,调用方按 raw.length 切 src 就会多吃掉后一个字符\n // —— 表格是最后一块时,被吃掉的正是表格收尾的 `|`。\n const tableStartInSrc = afterCommentRaw.search(/\\|/);\n const raw = src.slice(\n 0,\n offset + commentBlock.length + tableStartInSrc + table.rawLength,\n );\n\n return { payload, raw };\n}\n\n/** Serialize payload to flat comment + GFM table (author form). */\nexport function serializeChartPayload(payload: ChartPayload): string {\n const flat =\n payload.config.length === 1 ? payload.config[0] : payload.config;\n const comment = `<!-- ${JSON.stringify(flat)} -->`;\n const headers = payload.columns.map((c) => c.title);\n const keys = payload.columns.map((c) => c.dataIndex);\n const header = `| ${headers.join(' | ')} |`;\n const sep = `| ${headers.map(() => '---').join(' | ')} |`;\n const rows = payload.dataSource.map((row) => {\n const cells = keys.map((k) => {\n const v = row[k];\n if (v == null) return '';\n return String(v);\n });\n return `| ${cells.join(' | ')} |`;\n });\n return [comment, '', header, sep, ...rows].join('\\n');\n}\n\n/**\n * Replace valid comment+table chart pairs with ```tmr-chart fences\n * so TipTap markdown can tokenize a single fence.\n */\nexport function normalizeChartMarkdown(markdown: string): string {\n if (!markdown) return markdown;\n let rest = markdown;\n let out = '';\n\n while (rest.length > 0) {\n const idx = rest.indexOf('<!--');\n if (idx < 0) {\n out += rest;\n break;\n }\n out += rest.slice(0, idx);\n const attempt = tryParseCommentTableChart(rest.slice(idx));\n if (!attempt) {\n // Skip this comment opener without consuming as chart\n out += '<!--';\n rest = rest.slice(idx + 4);\n continue;\n }\n const fenceBody = JSON.stringify({\n config: attempt.payload.config,\n columns: attempt.payload.columns,\n dataSource: attempt.payload.dataSource,\n });\n out += `\\`\\`\\`tmr-chart\\n${fenceBody}\\n\\`\\`\\``;\n rest = rest.slice(idx + attempt.raw.length);\n // Preserve a newline after fence if original had trailing content\n if (rest.startsWith('\\n')) {\n /* keep */\n } else if (rest.length > 0) {\n out += '\\n';\n }\n }\n\n return out;\n}\n","import type { HTMLAttributes } from 'react';\nimport styles from './styles/content.module.css';\n\nexport interface ReportContentProps extends HTMLAttributes<HTMLDivElement> {\n /** `renderReportHtml(...).html` 产出的正文 HTML 字符串。 */\n html: string;\n}\n\n/**\n * 只读静态正文渲染(RSC / SSR 安全,无 client 代码、无编辑器实例)。\n *\n * 把 `renderReportHtml` 产出的 HTML 注入容器,并套上与编辑器 / 只读预览**同一个\n * 规范正文类**,保证「编辑所见 = 预览所见 = 发布后读者所见」。\n * 用于阅读页首屏直出(SEO 可见),无需 JS 即可显示。\n */\nexport function ReportContent({ html, className, ...rest }: ReportContentProps) {\n return (\n <div\n {...rest}\n className={\n className ? `${styles.editorContent} ${className}` : styles.editorContent\n }\n dangerouslySetInnerHTML={{ __html: html }}\n />\n );\n}\n"],"names":["configs"],"mappings":";AAWA,SAAS,WAAW,OAAuB;AACzC,SAAO,MACJ,QAAQ,MAAM,OAAO,EACrB,QAAQ,MAAM,QAAQ,EACtB,QAAQ,MAAM,MAAM,EACpB,QAAQ,MAAM,MAAM;AACzB;AAUO,SAAS,wBACd,UACA,SACQ;AACR,MAAI,CAAC,QAAQ,OAAQ,QAAO;AAE5B,SAAO,SAAS,QAAQ,gBAAgB,CAAC,OAAO,QAAgB;AAC9D,UAAM,SAAS,QAAQ,KAAK,CAAC,MAAM,EAAE,UAAU,GAAG;AAClD,QAAI,CAAC,OAAQ,QAAO;AAEpB,UAAM,QAAQ,CAAC,wBAAwB,eAAe,GAAG,GAAG;AAC5D,QAAI,OAAO,IAAK,OAAM,KAAK,aAAa,WAAW,OAAO,GAAG,CAAC,GAAG;AACjE,QAAI,OAAO,MAAO,OAAM,KAAK,eAAe,WAAW,OAAO,KAAK,CAAC,GAAG;AAEvE,QAAI,OAAO,KAAK;AACd,aAAO,SAAS,MAAM,KAAK,GAAG,CAAC,aAAa,WAAW,OAAO,GAAG,CAAC,YAAY,WAAW,OAAO,SAAS,EAAE,CAAC,oCAAoC,GAAG;AAAA,IACrJ;AACA,WAAO,SAAS,MAAM,KAAK,GAAG,CAAC,IAAI,GAAG;AAAA,EACxC,CAAC;AACH;AAEA,SAAS,kBACP,MACA,SACa;;AACb,MAAI,KAAK,SAAS,eAAe;AAC/B,UAAM,QAAQ,SAAO,UAAK,UAAL,mBAAY,UAAS,EAAE;AAC5C,UAAM,SAAS,QAAQ,IAAI,KAAK;AAChC,QAAI,CAAC,OAAQ,QAAO;AACpB,WAAO;AAAA,MACL,GAAG;AAAA,MACH,OAAO;AAAA,QACL,GAAG,KAAK;AAAA,QACR;AAAA,QACA,KAAK,OAAO,SAAO,UAAK,UAAL,mBAAY,QAAO;AAAA,QACtC,OAAO,OAAO,WAAS,UAAK,UAAL,mBAAY,UAAS;AAAA,MAAA;AAAA,IAC9C;AAAA,EAEJ;AAEA,MAAI,GAAC,UAAK,YAAL,mBAAc,QAAQ,QAAO;AAClC,SAAO;AAAA,IACL,GAAG;AAAA,IACH,SAAS,KAAK,QAAQ,IAAI,CAAC,UAAU,kBAAkB,OAAO,OAAO,CAAC;AAAA,EAAA;AAE1E;AAMO,SAAS,qBACd,KACA,SACa;AACb,MAAI,CAAC,QAAQ,OAAQ,QAAO;AAC5B,QAAM,UAAU,IAAI,IAAI,QAAQ,IAAI,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC,CAAC,CAAC;AACxD,SAAO,kBAAkB,KAAK,OAAO;AACvC;AAGA,MAAM,kBAAkB;AAGxB,MAAM,aACJ;AACF,MAAM,eAAe,IAAI,OAAO,WAAW,QAAQ,GAAG;AAGtD,SAAS,uBAAuB,SAAyB;AACvD,SAAO,QACJ;AAAA,IACC;AAAA,IACA,CAAC,QAAQ,MAAc,UAAkB,aACvC,QAAQ,YAAY,YAAY;AAAA,EAAA,EAEnC,KAAA;AACL;AAgCO,SAAS,uBAAuB,UAAsC;;AAC3E,MAAI,CAAC,YAAY,CAAC,SAAS,SAAS,IAAI,GAAG;AACzC,WAAO,EAAE,UAAU,SAAS,GAAC;AAAA,EAC/B;AAEA,QAAM,QAAQ,SAAS,MAAM,IAAI;AACjC,QAAM,YAAsB,CAAA;AAC5B,QAAM,UAAuB,CAAA;AAC7B,QAAM,2BAAW,IAAA;AACjB,MAAI,UAAU;AAEd,MAAI,UAAU;AACd,MAAI,YAAY;AAChB,MAAI,WAAW;AAEf,WAAS,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK,GAAG;AACxC,UAAM,OAAO,MAAM,CAAC;AAEpB,UAAM,QAAQ,KAAK,MAAM,gBAAgB;AACzC,QAAI,OAAO;AACT,YAAM,OAAO,MAAM,CAAC,EAAE,CAAC;AACvB,UAAI,CAAC,SAAS;AACZ,kBAAU;AACV,oBAAY;AACZ,mBAAW,MAAM,CAAC,EAAE;AAAA,MACtB,WACE,SAAS,aACT,MAAM,CAAC,EAAE,UAAU,YACnB,aAAa,KAAK,IAAI,GACtB;AACA,kBAAU;AAAA,MACZ;AACA,gBAAU,KAAK,IAAI;AACnB;AAAA,IACF;AACA,QAAI,SAAS;AACX,gBAAU,KAAK,IAAI;AACnB;AAAA,IACF;AAEA,UAAM,MAAM,gBAAgB,KAAK,IAAI;AACrC,QAAI,CAAC,KAAK;AACR,gBAAU,KAAK,IAAI;AACnB;AAAA,IACF;AACA,cAAU;AAEV,UAAM,QAAQ,CAAC,IAAI,CAAC,CAAC;AACrB,QAAI,IAAI,IAAI;AACZ,WACE,IAAI,MAAM,UACV,MAAM,CAAC,EAAE,KAAA,KACT,gBAAgB,KAAK,MAAM,CAAC,CAAC,GAC7B;AACA,YAAM,KAAK,MAAM,CAAC,EAAE,MAAM;AAC1B,WAAK;AAAA,IACP;AACA,QAAI,IAAI;AAER,UAAM,QAAQ,IAAI,CAAC;AACnB,QAAI,KAAK,IAAI,KAAK,EAAG;AACrB,SAAK,IAAI,KAAK;AAEd,UAAM,UAAU,MAAM,KAAK,IAAI;AAC/B,UAAM,OAAO,WAAW,KAAK,OAAO;AACpC,UAAM,SAAoB,EAAE,MAAA;AAC5B,QAAI,MAAM;AACR,YAAM,MAAM,KAAK,CAAC,KAAK,KAAK,CAAC;AAC7B,UAAI,YAAY,MAAM;AACtB,YAAM,QAAO,UAAK,CAAC,MAAN,mBAAS;AACtB,UAAI,aAAa,QAAQ;AAAA,IAC3B;AACA,UAAM,QAAQ,uBAAuB,OAAO;AAC5C,QAAI,OAAO;AACT,aAAO,UAAU;AACjB,UAAI,CAAC,OAAO,MAAO,QAAO,QAAQ,MAAM,MAAM,IAAI,EAAE,CAAC;AAAA,IACvD;AACA,YAAQ,KAAK,MAAM;AAAA,EACrB;AAEA,MAAI,CAAC,SAAS;AACZ,WAAO,EAAE,UAAU,SAAS,GAAC;AAAA,EAC/B;AAEA,SAAO;AAAA,IACL,UAAU,UACP,KAAK,IAAI,EACT,QAAQ,WAAW,MAAM,EACzB,QAAQ,QAAQ,EAAE,EAClB,QAAQ,QAAQ,EAAE;AAAA,IACrB;AAAA,EAAA;AAEJ;AC/NA,MAAM,+BAA+B;AAG9B,SAAS,gCACd,WACA,iBACS;AACT,QAAM,MAAM,aAAa,IAAI,KAAA;AAC7B,QAAM,KAAK,mBAAmB,IAAI,KAAA;AAClC,MAAI,CAAC,MAAM,CAAC,EAAG,QAAO;AACtB,MAAI,OAAO,EAAG,QAAO;AACrB,MAAI,CAAC,GAAG,WAAW,CAAC,EAAG,QAAO;AAC9B,QAAM,OAAO,GAAG,MAAM,EAAE,MAAM;AAC9B,SAAO,SAAS,MAAM,6BAA6B,KAAK,IAAI;AAC9D;AAMO,SAAS,iCACd,iBACA,YACoB;AACpB,MAAI,oBAAoB,UAAa,oBAAoB,MAAM;AAC7D,WAAO;AAAA,EACT;AACA,QAAM,IAAI,gBAAgB,KAAA;AAC1B,MAAI,CAAC,EAAG,QAAO;AACf,QAAM,SAAS,IAAI,IAAI,UAAU;AACjC,MAAI,OAAO,IAAI,CAAC,EAAG,QAAO;AAC1B,QAAM,MAAM,WAAW,KAAK,CAAC,MAAM,gCAAgC,GAAG,CAAC,CAAC;AACxE,SAAO,OAAO;AAChB;AAEO,SAAS,+BAEd,KAAQ,YAAyB;AACjC,SAAO;AAAA,IACL,GAAG;AAAA,IACH,GAAI,IAAI,MAAM,SACV,EAAE,GAAG,iCAAiC,IAAI,GAAG,UAAU,EAAA,IACvD,CAAA;AAAA,IACJ,GAAI,IAAI,MAAM,SACV,EAAE,GAAG,iCAAiC,IAAI,GAAG,UAAU,EAAA,IACvD,CAAA;AAAA,IACJ,GAAI,IAAI,YAAY,SAChB,EAAE,SAAS,iCAAiC,IAAI,SAAS,UAAU,EAAA,IACnE,CAAA;AAAA,IACJ,GAAI,IAAI,aAAa,SACjB;AAAA,MACE,UAAU,iCAAiC,IAAI,UAAU,UAAU;AAAA,IAAA,IAErE,CAAA;AAAA,IACJ,GAAI,IAAI,gBAAgB,SACpB;AAAA,MACE,aAAa;AAAA,QACX,IAAI;AAAA,QACJ;AAAA,MAAA;AAAA,IACF,IAEF,CAAA;AAAA,IACJ,GAAI,IAAI,WAAW,SACf,EAAE,QAAQ,iCAAiC,IAAI,QAAQ,UAAU,MACjE,CAAA;AAAA,EAAC;AAET;ACtEO,MAAM,kBAAkB;AAAA,EAC7B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAIO,SAAS,eAAe,OAAuC;AACpE,SACE,OAAO,UAAU,YAChB,gBAAsC,SAAS,KAAK;AAEzD;ACRA,MAAM,mBAAmB;AAGzB,MAAM,qBACJ;AAEF,SAAS,SAAS,MAAwB;AACxC,QAAM,UAAU,KAAK,KAAA;AACrB,QAAM,QACJ,QAAQ,WAAW,GAAG,KAAK,QAAQ,SAAS,GAAG,IAC3C,QAAQ,MAAM,GAAG,EAAE,IACnB;AACN,SAAO,MAAM,MAAM,GAAG,EAAE,IAAI,CAAC,MAAM,EAAE,MAAM;AAC7C;AAEA,SAAS,eAAe,MAAuB;AAC7C,QAAM,QAAQ,SAAS,IAAI;AAC3B,SAAO,MAAM,SAAS,KAAK,MAAM,MAAM,CAAC,MAAM,WAAW,KAAK,CAAC,CAAC;AAClE;AAEA,SAAS,WAAW,KAA8B;AAChD,QAAM,IAAI,IAAI,KAAA;AACd,MAAI,MAAM,GAAI,QAAO;AACrB,QAAM,aAAa,EAAE,QAAQ,MAAM,EAAE;AACrC,MAAI,4CAA4C,KAAK,UAAU,GAAG;AAChE,UAAM,IAAI,OAAO,UAAU;AAC3B,QAAI,CAAC,OAAO,MAAM,CAAC,EAAG,QAAO;AAAA,EAC/B;AACA,SAAO;AACT;AAGO,SAAS,cAAc,UAIrB;AACP,QAAM,QAAQ,mBAAmB,KAAK,QAAQ;AAC9C,MAAI,CAAC,MAAO,QAAO;AACnB,QAAM,aAAa,IAAI,MAAM,CAAC,CAAC;AAC/B,QAAM,UAAU,IAAI,MAAM,CAAC,CAAC;AAC5B,MAAI,CAAC,eAAe,OAAO,EAAG,QAAO;AAErC,QAAM,UAAU,SAAS,UAAU;AACnC,MAAI,QAAQ,WAAW,KAAK,QAAQ,MAAM,CAAC,MAAM,CAAC,CAAC,EAAG,QAAO;AAE7D,QAAM,UAAyB,QAAQ,IAAI,CAAC,OAAO;AAAA,IACjD,OAAO;AAAA,IACP,WAAW;AAAA,EAAA,EACX;AAEF,QAAM,OAAO,MAAM,CAAC,KAAK;AACzB,QAAM,aAAwC,CAAA;AAC9C,aAAW,QAAQ,KAAK,MAAM,IAAI,GAAG;AACnC,QAAI,CAAC,KAAK,OAAQ;AAClB,QAAI,CAAC,KAAK,SAAS,GAAG,EAAG;AACzB,UAAM,QAAQ,SAAS,IAAI;AAC3B,QAAI,MAAM,WAAW,EAAG;AACxB,UAAM,MAA+B,CAAA;AACrC,YAAQ,QAAQ,CAAC,GAAG,MAAM;AACxB,UAAI,CAAC,IAAI,WAAW,MAAM,CAAC,KAAK,EAAE;AAAA,IACpC,CAAC;AACD,eAAW,KAAK,GAAG;AAAA,EACrB;AAEA,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA,WAAW,MAAM,CAAC,EAAE;AAAA,EAAA;AAExB;AAEA,SAAS,0BAA0B,QAA0B;AAC3D,MAAI,MAAM,QAAQ,MAAM,GAAG;AACzB,WAAO,OAAO;AAAA,MACZ,CAAC,MACC,KACA,OAAO,MAAM,YACb,eAAgB,KAChB,OAAQ,EAAkB,cAAc;AAAA,IAAA;AAAA,EAE9C;AACA,MAAI,UAAU,OAAO,WAAW,UAAU;AACxC,UAAM,IAAI;AACV,QAAI,MAAM,QAAQ,EAAE,MAAM,EAAG,QAAO;AACpC,QAAI,OAAO,EAAE,cAAc,SAAU,QAAO;AAAA,EAC9C;AACA,SAAO;AACT;AAGO,SAAS,mBAAmB,KAAoC;AACrE,MAAI,OAAO,KAAM,QAAO;AACxB,MAAI,MAAM,QAAQ,GAAG,GAAG;AACtB,WAAO,IAAI;AAAA,MACT,CAAC,MACC,CAAC,CAAC,KAAK,OAAO,MAAM,YAAY,OAAQ,EAAkB,cAAc;AAAA,IAAA;AAAA,EAE9E;AACA,MAAI,OAAO,QAAQ,SAAU,QAAO;AACpC,QAAM,IAAI;AACV,MAAI,MAAM,QAAQ,EAAE,MAAM,GAAG;AAC3B,WAAO,mBAAmB,EAAE,MAAM;AAAA,EACpC;AACA,MAAI,OAAO,EAAE,cAAc,UAAU;AACnC,WAAO,CAAC,CAAgB;AAAA,EAC1B;AACA,SAAO;AACT;AAEA,SAAS,aACP,KACA,SACa;AACb,MAAI,IAAI,KAAK,IAAI,EAAG,QAAO;AAC3B,MAAI,QAAQ,SAAS,EAAG,QAAO;AAC/B,SAAO;AAAA,IACL,GAAG;AAAA,IACH,GAAG,IAAI,KAAK,QAAQ,CAAC,EAAE;AAAA,IACvB,GAAG,IAAI,KAAK,QAAQ,CAAC,EAAE;AAAA,EAAA;AAE3B;AAEA,SAAS,WAAW,OAA2B,MAAyB;AACtE,MAAI,CAAC,MAAO,QAAO;AACnB,SAAO,KAAK,SAAS,KAAK;AAC5B;AAMO,SAAS,sBACd,SACA,SACA,YACqB;AACrB,QAAM,OAAO,QAAQ,IAAI,CAAC,MAAM,EAAE,SAAS;AAC3C,QAAM,aAA4B,CAAA;AAElC,aAAW,OAAO,SAAS;AACzB,QAAI,CAAC,eAAe,IAAI,SAAS,EAAG;AACpC,QAAI,MAAM,+BAA+B,EAAE,GAAG,IAAA,GAAO,IAAI;AACzD,UAAM,OAAO,IAAI;AACjB,QAAI,SAAS,SAAS,SAAS,SAAS;AACtC,YAAM,aAAa,KAAK,OAAO;AAAA,IACjC;AACA,QAAI,CAAC,WAAW,IAAI,GAAG,IAAI,KAAK,CAAC,WAAW,IAAI,GAAG,IAAI,EAAG;AAE1D,QAAI,IAAI,YAAY;AAClB,YAAM,EAAE,YAAY,OAAO,GAAG,SAAS;AACvC,YAAM;AAAA,IACR;AACA,eAAW,KAAK,GAAG;AAAA,EACrB;AAEA,MAAI,WAAW,WAAW,EAAG,QAAO;AACpC,SAAO,EAAE,QAAQ,YAAY,SAAS,WAAA;AACxC;AAGO,SAAS,oBAAoB,MAAmC;AACrE,MAAI;AACJ,MAAI;AACF,aAAS,KAAK,MAAM,KAAK,KAAA,CAAM;AAAA,EACjC,QAAQ;AACN,WAAO;AAAA,EACT;AACA,MAAI,CAAC,UAAU,OAAO,WAAW,SAAU,QAAO;AAClD,QAAM,IAAI;AAGV,MAAI,MAAM,QAAQ,EAAE,OAAO,KAAK,MAAM,QAAQ,EAAE,UAAU,GAAG;AAC3D,UAAMA,WAAU,mBAAmB,EAAE,UAAU,CAAC,KAAK,CAAA;AACrD,UAAM,UAAW,EAAE,WAA6B,CAAA;AAChD,UAAM,aACH,EAAE,cACF,EAAE,QACH,CAAA;AACF,QAAI,CAAC,QAAQ,QAAQ;AAEnB,UAAI,MAAM,QAAQ,EAAE,IAAI,KAAK,OAAO,EAAE,cAAc,UAAU;AAC5D,cAAM,aAA4B;AAAA,UAChC,EAAE,OAAO,OAAO,EAAE,KAAK,GAAG,GAAG,WAAW,IAAA;AAAA,UACxC,EAAE,OAAO,OAAO,EAAE,KAAK,GAAG,GAAG,WAAW,IAAA;AAAA,QAAI;AAE9C,cAAM,OAAQ,EAAE,KAAmB,IAAI,CAAC,SAAS;AAC/C,cAAI,QAAQ,OAAO,SAAS,SAAU,QAAO;AAC7C,iBAAO,CAAA;AAAA,QACT,CAAC;AACD,eAAO;AAAA,UACL,CAAC,EAAE,WAAW,OAAO,EAAE,SAAS,GAAG,GAAG,KAAK,GAAG,KAAK,OAAO,EAAE,OAA6B;AAAA,UACzF;AAAA,UACA;AAAA,QAAA;AAAA,MAEJ;AACA,aAAO;AAAA,IACT;AACA,UAAM,UAAyB,QAAQ;AAAA,MAAI,CAAC,MAC1C,OAAO,MAAM,WACT,EAAE,OAAO,GAAG,WAAW,MACvB;AAAA,QACE,OAAO,OAAQ,EAAkB,SAAU,EAAkB,SAAS;AAAA,QACtE,WAAW,OAAQ,EAAkB,aAAc,EAAkB,KAAK;AAAA,MAAA;AAAA,IAC5E;AAEN,WAAO,sBAAsBA,UAAS,SAAS,UAAU;AAAA,EAC3D;AAEA,QAAM,UAAU,mBAAmB,MAAM;AACzC,MAAI,CAAC,QAAS,QAAO;AACrB,SAAO;AACT;AAOO,SAAS,0BACd,KAC+C;AAC/C,QAAM,eAAe,IAAI,QAAQ,QAAQ,EAAE;AAC3C,QAAM,SAAS,IAAI,SAAS,aAAa;AACzC,MAAI,CAAC,aAAa,WAAW,MAAM,EAAG,QAAO;AAE7C,QAAM,aAAa,aAAa,QAAQ,KAAK;AAC7C,MAAI,aAAa,EAAG,QAAO;AAC3B,QAAM,eAAe,aAAa,MAAM,GAAG,aAAa,CAAC;AACzD,QAAM,KAAK,iBAAiB,KAAK,aAAa,MAAM;AACpD,MAAI,CAAC,GAAI,QAAO;AAEhB,MAAI;AACJ,MAAI;AACF,WAAO,KAAK,MAAM,GAAG,CAAC,CAAC;AAAA,EACzB,QAAQ;AACN,WAAO;AAAA,EACT;AACA,MAAI,CAAC,0BAA0B,IAAI,EAAG,QAAO;AAE7C,QAAM,UAAU,mBAAmB,IAAI;AACvC,MAAI,CAAC,WAAW,QAAQ,WAAW,EAAG,QAAO;AAE7C,MAAI,QAAQ,MAAM,CAAC,MAAM,EAAE,cAAc,OAAO,EAAG,QAAO;AAE1D,QAAM,kBAAkB,aAAa,MAAM,aAAa,CAAC;AAEzD,QAAM,eAAe,gBAAgB,QAAQ,UAAU,IAAI;AAC3D,QAAM,aAAa,aAAa,OAAO,IAAI;AAC3C,MAAI,aAAa,EAAG,QAAO;AAC3B,QAAM,cAAc,aAAa,MAAM,GAAG,UAAU;AACpD,MAAI,YAAY,QAAQ,OAAO,EAAE,MAAM,GAAI,QAAO;AAElD,QAAM,UAAU,aAAa,MAAM,UAAU;AAC7C,QAAM,QAAQ,cAAc,OAAO;AACnC,MAAI,CAAC,MAAO,QAAO;AAEnB,QAAM,UAAU;AAAA,IACd;AAAA,IACA,MAAM;AAAA,IACN,MAAM;AAAA,EAAA;AAER,MAAI,CAAC,QAAS,QAAO;AAKrB,QAAM,kBAAkB,gBAAgB,OAAO,IAAI;AACnD,QAAM,MAAM,IAAI;AAAA,IACd;AAAA,IACA,SAAS,aAAa,SAAS,kBAAkB,MAAM;AAAA,EAAA;AAGzD,SAAO,EAAE,SAAS,IAAA;AACpB;AAGO,SAAS,sBAAsB,SAA+B;AACnE,QAAM,OACJ,QAAQ,OAAO,WAAW,IAAI,QAAQ,OAAO,CAAC,IAAI,QAAQ;AAC5D,QAAM,UAAU,QAAQ,KAAK,UAAU,IAAI,CAAC;AAC5C,QAAM,UAAU,QAAQ,QAAQ,IAAI,CAAC,MAAM,EAAE,KAAK;AAClD,QAAM,OAAO,QAAQ,QAAQ,IAAI,CAAC,MAAM,EAAE,SAAS;AACnD,QAAM,SAAS,KAAK,QAAQ,KAAK,KAAK,CAAC;AACvC,QAAM,MAAM,KAAK,QAAQ,IAAI,MAAM,KAAK,EAAE,KAAK,KAAK,CAAC;AACrD,QAAM,OAAO,QAAQ,WAAW,IAAI,CAAC,QAAQ;AAC3C,UAAM,QAAQ,KAAK,IAAI,CAAC,MAAM;AAC5B,YAAM,IAAI,IAAI,CAAC;AACf,UAAI,KAAK,KAAM,QAAO;AACtB,aAAO,OAAO,CAAC;AAAA,IACjB,CAAC;AACD,WAAO,KAAK,MAAM,KAAK,KAAK,CAAC;AAAA,EAC/B,CAAC;AACD,SAAO,CAAC,SAAS,IAAI,QAAQ,KAAK,GAAG,IAAI,EAAE,KAAK,IAAI;AACtD;AAMO,SAAS,uBAAuB,UAA0B;AAC/D,MAAI,CAAC,SAAU,QAAO;AACtB,MAAI,OAAO;AACX,MAAI,MAAM;AAEV,SAAO,KAAK,SAAS,GAAG;AACtB,UAAM,MAAM,KAAK,QAAQ,MAAM;AAC/B,QAAI,MAAM,GAAG;AACX,aAAO;AACP;AAAA,IACF;AACA,WAAO,KAAK,MAAM,GAAG,GAAG;AACxB,UAAM,UAAU,0BAA0B,KAAK,MAAM,GAAG,CAAC;AACzD,QAAI,CAAC,SAAS;AAEZ,aAAO;AACP,aAAO,KAAK,MAAM,MAAM,CAAC;AACzB;AAAA,IACF;AACA,UAAM,YAAY,KAAK,UAAU;AAAA,MAC/B,QAAQ,QAAQ,QAAQ;AAAA,MACxB,SAAS,QAAQ,QAAQ;AAAA,MACzB,YAAY,QAAQ,QAAQ;AAAA,IAAA,CAC7B;AACD,WAAO;AAAA,EAAoB,SAAS;AAAA;AACpC,WAAO,KAAK,MAAM,MAAM,QAAQ,IAAI,MAAM;AAE1C,QAAI,KAAK,WAAW,IAAI,EAAG;AAAA,aAEhB,KAAK,SAAS,GAAG;AAC1B,aAAO;AAAA,IACT;AAAA,EACF;AAEA,SAAO;AACT;;;;;;;;;;;;;;;;;ACzUO,SAAS,cAAc,EAAE,MAAM,WAAW,GAAG,QAA4B;AAC9E,SACE;AAAA,IAAC;AAAA,IAAA;AAAA,MACE,GAAG;AAAA,MACJ,WACE,YAAY,GAAG,OAAO,aAAa,IAAI,SAAS,KAAK,OAAO;AAAA,MAE9D,yBAAyB,EAAE,QAAQ,KAAA;AAAA,IAAK;AAAA,EAAA;AAG9C;"}
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Loose header ↔ config field matching (ported from agentic-ui columnMatching).
2
+ * Loose header ↔ config field matching for chart data tables.
3
3
  * Zero runtime deps beyond string/regex.
4
4
  */
5
5
  /** Exact match, or column = field + parenthetical unit suffix. */
@@ -1,4 +1,4 @@
1
- /** MVP chart types aligned with agentic-ui (subset). */
1
+ /** MVP chart types (subset). */
2
2
  export declare const MVP_CHART_TYPES: readonly ["line", "bar", "column", "pie", "donut", "area"];
3
3
  export type MvpChartType = (typeof MVP_CHART_TYPES)[number];
4
4
  export declare function isMvpChartType(value: unknown): value is MvpChartType;
@@ -29,7 +29,7 @@ export interface CommentAnchorResult {
29
29
  ranges: CommentRange[];
30
30
  status: CommentAnchorStatus;
31
31
  }
32
- /** djb2 哈希(捕获端与消费端必须用同一个实现;invret 接入时对齐)。 */
32
+ /** djb2 哈希(捕获端与消费端必须用同一个实现;接入时对齐)。 */
33
33
  export declare function blockTextHash(text: string): string;
34
34
  /** 主入口:评论列表 → 每条的 doc 区间 + 状态。 */
35
35
  export declare function mapCommentAnchors(doc: Node, comments: CommentAnchorInput[]): CommentAnchorResult[];
@@ -61,13 +61,24 @@ export interface EditorToolbarProps {
61
61
  /** 是否显示导入下拉,默认 `true`。卡片等场景可关掉。 */
62
62
  showImport?: boolean;
63
63
  labels?: Partial<ToolbarLabels>;
64
+ /**
65
+ * 工具栏最右端的搜索入口(放大镜)。**不传就不渲染**——条子本身归
66
+ * `<MarkdownWysiwygEditor>`,这里只给一个入口,宿主接上自己的编辑器句柄即可:
67
+ * `onSearch={() => handle.current?.openFind()}`。
68
+ *
69
+ * 条子归编辑器渲染(`findBar` 开)时才接;`findBar={false}` 的宿主自己摆条子,
70
+ * 这个入口点了也不会弹东西。只读态照样可查,所以不随 `editable` 灰显。
71
+ */
72
+ onSearch?: () => void;
73
+ /** 条子当前是否开着(驱动按钮的 aria-pressed / 高亮)。不传恒为未激活。 */
74
+ searchActive?: boolean;
64
75
  /** 追加到 More 菜单末尾的自定义项。 */
65
76
  extraToolbarItems?: ExtraToolbarItem[];
66
77
  className?: string;
67
78
  }
68
79
  export type { ImportMenuItem };
69
80
  /**
70
- * 顶部操作栏(antd-free,Radix Popover 菜单 + 内联 SVG 图标)。位置由父级控制,
81
+ * 顶部操作栏(零 UI 框架依赖,Radix Popover 菜单 + 内联 SVG 图标)。位置由父级控制,
71
82
  * 这里只渲染按钮 + 反映 editor 激活态。不含业务耦合,扩展项经 extraToolbarItems 注入。
72
83
  */
73
- export declare function EditorToolbar({ editor, onImageUpload, onError, onImportDocument, importAccept, importMenuItems, showImport, labels, extraToolbarItems, className, }: EditorToolbarProps): import("react").JSX.Element;
84
+ export declare function EditorToolbar({ editor, onImageUpload, onError, onImportDocument, importAccept, importMenuItems, showImport, labels, onSearch, searchActive, extraToolbarItems, className, }: EditorToolbarProps): import("react").JSX.Element;
@@ -0,0 +1 @@
1
+ export {};
@@ -17,11 +17,4 @@ export interface FindReplaceBarProps {
17
17
  /** 根节点内联样式(编辑器用它落 findBarOffset;宿主自绘时也可用)。 */
18
18
  style?: CSSProperties;
19
19
  }
20
- /**
21
- * 查找替换浮动条。匹配、高亮、替换全部由官方
22
- * `@tiptap/extension-find-and-replace` 承担(含 RE2 安全正则、跨标记匹配、单事务全部替换),
23
- * 这里只做输入、计数、开关与键位。
24
- *
25
- * 打开即把查询推给编辑器,关闭时 `clearSearch()`——高亮是会话级的,不留残影。
26
- */
27
20
  export declare function FindReplaceBar({ editor, onClose, labels, inputRef, className, style, }: FindReplaceBarProps): import("react").JSX.Element | null;
@@ -0,0 +1,19 @@
1
+ import { Editor } from '@tiptap/core';
2
+ import { ToolbarLabels } from '../labels';
3
+ export interface ImageAlignToolsProps {
4
+ /** 编辑器实例。只读态不出工具条。 */
5
+ editor: Editor;
6
+ /** 已合并默认值的工具栏文案(工具条与三个按钮的 aria-label 都取自它)。 */
7
+ labels: ToolbarLabels;
8
+ }
9
+ /**
10
+ * 图片对齐工具条(悬停式,与表格悬停手柄同一套交互):鼠标落在图片上时,图片上方
11
+ * (放不下就下方)浮出一排左 / 中 / 右按钮,点一下即对齐,当前对齐方向高亮。
12
+ *
13
+ * fixed 定位 + portal 到 body:它要贴着图片边缘,而工具栏并不在编辑器 DOM 内。
14
+ * 滚动 / 缩放时重新量悬停的那张图让工具条跟着走。
15
+ *
16
+ * 点击走 {@link setImageAlignAt}(按 DOM 位置改 attrs):鼠标只是飘过图片时图片
17
+ * 并未被选中,不能像快捷键那样走 selection。
18
+ */
19
+ export declare function ImageAlignTools({ editor, labels }: ImageAlignToolsProps): import('react').ReactPortal | null;
@@ -0,0 +1,10 @@
1
+ import { NodeViewProps } from '@tiptap/react';
2
+ /**
3
+ * 图片上传占位块的 NodeView:空态是点击 / 拖拽区,排队后是文件行(进度条 + 百分比 +
4
+ * 移除),全部成功时就地把自己换成图片节点。上传函数与回调都来自扩展 options。
5
+ *
6
+ * 组件结构 / 文件队列 / 拖拽 / 中止逻辑按官方 UI Components 的 `image-upload-node`
7
+ * 移植(MIT,见 THIRD_PARTY_LICENSES.md 第 2 节),按本库的样式体系(CSS Modules +
8
+ * --tmr-* 变量)与 labels 注入重写。
9
+ */
10
+ export declare function ImageUploadView(props: NodeViewProps): import("react").JSX.Element;
@@ -0,0 +1,28 @@
1
+ import { Editor } from '@tiptap/react';
2
+ import { CSSProperties, ReactNode } from 'react';
3
+ import { LinkPopoverLabels } from '../labels';
4
+ export interface LinkPopoverProps {
5
+ /** 编辑器实例。 */
6
+ editor: Editor;
7
+ /**
8
+ * 触发器:传宿主自己的按钮元素。Radix 的 `asChild` 要接管它的点击 / ref / aria——
9
+ * 传函数组件时得先 `forwardRef`(本库的 `<EditorToolbar>` 就是这么接的)。
10
+ */
11
+ trigger: ReactNode;
12
+ labels?: Partial<LinkPopoverLabels>;
13
+ /** 挂到浮层根节点上的 class / 内联样式(定位与层级归宿主,Radix 只管浮层方向)。 */
14
+ className?: string;
15
+ style?: CSSProperties;
16
+ }
17
+ /**
18
+ * 链接编辑浮层:输入地址 + 应用 / 在新窗口打开 / 移除。
19
+ *
20
+ * 逻辑全在 `linkEditing.ts`(从官方 UI Components 的 `useLinkPopover` 搬的),这里只负责输入、
21
+ * 按钮与状态。取代表驱动器的 `window.prompt`:能**编辑已有链接**(打开即预填当前地址,
22
+ * 而旧实现是「在链接里点一下就删除」)、能打开、能移除,且 `window.prompt` 在 Electron /
23
+ * 部分 WebView 里根本不可用。
24
+ *
25
+ * 地址被编辑器拒掉时(Tiptap 的 `isAllowedUri`)浮层不关,输入框转成危险色并带上 title,
26
+ * 让用户改——静默什么都不发生是最难查的。
27
+ */
28
+ export declare function LinkPopover({ editor, trigger, labels, className, style, }: LinkPopoverProps): import("react").JSX.Element;
@@ -0,0 +1 @@
1
+ export {};