@plannotator/ui 0.53.1 → 0.54.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.
- package/HANDOFF.md +217 -11
- package/README.md +65 -10
- package/components/AgentToolAnnouncementDialog.tsx +1 -1
- package/components/AgentsTab.tsx +1 -1
- package/components/AnnotationPanel.tsx +142 -24
- package/components/AnnotationSidebar.tsx +1 -1
- package/components/AnnotationToolbar.tsx +64 -9
- package/components/ApproveDropdown.tsx +7 -9
- package/components/AskSessionAnnouncementDialog.tsx +1 -1
- package/components/AttachmentsButton.tsx +1 -1
- package/components/BlockRenderer.tsx +19 -5
- package/components/CodeFilePopout.tsx +139 -4
- package/components/CommentPopover.tsx +44 -8
- package/components/DecisionControl.tsx +31 -5
- package/components/DiagramBlock.tsx +17 -12
- package/components/EditorAnnotationCard.tsx +1 -1
- package/components/ExportModal.tsx +77 -7
- package/components/HostTitle.tsx +0 -4
- package/components/ImageAnnotator/Toolbar.tsx +25 -1
- package/components/ImageAnnotator/index.tsx +24 -4
- package/components/ImageLightbox.tsx +658 -120
- package/components/ImagePeek.tsx +38 -4
- package/components/ImageRegions.tsx +369 -0
- package/components/ImageThumbnail.tsx +1 -1
- package/components/ImageZoomStage.tsx +796 -0
- package/components/InboxAnnouncementDialog.tsx +202 -0
- package/components/InlineMarkdown.tsx +152 -24
- package/components/KeyboardShortcuts.tsx +23 -0
- package/components/LazyBlockBoundary.tsx +100 -0
- package/components/ListMarker.tsx +3 -3
- package/components/LookAndFeelAnnouncementDialog.tsx +12 -2
- package/components/MenuVersionSection.tsx +31 -9
- package/components/PlanDecisionMenuExtras.tsx +129 -0
- package/components/PlanHeaderMenu.tsx +15 -1
- package/components/ProviderIcons.tsx +8 -0
- package/components/RefHoverCard.tsx +913 -0
- package/components/SendFailureToast.tsx +126 -0
- package/components/Settings.tsx +617 -126
- package/components/TerminalToolsAnnouncementDialog.tsx +1 -1
- package/components/ThemeProvider.tsx +14 -1
- package/components/ThemeTab.tsx +16 -24
- package/components/Tooltip.tsx +49 -18
- package/components/Viewer.tsx +280 -41
- package/components/ai/DocumentAIChatPanel.tsx +29 -3
- package/components/ai/NewChatButton.tsx +74 -0
- package/components/blocks/CodeBlock.tsx +1 -1
- package/components/blocks/HtmlBlock.tsx +7 -1
- package/components/blocks/QuestionBlock.tsx +522 -62
- package/components/diagram/DiagramComposer.tsx +5 -1
- package/components/diagram/DiagramPending.tsx +12 -2
- package/components/diagram/DiagramViewer.tsx +19 -3
- package/components/html-embed/HtmlEmbedBlock.tsx +374 -0
- package/components/html-embed/SvgFenceBlock.tsx +46 -0
- package/components/html-embed/index.ts +3 -0
- package/components/html-embed/languages.ts +15 -0
- package/components/html-embed/loadGate.ts +107 -0
- package/components/html-viewer/HtmlViewer.tsx +47 -6
- package/components/html-viewer/bridge-script.asset.js +173 -1
- package/components/html-viewer/bridge-script.ts +173 -1
- package/components/html-viewer/useHtmlAnnotation.ts +125 -0
- package/components/imageZoom.ts +265 -0
- package/components/plan-diff/PlanCleanDiffView.tsx +1 -1
- package/components/plan-diff/PlanDiffViewer.tsx +13 -2
- package/components/plan-diff/PlanRawDiffView.tsx +1 -1
- package/components/sidebar/FileBrowser.tsx +16 -2
- package/components/sidebar/SidebarTabs.tsx +3 -0
- package/config/index.ts +3 -0
- package/config/reviewView.ts +41 -1
- package/config/settings.ts +155 -4
- package/configure.ts +28 -0
- package/hooks/pfm/useCodeFilePopout.ts +7 -1
- package/hooks/useAIChat.ts +72 -25
- package/hooks/useAnnotationDraft.ts +71 -6
- package/hooks/useAnnotationHighlighter.ts +57 -3
- package/hooks/useCodeAnnotationDraft.ts +113 -17
- package/hooks/useCookieHostSetting.ts +96 -0
- package/hooks/useDocumentFont.ts +17 -0
- package/hooks/useDocumentTextSize.ts +20 -0
- package/hooks/useFileBrowser.ts +10 -1
- package/hooks/useLinkedDoc.ts +226 -13
- package/hooks/usePackageChangesSetting.ts +90 -0
- package/hooks/usePlanLookDialog.ts +67 -0
- package/hooks/useSharing.ts +11 -7
- package/hooks/useThemePaletteHalves.ts +37 -0
- package/hooks/useUpdateCheck.ts +22 -4
- package/hooks/useUpdateToast.ts +1 -1
- package/package.json +23 -22
- package/shortcuts/code-review/annotationToolbar.shortcuts.ts +1 -0
- package/shortcuts/code-review/explorer.shortcuts.ts +82 -0
- package/shortcuts/core.ts +63 -4
- package/shortcuts/index.ts +1 -0
- package/shortcuts/plan-review/annotationPanel.shortcuts.ts +1 -1
- package/shortcuts/plan-review/commentPopover.shortcuts.ts +1 -0
- package/shortcuts/runtime.ts +4 -3
- package/styles.css +1 -1
- package/theme.css +179 -1
- package/themes/plannotator-graphite.css +66 -0
- package/types.ts +26 -9
- package/utils/agentSwitch.ts +41 -20
- package/utils/agentToolSetting.ts +1 -1
- package/utils/aiChatFormat.ts +23 -7
- package/utils/annotationOwnsHighlight.ts +4 -2
- package/utils/asciidocParser.ts +570 -0
- package/utils/autoUpdateNotice.ts +24 -0
- package/utils/belowTitleSlot.ts +174 -0
- package/utils/codeLineScroll.ts +100 -0
- package/utils/commentSubmitKey.ts +48 -0
- package/utils/cssFontFamily.ts +65 -0
- package/utils/decisionSpec.ts +190 -2
- package/utils/defaultNotesApp.ts +1 -1
- package/utils/diffFonts.ts +4 -1
- package/utils/documentFont.ts +68 -0
- package/utils/documentTextSize.ts +54 -0
- package/utils/fontDetect.ts +63 -0
- package/utils/inboxAnnouncement.ts +78 -0
- package/utils/lookAndFeelAnnouncement.ts +82 -5
- package/utils/markdownExtensions.ts +2 -2
- package/utils/mermaid.ts +20 -3
- package/utils/mermaidTheme.ts +3 -51
- package/utils/notion.ts +58 -0
- package/utils/parser.ts +28 -1
- package/utils/platform.ts +46 -0
- package/utils/refCard.ts +82 -0
- package/utils/richClipboard.ts +380 -0
- package/utils/richClipboardLists.ts +57 -0
- package/utils/richCopyPolicy.ts +22 -0
- package/utils/scrollAnchor.ts +82 -0
- package/utils/sectionRefs.ts +123 -0
- package/utils/selectionBoundary.ts +40 -0
- package/utils/sendFailure.ts +85 -0
- package/utils/sharing.ts +5 -2
- package/utils/themeRegistry.ts +10 -0
- package/utils/themeTokens.ts +74 -0
- package/utils/updateNotice.ts +30 -8
- package/utils/vscodePanel.ts +9 -0
package/README.md
CHANGED
|
@@ -36,6 +36,8 @@ configurePlannotatorUI({
|
|
|
36
36
|
alertIconRenderer, // (name) => ReactNode | null for a GitHub alert title line's <!-- icon: name --> (default: null, the type's icon)
|
|
37
37
|
fenceTheme, // (colorTheme, mode) => Shiki theme name | undefined for code fences (default: SHIKI_THEME_MAP, else pierre-dark/light)
|
|
38
38
|
componentTooltips, // true: ui's own hover text (components/HostTitle) draws as ui's Tooltip, not a native title (ui 0.53.0; default false)
|
|
39
|
+
digitHexIssueRefs, // true: digit-only #123456 / #12345678 link as issue refs (when a ref link can be built), not hex swatches (default false)
|
|
40
|
+
refCardTransport, // (href, ref, signal) => card data for an issue / PR / MR link; with one, useRefHoverCards(rootRef) draws hover cards (default: none, no card, no request). refCardRouteTransport(route) builds one over a server route
|
|
39
41
|
loadSettingsFromBackend, // true: re-hydrate settings from your (synchronous) storageBackend after install, applied last
|
|
40
42
|
});
|
|
41
43
|
```
|
|
@@ -62,10 +64,10 @@ Building your own tooltip and removing the built-in double-click reset are host-
|
|
|
62
64
|
The Mermaid runtime, the Graphviz engine, KaTeX and the username dictionary are off the static import graph of `Viewer`, so a host that bundles by route does not download them for a plain markdown read. Graphviz has its own slot since 0.41.0 (`utils/graphviz`, the same shape as Mermaid's: lazy `import('@viz-js/viz')` on the first dot fence, or `setGraphvizRuntime` from the host) and the block shows the source fence under a status until the SVG lands. Mermaid, Graphviz, KaTeX and the dictionary sit behind synchronous slots:
|
|
63
65
|
|
|
64
66
|
- **Math.** Without registration, a math node renders its TeX as text in the same wrapper (same `data-math-tex` / `data-math-display` / `aria-label` / class names), loads KaTeX via `import('katex')`, and re-renders typeset. To keep math typeset on the very first commit, as Plannotator does, add one line to your entry: `import "@plannotator/ui/utils/math-eager";`. To put KaTeX and its stylesheet on one lazy chunk instead, pass `mathRendererLoader`. The stylesheet remains your job either way (see "Consuming it", step 3). The default `import('katex')` is the only runtime mention of `katex` in the package and lives in `utils/math-default-loader` (0.33.0), called only while no loader is registered; a registered loader is never backfilled by it, though a default load already in flight at registration still fills the slot (pre-existing), so register the loader before the first math render. Chunk emission is static, so a bundler still emits that chunk (never requested) unless you alias the module away; see HANDOFF.md "Lazy renderers and eager entries" for the two-line alias. The Mermaid runtime has its own `import("katex")` for `$$` labels, which leaves a second, shared KaTeX chunk in a host build even with the alias; since 0.34.0 a host redirects that one import (for importers inside the `mermaid` package only) to `@plannotator/ui/utils/mermaid-math-slot`, which typesets the labels through your registered renderer, so one KaTeX chunk remains and it is yours. Recipe and measurement in HANDOFF.md, same section. `resetMathRenderer()` empties the slot only and keeps a registered loader (0.34.0); `setMathRendererLoader(null)` is the explicit way back to the package default.
|
|
65
|
-
- **Mermaid.** Without registration, the first diagram on a page fetches the runtime through `import('mermaid')` and the block shows the source fence under a "Rendering diagram" status until the SVG lands; a failed import is dropped from the memo, re-attempted once after a short delay, and the error panel (with the source) offers Retry, which issues another fresh attempt. **Since 0.40.0 (Mermaid 12.0.0, ELK layout by default, Safari 17.4+) this lazy path is Plannotator's own**: the plan editor no longer imports the eager entry, so a document with no diagram never downloads the runtime in a chunked build. `import "@plannotator/ui/utils/mermaid-eager";` in your entry registers and initializes the runtime at startup instead, if you would rather it never fail separately from the app. Mermaid 12 lays flowchart, state, class, ER and requirement diagrams out with ELK (orthogonal edges, different packing); every generated SVG `id` keeps its 11.x shape, but children of `g.edgePaths` are now in declaration order, so select edges by id (`{renderId}-L_{from}_{to}_{n}`, `{renderId}-edge{n}`), never by DOM position. Details in HANDOFF.md "Mermaid 12 (0.40.0)". **Diagrams follow the colour theme.** Before every render `MermaidBlock` calls `applyMermaidTheme` (`utils/mermaidTheme`), which reads the theme tokens off the document (`--background`, `--foreground`, `--card`, `--border`, `--muted`, `--muted-foreground`, `--primary`, the accent tokens and `--font-sans`; `readThemeTokens`), derives a complete `themeVariables` set for every diagram family from them (`buildMermaidThemeVariables(tokens, mode)`, pure; base theme `dark` under a dark resolved mode, `default` under light; every text-on-fill pair guarded to WCAG 4.5:1 and every line 3:1, rule in the module doc) and runs the global `mermaid.initialize` once per `(palette, mode, shadow amount)` key, re-rendering mounted diagrams when the key changes. The key comes from `useTheme()`, so a host that mounts `ThemeProvider` and ships `theme.css` gets diagrams in its palette with nothing to configure. **The node shadow follows the palette too.** Mermaid 12's neo look shadows every node from a fixed `rgba(185,185,185,1)` grey; the mapping keeps the look and replaces that filter, setting `themeVariables.dropShadow` from the page's own ground at `options.shadowAmount` (0..1, default `DEFAULT_MERMAID_SHADOW_AMOUNT` = **0.7** of Mermaid's geometry; 1 reproduces its `1px 2px 2px` exactly, 0 publishes `dropShadow: false` and the neo rules render `filter: none`). Its polarity follows the page — lighter than the ground on a dark page, darker on a light one, which is what Mermaid's own `insertLookDefs` does and what keeps a shadow visible at all. A host that wants Mermaid's grey passes its own `themeVariables.dropShadow` after ours; one that wants none passes `{ shadowAmount: 0 }`. `mermaidThemeKey(palette, mode, amount?)` appends the amount only when it is not the default, so a host that names none keeps the key it had. **Fallback contract:** with no tokens on the document (no `ThemeProvider`, no `theme.css`) `readThemeTokens` returns `undefined`, nothing is re-initialized, and the runtime keeps the static `MERMAID_CONFIG` it was initialized with, so such a host renders byte-identically to 0.39.0. `MERMAID_CONFIG` keeps its value and meaning (`securityLevel: 'strict'` pinned); the new exports are additive. Honest limit of any in-page retry: a browser records a failed module fetch in its module map for the page lifetime, so a fresh `import()` of the same chunk URL rejects without a request; the retry recovers failures after the fetch (engine instantiation, initialize) and hosts that version chunk URLs. A host that needs recovery from a failed first fetch uses versioned chunk URLs or a `vite:preloadError` reload at app level.
|
|
67
|
+
- **Mermaid.** Without registration, the first diagram on a page fetches the runtime through `import('mermaid')` and the block shows the source fence under a "Rendering diagram" status until the SVG lands; a failed import is dropped from the memo, re-attempted once after a short delay, and the error panel (with the source) offers Retry, which issues another fresh attempt. **Since 0.40.0 (Mermaid 12.0.0, ELK layout by default, Safari 17.4+) this lazy path is Plannotator's own**: the plan editor no longer imports the eager entry, so a document with no diagram never downloads the runtime in a chunked build. `import "@plannotator/ui/utils/mermaid-eager";` in your entry registers and initializes the runtime at startup instead, if you would rather it never fail separately from the app. Mermaid 12 lays flowchart, state, class, ER and requirement diagrams out with ELK (orthogonal edges, different packing); every generated SVG `id` keeps its 11.x shape, but children of `g.edgePaths` are now in declaration order, so select edges by id (`{renderId}-L_{from}_{to}_{n}`, `{renderId}-edge{n}`), never by DOM position. Details in HANDOFF.md "Mermaid 12 (0.40.0)". **Diagrams follow the colour theme.** Before every render `MermaidBlock` calls `applyMermaidTheme` (`utils/mermaidTheme`), which reads the theme tokens off the document (`--background`, `--foreground`, `--card`, `--border`, `--muted`, `--muted-foreground`, `--primary`, the accent tokens and `--font-sans`; `readThemeTokens`), derives a complete `themeVariables` set for every diagram family from them (`buildMermaidThemeVariables(tokens, mode)`, pure; base theme `dark` under a dark resolved mode, `default` under light; every text-on-fill pair guarded to WCAG 4.5:1 and every line 3:1, rule in the module doc) and runs the global `mermaid.initialize` once per `(palette, mode, shadow amount)` key, re-rendering mounted diagrams when the key changes. The key comes from `useTheme()`, so a host that mounts `ThemeProvider` and ships `theme.css` gets diagrams in its palette with nothing to configure. **The node shadow follows the palette too.** Mermaid 12's neo look shadows every node from a fixed `rgba(185,185,185,1)` grey; the mapping keeps the look and replaces that filter, setting `themeVariables.dropShadow` from the page's own ground at `options.shadowAmount` (0..1, default `DEFAULT_MERMAID_SHADOW_AMOUNT` = **0.7** of Mermaid's geometry; 1 reproduces its `1px 2px 2px` exactly, 0 publishes `dropShadow: false` and the neo rules render `filter: none`). Its polarity follows the page — lighter than the ground on a dark page, darker on a light one, which is what Mermaid's own `insertLookDefs` does and what keeps a shadow visible at all. A host that wants Mermaid's grey passes its own `themeVariables.dropShadow` after ours; one that wants none passes `{ shadowAmount: 0 }`. `mermaidThemeKey(palette, mode, amount?)` appends the amount only when it is not the default, so a host that names none keeps the key it had. **Fallback contract:** with no tokens on the document (no `ThemeProvider`, no `theme.css`) `readThemeTokens` returns `undefined`, nothing is re-initialized, and the runtime keeps the static `MERMAID_CONFIG` it was initialized with, so such a host renders byte-identically to 0.39.0. `MERMAID_CONFIG` keeps its value and meaning (`securityLevel: 'strict'` pinned); the new exports are additive. Honest limit of any in-page retry: a browser records a failed module fetch in its module map for the page lifetime, so a fresh `import()` of the same chunk URL rejects without a request; the retry recovers failures after the fetch (engine instantiation, initialize) and hosts that version chunk URLs. A host that needs recovery from a failed first fetch uses versioned chunk URLs or a `vite:preloadError` reload at app level. The `Viewer`'s lazy blocks (Mermaid, Graphviz, HTML and SVG embeds), the diagram popout and the Source pane each sit in `components/LazyBlockBoundary`: when a chunk cannot load, that one block shows its source under a quiet "Couldn't load this view. Reload" line ("This page was updated. Reload to see this" when the chunk answers 404), and the rest of the page stays mounted.
|
|
66
68
|
- **Identity.** With an `identityProvider` the generator is never called and the word lists stay out of your bundle. Without one, default names come from a small built-in pool of the same `adjective-noun-tater` shape; `import "@plannotator/ui/utils/identity-tater";` registers the full dictionary, or pass your own `identityGenerator`.
|
|
67
69
|
|
|
68
|
-
Plannotator's own entries import the eager math and identity modules (`math-eager` and `identity-tater` in both `packages/editor/App.tsx` and `packages/review-editor/App.tsx`), which is what keeps math typeset on the first commit and names minted from the full dictionary; neither app imports `mermaid-eager` since 0.40.0, so the Mermaid runtime rides a lazy chunk on the share portal and
|
|
70
|
+
Plannotator's own entries import the eager math and identity modules (`math-eager` and `identity-tater` in both `packages/editor/App.tsx` and `packages/review-editor/App.tsx`), which is what keeps math typeset on the first commit and names minted from the full dictionary; neither app imports `mermaid-eager` since 0.40.0, so the Mermaid runtime rides a lazy chunk, both on the share portal and in the plan and review apps (which since #1618 ship as one packed file whose server answers each chunk at `/assets/*`, instead of inlining them). `tests/entry-assets.test.ts` fails if an eager math/identity import is dropped or an eager Mermaid import creeps back. See HANDOFF.md "Lazy renderers and eager entries" and "Mermaid 12 (0.40.0)".
|
|
69
71
|
|
|
70
72
|
### Markdown editor extensions + wiki links (`MarkdownEditor` / `InlineMarkdown`)
|
|
71
73
|
|
|
@@ -237,9 +239,40 @@ exactly. The standalone `StickyHeaderLane` remains supported for Plannotator's
|
|
|
237
239
|
hidden-at-rest ghost lane, but its always-visible mode is an overlay and is not
|
|
238
240
|
the in-flow host integration.
|
|
239
241
|
|
|
242
|
+
#### Content under the title (`Viewer.belowTitle`; ui 0.54.0)
|
|
243
|
+
|
|
244
|
+
`belowTitle?: ReactNode` draws a host's node right under the document's
|
|
245
|
+
title, e.g. a references block (pull requests, depends on, links, referenced
|
|
246
|
+
by). Use it instead of inserting into the Viewer's DOM.
|
|
247
|
+
|
|
248
|
+
- **Where.** When the body's first block is a level-1 heading, right after
|
|
249
|
+
that heading (`data-placement="below-title"`). Otherwise (a body that opens
|
|
250
|
+
with a paragraph, a `## heading`, or a `# title` further down) at the top of
|
|
251
|
+
the body, before the first block and after the frontmatter card
|
|
252
|
+
(`data-placement="top"`). The heading itself is unchanged.
|
|
253
|
+
- **Outside the document text.** The wrapper `[data-viewer-below-title]` is
|
|
254
|
+
`annotation-exclude` (no comment starts in it, its text is never searched
|
|
255
|
+
or quoted), `data-pinpoint-ignore` (pinpoint and Vim skip it),
|
|
256
|
+
`data-print-hide`, and carries no `data-block-id`; heading slugs, the TOC,
|
|
257
|
+
exports and share links come from `blocks` and never see it.
|
|
258
|
+
- **Comment positions ignore it.** web-highlighter's `startMeta`/`endMeta`
|
|
259
|
+
count elements by tag across the whole article, so a `<p>` or `<a>` in the
|
|
260
|
+
slot would renumber later comments. The highlighter stores and restores
|
|
261
|
+
positions as if the slot were not there, so the same comment has the same
|
|
262
|
+
metas with or without the slot, and with any slot contents. A selection
|
|
263
|
+
from the title that ends in the slot is cut back to the end of the title.
|
|
264
|
+
Only the wrapper the Viewer draws counts: its attribute carries a per-load
|
|
265
|
+
value, so a `<div data-viewer-below-title>` an author writes as raw HTML is
|
|
266
|
+
never taken for it. Match the wrapper by presence
|
|
267
|
+
(`[data-viewer-below-title]`), not by value.
|
|
268
|
+
- **Absent is absent.** `undefined`, `null` and booleans draw no wrapper; the
|
|
269
|
+
DOM is exactly the one without the prop.
|
|
270
|
+
|
|
271
|
+
See HANDOFF.md § "Viewer `belowTitle` (published in ui 0.54.0)".
|
|
272
|
+
|
|
240
273
|
### Diagram engine (`components/diagram`; 0.41.0)
|
|
241
274
|
|
|
242
|
-
One renderer slot and one canvas render every Mermaid and Graphviz diagram — the fences in `Viewer` (`MermaidBlock`, `GraphvizBlock`), their popout, and whatever a host renders itself. `DiagramViewer` from `@plannotator/ui/components/diagram` takes `kind`, `source`, `theme` (`{ colorTheme, mode, shadowAmount? }`), `comments: DiagramComment[]` and `onCreateComment(anchor, text, additionalTargets)`; pass `onSave(source) => Promise<{ status: 'ok' } | { status: 'stale', currentSource }>` to get the Source pane (left of the canvas on desktop, under it on the phone; `readOnlySource` shows it without Save), and `sourceOpen` to toggle it. Zoom, pan and fit are the canvas's (wheel, drag, `+` `-` `0`, arrow keys); a click opens the composer at the part beneath while a drag pans (4 px threshold, 10 px for a finger), nothing highlights on a plain mouse-over (the ring under the pointer needs the platform modifier held), every edge carries an invisible 14 px hit path in one top layer and a click is resolved by priority over everything under the pointer (node, then edge, then cluster; an edge label is its edge), a click on no part comments on the whole diagram (kind `diagram`), sequence diagrams are addressable (actors, messages, notes, frames), a saved comment paints a ring and a numbered badge from the array order, and every comment re-resolves against each render through the engine's finder (id, then label, then unanchored — reported through `onUnanchoredChange`). The anchor is `DiagramAnchor` from `@plannotator/core/diagram-anchor` (`{ v: 1, family, kind, id | from + to, label, sourceLine }`, document lines), and Plannotator stores it as `Annotation.diagramAnchor`. Runtime slots: `utils/mermaid` (as before) and `utils/graphviz` (new, same shape, `@viz-js/viz` pinned `3.
|
|
275
|
+
One renderer slot and one canvas render every Mermaid and Graphviz diagram — the fences in `Viewer` (`MermaidBlock`, `GraphvizBlock`), their popout, and whatever a host renders itself. `DiagramViewer` from `@plannotator/ui/components/diagram` takes `kind`, `source`, `theme` (`{ colorTheme, mode, shadowAmount? }`), `comments: DiagramComment[]` and `onCreateComment(anchor, text, additionalTargets)`; pass `onSave(source) => Promise<{ status: 'ok' } | { status: 'stale', currentSource }>` to get the Source pane (left of the canvas on desktop, under it on the phone; `readOnlySource` shows it without Save), and `sourceOpen` to toggle it. Zoom, pan and fit are the canvas's (wheel, drag, `+` `-` `0`, arrow keys); a click opens the composer at the part beneath while a drag pans (4 px threshold, 10 px for a finger), nothing highlights on a plain mouse-over (the ring under the pointer needs the platform modifier held), every edge carries an invisible 14 px hit path in one top layer and a click is resolved by priority over everything under the pointer (node, then edge, then cluster; an edge label is its edge), a click on no part comments on the whole diagram (kind `diagram`), sequence diagrams are addressable (actors, messages, notes, frames), a saved comment paints a ring and a numbered badge from the array order, and every comment re-resolves against each render through the engine's finder (id, then label, then unanchored — reported through `onUnanchoredChange`). The anchor is `DiagramAnchor` from `@plannotator/core/diagram-anchor` (`{ v: 1, family, kind, id | from + to, label, sourceLine }`, document lines), and Plannotator stores it as `Annotation.diagramAnchor`. Runtime slots: `utils/mermaid` (as before) and `utils/graphviz` (new, same shape, `@viz-js/viz` pinned `3.31.0`), each lazy on first use or filled by the host. `DiagramPopout` is the full-size viewer in the `PopoutDialog` chrome. See HANDOFF.md § "Diagram engine (0.41.0)" for every export, the adapter, the sanitizer and the migration notes.
|
|
243
276
|
|
|
244
277
|
### Host toolbar seams (0.43.0)
|
|
245
278
|
|
|
@@ -368,17 +401,37 @@ slot and mentions on the edit box (0.45.0)".
|
|
|
368
401
|
their own (the Inbox sorts a video's comments by the moment they are at).
|
|
369
402
|
Ties keep creation order; replies still thread under their parent. Omit it
|
|
370
403
|
and the panel sorts by creation exactly as before.
|
|
371
|
-
- **`title?`, `titleNote?`, `cardParts?: (annotation) => { quote?, images? }`**
|
|
372
|
-
(ui 0.53.0) — the header's title (default "Annotations")
|
|
373
|
-
at its right, and per card whether the built-in quote
|
|
374
|
-
drawn (the Inbox's video comments draw the
|
|
375
|
-
`renderCardHeader` instead).
|
|
404
|
+
- **`title?`, `titleNote?`, `cardParts?: (annotation) => { quote?, images?, text? }`**
|
|
405
|
+
(ui 0.53.0; `text` ui 0.54.0) — the header's title (default "Annotations")
|
|
406
|
+
and a quiet note at its right, and per card whether the built-in quote,
|
|
407
|
+
image row and comment text are drawn (the Inbox's video comments draw the
|
|
408
|
+
moment and the frame in `renderCardHeader` instead). `text: false` treats
|
|
409
|
+
the stored text as the host's stand-in, not the person's words: it is not
|
|
410
|
+
drawn and the card's edit box starts empty (the Inbox's drawing with no
|
|
411
|
+
words, whose text "See the marked-up image." is for the agent). Omit them
|
|
412
|
+
and the panel is as before.
|
|
413
|
+
- **`renderCardBody?: (annotation) => ReactNode`** (ui 0.54.0) — the host's
|
|
414
|
+
own body for a plan-annotation card, drawn where the comment text goes,
|
|
415
|
+
inside the card (a click on it selects the card). The Inbox draws a
|
|
416
|
+
drawing with no words as its picture. Omit it and nothing extra is drawn.
|
|
376
417
|
- **Export: `exportLocation`** (ui 0.53.0) — a string a host sets on the
|
|
377
418
|
COPY of an annotation it hands the exporters (`exportAnnotations`,
|
|
378
419
|
`exportLinkedDocAnnotations`): one line printed under the entry's heading
|
|
379
420
|
(the Inbox's `At 1:42 (102.4 s of 7:41)`). Never stored; an annotation
|
|
380
421
|
without it exports byte-identically.
|
|
381
422
|
|
|
423
|
+
### Image annotator seams (`components/ImageAnnotator`; ui 0.54.0)
|
|
424
|
+
|
|
425
|
+
- **`escapeCancels?: boolean`** — Escape closes the drawing screen and accepts
|
|
426
|
+
nothing; Enter and the ✓ accept; a click outside closes only an empty
|
|
427
|
+
drawing (strokes are never lost to a stray click); the ✓'s title reads
|
|
428
|
+
"Save (Enter)". The Inbox passes it (the owner's rule, 2026-10-26). Omit it
|
|
429
|
+
and Escape, Enter and a click outside all accept, under "Save (Esc)", as
|
|
430
|
+
plan review has always done.
|
|
431
|
+
- **`hint?: ReactNode`** — the line under the image in place of "Press Esc or
|
|
432
|
+
Enter or click outside to accept" (the Inbox: "Enter or ✓ adds to your
|
|
433
|
+
reply · Esc cancels"). Omit it and the line is as before.
|
|
434
|
+
|
|
382
435
|
### WebMCP provider (`@plannotator/ui/webmcp`; 0.32.0)
|
|
383
436
|
|
|
384
437
|
The engine that lets a browser-integrated agent (Chrome/Edge WebMCP, `document.modelContext`) call in-page tools on a document surface. Feature-detected once; a browser without the API sees no registration, no DOM, no network, no timers. Seam: `configurePlannotatorUI({ webmcp: { enabled, namePrefix } })`, default enabled with the `plannotator.` prefix; pass `enabled: false` to keep a host page tool-free, or your own prefix to namespace the tools beside your own. There is deliberately no confirmation seam: the catalog is read-and-comment only (no approve / submit / close tools), and the agent may only edit or remove comments stamped `source: "browser-agent"`.
|
|
@@ -387,7 +440,7 @@ The engine that lets a browser-integrated agent (Chrome/Edge WebMCP, `document.m
|
|
|
387
440
|
- `useToolset({ id, active, build, deps, hooks })` attaches a named tool set to the document registry; handlers read through refs, so re-renders never re-register, and `active: false` aborts every registration (what Plannotator's Settings opt-out drives).
|
|
388
441
|
- `AnnotationChangeTracker` / `buildNudges` are pure (no DOM): per-annotation `seq`, tombstones, a per-tab watermark with `since` override, and the nudge vocabulary every response carries.
|
|
389
442
|
- A host with its own document state builds the same adapter-driven catalog Plannotator uses (`packages/editor/webmcp/documentTools.ts`, `buildDocumentTools(adapter, state, options)`) over its own getters and actions; multi-document pages should register one set whose tools take `path` (the folder-session shape) rather than one set per viewer (duplicate names across sets are skipped with a warning, never replaced).
|
|
390
|
-
- Never register tools inside an untrusted iframe: the raw-HTML viewer's `sandbox="allow-scripts"` frame and the live-app frame carry no `allow="tools"`, and that is what keeps a framed page from impersonating the host's tools.
|
|
443
|
+
- Never register tools inside an untrusted iframe: the raw-HTML viewer's `sandbox="allow-scripts allow-modals"` frame and the live-app frame carry no `allow="tools"`, and that is what keeps a framed page from impersonating the host's tools.
|
|
391
444
|
|
|
392
445
|
The one additive data-model change that rides with it: `Annotation.inReplyTo` (threaded replies; the panel indents them under the parent, the export nests them, share links drop them).
|
|
393
446
|
|
|
@@ -479,12 +532,14 @@ npm install @plannotator/ui @plannotator/core
|
|
|
479
532
|
4. Import components: `import { Viewer } from "@plannotator/ui/components/Viewer";`
|
|
480
533
|
5. Build with a bundler that compiles TS/TSX (Vite + React 19 + Tailwind v4). The packages ship **source**, so your bundler compiles them — set `moduleResolution: "bundler"`, `allowImportingTsExtensions`, `jsx: "react-jsx"`.
|
|
481
534
|
|
|
535
|
+
**Upgrade note: the plan look defaults to Clean.** `gridEnabled` (storage key `plannotator-grid-enabled`) now defaults to `false`, and a stored value counts only with the v2 marker `plannotator-plan-look-v2 = 'resolved'`. Plannotator's chooser and its Settings switch write it. On the first settings read, a v1 marker (`plannotator-plan-look-choice-resolved = 'true'`, or the legacy `plannotator-look-feel-announcement-seen = '2'`) next to a stored `'true'` is moved to Clean once (the stored value is rewritten to `'false'` and the v2 key set to `'notice'`); a v1 marker with Clean becomes `'resolved'` silently. Plannotator's apps then show a one-time notice. That includes the share portal's root and demo pages; only a shared session puts it off. A host that renders no chooser (Workspaces) shows nothing, so its users simply open in Clean, and every user with no marker opens in Clean too. To keep stored values instead, call `markLookAndFeelChoiceResolved()` from `@plannotator/ui/utils/lookAndFeelAnnouncement` before the first settings read, or seed `plannotator-plan-look-v2 = 'resolved'` in your `storageBackend`. A host that writes `gridEnabled` from its own UI should call `markLookAndFeelChoiceResolved()` with it, or the value will not survive a reload.
|
|
536
|
+
|
|
482
537
|
## Packages & publishing
|
|
483
538
|
|
|
484
539
|
- `@plannotator/core` — pure utils + types, zero deps, browser-safe (CI enforces no `node:` imports). Published.
|
|
485
540
|
- `@plannotator/ui` — React components/hooks + theme + `configure()`. Depends on an exact published `@plannotator/core` version. Published.
|
|
486
541
|
- `@plannotator/shared`, `@plannotator/ai` — stay private to the monorepo; `shared` re-exports `core`'s modules via shims so Plannotator's internals are untouched.
|
|
487
|
-
- Currently `@plannotator/ui` 0.
|
|
542
|
+
- Currently `@plannotator/ui` 0.54.0 depends exactly on `@plannotator/core` 0.25.14. `core` is bumped only when something under `packages/core` changes, so `ui` can advance alone. Keep the published core version exact in `packages/ui/package.json`; do not use a `workspace:` protocol there, because a directly published manifest must remain installable outside this monorepo. Bun still links the matching local workspace during development. When both packages change, publish `core` first, then build and publish the UI tarball. `bun run check:sibling-deps @plannotator/ui` (from the repo root; it reads npm) fails while the pinned core isn't on npm. See HANDOFF.md "Publishing & versioning" for the verification command.
|
|
488
543
|
|
|
489
544
|
## The one rule
|
|
490
545
|
|
|
@@ -193,7 +193,7 @@ export function AgentToolAnnouncementDialog({
|
|
|
193
193
|
</p>
|
|
194
194
|
</div>
|
|
195
195
|
<p className="mt-3 text-pretty text-xs leading-relaxed text-muted-foreground" data-agent-tool-cost>
|
|
196
|
-
Adds about
|
|
196
|
+
Adds about 800 tokens to each request.{save.kind === 'saved' ? '' : ` ${startsLine(host)}`}
|
|
197
197
|
</p>
|
|
198
198
|
|
|
199
199
|
<div className="mt-5 flex flex-col gap-3">
|
package/components/AgentsTab.tsx
CHANGED
|
@@ -423,7 +423,7 @@ function JobCard({
|
|
|
423
423
|
e.stopPropagation();
|
|
424
424
|
onKill();
|
|
425
425
|
}}
|
|
426
|
-
className="absolute top-1.5 right-1.5 rounded p-1 text-muted-foreground opacity-0 transition-all hover:bg-destructive/10 hover:text-destructive group-hover:opacity-100"
|
|
426
|
+
className="absolute top-1.5 right-1.5 rounded p-1 text-muted-foreground opacity-0 transition-all hover:bg-destructive/10 hover:text-destructive group-hover:opacity-100 group-focus-within:opacity-100 [@media(hover:none)]:opacity-100"
|
|
427
427
|
title="Kill agent"
|
|
428
428
|
>
|
|
429
429
|
<X size={12} />
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import React, { useState, useRef, useEffect, useId } from 'react';
|
|
1
|
+
import React, { createContext, useContext, useMemo, useState, useRef, useEffect, useId } from 'react';
|
|
2
2
|
import { AnnotationType, type Annotation, type Block, type CodeAnnotation, type EditorAnnotation } from '../types';
|
|
3
3
|
import type { MentionSource } from '../utils/mentions';
|
|
4
4
|
import { useMentionAutocomplete } from '../hooks/useMentionAutocomplete';
|
|
5
5
|
import { MentionAutocompleteMenu, mentionActiveOptionId } from './MentionAutocomplete';
|
|
6
|
+
import { buildSectionRefIndex, parseSectionRefs } from '../utils/sectionRefs';
|
|
6
7
|
import { isCurrentUser } from '../utils/identity';
|
|
7
8
|
import { ImageThumbnail } from './ImageThumbnail';
|
|
8
9
|
import { EditorAnnotationCard } from './EditorAnnotationCard';
|
|
@@ -16,6 +17,15 @@ import type { AnnotationScope, AnnotationDocumentGroup } from '../utils/annotati
|
|
|
16
17
|
import type { QuestionPanelRow } from '../utils/questionAnswers';
|
|
17
18
|
import { QuestionsPanelSection } from './QuestionsPanelSection';
|
|
18
19
|
import { HostTitle } from './HostTitle';
|
|
20
|
+
import { isCommentSubmitKey } from '../utils/commentSubmitKey';
|
|
21
|
+
import { useConfigValue } from '../config/useConfig';
|
|
22
|
+
import { imageRegionNumbers, RegionCardInfo, RegionNumber, regionOf } from './ImageRegions';
|
|
23
|
+
|
|
24
|
+
/** What a region card (#2100) needs from its panel: each region's number on
|
|
25
|
+
* its image, and how to load the image for the crop thumbnail. */
|
|
26
|
+
const RegionCardContext = createContext<{ numbers: ReadonlyMap<string, number>; thumbSrc?: (src: string) => string | null }>({
|
|
27
|
+
numbers: new Map(),
|
|
28
|
+
});
|
|
19
29
|
|
|
20
30
|
// Card type-word colors. Deletion uses `destructive` (reliably red on every
|
|
21
31
|
// theme, matching the in-document .deletion highlight). Comment uses the
|
|
@@ -120,6 +130,8 @@ interface PanelProps {
|
|
|
120
130
|
editorAnnotations?: EditorAnnotation[];
|
|
121
131
|
onDeleteEditorAnnotation?: (id: string) => void;
|
|
122
132
|
onClose?: () => void;
|
|
133
|
+
/** Navigate the document to a heading anchor, for `#Heading` references in comment text. */
|
|
134
|
+
onNavigateAnchor?: (hash: string) => void;
|
|
123
135
|
/** Copy the full feedback payload. May resolve a success boolean; resolving
|
|
124
136
|
* `false` suppresses the "Copied" flash. A void resolution (existing hosts)
|
|
125
137
|
* is treated as success, preserving the original behavior. */
|
|
@@ -213,13 +225,30 @@ interface PanelProps {
|
|
|
213
225
|
* the frame in `renderCardHeader`) turns them off per card. Absent: both
|
|
214
226
|
* drawn, exactly as before.
|
|
215
227
|
*/
|
|
216
|
-
cardParts?: (annotation: Annotation) => { quote?: boolean; images?: boolean };
|
|
228
|
+
cardParts?: (annotation: Annotation) => { quote?: boolean; images?: boolean; text?: boolean };
|
|
229
|
+
/**
|
|
230
|
+
* Optional: the host's own body for a plan-annotation card, drawn where the
|
|
231
|
+
* comment text goes, inside the card (a click on it selects the card). The
|
|
232
|
+
* Inbox draws a drawing that has no words as its picture, with `cardParts`
|
|
233
|
+
* `text: false` (its stored text is a stand-in for the agent, not the
|
|
234
|
+
* person's words: not drawn, and the edit box starts empty). Absent:
|
|
235
|
+
* nothing extra, exactly as before.
|
|
236
|
+
*/
|
|
237
|
+
renderCardBody?: (annotation: Annotation) => React.ReactNode;
|
|
238
|
+
/**
|
|
239
|
+
* Comments on part of an image (`imageRegion`, #2100) draw a crop of their
|
|
240
|
+
* box in place of the quote; this turns an image's src as written into a
|
|
241
|
+
* URL the browser can load (null: no crop, the file and size still show).
|
|
242
|
+
* Absent: no crop.
|
|
243
|
+
*/
|
|
244
|
+
imageRegionSrc?: (src: string) => string | null;
|
|
217
245
|
}
|
|
218
246
|
|
|
219
247
|
export const AnnotationPanel: React.FC<PanelProps> = ({
|
|
220
248
|
emptyHint = 'Select text to annotate',
|
|
221
249
|
isOpen,
|
|
222
250
|
annotations,
|
|
251
|
+
blocks,
|
|
223
252
|
onSelect,
|
|
224
253
|
onDelete,
|
|
225
254
|
onEdit,
|
|
@@ -256,7 +285,19 @@ export const AnnotationPanel: React.FC<PanelProps> = ({
|
|
|
256
285
|
title = 'Annotations',
|
|
257
286
|
titleNote,
|
|
258
287
|
cardParts,
|
|
288
|
+
renderCardBody,
|
|
289
|
+
imageRegionSrc,
|
|
290
|
+
onNavigateAnchor,
|
|
259
291
|
}) => {
|
|
292
|
+
// `#Heading` links in comment text; nothing is parsed unless the host can navigate.
|
|
293
|
+
const sectionRefs = useMemo(
|
|
294
|
+
() => (onNavigateAnchor ? buildSectionRefIndex(blocks) : undefined),
|
|
295
|
+
[blocks, onNavigateAnchor],
|
|
296
|
+
);
|
|
297
|
+
const regionCards = useMemo(
|
|
298
|
+
() => ({ numbers: imageRegionNumbers(annotations), thumbSrc: imageRegionSrc }),
|
|
299
|
+
[annotations, imageRegionSrc],
|
|
300
|
+
);
|
|
260
301
|
const isMobile = useIsMobile();
|
|
261
302
|
const embedded = presentation === 'embedded';
|
|
262
303
|
const mobilePanel = isMobile && !embedded;
|
|
@@ -517,8 +558,11 @@ export const AnnotationPanel: React.FC<PanelProps> = ({
|
|
|
517
558
|
footer={group.isCurrent ? renderCardFooter?.(annotation) : undefined}
|
|
518
559
|
header={group.isCurrent ? renderCardHeader?.(annotation) : undefined}
|
|
519
560
|
parts={group.isCurrent ? cardParts?.(annotation) : undefined}
|
|
561
|
+
body={group.isCurrent ? renderCardBody?.(annotation) : undefined}
|
|
520
562
|
mentionSource={mentionSource}
|
|
521
563
|
unanchored={group.isCurrent ? (unanchoredIds?.has(annotation.id) ?? false) : false}
|
|
564
|
+
sectionRefs={group.isCurrent ? sectionRefs : undefined}
|
|
565
|
+
onNavigateAnchor={onNavigateAnchor}
|
|
522
566
|
/>
|
|
523
567
|
);
|
|
524
568
|
return isReply ? (
|
|
@@ -623,6 +667,8 @@ export const AnnotationPanel: React.FC<PanelProps> = ({
|
|
|
623
667
|
>
|
|
624
668
|
<AnnotationCard
|
|
625
669
|
annotation={entry.annotation}
|
|
670
|
+
sectionRefs={sectionRefs}
|
|
671
|
+
onNavigateAnchor={onNavigateAnchor}
|
|
626
672
|
isSelected={selectedId === entry.annotation.id}
|
|
627
673
|
isMe={isCurrentUser(entry.annotation.author)}
|
|
628
674
|
onSelect={() => onSelect(entry.annotation.id)}
|
|
@@ -632,6 +678,7 @@ export const AnnotationPanel: React.FC<PanelProps> = ({
|
|
|
632
678
|
footer={renderCardFooter?.(entry.annotation)}
|
|
633
679
|
header={renderCardHeader?.(entry.annotation)}
|
|
634
680
|
parts={cardParts?.(entry.annotation)}
|
|
681
|
+
body={renderCardBody?.(entry.annotation)}
|
|
635
682
|
mentionSource={mentionSource}
|
|
636
683
|
unanchored={unanchoredIds?.has(entry.annotation.id) ?? false}
|
|
637
684
|
/>
|
|
@@ -640,6 +687,8 @@ export const AnnotationPanel: React.FC<PanelProps> = ({
|
|
|
640
687
|
<AnnotationCard
|
|
641
688
|
key={entry.annotation.id}
|
|
642
689
|
annotation={entry.annotation}
|
|
690
|
+
sectionRefs={sectionRefs}
|
|
691
|
+
onNavigateAnchor={onNavigateAnchor}
|
|
643
692
|
isSelected={selectedId === entry.annotation.id}
|
|
644
693
|
isMe={isCurrentUser(entry.annotation.author)}
|
|
645
694
|
onSelect={() => onSelect(entry.annotation.id)}
|
|
@@ -649,6 +698,7 @@ export const AnnotationPanel: React.FC<PanelProps> = ({
|
|
|
649
698
|
footer={renderCardFooter?.(entry.annotation)}
|
|
650
699
|
header={renderCardHeader?.(entry.annotation)}
|
|
651
700
|
parts={cardParts?.(entry.annotation)}
|
|
701
|
+
body={renderCardBody?.(entry.annotation)}
|
|
652
702
|
mentionSource={mentionSource}
|
|
653
703
|
unanchored={unanchoredIds?.has(entry.annotation.id) ?? false}
|
|
654
704
|
/>
|
|
@@ -691,7 +741,7 @@ export const AnnotationPanel: React.FC<PanelProps> = ({
|
|
|
691
741
|
</OverlayScrollArea>
|
|
692
742
|
|
|
693
743
|
{/* Quick Actions Footer */}
|
|
694
|
-
{headerCount > 0 && (
|
|
744
|
+
{headerCount > 0 && (onQuickCopy || (sharingEnabled && onShare)) && (
|
|
695
745
|
<div className="border-t border-border/50 px-3 py-2 flex gap-1.5">
|
|
696
746
|
{onQuickCopy && (
|
|
697
747
|
<button
|
|
@@ -745,12 +795,12 @@ export const AnnotationPanel: React.FC<PanelProps> = ({
|
|
|
745
795
|
className="fixed inset-0 z-[59] bg-background/60 backdrop-blur-sm"
|
|
746
796
|
onClick={onClose}
|
|
747
797
|
/>
|
|
748
|
-
{panel}
|
|
798
|
+
<RegionCardContext.Provider value={regionCards}>{panel}</RegionCardContext.Provider>
|
|
749
799
|
</>
|
|
750
800
|
);
|
|
751
801
|
}
|
|
752
802
|
|
|
753
|
-
return panel
|
|
803
|
+
return <RegionCardContext.Provider value={regionCards}>{panel}</RegionCardContext.Provider>;
|
|
754
804
|
};
|
|
755
805
|
|
|
756
806
|
function formatTimestamp(ts: number): string {
|
|
@@ -899,6 +949,44 @@ const DirectEditsCard: React.FC<{
|
|
|
899
949
|
);
|
|
900
950
|
};
|
|
901
951
|
|
|
952
|
+
/** Comment text, with each `#Heading text` as a link that scrolls to that heading. */
|
|
953
|
+
const AnnotationText: React.FC<{
|
|
954
|
+
text: string;
|
|
955
|
+
className: string;
|
|
956
|
+
sectionRefs?: Map<string, string>;
|
|
957
|
+
onNavigateAnchor?: (hash: string) => void;
|
|
958
|
+
}> = ({ text, className, sectionRefs, onNavigateAnchor }) => {
|
|
959
|
+
const parts = React.useMemo(
|
|
960
|
+
() => (sectionRefs && onNavigateAnchor ? parseSectionRefs(text, sectionRefs) : [text]),
|
|
961
|
+
[text, sectionRefs, onNavigateAnchor],
|
|
962
|
+
);
|
|
963
|
+
|
|
964
|
+
return (
|
|
965
|
+
<p className={className}>
|
|
966
|
+
{parts.map((part, i) =>
|
|
967
|
+
typeof part === 'string' ? (
|
|
968
|
+
part
|
|
969
|
+
) : (
|
|
970
|
+
<a
|
|
971
|
+
key={i}
|
|
972
|
+
href={`#${part.anchor}`}
|
|
973
|
+
className="rounded-sm font-medium text-primary underline-offset-2 hover:underline focus-visible:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50"
|
|
974
|
+
onClick={(e) => {
|
|
975
|
+
// The host scrolls; the card's own onSelect would scroll to the
|
|
976
|
+
// annotation instead, and the URL hash stays as it was.
|
|
977
|
+
e.preventDefault();
|
|
978
|
+
e.stopPropagation();
|
|
979
|
+
onNavigateAnchor!(`#${part.anchor}`);
|
|
980
|
+
}}
|
|
981
|
+
>
|
|
982
|
+
{part.label}
|
|
983
|
+
</a>
|
|
984
|
+
),
|
|
985
|
+
)}
|
|
986
|
+
</p>
|
|
987
|
+
);
|
|
988
|
+
};
|
|
989
|
+
|
|
902
990
|
/**
|
|
903
991
|
* The plan-annotation card's edit box.
|
|
904
992
|
*
|
|
@@ -929,6 +1017,7 @@ const AnnotationEditComposer: React.FC<{
|
|
|
929
1017
|
const mention = useMentionAutocomplete({ text: value, setText: onChange, textareaRef, source: mentionSource });
|
|
930
1018
|
const listboxId = `annotation-card-mentions-${useId().replace(/:/g, '')}`;
|
|
931
1019
|
const menuOpen = mention.menu !== null;
|
|
1020
|
+
const enterSavesComment = useConfigValue('enterSavesComment');
|
|
932
1021
|
|
|
933
1022
|
// Focus-on-open, unchanged: this component mounts exactly when editing starts.
|
|
934
1023
|
useEffect(() => {
|
|
@@ -943,7 +1032,7 @@ const AnnotationEditComposer: React.FC<{
|
|
|
943
1032
|
// (and only a second Escape cancels the edit), and an arrowed-to row
|
|
944
1033
|
// takes Enter. Mod+Enter is never consumed by the menu.
|
|
945
1034
|
if (mention.onKeyDown(e)) return;
|
|
946
|
-
if (
|
|
1035
|
+
if (isCommentSubmitKey(e, enterSavesComment)) {
|
|
947
1036
|
e.preventDefault();
|
|
948
1037
|
onSave(mention.mentionIds);
|
|
949
1038
|
} else if (e.key === 'Escape') {
|
|
@@ -996,21 +1085,32 @@ const AnnotationCard: React.FC<{
|
|
|
996
1085
|
/** The annotation has no live location in the document (host-reported). */
|
|
997
1086
|
unanchored?: boolean;
|
|
998
1087
|
/** Built-in parts to draw (see `cardParts`). */
|
|
999
|
-
parts?: { quote?: boolean; images?: boolean };
|
|
1000
|
-
|
|
1088
|
+
parts?: { quote?: boolean; images?: boolean; text?: boolean };
|
|
1089
|
+
/** Host body, drawn where the comment text goes (see `renderCardBody`). */
|
|
1090
|
+
body?: React.ReactNode;
|
|
1091
|
+
sectionRefs?: Map<string, string>;
|
|
1092
|
+
onNavigateAnchor?: (hash: string) => void;
|
|
1093
|
+
}> = ({ annotation, isSelected, isMe, onSelect, onDelete, onEdit, readOnly = false, footer, header, mentionSource, unanchored = false, parts, body, sectionRefs, onNavigateAnchor }) => {
|
|
1094
|
+
// `text: false`: the stored text is the host's stand-in, never the person's words; the edit box starts empty.
|
|
1095
|
+
const shownText = parts?.text === false ? '' : annotation.text || '';
|
|
1001
1096
|
const [isEditing, setIsEditing] = useState(false);
|
|
1002
|
-
const [editText, setEditText] = useState(
|
|
1097
|
+
const [editText, setEditText] = useState(shownText);
|
|
1098
|
+
// A comment on part of an image (#2100): numbered like its box, with the
|
|
1099
|
+
// crop, file, size and line in place of the quote.
|
|
1100
|
+
const regionCards = useContext(RegionCardContext);
|
|
1101
|
+
const region = regionOf(annotation);
|
|
1102
|
+
const regionNumber = region ? regionCards.numbers.get(annotation.id) : undefined;
|
|
1003
1103
|
|
|
1004
1104
|
// Update editText when annotation.text changes
|
|
1005
1105
|
useEffect(() => {
|
|
1006
1106
|
if (!isEditing) {
|
|
1007
|
-
setEditText(
|
|
1107
|
+
setEditText(shownText);
|
|
1008
1108
|
}
|
|
1009
|
-
}, [
|
|
1109
|
+
}, [shownText, isEditing]);
|
|
1010
1110
|
|
|
1011
1111
|
const handleStartEdit = (e: React.MouseEvent) => {
|
|
1012
1112
|
e.stopPropagation();
|
|
1013
|
-
setEditText(
|
|
1113
|
+
setEditText(shownText);
|
|
1014
1114
|
setIsEditing(true);
|
|
1015
1115
|
};
|
|
1016
1116
|
|
|
@@ -1027,7 +1127,7 @@ const AnnotationCard: React.FC<{
|
|
|
1027
1127
|
};
|
|
1028
1128
|
|
|
1029
1129
|
const handleCancelEdit = () => {
|
|
1030
|
-
setEditText(
|
|
1130
|
+
setEditText(shownText);
|
|
1031
1131
|
setIsEditing(false);
|
|
1032
1132
|
};
|
|
1033
1133
|
|
|
@@ -1066,6 +1166,12 @@ const AnnotationCard: React.FC<{
|
|
|
1066
1166
|
{/* Header: type word + author · time + actions */}
|
|
1067
1167
|
<div className="mb-1.5 flex items-center gap-1.5">
|
|
1068
1168
|
<span className={cn('text-[11px] font-medium', typeColor)}>{typeLabel}</span>
|
|
1169
|
+
{region && regionNumber !== undefined && <RegionNumber n={regionNumber} selected={isSelected} />}
|
|
1170
|
+
{region && (
|
|
1171
|
+
<span data-annotation-region="true" className="text-[9px] px-1.5 py-0.5 rounded font-medium bg-muted text-muted-foreground">
|
|
1172
|
+
Region
|
|
1173
|
+
</span>
|
|
1174
|
+
)}
|
|
1069
1175
|
{annotation.diffContext && (
|
|
1070
1176
|
<span className="text-[9px] px-1.5 py-0.5 rounded font-medium bg-muted text-muted-foreground">
|
|
1071
1177
|
diff
|
|
@@ -1100,7 +1206,7 @@ const AnnotationCard: React.FC<{
|
|
|
1100
1206
|
</div>
|
|
1101
1207
|
)}
|
|
1102
1208
|
{!readOnly && (
|
|
1103
|
-
<div className="ml-auto flex items-center gap-0.5 opacity-0 transition-opacity group-hover:opacity-100 [@media(hover:none)]:opacity-100">
|
|
1209
|
+
<div className="ml-auto flex items-center gap-0.5 opacity-0 transition-opacity group-hover:opacity-100 group-focus-within:opacity-100 [@media(hover:none)]:opacity-100">
|
|
1104
1210
|
{onEdit && annotation.type !== AnnotationType.DELETION && !isEditing && (
|
|
1105
1211
|
<HostTitle title="Edit annotation" name>
|
|
1106
1212
|
<button
|
|
@@ -1130,14 +1236,21 @@ const AnnotationCard: React.FC<{
|
|
|
1130
1236
|
isEditing ? (
|
|
1131
1237
|
editComposer
|
|
1132
1238
|
) : (
|
|
1133
|
-
<
|
|
1134
|
-
{annotation.text}
|
|
1135
|
-
|
|
1239
|
+
<AnnotationText
|
|
1240
|
+
text={annotation.text || ''}
|
|
1241
|
+
className="whitespace-pre-wrap [overflow-wrap:anywhere] text-[13px] leading-relaxed text-foreground/90"
|
|
1242
|
+
sectionRefs={sectionRefs}
|
|
1243
|
+
onNavigateAnchor={onNavigateAnchor}
|
|
1244
|
+
/>
|
|
1136
1245
|
)
|
|
1137
1246
|
) : (
|
|
1138
1247
|
<>
|
|
1248
|
+
{/* A region shows its crop, file, size and line instead of a quote. */}
|
|
1249
|
+
{region && parts?.quote !== false && (
|
|
1250
|
+
<RegionCardInfo region={region} thumbSrc={regionCards.thumbSrc?.(region.src) ?? null} />
|
|
1251
|
+
)}
|
|
1139
1252
|
{/* Quote — the annotated text */}
|
|
1140
|
-
{parts?.quote !== false && (
|
|
1253
|
+
{!region && parts?.quote !== false && (
|
|
1141
1254
|
<p className="mb-1.5 line-clamp-2 whitespace-pre-wrap [overflow-wrap:anywhere] font-mono text-[11px] leading-relaxed text-muted-foreground/80">
|
|
1142
1255
|
"{annotation.originalText}"
|
|
1143
1256
|
</p>
|
|
@@ -1148,13 +1261,17 @@ const AnnotationCard: React.FC<{
|
|
|
1148
1261
|
isEditing ? (
|
|
1149
1262
|
editComposer
|
|
1150
1263
|
) : (
|
|
1151
|
-
|
|
1152
|
-
<
|
|
1153
|
-
{
|
|
1154
|
-
|
|
1264
|
+
shownText && (
|
|
1265
|
+
<AnnotationText
|
|
1266
|
+
text={shownText}
|
|
1267
|
+
className="whitespace-pre-wrap [overflow-wrap:anywhere] text-[13px] leading-relaxed text-foreground/90"
|
|
1268
|
+
sectionRefs={sectionRefs}
|
|
1269
|
+
onNavigateAnchor={onNavigateAnchor}
|
|
1270
|
+
/>
|
|
1155
1271
|
)
|
|
1156
1272
|
)
|
|
1157
1273
|
)}
|
|
1274
|
+
{body != null && body !== false && body}
|
|
1158
1275
|
</>
|
|
1159
1276
|
)}
|
|
1160
1277
|
|
|
@@ -1203,6 +1320,7 @@ const CodeAnnotationCard: React.FC<{
|
|
|
1203
1320
|
}> = ({ annotation, isSelected, isMe, onSelect, onDelete, onEdit, readOnly = false }) => {
|
|
1204
1321
|
const [isEditing, setIsEditing] = useState(false);
|
|
1205
1322
|
const [editText, setEditText] = useState(annotation.text || '');
|
|
1323
|
+
const enterSavesComment = useConfigValue('enterSavesComment');
|
|
1206
1324
|
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
|
1207
1325
|
|
|
1208
1326
|
useEffect(() => {
|
|
@@ -1253,7 +1371,7 @@ const CodeAnnotationCard: React.FC<{
|
|
|
1253
1371
|
<span className="text-[11px] font-medium text-primary">Code</span>
|
|
1254
1372
|
<CardByline author={annotation.author} isMe={isMe} timestamp={annotation.createdAt} />
|
|
1255
1373
|
{!readOnly && (
|
|
1256
|
-
<div className="ml-auto flex items-center gap-0.5 opacity-0 transition-opacity group-hover:opacity-100 [@media(hover:none)]:opacity-100">
|
|
1374
|
+
<div className="ml-auto flex items-center gap-0.5 opacity-0 transition-opacity group-hover:opacity-100 group-focus-within:opacity-100 [@media(hover:none)]:opacity-100">
|
|
1257
1375
|
{onEdit && !isEditing && (
|
|
1258
1376
|
<HostTitle title="Edit annotation" name>
|
|
1259
1377
|
<button
|
|
@@ -1299,7 +1417,7 @@ const CodeAnnotationCard: React.FC<{
|
|
|
1299
1417
|
value={editText}
|
|
1300
1418
|
onChange={(e) => setEditText(e.target.value)}
|
|
1301
1419
|
onKeyDown={(e) => {
|
|
1302
|
-
if (
|
|
1420
|
+
if (isCommentSubmitKey(e, enterSavesComment)) {
|
|
1303
1421
|
e.preventDefault();
|
|
1304
1422
|
handleSaveEdit();
|
|
1305
1423
|
} else if (e.key === 'Escape') {
|
|
@@ -61,7 +61,7 @@ export const AnnotationSidebar: React.FC<SidebarProps> = ({
|
|
|
61
61
|
</span>
|
|
62
62
|
<button
|
|
63
63
|
onClick={(e) => { e.stopPropagation(); onDelete(ann.id); }}
|
|
64
|
-
className="opacity-0 group-hover:opacity-100 text-muted-foreground hover:text-destructive transition-all p-1 rounded hover:bg-destructive/10 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
64
|
+
className="opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 [@media(hover:none)]:opacity-100 text-muted-foreground hover:text-destructive transition-all p-1 rounded hover:bg-destructive/10 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
65
65
|
title="Remove Annotation"
|
|
66
66
|
>
|
|
67
67
|
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
|