@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/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;
|