@plannotator/ui 0.41.1 → 0.42.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 +10 -2
- package/README.md +2 -2
- package/components/DiagramBlock.tsx +22 -6
- 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/useLinkedDoc.ts +11 -6
- package/hooks/usePrintMedia.ts +70 -0
- package/hooks/usePrintMode.ts +11 -15
- package/package.json +2 -2
- package/print.css +27 -4
- package/styles.css +1 -1
- package/types.ts +20 -0
- 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/utils/parser.ts +33 -0
- package/utils/sharing.ts +28 -1
package/types.ts
CHANGED
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
import type { DiagramAnchor } from '@plannotator/core/diagram-anchor';
|
|
2
|
+
import type { DiagramRenderKind } from '@plannotator/core/annotatable';
|
|
2
3
|
|
|
3
4
|
export type { DiagramAnchor } from '@plannotator/core/diagram-anchor';
|
|
5
|
+
export type { DiagramRenderKind } from '@plannotator/core/annotatable';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* How a document's body is rendered. `markdown` and `html` are the original
|
|
9
|
+
* pair; the two diagram kinds are whole-file diagram sources (.mmd/.mermaid,
|
|
10
|
+
* .dot/.gv) that render as ONE diagram through the same engine a ```mermaid
|
|
11
|
+
* fence uses — see diagramDocumentBlocks in utils/parser.
|
|
12
|
+
*/
|
|
13
|
+
export type DocumentRenderAs = 'markdown' | 'html' | DiagramRenderKind;
|
|
4
14
|
|
|
5
15
|
export enum AnnotationType {
|
|
6
16
|
DELETION = 'DELETION',
|
|
@@ -192,6 +202,16 @@ export interface Block {
|
|
|
192
202
|
order: number; // Sorting order
|
|
193
203
|
startLine: number; // 1-based line number in source
|
|
194
204
|
sourceLineCount?: number; // Number of source lines consumed when it differs from content lines
|
|
205
|
+
/**
|
|
206
|
+
* Line offset a diagram comment's `sourceLine` is measured from, when it
|
|
207
|
+
* differs from `startLine`. A ```mermaid fence in a document has its opening
|
|
208
|
+
* line ABOVE the diagram's first line, so `startLine` is the right offset
|
|
209
|
+
* there and this stays unset. A whole-file diagram source (.mmd/.dot) has no
|
|
210
|
+
* fence: its first line IS document line 1, so it sets 0 here while
|
|
211
|
+
* `startLine` keeps naming the block's own first line for the export's
|
|
212
|
+
* `(lines a–b)` label.
|
|
213
|
+
*/
|
|
214
|
+
diagramSourceLineOffset?: number;
|
|
195
215
|
}
|
|
196
216
|
|
|
197
217
|
export interface DiffResult {
|
package/utils/diagram-render.ts
CHANGED
|
@@ -48,14 +48,21 @@ type Mermaid = Awaited<ReturnType<typeof loadMermaidRuntime>>;
|
|
|
48
48
|
|
|
49
49
|
/**
|
|
50
50
|
* The (palette, mode) a render is for: the same pair `useTheme()` resolves
|
|
51
|
-
* for code fences
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
* passes any palette id with the
|
|
51
|
+
* for code fences, plus the node shadow amount the user chose. The mermaid
|
|
52
|
+
* entry passes them to `applyMermaidTheme`, which runs the global `initialize`
|
|
53
|
+
* once per key; the graphviz entry recolors its defaults onto CSS tokens and
|
|
54
|
+
* needs neither. A host without ThemeProvider passes any palette id with the
|
|
55
|
+
* mode it renders in.
|
|
55
56
|
*/
|
|
56
57
|
export interface DiagramTheme {
|
|
57
58
|
readonly colorTheme: string;
|
|
58
59
|
readonly mode: MermaidThemeMode;
|
|
60
|
+
/**
|
|
61
|
+
* Node drop shadow strength, 0..1 (see `mermaidTheme.buildMermaidShadow`).
|
|
62
|
+
* Absent means the shipped default, so a host that builds its own
|
|
63
|
+
* `DiagramTheme` renders exactly what Plannotator renders.
|
|
64
|
+
*/
|
|
65
|
+
readonly shadowAmount?: number;
|
|
59
66
|
}
|
|
60
67
|
|
|
61
68
|
export type DiagramRenderError = {
|
|
@@ -514,11 +521,11 @@ const mermaidRenderer: DiagramRenderer = {
|
|
|
514
521
|
}
|
|
515
522
|
// Derive every Mermaid theme variable from the page's tokens before each
|
|
516
523
|
// render, so the diagram follows the palette and mode. Keyed on the
|
|
517
|
-
// runtime plus (palette, mode), so the lazy runtime is
|
|
518
|
-
// first render and re-initialized only when one
|
|
524
|
+
// runtime plus (palette, mode, shadow amount), so the lazy runtime is
|
|
525
|
+
// themed on its first render and re-initialized only when one changes.
|
|
519
526
|
// With no tokens on the page it is a no-op and the static
|
|
520
527
|
// MERMAID_CONFIG (securityLevel strict) applies.
|
|
521
|
-
applyMermaidTheme(runtime, mermaidThemeKey(theme.colorTheme, theme.mode));
|
|
528
|
+
applyMermaidTheme(runtime, mermaidThemeKey(theme.colorTheme, theme.mode, theme.shadowAmount));
|
|
522
529
|
return renderMermaid(runtime, renderId, source);
|
|
523
530
|
},
|
|
524
531
|
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The user-facing scale for the Mermaid node drop shadow: whole percent, 0..100,
|
|
3
|
+
* where 100 is Mermaid 12's own default shadow and 0 is none.
|
|
4
|
+
*
|
|
5
|
+
* Deliberately free of imports so the settings registry can read it on every
|
|
6
|
+
* surface (the guides.show viewer included) without pulling the diagram theme
|
|
7
|
+
* mapping — and the runtime into which it feeds — into that module graph.
|
|
8
|
+
* `mermaidTheme.DEFAULT_MERMAID_SHADOW_AMOUNT` is the same value in the 0..1
|
|
9
|
+
* unit the mapping takes; `mermaidTheme.test.ts` pins the two together.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/** The steps Settings offers. A slider would imply a precision nobody can see. */
|
|
13
|
+
export const DIAGRAM_SHADOW_OPTIONS = [0, 40, 70, 100] as const;
|
|
14
|
+
|
|
15
|
+
/** Shipped default: the neo look with its grey halo toned down. */
|
|
16
|
+
export const DEFAULT_DIAGRAM_SHADOW = 70;
|
|
17
|
+
|
|
18
|
+
/** Any whole percent in range is valid, not only the offered steps. */
|
|
19
|
+
export function isDiagramShadow(value: unknown): value is number {
|
|
20
|
+
return typeof value === 'number' && Number.isInteger(value) && value >= 0 && value <= 100;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** The 0..1 amount `buildMermaidThemeVariables` takes. */
|
|
24
|
+
export function diagramShadowAmount(percent: number): number {
|
|
25
|
+
if (!Number.isFinite(percent)) return DEFAULT_DIAGRAM_SHADOW / 100;
|
|
26
|
+
return Math.min(100, Math.max(0, percent)) / 100;
|
|
27
|
+
}
|
package/utils/mermaid.ts
CHANGED
|
@@ -45,6 +45,18 @@ export const MERMAID_CONFIG: MermaidConfig = {
|
|
|
45
45
|
clusterBorder: '#475569',
|
|
46
46
|
titleColor: '#f8fafc',
|
|
47
47
|
edgeLabelBackground: '#1e293b',
|
|
48
|
+
/**
|
|
49
|
+
* Mermaid 12's neo look shadows every node from a fixed
|
|
50
|
+
* `drop-shadow(1px 2px 2px rgba(185,185,185,1))` grey, which reads as a
|
|
51
|
+
* halo. The token-driven mapping replaces it per palette
|
|
52
|
+
* (`buildMermaidShadow` in `./mermaidTheme`); this static config is what a
|
|
53
|
+
* host with no theme tokens renders with, so it carries the same
|
|
54
|
+
* toned-down 0.7 geometry with a fixed colour. The value is what
|
|
55
|
+
* `buildMermaidShadow(#1e293b, 0.7)` returns for THIS config's own slate
|
|
56
|
+
* ground, which is dark — hence a light shadow, exactly as Mermaid's own
|
|
57
|
+
* `insertLookDefs` uses a white flood colour on dark themes.
|
|
58
|
+
*/
|
|
59
|
+
dropShadow: 'drop-shadow(0.79px 1.58px 1.58px rgba(210, 212, 217, 0.684))',
|
|
48
60
|
},
|
|
49
61
|
flowchart: {
|
|
50
62
|
htmlLabels: true,
|
package/utils/mermaidTheme.ts
CHANGED
|
@@ -24,7 +24,18 @@
|
|
|
24
24
|
* Mermaid's colour library does not parse `oklch()`.
|
|
25
25
|
* 3. `applyMermaidTheme(mermaid, key)` is the runtime step `MermaidBlock`
|
|
26
26
|
* calls before every render: `mermaid.initialize` is global state, so it
|
|
27
|
-
* runs only when the `(palette, mode)` key changed since the
|
|
27
|
+
* runs only when the `(palette, mode, shadow amount)` key changed since the
|
|
28
|
+
* last apply.
|
|
29
|
+
*
|
|
30
|
+
* Node shadow: Mermaid 12's neo look paints a drop shadow on every node,
|
|
31
|
+
* cluster and actor, from its own fixed `rgba(185,185,185,1)` grey at
|
|
32
|
+
* `1px 2px 2px`. The mapping keeps the look and replaces that one filter:
|
|
33
|
+
* `themeVariables.dropShadow` is built from the palette's own ground at
|
|
34
|
+
* `DEFAULT_MERMAID_SHADOW_AMOUNT` (0.7 of Mermaid's geometry), in the polarity
|
|
35
|
+
* the page reads in — see `buildMermaidShadow`. A host that wants Mermaid's
|
|
36
|
+
* grey back sets its own `themeVariables.dropShadow` after ours; a host that
|
|
37
|
+
* wants none passes `{ shadowAmount: 0 }`, which publishes `dropShadow: false`
|
|
38
|
+
* and the neo rules render `filter: none`.
|
|
28
39
|
*
|
|
29
40
|
* Fallback contract (hosts): when no tokens resolve, the runtime keeps the
|
|
30
41
|
* static `MERMAID_CONFIG` it was initialized with, and nothing is
|
|
@@ -106,6 +117,35 @@ export type MermaidThemeTokens = Partial<Record<MermaidThemeTokenName, string>>;
|
|
|
106
117
|
export interface MermaidThemeSpec {
|
|
107
118
|
theme: 'dark' | 'default';
|
|
108
119
|
themeVariables: Record<string, unknown>;
|
|
120
|
+
/** The node shadow amount baked into `themeVariables.dropShadow` (0..1). */
|
|
121
|
+
shadowAmount: number;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Options `buildMermaidThemeVariables` accepts beyond the tokens and mode. */
|
|
125
|
+
export interface MermaidThemeOptions {
|
|
126
|
+
/**
|
|
127
|
+
* How much node drop shadow to paint, 0..1, where 1 reproduces Mermaid's
|
|
128
|
+
* own default geometry (`1px 2px 2px`) and 0 means no shadow at all.
|
|
129
|
+
* Default: `DEFAULT_MERMAID_SHADOW_AMOUNT`.
|
|
130
|
+
*/
|
|
131
|
+
readonly shadowAmount?: number;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* The shipped shadow amount: Mermaid 12's neo look at 70% of its own shadow.
|
|
136
|
+
* The full-strength default reads as a grey halo around every node on a
|
|
137
|
+
* Plannotator page; 70 keeps the lift and drops the halo.
|
|
138
|
+
*/
|
|
139
|
+
export const DEFAULT_MERMAID_SHADOW_AMOUNT = 0.7;
|
|
140
|
+
|
|
141
|
+
/** Mermaid 12's own node shadow, the colour included. Amount 1 reproduces
|
|
142
|
+
* this geometry; the colour is ours unless the palette yields nothing. */
|
|
143
|
+
export const MERMAID_DEFAULT_SHADOW_COLOR = 'rgba(185, 185, 185, 1)';
|
|
144
|
+
|
|
145
|
+
/** Clamp an amount to 0..1 and round it, so it is stable in a cache key. */
|
|
146
|
+
export function clampShadowAmount(amount: number | undefined): number {
|
|
147
|
+
if (typeof amount !== 'number' || !Number.isFinite(amount)) return DEFAULT_MERMAID_SHADOW_AMOUNT;
|
|
148
|
+
return Math.round(Math.min(1, Math.max(0, amount)) * 1000) / 1000;
|
|
109
149
|
}
|
|
110
150
|
|
|
111
151
|
/** WCAG minimums the guard enforces. */
|
|
@@ -317,6 +357,62 @@ export function isDarkBackground(background: RgbColor): boolean {
|
|
|
317
357
|
return relativeLuminance(background) < 0.179;
|
|
318
358
|
}
|
|
319
359
|
|
|
360
|
+
/** `rgba()` from an RgbColor plus an explicit alpha. */
|
|
361
|
+
function rgba(c: RgbColor, alpha: number): string {
|
|
362
|
+
const ch = (v: number) => Math.round(Math.max(0, Math.min(1, v)) * 255);
|
|
363
|
+
return `rgba(${ch(c.r)}, ${ch(c.g)}, ${ch(c.b)}, ${Math.round(alpha * 1000) / 1000})`;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* The node drop shadow, as the `drop-shadow(x y blur colour)` string Mermaid's
|
|
368
|
+
* `dropShadow` theme variable takes, or `false` — its falsy value, which every
|
|
369
|
+
* `[data-look="neo"] … { filter: … }` rule turns into `filter: none`.
|
|
370
|
+
*
|
|
371
|
+
* Geometry scales with the amount over a small floor, so a toned-down shadow
|
|
372
|
+
* still reads as a lift rather than vanishing: at 1 it is exactly Mermaid's own
|
|
373
|
+
* `1px 2px 2px`, at 0 there is no shadow at all.
|
|
374
|
+
*
|
|
375
|
+
* The COLOUR comes from the palette rather than Mermaid's fixed
|
|
376
|
+
* `rgba(185,185,185,1)` grey — that grey is the halo that reads as wrong on a
|
|
377
|
+
* themed page — and its POLARITY follows the page, exactly as Mermaid's own
|
|
378
|
+
* `insertLookDefs` does (`floodColor = theme.includes('dark') ? '#FFFFFF' :
|
|
379
|
+
* '#000000'`): on a dark page a shadow's job is to LIFT the node off the
|
|
380
|
+
* ground, and a black shadow on a near-black ground paints nothing the eye can
|
|
381
|
+
* see. What is kept from the palette is the tint and the strength, not the
|
|
382
|
+
* direction:
|
|
383
|
+
* - dark page: the ground lifted 82% toward white, alpha 0.18 -> 0.90;
|
|
384
|
+
* - light page: the ground darkened 40% toward black, alpha 0.25 -> 0.55.
|
|
385
|
+
* Each is then asserted to differ from the ground in the READABLE direction,
|
|
386
|
+
* falling back to plain white or black, so no palette can produce an invisible
|
|
387
|
+
* shadow; a ground that is not a usable colour at all falls back to Mermaid's
|
|
388
|
+
* own grey.
|
|
389
|
+
*
|
|
390
|
+
* Pure. `ground` is the page's opaque background.
|
|
391
|
+
*/
|
|
392
|
+
export function buildMermaidShadow(ground: RgbColor | undefined, amount: number): string | false {
|
|
393
|
+
const a = clampShadowAmount(amount);
|
|
394
|
+
if (a <= 0) return false;
|
|
395
|
+
const round = (v: number) => Math.round(v * 100) / 100;
|
|
396
|
+
const geometry = `${round(0.3 + 0.7 * a)}px ${round(0.6 + 1.4 * a)}px ${round(0.6 + 1.4 * a)}px`;
|
|
397
|
+
const usable =
|
|
398
|
+
ground !== undefined &&
|
|
399
|
+
Number.isFinite(ground.r) &&
|
|
400
|
+
Number.isFinite(ground.g) &&
|
|
401
|
+
Number.isFinite(ground.b);
|
|
402
|
+
// Nothing usable from the palette: Mermaid's own colour at our geometry.
|
|
403
|
+
if (!usable) return `drop-shadow(${geometry} ${MERMAID_DEFAULT_SHADOW_COLOR})`;
|
|
404
|
+
const dark = isDarkBackground(ground);
|
|
405
|
+
let colour = dark ? mixOklab(ground, WHITE, 0.82) : mixOklab(ground, BLACK, 0.4);
|
|
406
|
+
// The dark-page top end is calibrated so amount 1 reads as strongly as
|
|
407
|
+
// Mermaid's own `rgba(185,185,185,1)` over the same ground — 1 is defined as
|
|
408
|
+
// "Mermaid's default", so it has to actually look like it.
|
|
409
|
+
const alpha = dark ? 0.18 + 0.72 * a : 0.25 + 0.3 * a;
|
|
410
|
+
const groundL = relativeLuminance(ground);
|
|
411
|
+
if (dark && relativeLuminance(colour) <= groundL) colour = WHITE;
|
|
412
|
+
if (!dark && relativeLuminance(colour) >= groundL) colour = BLACK;
|
|
413
|
+
return `drop-shadow(${geometry} ${rgba(colour, alpha)})`;
|
|
414
|
+
}
|
|
415
|
+
|
|
320
416
|
/**
|
|
321
417
|
* Push a fill's lightness away from `ink` until `ink` reads on it at 4.5:1.
|
|
322
418
|
* Used for the categorical scale, whose single ink is fixed per mode.
|
|
@@ -376,10 +472,15 @@ export function buildCategoricalScale(p: Palette, polarity: MermaidThemeMode): R
|
|
|
376
472
|
* the tokens. Returns `null` when the required tokens are missing or
|
|
377
473
|
* unparsable, which callers treat as "use the static config".
|
|
378
474
|
*/
|
|
379
|
-
export function buildMermaidThemeVariables(
|
|
475
|
+
export function buildMermaidThemeVariables(
|
|
476
|
+
tokens: MermaidThemeTokens | undefined,
|
|
477
|
+
mode: MermaidThemeMode,
|
|
478
|
+
options?: MermaidThemeOptions,
|
|
479
|
+
): MermaidThemeSpec | null {
|
|
380
480
|
if (!tokens) return null;
|
|
381
481
|
const p = resolvePalette(tokens);
|
|
382
482
|
if (!p) return null;
|
|
483
|
+
const shadowAmount = clampShadowAmount(options?.shadowAmount);
|
|
383
484
|
|
|
384
485
|
// Base theme follows the mode; fill lightness and ink follow the measured page.
|
|
385
486
|
const polarity: MermaidThemeMode = isDarkBackground(p.background) ? 'dark' : 'light';
|
|
@@ -428,6 +529,9 @@ export function buildMermaidThemeVariables(tokens: MermaidThemeTokens | undefine
|
|
|
428
529
|
const scaleLabel = scale.map((c) => toHex(text(scaleInk, c)));
|
|
429
530
|
|
|
430
531
|
const h = toHex;
|
|
532
|
+
// The shadow nodes, clusters and actors are painted with. Derived from the
|
|
533
|
+
// page's own ground, never Mermaid's fixed grey (see buildMermaidShadow).
|
|
534
|
+
const shadow = buildMermaidShadow(p.background, shadowAmount);
|
|
431
535
|
const vars: Record<string, unknown> = {
|
|
432
536
|
darkMode: mode === 'dark',
|
|
433
537
|
...(p.fontFamily ? { fontFamily: p.fontFamily } : {}),
|
|
@@ -459,6 +563,13 @@ export function buildMermaidThemeVariables(tokens: MermaidThemeTokens | undefine
|
|
|
459
563
|
errorBkgColor: h(errorBg),
|
|
460
564
|
errorTextColor: h(errorText),
|
|
461
565
|
useGradient: false,
|
|
566
|
+
// Paint-time only: a filter on the shape, never a fill or a label colour.
|
|
567
|
+
dropShadow: shadow,
|
|
568
|
+
// `nodeShadow` gates ONE thing in Mermaid 12: the inline
|
|
569
|
+
// `filter:url(#…-drop-shadow-small)` a state diagram's small start / end
|
|
570
|
+
// dots carry, which no theme variable string reaches. Off at amount 0 so
|
|
571
|
+
// "no shadow" is true of every element.
|
|
572
|
+
...(shadow === false ? { nodeShadow: false } : {}),
|
|
462
573
|
|
|
463
574
|
// Flowchart
|
|
464
575
|
nodeBkg: h(p.card),
|
|
@@ -669,7 +780,7 @@ export function buildMermaidThemeVariables(tokens: MermaidThemeTokens | undefine
|
|
|
669
780
|
vars[`fillType${i}`] = scaleHex[i];
|
|
670
781
|
vars[`venn${i + 1}`] = scaleHex[i];
|
|
671
782
|
}
|
|
672
|
-
return { theme: mode === 'dark' ? 'dark' : 'default', themeVariables: vars };
|
|
783
|
+
return { theme: mode === 'dark' ? 'dark' : 'default', themeVariables: vars, shadowAmount };
|
|
673
784
|
}
|
|
674
785
|
|
|
675
786
|
/** The full Mermaid config for a spec: `MERMAID_CONFIG` with the theme swapped. */
|
|
@@ -682,15 +793,33 @@ export function buildMermaidConfig(spec: MermaidThemeSpec | null): MermaidConfig
|
|
|
682
793
|
// Runtime application (cached by key)
|
|
683
794
|
// ---------------------------------------------------------------------------
|
|
684
795
|
|
|
685
|
-
/**
|
|
686
|
-
|
|
687
|
-
|
|
796
|
+
/**
|
|
797
|
+
* `mode:palette`, the cache key `applyMermaidTheme` compares — plus a
|
|
798
|
+
* `#s<amount>` suffix when the shadow amount is not the shipped default, so a
|
|
799
|
+
* changed amount re-initializes the runtime. The default amount keeps the key
|
|
800
|
+
* byte-identical to the `(palette, mode)` pair it always was, which is also
|
|
801
|
+
* what keeps a host that never names an amount on the same key it had.
|
|
802
|
+
*/
|
|
803
|
+
export function mermaidThemeKey(
|
|
804
|
+
colorTheme: string,
|
|
805
|
+
mode: MermaidThemeMode,
|
|
806
|
+
shadowAmount: number = DEFAULT_MERMAID_SHADOW_AMOUNT,
|
|
807
|
+
): string {
|
|
808
|
+
const amount = clampShadowAmount(shadowAmount);
|
|
809
|
+
const base = `${mode}:${colorTheme}`;
|
|
810
|
+
return amount === DEFAULT_MERMAID_SHADOW_AMOUNT ? base : `${base}#s${amount}`;
|
|
688
811
|
}
|
|
689
812
|
|
|
690
813
|
function modeFromKey(key: string): MermaidThemeMode {
|
|
691
814
|
return key.startsWith('light:') ? 'light' : 'dark';
|
|
692
815
|
}
|
|
693
816
|
|
|
817
|
+
/** The amount a key carries, the default when it carries none. */
|
|
818
|
+
export function shadowAmountFromKey(key: string): number {
|
|
819
|
+
const match = /#s(\d*\.?\d+)$/u.exec(key);
|
|
820
|
+
return match ? clampShadowAmount(Number(match[1])) : DEFAULT_MERMAID_SHADOW_AMOUNT;
|
|
821
|
+
}
|
|
822
|
+
|
|
694
823
|
export type MermaidThemeApplyResult = 'unchanged' | 'dynamic' | 'static';
|
|
695
824
|
|
|
696
825
|
let appliedRuntime: Pick<Mermaid, 'initialize'> | null = null;
|
|
@@ -710,7 +839,9 @@ export function applyMermaidTheme(
|
|
|
710
839
|
root?: Element | null,
|
|
711
840
|
): MermaidThemeApplyResult {
|
|
712
841
|
if (appliedRuntime === mermaid && appliedKey === key) return 'unchanged';
|
|
713
|
-
const spec = buildMermaidThemeVariables(readThemeTokens(root), modeFromKey(key)
|
|
842
|
+
const spec = buildMermaidThemeVariables(readThemeTokens(root), modeFromKey(key), {
|
|
843
|
+
shadowAmount: shadowAmountFromKey(key),
|
|
844
|
+
});
|
|
714
845
|
const sameRuntime = appliedRuntime === mermaid;
|
|
715
846
|
appliedRuntime = mermaid;
|
|
716
847
|
appliedKey = key;
|
package/utils/parser.ts
CHANGED
|
@@ -541,6 +541,39 @@ export const resolveReferenceLinks = (markdown: string): string => {
|
|
|
541
541
|
.join('\n');
|
|
542
542
|
};
|
|
543
543
|
|
|
544
|
+
/**
|
|
545
|
+
* The block list for a whole-file diagram source (`plannotator annotate
|
|
546
|
+
* flow.mmd`): ONE code block carrying the file's raw text, which `Viewer`
|
|
547
|
+
* hands to the same `DiagramBlock` a ```mermaid fence in a plan produces.
|
|
548
|
+
* Everything downstream — diagram comments, the annotations rail, the export's
|
|
549
|
+
* `Diagram node <label> (<id>), line <n>` location line, drafts, restore — is
|
|
550
|
+
* the fence path unchanged.
|
|
551
|
+
*
|
|
552
|
+
* `diagramSourceLineOffset: 0` is the load-bearing part. `DiagramBlock` passes
|
|
553
|
+
* it as the viewer's `sourceLineOffset` and the codec adds it to the 1-based
|
|
554
|
+
* line WITHIN the diagram source; for a fence that offset is the fence's own
|
|
555
|
+
* opening line, which sits one line above the diagram's first line. A diagram
|
|
556
|
+
* FILE has no fence, so its first line is document line 1 and the offset is 0
|
|
557
|
+
* — the 1 a synthesized ```mermaid wrapper would produce puts every exported
|
|
558
|
+
* diagram line one too high. `startLine`/`sourceLineCount` still describe the
|
|
559
|
+
* block itself, so the export's `(lines a–b)` label names the file's real
|
|
560
|
+
* span.
|
|
561
|
+
*/
|
|
562
|
+
export const diagramDocumentBlocks = (text: string, kind: 'mermaid' | 'graphviz'): Block[] => [
|
|
563
|
+
{
|
|
564
|
+
id: 'block-0',
|
|
565
|
+
type: 'code',
|
|
566
|
+
content: text,
|
|
567
|
+
// `dot` is what isGraphvizLanguage reads for the Graphviz engine.
|
|
568
|
+
language: kind === 'graphviz' ? 'dot' : 'mermaid',
|
|
569
|
+
order: 1,
|
|
570
|
+
startLine: 1,
|
|
571
|
+
// A trailing newline ends the last line, it does not start another.
|
|
572
|
+
sourceLineCount: text === '' ? 0 : text.replace(/\n$/, '').split('\n').length,
|
|
573
|
+
diagramSourceLineOffset: 0,
|
|
574
|
+
},
|
|
575
|
+
];
|
|
576
|
+
|
|
544
577
|
/**
|
|
545
578
|
* A simplified markdown parser that splits content into linear blocks.
|
|
546
579
|
* For a production app, we would use a robust AST walker (remark),
|
package/utils/sharing.ts
CHANGED
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
* Inspired by textarea.my's approach.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import { AnnotationType, type Annotation, type ImageAttachment } from '../types';
|
|
11
|
+
import { AnnotationType, type Annotation, type DocumentRenderAs, type ImageAttachment } from '../types';
|
|
12
|
+
import { isDiagramRenderKind } from '@plannotator/core/annotatable';
|
|
12
13
|
import { compress, decompress } from '@plannotator/core/compress';
|
|
13
14
|
import { encrypt, decrypt } from '@plannotator/core/crypto';
|
|
14
15
|
|
|
@@ -31,6 +32,32 @@ export interface SharePayload {
|
|
|
31
32
|
r?: 'html'; // render mode flag (omitted = markdown)
|
|
32
33
|
}
|
|
33
34
|
|
|
35
|
+
/**
|
|
36
|
+
* The markdown a document ships as in a share payload's `p`.
|
|
37
|
+
*
|
|
38
|
+
* A whole-file diagram source (.mmd/.dot) has no fence of its own — the
|
|
39
|
+
* annotate session renders it as one diagram because the SERVER said so in
|
|
40
|
+
* `renderAs`, and a share link carries no server. So it travels as the fenced
|
|
41
|
+
* form, which the portal's ordinary markdown parse turns back into the same
|
|
42
|
+
* diagram block. This is deliberately smaller than adding a render-mode flag
|
|
43
|
+
* (`r: 'html'`'s sibling): the portal needs no change at all.
|
|
44
|
+
*
|
|
45
|
+
* Diagram comments themselves still degrade to text comments in a share link,
|
|
46
|
+
* exactly as they do today — `diagramAnchor` is dropped like `htmlAnchor`
|
|
47
|
+
* (see sharing.multiTarget.test.ts).
|
|
48
|
+
*/
|
|
49
|
+
export function shareableDocumentMarkdown(
|
|
50
|
+
markdown: string,
|
|
51
|
+
renderAs: DocumentRenderAs | undefined,
|
|
52
|
+
): string {
|
|
53
|
+
if (!isDiagramRenderKind(renderAs) || markdown === '') return markdown;
|
|
54
|
+
const language = renderAs === 'graphviz' ? 'dot' : 'mermaid';
|
|
55
|
+
// A diagram source can itself contain a ``` run only in a comment/label;
|
|
56
|
+
// a four-backtick fence keeps such a body intact.
|
|
57
|
+
const fence = markdown.includes('```') ? '````' : '```';
|
|
58
|
+
return `${fence}${language}\n${markdown.replace(/\n+$/, '')}\n${fence}`;
|
|
59
|
+
}
|
|
60
|
+
|
|
34
61
|
/**
|
|
35
62
|
* Convert ShareableImage[] to ImageAttachment[] (handles old plain-string format)
|
|
36
63
|
*/
|