@plannotator/ui 0.53.1 → 0.55.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 +274 -14
- package/README.md +114 -11
- 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 +22 -5
- package/components/CodeFilePopout.tsx +139 -4
- package/components/CodeSnippetPreview.tsx +174 -0
- package/components/CommentPopover.tsx +44 -8
- package/components/DecisionControl.tsx +31 -5
- package/components/DeferredMarkdownContentsRail.tsx +38 -0
- package/components/DiagramBlock.tsx +17 -12
- package/components/DiffFileTree.tsx +7 -6
- package/components/DocumentContents.tsx +74 -0
- 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 +722 -126
- 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 +167 -174
- 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/MarkdownContentsRail.tsx +777 -0
- 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/TableOfContents.tsx +20 -6
- 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 +288 -43
- 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 +532 -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 +82 -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 +276 -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/contents-rail.css +337 -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/useHeadingScrollSpy.ts +77 -0
- package/hooks/useHoverPreview.ts +94 -0
- package/hooks/useLinkedDoc.ts +226 -13
- package/hooks/usePackageChangesSetting.ts +90 -0
- package/hooks/usePlanLookDialog.ts +67 -0
- package/hooks/usePrefersReducedMotion.ts +23 -0
- package/hooks/useSharing.ts +11 -7
- package/hooks/useSwipeStep.ts +597 -0
- package/hooks/useThemePaletteHalves.ts +37 -0
- package/hooks/useUpdateCheck.ts +22 -4
- package/hooks/useUpdateToast.ts +1 -1
- package/package.json +26 -23
- 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/annotationHelpers.ts +32 -4
- 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/blockParse.ts +45 -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/headingScrollSpy.ts +92 -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/HANDOFF.md
CHANGED
|
@@ -102,6 +102,8 @@ Pass any subset of these to `configurePlannotatorUI({ ... })`. Anything omitted
|
|
|
102
102
|
| `skillInstructionsDestination` | `string` | What a composer's "Includes skill instructions" line says the instructions go with (ui 0.53.0) | `'your feedback'` |
|
|
103
103
|
| `webmcp` | `WebMcpPolicy` | The WebMCP provider policy, `{ enabled, namePrefix }` | Enabled whenever the browser exposes `document.modelContext`, prefix `plannotator.` |
|
|
104
104
|
| `componentTooltips` | `boolean` | Draw ui's own hover text through `components/HostTitle` as ui's `Tooltip` instead of a native `title` (ui 0.53.0) | false: native titles |
|
|
105
|
+
| `digitHexIssueRefs` | `boolean` | Render a digit-only 6/8-digit `#123456` / `#12345678` (word boundary, no leading zero) as an issue ref instead of a hex swatch, only where `InlineMarkdown` can build a ref link (`githubRepo` on a known forge). Also `setDigitHexIssueRefs` / `resetDigitHexIssueRefs` / `getDigitHexIssueRefs` on `components/InlineMarkdown` for tests (#1932) | false: swatches, exactly as before |
|
|
106
|
+
| `richCopy` | `boolean` | The selection toolbar's Copy button (`Viewer`, `HtmlViewer`) copies rich text: one clipboard item, `text/html` plus `text/plain` (#1972). Also `setRichCopyEnabled` / `resetRichCopyEnabled` / `getRichCopyEnabled` on `utils/richCopyPolicy` for tests | false: Copy copies plain text, the toolbar exactly as before |
|
|
105
107
|
|
|
106
108
|
### Interface details worth knowing
|
|
107
109
|
|
|
@@ -218,6 +220,7 @@ We deliberately did **not** restructure the exports map in this PR (move-don't-r
|
|
|
218
220
|
| `components/TableOfContents` | Pure — renders from `blocks`; pair with `useActiveSection` for scroll-spy. *(Blessed in 0.24.0.)* |
|
|
219
221
|
| `components/ResizeHandle` + `hooks/useResizablePanel` | Layout pair for draggable panel widths; persists the width through the `storageBackend` seam. *(Blessed in 0.24.0.)* |
|
|
220
222
|
| `hooks/useActiveSection` | Scroll-spy over rendered headings; no backend. *(Blessed in 0.24.0.)* |
|
|
223
|
+
| `components/MarkdownContentsRail` + `components/DeferredMarkdownContentsRail` | Workspaces' document contents rail (ticks in the right gutter, the Contents panel on click), moved here; no backend. Its styles are their own sheet, `@plannotator/ui/contents-rail.css`: import it outside any CSS layer. Its scroll math is `utils/headingScrollSpy` (+ `hooks/useHeadingScrollSpy` for a list, `components/DocumentContents`). See README "Document contents rail" and "Scroll-spies: one per surface". |
|
|
221
224
|
| `hooks/useScrollViewport` | Resolves the scrolling element for viewport-aware UI; no backend. *(Blessed in 0.24.0.)* |
|
|
222
225
|
| `utils/annotationHelpers` | Pure annotation utilities (`getAnnotationCountBySection`, `buildTocHierarchy` + `TocItem`). *(Blessed in 0.24.0.)* |
|
|
223
226
|
| `components/html-viewer` (`HtmlViewer`, `projectHostThreads`, `buildPersistedHtmlAnchor`) | The raw-HTML annotation viewer: overlay-projected placed markers, pinpoint anchors, multi-target comments. Props + validated bridge protocol; no backend of its own. See "Raw-HTML annotation viewer + syntax-highlighting migration (0.29.0)" and "HTML annotation parity seams". *(Blessed in 0.29.0.)* |
|
|
@@ -262,7 +265,7 @@ If Workspaces ever wants one of these surfaces, the path is the same as everythi
|
|
|
262
265
|
The renderer's `MathBlock` (and inline math) uses KaTeX. **KaTeX's stylesheet and its ~1.1MB of math fonts are deliberately NOT in the published `styles.css`** — bundling them would 9x the CSS for every page load, math or not. This is app-developer setup, done once; end users never touch it. Pick one:
|
|
263
266
|
|
|
264
267
|
1. **Self-hosted (recommended for production):** copy `katex/dist/katex.min.css` + `katex/dist/fonts/` to your own asset origin and add one `<link rel="stylesheet">`. No third-party dependency in your serving path; fonts download lazily, only on pages that actually render math.
|
|
265
|
-
2. **CDN tag:** `<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@<version>/dist/katex.min.css">` in your HTML — pin `<version>` to the `katex` version in `@plannotator/ui`'s package.json so CSS and the bundled KaTeX JS stay in step. Same lazy-font behavior; adds a third-party origin.
|
|
268
|
+
2. **CDN tag:** `<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@<version>/dist/katex.min.css">` in your HTML — pin `<version>` to the `katex` version in `@plannotator/ui`'s package.json (`^0.19.0` from ui 0.55.0, `^0.18.11` in ui 0.54.0, `^0.16.47` before) so CSS and the bundled KaTeX JS stay in step. A stylesheet from another KaTeX minor does not match the markup: 0.18 renamed 21 internal classes (see "Dependency bumps: KaTeX 0.18, @pierre/diffs 1.5.2, Mermaid 12.2 (published in ui 0.54.0)"). The same goes for a self-hosted copy (option 1). Same lazy-font behavior; adds a third-party origin.
|
|
266
269
|
3. **Bundler import:** `import 'katex/dist/katex.min.css';` next to your `styles.css` import — your bundler ships the fonts as separate lazy-loaded files. With npm/bun this resolves out of the box (`katex` is a dependency of `@plannotator/ui` and gets hoisted); under pnpm's strict `node_modules`, add `katex` to your own dependencies to import it directly.
|
|
267
270
|
|
|
268
271
|
If you skip all three and render math, equations appear as broken-looking raw HTML — that's the symptom to recognize. If you never render math, do nothing.
|
|
@@ -631,7 +634,7 @@ The overlay-projection annotation viewer for raw HTML (placed comment markers, p
|
|
|
631
634
|
1. **The contract is props + the validated message protocol — not `configurePlannotatorUI`.** `HtmlViewer` is driven by its props (`rawHtml`, `annotations`, `onAddAnnotation`, `onSelectAnnotation`, `selectedAnnotationId`, `mode`, `inputMethod`, `readOnly`, …) and adapts the sandboxed iframe's validated bridge messages to the same annotation controls the markdown `Viewer` uses. The `configure()` seams still govern what surrounds it (storage, drafts, images, AI), but nothing about the viewer itself routes through `configure()`. Integrate on props; that is the path we maintain.
|
|
632
635
|
2. **Numbering derives from the `annotations` prop — drive the prop.** Marker numbers are computed from the prop's array order (matching `exportAnnotations` numbering, globals occupying slots) and synced to the iframe on every prop change. Mounting with an empty prop and driving the viewer imperatively is NOT supported and will leave bubbles unnumbered; the imperative handle (`applySharedAnnotations`, `removeHighlight`, `clearAllHighlights`) exists for repaint scenarios on top of a prop-driven mount, not as a substitute for it. If your host architecture truly cannot supply the prop, ask for a numbering seam rather than working around it.
|
|
633
636
|
3. **`readOnly` is view-only, not blank.** With `readOnly`, committed annotations still restore, markers still paint with correct numbers, and clicking a marker still fires `onSelectAnnotation`; every authoring entry point (composer, toolbar, quick label, vim) is disabled. Pinned by the "readOnly view-only contract" tests in `components/html-viewer/htmlPinpointProtocol.test.tsx`.
|
|
634
|
-
4. **Security envelope: opaque-origin `srcdoc` sandbox only.** The iframe is `sandbox="allow-scripts"` (no `allow-same-origin`) and both sides authenticate messages by source identity with `targetOrigin: "*"`. That pattern is safe **only** because a `srcdoc` sandbox has an opaque origin. If a host serves annotated content from a real origin (a proxy, a hosted iframe), it must add strict `targetOrigin` and origin checks — do not reuse the `"*"` pattern there.
|
|
637
|
+
4. **Security envelope: opaque-origin `srcdoc` sandbox only.** The iframe is `sandbox="allow-scripts allow-modals"` (no `allow-same-origin`; `allow-modals` lets alert/confirm/prompt open, #2063) and both sides authenticate messages by source identity with `targetOrigin: "*"`. That pattern is safe **only** because a `srcdoc` sandbox has an opaque origin. If a host serves annotated content from a real origin (a proxy, a hosted iframe), it must add strict `targetOrigin` and origin checks — do not reuse the `"*"` pattern there.
|
|
635
638
|
5. **Multi-target cap is 16 on our side.** `htmlAdditionalTargets` accepts up to 16 additional anchors per comment; a host enforcing a smaller product cap (e.g. 7) should cap at composer level before submit — the stored schema is unchanged either way. As with every anchor field, persist `htmlAnchor`/`htmlAdditionalTargets` as opaque JSON and round-trip them unchanged (see "The annotation anchor schema").
|
|
636
639
|
|
|
637
640
|
### `@plannotator/core` 0.23.0
|
|
@@ -779,7 +782,9 @@ Three class-level deltas, none of which breaks an id- or class-based selector: `
|
|
|
779
782
|
- To keep the 0.39 behaviour — runtime registered and initialized at startup, in your entry chunk, unable to fail separately from the app — add the one line Plannotator used to have, before the first render: `import '@plannotator/ui/utils/mermaid-eager';`. It statically imports `mermaid`, runs `mermaid.initialize(MERMAID_CONFIG)` at module evaluation and fills the slot (`setMermaidRuntime(mermaid, 'plannotator-mermaid-eager')`); the ELK chunk is still Mermaid's own lazy import and is not hoisted by this.
|
|
780
783
|
- Own loader: `setMermaidRuntime(runtime, 'host')` after your own `import('mermaid')` + `initialize(MERMAID_CONFIG)` fills the slot the same way; there is no `mermaidRuntimeLoader` seam on `configurePlannotatorUI` (the test hook `__setMermaidRuntimeLoaderForTests` is not host surface).
|
|
781
784
|
- `applyMermaidTheme(mermaid, key)` is keyed on the **runtime object** as well as the `(palette, mode)` key, so a lazily loaded runtime is themed on its first render exactly like an eagerly registered one, and a host that swaps runtimes gets a fresh `initialize` (pinned by `components/MermaidBlock.theme.test.tsx`).
|
|
782
|
-
-
|
|
785
|
+
- At 0.40.0 the single-file builds gained nothing from the lazy import: `inlineDynamicImports` inlined the `import('mermaid')` target (and ELK) into the one HTML file. Since #1618 they no longer inline: the plan and review apps ship as one packed file whose server answers each chunk at `/assets/*`, so these imports are network fetches there too, and a chunk that cannot load leaves its block showing the source with a Reload line (`components/LazyBlockBoundary`). The 0.40.0 numbers: Plannotator's own hook bundle is 21.74 MB → 23.50 MB (+1.76 MB, +8.1%; gzip 6.66 MB → 7.19 MB); that delta is Mermaid 12's own size, not the loading strategy, and the review bundle is unchanged (17.43 MB, byte-identical to main) because it never carried Mermaid. On the chunked share portal the entry chunk goes 4,688 KB → 4,044 KB (-645 KB, -13.7%) with `mermaid.core` moving to its own chunk.
|
|
786
|
+
- **Block-level chunk failures stay inside the block (#1618).** A lazy `Viewer` block (Mermaid, Graphviz, HTML/SVG embed), the diagram popout and the Source pane each sit in `components/LazyBlockBoundary`, so a chunk that fails to load shows that block's source under "Couldn't load this view. Reload" ("This page was updated. Reload to see this" on a 404) and no longer reaches the host's route error boundary.
|
|
787
|
+
- **`ThemeProvider` keeps an inline `color-scheme` it finds on `<html>` (#1618).** Plannotator's packed pages set one from the theme cookie before any script runs, so the empty page does not paint white under a dark theme; from its first render `ThemeProvider` keeps that value equal to the resolved mode. A host page that sets no inline `color-scheme` gets none, so its rendering is unchanged.
|
|
783
788
|
|
|
784
789
|
**Theming contract, restated for 12.** `MermaidBlock` calls `applyMermaidTheme(runtime, mermaidThemeKey(colorTheme, mode))` before every render. `readThemeTokens()` reads `--background`, `--foreground`, `--card`, `--card-foreground`, `--popover`, `--border`, `--muted`, `--muted-foreground`, `--primary`, `--primary-foreground`, `--secondary`, `--accent`, `--destructive`, `--success`, `--warning` and `--font-sans` off the document element (resolving `color-mix()` / `var()` chains through a probe element); `buildMermaidThemeVariables(tokens, mode)` turns them into the base theme (`dark` / `default`) plus a complete `themeVariables` set for every family (node fills ← `card`, text ← `foreground` / `card-foreground`, borders ← `border`, edges and arrowheads ← `muted-foreground`, clusters ← `muted`, twelve categorical fills seeded from `primary`, `accent`, `success`, `warning`, `destructive`, `secondary`; every colour opaque hex, every text-on-fill pair guarded to 4.5:1 and every line 3:1 against every surface it can cross), and `mermaid.initialize(buildMermaidConfig(spec))` runs once per key change. **Without tokens** (no `ThemeProvider`, no `theme.css`, or the tokens kept off the document element) `readThemeTokens` returns `undefined`, `buildMermaidThemeVariables` returns `null`, nothing is re-initialized, and the runtime keeps the static `MERMAID_CONFIG` it was initialized with (12's own `dark` base theme with the slate `themeVariables`, ELK layout). None of the variable names changed between 11 and 12; the 0.40 sweep (78 combinations, 12,870 text pairs, 7,410 line pairs, 0 failures) is the proof that the mapping holds under ELK's re-laid-out geometry.
|
|
785
790
|
|
|
@@ -1180,7 +1185,7 @@ handed to `onAddAnnotation` has no `mentions` key at all (`'mentions' in ann`
|
|
|
1180
1185
|
is false, asserted rather than `toBeUndefined()` — the difference between the
|
|
1181
1186
|
conditional spread and the bare shorthand is invisible to the latter).
|
|
1182
1187
|
|
|
1183
|
-
Tests (both DOM-gated, both in
|
|
1188
|
+
Tests (both DOM-gated, both in `tests/dom-test-list.txt`):
|
|
1184
1189
|
`components/Viewer.mentionSource.test.tsx` (5) drives the real Viewer — the
|
|
1185
1190
|
selection composer through the Vim toolbar's type-to-comment gesture and the
|
|
1186
1191
|
global composer through its button — and
|
|
@@ -1334,8 +1339,8 @@ line (3 lines above it) and with the textarea scrolled (`scrollTop` 40) —
|
|
|
1334
1339
|
and the caret x after the token is **0.016px** from the span's end.
|
|
1335
1340
|
|
|
1336
1341
|
Tests: `utils/composerTokens.test.ts` (16, DOM-free) and
|
|
1337
|
-
`components/CommentPopover.mentionChips.test.tsx` (11, DOM-gated, in
|
|
1338
|
-
|
|
1342
|
+
`components/CommentPopover.mentionChips.test.tsx` (11, DOM-gated, in
|
|
1343
|
+
`tests/dom-test-list.txt`).
|
|
1339
1344
|
|
|
1340
1345
|
## Annotation card header slot and mentions on the edit box (0.45.0)
|
|
1341
1346
|
|
|
@@ -1504,7 +1509,7 @@ edit textarea carries none of the five mention ARIA attributes, typing `@`
|
|
|
1504
1509
|
opens nothing, and `onEdit` is called with an updates object whose key list is
|
|
1505
1510
|
exactly `['text']`.
|
|
1506
1511
|
|
|
1507
|
-
Tests (both DOM-gated, both added to
|
|
1512
|
+
Tests (both DOM-gated, both added to `tests/dom-test-list.txt`):
|
|
1508
1513
|
`components/AnnotationPanel.cardHeader.test.tsx` (6) and
|
|
1509
1514
|
`components/AnnotationPanel.editMentions.test.tsx` (10).
|
|
1510
1515
|
|
|
@@ -1684,6 +1689,7 @@ Everything host-facing that changed between ui 0.50.0 / core 0.25.10 and this pa
|
|
|
1684
1689
|
**Question cards (#1683, #1699, #1702).**
|
|
1685
1690
|
|
|
1686
1691
|
- **Pinpoint inside a card targets what was clicked (#1683).** The card (`fieldset.question-block`) is now one block target whose prompt, context and each choice's text are child targets, marked `data-question-part="prompt" | "context" | "choice"`. Before, every pinpoint hover or click in a card resolved to the prompt. Clicking the radio/checkbox still answers.
|
|
1692
|
+
- **Each card tag carries `data-question-tag="<tone>"`** (`rec`, `ok`, `skip`, `open`, `warn`) on ui's `Tag` span, so host CSS can target the Recommended tag inside a choice without also catching an image or inline code there (both carry `rounded` too). Nothing else about the tag changed.
|
|
1687
1693
|
- **Plain-bullet choices keep their wrapped lines (#1699, core).** A `:::question` / `:::question-multi` with no task-list items reads each plain bullet the way CommonMark reads a list item (indented lines, lazy lines, an indented paragraph after a blank line), and those lines no longer leak into `context`. A bullet written `- **Name:** prose` (bold name set off by punctuation, `**` only) takes `Name` as its label and the prose as its description; a label longer than `MAX_QUESTION_CHOICE_LABEL_CHARS` (200) is cut at its first sentence, the rest leading the description; two bullets never end up with the same label. A `Recommended:` line continues onto the next line only when that line wraps the same sentence. **Task-list choices (`- [ ]`) parse exactly as before**, and `questionKey` does not change.
|
|
1688
1694
|
- **Behavior change for hosts that store answers themselves:** the labels of PLAIN-BULLET choices can differ from what core 0.25.10 produced (the wrapped text joins the label, or a bold lead becomes the label). An answer stores the labels it picked, so core 0.25.11 adds **`QuestionChoice.aliases?: string[]`** (the older names of a plain-bullet choice: its full text, its first line, that line's earlier label) and **`canonicalQuestionAnswer(question, answer)`** (`@plannotator/core/question-block`), which maps a stored label that names exactly one choice by an alias to its current label and returns the same object when nothing maps; an ambiguous label stays as stored. `QuestionBlock`, the Questions panel rows (`utils/questionAnswers`), WebMCP `read_document` and `formatQuestionAnswersSection` all read answers through it, so a host that renders `QuestionBlock` with its stored answer sees the right pick without changes. **A host that reads stored answers on its own (turning an answer into a decision, its own export) should run them through `canonicalQuestionAnswer` too.** `QuestionExportItem` gains an optional `choices` (label + aliases) so the export prints current labels.
|
|
1689
1695
|
- **Context renders with the document's block renderers (#1699).** A card's context goes through `parseMarkdownToBlocks` + `BlockRenderer` (new `components/blocks/QuestionContext`), so tables, lists, fences, quotes, headings and images look as they do in the document. Those blocks carry no `data-block-id`. Its styles are a `<style href="plannotator-question-context" precedence="default">` that React 19 hoists into `<head>` once, so a host needs no Tailwind change. `useActiveSection` observes only headings that have a block id, so a heading inside a card never stalls the table of contents.
|
|
@@ -1765,6 +1771,26 @@ Additive, one optional `Viewer` prop; Plannotator's plan review and annotate pas
|
|
|
1765
1771
|
|
|
1766
1772
|
- **`textComments?: boolean`** on `Viewer`, with `answerOnly`: a text selection opens the selection toolbar again, comment-only (the toolbar's `commentOnly`: Comment and the one-click 👍, no Delete, no label picker), with its comment composer, and `annotations` are drawn as highlights (the highlighter is enabled). Question cards (and a host's `renderQuestionCard`) are wrapped `annotation-exclude`, so their words are never commented on. Nothing else of authoring comes back: no pinpoint, code-block toolbar, Global comment, Copy or Vim. Ignored without `answerOnly` and under `readOnly`. Inside, `selectionCommentsOn = !authoringOff || (answerOnly && textComments && !readOnly)` replaces `!authoringOff` for the highlighter and the selection toolbar, its composer and its label picker only.
|
|
1767
1773
|
|
|
1774
|
+
## Dependency bumps: KaTeX 0.18, @pierre/diffs 1.5.2, Mermaid 12.2 (published in ui 0.54.0)
|
|
1775
|
+
|
|
1776
|
+
Release as a **minor**: 0.53.1 → 0.54.0. No ui API changed, but a host that serves its own KaTeX stylesheet has to act.
|
|
1777
|
+
|
|
1778
|
+
- **`katex` is now `^0.18.11`** (was `^0.16.47`). Mermaid 12.2 needs `katex ^0.18`. With ui still on 0.16, a host build (and Plannotator's own plan, Inbox and phone-surface pages) carried two KaTeX copies: about 265 to 285 KB more per single-file page. With one version the tree dedupes to one. It also fixes CVE-2026-103923 (low).
|
|
1779
|
+
- **Hosts that load `katex.min.css` themselves must move it to 0.18** (CDN or self-hosted; see "Math rendering (KaTeX)" above). KaTeX 0.18 renamed 21 internal classes. Twenty now carry the `katex-` prefix: `.accent`, `.base`, `.fix`, `.hdashline`, `.hline`, `.inner`, `.newline`, `.overlay`, `.overline`, `.root`, `.rule`, `.sizing`, `.smash`, `.sout`, `.stretchy`, `.strut`, `.tag`, `.thinbox`, `.underline` and `.vbox` (so `.base`→`.katex-base`, `.strut`→`.katex-strut`). `.hbox` has no rule any more. With a 0.16 stylesheet over 0.18 markup the renamed rules never apply, so equations lay out wrong.
|
|
1780
|
+
- A host that bundles `katex/dist/katex.min.css` (option 3) gets the right version on its next install. `.katex` and `.katex-display`, the only KaTeX classes ui's own CSS and code target, are unchanged. KaTeX 0.17's only other break was the internal `__defineFunction` API, which ui does not use.
|
|
1781
|
+
- **`@pierre/diffs` is exactly `1.5.2`** (was `1.3.6`). 1.5 repairs a hunk whose declared line counts are wrong and reports it with `console.error("parsePatchContent: hunk line count mismatch: …")`. Before, it was silent. A host that treats console errors as failures will see the message for a miscounted patch. Git's own diffs are never miscounted.
|
|
1782
|
+
- **`mermaid` is exactly `12.2.0`** (was `12.0.0`). **`@base-ui/react` is `^1.9.0`** (was `^1.7.0`). Also: `@viz-js/viz` `3.31.0`, `lucide-react` `^1.59.0`, `tailwind-merge` `^3.7.0`, CodeMirror and Lezer patch releases.
|
|
1783
|
+
- The React peer range is unchanged (`^19.2.3`); only the dev copy moved to 19.3.1.
|
|
1784
|
+
|
|
1785
|
+
## Viewer `belowTitle` (published in ui 0.54.0)
|
|
1786
|
+
|
|
1787
|
+
Additive, one optional `Viewer` prop; Plannotator passes none, so its render is unchanged. Added for Workspaces, which draws a references block (Linear-style property rows) under a document's title and had been inserting it into the Viewer's DOM.
|
|
1788
|
+
|
|
1789
|
+
- **`belowTitle?: ReactNode`**. Placement: when `blocks[0]` is a level-1 heading, the node is drawn right after that heading's element (`data-placement="below-title"`); otherwise at the top of the body, before the first block and after the frontmatter card (`data-placement="top"`), so a host never has to parse the body to know whether to pass it. An empty body draws it alone. The heading element is the same as without the prop.
|
|
1790
|
+
- **The wrapper** is `<div data-viewer-below-title="<marker>" data-placement data-pinpoint-ignore data-print-hide class="annotation-exclude">`, with no `data-block-id`. `<marker>` is `BELOW_TITLE_SLOT_MARKER`, random per page load: the translation only counts a direct child of the article carrying exactly that value, so a `<div data-viewer-below-title>` an author writes as raw HTML (DOMPurify keeps `data-*`) never turns it on. A host's CSS matches the attribute by presence. So: no selection starts or ends in it as a comment (a selection wholly inside it paints nothing and opens no toolbar; one that crosses into the body is snapped past it, like any excluded chrome), the restore text search and quote repair skip its text, pinpoint and Vim targeting skip it (`[data-pinpoint-ignore]`), print hides it, and the TOC, heading slugs, exports, share links and WebMCP read `blocks`, not the DOM.
|
|
1791
|
+
- **Positions.** web-highlighter's `parentIndex` is an element's index among every element of its tag in the article, and the slot is inside the article. `utils/belowTitleSlot` translates at the hook's two edges: a new selection's metas are rewritten to the index the element has with the slot's elements left out (`metaWithoutBelowTitle`, in the CREATE handler), and a stored meta is mapped back to the live element before `fromStore` (`metaWithBelowTitle`). A selection with one end inside the slot (from the title into it) has that end moved out before the library reads it (`snapRangeOutOfBelowTitle`: an end back to the end of the last comment text before the slot, a start on to the first after it, in the pointer, touch and `highlightRange` paths), and `metaWithoutBelowTitle` does the same to a meta as a backstop, so a stored meta never points into the slot. With no `[data-viewer-below-title]` in the article both return their argument unchanged, so a document without the slot stores and restores exactly as before. Metas stored by a host before it adopted the prop (or by its prototype that inserted into the DOM) were counted with whatever was there; Viewer's content verification plus the text search recover those as for any drift (see "The annotation anchor schema").
|
|
1792
|
+
- **No-op guarantee, and how it is pinned:** `components/Viewer.belowTitle.test.tsx` (absent / null / false / true render the same article HTML as no prop; placement with and without a leading `# title`; a comment made on the paragraph after the title stores the same metas with and without a slot holding `<p>`, `<span>` and `<a>`; a stored comment on the second of two identical paragraphs restores onto that paragraph with the slot present, which a counted slot would paint on the wrong one; a selection inside the slot makes no comment and opens no toolbar; a selection from the title into the slot stores `plan` with both metas in the title; raw HTML carrying the attribute, with no prop, stores the metas web-highlighter computes) and `utils/belowTitleSlot.test.ts` (identity without a slot, the round trip, sentinels left alone, boundaries moved out of the slot, raw HTML or a nested marker never taken for the slot).
|
|
1793
|
+
|
|
1768
1794
|
## Viewer `resolveLocalLink` (ui 0.53.0)
|
|
1769
1795
|
|
|
1770
1796
|
Additive, one optional `Viewer` prop; Plannotator's plan review and annotate pass none, so their render is unchanged. Added for the Plannotator Inbox, where a link in a message to a file the message attached opens that file beside the thread.
|
|
@@ -1800,13 +1826,37 @@ Additive, all opt-in; Plannotator passes none of it, so its render is unchanged.
|
|
|
1800
1826
|
|
|
1801
1827
|
Additive, opt-in; Plannotator passes none of it, so its render is unchanged. Added for the iPhone app's attachment screen (PLAN step M2): the app's `WKWebView` decides every link itself, and a link in an agent's page must reach it as a new window opened by a person's tap, which the app checks against its own record of the touch, rather than as a frame message, which a page's script can forge.
|
|
1802
1828
|
|
|
1803
|
-
- **`HtmlViewer` `hostNavigates?: boolean`**: in Interact, a person's tap on a non-fragment link opens it as a new window (`window.open(href, '_blank', 'noopener')`; the srcdoc sandbox becomes `allow-scripts allow-popups`) instead of being reported through the frame protocol, so `onOpenLink` does not fire for it. The host's web view hands the window to its own delegate, which decides it; the frame itself never navigates (the surface's CSP would refuse an off-folder frame load in any case). A scripted click (`isTrusted` false) opens nothing. In-page `#` links still scroll in the frame and an armed pinpoint tap still pins.
|
|
1829
|
+
- **`HtmlViewer` `hostNavigates?: boolean`**: in Interact, a person's tap on a non-fragment link opens it as a new window (`window.open(href, '_blank', 'noopener')`; the srcdoc sandbox becomes `allow-scripts allow-modals allow-popups`) instead of being reported through the frame protocol, so `onOpenLink` does not fire for it. The host's web view hands the window to its own delegate, which decides it; the frame itself never navigates (the surface's CSP would refuse an off-folder frame load in any case). A scripted click (`isTrusted` false) opens nothing. In-page `#` links still scroll in the frame and an armed pinpoint tap still pins.
|
|
1804
1830
|
- **Bridge message `plannotator-bridge-set-host-navigates`** (`{ active }`), posted on ready while the prop is set. Additive to the protocol: an older bridge asset ignores it and keeps cancelling, so `BRIDGE_PROTOCOL_VERSION` is unchanged.
|
|
1805
1831
|
- **`ViewerHandle.revealPin?: () => void`** (`HtmlViewer`): scrolls the pinned draft into the middle of the frame through the new bridge message `plannotator-bridge-reveal-pin`, for a host whose own panel covers the page's lower part (the iPhone's comment panel shrinks the frame and the pin must stay in sight while Parent and Child move it). Additive to the protocol; an older bridge asset ignores it.
|
|
1806
1832
|
- **`ViewerHandle.takeSelection?: () => boolean`** (markdown `Viewer`, from `useAnnotationHighlighter`): with `onHostDraft` set on a touch screen, a text selection is no longer painted as a pending highlight, neither by the library at `touchend` (`highlighter.run()` is not called) nor when it settles (400 ms after the last `selectionchange`); painting replaced the system selection, and the iPhone's edit menu with it, before the person could reach the app's Comment. The host calls `takeSelection()` from its own Comment action: the same paint runs then and the draft goes out through `onHostDraft` as `'selection'`. Returns false with nothing selected in the document. Without `onHostDraft`, and with a mouse, nothing changes.
|
|
1807
1833
|
|
|
1834
|
+
## Comments on part of an image (published in ui 0.54.0 and core 0.25.14; #2100; additive, needs a new core subpath)
|
|
1835
|
+
|
|
1836
|
+
Everything below is opt-in and byte-identical when a host passes none of it.
|
|
1837
|
+
|
|
1838
|
+
- **core `image-region`** (new subpath, so core must publish before ui): `ImageRegion` (`{ v: 1, src, sourceLine, box: { x, y, w, h }, naturalWidth, naturalHeight }`, the box in the image's own pixels), the fail-closed `parseImageRegion`, and `imageRegionLocationLines` / `imageRegionFileName` / `imageRegionSize`. `external-annotation` validates `imageRegion` on POST and PATCH with it (a PATCH may not clear it).
|
|
1839
|
+
- **`Annotation.imageRegion`** (ui `types`, re-exports `ImageRegion` / `ImageRegionBox`). The highlighter and `annotationOwnsHighlight` skip such rows; the exporters print `Feedback on a region of the image <src>`, `Image: <src> (line N)` and `Region: x=… (of W×H)`; share links drop it.
|
|
1840
|
+
- **`Viewer` `imageRegionComments`**, with `onDeleteAnnotation` / `onEditAnnotation`: the lightbox gets a Move | Region switch over markdown images, draws boxes, opens `CommentPopover` beside a box and saves an ordinary COMMENT through `onAddAnnotation`. Selecting such an annotation from outside opens the lightbox on it. **`ImageLightbox` `regions`** (`LightboxRegionComments`) and **`LightboxImage` `writtenSrc` / `line` / `blockId`** are the same seam one level down; `collectDocumentImages` fills `writtenSrc` and `blockId` from `InlineMarkdown`'s new `data-image-src` attribute.
|
|
1841
|
+
- **`ImageZoomStage`** `drawing` / `onDraw` (`ZoomDraw`), `onViewChange`, `description`, `children`, and the handle's `zoomTo(box)` / `visibleBox()`; `imageZoom` adds `naturalBoxFromPoints`, `naturalBoxToStage`, `stageToNatural`, `visibleNaturalBox`, `viewportForBox`.
|
|
1842
|
+
- **`CommentPopover`** `placement: 'side'`, `headerText`, `zIndex`; `computeCommentPopoverPosition` takes the placement as a third argument.
|
|
1843
|
+
- **`AnnotationPanel` `imageRegionSrc`**: region cards show a crop thumbnail (absent: no crop, the file, size and line still show). The quick-actions footer is no longer drawn when it would hold no button.
|
|
1844
|
+
- **`components/ImageRegions`**: `RegionToolStrip`, `RegionBoxLayer`, `RegionThumb`, `RegionCardInfo`, `RegionNumber`, `regionOf`, `imageRegionNumbers`, `imageSourceLine`, `loadableImageSrc`.
|
|
1845
|
+
|
|
1808
1846
|
## Publishing & versioning
|
|
1809
1847
|
|
|
1848
|
+
- **ui 0.55.0: ui only. `@plannotator/core` is unchanged at 0.25.14** (nothing under `packages/core` moved since v0.28.37), so ui publishes alone and keeps pinning core `0.25.14` exactly. No guides.show deploy is needed. It is cut from `main` at `f103b6703` plus the version bump, and packed with `bun pm pack` from a clean `bun install --frozen-lockfile` at Bun 1.4.4. **No export name is removed** (checked name by name against ui 0.54.0 on npm), and peer ranges are unchanged. Hosts must act on these:
|
|
1849
|
+
1. **KaTeX 0.19.** `katex` is `^0.19.0` (was `^0.18.11`, which npm marks deprecated). Rendering is unchanged, but a host that serves its own `katex.min.css` should move it to 0.19. Mermaid 12.2.0 still declares `katex ^0.18.0`, so without action the install holds two KaTeX copies. Add a root override `"katex": "^0.19.0"` (`pnpm.overrides`, `overrides` or `resolutions`), or point Mermaid's `katex` at `utils/mermaid-math-slot`. See "KaTeX 0.19".
|
|
1850
|
+
2. **`HtmlViewer` `sandbox?: string`, for Workspaces.** The srcdoc frame's whole `sandbox` attribute. Workspaces passes `sandbox="allow-scripts"` so a shared document's `alert` / `confirm` / `prompt` cannot draw over its origin, and can drop its pnpm patch. Absent, the default stays `"allow-scripts allow-modals"`. See "`HtmlViewer` `sandbox`".
|
|
1851
|
+
3. **Smaller changes with no action.** A `Viewer`'s `#anchor` link looks the id up inside its own container, so a matching id elsewhere on the page no longer hides the target (#2316). Additive and inert until a host passes it (#2315): `Viewer` `onQuestionPictureZoom` (`BlockRenderer` `onQuestionPictureZoom`, `QuestionBlock` `onZoomChoice`), `ImageLightbox` `swipe` and `LightboxImage.name`, `imageZoom`'s `canPanToward`, and the new `hooks/useSwipeStep`.
|
|
1852
|
+
- **core 0.25.14 / ui 0.54.0: both change. Order: `core` 0.25.14, then `ui` 0.54.0. guides.show is already deployed.** Both are cut from the v0.28.37 release tag, `1722a14de`, and packed with `bun pm pack` from a clean `bun install --frozen-lockfile` at Bun 1.4.4. The previous pairs were ui 0.53.0 and ui 0.53.1 on core 0.25.13 (0.53.1 was published from `b68f0be02`). ui pins core `0.25.14` exactly and imports four core subpaths that 0.25.13 lacks: `agent-switch`, `forge-ref-card`, `html-embed` and `image-region`. So core goes first. core 0.25.14 moves the guides.show viewer pin from `viewer.DERExLls.js` + `viewer.BjyNeslq.css` (0.25.13) to `viewer.SgPt8W-S.js` + `viewer.DejL7_DK.css`. guides.show deployed on the v0.28.37 tag: both files and all 59 `langs` chunks answer 200 on `https://guides.show/v1/`, and the two viewer files match the manifest's SRI hashes, so no manual deploy is needed. **No export name is removed from either package** (checked name by name against core 0.25.13 and ui 0.53.1). Peer ranges are unchanged. Some defaults and outputs change, so Workspaces should check these before it upgrades:
|
|
1853
|
+
1. **KaTeX 0.18 (ui dependency).** A host that loads `katex.min.css` itself must move it to 0.18, or equations lay out wrong (21 classes were renamed). `@pierre/diffs` 1.5.2 now logs `console.error` for a miscounted hunk. `mermaid` is 12.2.0 and `motion` is `^14.4.0` (was `^12.38.0`). See "Dependency bumps: KaTeX 0.18, @pierre/diffs 1.5.2, Mermaid 12.2".
|
|
1854
|
+
2. **Plans open in the Clean look.** `gridEnabled` now defaults to `false`, and a stored value counts only with the v2 marker `plannotator-plan-look-v2 = 'resolved'`. A host that renders no chooser (Workspaces) opens every user in Clean. To keep stored values, call `markLookAndFeelChoiceResolved()` before the first settings read. A host that writes `gridEnabled` itself must also call it, or the value is lost on reload. See the README's "Upgrade note: the plan look defaults to Clean".
|
|
1855
|
+
3. **Wider types** (a compile error only where a host passes a narrower type). `DocumentRenderAs` gains `'asciidoc'`, and `useSharing`'s `setRenderAs` parameter widens with it, so a host whose own state is typed `'markdown' | 'html'` must widen its setter ("AsciiDoc documents"). core `inbox-types`' `InboxQuestionRecord.kind` and `InboxQuestion.kind` become `InboxQuestionKind` (`QuestionKind | "action"`), so code that passes a record's `kind` to `QuestionAnswer` must narrow it first. `computeCommentPopoverPosition` takes an optional third `placement` argument.
|
|
1856
|
+
4. **Visible changes with no prop.** `Tooltip` is a borderless float that opens after 150 ms (`--float-shadow`; a provider's own delays still win). `ImageLightbox` zooms and pans: its DOM changed, and a click beside the image closes it. `ImageZoomStage` shows a one-time "Use ⌘ + scroll to zoom" pill. `Settings` gains an "Enter saves a comment" row in General (`hideEnterSavesComment` hides it). `AnnotationPanel` no longer draws an empty quick-actions footer. With `onNavigateAnchor` set, `#Heading` in a card's text links to that section (#1419). `HtmlViewer`'s srcdoc frame is `sandbox="allow-scripts allow-modals"`, so an annotated page can open `alert` / `confirm` / `prompt` (#2063). A lazy block whose chunk fails to load shows its failure inside the block (`components/LazyBlockBoundary`, #1618), and `ThemeProvider` keeps an inline `color-scheme` it finds on `<html>`.
|
|
1857
|
+
5. **Question cards** (plan review, annotate and the Inbox change too). A recommendation that names no choice is now one more answer row tagged Recommended: the "Suggested" line and its **Use** button are gone. A `Recommended:` that begins a choice's label names that choice. A `Context:` line after the prompt is context. A choice's markdown image is taken out of its label (`QuestionChoice.image`), so the export prints the plain label. Contract lines (`Stakes:`, `Shows:`, `Why:`, `Reversible:`) parse by default but draw only with `Viewer` `questionContractLines`. Each card tag carries `data-question-tag="<tone>"`. See "Question cards: a recommendation names the choice it begins; …" and "Question cards: contract lines and picture choices".
|
|
1858
|
+
6. **core output changes for Inbox helpers.** `composeInboxReplyBody` adds an "other questions open" line and takes optional `messages` / `replyTo`; `inboxDefaultSkip` always returns false; `formatQuestionAnswersSection` prints a `by_reply` skip as "Skipped by the person" and drops the "treat them as skipped" sentence; `QUESTION_AUTHORING_GUIDE` changed. See "A Send never skips on its own; one reply per session".
|
|
1859
|
+
Everything else is additive and inert until a host passes it: `Viewer` `belowTitle`, `imageRegionComments`, `renderDirectiveBlock`, `questionContractLines` / `questionPictureChoices` / `questionPictureBadge`; `QuestionPictureTiles`; `AnnotationPanel` `imageRegionSrc`, `renderCardBody` and `cardParts`' `text`; `ImageAnnotator` `escapeCancels` / `hint`; `ImageZoomStage` seams (`fitAxis`, `padding`, `overlay`, `wheelHint`, drawing); `CommentPopover` `placement: 'side'`; `CodeFileView` `extraCss`, `revealLine`, `onTokenTarget`, `onTokenNavigate` and `CodeAnnotation.outsideDiff`; `DecisionControl` `menuFooter` and the plan arm of `utils/decisionSpec`; `useLinkedDoc` `scrollViewport` / `restoreScroll`; `useRefHoverCards` `onlyWithData`, `RefHoverCards`, `refCardRouteTransport`; `configurePlannotatorUI`'s `richCopy`, `digitHexIssueRefs` and `refCardTransport`; the `enterSavesComment`, `documentTextSize` and `documentFontFamily` settings (`Settings` `showDocumentTextSize` / `showDocumentFont`, `hooks/useDocumentTextSize`, `hooks/useDocumentFont`); `Settings` `onPermissionModeChange` and `cookieHostSetting`; `utils/asciidocParser`; the `shortcuts` barrel's `reviewExplorerShortcuts` and OS helpers; and the other new modules (`components/ImageRegions`, `RefHoverCard`, `SendFailureToast`, `PlanDecisionMenuExtras`, `utils/richClipboard`, `utils/notion`, `utils/sectionRefs`, `utils/scrollAnchor`, `utils/commentSubmitKey` and more). Core adds the subpaths `image-region`, `inbox-actions`, `inbox-folding`, `inbox-delivery-line`, `inbox-open-questions` and `inbox-format` (Experimental), plus `agent-switch`, `forge-ref-card`, `html-embed`, `guide-builder`, `guide-builder-tools`, `guide-links`, `package-changes`, `package-changes-npm`, `product-feedback` and `update-exit-codes`. It also adds `annotatable`'s `DocumentRenderKind` helpers, `forge-refs`' cross-repo refs, `question-block`'s `findDirectiveBlocks` / `blockKeyHash8` / contract-line types, and many `inbox-types` additions. Before core 0.25.14 is on npm, `bun run check:sibling-deps @plannotator/ui` fails and the package smoke cannot install ui outside the repo.
|
|
1810
1860
|
- **core 0.25.13 / ui 0.53.0: both change. Order: `core` 0.25.13, then `ui` 0.53.0. guides.show is already deployed.** Both were published on 2026-10-18, cut from `main` at `7aba0e9a5` (#1985): the npm tarballs match that tree file for file, apart from the generated `styles.css` and bridge assets. That is later than the publish prep (#1898), so the pair also carries what merged between the two; the notable changes are listed after item 5 below. ui pins core `0.25.13` exactly and imports one core subpath that 0.25.12 lacks, `@plannotator/core/skill-reference-block` (#1914, from `utils/skillReferences`), so core goes first. core 0.25.13 moves the guides.show viewer pin from `viewer.BYGvfCcj.js` + `viewer.CBpqTZpM.css` (0.25.12) to `viewer.DERExLls.js` + `viewer.BjyNeslq.css`. Both files, `chunks/index.DfozTB5d.js` and all 59 `langs` chunks answer 200 on `https://guides.show/v1/`, and the two viewer files match the manifest's SRI hashes, so no manual deploy is needed. The previous pairs were ui 0.52.0 and ui 0.52.1 on core 0.25.12. **No export name is removed from either package** (checked name by name against core 0.25.12 and ui 0.52.1), but core removes two Inbox fields from three types (item 6), and these behave differently, so Workspaces should check them:
|
|
1811
1861
|
1. **Block quotes and headings (ui `utils/parser`).** `parseMarkdownToBlocks` returns ONE `blockquote` block per run of `>` lines, its `content` the quote's inner markdown, rendered by the new `BlockquoteBlock` (fences, lists, nested quotes, tables inside one `<blockquote>`). Block ids after a quote that used to split shift down (restore re-anchors by text), and a host that reads a quote's `block.content` now sees fence and list markers. A heading now needs a space (or the line's end) after its `#`s, at top level too: `#123 fixed`, `#hashtag` and `#!/bin/sh` are paragraph text, no longer `<h1>`. See "Block quotes render their content (ui 0.53.0; …)".
|
|
1812
1862
|
2. **Long choice labels (core `question-block`).** `parseQuestionAnswer` caps `selected` labels at the new `MAX_QUESTION_SELECTED_CHARS` (20,000) instead of 200, `canonicalQuestionAnswer` maps an answer an older build cut at 200 back to its full label, and `questionExportItems` passes `choices` for labels over 200 (`questionChoicesNeedCanonicalLabels`). A host that checks picks against the choices itself must not cut picked labels shorter than the labels it compares them with. See "Long choice labels are picked whole (core 0.25.13; …)".
|
|
@@ -1820,7 +1870,7 @@ Additive, opt-in; Plannotator passes none of it, so its render is unchanged. Add
|
|
|
1820
1870
|
8. **Smaller changes.** `Tooltip`'s popup draws at `z-[110]` (was `z-50`) and carries `data-pn-tooltip` (#1982). Four default wires send `serverSession` once a tab has adopted one (`adoptServerSession`), and do nothing new otherwise: `ExportModal`'s notes wire, `PlanDiffViewer`'s VS Code diff wire and `useAgentJobs` (#1921), and `OpenInAppButton` (#1915), whose server now refuses an `/api/open-in` without a catalog `appId`. All four are on the unsupported list.
|
|
1821
1871
|
Additive and inert until a host passes it: `Viewer` `questionDecisionLines` with core's `ParseQuestionBlockOptions` (`decisionLines`) on `parseQuestionBlock` / `indexQuestionBlocks` (#1985; see "Questions without decision lines"), `questionReadOnly`, `questionStatusLabel` (`QuestionStatusLabel`), `imageLightboxStepping` and `onImageClick`; `AnnotationPanel` `compareAnnotations`, `title`, `titleNote` and `cardParts`, and the export-only `exportLocation` field the exporters print under an entry's heading (#1931; described in `README.md`); `components/ComposerTokenTextarea`, `HumanOnlySkillNotice` `destination`, `renderSkillReferencesBlock`, `normalizeSkillContent`, `setSkillCatalogExportRegistration` and `configurePlannotatorUI`'s `skillReferenceExport` / `skillInstructionsDestination` (#1914); `components/HostTitle`, `utils/textCut` and `configurePlannotatorUI`'s `componentTooltips` (#1982); `components/ImagePeek` and `ImageLightbox` `images` (#1944); `Tooltip` `shouldOpen`, `container` and `escapePassesThrough`; `CodeFileView` `overflow`; `hooks/useOpenInApps` and `utils/storage`'s `getStoredOpenInApp` (#1920); `utils/pasteInsert` (`installPlainTextPaste`, the Chrome 155-156 macOS paste workaround Plannotator installs, #1970); `SessionBridgeInfo.interruptible`, `hasNonInterruptibleSession` and `SessionAskNotice` `canInterrupt` (#1807: a bridge that advertises `interruptible: false` gets no "Interrupt and ask now", and its Stop reads "Stop listening"). Core adds the subpaths `file-kind` (#1916), `inbox-search` (#1902) and `skill-reference-block` (#1914), `open-in-apps`' `checkOpenInAppId` (#1915), `markdown-structure`'s `codeFenceFor` (#1914), `question-block`'s `capQuestionPrompt`, `inbox-questions`' `cutText` / `sliceText` / `inboxStillOpenLine` and the person-thread names (`derivePersonThreadSubject`, `suggestPersonThreadName`, #1956), and `inbox-types` now re-exports `inbox-open-in` (#1920). Also new in this pair, for hosts that read the Inbox's wire: `inbox-attachments`' video support (the `"video"` kind, `INBOX_VIDEO_BLOCK_ID`, `inboxVideoMeta`, `readInboxVideoTime`, `formatInboxVideoTime`; #1931) and `InboxInlineImage` (#1944); and in `inbox-types`, `InboxAck` / `INBOX_ACK_LABEL` (#1947, #1980), `InboxDraft*` / `InboxCompose*` / `InboxUpload` and the `upl` / `drf` id prefixes (#1917), `InboxNotifyReach` / `InboxWindowPermission` (#1967), `InboxThreadNaming` / `InboxThreadNamedBy` (#1956), `INBOX_TOOL_COST` / `inboxToolCostText` (#1962), `inboxRowOffersMarkRead` / `inboxRowWaitsOnPerson` (#1926) and `THREAD_NAME_BIDI` / `THREAD_NAME_CONTROL`. ui also adds `collectDocumentImages`, `LightboxImage` and `lightboxImageName` beside `ImageLightbox` (#1944), `COMPOSER_TEXT_CLASSES` and `ComposerMentionChips` on `components/ComposerTokenTextarea` (#1914), and `useRememberedOpenInApp` / `loadOpenInApps` in `hooks/useOpenInApps` (#1920).
|
|
1822
1872
|
- **core 0.25.11 / ui 0.51.0: both change. Order: guides.show deploy, then `core` 0.25.11, then `ui` 0.51.0.** ui imports `canonicalQuestionAnswer` and `PI_THINKING_LEVELS`, which no published core before 0.25.11 has, and pins core `0.25.11` exactly. core 0.25.11 pins a new guides.show viewer stylesheet (`viewer.DuqkfIUz.css`, from the gruvbox fix #1721; the viewer JS `viewer.BYGvfCcj.js` is unchanged), which guides.show serves only after a deploy, so run the deploy by hand first (next bullet). **ui 0.51.0 is BREAKING for 0.50.0 consumers of the "Ask this session" exports**: it removes `findUsableSessionBridge`, `resolveSessionBridgeFallback`, the `'fallback'` `SessionAskAction` member, `SessionAskActions`' `fallbackLabel` and `DocumentAIChatPanel`'s `sessionAskFallbackLabel`, and makes a listed bridge the only Ask AI selection. Release notes must say so. Everything else is additive or a fix (including #1725's agent tool switch and offer); see "Ask this session (shipped in 0.50.0; …)", "Comment composer trim (0.51.0; …)", "Ask AI from a diagram comment (0.51.0; …)" and "Questions, restore, drafts and the rest (ui 0.51.0, core 0.25.11)".
|
|
1823
|
-
- **guides.show before a core publish that moves the viewer pin.** guides.show deploys on Plannotator release tags only. When a core publish carries a new `guide-viewer-manifest` hash (any change to the shared CSS or the guide chain) and no release tag has shipped it yet, run the deploy by hand first (`gh workflow run guides-show-deploy.yml --ref main`), then check that `curl -I https://guides.show/v1/<css>` and `<js>` from the manifest answer 200. Otherwise every host's guide pages load without that asset (core 0.25.9 hit this; fixed by a manual deploy). The deploy is add-only.
|
|
1873
|
+
- **guides.show before a core publish that moves the viewer pin.** guides.show deploys on Plannotator release tags only. When a core publish carries a new `guide-viewer-manifest` hash (any change to the shared CSS or the guide chain) and no release tag has shipped it yet, run the deploy by hand first (`gh workflow run guides-show-deploy.yml --ref main`), then check that `curl -I https://guides.show/v1/<css>` and `<js>` from the manifest answer 200. Otherwise every host's guide pages load without that asset (core 0.25.9 hit this; fixed by a manual deploy). The deploy is add-only. The manifest is no longer committed: core's `prepack` runs `bun run build:viewer-pin`, which writes it from the viewer built from your checkout, so pack from a clean `bun install --frozen-lockfile` at the CI Bun (1.4.4) on the commit the deploy ran from, and read `<js>` and `<css>` from the packed `package/guide-viewer-manifest.ts` (`tar -xOf <tgz> package/guide-viewer-manifest.ts`) for the curl check.
|
|
1824
1874
|
- **core 0.25.9 / ui 0.49.0 (question host seams): both change. Publish `core` first**: ui imports the new `@plannotator/core/markdown-structure` subpath and the new `findQuestionBlocks` / decision fields, so a ui on a published core 0.25.8 fails to compile in a consumer. See "Question host seams (ui 0.49.0, core 0.25.9)".
|
|
1825
1875
|
- **core 0.25.8 / ui 0.48.0 (questions in documents): both change. Publish `core` 0.25.8 first, then `ui` 0.48.0**, which pins core `0.25.8` exactly and imports the new `@plannotator/core/question-block` subpath. Core is a patch bump because its change is additive and pre-1.0 caret ranges would not accept a minor. See "Questions in documents (0.48.0, core 0.25.8)".
|
|
1826
1876
|
- **core 0.25.7 / ui 0.47.0 (host link widgets + model source hint + `persistViewedFiles`): both packages change, and both need unpublished upstream packages first.** Order: `@plannotator/atomic-editor` 0.9.0, then `@plannotator/markdown-editor` 0.5.0, then `bun install` here to refresh `bun.lock`, then publish `core` 0.25.7, then `ui` 0.47.0. ui pins core `0.25.7` exactly, `@plannotator/atomic-editor` `^0.9.0` and `@plannotator/markdown-editor` `^0.5.0`. See "Host link widgets (0.47.0, core 0.25.7)".
|
|
@@ -1836,7 +1886,7 @@ Additive, opt-in; Plannotator passes none of it, so its render is unchanged. Add
|
|
|
1836
1886
|
- The previous pair was `@plannotator/ui` `0.40.0` on `@plannotator/core` `0.25.3` (publish order the same). Three things shipped in 0.40.0: (1) **Mermaid 12.0.0**, pinned exactly (was `^11.17.2`): ELK layout by default for flowchart/state/class/ER/requirement, Safari 17.4+ / ES2024 floor, SVG ids byte-identical to 11 but `g.edgePaths` children now in declaration order, and the plan editor no longer imports `utils/mermaid-eager` (the lazy path is the default for everyone; hosts that want startup registration import the eager entry themselves) — see "Mermaid 12 (0.40.0)"; (2) **theme-aware Mermaid diagrams**: New additive exports `utils/mermaidTheme` (`buildMermaidThemeVariables`, `readThemeTokens`, `applyMermaidTheme`, `mermaidThemeKey`, `buildMermaidConfig`, `ensureContrast`, `isDarkBackground`, `MERMAID_THEME_TOKEN_NAMES`) and `utils/cssColor` (parser + OKLab/contrast toolkit). `MermaidBlock` now calls `useTheme()` and `applyMermaidTheme` before each render; `MERMAID_CONFIG`, `loadMermaidRuntime`, `mermaid-eager` and `securityLevel: 'strict'` are unchanged. A host whose document carries no theme tokens renders diagrams byte-identically to 0.39.0; a host that mounts `ThemeProvider` with `theme.css` gets diagrams in its palette and mode with no configuration. No new peer dependencies; core unchanged. See "Theme-aware Mermaid diagrams (0.40.0)".; (3) **element context through the host seam (#1521, #1549), which is what moves `core` to 0.25.3:** `@plannotator/core/html-anchor` gains `parseHtmlElementContext`, `MAX_ELEMENT_CONTEXT_BYTES` and `MAX_PAGE_URL_LENGTH`; `PersistedHtmlAnchor.elementContext?` and `HtmlAnnotationTarget.context?` now round-trip through `buildPersistedHtmlAnchor` and `projectHostThreads`. **Core changes here, so bump and publish `core` first** and update UI's exact core dependency before packing ui — a ui build that imports these from an older published core fails to compile in a consumer, the 0.38.0 failure mode. `@plannotator/ui/components/html-viewer` re-exports the validator, so 0.39.0's import site is unchanged, and rows without context stay byte-identical on the wire. `utils/parser` gains `includeOutline` on `elementContextExportBlock` / `exportAnnotationEntry`, and `exportAnnotationEntry`'s `includeRoute` now defaults to true per field. See "Element context through the host seam".
|
|
1837
1887
|
- The previous pair was `@plannotator/ui` `0.39.0` on `@plannotator/core` `0.25.2` (core unchanged; nothing under `packages/core` moved). UI 0.39.0 adds **element context** to raw-HTML and live-app pinpoint annotations (#1517, #1520): a new optional `Annotation.elementContext` (`HtmlElementContext` in `@plannotator/ui/types`) and `HtmlAnnotationTarget.context`, captured by the bridge at click time (tag, id, author classes, ancestor `path`, `role`, accessible `name`, an allowlisted `attrs` set with href/src scrubbed of query and fragment, rendered `text`, an adaptive collapsed HTML `outline`, child count, viewport `rect`, nearest `landmark` and `heading`, a `component` hint, and in live-app sessions `page`), hard-capped at 2 KiB serialized per primary and 1 KiB per extra target, and re-validated at the parent trust boundary by the new `parseHtmlElementContext` export of `@plannotator/ui/components/html-viewer`. New helpers on `@plannotator/ui/utils/parser`: `elementContextExportBlock(ann, { includeRoute })` (the fenced skeleton plus selector/path/role/name/attrs/text/box/near lines the full export now prints under a context-bearing comment) and `exportAnnotationEntry(ann, { includeRoute })` (one annotation as a standalone feedback entry, a pure helper for hosts; `AnnotationPanel`'s card chrome is unchanged from 0.38.2). The field is purely descriptive: `HtmlElementAnchor` and restore are untouched, no `BRIDGE_PROTOCOL_VERSION` bump, share links drop it like anchors, annotations without it export byte-identically, and the repaint path posts only anchors to the bridge. **Host persistence gap in 0.39.0 itself, closed in the next publish (#1521, #1549)**: as shipped, 0.39.0's `@plannotator/core/html-anchor` (`buildPersistedHtmlAnchor`, `projectHostThreads`) does not carry `elementContext`, so a host pinned to 0.39.0 that persists through those helpers drops it on save and must persist and project the field itself. The next publish carries it end to end — see "Element context through the host seam". Peer ranges are unchanged from 0.38.2: `react` / `react-dom` `^19.2.3`, `tailwindcss` as before, and `@codemirror/state ^6.7.2` beside `@codemirror/view ^6.43.10`. Decision-control change in the same window (#1516): the header primary reads `Send Feedback` / `Post Comments` with no inline count (`DecisionPrimary.count` removed; internal, not host-supported surface).
|
|
1838
1888
|
- Before that, `@plannotator/ui` `0.38.2` on `@plannotator/core` `0.25.2`. UI 0.38.2 keeps the type word in a titled alert's accessible name through a visually hidden `sr-only` span before the title instead of an `aria-label` on the title row (naming a generic `div` is prohibited by ARIA and WebKit drops it, so VoiceOver on Safari read only the bold title in 0.38.1), and loosens the React peer back to `^19.2.3` (0.38.1 declared `^19.2.8` only because the dependency batch moved it; nothing in the package needs a newer API). **Do not consume ui 0.38.0**: it imports `@plannotator/core/token-hover` (the hover-card trigger settings, #1462) but pins core 0.25.1, which never exported that subpath, so it fails to compile in any consumer; 0.38.1 is the same UI pinning core 0.25.2, which publishes `./token-hover`, the rotated `guide-viewer-manifest` pin, and the `config-types` hover fields (core 0.25.2 is the first core publish since 0.25.1 even though those changes landed over several releases; the package smoke now diffs the UI's core imports against the registry so an unpublished core subpath fails preflight instead of the consumer). UI 0.38.1 also aligns `@codemirror/state` to `^6.7.2` beside `@codemirror/view ^6.43.10`, so a consumer can no longer resolve two state copies. UI 0.38.0 also renders a GitHub alert's bold-only first body line as its title on the icon row (an emoji on that line becomes the icon; `<!-- icon: name -->` is stripped and resolved through the new `alertIconRenderer` seam, null by default; grammar in `utils/alertTitle`, importable by a host editor so it writes the bytes the reader parses; a fenced code block inside an alert body still renders as text, deferred because nesting a `CodeBlock` inside a block interacts with the positional annotation anchors and needs its own design). UI 0.38.0 carries the whole unified decision-control stack: the internal primitives (`DecisionControl`, `utils/decisionSpec`, `hooks/useDismissablePopover` — not host-supported surface, see the unsupported list; `useDismissablePopover` also replaced the hand-rolled dismissal inside `ActionMenu`/`ApproveDropdown`, both likewise unsupported) plus one blessed-barrel addition: `decisionControlShortcuts` on `@plannotator/ui/shortcuts` (pure scope data, fetch-free, same contract as the other scopes). The removal of `ToolbarButtons`' platform-mode `muted` prop is internal — `ToolbarButtons` is not host-supported surface. UI 0.37.0 added the Viewer-owned document-header seam (a new public API, hence the minor bump; 0.36.1 was reserved for it but never published) while retaining the `hideQuickLabel` and `StickyHeaderLane` seams from the 0.35.x and 0.36.0 releases; core 0.25.1 publishes the `annotation-threads` subpath already used by `AnnotationPanel` and `utils/parser`, and UI pins that corrected core exactly.
|
|
1839
|
-
- Recent pairs, for the consumer's install matrix: ui 0.32.0 on core 0.25.0 (lockstep, `html-anchor`), ui 0.33.0 and ui 0.34.0 on core 0.25.0 (ui only), and ui 0.35.2, ui 0.36.0, and ui 0.37.0 on core 0.25.1 (0.36.1 was never published), ui 0.38.1, ui 0.38.2, and ui 0.39.0 on core 0.25.2, and ui 0.40.0 on core 0.25.3 (lockstep, `html-anchor` element context). Since then, from the registry: ui 0.41.0, 0.41.1 and 0.41.2 on core 0.25.4; ui 0.42.0, 0.43.0 to 0.43.2, 0.44.0, 0.45.0 and 0.45.1 on core 0.25.5; ui 0.46.0 and 0.46.1 on core 0.25.6; ui 0.47.0 on core 0.25.7; ui 0.49.0 on core 0.25.9 (core 0.25.8 and ui 0.48.0 were never published; their changes first shipped in ui 0.49.0 / core 0.25.9); ui 0.50.0 on core 0.25.10; ui 0.51.0 on core 0.25.11; ui 0.52.0 and 0.52.1 on core 0.25.12; ui 0.53.0 on core 0.25.13 (the current `latest`
|
|
1889
|
+
- Recent pairs, for the consumer's install matrix: ui 0.32.0 on core 0.25.0 (lockstep, `html-anchor`), ui 0.33.0 and ui 0.34.0 on core 0.25.0 (ui only), and ui 0.35.2, ui 0.36.0, and ui 0.37.0 on core 0.25.1 (0.36.1 was never published), ui 0.38.1, ui 0.38.2, and ui 0.39.0 on core 0.25.2, and ui 0.40.0 on core 0.25.3 (lockstep, `html-anchor` element context). Since then, from the registry: ui 0.41.0, 0.41.1 and 0.41.2 on core 0.25.4; ui 0.42.0, 0.43.0 to 0.43.2, 0.44.0, 0.45.0 and 0.45.1 on core 0.25.5; ui 0.46.0 and 0.46.1 on core 0.25.6; ui 0.47.0 on core 0.25.7; ui 0.49.0 on core 0.25.9 (core 0.25.8 and ui 0.48.0 were never published; their changes first shipped in ui 0.49.0 / core 0.25.9); ui 0.50.0 on core 0.25.10; ui 0.51.0 on core 0.25.11; ui 0.52.0 and 0.52.1 on core 0.25.12; ui 0.53.0 and 0.53.1 on core 0.25.13; ui 0.54.0 and 0.55.0 on core 0.25.14 (ui 0.55.0 and core 0.25.14 are the current `latest` once published). Do not consume ui 0.35.0 externally because its published manifest contains `workspace:*`; do not consume ui 0.35.1 because its exact core 0.25.0 dependency lacks the `annotation-threads` export. Do not consume ui 0.38.0 because its exact core 0.25.1 dependency lacks the `token-hover` export.
|
|
1840
1890
|
- When both packages change, **publish `core` first**: ui 0.32.0 imports the `@plannotator/core/html-anchor` subpath, which no earlier published core (0.24.0 and before) has, just as ui 0.29.0 needed core 0.23.0 for `@plannotator/core/annotatable`. Bump core, update UI's exact core dependency to the same new version, and run `bun install` so `bun.lock` records the new workspace versions before packing either package.
|
|
1841
1891
|
- The HTML annotation seams also changed the guides.show viewer **stylesheet** (five utility rules from `HtmlSurfaceControls`; the viewer JS is unchanged), so `packages/core/guide-viewer-manifest.ts` now pins a CSS hash that exists on guides.show only after the deploy workflow has published this build's `/v1/` assets. A guide exported from this build before that deploy would pin a stylesheet the host does not serve yet: **deploy guides.show before any release that ships this manifest.**
|
|
1842
1892
|
- UI declares the already published core version exactly in its source manifest. Do not replace it with `workspace:*`: direct publication can preserve that protocol and make the package impossible to install outside this repository. Bun links the local core workspace whenever its version matches the exact dependency. Before publishing, run `bun run --cwd packages/ui smoke:package`; it checks the source and packed manifests, required tarball subpaths, local Bun linking, and a real pnpm install in an external temporary consumer. When both packages change, publish **`core` first, then `ui`**.
|
|
@@ -1885,8 +1935,18 @@ was rebuilt.
|
|
|
1885
1935
|
The `file-tree-item` CSS was already in `theme.css`. The new pieces are the
|
|
1886
1936
|
`DiffFileTree` component itself and its ARIA/keyboard handling.
|
|
1887
1937
|
|
|
1938
|
+
**Since the aligned sidebar (approved design record "review-sidebar-aligned"),
|
|
1939
|
+
the review draws its own rows.** Its Changes tree has its own folder row and
|
|
1940
|
+
file row (`FileTreeNode.tsx`: 12 px indent steps on the panel's edges, `+`/`−`
|
|
1941
|
+
in two aligned columns, `CountColumns` in `FileRowBits.tsx`); it no longer
|
|
1942
|
+
renders `DiffFileTreeFolderRow`, `DiffFileTreeFileRowContent` or `DiffCounts`.
|
|
1943
|
+
It still shares `buildDiffFileTree`, `useDiffFileTreeExpansion`,
|
|
1944
|
+
`ChangeTypeLetter`, and `diffFileTreeIndent` (All files keeps that indent).
|
|
1945
|
+
The atoms below are unchanged and stay the host's starting point for a custom
|
|
1946
|
+
row; they no longer show what the review's rows look like.
|
|
1947
|
+
|
|
1888
1948
|
**Review-only, and why.** The review's `FileTree` keeps its own wrapper and
|
|
1889
|
-
|
|
1949
|
+
rows, adding: the viewed checkbox and the
|
|
1890
1950
|
hide-viewed filter, stage controls and the staged/committed/untracked marks,
|
|
1891
1951
|
the annotation badge and `has-annotations` tint, the scroll-highlight row, the
|
|
1892
1952
|
"Copy path" context menu, the double-click handler, the panel header, view toggle,
|
|
@@ -1894,8 +1954,8 @@ diff-type / base / worktree / evolog pickers, search results, the nav rows
|
|
|
1894
1954
|
(PR overview, artifacts, call flow, semantic diff, all files), and its
|
|
1895
1955
|
**window-wide** j/k/arrow/Home/End shortcut (an app shortcut, gated on inputs,
|
|
1896
1956
|
menus and dialogs). None of those is a host concern, so `DiffFileTree` takes no
|
|
1897
|
-
props for them; a host that needs any of them builds its own row from the atoms
|
|
1898
|
-
|
|
1957
|
+
props for them; a host that needs any of them builds its own row from the atoms
|
|
1958
|
+
(the review now goes further and draws its own rows, above). The review rows also keep their current DOM: no ARIA tree
|
|
1899
1959
|
roles were added there (that would be a behavior change to Plannotator's app,
|
|
1900
1960
|
out of scope here).
|
|
1901
1961
|
|
|
@@ -2352,9 +2412,20 @@ For the Plannotator Inbox's composers (approval "inbox-skill-references"). Every
|
|
|
2352
2412
|
For the Plannotator Inbox's one viewer (approval "inbox-image-viewer", 2026-10-15). A host that passes none of the new props sees no change.
|
|
2353
2413
|
|
|
2354
2414
|
- **`components/ImagePeek`** (new): the picture list a stepper opens ("A · Peek"). Props `rows: ImagePeekRow[]` (`{ key, label, items: ImagePeekItem[] }`; an item is `{ key, name, thumb?, tile?, count?, attached?, detail? }`), `current`, `onPick(key)`, `onClose(reason: 'key' | 'outside')`, `variant: 'popover' | 'sheet'` (a phone's bottom sheet, three across, with a backdrop), `label`, `id`, `className`. An ARIA `grid` (rows with a `rowheader`, `gridcell` items, `aria-selected` on the open one); focus starts on the open item; ← → along the items, ↑ ↓ between rows, Home / End, Enter / Space pick, Esc / Tab close. A press outside a popover closes it, except on an element marked `data-peek-opener`. `forwardRef` exposes **`ImagePeekHandle.handleKey(event)`** for a modal that intercepts keys before they reach it. Images are only `<img src>`. Also exported: `peekMove` (the key rule, pure).
|
|
2355
|
-
- **`ImageLightbox`** gains **`images?: LightboxImage[]`** (`{ src, alt, section? }`): with two or more it is a full-screen stepping view ("2 of 4 · in “Options”", ← → and `]` `[`, the end note, Actual size, the pill and the peek); without it, the
|
|
2415
|
+
- **`ImageLightbox`** gains **`images?: LightboxImage[]`** (`{ src, alt, section? }`): with two or more it is a full-screen stepping view ("2 of 4 · in “Options”", ← → and `]` `[`, the end note, Actual size, the pill and the peek); without it, the single-image overlay (byte for byte until the zoom below). Also exported: `collectDocumentImages(container)` and `lightboxImageName(image)`.
|
|
2356
2416
|
- **`Viewer`** gains **`imageLightboxStepping?: boolean`** (the lightbox steps through every zoomable image of the rendered document; Plannotator's plan review and annotate pass it) and **`onImageClick?: (src, alt) => void`** (the host takes image clicks and no lightbox opens; the Inbox opens a message's captured image in its own viewer). Both absent: unchanged.
|
|
2357
2417
|
|
|
2418
|
+
## Zoom in `ImageLightbox` (#2067; published in ui 0.54.0; no prop change, no core change)
|
|
2419
|
+
|
|
2420
|
+
Both forms of **`ImageLightbox`** now zoom and pan (`components/ImageZoomStage`, with the pure math in `components/imageZoom.ts`): Ctrl/⌘ + wheel, trackpad pinch (Safari's gesture events too) and two fingers zoom about the pointer; a plain wheel scrolls an image larger than the stage and zooms one that fits; drag pans; a double click toggles Fit and 2× Fit; Fit to 800%. What a host notices:
|
|
2421
|
+
|
|
2422
|
+
- **The single overlay's DOM changed.** The image sits in a full-screen focusable stage (`role="group"`, `tabIndex=0`, `data-lightbox-stage`) and is placed with a transform; the alt caption is pinned near the bottom. `[data-image-lightbox] img` still finds the one image.
|
|
2423
|
+
- **A tap or click beside the image closes it, from the click** (a press that moved, a cancelled touch, or a click on the image does not). Closing on the pointerup let a touch's synthesized click land on the page beneath.
|
|
2424
|
+
- **Actual size pans instead of scrolling.** The stepping stage is no longer an `overflow: auto` scroller with scrollbars: the wheel, a drag, and the browser's scroll keys while the stage has focus move the image.
|
|
2425
|
+
- **The page never zooms over the lightbox**: Ctrl + wheel and `gesturestart` / `gesturechange` / `gestureend` are prevented anywhere over it.
|
|
2426
|
+
|
|
2427
|
+
**`ImageZoomStage`** itself gains optional host seams (additive; the lightbox passes none, so it is unchanged): **`fitAxis?: 'contain' | 'width' | 'height'`** (Fit fills the width, a tall screenshot read from the top, or the height, a panorama read from the left; the smallest scale stays the whole image, and a gesture reports `fit` at that Fit scale), **`padding?: number`** (Fit leaves that many CSS pixels on every side, measured with the stage; wins over `inset`), **`overlay?: ReactNode`** (drawn over the image at its own size in the image's own transform, so a marked-up copy stays aligned at any zoom and pan; `data-zoom-overlay`), **`onImageLoad?: (size) => void`** and **`onImageError?: () => void`** (the host shows its own failure; the stage then shows none). `imageZoom.ts` adds `FitAxis`, `fitScale(…, axis)`, `fitViewport(…, axis)`, `paddingInset(stage, padding)`, and `atFit(view, limitsOrScale)`. The Plannotator Inbox's file viewer uses all five.
|
|
2428
|
+
|
|
2358
2429
|
## `Tooltip` `shouldOpen` (ui 0.53.0; additive, no core change)
|
|
2359
2430
|
|
|
2360
2431
|
**`Tooltip`** (`components/Tooltip`) gains **`shouldOpen?: (trigger: Element) => boolean`**, asked each time the tooltip would open; `false` keeps it closed that time (Base UI's `onOpenChange` with `cancel()`). Unset, it always opens, as before. The Inbox passes a check that the trigger's text is cut (`isTextCut` in `utils/textCut.ts`: any whole pixel of `scrollWidth` over `clientWidth`, or the text's Range width over the box's content width by a hundredth of a pixel, since Chrome draws an ellipsis for a 0.06 px overflow that the integer widths hide), so a cut label shows its whole text only while it is cut.
|
|
@@ -2370,6 +2441,140 @@ For a host that records no decisions; the Plannotator Inbox passes it since its
|
|
|
2370
2441
|
- **Core:** `parseQuestionBlock(kind, body, options?)` and `indexQuestionBlocks(blocks, options?)` take an optional **`ParseQuestionBlockOptions`** with **`decisionLines?: boolean`** (default true, the old parse). False: a `Decision:` line is kept as context, and the result never carries `decisionOnAnswer` or `decision`. A line the default parse reads still ends the choice, bullet or recommendation above it, so the prompt, `questionKey`, numbering and choices are the default parse's, and an answer stored against one parse matches the other.
|
|
2371
2442
|
- **ui:** `Viewer` **`questionDecisionLines?: boolean`** (default true) passes that option to its question index, so with `false` no card gets a decision tag or row from a `Decision:` line. Absent: unchanged.
|
|
2372
2443
|
|
|
2444
|
+
## Enter saves a comment (#1934; published in ui 0.54.0; additive, no core change)
|
|
2445
|
+
|
|
2446
|
+
An opt-in key for every comment composer. Default off, so a host that does nothing sees no change: comments save with Cmd/Ctrl+Enter, Enter adds a new line.
|
|
2447
|
+
|
|
2448
|
+
- **`utils/commentSubmitKey`** (new): **`isCommentSubmitKey(event, enterSaves)`**, the one check every comment composer asks. Cmd/Ctrl+Enter always saves; with `enterSaves` a plain Enter saves too (not with Shift or Alt); an Enter that belongs to an IME composition never saves (`isComposing`, or `keyCode` 229, read off the event or React's `nativeEvent`). Also **`isImeCompositionKey(event)`**. Callers offer the key to their skill / mention menu first, so an arrowed-to row still takes Enter.
|
|
2449
|
+
- **Setting `enterSavesComment`** (`config/settings.ts`, cookie-only `plannotator-enter-saves-comment`, default false). Read with `useConfigValue('enterSavesComment')`. Each tab reads it at load (a change in another tab, or on another localhost port, applies after a reload). Composers that follow it: `CommentPopover`, `AnnotationPanel`'s two edit boxes, `CodeFilePopout`'s edit box. `DiagramComposer` saves on Enter whatever the setting says, as it always has.
|
|
2450
|
+
- **`DecisionNoteField`** gains **`enterSubmits?: boolean`** (default false: only Mod+Enter submits, the decision control's note composer). True: `isCommentSubmitKey(event, true)`, for a field used as a comment (Plannotator's review sidebar passes the setting's value).
|
|
2451
|
+
- **`Settings`**: General gets an **"Enter saves a comment"** switch row, so a host that renders `Settings` gets the row too. **`hideEnterSavesComment?: boolean`** (default false) hides it.
|
|
2452
|
+
|
|
2453
|
+
## A Send never skips on its own; one reply per session (published in core 0.25.14; additive exports, two output changes)
|
|
2454
|
+
|
|
2455
|
+
Approval "inbox-explicit-skip" (2026-10-21). `@plannotator/core/inbox-questions` gains **`inboxOtherStillOpenLine(questions)`** and the type **`InboxReplyMessageAnswers`**; **`INBOX_STILL_OPEN_LINE`** also matches the new line. **`composeInboxReplyBody`** gains two optional inputs, **`messages`** (every message a reply answers, each with its questions, sent and earlier answers) and **`replyTo`**; with answers on several messages, or on another message than `replyTo`, the answers section gets a `### Your message <id>` heading per message. Output changes: (1) a reply that sends answers or skips while questions stay open now carries "You have N other questions open in this thread. They stay open; the person will answer them in the Inbox. Don't re-ask them." after the words (before, no line); (2) the still-open count never counts the questions the reply sends. `@plannotator/core/inbox-delivery`'s **`inboxDefaultSkip`** keeps its signature and always returns false. `@plannotator/core/question-block`'s **`formatQuestionAnswersSection`** prints a `by_reply` skip as "Skipped by the person" (was "Skipped (not answered in this reply)") and no longer adds the "N were not answered in this reply; treat them as skipped…" sentence; nothing else in its output changes. A host that composes Inbox replies itself and wants the old body passes neither input and gets (1) anyway.
|
|
2456
|
+
|
|
2457
|
+
## Plan review's decision control (published in ui 0.54.0; additive, no core change)
|
|
2458
|
+
|
|
2459
|
+
Plan review now uses the same adaptive `DecisionControl` annotate and code review use (#1057). Everything below is optional; a host that passes none of it renders and posts exactly what it did before.
|
|
2460
|
+
|
|
2461
|
+
- **`utils/decisionSpec`**: `DecisionSpecInput.app` gains **`'plan'`**, and the input gains **`plan?: DecisionPlanInput`** (`{ answers, directEdits, answersOnly, agentName, approveTarget?, savedFilesOnly? }`). The plan arm: Approve (or `Approve → <agent>` for OpenCode's hand-off) with nothing to send, its caret holding "Approve with a note…" (only with `approvalNotesSupported`) and "Request changes…"; with feedback, Send Feedback (or "Send answers" when the only feedback is question answers), its caret holding "Send with a note…", "Approve with notes" (only with `approvalNotesSupported`) and "Approve, discard …" (always, with a confirm that names the agent when notes on approval would be lost). Ids are the existing `DecisionActionId`s; nothing new to handle.
|
|
2462
|
+
- **`DecisionControl`** gains **`menuFooter?: ReactNode | ((api: { closeMenu }) => ReactNode)`**: rows under the decisions in the same menu. The arrow keys now also walk `role="menuitemradio"` rows. Absent: the menu is the decisions alone, as before.
|
|
2463
|
+
- **`components/PlanDecisionMenuExtras`** (new, Plannotator chrome like `DecisionControl`): `AgentSwitchMenuSection({ agents })` (the "After approval, switch to" radio section; reads and writes the `agentSwitch` setting, the menu stays open on a pick) and `PermissionModeMenuLine({ agentName, modeLabel, onChange })` (one read-only line with a Change link).
|
|
2464
|
+
- **`utils/agentSwitch`** gains **`agentSwitchTargetLabel(setting, agents)`** (the display name an approval hands off to; null when switching is off, or when it is `custom` with no name). `ApproveDropdown` uses it, which fixes one label: a `custom` switch with no name used to read "Approve → custom" (an agent that does not exist; approval sends no switch then) and now reads plain "Approve". Plannotator no longer renders `ApproveDropdown`.
|
|
2465
|
+
- **`Settings`** gains **`onPermissionModeChange?: (mode) => void`**, called when its Permission Mode row changes. Absent: unchanged.
|
|
2466
|
+
|
|
2467
|
+
## Viewer `renderDirectiveBlock`; core `findDirectiveBlocks`, `blockKeyHash8` and `./inbox-actions` (published in ui 0.54.0 and core 0.25.14; additive, plan review unchanged)
|
|
2468
|
+
|
|
2469
|
+
Approval "inbox-next-actions" (2026-10-21): the Plannotator Inbox draws an agent's `:::action` block as a Action item card. Nothing below changes plan review, annotate or code review; a host that passes nothing renders exactly as before.
|
|
2470
|
+
|
|
2471
|
+
- **`Viewer` `renderDirectiveBlock?: (block: Block) => ReactNode | undefined`**: called for each `:::kind` directive block that is not a question; a returned node is drawn in place of the block's callout, inside a `<div data-block-id data-block-type="directive">` (wrapped `annotation-exclude` under `textComments`, as `renderQuestionCard`); `undefined` keeps the callout. Question blocks never reach it (they keep `renderQuestionCard`). Absent: every directive draws as today. Adds no classes to the shared stylesheet.
|
|
2472
|
+
- **Core `question-block`** gains **`findDirectiveBlocks(markdown, { frontmatter? })`** → `{ directiveKind, startLine, endLine, inner, text }[]`: every top-level directive of any kind, found exactly as `findQuestionBlocks` finds question blocks (which now runs on it: its output is unchanged, tested), and **`blockKeyHash8(value)`**, the FNV-1a hash question keys use, for a host's own block keys. Question grammar is unchanged: `:::action` is not a question kind.
|
|
2473
|
+
- **New core subpath `./inbox-actions`** (`packages/core/inbox-actions.ts`): the Inbox-only `:::action` reader (`parseInboxActionBlocks`, `parseInboxActionBlock`, `parseInboxActionWhere`, `inboxActionKey`, `INBOX_ACTION_KEY_RE`), the person's result check (`checkInboxActionResult`, `INBOX_ACTION_NOTE_MAX`), the reply section (`composeInboxActionsReply`, `INBOX_ACTIONS_HEADING`, `inboxActionTime`, `inboxActionResultLine`), `parseInboxDoneRef`, and the agent-facing texts (`INBOX_ACTION_GUIDE`, `INBOX_ACTION_EXAMPLE`, `INBOX_DONE_DESCRIPTION`). Pure, browser-safe.
|
|
2474
|
+
- **Widened kinds in `inbox-types`**: `InboxQuestionRecord.kind` and `InboxQuestion.kind` are now **`InboxQuestionKind`** (`QuestionKind | "action"`); read a stored kind with **`readInboxQuestionKind(value)`** (`"unknown"` for a kind this build does not know: never answerable) or **`isInboxAction`**. Additive fields: `InboxActionFacts` / `InboxActionResult` on records and the wire (`action`, `action_result`), `InboxQuestionSummary.actions`, `InboxMessage.done` / `action_report`, `InboxMessageWire.actions` (window-side), `isInboxActionReport`. Code that passes a record's `kind` to `QuestionAnswer` must narrow it first (an action item has no `QuestionAnswer`). `inbox-questions`' `inboxQuestionState` reads an action item's `action_result`; `inboxSkipAnswer` takes a `QuestionKind`; `composeInboxReplyBody` leaves action items out; the set-aside and reopened notes list them as `- A1.`
|
|
2475
|
+
|
|
2476
|
+
## Tooltips: a borderless float at 150 ms (published in ui 0.54.0; visual change, no core change)
|
|
2477
|
+
|
|
2478
|
+
The owner's design-system ruling q4 (2026-10-22): the tooltip takes Soft's borderless float at 150 ms in every product. A host that renders ui's `Tooltip` sees two changes without passing anything:
|
|
2479
|
+
|
|
2480
|
+
- **No visible border.** The popup keeps a 1 px border, now `border-transparent` (so forced colors, Windows High Contrast, still paint its edge in the system colour), and its `shadow-md` is now **`--float-shadow`**, a new `theme.css` token: a faint 1 px ring inside the shadow plus a soft drop, defined in dark (`:root`, its ink the background taken toward black in its own hue where relative colours are supported, else the background) and in light (`.light`, ink the foreground). It is the formula of the Inbox's Soft float shadow. Override `--float-shadow` (and `--float-shadow-ink`) to restyle it; a host that wants the old outline back styles `[data-pn-tooltip]` (`border-color: var(--border)`).
|
|
2481
|
+
- **150 ms to open, 400 ms to the next.** `TooltipProvider` without props now uses **`TOOLTIP_DELAY_MS`** (150) and **`TOOLTIP_SKIP_DELAY_MS`** (400), both exported from `components/Tooltip`; a `Tooltip` rendered outside any `TooltipProvider` opens after 150 ms too (Base UI's own default was 600). A provider's `delayDuration` / `skipDelayDuration`, or a trigger's `delayDuration`, still win, so a host that sets its own timing keeps it. `HostTitle` no longer passes its own 150 ms: it follows the provider (or the 150 ms default).
|
|
2482
|
+
|
|
2483
|
+
Plannotator's apps dropped their own timings (plan review and annotate 900 / 200, code review and the guides.show viewer 200 / 100, and the review app's per-trigger 200 to 300 ms) and use the defaults. The Inbox already drew its tooltips this way; its `[data-pn-tooltip]` rule now sets only the shadow (its Soft float, falling back to `--float-shadow`), and its sidebar edge tooltip (`.ib-edge-tip`) uses `--float-shadow` instead of a hard-coded shadow.
|
|
2484
|
+
|
|
2485
|
+
## Document text size (#2066; published in ui 0.54.0; additive, no core change)
|
|
2486
|
+
|
|
2487
|
+
Settings → Display can show a **Text size** row (13 / 15 / 17 / 19 / 21 px of body text, 15 = today). Everything is inert until a host opts in; a host that does nothing sees no change.
|
|
2488
|
+
|
|
2489
|
+
- **`utils/documentTextSize`** (new, import-free): `DOCUMENT_TEXT_SIZE_OPTIONS`, `DEFAULT_DOCUMENT_TEXT_SIZE` (15), `isDocumentTextSize`, `documentTextScale`, and **`applyDocumentTextSize(root, size)`**, which sets `data-doc-text-size` and `--doc-text-scale` (size / 15) on `root`, and removes both at the default.
|
|
2490
|
+
- **`hooks/useDocumentTextSize()`** (new): applies the setting to the document element while mounted. Plannotator's plan / annotate app calls it; a host that wants the row to work calls it too.
|
|
2491
|
+
- **Setting `documentTextSize`** (`config/settings.ts`, cookie-only `plannotator-document-text-size`, default 15).
|
|
2492
|
+
- **`theme.css`**: rules gated on `[data-doc-text-size]` scale the blocks (`[data-block-id]`, `[data-diff-block-index]`, `[data-doc-text]`) inside a document container (`[data-doc-text-root]`: `Viewer`'s article and `PlanDiffViewer`'s), leaving out diagrams and the table / diagram popouts (`[data-popout]`): Tailwind's `--text-*` scale, the size a block root passes down (in the base layer, so a block's own size class wins), the renderers' fixed 12.5–15 px sizes, `pre code.pn-code`, the question context, and the controls beside the text (`ListMarker`'s `[data-list-numeral]` / `[data-list-check]`, `QuestionBlock`'s `[data-question-control]`). Without the attribute nothing applies. `Viewer`'s frontmatter card and `PlanRawDiffView` carry `data-doc-text`.
|
|
2493
|
+
- **`Settings`** gains **`showDocumentTextSize?: boolean`** (default false): the row shows only when a host passes true, since it does nothing unless the page applies the setting.
|
|
2494
|
+
|
|
2495
|
+
## Back from a linked document keeps the reader's place (#2082; published in ui 0.54.0; additive, no core change)
|
|
2496
|
+
|
|
2497
|
+
- **`useLinkedDoc` option `scrollViewport?: HTMLElement | null`** (the element the documents scroll in, the host's `ScrollViewportContext` value). With it, opening a linked document from the root remembers where the reader was, and `back()` returns them there once the root has rendered. Without it nothing about scrolling changes.
|
|
2498
|
+
- **`utils/scrollAnchor`** (new): `captureScrollAnchor(viewport)`, `restoreScrollAnchor(viewport, anchor)`, `scrollAnchorReady(viewport, anchor)`, `ScrollAnchor`. The anchor is the first `[data-block-id]` visible at the viewport's top plus its offset (the raw scrollTop is the fallback), so a reflowed column or a late image does not move the reader. `back()` re-anchors on the next frames until the block is in the DOM, then at about 150, 400 and 1000 ms. It ends early on the reader's own input (wheel, touchstart, keydown, pointerdown), on any scroll that is not its own, on the next navigation and on unmount. A raw-HTML root scrolls inside its own frame and is not restored.
|
|
2499
|
+
- **`back(options?: { restoreScroll?: boolean })`** (was `back()`): `restoreScroll: false` skips the restore, for a caller that scrolls to a target itself; only an explicit false turns it off, so `back` handed to an onClick is unchanged. **`cancelScrollRestore()`** (new on the return): stops a restore still settling. Call it before a programmatic scroll (a jump to a comment, a Contents click, a reveal); assigning scrollTop during a smooth `scrollIntoView` cancels it in Chromium, so a live restore would otherwise undo the jump.
|
|
2500
|
+
- **`open()` / `openLoaded()` option `restoreScroll?: boolean`** (#2128, additive): leaving a linked document for another one now also remembers where the reader was in it (per path), and reopening it with `restoreScroll: true` puts them back there with the same restore `back()` runs. For a host whose Back reopens a document that is not the root (Plannotator's review of several files reopens the bundle file a link was followed from). Without the flag nothing about scrolling changes.
|
|
2501
|
+
|
|
2502
|
+
## Image viewer: "Use ⌘ + scroll to zoom" (#2127; published in ui 0.54.0; visual change, additive prop, no core change)
|
|
2503
|
+
|
|
2504
|
+
The first time a plain wheel scrolls an image in **`ImageZoomStage`** (the lightbox, and any host stage), a pill in the middle of the stage says "Use ⌘ + scroll to zoom" ("Ctrl" off macOS), held while the wheel keeps scrolling and 2 s after (`WHEEL_HINT_HOLD_MS`), then faded (no fade under reduced motion). It is `aria-hidden` with `pointer-events: none`; a polite live region (`[data-wheel-hint-live]`, always in the stage) says "Scrolling moves the image. To zoom, hold Command (Control) and scroll." once, then empties. Never for a fitted image, Ctrl/⌘ + wheel, a pinch, or on a touch-only device. What a host notices:
|
|
2505
|
+
|
|
2506
|
+
- **Without a prop, once per stage mount** (so once per image, since hosts key the stage by image). To show it once per viewer instead, hold a **`WheelHintGate`** (`{ shown: boolean }`, exported from `components/ImageZoomStage`) for the viewer's life and pass it to every stage as **`wheelHint`**; `wheelHint={false}` turns it off. `ImageLightbox` does this itself (once per open).
|
|
2507
|
+
- **`utils/platform`** adds `modKeySpoken` (`'Command'` / `'Control'`), the modifier as a screen reader says it in a sentence.
|
|
2508
|
+
|
|
2509
|
+
## Question cards: a recommendation names the choice it begins; an unmatched recommendation is an answer row; a `Context:` line is context (published in ui 0.54.0 and core 0.25.14; plan review, annotate and the Inbox change too)
|
|
2510
|
+
|
|
2511
|
+
From an Inbox card the owner saw: `Recommended: Unpin it` over `- Unpin it: the toast says …` drew a "Suggested · Unpin it" line with a **Use** button beside **Accept recommended** (two buttons doing the same thing), and a `Context:` line after the last bullet was read as that bullet's wrapped text.
|
|
2512
|
+
|
|
2513
|
+
- **core `parseQuestionBlock`: a recommendation that names the start of a choice.** When `Recommended:` matches no label exactly (or by any of the existing rules), it now names the ONE choice whose label (or full task-list text) begins with it followed by `:`, `—`, `–`, ` - ` or `(`, compared case-insensitively after the usual normalization (emphasis, whitespace, a trailing period). Zero or several such choices: unchanged, a suggested answer. Also per part of a pick-any list. Labels, descriptions and keys do not change (no "X: why" split of plain bullets: a stored answer quotes the label, and a stored Inbox record keeps the label it was parsed with, so splitting would make a new card's pick an unknown label to an older record).
|
|
2514
|
+
- **core `parseQuestionBlock`: a `Context:` line** (any case, `**Context:**` too) after the prompt is context, never the continuation of the plain bullet, task-list choice or recommendation above it; the lines that wrap it stay context with it. It is kept as written (the `Context:` word included) and shows in the card's context. A `Context:` FIRST line is still the prompt, so no key moves; other `Word:` lines are unchanged. A plain bullet it used to swallow gains the old swallowed label as an alias, so an answer stored under it still selects the bullet (`canonicalQuestionAnswer`).
|
|
2515
|
+
- **ui `QuestionBlock`: one concept, an answer row.** No "Suggested" line on a pick card any more (the owner: "What is the difference between suggested/recommended?", and "It should be inline as an answer, not an isolated thing"). A choice question whose recommendation names no choice draws it as one more row of the choice list, right after the agent's choices and before "Other…": the same radio (pick one) or checkbox (pick any), in the same group, the same row classes and `data-question-option`, the same **Recommended** tag a matched choice carries, so its accessible name is "<text> Recommended". The row carries `data-question-suggested`. Picking it stores exactly what **Accept recommended** stores, the text as the `other` answer (`{ selected: [], other }` on pick one; on pick any the ticks stay and `other` is set or cleared), so `question.choices`, their labels and order, keys and a host's check of picked labels (the Inbox's `checkInboxAnswer`) never see it, and the row never 422s. While the row holds the answer the "Other…" field shows empty (no second copy); typing there replaces it. The card knows the row holds it only when the row (its pick, or Accept recommended) set it while the card was open, or, for an answer it did not set (stored, from the host), when the Other answer is exactly the agent's text and the field does not have focus; typing in the field never becomes the row, so `Recommended: Yes` with "Yes, but slower" typed one key at a time stays in the field. On pick any, ticking the row while the field holds the reviewer's own text keeps that text, and unticking the row puts it back (in the card's memory only, for as long as it is open; a reload keeps the row's answer). The row stays after the card is answered (it is a choice), shows on read-only cards, checked when the answer was it, and the read-only Other row is then not drawn. Accept recommended moves focus to the row's control. A text question keeps its line above the field, now tagged **Recommended** instead of "Suggested" (still `data-question-suggested`, still in `aria-describedby`). The footer's Accept recommended is the one button (the line's **Use** button is gone). Settled cards are unchanged: no Accept on a settled card, and the row is pickable like any choice.
|
|
2516
|
+
- **core: "(your recommendation)" for older answers.** `isRecommendedQuestionAnswer` takes an optional fourth argument, `RecommendedAnswerContext` (`recommendation`, `choiceLabels`, and `choiceAliases`, parallel to the labels and read with them), and `QuestionExportItem` the same three optional fields (`questionExportItems` and the Inbox fill them). With them, an answer that put the recommendation's own words in Other (a card through 0.28.26 read `Recommended: Unpin it` over `- Unpin it: …` as a suggestion and Accept filled Other) still exports as the recommendation, and so does a pick of the one choice an Inbox record's `suggested_text` begins (`recommendationHeadChoice`, exported: a record stored then has no recommended choice). Without the argument the function is unchanged.
|
|
2517
|
+
- **core `QUESTION_AUTHORING_GUIDE`** (agent-facing; the `plannotator` skill and the Inbox's `send_message` description carry it): the Recommended bullet now teaches several recommendations on a pick-any question and says text naming no choice "shows as one more answer, tagged Recommended" (it said "a suggested answer"). Two phrases were shortened ("no jargon or internal names.", "(a plan, a mock)") so the `- [x]` rule still fits Claude Code's 2,048-character cut of `send_message`'s description. The `plannotator_inbox` tool entry does not carry the guide, so its size (`INBOX_TOOL_COST`) is unchanged.
|
|
2518
|
+
- **core: pick any.** `Recommended: Search and replace` names a `Search and replace: both` choice whole before the text is split into `Search` and `replace`.
|
|
2519
|
+
- **core: a `Context:` line inside a code fence** in a choice's continuation lines is the code's, not context.
|
|
2520
|
+
- **core (Inbox): older records.** A stored plain-bullet label with a `Context:` line joined on (`Keep it pinned forever Context: …`, recorded by an Inbox through 0.28.26) also answers to the part before it (each ` Context:` in the label is tried, since the bullet's own words may hold the word), unless another choice has that name or another choice yields the same part, so two choices never share a name, so the card's pick of `Keep it pinned forever` is taken instead of `422 … is not one of the question's choices`.
|
|
2521
|
+
|
|
2522
|
+
## Question cards: contract lines and picture choices (published in ui 0.54.0 and core 0.25.14; additive; plan review, annotate and the Inbox show them)
|
|
2523
|
+
|
|
2524
|
+
Approval "inbox-decision-mode", agent contract lines (2026-10-24). All optional and additive to `:::question` blocks; a block without them parses and renders exactly as before.
|
|
2525
|
+
|
|
2526
|
+
- **core `ParsedQuestion`** gains optional `stakes`, `shows` (`QuestionShows { kind: 'image' | 'link', label, src }`), `why` and `reversible` (`QuestionIrreversible { value: false, why? }`; absent means reversible). They come from `Stakes:`, `Shows: ` / `Shows: [name](file)`, `Why:` and `Reversible: no — <why>` lines after the prompt at the block's own indent (an indented one under a choice is that choice's text), read with `Context:`'s leniency, never the prompt, never context, never changing the key. A value that does not fit stays prose. **New parse option `contractLines`** (default true, the `decisionLines` precedent): false keeps the lines as context and never sets the fields; prompt, key and choices are unchanged.
|
|
2527
|
+
- **ui `Viewer` `questionContractLines`** (default **false**): the card draws the lines only when a host passes true (the Plannotator Inbox does). Plan review and annotate leave it off, so they show the lines as plain text, as approved. `BlockRenderer` gains `questionContractLines` (default false, for a question it parses alone) and `questionPictureChoices`; `Viewer` gains `questionPictureChoices` (default true) and passes both on.
|
|
2528
|
+
- **core `QuestionChoice.image`** (`QuestionChoiceImage { alt, src }`): a choice's first markdown image (`- [ ] Label —  rest`, a plain bullet the same way, or an image inside the label). The label stays plain (an image in the label is taken out and the raw label kept in `aliases`, so `canonicalQuestionAnswer` maps an answer stored against it); `alt` falls back to the label; `src` is exactly as written. **A behaviour change for documents that already put an image in a choice**: their label no longer carries the markdown, so the export prints the plain label. Also exported: `questionLabelWithoutImage(label)`.
|
|
2529
|
+
- **ui `QuestionBlock`** draws Stakes under the prompt (`data-question-part="stakes"`, part of the card's `aria-describedby`), Shows as a chip (an image chip calls `onImageClick` with the resolved URL; a file is an ordinary inline link, so a host's `LocalLinkContext` applies), Why under the choices ("Why <recommended>: …"), and Reversible: no as a "Can't be undone" tag (tone `warn`, `bg-warning/15 text-warning`). No confirm step.
|
|
2530
|
+
- **Picture tiles.** With any picture choice, the choices are a grid of tiles (`[data-question-tiles]`, `[data-question-tile]`): a native `<label>` holds the picture (`img[data-question-picture][data-zoomable]`) and a real radio or checkbox, so the picture and the label pick and the keyboard works; a separate zoom button (`[data-question-zoom]`, outside the label) calls `onImageClick`. The picture's URL is resolved exactly as an inline markdown image (the `ImageResolverContext` resolver, else `getImageSrc(src, imageBaseDir)`). The extra Recommended row and Other stay text rows. New prop **`pictureChoices?: boolean`** (default true; `Viewer` / `BlockRenderer` `questionPictureChoices`): false draws text rows. The card also draws text rows when the resolver answers `unavailable` for every picture or every picture fails to load. A picture the resolver answers `unavailable` for is drawn as the same placeholder an inline image gets (`[data-image-unavailable]` carrying the resolver's `title`), in its tile or after its row's label, so a host that counts or loads held images by that placeholder sees these too. Also exported: `parseQuestionChoiceText(raw)` (a task-list choice's text read as this parser reads it). A host that wires its own click handling on document images should skip `img[data-question-picture]` (the Plannotator Inbox does), or one click picks and zooms.
|
|
2531
|
+
- **`pictureBadge?: (choice) => string | null`** (`QuestionBlock`; `Viewer` / `BlockRenderer` `questionPictureBadge`): a small label over a picture tile's picture, top left (`[data-question-picture-badge]`). Drawn by `QuestionPictureTiles` (its `pictureBadge` prop, which the card passes through), so a host's own decision surface can label its tiles the same way; styled inline, so it adds no CSS class. Display only: never the answer, never parsed. Default absent, so every card draws as before. The Plannotator Inbox's onboarding tour labels its pictures ("Mockup · VS Code", "Screenshot · Inbox").
|
|
2532
|
+
- **`collectDocumentImages`** (the lightbox's stepping) also counts `img[data-zoomable]`.
|
|
2533
|
+
- New Tailwind classes in the card change the shared stylesheet (the guides.show viewer's pinned CSS was re-synced).
|
|
2534
|
+
|
|
2535
|
+
## `QuestionPictureTiles` (published in ui 0.54.0; additive, the card unchanged)
|
|
2536
|
+
|
|
2537
|
+
- **`QuestionPictureTiles`** (exported from `components/blocks/QuestionBlock`): the question card's picture-choice tile grid as a component of its own, so a host that draws its own decision surface draws the same tile (the Plannotator Inbox's decision mode does). Props: `choices` (`QuestionPictureTileChoice`: `label`, `description?`, `recommended?`, `image?`), `urls` (each choice's resolved picture URL, null for none), `selected`, `inputType` (`radio` / `checkbox`), `name`, `readOnly?`, `onPick(label, checked)` (the tile's input changed: the picture and the label pick through it), `onZoom?(url, alt)` (the separate zoom button; absent, no button; it never picks), `onPictureError?`, `heldTitle?`, `renderText(text)`, and host seams `labelProps?(label)`, `inputProps?(label)` (the card passes its drag guard; decision mode answers on a click or Enter) `renderBadges?(choice, index)` (default: the Recommended tag) and `pictureBadge?(choice)` (a label over the picture, see above). New hooks for host CSS: `[data-question-tile-recommended]`, `[data-question-tile-picture]`, `[data-question-tile-caption]`, `[data-question-tile-label]`, `[data-question-tile-description]`. The card draws exactly what it drew before (same classes and attributes, plus those hooks).
|
|
2538
|
+
|
|
2539
|
+
## Hover cards outside the Inbox: `onlyWithData`, `RefHoverCards`, `refCardRouteTransport` (published in ui 0.54.0; additive, no core change)
|
|
2540
|
+
|
|
2541
|
+
- **`useRefHoverCards(rootRef, { onlyWithData })`** (default false, so the Inbox's card is unchanged): true opens a card only once the transport answered `ok`, with no loading skeleton and no failure card; a hover or focus that leaves first shows nothing, and a tap on a touch screen follows the link (no sheet). Plannotator's plan, annotate and review apps pass it, so a person without gh / glab sees links exactly as before.
|
|
2542
|
+
- **`RefHoverCards`** (`components/RefHoverCard`): the hook as a component (`rootRef`, `delayMs?`, `onlyWithData?`), for a host whose root element is not mounted at its first render (behind a loading state): rendered inside that root, its listeners attach once the element exists.
|
|
2543
|
+
- **`refCardRouteTransport(route)`** (`utils/refCard`): a `RefCardTransport` over a server route answering like Plannotator's `GET /api/ref` (`?url=` → `ForgeRefCardResult`); a `404` turns it off for the life of the page.
|
|
2544
|
+
|
|
2545
|
+
## Rich-text Copy (#1972, #2191; published in ui 0.54.0; additive, off unless a host opts in)
|
|
2546
|
+
|
|
2547
|
+
- **`configurePlannotatorUI({ richCopy })`** (default false; `utils/richCopyPolicy`): with it on, the selection toolbar's one Copy button (`Viewer`, `HtmlViewer`; the same copy icon and "Copy" tooltip) copies a text selection as rich text, `text/html` plus `text/plain` in one clipboard write, so a plain field still pastes plain text; Copy then shows also where `hideCopyButton` hid it (the Viewer on desktop). There is no second button. Off, Copy is exactly as before: plain text, nothing built. Plannotator's plan review, annotate, code review and Inbox window turn it on. No keyboard shortcut; Ctrl+C / Cmd+C and Vim `y` are unchanged.
|
|
2548
|
+
- **`AnnotationToolbar` `getRichCopy`** (new, optional): a builder called only when Copy is pressed, returning `RichCopySource` (`{ html, text, truncated? }`, `{ tooLarge, text, truncated? }` or null), synchronously or as a promise. `ToolbarState.getRichCopy` carries it from `useAnnotationHighlighter` (a range over the pending highlight's marks) and from `useHtmlAnnotation` (a bridge round trip). After a rich copy the tooltip reads "Copied!"; a plain-text fallback says what went on the clipboard (`richCopyOutcomeLabel`).
|
|
2549
|
+
- **`utils/richClipboard`** (new): `copyRichSelection`, `copyRichTextToClipboard`, `selectionRangeToHtml`, `richCopySourceFromRange`, `sanitizeRichHtml`, `plainTextClipboardHtml`, `isLoopbackHostname`, the caps `MAX_RICH_CLIPBOARD_HTML_LENGTH` (256 KiB, the bridge) and `MAX_RICH_CLIPBOARD_OUTPUT_LENGTH` (512 KiB, the clipboard), and `utils/richClipboardLists`. The copied HTML is rebuilt from an allowlist: structural and inline tags, `href` (http, https, mailto, tel; never one on the page's origin or a loopback host), list `start` / `value`, table spans and scope, `dir`; no `title`, script, style, handler, class or `data-*`.
|
|
2550
|
+
- **Bridge** (additive, no `BRIDGE_PROTOCOL_VERSION` bump): the text-selection message gains `richCopy: true` and `textCut`; the parent posts `rich-copy-request` `{ id }` and the bridge answers `rich-copy` `{ id, html, text, truncated, tooLarge }`. An older bridge asset sends no `richCopy`, so its Copy stays plain text.
|
|
2551
|
+
- **`BlockRenderer`** list items carry `data-rich-list-level`, `data-rich-list-kind`, `data-rich-list-index` and `data-rich-list-checked` (read by the list rebuild).
|
|
2552
|
+
|
|
2553
|
+
## The Inbox thread's rules in core: `./inbox-folding`, `./inbox-delivery-line`, `./inbox-open-questions`, `./inbox-format` (published in core 0.25.14; additive, no ui change)
|
|
2554
|
+
|
|
2555
|
+
Slice 1 of the Inbox thread extract (plan record, not in this repo). These pure modules moved from `packages/inbox` into core **unchanged**, so a host of the shared thread parts can use the Inbox's own rules. Only their import specifiers changed: `@plannotator/core/x` became `./x`, and `./format` became `./inbox-format`. Each is browser-safe and imports only core. The old paths (`packages/inbox/folding.ts`, `deliveryLine.ts`, `openQuestions.ts`, `format.ts`) are one-line `export *` shims, so every Inbox import and test runs unedited. No ui export changes yet. **Experimental** until Workspaces ships on them: the names and shapes follow the Inbox and may still change with it.
|
|
2556
|
+
|
|
2557
|
+
- **`./inbox-folding`**: what folds when a thread opens and what a folded line says (`lookBoundary`, `planFolds`, `foldSnippet`, `holdingsOf` …).
|
|
2558
|
+
- **`./inbox-delivery-line`**: the words under the person's last reply (`deliveryLine`, `deliveryText`, `restartNote`, `RecipientConnection` …). It is not named `inbox-delivery` because `inbox-delivery.ts` already exists.
|
|
2559
|
+
- **`./inbox-open-questions`**: the open questions list's rules (`threadOpenQuestions`, `openQuestionsLabel`, `skipSelection` …). It needs `./inbox-format`'s `formatCount`.
|
|
2560
|
+
- **`./inbox-format`**: display helpers with fixed `en-US` `Intl` formats (`shortTime`, `whenTime`, `fullTime`, `formatCount`, `agentName` …).
|
|
2561
|
+
|
|
2562
|
+
Publish core first, then ui, as always. These subpaths need no ui bump until ui imports them.
|
|
2563
|
+
|
|
2564
|
+
## Document font (#1657; published in ui 0.54.0; additive, no core change)
|
|
2565
|
+
|
|
2566
|
+
Settings → Display can show a **Document font** row beside Text size. Inert until a host opts in, like Text size.
|
|
2567
|
+
|
|
2568
|
+
- **`utils/documentFont`** (new): `DOCUMENT_FONT_OPTIONS` (Theme Default, Serif, Sans Serif, Monospace), `documentFontStack(value)`, and **`applyDocumentFont(root, value)`**, which sets `data-doc-font` and `--doc-font-family` on `root`, and removes both at the default.
|
|
2569
|
+
- **`hooks/useDocumentFont()`** (new): applies the setting to the document element while mounted.
|
|
2570
|
+
- **Setting `documentFontFamily`** (`config/settings.ts`, cookie-only `plannotator-document-font-family`, default `''` = the theme's font).
|
|
2571
|
+
- **`theme.css`**: one base-layer rule gated on `[data-doc-font]` sets the font on the blocks Text size scales; code keeps its monospace font, and figure captions keep the sans font. `PlanCleanDiffView`'s unchanged chunks now carry `data-doc-text`, so both Text size and the font reach them.
|
|
2572
|
+
- **`Settings`** gains **`showDocumentFont?: boolean`** (default false). The Code Font row and the new row share one `FontFamilyField`; Code Font behaves as before (its select now has an `aria-label`).
|
|
2573
|
+
|
|
2574
|
+
## AsciiDoc documents (#1009; published in ui 0.54.0 and core 0.25.14; additive, one widened setter)
|
|
2575
|
+
|
|
2576
|
+
`@plannotator/core/annotatable` gains `DocumentRenderKind` (`DiagramRenderKind | "asciidoc"`), `documentRenderKindForPath`, `isDocumentRenderKind` and `annotateDocumentRenderKind`; `.adoc`/`.asciidoc` join the annotatable sets. `@plannotator/ui` gains `utils/asciidocParser` (`parseAsciidocToBlocks`, the same Block model as the markdown parser), `DocumentRenderAs` widens to include `'asciidoc'`, and share payloads may carry `r: 'asciidoc'`. `useSharing` takes an optional trailing `renderAs` and its `setRenderAs` parameter widens to `DocumentRenderAs`, so a host whose state is typed `'markdown' | 'html'` must widen its setter. Publish core first.
|
|
2577
|
+
|
|
2373
2578
|
## The law (guardrails for anyone editing `@plannotator/ui`)
|
|
2374
2579
|
|
|
2375
2580
|
These are enforced socially and, where possible, by CI. They exist because a prior from-scratch reimplementation of this UI broke the app and was reverted.
|
|
@@ -2380,3 +2585,58 @@ These are enforced socially and, where possible, by CI. They exist because a pri
|
|
|
2380
2585
|
4. **Never delete working Plannotator code until a human confirms parity in the browser.**
|
|
2381
2586
|
|
|
2382
2587
|
See `packages/ui/README.md` and `packages/ui/AGENTS.md` (CLAUDE.md symlink) for the short version that lives next to the code.
|
|
2588
|
+
|
|
2589
|
+
## `CodeFileView` `extraCss` and `CodeAnnotation.outsideDiff` (published in ui 0.54.0; additive, no core change)
|
|
2590
|
+
|
|
2591
|
+
**`CodeFileView`** (`components/CodeFilePopout`) gains **`extraCss?: string`**, appended to the theme CSS inside the file's shadow root (Pierre's `unsafeCSS`). Plannotator's code review draws the Whole file view's change bars with it (rules on `[data-gutter] [data-column-number="N"]`). Absent: today's view, byte for byte.
|
|
2592
|
+
|
|
2593
|
+
**`CodeAnnotation.outsideDiff?: boolean`** marks a code-review comment on lines outside the diff (a file the review does not change, or a changed file's lines outside its hunks). `CodeFileView`'s inline comment card shows a "not in the diff" chip for it. Annotations without it render exactly as before.
|
|
2594
|
+
|
|
2595
|
+
## `CodeFileView` `revealLine`, `onTokenTarget`, `onTokenNavigate` (published in ui 0.54.0; additive, no core change)
|
|
2596
|
+
|
|
2597
|
+
**`CodeFileView`** (`components/CodeFilePopout`) gains three optional props for a host's code navigation (Plannotator's code review: Go to Definition, references and Back/Forward land in a file at a line):
|
|
2598
|
+
|
|
2599
|
+
- **`revealLine?: { line: number; token: number }`** scrolls the line to the middle and marks it (Pierre's selected line) once per `token`, until a comment or another file takes the mark.
|
|
2600
|
+
- **`onTokenTarget?: (kind: 'enter' | 'leave' | 'click', target: CodeFileTokenTarget) => void`** reports the token under the pointer and the last one clicked. Passing it (or `onTokenNavigate`) turns on Pierre's token transformer for the file.
|
|
2601
|
+
- **`onTokenNavigate?: (target: CodeFileTokenTarget) => void`** is a Cmd/Ctrl-click (or Alt-click) on a token.
|
|
2602
|
+
|
|
2603
|
+
`CodeFileTokenTarget` is `{ tokenText, tokenElement, lineNumber, lineCharStart }`. Absent: today's view, byte for byte (`CodeFilePopout` and the Inbox pass none).
|
|
2604
|
+
|
|
2605
|
+
## `HtmlViewer` `sandbox` (published in ui 0.55.0; additive, no core change)
|
|
2606
|
+
|
|
2607
|
+
**`HtmlViewer`** (`components/html-viewer`) gains **`sandbox?: string`**, the srcdoc frame's whole `sandbox` attribute. Added for Workspaces: #2063 changed the default from `allow-scripts` to `allow-scripts allow-modals` with no way to opt out, and in Workspaces a `prompt()` or `confirm()` from a shared, user-written document would draw a dialog over its origin.
|
|
2608
|
+
|
|
2609
|
+
- **Absent:** `HTML_VIEWER_SANDBOX`, `"allow-scripts allow-modals"`, as before. Plannotator passes none, so the Inbox, annotate and review frames are unchanged.
|
|
2610
|
+
- **Set:** the value is the attribute as given. Workspaces passes `sandbox="allow-scripts"` and can drop its pnpm patch.
|
|
2611
|
+
- **Not a string** (`null` from an unset config field, for example): the default, never a frame without a sandbox. React drops a `null` attribute, and a destructuring default does not catch `null`, so `HtmlViewer` checks the type itself.
|
|
2612
|
+
- **`""`** (or only spaces): kept as given. That is the strictest frame, with no scripts, so the annotation bridge never starts and the page cannot be annotated. It never widens to the default. With `hostNavigates` it becomes `allow-popups` alone.
|
|
2613
|
+
- **A changed value** applies when the frame next loads a document (a new `rawHtml`, for example): browsers read `sandbox` at navigation. To apply it to the document already shown, remount the viewer (for example, key it on the value).
|
|
2614
|
+
- **With `hostNavigates`:** `allow-popups` is still added when the value lacks it, because that mode opens links with `window.open`.
|
|
2615
|
+
- **Never add `allow-same-origin`** beside `allow-scripts`: the page could then remove its own sandbox and reach the host's origin.
|
|
2616
|
+
- Live-app mode (`src`) has no sandbox and ignores the prop. `HtmlViewer` draws one frame, so the prop covers every frame of the document.
|
|
2617
|
+
- Pinned by `components/html-viewer/HtmlViewer.sandbox.test.tsx`: no prop gives `allow-scripts allow-modals`, `sandbox="allow-scripts"` gives exactly `allow-scripts`, `hostNavigates` adds `allow-popups` to either, `null` gives the default (with and without `hostNavigates`), `""` gives an empty attribute, and `src` mode has no sandbox.
|
|
2618
|
+
|
|
2619
|
+
**Note for hosts that pin CodeMirror (from ui 0.54.0).** ui 0.54.0 raised its CodeMirror and Lezer minimums. A host that pins these packages lower must raise its pins to at least these, or its build ships two copies (two `@codemirror/state` copies break editor extensions at runtime):
|
|
2620
|
+
|
|
2621
|
+
| Package | ui 0.54.0 range | ui 0.53.1 range |
|
|
2622
|
+
|---|---|---|
|
|
2623
|
+
| `@codemirror/commands` | `^6.11.1` | `^6.11.0` |
|
|
2624
|
+
| `@codemirror/language` | `^6.13.1` | `^6.12.4` |
|
|
2625
|
+
| `@codemirror/legacy-modes` | `^6.5.5` | `^6.5.3` |
|
|
2626
|
+
| `@codemirror/state` | `^6.7.6` | `^6.7.2` |
|
|
2627
|
+
| `@codemirror/view` | `^6.43.14` | `^6.43.10` |
|
|
2628
|
+
| `@lezer/common` | `^1.5.3` | `^1.5.2` |
|
|
2629
|
+
| `@lezer/highlight` | `^1.2.5` | `^1.2.3` |
|
|
2630
|
+
|
|
2631
|
+
These did not change: `@codemirror/autocomplete` `^6.20.3`, `lang-javascript` `^6.2.5`, `lang-json` `^6.0.2`, `lang-markdown` `^6.5.2`, `lang-python` `^6.2.1`, `lang-yaml` `^6.1.3`, `merge` `^6.12.2`, `search` `^6.7.2`.
|
|
2632
|
+
|
|
2633
|
+
## KaTeX 0.19 (published in ui 0.55.0; dependency bump, no API change, no core change)
|
|
2634
|
+
|
|
2635
|
+
**`katex` is now `^0.19.0`** (was `^0.18.11`). npm marks 0.18.11 deprecated ("Accidentally published with breaking changes. Use 0.19.0 instead."), and `^0.18.11` resolves only to 0.18.x.
|
|
2636
|
+
|
|
2637
|
+
- **Rendering is unchanged.** The 0.19.0 tarball matches 0.18.11 file for file, except for the version string (`katex.version` and the `.katex-version::after` content) and the READMEs. The fonts are identical. The breaking change named in the 0.19.0 notes shipped in 0.18.11: a missing character metric is reported through `strict` (code `symbolNotInFont`), and a `strict` callback that returns `undefined` or `null` now warns. ui calls KaTeX with `strict: 'warn'` and no callback, so ui is not affected. Plan review screenshots of a math fixture (inline, display, `aligned`, `\text`, `\def` / `\newcommand`, `\tag`, `cases`, and errors) show 0 changed pixels between 0.18.11 and 0.19.0, in dark and light.
|
|
2638
|
+
- **Hosts that serve their own KaTeX stylesheet should move it to 0.19** (CDN or self-hosted; see "Math rendering (KaTeX)" above). A 0.18.11 stylesheet still works, because its rules are the same as 0.19's. But 0.18.x is deprecated, and the stylesheet and the bundled JS should stay on one version.
|
|
2639
|
+
- **Mermaid 12.2.0 still declares `katex ^0.18.0`.** ^0.18.0 does not accept 0.19, so without action the install holds two KaTeX copies: 0.19 for ui and 0.18.11 (deprecated, with an npm warning) for Mermaid's `$$` labels. A bundle that imports Mermaid with no `katex` redirect then carries both copies. In Plannotator these are the plan page, the Inbox and the phone surface: without the override, each carried Mermaid's 0.18.11 too, about 262 KB more on the plan page. Code review's diagram engine is not affected, because `apps/review/diagram-engine/build.ts` (`mermaidKatexFromPage`) already sends Mermaid's `katex` import to the page's copy. Plannotator's root `package.json` has the override `"katex": "^0.19.0"`, so the install and those bundles hold one copy. **Remove that override when ui moves to a Mermaid that declares `katex ^0.19`** (package.json cannot hold a comment, so the note is here). A host has two ways to keep one copy:
|
|
2640
|
+
- Add the same override: `pnpm.overrides` for pnpm, `overrides` for npm, or `resolutions` for Yarn.
|
|
2641
|
+
- Point Mermaid's `katex` import at `utils/mermaid-math-slot` (see "Lazy renderers and eager entries", item 2).
|
|
2642
|
+
- **0.19.1** (2026-10-23) fixes a `%` inside a raw argument. `^0.19.0` accepts it. This repo's lockfile records 0.19.0, because `bunfig.toml`'s seven-day `minimumReleaseAge` holds 0.19.1 back until 2026-10-30.
|