@plannotator/ui 0.41.1 → 0.41.2
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 +9 -2
- package/README.md +2 -2
- package/components/DiagramBlock.tsx +19 -5
- package/components/Settings.tsx +30 -0
- package/components/ThemeProvider.tsx +27 -1
- package/components/Viewer.tsx +4 -1
- package/config/settings.ts +22 -0
- package/hooks/usePrintMedia.ts +70 -0
- package/hooks/usePrintMode.ts +11 -15
- package/package.json +1 -1
- package/print.css +27 -4
- package/styles.css +1 -1
- package/utils/diagram-render.ts +14 -7
- package/utils/diagramShadow.ts +27 -0
- package/utils/mermaid.ts +12 -0
- package/utils/mermaidTheme.ts +138 -7
package/HANDOFF.md
CHANGED
|
@@ -219,7 +219,7 @@ We deliberately did **not** restructure the exports map in this PR (move-don't-r
|
|
|
219
219
|
| `utils/mermaid-math-slot` | Alias target only: what a host redirects Mermaid's own `katex` import to, so `$$` labels in diagrams typeset through the math slot and the host build carries one KaTeX chunk. Never import it yourself. See "Lazy renderers and eager entries", item 2. |
|
|
220
220
|
| `utils/identity-tater` | Side-effect entry that registers the full username dictionary into the identity generator slot. Import it only if you rely on the default tater names and want the full dictionary; a host with `identityProvider` should not. |
|
|
221
221
|
| `utils/mermaid` (`loadMermaidRuntime`, `getMermaidRuntime`, `getMermaidRuntimeSource`, `setMermaidRuntime`, `MERMAID_CONFIG`) and `utils/mermaid-eager` | The Mermaid runtime slot and its eager registration. Omit `utils/mermaid-eager` for the lazy path with retry, which is what Plannotator itself does since 0.40.0 (Mermaid 12); import it to register the runtime in your entry chunk at startup. See "Lazy renderers and eager entries" and "Mermaid 12 (0.40.0)". |
|
|
222
|
-
| `utils/mermaidTheme` (`buildMermaidThemeVariables`, `readThemeTokens`, `applyMermaidTheme`, `mermaidThemeKey`, `buildMermaidConfig`, `ensureContrast`) and `utils/cssColor` | Theme-aware diagram configuration: the pure token-to-`themeVariables` mapping with its contrast guard, the document token reader, and the cached per-`(palette, mode)` `initialize` step `MermaidBlock` runs before each render. Additive; with no theme tokens on the document the static `MERMAID_CONFIG` stays in force. See "Theme-aware Mermaid diagrams (0.40.0)". |
|
|
222
|
+
| `utils/mermaidTheme` (`buildMermaidThemeVariables`, `readThemeTokens`, `applyMermaidTheme`, `mermaidThemeKey`, `buildMermaidConfig`, `ensureContrast`, `buildMermaidShadow`, `DEFAULT_MERMAID_SHADOW_AMOUNT`), `utils/diagramShadow` and `utils/cssColor` | Theme-aware diagram configuration: the pure token-to-`themeVariables` mapping with its contrast guard, the palette-derived node shadow (`options.shadowAmount`, default 0.7 of Mermaid's own), the document token reader, and the cached per-`(palette, mode, shadow amount)` `initialize` step `MermaidBlock` runs before each render. Additive; with no theme tokens on the document the static `MERMAID_CONFIG` stays in force. See "Theme-aware Mermaid diagrams (0.40.0)". |
|
|
223
223
|
|
|
224
224
|
**AI is fully avoidable** — with one precision worth knowing. No AI *UI* is reachable from the supported components: `useAIChat` is imported only by `components/ai/DocumentAIChatPanel` and `useAIProviderConfig`, neither of which any supported component imports, and `CommentPopover`'s Ask-AI affordance exists only behind the optional `onAskAI` prop. `configure.ts` does statically import the `useAIChat` module (it needs `setAITransport`), but if you never use AI the hook is dead code and bundlers eliminate it — verified empirically: a standalone consumer's production bundle importing the full supported surface contains zero `/api/ai` strings. Don't import `components/ai/*` and don't pass `aiTransport`, and you ship no AI code.
|
|
225
225
|
|
|
@@ -689,6 +689,12 @@ Mermaid diagrams used to render from one static config in every palette and both
|
|
|
689
689
|
|
|
690
690
|
**Fallback contract for hosts.** Nothing changes for a host that does not use the tokens: with no `--background`/`--foreground` on the document (no `ThemeProvider`, no `theme.css`), `readThemeTokens` returns `undefined`, `buildMermaidThemeVariables` returns `null`, `buildMermaidConfig(null)` is `MERMAID_CONFIG` itself, and `applyMermaidTheme` records the key without calling `initialize` at all, so the runtime keeps the static config the loader or the eager entry initialized it with and renders byte-identically to 0.39.0 (pinned by `utils/mermaidTheme.test.ts` and `components/MermaidBlock.theme.test.tsx`). Outside a `ThemeProvider`, `useTheme()` yields the default context (Plannotator dark), which only names the key. `MERMAID_CONFIG` keeps its value and meaning (`securityLevel: 'strict'` still pinned by `components/MermaidBlock.test.ts`; `flowchart.htmlLabels` and `curve` are carried into the dynamic config unchanged) and `loadMermaidRuntime` / the eager entry are untouched: the runtime is still initialized once at registration, and the theme apply is a second, cached `initialize` on top. A host that ships its own tokens under the same names gets themed diagrams for free; a host that wants the old slate look in a themed document can keep the tokens off the diagram's ancestors, since `readThemeTokens` reads the document element by default. New exports are additive; the only behaviour change is for documents that carry the tokens, where diagrams now follow them.
|
|
691
691
|
|
|
692
|
+
**Node shadow (the one thing that is not a colour).** Mermaid 12's neo look paints a drop shadow on every node, cluster and actor, from its own fixed `drop-shadow(1px 2px 2px rgba(185,185,185,1))` — a grey that reads as a halo on a themed page and follows no palette. The look is kept and that one filter is replaced: `buildMermaidThemeVariables(tokens, mode, options?)` takes an optional **`options.shadowAmount`, 0..1, default `DEFAULT_MERMAID_SHADOW_AMOUNT` (0.7)**, and publishes `themeVariables.dropShadow` from the new pure `buildMermaidShadow(ground, amount)`; the returned `MermaidThemeSpec` carries the resolved `shadowAmount`. Geometry scales over a small floor — `x = 0.3 + 0.7a`, `y = blur = 0.6 + 1.4a` — so **1 reproduces Mermaid's `1px 2px 2px` exactly** and **0 publishes `dropShadow: false`** (plus `nodeShadow: false`, the only thing that reaches the inline `filter:url(#…-drop-shadow-small)` on a state diagram's small start/end dots), which the neo rules render as `filter: none`. The colour comes from the palette's own ground and its POLARITY follows the page, the same rule Mermaid's `insertLookDefs` uses (`floodColor = theme.includes('dark') ? '#FFFFFF' : '#000000'`): on a dark page the ground lifted 82% toward white at alpha `0.18 + 0.72a`, on a light page darkened 40% toward black at `0.25 + 0.3a`, each asserted to differ from the ground in the direction that reads (falling back to plain white or black; to Mermaid's own grey when the ground is not a usable colour at all). Polarity, not just alpha, is the point: a black shadow on a near-black ground is a valid filter that paints nothing.
|
|
693
|
+
|
|
694
|
+
`mermaidThemeKey(colorTheme, mode, shadowAmount?)` takes the amount as a third argument and appends `#s<amount>` to the key **only when it is not the default**, so the cache key for a host that never names an amount is byte-identical to the `(palette, mode)` key of 0.40.0 and no extra `initialize` happens. `DiagramTheme` (`utils/diagram-render`) gains an optional `shadowAmount`, which `DiagramBlock` fills from Plannotator's cookie-only `diagramShadow` setting (Settings → Display → "Diagram Shadow": 0 / 40 / 70 / 100 on a 0-100 percent scale; `utils/diagramShadow` exports `DEFAULT_DIAGRAM_SHADOW`, `DIAGRAM_SHADOW_OPTIONS`, `isDiagramShadow` and `diagramShadowAmount`, and is deliberately import-free so the settings registry can read it without pulling the diagram mapping into every surface's module graph).
|
|
695
|
+
|
|
696
|
+
**What a host does.** Nothing, to get the toned-down default. To keep **Mermaid's own grey**, pass your own `themeVariables.dropShadow` after ours (`{ ...buildMermaidConfig(spec), themeVariables: { ...spec.themeVariables, dropShadow: 'drop-shadow(1px 2px 2px rgba(185,185,185,1))' } }`) — or simply `buildMermaidThemeVariables(tokens, mode, { shadowAmount: 1 })` for the same geometry in your palette's own colour. To ship **no shadow**, pass `{ shadowAmount: 0 }`, or build the key with `mermaidThemeKey(palette, mode, 0)` if you drive `applyMermaidTheme` yourself. The static `MERMAID_CONFIG` a token-less host renders with carries the same 0.7 geometry with one fixed light colour (its own slate palette is dark) — that is the ONE byte that changed in the fallback config; `securityLevel: 'strict'`, `startOnLoad`, `flowchart.htmlLabels` and `curve` are untouched. The shadow is paint-time only: at every amount the fills and label colours the mapping produces are identical (pinned in `utils/mermaidTheme.test.ts`, which also sweeps every shipped palette in both modes for a default shadow in the readable direction).
|
|
697
|
+
|
|
692
698
|
**Known limits.** `GraphvizBlock` already maps its output to `var(--foreground)` / `var(--muted-foreground)` / `var(--muted)` and needs nothing. Mermaid hardcodes a `#000000` stroke on the sequence `crosshead` marker (lost messages, `-x`), which no theme variable reaches; it stays as in every Mermaid theme. The Mermaid 12 upgrade shipped in the same 0.40.0 publish and reuses this mapping unchanged; the re-sweep on 12 is in "Mermaid 12 (0.40.0)".
|
|
693
699
|
|
|
694
700
|
## Element context through the host seam (0.40.0)
|
|
@@ -869,7 +875,8 @@ to fall back on:
|
|
|
869
875
|
|
|
870
876
|
## Publishing & versioning
|
|
871
877
|
|
|
872
|
-
- **The current pair is `@plannotator/ui` `0.41.
|
|
878
|
+
- **The current pair is `@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`).
|
|
879
|
+
- 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.
|
|
873
880
|
- **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.
|
|
874
881
|
- 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".
|
|
875
882
|
- 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).
|
package/README.md
CHANGED
|
@@ -56,7 +56,7 @@ Building your own tooltip and removing the built-in double-click reset are host-
|
|
|
56
56
|
The Mermaid runtime, the Graphviz engine, KaTeX and the username dictionary are off the static import graph of `Viewer`, so a host that bundles by route does not download them for a plain markdown read. Graphviz has its own slot since 0.41.0 (`utils/graphviz`, the same shape as Mermaid's: lazy `import('@viz-js/viz')` on the first dot fence, or `setGraphvizRuntime` from the host) and the block shows the source fence under a status until the SVG lands. Mermaid, Graphviz, KaTeX and the dictionary sit behind synchronous slots:
|
|
57
57
|
|
|
58
58
|
- **Math.** Without registration, a math node renders its TeX as text in the same wrapper (same `data-math-tex` / `data-math-display` / `aria-label` / class names), loads KaTeX via `import('katex')`, and re-renders typeset. To keep math typeset on the very first commit, as Plannotator does, add one line to your entry: `import "@plannotator/ui/utils/math-eager";`. To put KaTeX and its stylesheet on one lazy chunk instead, pass `mathRendererLoader`. The stylesheet remains your job either way (see "Consuming it", step 3). The default `import('katex')` is the only runtime mention of `katex` in the package and lives in `utils/math-default-loader` (0.33.0), called only while no loader is registered; a registered loader is never backfilled by it, though a default load already in flight at registration still fills the slot (pre-existing), so register the loader before the first math render. Chunk emission is static, so a bundler still emits that chunk (never requested) unless you alias the module away; see HANDOFF.md "Lazy renderers and eager entries" for the two-line alias. The Mermaid runtime has its own `import("katex")` for `$$` labels, which leaves a second, shared KaTeX chunk in a host build even with the alias; since 0.34.0 a host redirects that one import (for importers inside the `mermaid` package only) to `@plannotator/ui/utils/mermaid-math-slot`, which typesets the labels through your registered renderer, so one KaTeX chunk remains and it is yours. Recipe and measurement in HANDOFF.md, same section. `resetMathRenderer()` empties the slot only and keeps a registered loader (0.34.0); `setMathRendererLoader(null)` is the explicit way back to the package default.
|
|
59
|
-
- **Mermaid.** Without registration, the first diagram on a page fetches the runtime through `import('mermaid')` and the block shows the source fence under a "Rendering diagram" status until the SVG lands; a failed import is dropped from the memo, re-attempted once after a short delay, and the error panel (with the source) offers Retry, which issues another fresh attempt. **Since 0.40.0 (Mermaid 12.0.0, ELK layout by default, Safari 17.4+) this lazy path is Plannotator's own**: the plan editor no longer imports the eager entry, so a document with no diagram never downloads the runtime in a chunked build. `import "@plannotator/ui/utils/mermaid-eager";` in your entry registers and initializes the runtime at startup instead, if you would rather it never fail separately from the app. Mermaid 12 lays flowchart, state, class, ER and requirement diagrams out with ELK (orthogonal edges, different packing); every generated SVG `id` keeps its 11.x shape, but children of `g.edgePaths` are now in declaration order, so select edges by id (`{renderId}-L_{from}_{to}_{n}`, `{renderId}-edge{n}`), never by DOM position. Details in HANDOFF.md "Mermaid 12 (0.40.0)". **Diagrams follow the colour theme.** Before every render `MermaidBlock` calls `applyMermaidTheme` (`utils/mermaidTheme`), which reads the theme tokens off the document (`--background`, `--foreground`, `--card`, `--border`, `--muted`, `--muted-foreground`, `--primary`, the accent tokens and `--font-sans`; `readThemeTokens`), derives a complete `themeVariables` set for every diagram family from them (`buildMermaidThemeVariables(tokens, mode)`, pure; base theme `dark` under a dark resolved mode, `default` under light; every text-on-fill pair guarded to WCAG 4.5:1 and every line 3:1, rule in the module doc) and runs the global `mermaid.initialize` once per `(palette, mode)` key, re-rendering mounted diagrams when the key changes. The key comes from `useTheme()`, so a host that mounts `ThemeProvider` and ships `theme.css` gets diagrams in its palette with nothing to configure. **Fallback contract:** with no tokens on the document (no `ThemeProvider`, no `theme.css`) `readThemeTokens` returns `undefined`, nothing is re-initialized, and the runtime keeps the static `MERMAID_CONFIG` it was initialized with, so such a host renders byte-identically to 0.39.0. `MERMAID_CONFIG` keeps its value and meaning (`securityLevel: 'strict'` pinned); the new exports are additive. Honest limit of any in-page retry: a browser records a failed module fetch in its module map for the page lifetime, so a fresh `import()` of the same chunk URL rejects without a request; the retry recovers failures after the fetch (engine instantiation, initialize) and hosts that version chunk URLs. A host that needs recovery from a failed first fetch uses versioned chunk URLs or a `vite:preloadError` reload at app level.
|
|
59
|
+
- **Mermaid.** Without registration, the first diagram on a page fetches the runtime through `import('mermaid')` and the block shows the source fence under a "Rendering diagram" status until the SVG lands; a failed import is dropped from the memo, re-attempted once after a short delay, and the error panel (with the source) offers Retry, which issues another fresh attempt. **Since 0.40.0 (Mermaid 12.0.0, ELK layout by default, Safari 17.4+) this lazy path is Plannotator's own**: the plan editor no longer imports the eager entry, so a document with no diagram never downloads the runtime in a chunked build. `import "@plannotator/ui/utils/mermaid-eager";` in your entry registers and initializes the runtime at startup instead, if you would rather it never fail separately from the app. Mermaid 12 lays flowchart, state, class, ER and requirement diagrams out with ELK (orthogonal edges, different packing); every generated SVG `id` keeps its 11.x shape, but children of `g.edgePaths` are now in declaration order, so select edges by id (`{renderId}-L_{from}_{to}_{n}`, `{renderId}-edge{n}`), never by DOM position. Details in HANDOFF.md "Mermaid 12 (0.40.0)". **Diagrams follow the colour theme.** Before every render `MermaidBlock` calls `applyMermaidTheme` (`utils/mermaidTheme`), which reads the theme tokens off the document (`--background`, `--foreground`, `--card`, `--border`, `--muted`, `--muted-foreground`, `--primary`, the accent tokens and `--font-sans`; `readThemeTokens`), derives a complete `themeVariables` set for every diagram family from them (`buildMermaidThemeVariables(tokens, mode)`, pure; base theme `dark` under a dark resolved mode, `default` under light; every text-on-fill pair guarded to WCAG 4.5:1 and every line 3:1, rule in the module doc) and runs the global `mermaid.initialize` once per `(palette, mode, shadow amount)` key, re-rendering mounted diagrams when the key changes. The key comes from `useTheme()`, so a host that mounts `ThemeProvider` and ships `theme.css` gets diagrams in its palette with nothing to configure. **The node shadow follows the palette too.** Mermaid 12's neo look shadows every node from a fixed `rgba(185,185,185,1)` grey; the mapping keeps the look and replaces that filter, setting `themeVariables.dropShadow` from the page's own ground at `options.shadowAmount` (0..1, default `DEFAULT_MERMAID_SHADOW_AMOUNT` = **0.7** of Mermaid's geometry; 1 reproduces its `1px 2px 2px` exactly, 0 publishes `dropShadow: false` and the neo rules render `filter: none`). Its polarity follows the page — lighter than the ground on a dark page, darker on a light one, which is what Mermaid's own `insertLookDefs` does and what keeps a shadow visible at all. A host that wants Mermaid's grey passes its own `themeVariables.dropShadow` after ours; one that wants none passes `{ shadowAmount: 0 }`. `mermaidThemeKey(palette, mode, amount?)` appends the amount only when it is not the default, so a host that names none keeps the key it had. **Fallback contract:** with no tokens on the document (no `ThemeProvider`, no `theme.css`) `readThemeTokens` returns `undefined`, nothing is re-initialized, and the runtime keeps the static `MERMAID_CONFIG` it was initialized with, so such a host renders byte-identically to 0.39.0. `MERMAID_CONFIG` keeps its value and meaning (`securityLevel: 'strict'` pinned); the new exports are additive. Honest limit of any in-page retry: a browser records a failed module fetch in its module map for the page lifetime, so a fresh `import()` of the same chunk URL rejects without a request; the retry recovers failures after the fetch (engine instantiation, initialize) and hosts that version chunk URLs. A host that needs recovery from a failed first fetch uses versioned chunk URLs or a `vite:preloadError` reload at app level.
|
|
60
60
|
- **Identity.** With an `identityProvider` the generator is never called and the word lists stay out of your bundle. Without one, default names come from a small built-in pool of the same `adjective-noun-tater` shape; `import "@plannotator/ui/utils/identity-tater";` registers the full dictionary, or pass your own `identityGenerator`.
|
|
61
61
|
|
|
62
62
|
Plannotator's own entries import the eager math and identity modules (`math-eager` and `identity-tater` in both `packages/editor/App.tsx` and `packages/review-editor/App.tsx`), which is what keeps math typeset on the first commit and names minted from the full dictionary; neither app imports `mermaid-eager` since 0.40.0, so the Mermaid runtime rides a lazy chunk on the share portal and is inlined by `inlineDynamicImports` in the single-file builds. `tests/entry-assets.test.ts` fails if an eager math/identity import is dropped or an eager Mermaid import creeps back. See HANDOFF.md "Lazy renderers and eager entries" and "Mermaid 12 (0.40.0)".
|
|
@@ -218,7 +218,7 @@ the in-flow host integration.
|
|
|
218
218
|
|
|
219
219
|
### Diagram engine (`components/diagram`; 0.41.0)
|
|
220
220
|
|
|
221
|
-
One renderer slot and one canvas render every Mermaid and Graphviz diagram — the fences in `Viewer` (`MermaidBlock`, `GraphvizBlock`), their popout, and whatever a host renders itself. `DiagramViewer` from `@plannotator/ui/components/diagram` takes `kind`, `source`, `theme` (`{ colorTheme, mode }`), `comments: DiagramComment[]` and `onCreateComment(anchor, text, additionalTargets)`; pass `onSave(source) => Promise<{ status: 'ok' } | { status: 'stale', currentSource }>` to get the Source pane (left of the canvas on desktop, under it on the phone; `readOnlySource` shows it without Save), and `sourceOpen` to toggle it. Zoom, pan and fit are the canvas's (wheel, drag, `+` `-` `0`, arrow keys); a click opens the composer at the part beneath while a drag pans (4 px threshold, 10 px for a finger), nothing highlights on a plain mouse-over (the ring under the pointer needs the platform modifier held), every edge carries an invisible 14 px hit path in one top layer and a click is resolved by priority over everything under the pointer (node, then edge, then cluster; an edge label is its edge), a click on no part comments on the whole diagram (kind `diagram`), sequence diagrams are addressable (actors, messages, notes, frames), a saved comment paints a ring and a numbered badge from the array order, and every comment re-resolves against each render through the engine's finder (id, then label, then unanchored — reported through `onUnanchoredChange`). The anchor is `DiagramAnchor` from `@plannotator/core/diagram-anchor` (`{ v: 1, family, kind, id | from + to, label, sourceLine }`, document lines), and Plannotator stores it as `Annotation.diagramAnchor`. Runtime slots: `utils/mermaid` (as before) and `utils/graphviz` (new, same shape, `@viz-js/viz` pinned `3.30.0`), each lazy on first use or filled by the host. `DiagramPopout` is the full-size viewer in the `PopoutDialog` chrome. See HANDOFF.md § "Diagram engine (0.41.0)" for every export, the adapter, the sanitizer and the migration notes.
|
|
221
|
+
One renderer slot and one canvas render every Mermaid and Graphviz diagram — the fences in `Viewer` (`MermaidBlock`, `GraphvizBlock`), their popout, and whatever a host renders itself. `DiagramViewer` from `@plannotator/ui/components/diagram` takes `kind`, `source`, `theme` (`{ colorTheme, mode, shadowAmount? }`), `comments: DiagramComment[]` and `onCreateComment(anchor, text, additionalTargets)`; pass `onSave(source) => Promise<{ status: 'ok' } | { status: 'stale', currentSource }>` to get the Source pane (left of the canvas on desktop, under it on the phone; `readOnlySource` shows it without Save), and `sourceOpen` to toggle it. Zoom, pan and fit are the canvas's (wheel, drag, `+` `-` `0`, arrow keys); a click opens the composer at the part beneath while a drag pans (4 px threshold, 10 px for a finger), nothing highlights on a plain mouse-over (the ring under the pointer needs the platform modifier held), every edge carries an invisible 14 px hit path in one top layer and a click is resolved by priority over everything under the pointer (node, then edge, then cluster; an edge label is its edge), a click on no part comments on the whole diagram (kind `diagram`), sequence diagrams are addressable (actors, messages, notes, frames), a saved comment paints a ring and a numbered badge from the array order, and every comment re-resolves against each render through the engine's finder (id, then label, then unanchored — reported through `onUnanchoredChange`). The anchor is `DiagramAnchor` from `@plannotator/core/diagram-anchor` (`{ v: 1, family, kind, id | from + to, label, sourceLine }`, document lines), and Plannotator stores it as `Annotation.diagramAnchor`. Runtime slots: `utils/mermaid` (as before) and `utils/graphviz` (new, same shape, `@viz-js/viz` pinned `3.30.0`), each lazy on first use or filled by the host. `DiagramPopout` is the full-size viewer in the `PopoutDialog` chrome. See HANDOFF.md § "Diagram engine (0.41.0)" for every export, the adapter, the sanitizer and the migration notes.
|
|
222
222
|
|
|
223
223
|
### WebMCP provider (`@plannotator/ui/webmcp`; 0.32.0)
|
|
224
224
|
|
|
@@ -2,7 +2,9 @@ import React, { lazy, Suspense, useCallback, useContext, useEffect, useMemo, use
|
|
|
2
2
|
import { diagramTargetText, type DiagramKind } from '@plannotator/core/diagram-anchor';
|
|
3
3
|
import type { AnnotationRestoreReport } from '../hooks/useAnnotationHighlighter';
|
|
4
4
|
import { AnnotationType, type Annotation, type Block } from '../types';
|
|
5
|
+
import { useConfigValue } from '../config';
|
|
5
6
|
import type { DiagramTheme } from '../utils/diagram-render';
|
|
7
|
+
import { diagramShadowAmount } from '../utils/diagramShadow';
|
|
6
8
|
import { getIdentity } from '../utils/identity';
|
|
7
9
|
import { createRuntimeRetryEpoch } from '../utils/runtimeRetry';
|
|
8
10
|
import { DiagramAnchorClaims, DiagramAnchorClaimsContext } from './diagram/anchorClaims';
|
|
@@ -103,9 +105,17 @@ export const DiagramBlock: React.FC<DiagramBlockProps & { kind: DiagramKind }> =
|
|
|
103
105
|
// provider renders exactly as before. A key change re-runs the render,
|
|
104
106
|
// which is what re-themes an already rendered diagram.
|
|
105
107
|
const { colorTheme, resolvedMode } = useTheme();
|
|
108
|
+
// The node shadow reaches the renderer the same way the palette does: as
|
|
109
|
+
// part of the theme key, so changing it re-initializes Mermaid and re-renders
|
|
110
|
+
// every mounted diagram.
|
|
111
|
+
const diagramShadow = useConfigValue('diagramShadow');
|
|
106
112
|
const theme = useMemo<DiagramTheme>(
|
|
107
|
-
() => ({
|
|
108
|
-
|
|
113
|
+
() => ({
|
|
114
|
+
colorTheme,
|
|
115
|
+
mode: resolvedMode === 'light' ? 'light' : 'dark',
|
|
116
|
+
shadowAmount: diagramShadowAmount(diagramShadow),
|
|
117
|
+
}),
|
|
118
|
+
[colorTheme, resolvedMode, diagramShadow],
|
|
109
119
|
);
|
|
110
120
|
|
|
111
121
|
// A sibling's Retry re-attempts this block too, but only while its own
|
|
@@ -134,19 +144,23 @@ export const DiagramBlock: React.FC<DiagramBlockProps & { kind: DiagramKind }> =
|
|
|
134
144
|
const claims = sharedClaims ?? ownClaims;
|
|
135
145
|
const claimsVersion = useSyncExternalStore(claims.subscribe, claims.getVersion, claims.getVersion);
|
|
136
146
|
|
|
147
|
+
// `diagramAnchor` is read defensively everywhere: a row can reach the
|
|
148
|
+
// renderer from any local ingest (the external-annotations API, a draft, a
|
|
149
|
+
// share link), so a nullish or malformed anchor must list as unanchored,
|
|
150
|
+
// never take the page down with a property read (`null.family`).
|
|
137
151
|
const ownAnnotations = useMemo(
|
|
138
|
-
() => annotations.filter((ann) => ann.diagramAnchor
|
|
152
|
+
() => annotations.filter((ann) => ann.diagramAnchor != null && ann.blockId === block.id),
|
|
139
153
|
[annotations, block.id],
|
|
140
154
|
);
|
|
141
155
|
const unownedAnnotations = useMemo(
|
|
142
156
|
() =>
|
|
143
157
|
annotations.filter(
|
|
144
158
|
(ann) =>
|
|
145
|
-
ann.diagramAnchor
|
|
159
|
+
ann.diagramAnchor != null &&
|
|
146
160
|
ann.blockId !== block.id &&
|
|
147
161
|
!claims.blockIds.includes(ann.blockId) &&
|
|
148
162
|
// A Graphviz anchor names a DOT part; it is never a Mermaid one.
|
|
149
|
-
(ann.diagramAnchor
|
|
163
|
+
(ann.diagramAnchor?.family === 'graphviz') === (kind === 'graphviz'),
|
|
150
164
|
),
|
|
151
165
|
[annotations, block.id, claims, kind],
|
|
152
166
|
);
|
package/components/Settings.tsx
CHANGED
|
@@ -6,6 +6,7 @@ import type { DiffLineBgIntensity } from '@plannotator/core/config-types';
|
|
|
6
6
|
import type { TokenHoverDelay } from '@plannotator/core/token-hover';
|
|
7
7
|
import { configStore, useConfigValue, setReviewPanelView, setReviewDefaultDiffType, setReviewAutoViewed } from '../config';
|
|
8
8
|
import { setWebMcpToolsEnabled, useWebMcpToolsEnabled } from '../webmcp/preference';
|
|
9
|
+
import { DIAGRAM_SHADOW_OPTIONS } from '../utils/diagramShadow';
|
|
9
10
|
import { loadDiffFont } from '../utils/diffFonts';
|
|
10
11
|
import { TaterSpritePullup } from './TaterSpritePullup';
|
|
11
12
|
import { getIdentity, regenerateIdentity, setCustomIdentity, isIdentityEditable } from '../utils/identity';
|
|
@@ -959,6 +960,7 @@ export const Settings: React.FC<SettingsProps> = ({ taterMode, onTaterModeChange
|
|
|
959
960
|
}, [themePreview]);
|
|
960
961
|
const [activeTab, setActiveTab] = useState<SettingsTab>('general');
|
|
961
962
|
const gridEnabled = useConfigValue('gridEnabled');
|
|
963
|
+
const diagramShadow = useConfigValue('diagramShadow');
|
|
962
964
|
const vimModeEnabled = useConfigValue('vimModeEnabled');
|
|
963
965
|
const vimHudEnabled = useConfigValue('vimHudEnabled');
|
|
964
966
|
const vimHudKeyPanelEnabled = useConfigValue('vimHudKeyPanelEnabled');
|
|
@@ -1656,6 +1658,34 @@ export const Settings: React.FC<SettingsProps> = ({ taterMode, onTaterModeChange
|
|
|
1656
1658
|
|
|
1657
1659
|
<div className="border-t border-border" />
|
|
1658
1660
|
|
|
1661
|
+
{/* Diagram Shadow */}
|
|
1662
|
+
<div className="space-y-3" data-diagram-shadow-setting>
|
|
1663
|
+
<div>
|
|
1664
|
+
<div className="text-sm font-medium">Diagram Shadow</div>
|
|
1665
|
+
<div className="text-xs text-muted-foreground">
|
|
1666
|
+
Drop shadow under diagram nodes (100 = Mermaid's own)
|
|
1667
|
+
</div>
|
|
1668
|
+
</div>
|
|
1669
|
+
<div className="flex items-center gap-1 bg-muted/50 rounded-lg p-0.5">
|
|
1670
|
+
{DIAGRAM_SHADOW_OPTIONS.map((value) => (
|
|
1671
|
+
<button
|
|
1672
|
+
key={value}
|
|
1673
|
+
onClick={() => configStore.set('diagramShadow', value)}
|
|
1674
|
+
aria-pressed={diagramShadow === value}
|
|
1675
|
+
className={`flex-1 px-3 py-1.5 text-xs rounded-md transition-colors ${
|
|
1676
|
+
diagramShadow === value
|
|
1677
|
+
? 'bg-background text-foreground shadow-sm font-medium'
|
|
1678
|
+
: 'text-muted-foreground hover:text-foreground'
|
|
1679
|
+
}`}
|
|
1680
|
+
>
|
|
1681
|
+
{value === 0 ? 'None' : String(value)}
|
|
1682
|
+
</button>
|
|
1683
|
+
))}
|
|
1684
|
+
</div>
|
|
1685
|
+
</div>
|
|
1686
|
+
|
|
1687
|
+
<div className="border-t border-border" />
|
|
1688
|
+
|
|
1659
1689
|
{/* Plan Width */}
|
|
1660
1690
|
<div className="space-y-3">
|
|
1661
1691
|
<div>
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
type ThemePair,
|
|
18
18
|
} from '../utils/themeRegistry';
|
|
19
19
|
import type { Mode } from './themeModes';
|
|
20
|
+
import { usePrintMedia } from '../hooks/usePrintMedia';
|
|
20
21
|
|
|
21
22
|
// Kept here because published consumers already import Mode from ThemeProvider.
|
|
22
23
|
export type { Mode } from './themeModes';
|
|
@@ -181,12 +182,37 @@ export function ThemeProvider({
|
|
|
181
182
|
|
|
182
183
|
const [systemIsLight, setSystemIsLight] = useState(getSystemIsLight);
|
|
183
184
|
|
|
185
|
+
// Paper is white: printing renders the LIGHT half of the user's pair, on
|
|
186
|
+
// every surface at once. That is what the print stylesheet has always
|
|
187
|
+
// assumed (white ground, near-black text) and what a dark-palette page could
|
|
188
|
+
// not deliver on its own — a Mermaid label, drawn as HTML inside
|
|
189
|
+
// `<foreignObject>`, took the stylesheet's near-black text onto a near-black
|
|
190
|
+
// node fill and printed illegibly. Light-mode users see no change.
|
|
191
|
+
const printThemeRef = useRef<{ enter: () => void; exit: () => void }>({ enter: () => {}, exit: () => {} });
|
|
192
|
+
const printing = usePrintMedia({
|
|
193
|
+
// Applied from inside `beforeprint`, because the print snapshot is taken
|
|
194
|
+
// before React would flush the state update below. The re-render then
|
|
195
|
+
// applies the same classes, so the two can never disagree.
|
|
196
|
+
onEnter: () => printThemeRef.current.enter(),
|
|
197
|
+
onExit: () => printThemeRef.current.exit(),
|
|
198
|
+
});
|
|
199
|
+
|
|
184
200
|
// Keep the OS-resolved preference separate from the half it selects.
|
|
185
|
-
const
|
|
201
|
+
const screenPreferredMode: 'dark' | 'light' =
|
|
186
202
|
mode === 'system' ? (systemIsLight ? 'light' : 'dark') : mode;
|
|
203
|
+
const preferredMode: 'dark' | 'light' = printing ? 'light' : screenPreferredMode;
|
|
187
204
|
const colorTheme = resolvePairTheme(pair, preferredMode);
|
|
188
205
|
const resolvedMode = resolveThemeMode(colorTheme, preferredMode);
|
|
189
206
|
|
|
207
|
+
const applyHalf = useCallback((half: ThemeHalf) => {
|
|
208
|
+
const theme = resolvePairTheme(configStore.get('themePair'), half);
|
|
209
|
+
applyThemeClasses(theme, resolveThemeMode(theme, half));
|
|
210
|
+
}, []);
|
|
211
|
+
printThemeRef.current = {
|
|
212
|
+
enter: () => applyHalf('light'),
|
|
213
|
+
exit: () => applyHalf(screenPreferredMode),
|
|
214
|
+
};
|
|
215
|
+
|
|
190
216
|
// Read by the legacy setColorTheme, which must target the half on screen
|
|
191
217
|
// without re-creating its callback on every mode change.
|
|
192
218
|
const preferredModeRef = useRef(preferredMode);
|
package/components/Viewer.tsx
CHANGED
|
@@ -1071,7 +1071,10 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
1071
1071
|
// it is unanchored, and the highlighter (which skips it) will not say so.
|
|
1072
1072
|
useEffect(() => {
|
|
1073
1073
|
if (diagramBlockKey !== '' || onRestoreReport === undefined) return;
|
|
1074
|
-
|
|
1074
|
+
// `!= null`, not `!== undefined`: a nullish anchor is no anchor at all —
|
|
1075
|
+
// the highlighter restores such a row by text, so it must not be counted
|
|
1076
|
+
// here as a diagram comment nothing could resolve.
|
|
1077
|
+
const ids = annotations.filter((ann) => ann.diagramAnchor != null).map((ann) => ann.id);
|
|
1075
1078
|
if (ids.length > 0) onRestoreReport({ attempted: ids, unanchored: ids });
|
|
1076
1079
|
}, [annotations, diagramBlockKey, onRestoreReport]);
|
|
1077
1080
|
|
package/config/settings.ts
CHANGED
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
type TokenHoverDelay,
|
|
23
23
|
type TokenHoverTrigger,
|
|
24
24
|
} from '@plannotator/core/token-hover';
|
|
25
|
+
import { DEFAULT_DIAGRAM_SHADOW, isDiagramShadow } from '../utils/diagramShadow';
|
|
25
26
|
import { storage } from '../utils/storage';
|
|
26
27
|
import { generateIdentity } from '../utils/generateIdentity';
|
|
27
28
|
import {
|
|
@@ -170,6 +171,27 @@ export const SETTINGS = {
|
|
|
170
171
|
serverKey: undefined, fromServer: undefined, toServer: undefined,
|
|
171
172
|
},
|
|
172
173
|
|
|
174
|
+
/**
|
|
175
|
+
* How strong the drop shadow under Mermaid diagram nodes is, 0..100, where
|
|
176
|
+
* 100 is Mermaid 12's own default geometry. Default 70: the shipped neo look
|
|
177
|
+
* with its halo toned down (the colour is always derived from the palette,
|
|
178
|
+
* see `utils/mermaidTheme`). Cookie-only, like the other display knobs.
|
|
179
|
+
*/
|
|
180
|
+
diagramShadow: {
|
|
181
|
+
defaultValue: DEFAULT_DIAGRAM_SHADOW as number,
|
|
182
|
+
fromCookie: () => {
|
|
183
|
+
// `Number(null)` and `Number('')` are 0, which is a VALID amount here
|
|
184
|
+
// (unlike the token-hover steps), so an absent cookie must be rejected
|
|
185
|
+
// before the guard sees it — otherwise no cookie reads as "no shadow".
|
|
186
|
+
const raw = storage.getItem('plannotator-diagram-shadow');
|
|
187
|
+
if (raw === null || raw.trim() === '') return undefined;
|
|
188
|
+
const parsed = Number(raw);
|
|
189
|
+
return isDiagramShadow(parsed) ? parsed : undefined;
|
|
190
|
+
},
|
|
191
|
+
toCookie: (value: number) => storage.setItem('plannotator-diagram-shadow', String(value)),
|
|
192
|
+
serverKey: undefined, fromServer: undefined, toServer: undefined,
|
|
193
|
+
},
|
|
194
|
+
|
|
173
195
|
vimModeEnabled: {
|
|
174
196
|
// Vim bindings deliberately default OFF. Unmodified letter keys must remain
|
|
175
197
|
// inert for existing users until they explicitly opt into modal document
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* "Is this document being rendered for paper right now?"
|
|
5
|
+
*
|
|
6
|
+
* Two signals, because neither one alone covers every path:
|
|
7
|
+
*
|
|
8
|
+
* - `beforeprint` / `afterprint` — what a real Cmd+P and the browser's print
|
|
9
|
+
* preview fire. Handlers run BEFORE the print snapshot, so a DOM or class
|
|
10
|
+
* write made from one is in the printed output; a React state update
|
|
11
|
+
* scheduled from one is not guaranteed to be.
|
|
12
|
+
* - `matchMedia('print')` change — what headless print emulation
|
|
13
|
+
* (`page.emulateMedia({ media: 'print' })`) and some preview
|
|
14
|
+
* implementations fire instead; there the page keeps living in print media,
|
|
15
|
+
* so async work (a diagram re-render) does land.
|
|
16
|
+
*
|
|
17
|
+
* Firefox may skip `afterprint` when a preview is dismissed, so a
|
|
18
|
+
* `visibilitychange` back to a visible document also exits print mode.
|
|
19
|
+
*/
|
|
20
|
+
export function subscribePrintMedia(setPrinting: (printing: boolean) => void): () => void {
|
|
21
|
+
if (typeof window === 'undefined') return () => {};
|
|
22
|
+
|
|
23
|
+
const onBeforePrint = () => setPrinting(true);
|
|
24
|
+
const onAfterPrint = () => setPrinting(false);
|
|
25
|
+
const onVisibilityChange = () => {
|
|
26
|
+
if (!document.hidden) setPrinting(false);
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
window.addEventListener('beforeprint', onBeforePrint);
|
|
30
|
+
window.addEventListener('afterprint', onAfterPrint);
|
|
31
|
+
document.addEventListener('visibilitychange', onVisibilityChange);
|
|
32
|
+
|
|
33
|
+
const query = typeof window.matchMedia === 'function' ? window.matchMedia('print') : null;
|
|
34
|
+
const onMediaChange = (event: MediaQueryListEvent) => setPrinting(event.matches);
|
|
35
|
+
query?.addEventListener?.('change', onMediaChange);
|
|
36
|
+
if (query?.matches) setPrinting(true);
|
|
37
|
+
|
|
38
|
+
return () => {
|
|
39
|
+
window.removeEventListener('beforeprint', onBeforePrint);
|
|
40
|
+
window.removeEventListener('afterprint', onAfterPrint);
|
|
41
|
+
document.removeEventListener('visibilitychange', onVisibilityChange);
|
|
42
|
+
query?.removeEventListener?.('change', onMediaChange);
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* React binding over `subscribePrintMedia`. `onEnter` / `onExit` run inside the
|
|
48
|
+
* event handler itself, which is the only place a change is guaranteed to
|
|
49
|
+
* reach a real print snapshot; the returned boolean is the ordinary (async)
|
|
50
|
+
* state for everything that can wait.
|
|
51
|
+
*/
|
|
52
|
+
export function usePrintMedia(callbacks?: { onEnter?: () => void; onExit?: () => void }): boolean {
|
|
53
|
+
const [printing, setPrinting] = useState(false);
|
|
54
|
+
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
let active = false;
|
|
57
|
+
return subscribePrintMedia((next) => {
|
|
58
|
+
if (next === active) return;
|
|
59
|
+
active = next;
|
|
60
|
+
if (next) callbacks?.onEnter?.();
|
|
61
|
+
else callbacks?.onExit?.();
|
|
62
|
+
setPrinting(next);
|
|
63
|
+
});
|
|
64
|
+
// The callbacks are read through the closure on purpose: re-subscribing on
|
|
65
|
+
// every render would drop the listener mid-print.
|
|
66
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
67
|
+
}, []);
|
|
68
|
+
|
|
69
|
+
return printing;
|
|
70
|
+
}
|
package/hooks/usePrintMode.ts
CHANGED
|
@@ -1,26 +1,22 @@
|
|
|
1
1
|
import { useEffect } from 'react';
|
|
2
|
+
import { subscribePrintMedia } from './usePrintMedia';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Manages print mode by toggling 'plannotator-print' class on <html>.
|
|
5
|
-
*
|
|
6
|
-
*
|
|
6
|
+
*
|
|
7
|
+
* Driven by `subscribePrintMedia`, so the class is applied for print media
|
|
8
|
+
* emulation (`page.emulateMedia({ media: 'print' })`, which fires no
|
|
9
|
+
* `beforeprint`) as well as for a real print, and is removed again on
|
|
10
|
+
* `afterprint` or on the visibility change Firefox leaves behind when a
|
|
11
|
+
* preview is dismissed without printing.
|
|
7
12
|
*/
|
|
8
13
|
export function usePrintMode() {
|
|
9
14
|
useEffect(() => {
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
if (!document.hidden && document.documentElement.classList.contains('plannotator-print')) {
|
|
14
|
-
document.documentElement.classList.remove('plannotator-print');
|
|
15
|
-
}
|
|
16
|
-
};
|
|
17
|
-
window.addEventListener('beforeprint', onBeforePrint);
|
|
18
|
-
window.addEventListener('afterprint', onAfterPrint);
|
|
19
|
-
document.addEventListener('visibilitychange', onVisibilityChange);
|
|
15
|
+
const unsubscribe = subscribePrintMedia((printing) => {
|
|
16
|
+
document.documentElement.classList.toggle('plannotator-print', printing);
|
|
17
|
+
});
|
|
20
18
|
return () => {
|
|
21
|
-
|
|
22
|
-
window.removeEventListener('afterprint', onAfterPrint);
|
|
23
|
-
document.removeEventListener('visibilitychange', onVisibilityChange);
|
|
19
|
+
unsubscribe();
|
|
24
20
|
document.documentElement.classList.remove('plannotator-print');
|
|
25
21
|
};
|
|
26
22
|
}, []);
|
package/package.json
CHANGED
package/print.css
CHANGED
|
@@ -177,16 +177,39 @@
|
|
|
177
177
|
|
|
178
178
|
/* ========== Typography ========== */
|
|
179
179
|
|
|
180
|
-
|
|
180
|
+
/*
|
|
181
|
+
* `:not([data-diagram-block] *)` keeps every typography rule off DIAGRAM
|
|
182
|
+
* content. Mermaid 12 draws node and edge labels as real HTML inside
|
|
183
|
+
* `<foreignObject>` (`span.nodeLabel > p`, `span.edgeLabel`), so a blanket
|
|
184
|
+
* `div, span, p { color: #1a1a1a !important }` repainted them — over a dark
|
|
185
|
+
* node fill that prints as an empty box. A diagram colours itself (and
|
|
186
|
+
* prints in the light half of the palette, see ThemeProvider), and its own
|
|
187
|
+
* `<style>` has no `!important` to defend itself with, so the exemption has
|
|
188
|
+
* to live here.
|
|
189
|
+
*/
|
|
190
|
+
body,
|
|
191
|
+
div:not([data-diagram-block] *),
|
|
192
|
+
span:not([data-diagram-block] *),
|
|
193
|
+
p:not([data-diagram-block] *),
|
|
194
|
+
li:not([data-diagram-block] *),
|
|
195
|
+
td:not([data-diagram-block] *),
|
|
196
|
+
th:not([data-diagram-block] *),
|
|
197
|
+
label:not([data-diagram-block] *),
|
|
198
|
+
strong:not([data-diagram-block] *),
|
|
199
|
+
em:not([data-diagram-block] *),
|
|
200
|
+
b:not([data-diagram-block] *),
|
|
201
|
+
i:not([data-diagram-block] *) {
|
|
181
202
|
color: #1a1a1a !important;
|
|
182
203
|
}
|
|
183
204
|
|
|
184
|
-
strong,
|
|
205
|
+
strong:not([data-diagram-block] *),
|
|
206
|
+
b:not([data-diagram-block] *) {
|
|
185
207
|
font-weight: bold !important;
|
|
186
208
|
color: #000 !important;
|
|
187
209
|
}
|
|
188
210
|
|
|
189
|
-
em,
|
|
211
|
+
em:not([data-diagram-block] *),
|
|
212
|
+
i:not([data-diagram-block] *) {
|
|
190
213
|
font-style: italic !important;
|
|
191
214
|
color: #1a1a1a !important;
|
|
192
215
|
}
|
|
@@ -231,7 +254,7 @@
|
|
|
231
254
|
color: #000 !important;
|
|
232
255
|
}
|
|
233
256
|
|
|
234
|
-
p {
|
|
257
|
+
p:not([data-diagram-block] *) {
|
|
235
258
|
margin: 0.5em 0;
|
|
236
259
|
color: #1a1a1a !important;
|
|
237
260
|
}
|