@plannotator/ui 0.53.0 → 0.54.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/HANDOFF.md +284 -27
- package/README.md +84 -14
- package/components/AgentToolAnnouncementDialog.tsx +1 -1
- package/components/AgentsTab.tsx +1 -1
- package/components/AnnotationPanel.tsx +177 -34
- package/components/AnnotationSidebar.tsx +1 -1
- package/components/AnnotationToolbar.tsx +64 -9
- package/components/AnnotationToolstrip.tsx +45 -40
- package/components/ApproveDropdown.tsx +7 -9
- package/components/AskSessionAnnouncementDialog.tsx +1 -1
- package/components/AttachmentsButton.tsx +1 -1
- package/components/BlockRenderer.tsx +19 -5
- package/components/CodeFilePopout.tsx +140 -5
- package/components/CommentPopover.tsx +46 -8
- package/components/DecisionControl.tsx +31 -5
- package/components/DiagramBlock.tsx +17 -12
- package/components/EditorAnnotationCard.tsx +1 -1
- package/components/ExportModal.tsx +77 -7
- package/components/HostTitle.tsx +0 -4
- package/components/HtmlSurfaceControls.tsx +2 -1
- package/components/ImageAnnotator/Toolbar.tsx +25 -1
- package/components/ImageAnnotator/index.tsx +24 -4
- package/components/ImageLightbox.tsx +658 -120
- package/components/ImagePeek.tsx +38 -4
- package/components/ImageRegions.tsx +369 -0
- package/components/ImageThumbnail.tsx +1 -1
- package/components/ImageZoomStage.tsx +796 -0
- package/components/InboxAnnouncementDialog.tsx +202 -0
- package/components/InlineMarkdown.tsx +152 -24
- package/components/KeyboardShortcuts.tsx +23 -0
- package/components/LazyBlockBoundary.tsx +100 -0
- package/components/ListMarker.tsx +3 -3
- package/components/LookAndFeelAnnouncementDialog.tsx +12 -2
- package/components/MentionPicker.tsx +51 -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/SkillReferenceMenu.tsx +39 -12
- 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 +316 -47
- package/components/ai/DocumentAIChatPanel.tsx +29 -3
- package/components/ai/NewChatButton.tsx +74 -0
- package/components/blocks/CodeBlock.tsx +1 -1
- package/components/blocks/HtmlBlock.tsx +7 -1
- package/components/blocks/QuestionBlock.tsx +522 -62
- package/components/blocks/QuestionContext.tsx +3 -0
- 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 +69 -6
- package/components/html-viewer/bridge-script.asset.js +504 -12
- package/components/html-viewer/bridge-script.lite.ts +1 -1
- package/components/html-viewer/bridge-script.ts +545 -12
- package/components/html-viewer/useHtmlAnnotation.ts +125 -0
- package/components/imageZoom.ts +265 -0
- package/components/plan-diff/PlanCleanDiffView.tsx +1 -1
- package/components/plan-diff/PlanDiffViewer.tsx +13 -2
- package/components/plan-diff/PlanRawDiffView.tsx +1 -1
- package/components/sidebar/FileBrowser.tsx +16 -2
- package/components/sidebar/SidebarTabs.tsx +3 -0
- package/config/index.ts +3 -0
- package/config/reviewView.ts +41 -1
- package/config/settings.ts +155 -4
- package/configure.ts +28 -0
- package/hooks/pfm/useCodeFilePopout.ts +7 -1
- package/hooks/useAIChat.ts +72 -25
- package/hooks/useAnnotationDraft.ts +71 -6
- package/hooks/useAnnotationHighlighter.ts +57 -3
- package/hooks/useCodeAnnotationDraft.ts +113 -17
- package/hooks/useCookieHostSetting.ts +96 -0
- package/hooks/useDocumentFont.ts +17 -0
- package/hooks/useDocumentTextSize.ts +20 -0
- package/hooks/useFileBrowser.ts +10 -1
- package/hooks/useLinkedDoc.ts +226 -13
- package/hooks/useMentionAutocomplete.ts +1 -1
- package/hooks/usePackageChangesSetting.ts +90 -0
- package/hooks/usePlanLookDialog.ts +67 -0
- package/hooks/useSharing.ts +11 -7
- package/hooks/useSkillReferenceAutocomplete.ts +7 -3
- package/hooks/useThemePaletteHalves.ts +37 -0
- package/hooks/useUpdateCheck.ts +22 -4
- package/hooks/useUpdateToast.ts +1 -1
- package/hooks/useVimSelection.ts +9 -0
- package/package.json +23 -22
- package/shortcuts/code-review/annotationToolbar.shortcuts.ts +1 -0
- package/shortcuts/code-review/explorer.shortcuts.ts +82 -0
- package/shortcuts/core.ts +63 -4
- package/shortcuts/index.ts +1 -0
- package/shortcuts/plan-review/annotationPanel.shortcuts.ts +1 -1
- package/shortcuts/plan-review/commentPopover.shortcuts.ts +1 -0
- package/shortcuts/runtime.ts +14 -6
- package/styles.css +1 -1
- package/theme.css +179 -1
- package/themes/plannotator-graphite.css +66 -0
- package/types.ts +26 -9
- package/utils/agentSwitch.ts +41 -20
- package/utils/agentToolSetting.ts +1 -1
- package/utils/aiChatFormat.ts +23 -7
- package/utils/annotationOwnsHighlight.ts +4 -2
- package/utils/asciidocParser.ts +570 -0
- package/utils/autoUpdateNotice.ts +24 -0
- package/utils/belowTitleSlot.ts +174 -0
- package/utils/codeLineScroll.ts +100 -0
- package/utils/commentSubmitKey.ts +48 -0
- package/utils/cssFontFamily.ts +65 -0
- package/utils/decisionSpec.ts +190 -2
- package/utils/defaultNotesApp.ts +1 -1
- package/utils/diffFonts.ts +4 -1
- package/utils/documentFont.ts +68 -0
- package/utils/documentTextSize.ts +54 -0
- package/utils/fontDetect.ts +63 -0
- package/utils/inboxAnnouncement.ts +78 -0
- package/utils/lookAndFeelAnnouncement.ts +82 -5
- package/utils/markdownExtensions.ts +2 -2
- package/utils/mentions.ts +34 -5
- 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
|
@@ -96,6 +96,14 @@ Pass any subset of these to `configurePlannotatorUI({ ... })`. Anything omitted
|
|
|
96
96
|
| `identityGenerator` | `() => string` | The synchronous generator behind the default "tater" display name when no `identityProvider` is installed | A built-in 16 x 16 word pool of the same `adjective-noun-tater` shape; Plannotator registers the full dictionary via `utils/identity-tater` |
|
|
97
97
|
| `alertIconRenderer` | `(name: string) => ReactNode | null` | The icon rendered for a GitHub alert whose title line carries `<!-- icon: name -->` (0.38.0; grammar in `utils/alertTitle`). Called only for a title line with an icon comment and no leading emoji; a null return falls back to the type icon | `null` for every name: the type's own icon, the package bundles no icon set |
|
|
98
98
|
| `fenceTheme` | `(colorTheme: string, mode: 'light' \| 'dark') => string \| undefined` | The Shiki theme every fence-style surface renders in (Viewer fences, `CodeBlock`, the code-file hover preview, the plan diff view, code review suggestion snippets: everything that reads `useFenceTheme` / `resolveFenceTheme`). Return a theme name to use it, `undefined` to keep the default for that (palette, mode); a throw or an empty string also falls through, and a name Shiki does not know renders the block as plain text. Install it before the first render; it is read on every render, so a later change applies the next time a fence re-renders. The code-review diff pane's dark/light pair (`resolveSyntaxTheme`) is not affected. Also `setFenceThemeResolver` / `resetFenceThemeResolver` on `utils/syntaxTheme` (0.50.0) | The palette's `SHIKI_THEME_MAP` entry, else `pierre-dark` / `pierre-light`. Replaces mutating the exported `SHIKI_THEME_MAP`, which keeps working but is not a supported seam |
|
|
99
|
+
| `skillCatalogTransport` | `() => Promise<SkillCatalogEntry[]>` | The skill catalog the comment composers' `/` and `$` picker lists | `GET /api/skills` on the page origin |
|
|
100
|
+
| `skillContentTransport` | `(name: string) => Promise<unknown>` | A human-only skill's SKILL.md, pasted into feedback | `GET /api/skills/content?name=` on the page origin |
|
|
101
|
+
| `skillReferenceExport` | `boolean` | Whether the fetched catalog feeds each comment's "Skills referenced" list in the export; false for a host that composes one block per message (ui 0.53.0) | true |
|
|
102
|
+
| `skillInstructionsDestination` | `string` | What a composer's "Includes skill instructions" line says the instructions go with (ui 0.53.0) | `'your feedback'` |
|
|
103
|
+
| `webmcp` | `WebMcpPolicy` | The WebMCP provider policy, `{ enabled, namePrefix }` | Enabled whenever the browser exposes `document.modelContext`, prefix `plannotator.` |
|
|
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 |
|
|
99
107
|
|
|
100
108
|
### Interface details worth knowing
|
|
101
109
|
|
|
@@ -256,7 +264,7 @@ If Workspaces ever wants one of these surfaces, the path is the same as everythi
|
|
|
256
264
|
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:
|
|
257
265
|
|
|
258
266
|
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.
|
|
259
|
-
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.
|
|
267
|
+
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.18.11` since ui 0.54.0; it was `^0.16.47`) 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 (unreleased, 0.54.0)"). The same goes for a self-hosted copy (option 1). Same lazy-font behavior; adds a third-party origin.
|
|
260
268
|
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.
|
|
261
269
|
|
|
262
270
|
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.
|
|
@@ -625,7 +633,7 @@ The overlay-projection annotation viewer for raw HTML (placed comment markers, p
|
|
|
625
633
|
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.
|
|
626
634
|
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.
|
|
627
635
|
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`.
|
|
628
|
-
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.
|
|
636
|
+
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.
|
|
629
637
|
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").
|
|
630
638
|
|
|
631
639
|
### `@plannotator/core` 0.23.0
|
|
@@ -773,7 +781,9 @@ Three class-level deltas, none of which breaks an id- or class-based selector: `
|
|
|
773
781
|
- 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.
|
|
774
782
|
- 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).
|
|
775
783
|
- `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`).
|
|
776
|
-
-
|
|
784
|
+
- 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.
|
|
785
|
+
- **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.
|
|
786
|
+
- **`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.
|
|
777
787
|
|
|
778
788
|
**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.
|
|
779
789
|
|
|
@@ -964,6 +974,8 @@ that state out of `Viewer`, exactly as with `AnnotationToolstrip`'s
|
|
|
964
974
|
|
|
965
975
|
### 3. `CommentPopover` `mentionSource` — an `@` mention source for the composer
|
|
966
976
|
|
|
977
|
+
> **Agent tags (ui 0.53.1; opt-in, byte-identical when absent):** `MentionSource.kinds?: readonly ('user' | 'agent')[]` (default `['user']`) is the kinds `mentionMatches` offers; `MentionSource.matchDetail?: boolean` (default `true`) whether a query also matches `detail`; `MentionPerson.group?: string` makes `MentionPicker` draw each run of one group as `role="group"` (`data-mention-group`) labelled by its heading (`data-mention-group-heading`), the option ids unchanged (`<id>-option-<flat index>`); `MentionSource.renderNotice?: (tagged: readonly MentionPerson[]) => ReactNode` is drawn under the composer field in both `CommentPopover` layouts. The Plannotator Inbox uses all four to tag agent sessions in a file comment. `mentionGroups(people)` (`components/MentionPicker`) is the run splitter, exported for a host that draws its own grouped list.
|
|
978
|
+
|
|
967
979
|
> **0.43.2 additions (opt-in, byte-identical when absent):** `MentionSource.heading?: string | null` draws one non-selectable heading row (`data-mention-heading`, e.g. "People in this workspace") above the list; `MentionPerson.avatar?: { url?, initials?, tint? }` draws an avatar (`data-mention-avatar="image" | "initials"`) before the label — an `<img>` when `url` is set, else `initials` (defaulting to the label's first letter) on a disc tinted with `tint` (any CSS color; absent means the muted surface). Neither the grammar, the keyboard state machine, `onMentionsChange`, `onPickBlocked` nor `Annotation.mentions` changes.
|
|
968
980
|
|
|
969
981
|
```ts
|
|
@@ -1172,7 +1184,7 @@ handed to `onAddAnnotation` has no `mentions` key at all (`'mentions' in ann`
|
|
|
1172
1184
|
is false, asserted rather than `toBeUndefined()` — the difference between the
|
|
1173
1185
|
conditional spread and the bare shorthand is invisible to the latter).
|
|
1174
1186
|
|
|
1175
|
-
Tests (both DOM-gated, both in
|
|
1187
|
+
Tests (both DOM-gated, both in `tests/dom-test-list.txt`):
|
|
1176
1188
|
`components/Viewer.mentionSource.test.tsx` (5) drives the real Viewer — the
|
|
1177
1189
|
selection composer through the Vim toolbar's type-to-comment gesture and the
|
|
1178
1190
|
global composer through its button — and
|
|
@@ -1326,8 +1338,8 @@ line (3 lines above it) and with the textarea scrolled (`scrollTop` 40) —
|
|
|
1326
1338
|
and the caret x after the token is **0.016px** from the span's end.
|
|
1327
1339
|
|
|
1328
1340
|
Tests: `utils/composerTokens.test.ts` (16, DOM-free) and
|
|
1329
|
-
`components/CommentPopover.mentionChips.test.tsx` (11, DOM-gated, in
|
|
1330
|
-
|
|
1341
|
+
`components/CommentPopover.mentionChips.test.tsx` (11, DOM-gated, in
|
|
1342
|
+
`tests/dom-test-list.txt`).
|
|
1331
1343
|
|
|
1332
1344
|
## Annotation card header slot and mentions on the edit box (0.45.0)
|
|
1333
1345
|
|
|
@@ -1496,7 +1508,7 @@ edit textarea carries none of the five mention ARIA attributes, typing `@`
|
|
|
1496
1508
|
opens nothing, and `onEdit` is called with an updates object whose key list is
|
|
1497
1509
|
exactly `['text']`.
|
|
1498
1510
|
|
|
1499
|
-
Tests (both DOM-gated, both added to
|
|
1511
|
+
Tests (both DOM-gated, both added to `tests/dom-test-list.txt`):
|
|
1500
1512
|
`components/AnnotationPanel.cardHeader.test.tsx` (6) and
|
|
1501
1513
|
`components/AnnotationPanel.editMentions.test.tsx` (10).
|
|
1502
1514
|
|
|
@@ -1676,6 +1688,7 @@ Everything host-facing that changed between ui 0.50.0 / core 0.25.10 and this pa
|
|
|
1676
1688
|
**Question cards (#1683, #1699, #1702).**
|
|
1677
1689
|
|
|
1678
1690
|
- **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.
|
|
1691
|
+
- **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.
|
|
1679
1692
|
- **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.
|
|
1680
1693
|
- **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.
|
|
1681
1694
|
- **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.
|
|
@@ -1718,6 +1731,31 @@ Everything host-facing that changed between ui 0.50.0 / core 0.25.10 and this pa
|
|
|
1718
1731
|
- **"Ask this session" announcement (#1690):** new `components/AskSessionAnnouncementDialog`, `utils/askSessionAnnouncement` (cookie `plannotator-announce-ask-session-seen`) and `hooks/useFirstRunAnnouncementWindow`. Plannotator's apps mount it; nothing in the library mounts it for a host.
|
|
1719
1732
|
- **gruvbox palette (#1721):** sets `--diffs-addition-color-override` (dark `#b8bb26`, light `#79740e`), so added diff lines draw in gruvbox green instead of the text colour. This changed the guides.show viewer stylesheet (see "Publishing & versioning").
|
|
1720
1733
|
|
|
1734
|
+
## Two fewer forced layouts on document open (ui 0.53.1)
|
|
1735
|
+
|
|
1736
|
+
No API change. Two reads that forced a synchronous layout every time a
|
|
1737
|
+
`Viewer` mounted are gone (found by the Workspaces perf program: a warm
|
|
1738
|
+
document open spent most of its mount task in forced layouts):
|
|
1739
|
+
|
|
1740
|
+
- **`useVimSelection`** no longer resets on a mount, or on a `contentVersion`
|
|
1741
|
+
change, while vim was never on. The reset (state, focus, HUD, and the page
|
|
1742
|
+
selection inside the viewer through `window.getSelection()`) now runs only
|
|
1743
|
+
when vim goes from on to off. Behaviour change a host may notice: with vim
|
|
1744
|
+
off, a selection already inside the viewer's container is no longer
|
|
1745
|
+
cleared when the document mounts or its content changes; it is left to the
|
|
1746
|
+
browser, as on any page. Turning vim off still clears it, and a selection
|
|
1747
|
+
outside the container is never touched (0.46.1).
|
|
1748
|
+
- **`AnnotationToolstrip`**'s buttons no longer read `offsetWidth` in a layout
|
|
1749
|
+
effect. The label's width comes from layout: a hidden copy of the label sits
|
|
1750
|
+
in a one-column grid whose column animates `0fr` -> `1fr`, and the visible
|
|
1751
|
+
label is placed beside the icon with `inset-inline-start` (so it follows the
|
|
1752
|
+
icon under `dir="rtl"`) and clipped by the button. Collapsed
|
|
1753
|
+
size, padding, gap, the 180 ms curve, compact / iconOnly / touch rules and
|
|
1754
|
+
colours are unchanged; an expanded button is now its exact subpixel width
|
|
1755
|
+
(up to 0.5 px wider than the integer `offsetWidth` it used before). The
|
|
1756
|
+
button still carries the label twice in its text (the old hidden measure
|
|
1757
|
+
span did too); the copy is `aria-hidden`.
|
|
1758
|
+
|
|
1721
1759
|
## Viewer `answerOnly` (ui 0.53.0)
|
|
1722
1760
|
|
|
1723
1761
|
Additive, one optional `Viewer` prop; Plannotator passes none, so its render is unchanged. Added for the Plannotator Inbox window (`packages/inbox`), which shows an agent's message with answerable `:::question` cards and takes no comments on the message text.
|
|
@@ -1726,6 +1764,32 @@ Additive, one optional `Viewer` prop; Plannotator passes none, so its render is
|
|
|
1726
1764
|
- Inside, `Viewer` computes `authoringOff = readOnly || answerOnly` and uses it wherever it used `readOnly` for authoring; the question and checkbox handlers still read `readOnly`. Absent or false, every path is the one it was.
|
|
1727
1765
|
- The article keeps its card padding; a host that wants the bare text (as the Inbox does) overrides it from its own CSS on `[data-print-region="article"]`.
|
|
1728
1766
|
|
|
1767
|
+
## Viewer `textComments` (ui 0.53.1)
|
|
1768
|
+
|
|
1769
|
+
Additive, one optional `Viewer` prop; Plannotator's plan review and annotate pass none, so their render is unchanged. Added for the Plannotator Inbox (approval "inbox-message-comments", 2026-10-19), where the person comments on the words of an agent's message.
|
|
1770
|
+
|
|
1771
|
+
- **`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.
|
|
1772
|
+
|
|
1773
|
+
## Dependency bumps: KaTeX 0.18, @pierre/diffs 1.5.2, Mermaid 12.2 (published in ui 0.54.0)
|
|
1774
|
+
|
|
1775
|
+
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.
|
|
1776
|
+
|
|
1777
|
+
- **`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).
|
|
1778
|
+
- **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.
|
|
1779
|
+
- 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.
|
|
1780
|
+
- **`@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.
|
|
1781
|
+
- **`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.
|
|
1782
|
+
- The React peer range is unchanged (`^19.2.3`); only the dev copy moved to 19.3.1.
|
|
1783
|
+
|
|
1784
|
+
## Viewer `belowTitle` (published in ui 0.54.0)
|
|
1785
|
+
|
|
1786
|
+
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.
|
|
1787
|
+
|
|
1788
|
+
- **`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.
|
|
1789
|
+
- **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.
|
|
1790
|
+
- **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").
|
|
1791
|
+
- **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).
|
|
1792
|
+
|
|
1729
1793
|
## Viewer `resolveLocalLink` (ui 0.53.0)
|
|
1730
1794
|
|
|
1731
1795
|
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.
|
|
@@ -1734,7 +1798,7 @@ Additive, one optional `Viewer` prop; Plannotator's plan review and annotate pas
|
|
|
1734
1798
|
|
|
1735
1799
|
## Viewer `questionDecisionLocked` (ui 0.53.0)
|
|
1736
1800
|
|
|
1737
|
-
Additive, one optional `Viewer` prop; Plannotator passes none, so its render is unchanged. Added for the Plannotator Inbox (step 3, decisions), where one message can hold a sent question and an open one.
|
|
1801
|
+
Additive, one optional `Viewer` prop; Plannotator passes none, so its render is unchanged. Added for the Plannotator Inbox (step 3, decisions), where one message can hold a sent question and an open one. The Inbox no longer passes it: it records no decisions since #1985, parses its cards with `questionDecisionLines={false}` and locks sent questions with `questionReadOnly` instead.
|
|
1738
1802
|
|
|
1739
1803
|
- **`questionDecisionLocked?: (question: IndexedQuestion) => boolean`**: true for a question passes it neither `onToggleQuestionDecisionRecording` nor `onOpenQuestionDecision`, so its decision tag draws as a plain tag (on or dimmed off, as `questionDecisionRecording` says), exactly as under `readOnly`. Other questions in the same `Viewer` keep their switch and opener. Absent or false, every path is the one it was.
|
|
1740
1804
|
|
|
@@ -1761,22 +1825,47 @@ Additive, all opt-in; Plannotator passes none of it, so its render is unchanged.
|
|
|
1761
1825
|
|
|
1762
1826
|
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.
|
|
1763
1827
|
|
|
1764
|
-
- **`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.
|
|
1828
|
+
- **`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.
|
|
1765
1829
|
- **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.
|
|
1766
1830
|
- **`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.
|
|
1767
1831
|
- **`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.
|
|
1768
1832
|
|
|
1833
|
+
## Comments on part of an image (published in ui 0.54.0 and core 0.25.14; #2100; additive, needs a new core subpath)
|
|
1834
|
+
|
|
1835
|
+
Everything below is opt-in and byte-identical when a host passes none of it.
|
|
1836
|
+
|
|
1837
|
+
- **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).
|
|
1838
|
+
- **`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.
|
|
1839
|
+
- **`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.
|
|
1840
|
+
- **`ImageZoomStage`** `drawing` / `onDraw` (`ZoomDraw`), `onViewChange`, `description`, `children`, and the handle's `zoomTo(box)` / `visibleBox()`; `imageZoom` adds `naturalBoxFromPoints`, `naturalBoxToStage`, `stageToNatural`, `visibleNaturalBox`, `viewportForBox`.
|
|
1841
|
+
- **`CommentPopover`** `placement: 'side'`, `headerText`, `zIndex`; `computeCommentPopoverPosition` takes the placement as a third argument.
|
|
1842
|
+
- **`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.
|
|
1843
|
+
- **`components/ImageRegions`**: `RegionToolStrip`, `RegionBoxLayer`, `RegionThumb`, `RegionCardInfo`, `RegionNumber`, `regionOf`, `imageRegionNumbers`, `imageSourceLine`, `loadableImageSrc`.
|
|
1844
|
+
|
|
1769
1845
|
## Publishing & versioning
|
|
1770
1846
|
|
|
1771
|
-
- **core 0.25.
|
|
1847
|
+
- **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:
|
|
1848
|
+
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".
|
|
1849
|
+
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".
|
|
1850
|
+
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.
|
|
1851
|
+
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>`.
|
|
1852
|
+
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".
|
|
1853
|
+
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".
|
|
1854
|
+
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.
|
|
1855
|
+
- **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:
|
|
1772
1856
|
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; …)".
|
|
1773
1857
|
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; …)".
|
|
1774
1858
|
3. **Wider unions** (a compile error only in an exhaustive `switch`): `AgentToolHost` gains `'oh-my-pi'`; core `external-annotation`'s `session-closed` event has `by: "agent" | "session"` (ui's `useExternalAnnotations` passes it on, `utils/agentClosed` adds `closedReviewCopy`); core `inbox-types` gains the `"superseded"` question state and more `InboxRecordKind` / `InboxIdPrefix` members, and `inbox-questions` reads a bare negative word (`Stopped: no`) as no stop.
|
|
1775
1859
|
4. **Smaller visible changes.** Core `CODE_FILE_REGEX` also matches `.mjs`, `.cjs`, `.mts` and `.cts`, so those paths link as code files; the Pi provider icon is the three-colour pi.dev mark (no longer `currentColor`); the question card's eyebrow pills share one 18 px box and an off decision tag is fainter; `hooks/useUpdateCheck` (Plannotator-only, see "not for hosts") no longer asks GitHub from the browser and returns null unless the server's advert is passed.
|
|
1776
1860
|
5. **Questions (core `question-block`).** (a) `QUESTION_AUTHORING_GUIDE` was rewritten (2,213 to 2,654 characters) and now ends with an Inbox-only bullet naming `read_thread`, `supersedes` and `Replaces:`. A host that pastes the guide into its own agent prompts now passes on Plannotator Inbox wording; filter that bullet out or write your own. (b) `parseQuestionBlock` reads a `Replaces:` line whose every token is id-shaped (`msg_…/q-…`, or a bare `q-…`) into the new optional `ParsedQuestion.replaces` instead of context, wherever it sits in the block. Any other `Replaces:` line is prose as before, and `questionKey` is unchanged. (c) `QuestionAnswer` gains optional `by_reply` (kept by `parseQuestionAnswer` only beside `skipped: true`). `formatQuestionAnswersSection` prints such a skip as "Skipped (not answered in this reply)" and adds a count line telling the agent to treat those as skipped.
|
|
1777
1861
|
Everything else is additive and inert until a host passes it: on `Viewer` `answerOnly`, `resolveLocalLink`, `questionDecisionLocked`, `renderQuestionCard` / `renderQuestionBelowPrompt`, `refRepo` and `onHostDraft` (also on `HtmlViewer` and `DiagramViewer`, with `DiagramHostDraft`); `ViewerHandle.stepPin` / `revealPin` / `takeSelection`; `HtmlViewer` `hostNavigates`; `AnnotationToolstrip` `hideRedline`; `RenderedMarkdown` `resolveLocalLink` / `resolveImage` / `headingAnchors`; the `PlanCleanDiffView` seams and `foldUnchangedPlanDiff`; `CodeAnnotation.markdownPreview`; `AnnotationPanel` `emptyHint`; `CodeFileView` (the body of `CodeFilePopout`, exported); the Oh My Pi setup hint; Plannotator's own update modules (`Settings` `updateCheckSetting`, `hooks/useUpdateToast`, `hooks/useUpdateCheckSetting`, `utils/updateNotice`). Core adds `crypto`'s `encryptWithKey` / `decryptWithKey` / `deriveRelayKeys`, `diff-files`' `isLineRangeInPatch`, `source-save`'s decided and cross-origin responses, an optional error on `serverSessionMismatchBody`, Inbox wire types (`inbox-attachments.ts`, `inbox-surface-bridge.ts`, both re-exported by `inbox-types`), and `annotatable`'s `isDotenvFileName(name)` (#1568): the dotenv-family rule on a file NAME (`.env`, any name ending in `.env`, `.envrc`, and `.env.*` outside the annotatable docs, so `.env.example` stays). The Inbox attachment rule and Plannotator's HTML asset route both use it; the attachment rule is unchanged.
|
|
1862
|
+
Merged after the publish prep (#1898) and shipped in this pair:
|
|
1863
|
+
6. **Inbox decisions removed (core `inbox-types`, `inbox-questions`; #1985).** The wire types `InboxQuestionRecord` and `InboxQuestion` lose `decision_on_answer` and `decision_id`, and the parser's output `ParsedInboxQuestion` loses `decision_on_answer`: code that reads them no longer compiles. `parseInboxQuestionBlocks` parses with `{ decisionLines: false }`, so a `Decision:` line in an Inbox question is context. The Inbox decision types and helpers that were on `main` between the two publishes (`InboxDecision`, `inboxDecisionWords`, the search `decision` group and others) were never published.
|
|
1864
|
+
7. **Question behaviour in every host** (plan review and annotate too): a choice question whose `Recommended:` names no choice shows its Suggested line, and a read-only card draws no empty Other row (#1906); "Accept recommended" stays on a pick-any card until the picks match, and a `Recommended:` list that names a whole choice whose description holds the separator now matches it (#1913); an answer's prompt is cut between grapheme clusters (`capQuestionPrompt`, #1906). For Inbox helpers: `composeInboxReplyBody` adds the still-open line to a reply with no answer (#1903), and `deriveInboxSubject` no longer cuts inside an emoji (#1899). See "Grapheme-safe cuts on `inbox-questions`", "Question prompts are cut whole", "Question cards: the Suggested line, the read-only Other row, a host status label", "A reply with no answers leaves the questions open" and "Accept recommended on pick-any questions".
|
|
1865
|
+
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.
|
|
1866
|
+
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).
|
|
1778
1867
|
- **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)".
|
|
1779
|
-
- **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.
|
|
1868
|
+
- **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.
|
|
1780
1869
|
- **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)".
|
|
1781
1870
|
- **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)".
|
|
1782
1871
|
- **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)".
|
|
@@ -1786,13 +1875,13 @@ Additive, opt-in; Plannotator passes none of it, so its render is unchanged. Add
|
|
|
1786
1875
|
- **ui 0.43.1 (`mentionSource` on the viewers): `@plannotator/ui` only — `@plannotator/core` is UNCHANGED at `0.25.5`, so this publishes alone** (core 0.25.5 must already be published). Purely additive over 0.43.0: `mentionSource` on `Viewer` and `HtmlViewer` (forwarded to every comment composer each mounts) and the optional `Annotation.mentions` field the picked ids land on, set only when a source was supplied and a token survived. Nothing is removed, no new modules, no export-, share- or archive-visible change, and Plannotator passes none of it. See "`mentionSource` on the viewers (0.43.1)".
|
|
1787
1876
|
- **ui 0.43.0 (host toolbar seams): `@plannotator/ui` only — `@plannotator/core` is UNCHANGED at `0.25.5`, so this publishes alone** (core 0.25.5 must already be published). Purely additive: `selectionActions` + `quickLabels` on `AnnotationToolbar` (forwarded by `Viewer`; `selectionActions` also by `HtmlViewer`), `mentionSource` on `CommentPopover`, an optional third `mentions` argument on that component's `onSubmit`, and the new supported modules `utils/selectionActions`, `utils/mentions`, `components/SelectionActionsDropdown`, `components/MentionPicker`, `hooks/useMentionAutocomplete`. Nothing is removed and Plannotator passes none of it. See "Host toolbar seams (0.43.0)".
|
|
1788
1877
|
- **core 0.25.5 / ui 0.42.0 (diagram FILES, `.mmd`/`.mermaid`/`.dot`/`.gv`): additive on both packages, so the next publish is core-first.** `@plannotator/core/annotatable` gains `DiagramRenderKind`, `diagramRenderKindForPath`, `isDiagramRenderKind` and `annotateDiagramRenderKind`, and its built-in annotatable sets now include the four diagram extensions (`shouldStripFrontmatter` returns false for them — Mermaid's `--- … ---` config block is content — and they can no longer be registered through `markdownExtensions`). `@plannotator/ui` gains `diagramDocumentBlocks(text, kind)` on `utils/parser` (the ONE `code` block a whole-file diagram source renders as), `shareableDocumentMarkdown(markdown, renderAs)` on `utils/sharing`, the `DocumentRenderAs` type on `types` (`'markdown' | 'html' | DiagramRenderKind`, re-exporting core's kind), and an optional `Block.diagramSourceLineOffset` that `DiagramBlock` prefers over `Block.startLine` when resolving a diagram comment's `sourceLine` (unset on every parser-produced fence, so fences are byte-identical). `useLinkedDoc`'s `renderAs`/`setRenderAs`/`LinkedDocLoadData.renderAs` widen from `'markdown' | 'html'` to `DocumentRenderAs` — source-compatible for a host that only ever passes the old two, but a host whose own state is typed `'markdown' | 'html'` must widen its setter. Since core changes, **publish `core` first** and update UI's exact core dependency before packing ui.
|
|
1789
|
-
- **The
|
|
1878
|
+
- **The pair 0.41.2 shipped as was `@plannotator/ui` `0.41.2` on `@plannotator/core` `0.25.4`.** Core is UNCHANGED from 0.41.1, so 0.41.2 publishes alone (`ui` only; core 0.25.4 must already be published). 0.41.2 ships the palette-derived Mermaid node shadow at a default of 70% (`DEFAULT_MERMAID_SHADOW_AMOUNT`) and the Settings → Display "Diagram Shadow" control (0 / 40 / 70 / 100); see "Node shadow (the one thing that is not a colour)" under "Theme-aware Mermaid diagrams (0.40.0)" for the mapping — no API removal, only additive exports (`buildMermaidShadow`, `DEFAULT_MERMAID_SHADOW_AMOUNT`, `utils/diagramShadow`).
|
|
1790
1879
|
- The pair 0.41.1 shipped as was `@plannotator/ui` `0.41.1` on `@plannotator/core` `0.25.4`. Core is UNCHANGED from 0.41.0, so 0.41.1 published alone (`ui` only; core 0.25.4 must already be published). 0.41.1 is two fixes over 0.41.0 with no API change — the diagram engine is loaded lazily by the first diagram fence instead of riding every document read, and a press on the canvas's own controls no longer comments on the part behind them; see "0.41.1 — the engine is lazy, and the controls are not part of the diagram". The 0.41.0 notes below still describe the engine itself.
|
|
1791
1880
|
- **The pair 0.41.0 shipped as was `@plannotator/ui` `0.41.0` on `@plannotator/core` `0.25.4`. Publish `core` 0.25.4 first, then `ui` 0.41.0** (both by hand from `main` after merge; CI never publishes these packages). 0.41.0 is the diagram engine (see "Diagram engine (0.41.0)"): one renderer slot and one canvas behind `MermaidBlock` / `GraphvizBlock`, the `components/diagram` surface, the Graphviz runtime slot with `@viz-js/viz` pinned `3.30.0`, and `Annotation.diagramAnchor`; core 0.25.4 adds the `diagram-anchor` subpath ui imports, so a ui 0.41.0 on a published core 0.25.3 would fail to compile in a consumer.
|
|
1792
1881
|
- 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".
|
|
1793
1882
|
- 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).
|
|
1794
1883
|
- 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.
|
|
1795
|
-
- 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). 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.
|
|
1884
|
+
- 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 on core 0.25.14 (the current `latest` of both 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.
|
|
1796
1885
|
- 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.
|
|
1797
1886
|
- 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.**
|
|
1798
1887
|
- 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`**.
|
|
@@ -2259,15 +2348,15 @@ Plannotator's servers issue one random `serverSession` nonce per process (new `@
|
|
|
2259
2348
|
|
|
2260
2349
|
`parseQuestionAnswer` used to cut every `selected` label to `MAX_QUESTION_CHOICE_LABEL_CHARS` (200). Task-list choices (`- [ ]`) are never cut by the parser, so a task-list label over 200 characters could never be picked: the card sent the whole label, the read-back answer held the first 200 characters, the card did not show it picked, and the Plannotator Inbox refused it (`422 "<first 200 chars>" is not one of the question's choices`). Now `selected` labels are capped at the new **`MAX_QUESTION_SELECTED_CHARS`** (= `MAX_QUESTION_BODY_CHARS`, 20,000: no label a block can hold is longer), so every label round-trips whole. `MAX_QUESTION_CHOICE_LABEL_CHARS` keeps its value and its one parse use (cutting a long plain-bullet label at a sentence end). **`canonicalQuestionAnswer`** also maps an answer an older build cut at 200 characters back to the one choice whose label (or alias) begins with it; two choices sharing their first 200 characters leave it as stored. That lookup is exact: a long label adds no case or punctuation matching (normalized matching still runs only for choices with `aliases`). `questionExportItems` now also passes `choices` when a label is over 200 characters (new **`questionChoicesNeedCanonicalLabels(choices)`** decides it), so the export prints the full label for such an answer; a host that builds its own `QuestionExportItem`s should use the same rule. The parser, labels and `questionKey` do not change. **A host that validates answers itself** (Workspaces' own check that a pick is one of the choices) must not cut picked labels shorter than the choice labels it compares them with.
|
|
2261
2350
|
|
|
2262
|
-
## Grapheme-safe cuts on `inbox-questions` (
|
|
2351
|
+
## Grapheme-safe cuts on `inbox-questions` (core 0.25.13; additive, no new subpath)
|
|
2263
2352
|
|
|
2264
|
-
`@plannotator/core/inbox-questions` gains two exports, **`cutText(text, max, ellipsis = "…")`** and **`sliceText(text, max)`** (source `packages/core/text-cut.ts`, which is deliberately NOT its own subpath, so core's export map stays the published one). Both count UTF-16 code units like `slice`, so a limit means what it did, but cut only between grapheme clusters (`Intl.Segmenter`, with a surrogate-pair fallback): an emoji, a flag, a family or a letter with its marks is kept whole or dropped whole, never drawn as "�". **`deriveInboxSubject`** now uses `cutText`, so a derived subject whose 120-character cut used to land inside an emoji ends before it instead; any other subject is byte-identical. The UI does not import them;
|
|
2353
|
+
`@plannotator/core/inbox-questions` gains two exports, **`cutText(text, max, ellipsis = "…")`** and **`sliceText(text, max)`** (source `packages/core/text-cut.ts`, which is deliberately NOT its own subpath, so core's export map stays the published one). Both count UTF-16 code units like `slice`, so a limit means what it did, but cut only between grapheme clusters (`Intl.Segmenter`, with a surrogate-pair fallback): an emoji, a flag, a family or a letter with its marks is kept whole or dropped whole, never drawn as "�". **`deriveInboxSubject`** now uses `cutText`, so a derived subject whose 120-character cut used to land inside an emoji ends before it instead; any other subject is byte-identical. The UI does not import them; they shipped in core 0.25.13, with no ui change needed. The question-prompt cut changed in the next section.
|
|
2265
2354
|
|
|
2266
|
-
## Question prompts are cut whole (
|
|
2355
|
+
## Question prompts are cut whole (core 0.25.13; additive, no new subpath)
|
|
2267
2356
|
|
|
2268
2357
|
`@plannotator/core/question-block` gains **`capQuestionPrompt(prompt)`**: the prompt an answer keeps, cut at `MAX_QUESTION_PROMPT_CHARS` (400) between grapheme clusters (`sliceText`), never inside an emoji. `parseQuestionAnswer` and `emptyQuestionAnswer` use it, and so does `inbox-questions` wherever it compares or stores a prompt, so the two never disagree (`checkInboxAnswer` also takes the plain 400-unit cut an older client made). A prompt of 400 code units or fewer is byte-identical; a longer one whose 400th unit is half of an emoji now ends before the emoji instead of with a lone surrogate. **A host that cuts prompts itself** (Workspaces' own answer check) should call `capQuestionPrompt` rather than `slice(0, 400)`, or an answer whose prompt was cut across an emoji will read as a different question. The Pi extension vendors `text-cut.ts` beside `question-block.ts` for this.
|
|
2269
2358
|
|
|
2270
|
-
## Question cards: the Suggested line, the read-only Other row, a host status label (ui
|
|
2359
|
+
## Question cards: the Suggested line, the read-only Other row, a host status label (ui 0.53.0; plan review and annotate change too)
|
|
2271
2360
|
|
|
2272
2361
|
Three changes to `components/blocks/QuestionBlock`, from the Plannotator Inbox stress test (rows 36 to 38). The first two are bug fixes that show in plan review and annotate exactly as in the Inbox (the owner chose "fix them for everyone"); the third is an optional seam.
|
|
2273
2362
|
|
|
@@ -2277,41 +2366,52 @@ Three changes to `components/blocks/QuestionBlock`, from the Plannotator Inbox s
|
|
|
2277
2366
|
|
|
2278
2367
|
## Guided review cards: one count, any heading level (guide-viewer; the guides.show viewer is regenerated)
|
|
2279
2368
|
|
|
2280
|
-
`@plannotator/guide-viewer` is not published, but code review and the guides.show viewer use it, so for the record: **`GuideSectionCard`** gains **`numbered?: boolean`** (default true). `GuideView` passes `numbered={false}` to the "Everything else" card and numbers the sections against `guide.sections.length`, so the header's "14 sections" and the cards' "01 / 14" agree (the cards used to read "01 / 15"). A card with no position and no Reviewed control draws no empty row. **`renderMarkdownProse`** reads `#` to `######` as the card's one sub-heading style (an `h3`), where only `### ` was a heading and `## Heading` showed its hashes; a heading also ends the paragraph above it. Seven hashes, or none followed by a space, are prose. The pinned viewer manifest (`@plannotator/core/guide-viewer-manifest`) is regenerated: 399.7 KB gz of the 400 KB budget (it was 399.6).
|
|
2369
|
+
`@plannotator/guide-viewer` is not published, but code review and the guides.show viewer use it, so for the record: **`GuideSectionCard`** gains **`numbered?: boolean`** (default true). `GuideView` passes `numbered={false}` to the "Everything else" card and numbers the sections against `guide.sections.length`, so the header's "14 sections" and the cards' "01 / 14" agree (the cards used to read "01 / 15"). A card with no position and no Reviewed control draws no empty row. **`renderMarkdownProse`** reads `#` to `######` as the card's one sub-heading style (an `h3`), where only `### ` was a heading and `## Heading` showed its hashes; a heading also ends the paragraph above it. Seven hashes, or none followed by a space, are prose. The pinned viewer manifest (`@plannotator/core/guide-viewer-manifest`) is regenerated: 399.7 KB gz of the 400 KB budget (it was 399.6). That was the figure for that build; the pin core 0.25.13 ships, `viewer.DERExLls.js`, is 335.1 KB gz by the same measure (gzip level 9, `apps/guides-show/build/check-budgets.ts`).
|
|
2281
2370
|
|
|
2282
|
-
## `CodeFileView` `overflow` (ui
|
|
2371
|
+
## `CodeFileView` `overflow` (ui 0.53.0; additive, no core change)
|
|
2283
2372
|
|
|
2284
2373
|
**`CodeFileView`** (`components/CodeFilePopout`) gains **`overflow?: 'scroll' | 'wrap'`**, handed to Pierre's `File`: `'scroll'` (the default, today's view) scrolls long lines sideways, `'wrap'` wraps them. `CodeFilePopout` passes nothing and is unchanged. The Inbox wraps a code file with a line over 1,000 characters (a minified bundle), with no toggle.
|
|
2285
2374
|
|
|
2286
|
-
## A reply with no answers leaves the questions open (
|
|
2375
|
+
## A reply with no answers leaves the questions open (core 0.25.13; additive exports, one output change)
|
|
2287
2376
|
|
|
2288
2377
|
`@plannotator/core/inbox-questions` gains **`inboxStillOpenLine(questions)`** and **`INBOX_STILL_OPEN_LINE`** (a RegExp matching one line of it). **`composeInboxReplyBody`** changes output in one case only: when `sent` is empty (the reply carries no answer and no skip), it now puts the still-open line after the words: "No questions answered yet; all N are still open. Don't re-ask them; the person will answer in the Inbox." (N: the questions still open or picked and not sent, counted over the new optional `stillOpen` input, default `questions`; the Inbox passes every question the replied-to message's session asked in the thread; the singular and an "earlier answers" variant are in the source; nothing when none is open). A body with at least one answer or skip is byte-identical. A host that composes Inbox replies with it gets the new line; a host that strips the store's text from a reply (as the window's `replyWords` does) should cut at a line `INBOX_STILL_OPEN_LINE` matches. `question-block.ts` (plan review's `formatQuestionAnswersSection`) is unchanged. No ui change.
|
|
2289
2378
|
|
|
2290
|
-
## Accept recommended on pick-any questions; a whole choice in a `Recommended:` list (ui and core
|
|
2379
|
+
## Accept recommended on pick-any questions; a whole choice in a `Recommended:` list (ui 0.53.0 and core 0.25.13; plan review, annotate and the Inbox change too)
|
|
2291
2380
|
|
|
2292
2381
|
- **`QuestionBlock`**: on a `:::question-multi` card whose `Recommended:` names choices, "Accept recommended" now stays offered while the answer is not exactly those choices (`isRecommendedQuestionAnswer`), where it went away at the first tick. Accept still sets exactly `recommendedQuestionAnswer` (the recommended choices, extra ticks and Other cleared, the note kept), so after it the button goes away, and focus moves to the first checked choice (or the field holding the suggestion) instead of dropping to the page. Pick-any questions recommending only a suggestion, pick-one and text questions, settled questions and read-only cards are unchanged. No prop or DOM change otherwise.
|
|
2293
2382
|
- **`parseQuestionBlock`** (core): a multi `Recommended:` list that names a choice by its full text, `— description` included, whose description holds the list's separator (`Reply box, Comments on attached files — one line of work, could ship first`), now matches every choice instead of none (it was a Suggested line). After the plain split fails, neighbouring parts are joined back, but only into a span that is exactly a choice's full text, label or name, and only when every part is covered. It runs only where the plain split failed, so a recommendation that split cleanly is unchanged; one main matched only through the final lettered fallback can now match more (plain bullets `a. Alpha`, `b. Beta — slow, steady` with `Recommended: a. Alpha first, b. Beta — slow, steady` named only `a.` and now names both).
|
|
2294
2383
|
- **`Viewer` `questionReadOnly?: (question: IndexedQuestion) => boolean`** (additive): true draws that one question card read-only, exactly as under `readOnly` (no picks, Other, note, Skip or Accept recommended; its decision tag a plain tag), while the other questions in the document stay answerable. The Inbox passes it for every question whose state is not open or picked (sent, closed by a Mark read or the archive, superseded), so a sent card in a message whose other question is still open no longer offers to re-answer it (the store would take that pick and make the question picked again; the thread-state model says the window does not offer it, `adr/implementation/inbox-thread-state.md`, the Pick row). Absent (plan review, annotate): every question follows `readOnly`, unchanged.
|
|
2295
2384
|
|
|
2296
|
-
## Skill references outside the comment popover (ui + core
|
|
2385
|
+
## Skill references outside the comment popover (ui 0.53.0 + core 0.25.13; additive, Plannotator unchanged)
|
|
2297
2386
|
|
|
2298
2387
|
For the Plannotator Inbox's composers (approval "inbox-skill-references"). Every default reproduces today's markup and export byte for byte.
|
|
2299
2388
|
|
|
2300
2389
|
- **`components/ComposerTokenTextarea`** (new, exported): the comment composer's textarea with its token highlight mirror, moved out of `CommentPopover` (which renders it with every default). New optional props: **`textClassName`** (classes on both the textarea and the mirror; default `COMPOSER_TEXT_CLASSES`, also exported), **`inputClassName`** / **`mirrorClassName`** (extra classes on one side), **`inputProps`** (further textarea attributes; an `aria-label` there replaces the placeholder default). `CommentPopover` re-exports the `ComposerMentionChips` type.
|
|
2301
|
-
- **`HumanOnlySkillNotice`** gains **`destination?: string`**; without it the line uses the host's word, **`setSkillInstructionsDestination`** / **`configurePlannotatorUI({ skillInstructionsDestination })`**, default `'your feedback'`. **`SkillReferenceMenu`** rows carry **`data-skill-item-part="name" | "description" | "root"`** on their three spans (a styling hook; no visual change).
|
|
2302
|
-
- **`utils/skillReferences`**: **`renderSkillReferencesBlock(refs, { contentOf?, injectedNames?, external?, audience? })`**, the block for references a caller already resolved; `skillReferenceExportBlock` now calls it with the reviewer wording (output unchanged). `audience` is a `SkillBlockAudience` from the new **`@plannotator/core/skill-reference-block`** (`REVIEWER_SKILL_AUDIENCE`, `INBOX_REPLY_SKILL_AUDIENCE`, `INBOX_MESSAGE_SKILL_AUDIENCE`, `skillsReferencedHeading`, `skillInstructionsIncludedNote`, and the Inbox's reader `splitInboxSkillsBlock` / `withoutInboxSkillsBlock`). ui
|
|
2390
|
+
- **`HumanOnlySkillNotice`** gains **`destination?: string`**; without it the line uses the host's word, **`setSkillInstructionsDestination`** / **`configurePlannotatorUI({ skillInstructionsDestination })`**, default `'your feedback'`. **`SkillReferenceMenu`** rows carry **`data-skill-item-part="name" | "description" | "root"`** on their three spans (a styling hook; no visual change). **`SkillReferenceMenu`** also takes optional **`boundary?: () => { top, bottom } | null`** (the box to stay inside, default the viewport), **`prefer?: 'above' | 'below'`** (default above) and **`maxListHeight?`** (default 256), and **`computeMenuPlacement`** is exported; **`useSkillReferenceAutocomplete`** takes **`bareEscapeDismisses?`** (default off: Escape on a bare trigger passes through). Without them nothing changes (the Inbox's compose dialog passes them).
|
|
2391
|
+
- **`utils/skillReferences`**: **`renderSkillReferencesBlock(refs, { contentOf?, injectedNames?, external?, audience? })`**, the block for references a caller already resolved; `skillReferenceExportBlock` now calls it with the reviewer wording (output unchanged). `audience` is a `SkillBlockAudience` from the new **`@plannotator/core/skill-reference-block`** (`REVIEWER_SKILL_AUDIENCE`, `INBOX_REPLY_SKILL_AUDIENCE`, `INBOX_MESSAGE_SKILL_AUDIENCE`, `skillsReferencedHeading`, `skillInstructionsIncludedNote`, and the Inbox's reader `splitInboxSkillsBlock` / `withoutInboxSkillsBlock`). ui imports this new core subpath, so ui 0.53.0 needs core 0.25.13.
|
|
2303
2392
|
- **`@plannotator/core/markdown-structure`** gains **`codeFenceFor(code)`** (three backticks, or one more than the longest backtick run in the code). Code review's feedback export fences Highlighted text, Replaces and Suggested code with it: byte-identical unless the quoted code holds a ``` run.
|
|
2304
2393
|
- **`utils/skillCatalog`**: **`normalizeSkillContent`** is exported; **`setSkillCatalogExportRegistration(on)`** (host configuration: `resetSkillCatalogCache` leaves it alone), and **`configurePlannotatorUI({ skillReferenceExport })`** (default true): false keeps the fetched catalog out of the per-comment export, for a host that composes one block per message itself.
|
|
2305
2394
|
|
|
2306
|
-
## The image peek, a stepping lightbox, and `Viewer` image seams (ui
|
|
2395
|
+
## The image peek, a stepping lightbox, and `Viewer` image seams (ui 0.53.0; additive, no core change)
|
|
2307
2396
|
|
|
2308
2397
|
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.
|
|
2309
2398
|
|
|
2310
2399
|
- **`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).
|
|
2311
|
-
- **`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
|
|
2400
|
+
- **`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)`.
|
|
2312
2401
|
- **`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.
|
|
2313
2402
|
|
|
2314
|
-
##
|
|
2403
|
+
## Zoom in `ImageLightbox` (#2067; published in ui 0.54.0; no prop change, no core change)
|
|
2404
|
+
|
|
2405
|
+
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:
|
|
2406
|
+
|
|
2407
|
+
- **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.
|
|
2408
|
+
- **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.
|
|
2409
|
+
- **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.
|
|
2410
|
+
- **The page never zooms over the lightbox**: Ctrl + wheel and `gesturestart` / `gesturechange` / `gestureend` are prevented anywhere over it.
|
|
2411
|
+
|
|
2412
|
+
**`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.
|
|
2413
|
+
|
|
2414
|
+
## `Tooltip` `shouldOpen` (ui 0.53.0; additive, no core change)
|
|
2315
2415
|
|
|
2316
2416
|
**`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.
|
|
2317
2417
|
|
|
@@ -2319,6 +2419,147 @@ For the Plannotator Inbox's one viewer (approval "inbox-image-viewer", 2026-10-1
|
|
|
2319
2419
|
|
|
2320
2420
|
**`Tooltip`** also gains **`container?: HTMLElement | null`**, where the popup is portaled (Base UI's `Portal` `container`). Unset or null: the document body, as before. The Inbox passes the open native `<dialog>` a trigger sits in (its search palette, compose and its modal dialogs), since a popup in the body is drawn under the dialog's top layer. And **`escapePassesThrough?: boolean`**: Escape closes the tooltip without using the key up (the close is canceled, so Base UI neither prevents nor stops it, and the tooltip closes through its own state), so a page's Escape still works while a tooltip is open. Unset: Base UI's default, unchanged.
|
|
2321
2421
|
|
|
2422
|
+
## Questions without decision lines (ui 0.53.0, core 0.25.13; additive)
|
|
2423
|
+
|
|
2424
|
+
For a host that records no decisions; the Plannotator Inbox passes it since its Decisions were removed (#1985). Every decision seam above is unchanged.
|
|
2425
|
+
|
|
2426
|
+
- **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.
|
|
2427
|
+
- **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.
|
|
2428
|
+
|
|
2429
|
+
## Enter saves a comment (#1934; published in ui 0.54.0; additive, no core change)
|
|
2430
|
+
|
|
2431
|
+
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.
|
|
2432
|
+
|
|
2433
|
+
- **`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.
|
|
2434
|
+
- **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.
|
|
2435
|
+
- **`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).
|
|
2436
|
+
- **`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.
|
|
2437
|
+
|
|
2438
|
+
## A Send never skips on its own; one reply per session (published in core 0.25.14; additive exports, two output changes)
|
|
2439
|
+
|
|
2440
|
+
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.
|
|
2441
|
+
|
|
2442
|
+
## Plan review's decision control (published in ui 0.54.0; additive, no core change)
|
|
2443
|
+
|
|
2444
|
+
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.
|
|
2445
|
+
|
|
2446
|
+
- **`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.
|
|
2447
|
+
- **`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.
|
|
2448
|
+
- **`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).
|
|
2449
|
+
- **`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`.
|
|
2450
|
+
- **`Settings`** gains **`onPermissionModeChange?: (mode) => void`**, called when its Permission Mode row changes. Absent: unchanged.
|
|
2451
|
+
|
|
2452
|
+
## Viewer `renderDirectiveBlock`; core `findDirectiveBlocks`, `blockKeyHash8` and `./inbox-actions` (published in ui 0.54.0 and core 0.25.14; additive, plan review unchanged)
|
|
2453
|
+
|
|
2454
|
+
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.
|
|
2455
|
+
|
|
2456
|
+
- **`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.
|
|
2457
|
+
- **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.
|
|
2458
|
+
- **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.
|
|
2459
|
+
- **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.`
|
|
2460
|
+
|
|
2461
|
+
## Tooltips: a borderless float at 150 ms (published in ui 0.54.0; visual change, no core change)
|
|
2462
|
+
|
|
2463
|
+
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:
|
|
2464
|
+
|
|
2465
|
+
- **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)`).
|
|
2466
|
+
- **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).
|
|
2467
|
+
|
|
2468
|
+
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.
|
|
2469
|
+
|
|
2470
|
+
## Document text size (#2066; published in ui 0.54.0; additive, no core change)
|
|
2471
|
+
|
|
2472
|
+
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.
|
|
2473
|
+
|
|
2474
|
+
- **`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.
|
|
2475
|
+
- **`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.
|
|
2476
|
+
- **Setting `documentTextSize`** (`config/settings.ts`, cookie-only `plannotator-document-text-size`, default 15).
|
|
2477
|
+
- **`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`.
|
|
2478
|
+
- **`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.
|
|
2479
|
+
|
|
2480
|
+
## Back from a linked document keeps the reader's place (#2082; published in ui 0.54.0; additive, no core change)
|
|
2481
|
+
|
|
2482
|
+
- **`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.
|
|
2483
|
+
- **`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.
|
|
2484
|
+
- **`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.
|
|
2485
|
+
- **`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.
|
|
2486
|
+
|
|
2487
|
+
## Image viewer: "Use ⌘ + scroll to zoom" (#2127; published in ui 0.54.0; visual change, additive prop, no core change)
|
|
2488
|
+
|
|
2489
|
+
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:
|
|
2490
|
+
|
|
2491
|
+
- **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).
|
|
2492
|
+
- **`utils/platform`** adds `modKeySpoken` (`'Command'` / `'Control'`), the modifier as a screen reader says it in a sentence.
|
|
2493
|
+
|
|
2494
|
+
## 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)
|
|
2495
|
+
|
|
2496
|
+
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.
|
|
2497
|
+
|
|
2498
|
+
- **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).
|
|
2499
|
+
- **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`).
|
|
2500
|
+
- **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.
|
|
2501
|
+
- **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.
|
|
2502
|
+
- **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.
|
|
2503
|
+
- **core: pick any.** `Recommended: Search and replace` names a `Search and replace: both` choice whole before the text is split into `Search` and `replace`.
|
|
2504
|
+
- **core: a `Context:` line inside a code fence** in a choice's continuation lines is the code's, not context.
|
|
2505
|
+
- **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`.
|
|
2506
|
+
|
|
2507
|
+
## 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)
|
|
2508
|
+
|
|
2509
|
+
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.
|
|
2510
|
+
|
|
2511
|
+
- **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.
|
|
2512
|
+
- **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.
|
|
2513
|
+
- **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)`.
|
|
2514
|
+
- **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.
|
|
2515
|
+
- **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.
|
|
2516
|
+
- **`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").
|
|
2517
|
+
- **`collectDocumentImages`** (the lightbox's stepping) also counts `img[data-zoomable]`.
|
|
2518
|
+
- New Tailwind classes in the card change the shared stylesheet (the guides.show viewer's pinned CSS was re-synced).
|
|
2519
|
+
|
|
2520
|
+
## `QuestionPictureTiles` (published in ui 0.54.0; additive, the card unchanged)
|
|
2521
|
+
|
|
2522
|
+
- **`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).
|
|
2523
|
+
|
|
2524
|
+
## Hover cards outside the Inbox: `onlyWithData`, `RefHoverCards`, `refCardRouteTransport` (published in ui 0.54.0; additive, no core change)
|
|
2525
|
+
|
|
2526
|
+
- **`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.
|
|
2527
|
+
- **`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.
|
|
2528
|
+
- **`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.
|
|
2529
|
+
|
|
2530
|
+
## Rich-text Copy (#1972, #2191; published in ui 0.54.0; additive, off unless a host opts in)
|
|
2531
|
+
|
|
2532
|
+
- **`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.
|
|
2533
|
+
- **`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`).
|
|
2534
|
+
- **`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-*`.
|
|
2535
|
+
- **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.
|
|
2536
|
+
- **`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).
|
|
2537
|
+
|
|
2538
|
+
## 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)
|
|
2539
|
+
|
|
2540
|
+
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.
|
|
2541
|
+
|
|
2542
|
+
- **`./inbox-folding`**: what folds when a thread opens and what a folded line says (`lookBoundary`, `planFolds`, `foldSnippet`, `holdingsOf` …).
|
|
2543
|
+
- **`./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.
|
|
2544
|
+
- **`./inbox-open-questions`**: the open questions list's rules (`threadOpenQuestions`, `openQuestionsLabel`, `skipSelection` …). It needs `./inbox-format`'s `formatCount`.
|
|
2545
|
+
- **`./inbox-format`**: display helpers with fixed `en-US` `Intl` formats (`shortTime`, `whenTime`, `fullTime`, `formatCount`, `agentName` …).
|
|
2546
|
+
|
|
2547
|
+
Publish core first, then ui, as always. These subpaths need no ui bump until ui imports them.
|
|
2548
|
+
|
|
2549
|
+
## Document font (#1657; published in ui 0.54.0; additive, no core change)
|
|
2550
|
+
|
|
2551
|
+
Settings → Display can show a **Document font** row beside Text size. Inert until a host opts in, like Text size.
|
|
2552
|
+
|
|
2553
|
+
- **`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.
|
|
2554
|
+
- **`hooks/useDocumentFont()`** (new): applies the setting to the document element while mounted.
|
|
2555
|
+
- **Setting `documentFontFamily`** (`config/settings.ts`, cookie-only `plannotator-document-font-family`, default `''` = the theme's font).
|
|
2556
|
+
- **`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.
|
|
2557
|
+
- **`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`).
|
|
2558
|
+
|
|
2559
|
+
## AsciiDoc documents (#1009; published in ui 0.54.0 and core 0.25.14; additive, one widened setter)
|
|
2560
|
+
|
|
2561
|
+
`@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.
|
|
2562
|
+
|
|
2322
2563
|
## The law (guardrails for anyone editing `@plannotator/ui`)
|
|
2323
2564
|
|
|
2324
2565
|
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.
|
|
@@ -2329,3 +2570,19 @@ These are enforced socially and, where possible, by CI. They exist because a pri
|
|
|
2329
2570
|
4. **Never delete working Plannotator code until a human confirms parity in the browser.**
|
|
2330
2571
|
|
|
2331
2572
|
See `packages/ui/README.md` and `packages/ui/AGENTS.md` (CLAUDE.md symlink) for the short version that lives next to the code.
|
|
2573
|
+
|
|
2574
|
+
## `CodeFileView` `extraCss` and `CodeAnnotation.outsideDiff` (published in ui 0.54.0; additive, no core change)
|
|
2575
|
+
|
|
2576
|
+
**`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.
|
|
2577
|
+
|
|
2578
|
+
**`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.
|
|
2579
|
+
|
|
2580
|
+
## `CodeFileView` `revealLine`, `onTokenTarget`, `onTokenNavigate` (published in ui 0.54.0; additive, no core change)
|
|
2581
|
+
|
|
2582
|
+
**`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):
|
|
2583
|
+
|
|
2584
|
+
- **`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.
|
|
2585
|
+
- **`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.
|
|
2586
|
+
- **`onTokenNavigate?: (target: CodeFileTokenTarget) => void`** is a Cmd/Ctrl-click (or Alt-click) on a token.
|
|
2587
|
+
|
|
2588
|
+
`CodeFileTokenTarget` is `{ tokenText, tokenElement, lineNumber, lineCharStart }`. Absent: today's view, byte for byte (`CodeFilePopout` and the Inbox pass none).
|