@ripple-ui/svelte 0.8.2 → 0.9.0-next.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/dist/discover/CreatorAvatar.svelte +37 -0
- package/dist/discover/CreatorAvatar.svelte.d.ts +10 -0
- package/dist/discover/CreatorLine.svelte +18 -0
- package/dist/discover/CreatorLine.svelte.d.ts +8 -0
- package/dist/discover/DiscoverChips.svelte +15 -2
- package/dist/discover/DiscoverFilters.svelte +56 -41
- package/dist/discover/DiscoverHeader.svelte +33 -10
- package/dist/discover/DiscoverHeader.svelte.d.ts +3 -0
- package/dist/discover/DiscoverRow.svelte +36 -10
- package/dist/discover/DiscoverRow.svelte.d.ts +7 -0
- package/dist/discover/DiscoverSection.svelte +18 -10
- package/dist/discover/DiscoverSkeleton.svelte +5 -3
- package/dist/discover/DiscoverTile.svelte +52 -17
- package/dist/discover/DiscoverTile.svelte.d.ts +7 -0
- package/dist/discover/ItemActions.svelte +46 -17
- package/dist/discover/ItemActions.svelte.d.ts +6 -0
- package/dist/discover/ItemDetail.svelte +280 -0
- package/dist/discover/ItemDetail.svelte.d.ts +32 -0
- package/dist/discover/ItemMenu.svelte +61 -0
- package/dist/discover/ItemMenu.svelte.d.ts +13 -0
- package/dist/discover/LiveFrame.svelte +162 -0
- package/dist/discover/LiveFrame.svelte.d.ts +21 -0
- package/dist/discover/WhatsAppQrCard.svelte +302 -0
- package/dist/discover/WhatsAppQrCard.svelte.d.ts +18 -0
- package/dist/discover/frame.d.ts +7 -0
- package/dist/discover/frame.js +27 -0
- package/dist/discover/href.d.ts +7 -1
- package/dist/discover/href.js +16 -6
- package/dist/discover/index.d.ts +7 -2
- package/dist/discover/index.js +15 -7
- package/dist/discover/item.d.ts +14 -0
- package/dist/discover/item.js +41 -2
- package/dist/discover/layout.d.ts +1 -1
- package/dist/discover/layout.js +1 -1
- package/dist/discover/sections.d.ts +39 -0
- package/dist/discover/sections.js +84 -0
- package/dist/discover/types.d.ts +17 -0
- package/dist/discover/types.js +5 -2
- package/dist/discover/whatsapp-card-export.d.ts +9 -0
- package/dist/discover/whatsapp-card-export.js +72 -0
- package/dist/discover/whatsapp-card.d.ts +106 -0
- package/dist/discover/whatsapp-card.js +226 -0
- package/dist/discover/whatsapp-qr.d.ts +7 -0
- package/dist/discover/whatsapp-qr.js +20 -0
- package/dist/figures/bike-gears/figure.js +2 -6
- package/dist/figures/fission-reactor/figure.js +2 -6
- package/dist/figures/heart/figure.js +2 -6
- package/dist/figures/ion-thruster/figure.js +2 -6
- package/dist/figures/particle-collider/figure.js +2 -6
- package/dist/figures/rocket-engine/figure.js +2 -6
- package/dist/figures/style.d.ts +2 -0
- package/dist/figures/style.js +18 -0
- package/dist/figures/tokamak/figure.js +2 -6
- package/dist/manifest/entries/chart.js +1 -1
- package/dist/manifest/entries/markdown.js +2 -2
- package/dist/manifest.json +5 -5
- package/dist/styles.css +11 -10
- package/dist/utils/markdown-ext.d.ts +6 -0
- package/dist/utils/markdown-ext.js +68 -0
- package/dist/utils/mermaid.d.ts +94 -0
- package/dist/utils/mermaid.js +144 -0
- package/dist/utils/sanitize-html.d.ts +6 -0
- package/dist/utils/sanitize-html.js +15 -0
- package/dist/version.d.ts +2 -2
- package/dist/version.js +2 -2
- package/dist/widgets/data/Chart.svelte +34 -6
- package/dist/widgets/data/chart-layout.d.ts +4 -4
- package/dist/widgets/data/chart-layout.js +9 -8
- package/dist/widgets/display/Markdown.svelte +147 -3
- package/dist/widgets/display/Markdown.svelte.d.ts +1 -1
- package/package.json +4 -3
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
export type MermaidConfig = ReturnType<typeof mermaidConfig>;
|
|
2
|
+
export type Palette = Record<'ground' | 'ink' | 'soft' | 'border' | 'card' | 'panel' | 'inset' | 'primary' | 'tint' | 'onPrimary', string>;
|
|
3
|
+
/**
|
|
4
|
+
* CSS colours (any syntax, var() resolved at `scope`) to opaque #rrggbb, each
|
|
5
|
+
* painted over `ground` (itself over the page background). Null without a 2D canvas.
|
|
6
|
+
*/
|
|
7
|
+
export declare function resolveColors<K extends string>(css: Record<K, string>, scope?: HTMLElement, ground?: string): Record<K, string> | null;
|
|
8
|
+
/** True when a #rrggbb colour is dark (relative luminance under 0.4). */
|
|
9
|
+
export declare function isDarkHex(hex: string): boolean;
|
|
10
|
+
/**
|
|
11
|
+
* mermaid's config for one palette. `htmlLabels: false` keeps labels as SVG
|
|
12
|
+
* text (no foreignObject), which a caller drawing untrusted source needs so it
|
|
13
|
+
* can run the result through an SVG sanitizer.
|
|
14
|
+
*/
|
|
15
|
+
export declare function mermaidConfig(c: Palette, font: string, { htmlLabels }?: {
|
|
16
|
+
htmlLabels?: boolean | undefined;
|
|
17
|
+
}): {
|
|
18
|
+
startOnLoad: boolean;
|
|
19
|
+
securityLevel: "strict";
|
|
20
|
+
suppressErrorRendering: boolean;
|
|
21
|
+
theme: "base";
|
|
22
|
+
fontFamily: string;
|
|
23
|
+
htmlLabels: boolean;
|
|
24
|
+
look: "neo";
|
|
25
|
+
layout: string;
|
|
26
|
+
themeCSS: string;
|
|
27
|
+
flowchart: {
|
|
28
|
+
wrappingWidth: number;
|
|
29
|
+
htmlLabels: boolean;
|
|
30
|
+
curve: "basis";
|
|
31
|
+
nodeSpacing: number;
|
|
32
|
+
rankSpacing: number;
|
|
33
|
+
padding: number;
|
|
34
|
+
};
|
|
35
|
+
sequence: {
|
|
36
|
+
actorFontFamily: string;
|
|
37
|
+
messageFontFamily: string;
|
|
38
|
+
noteFontFamily: string;
|
|
39
|
+
};
|
|
40
|
+
themeVariables: {
|
|
41
|
+
darkMode: boolean;
|
|
42
|
+
fontFamily: string;
|
|
43
|
+
fontSize: string;
|
|
44
|
+
background: string;
|
|
45
|
+
textColor: string;
|
|
46
|
+
titleColor: string;
|
|
47
|
+
lineColor: string;
|
|
48
|
+
primaryColor: string;
|
|
49
|
+
primaryTextColor: string;
|
|
50
|
+
primaryBorderColor: string;
|
|
51
|
+
secondaryColor: string;
|
|
52
|
+
secondaryTextColor: string;
|
|
53
|
+
secondaryBorderColor: string;
|
|
54
|
+
tertiaryColor: string;
|
|
55
|
+
tertiaryTextColor: string;
|
|
56
|
+
tertiaryBorderColor: string;
|
|
57
|
+
mainBkg: string;
|
|
58
|
+
nodeBorder: string;
|
|
59
|
+
nodeTextColor: string;
|
|
60
|
+
clusterBkg: string;
|
|
61
|
+
clusterBorder: string;
|
|
62
|
+
arrowheadColor: string;
|
|
63
|
+
edgeLabelBackground: string;
|
|
64
|
+
actorBkg: string;
|
|
65
|
+
actorBorder: string;
|
|
66
|
+
actorTextColor: string;
|
|
67
|
+
actorLineColor: string;
|
|
68
|
+
signalColor: string;
|
|
69
|
+
signalTextColor: string;
|
|
70
|
+
labelBoxBkgColor: string;
|
|
71
|
+
labelBoxBorderColor: string;
|
|
72
|
+
labelTextColor: string;
|
|
73
|
+
loopTextColor: string;
|
|
74
|
+
noteBkgColor: string;
|
|
75
|
+
noteTextColor: string;
|
|
76
|
+
noteBorderColor: string;
|
|
77
|
+
activationBkgColor: string;
|
|
78
|
+
activationBorderColor: string;
|
|
79
|
+
sequenceNumberColor: string;
|
|
80
|
+
stateBkg: string;
|
|
81
|
+
stateLabelColor: string;
|
|
82
|
+
stateBorder: string;
|
|
83
|
+
labelColor: string;
|
|
84
|
+
transitionColor: string;
|
|
85
|
+
transitionLabelColor: string;
|
|
86
|
+
compositeBackground: string;
|
|
87
|
+
compositeTitleBackground: string;
|
|
88
|
+
altBackground: string;
|
|
89
|
+
specialStateColor: string;
|
|
90
|
+
innerEndBackground: string;
|
|
91
|
+
};
|
|
92
|
+
};
|
|
93
|
+
/** Render mermaid `source` to an SVG string. Rejects when the source does not parse. */
|
|
94
|
+
export declare function renderMermaid(source: string, config: MermaidConfig): Promise<string>;
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
// utils/mermaid.ts — Draws mermaid source as SVG for the docs site
|
|
2
|
+
// (site/docs/diagrams.ts) and the markdown widget. Browser only: call it from
|
|
3
|
+
// an effect or an attachment, never at module top level.
|
|
4
|
+
// - mermaid is a dynamic import, fetched the first time anything renders, so a
|
|
5
|
+
// bundle (or a chat card) with no diagram never loads it.
|
|
6
|
+
// - mermaidConfig maps a palette of opaque hex colours onto mermaid's 'base'
|
|
7
|
+
// theme in the neo look: rounded nodes with brand (palette.primary) strokes
|
|
8
|
+
// on a soft card fill, curved (basis) flowchart edges in the soft ink.
|
|
9
|
+
// Callers resolve their own tokens with resolveColors: mermaid's colour maths
|
|
10
|
+
// cannot parse oklch or color-mix, so each token is painted over the ground
|
|
11
|
+
// on a 1px canvas and read back as #rrggbb.
|
|
12
|
+
// - mermaid.initialize is global, and the site and a widget can draw on one
|
|
13
|
+
// page with different palettes, so every render goes through one promise
|
|
14
|
+
// chain and re-initializes only when the config changed. securityLevel is
|
|
15
|
+
// always 'strict'.
|
|
16
|
+
let loading;
|
|
17
|
+
let queue = Promise.resolve();
|
|
18
|
+
let initialized = '';
|
|
19
|
+
let seq = 0;
|
|
20
|
+
/**
|
|
21
|
+
* CSS colours (any syntax, var() resolved at `scope`) to opaque #rrggbb, each
|
|
22
|
+
* painted over `ground` (itself over the page background). Null without a 2D canvas.
|
|
23
|
+
*/
|
|
24
|
+
export function resolveColors(css, scope = document.body, ground = 'var(--background, Canvas)') {
|
|
25
|
+
const ctx = document.createElement('canvas').getContext('2d', { willReadFrequently: true });
|
|
26
|
+
if (!ctx)
|
|
27
|
+
return null;
|
|
28
|
+
const probe = document.createElement('span');
|
|
29
|
+
probe.style.display = 'none';
|
|
30
|
+
scope.append(probe);
|
|
31
|
+
const computed = (value) => {
|
|
32
|
+
probe.style.color = '';
|
|
33
|
+
probe.style.color = value;
|
|
34
|
+
return getComputedStyle(probe).color;
|
|
35
|
+
};
|
|
36
|
+
const layers = [computed('var(--background, Canvas)'), computed(ground)];
|
|
37
|
+
const out = {};
|
|
38
|
+
for (const key of Object.keys(css)) {
|
|
39
|
+
for (const fill of [...layers, computed(css[key])]) {
|
|
40
|
+
ctx.fillStyle = fill;
|
|
41
|
+
ctx.fillRect(0, 0, 1, 1);
|
|
42
|
+
}
|
|
43
|
+
const [r, g, b] = ctx.getImageData(0, 0, 1, 1).data;
|
|
44
|
+
out[key] = `#${[r, g, b].map((n) => n.toString(16).padStart(2, '0')).join('')}`;
|
|
45
|
+
}
|
|
46
|
+
probe.remove();
|
|
47
|
+
return out;
|
|
48
|
+
}
|
|
49
|
+
/** True when a #rrggbb colour is dark (relative luminance under 0.4). */
|
|
50
|
+
export function isDarkHex(hex) {
|
|
51
|
+
const [r, g, b] = [1, 3, 5].map((i) => parseInt(hex.slice(i, i + 2), 16) / 255);
|
|
52
|
+
return 0.2126 * r + 0.7152 * g + 0.0722 * b < 0.4;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* mermaid's config for one palette. `htmlLabels: false` keeps labels as SVG
|
|
56
|
+
* text (no foreignObject), which a caller drawing untrusted source needs so it
|
|
57
|
+
* can run the result through an SVG sanitizer.
|
|
58
|
+
*/
|
|
59
|
+
export function mermaidConfig(c, font, { htmlLabels = true } = {}) {
|
|
60
|
+
return {
|
|
61
|
+
startOnLoad: false,
|
|
62
|
+
securityLevel: 'strict',
|
|
63
|
+
suppressErrorRendering: true,
|
|
64
|
+
theme: 'base',
|
|
65
|
+
fontFamily: font,
|
|
66
|
+
htmlLabels,
|
|
67
|
+
look: 'neo',
|
|
68
|
+
// mermaid 12 defaults to elk, which forces orthogonal edges; dagre honours `curve`.
|
|
69
|
+
layout: 'dagre',
|
|
70
|
+
// Rounded nodes even where the neo look keeps a square shape; lives in the SVG's own <style>.
|
|
71
|
+
themeCSS: '.node rect, .cluster rect { rx: 10px; ry: 10px; } .edgePath .path, .flowchart-link { stroke-width: 1.5px; }',
|
|
72
|
+
flowchart: { wrappingWidth: 260, htmlLabels, curve: 'basis', nodeSpacing: 36, rankSpacing: 44, padding: 14 },
|
|
73
|
+
sequence: { actorFontFamily: font, messageFontFamily: font, noteFontFamily: font },
|
|
74
|
+
themeVariables: {
|
|
75
|
+
darkMode: isDarkHex(c.ground),
|
|
76
|
+
fontFamily: font,
|
|
77
|
+
fontSize: '14px',
|
|
78
|
+
background: c.ground,
|
|
79
|
+
textColor: c.ink,
|
|
80
|
+
titleColor: c.ink,
|
|
81
|
+
lineColor: c.soft,
|
|
82
|
+
primaryColor: c.card,
|
|
83
|
+
primaryTextColor: c.ink,
|
|
84
|
+
primaryBorderColor: c.primary,
|
|
85
|
+
secondaryColor: c.inset,
|
|
86
|
+
secondaryTextColor: c.ink,
|
|
87
|
+
secondaryBorderColor: c.border,
|
|
88
|
+
tertiaryColor: c.panel,
|
|
89
|
+
tertiaryTextColor: c.ink,
|
|
90
|
+
tertiaryBorderColor: c.border,
|
|
91
|
+
mainBkg: c.card,
|
|
92
|
+
nodeBorder: c.primary,
|
|
93
|
+
nodeTextColor: c.ink,
|
|
94
|
+
clusterBkg: c.panel,
|
|
95
|
+
clusterBorder: c.border,
|
|
96
|
+
arrowheadColor: c.soft,
|
|
97
|
+
edgeLabelBackground: c.ground,
|
|
98
|
+
actorBkg: c.tint,
|
|
99
|
+
actorBorder: c.primary,
|
|
100
|
+
actorTextColor: c.ink,
|
|
101
|
+
actorLineColor: c.border,
|
|
102
|
+
signalColor: c.soft,
|
|
103
|
+
signalTextColor: c.ink,
|
|
104
|
+
labelBoxBkgColor: c.card,
|
|
105
|
+
labelBoxBorderColor: c.border,
|
|
106
|
+
labelTextColor: c.ink,
|
|
107
|
+
loopTextColor: c.ink,
|
|
108
|
+
noteBkgColor: c.inset,
|
|
109
|
+
noteTextColor: c.ink,
|
|
110
|
+
noteBorderColor: c.border,
|
|
111
|
+
activationBkgColor: c.tint,
|
|
112
|
+
activationBorderColor: c.primary,
|
|
113
|
+
sequenceNumberColor: c.onPrimary,
|
|
114
|
+
stateBkg: c.card,
|
|
115
|
+
stateLabelColor: c.ink,
|
|
116
|
+
stateBorder: c.border,
|
|
117
|
+
labelColor: c.ink,
|
|
118
|
+
transitionColor: c.soft,
|
|
119
|
+
transitionLabelColor: c.ink,
|
|
120
|
+
compositeBackground: c.panel,
|
|
121
|
+
compositeTitleBackground: c.card,
|
|
122
|
+
altBackground: c.inset,
|
|
123
|
+
specialStateColor: c.primary,
|
|
124
|
+
innerEndBackground: c.primary
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
/** Render mermaid `source` to an SVG string. Rejects when the source does not parse. */
|
|
129
|
+
export function renderMermaid(source, config) {
|
|
130
|
+
loading ??= import('mermaid').then((m) => m.default);
|
|
131
|
+
const run = queue.then(async () => {
|
|
132
|
+
const mermaid = await loading;
|
|
133
|
+
await document.fonts?.ready;
|
|
134
|
+
const key = JSON.stringify(config);
|
|
135
|
+
if (key !== initialized) {
|
|
136
|
+
mermaid.initialize(config);
|
|
137
|
+
initialized = key;
|
|
138
|
+
}
|
|
139
|
+
const { svg } = await mermaid.render(`ripple-mermaid-${++seq}`, source);
|
|
140
|
+
return svg;
|
|
141
|
+
});
|
|
142
|
+
queue = run.catch(() => { });
|
|
143
|
+
return run;
|
|
144
|
+
}
|
|
@@ -6,3 +6,9 @@ export declare function sanitizeHtml(html: string): string;
|
|
|
6
6
|
* fix a generator that interpolates attacker text into markup.
|
|
7
7
|
*/
|
|
8
8
|
export declare function sanitizeSvg(svg: string): string;
|
|
9
|
+
/**
|
|
10
|
+
* Sanitize generated MathML (temml output) for insertion. `style` is dropped
|
|
11
|
+
* like everywhere else: it only carries \color and a few spacing tweaks, and
|
|
12
|
+
* keeping it would reopen the url() beacon channel.
|
|
13
|
+
*/
|
|
14
|
+
export declare function sanitizeMath(mathml: string): string;
|
|
@@ -11,6 +11,9 @@
|
|
|
11
11
|
// url(https://evil/...) beacons on render, and position:fixed can overlay the
|
|
12
12
|
// host app with a fake credential prompt. Structural markup is unaffected.
|
|
13
13
|
//
|
|
14
|
+
// sanitizeMath is the MathML-profile sibling for temml output (the markdown
|
|
15
|
+
// widget's math); the HTML profile would delete the <math> element outright.
|
|
16
|
+
//
|
|
14
17
|
// sanitizeSvg is the SVG-profile sibling for widgets that {@html} generated SVG
|
|
15
18
|
// (Qr): the HTML profile would delete the <svg> element outright.
|
|
16
19
|
//
|
|
@@ -45,3 +48,15 @@ export function sanitizeSvg(svg) {
|
|
|
45
48
|
return svg; // SSR fail-open — see header
|
|
46
49
|
return DOMPurify.sanitize(svg, { USE_PROFILES: { svg: true, svgFilters: true } });
|
|
47
50
|
}
|
|
51
|
+
/**
|
|
52
|
+
* Sanitize generated MathML (temml output) for insertion. `style` is dropped
|
|
53
|
+
* like everywhere else: it only carries \color and a few spacing tweaks, and
|
|
54
|
+
* keeping it would reopen the url() beacon channel.
|
|
55
|
+
*/
|
|
56
|
+
export function sanitizeMath(mathml) {
|
|
57
|
+
if (!mathml)
|
|
58
|
+
return '';
|
|
59
|
+
if (typeof window === 'undefined')
|
|
60
|
+
return mathml; // SSR fail-open, see header
|
|
61
|
+
return DOMPurify.sanitize(mathml, { USE_PROFILES: { mathMl: true }, FORBID_ATTR: ['style'] });
|
|
62
|
+
}
|
package/dist/version.d.ts
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* (scripts/build-manifest.ts) — unlike a Vite `define`, which would be
|
|
12
12
|
* undefined under bun. version.test.ts asserts this stays in lockstep with
|
|
13
13
|
* package.json so a release bump can't silently drift.
|
|
14
|
-
* Released versions: svelte 0.
|
|
14
|
+
* Released versions: svelte 0.9.0-next.0 with core 0.6.1-next.0 (pre-release, npm dist-tag next), embed 0.1.0 and react 0.1.0. A bump
|
|
15
15
|
* changes this constant and packages/svelte/package.json together.
|
|
16
16
|
*/
|
|
17
|
-
export declare const RIPPLE_VERSION = "0.
|
|
17
|
+
export declare const RIPPLE_VERSION = "0.9.0-next.0";
|
package/dist/version.js
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* (scripts/build-manifest.ts) — unlike a Vite `define`, which would be
|
|
12
12
|
* undefined under bun. version.test.ts asserts this stays in lockstep with
|
|
13
13
|
* package.json so a release bump can't silently drift.
|
|
14
|
-
* Released versions: svelte 0.
|
|
14
|
+
* Released versions: svelte 0.9.0-next.0 with core 0.6.1-next.0 (pre-release, npm dist-tag next), embed 0.1.0 and react 0.1.0. A bump
|
|
15
15
|
* changes this constant and packages/svelte/package.json together.
|
|
16
16
|
*/
|
|
17
|
-
export const RIPPLE_VERSION = '0.
|
|
17
|
+
export const RIPPLE_VERSION = '0.9.0-next.0';
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
<!--
|
|
2
2
|
Chart.svelte: 10 chart types (bar, line, area, pie, donut, candlestick,
|
|
3
3
|
sparkline, heatmap, gauge, radar) drawn with echarts. Colours, axis label fit
|
|
4
|
-
and the pie legend come from chart-layout.ts:
|
|
5
|
-
|
|
4
|
+
and the pie legend come from chart-layout.ts: an explicit `colors` prop wins,
|
|
5
|
+
otherwise series take the theme's --chart-1..5 in order (read as hex because
|
|
6
|
+
echarts draws on a canvas; an unset slot falls back to chart-layout's
|
|
7
|
+
categorical palette), one series is one colour, `reference` series draw muted and
|
|
6
8
|
dashed, the category axis rotates or evenly thins labels (keeping first and
|
|
7
9
|
last) to the measured width, and pie/donut legends carry value and percent.
|
|
8
10
|
The instance is created once, when ResizeObserver first reports a size, and
|
|
@@ -19,7 +21,7 @@
|
|
|
19
21
|
import { onMount } from 'svelte';
|
|
20
22
|
import { cn } from '../../utils.js';
|
|
21
23
|
import { safeArray, safeObject } from '../../utils/safe-props.js';
|
|
22
|
-
import { fitAxisLabels, pieLayout, pointColors, seriesColor, sliceColors, sliceLegendText } from './chart-layout.js';
|
|
24
|
+
import { CATEGORICAL, fitAxisLabels, pieLayout, pointColors, seriesColor, sliceColors, sliceLegendText } from './chart-layout.js';
|
|
23
25
|
|
|
24
26
|
interface DataPoint {
|
|
25
27
|
label: string;
|
|
@@ -104,7 +106,32 @@
|
|
|
104
106
|
|
|
105
107
|
/** The i-th series colour, a `var(--x)` resolved for the canvas. */
|
|
106
108
|
function getColor(i: number): string {
|
|
107
|
-
return cssColor(seriesColor(i, colors));
|
|
109
|
+
return cssColor(seriesColor(i, colors, palette));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** This draw's default palette, read once per buildOption. */
|
|
113
|
+
let palette: readonly string[] = CATEGORICAL;
|
|
114
|
+
|
|
115
|
+
/** The default palette: the theme's --chart-1..5 where they resolve, then chart-layout's categorical one. */
|
|
116
|
+
function themePalette(): string[] {
|
|
117
|
+
return CATEGORICAL.map((c, i) => (i < 5 && hexOf(cssColor(`var(--chart-${i + 1})`))) || c);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function hexOf(color: string): string | undefined {
|
|
121
|
+
if (/^#[0-9a-f]{6}$/i.test(color)) return color;
|
|
122
|
+
// An unset token comes back as `var(...)`: CSS.supports accepts it, canvas ignores it and reads black.
|
|
123
|
+
if (!color || color.startsWith('var(')) return undefined;
|
|
124
|
+
try {
|
|
125
|
+
if (!CSS.supports('color', color)) return undefined;
|
|
126
|
+
const ctx = document.createElement('canvas').getContext('2d');
|
|
127
|
+
if (!ctx) return undefined;
|
|
128
|
+
ctx.fillStyle = color;
|
|
129
|
+
ctx.fillRect(0, 0, 1, 1);
|
|
130
|
+
const [r, g, b] = ctx.getImageData(0, 0, 1, 1).data;
|
|
131
|
+
return `#${[r, g, b].map((n) => n.toString(16).padStart(2, '0')).join('')}`;
|
|
132
|
+
} catch {
|
|
133
|
+
return undefined;
|
|
134
|
+
}
|
|
108
135
|
}
|
|
109
136
|
|
|
110
137
|
let chartEl: HTMLDivElement | undefined = $state();
|
|
@@ -193,11 +220,12 @@
|
|
|
193
220
|
);
|
|
194
221
|
|
|
195
222
|
function buildOption() {
|
|
223
|
+
palette = themePalette();
|
|
196
224
|
const tc = themeColors();
|
|
197
225
|
const labels = data.map(d => d.label);
|
|
198
226
|
const values = data.map(d => d.value ?? 0);
|
|
199
227
|
// One series is one colour, unless the spec colours points itself.
|
|
200
|
-
const itemColors = pointColors(data, colors).map(cssColor);
|
|
228
|
+
const itemColors = pointColors(data, colors, palette).map(cssColor);
|
|
201
229
|
|
|
202
230
|
// Detect multi-series: any point carries a `series` map → one ECharts series per key.
|
|
203
231
|
const seriesKeys: string[] = (() => {
|
|
@@ -342,7 +370,7 @@
|
|
|
342
370
|
};
|
|
343
371
|
}
|
|
344
372
|
} else if (type === 'pie' || type === 'donut') {
|
|
345
|
-
const slices = sliceColors(data, colors).map(cssColor);
|
|
373
|
+
const slices = sliceColors(data, colors, palette).map(cssColor);
|
|
346
374
|
const legendText = sliceLegendText(data);
|
|
347
375
|
const pie = pieLayout(boxWidth, height, [...legendText.values()]);
|
|
348
376
|
const r = pie.radius;
|
|
@@ -5,16 +5,16 @@
|
|
|
5
5
|
* A spec that wants status colours passes them in `colors`.
|
|
6
6
|
*/
|
|
7
7
|
export declare const CATEGORICAL: string[];
|
|
8
|
-
/** The i-th series colour: the spec's `colors[i]`, else
|
|
9
|
-
export declare function seriesColor(i: number, colors: readonly string[]): string;
|
|
8
|
+
/** The i-th series colour: the spec's `colors[i]`, else `palette` in order. */
|
|
9
|
+
export declare function seriesColor(i: number, colors: readonly string[], palette?: readonly string[]): string;
|
|
10
10
|
/**
|
|
11
11
|
* Colours for the points of ONE series (bar, line, heatmap, gauge, radar).
|
|
12
12
|
* One series is one colour. Points differ only when the spec says so: a point's
|
|
13
13
|
* own `color`, or a `colors` list longer than one (read as per point).
|
|
14
14
|
*/
|
|
15
|
-
export declare function pointColors(data: readonly object[], colors: readonly string[]): string[];
|
|
15
|
+
export declare function pointColors(data: readonly object[], colors: readonly string[], palette?: readonly string[]): string[];
|
|
16
16
|
/** Slice colours for pie and donut: every slice is its own category. */
|
|
17
|
-
export declare function sliceColors(data: readonly object[], colors: readonly string[]): string[];
|
|
17
|
+
export declare function sliceColors(data: readonly object[], colors: readonly string[], palette?: readonly string[]): string[];
|
|
18
18
|
export interface AxisLabelFit {
|
|
19
19
|
/** echarts `axisLabel.interval`: 0 shows all, a function picks which. */
|
|
20
20
|
interval: number | ((index: number) => boolean);
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
// Palette order, which colour each point or series wears, how the category
|
|
4
4
|
// axis fits its labels to the measured width, and the pie/donut legend text
|
|
5
5
|
// and layout. Chart.svelte resolves `var(--x)` colours and the theme, then
|
|
6
|
-
// feeds these the numbers
|
|
6
|
+
// feeds these the numbers; its default palette is the theme's --chart-1..5
|
|
7
|
+
// laid over CATEGORICAL, passed in as `palette`.
|
|
7
8
|
/**
|
|
8
9
|
* Categorical palette in draw order. Brand blue first, then hues that read as
|
|
9
10
|
* categories. Status colours (red, green) are not in it and amber sits late, so
|
|
@@ -11,28 +12,28 @@
|
|
|
11
12
|
* A spec that wants status colours passes them in `colors`.
|
|
12
13
|
*/
|
|
13
14
|
export const CATEGORICAL = ['#3b82f6', '#14b8a6', '#8b5cf6', '#ec4899', '#f59e0b', '#06b6d4', '#f97316', '#64748b'];
|
|
14
|
-
/** The i-th series colour: the spec's `colors[i]`, else
|
|
15
|
-
export function seriesColor(i, colors) {
|
|
16
|
-
return colors[i] ||
|
|
15
|
+
/** The i-th series colour: the spec's `colors[i]`, else `palette` in order. */
|
|
16
|
+
export function seriesColor(i, colors, palette = CATEGORICAL) {
|
|
17
|
+
return colors[i] || palette[i % palette.length];
|
|
17
18
|
}
|
|
18
19
|
/**
|
|
19
20
|
* Colours for the points of ONE series (bar, line, heatmap, gauge, radar).
|
|
20
21
|
* One series is one colour. Points differ only when the spec says so: a point's
|
|
21
22
|
* own `color`, or a `colors` list longer than one (read as per point).
|
|
22
23
|
*/
|
|
23
|
-
export function pointColors(data, colors) {
|
|
24
|
+
export function pointColors(data, colors, palette = CATEGORICAL) {
|
|
24
25
|
return data.map((d, i) => {
|
|
25
26
|
const own = ownColor(d);
|
|
26
27
|
if (own)
|
|
27
28
|
return own;
|
|
28
29
|
if (colors.length > 1)
|
|
29
30
|
return colors[i % colors.length];
|
|
30
|
-
return seriesColor(0, colors);
|
|
31
|
+
return seriesColor(0, colors, palette);
|
|
31
32
|
});
|
|
32
33
|
}
|
|
33
34
|
/** Slice colours for pie and donut: every slice is its own category. */
|
|
34
|
-
export function sliceColors(data, colors) {
|
|
35
|
-
return data.map((d, i) => ownColor(d) ?? seriesColor(i, colors));
|
|
35
|
+
export function sliceColors(data, colors, palette = CATEGORICAL) {
|
|
36
|
+
return data.map((d, i) => ownColor(d) ?? seriesColor(i, colors, palette));
|
|
36
37
|
}
|
|
37
38
|
/** A point's own `color`, when it names one. */
|
|
38
39
|
function ownColor(d) {
|
|
@@ -1,5 +1,107 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
widgets/display/Markdown.svelte — The `markdown` widget: GFM markdown, plus
|
|
3
|
+
LaTeX math ($…$, $$…$$, \(…\), \[…\]) and ```mermaid diagrams.
|
|
4
|
+
- marked runs synchronously (also under SSR) and its output is sanitized
|
|
5
|
+
before {@html}: the source is spec-controlled and may be LLM-authored.
|
|
6
|
+
- Math and diagrams arrive as placeholders holding the source as written
|
|
7
|
+
(utils/markdown-ext.ts). An effect upgrades them in the browser: temml and
|
|
8
|
+
mermaid are dynamic imports, fetched only when a placeholder exists, so a
|
|
9
|
+
card without either pays nothing. A failed render leaves the source.
|
|
10
|
+
- Upgrade output is sanitized again (sanitizeMath, sanitizeSvg) and mermaid
|
|
11
|
+
runs with strict security and SVG-only labels. Diagrams wait until a
|
|
12
|
+
streamed spec is done, so a half-written fence is not parsed every frame.
|
|
13
|
+
- Diagrams take their palette from the --ripple-* tokens at this element (so
|
|
14
|
+
inside an embed's shadow root too) and redraw when class or data-theme
|
|
15
|
+
changes on <html>, on the shadow host, or on .ripple-embed-root.
|
|
16
|
+
-->
|
|
17
|
+
<script lang="ts" module>
|
|
18
|
+
import { Marked } from 'marked';
|
|
19
|
+
import { markedMath, markedMermaid, MATH_CLASS } from '../../utils/markdown-ext.js';
|
|
20
|
+
import { mermaidConfig, renderMermaid, resolveColors } from '../../utils/mermaid.js';
|
|
21
|
+
import { sanitizeMath, sanitizeSvg } from '../../utils/sanitize-html.js';
|
|
22
|
+
|
|
23
|
+
const md = new Marked(markedMath(), markedMermaid());
|
|
24
|
+
|
|
25
|
+
type Temml = (typeof import('temml'))['default'];
|
|
26
|
+
let temml: Promise<Temml> | undefined;
|
|
27
|
+
|
|
28
|
+
function upgradeMath(root: HTMLElement) {
|
|
29
|
+
const spots = [...root.querySelectorAll<HTMLElement>(`.${MATH_CLASS}:not([data-rendered])`)];
|
|
30
|
+
if (!spots.length) return;
|
|
31
|
+
temml ??= import('temml').then((m) => m.default);
|
|
32
|
+
void temml.then((t) => {
|
|
33
|
+
for (const el of spots) {
|
|
34
|
+
if (!el.isConnected || el.dataset.rendered != null) continue;
|
|
35
|
+
try {
|
|
36
|
+
const display = el.dataset.display != null;
|
|
37
|
+
const out = sanitizeMath(t.renderToString(el.dataset.tex ?? '', { displayMode: display, throwOnError: true }));
|
|
38
|
+
if (!out) continue;
|
|
39
|
+
el.innerHTML = out;
|
|
40
|
+
el.dataset.rendered = '';
|
|
41
|
+
} catch {
|
|
42
|
+
// Unparseable TeX: the source stays on screen as written.
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Mermaid's url()/@import in CSS (classDef, style) could beacon out; keep only #fragment refs. */
|
|
49
|
+
const stripOutsideRefs = (svg: string) =>
|
|
50
|
+
svg.replace(/@import[^;]*;?/gi, '').replace(/url\(\s*(?!['"]?#)[^)]*\)/gi, 'none');
|
|
51
|
+
|
|
52
|
+
function diagramConfig(root: HTMLElement) {
|
|
53
|
+
const c = resolveColors(
|
|
54
|
+
{
|
|
55
|
+
ground: 'var(--ripple-surface)',
|
|
56
|
+
ink: 'var(--ripple-surface-foreground)',
|
|
57
|
+
soft: 'var(--ripple-muted-foreground)',
|
|
58
|
+
border: 'color-mix(in oklab, var(--ripple-surface-foreground) 28%, transparent)',
|
|
59
|
+
// workflow's lifted card (--wf-card) with a soft accent wash
|
|
60
|
+
card: 'color-mix(in oklab, var(--ripple-accent) 6%, oklch(from var(--ripple-surface-foreground) calc(1.18 - l * 0.92) calc(c * 0.5) h))',
|
|
61
|
+
panel: 'var(--ripple-muted)',
|
|
62
|
+
inset: 'var(--ripple-muted)',
|
|
63
|
+
primary: 'var(--ripple-accent)',
|
|
64
|
+
tint: 'color-mix(in oklab, var(--ripple-accent) 14%, transparent)',
|
|
65
|
+
onPrimary: 'var(--ripple-accent-foreground)'
|
|
66
|
+
},
|
|
67
|
+
root,
|
|
68
|
+
'var(--ripple-surface)'
|
|
69
|
+
);
|
|
70
|
+
return c && mermaidConfig(c, getComputedStyle(root).fontFamily || 'sans-serif', { htmlLabels: false });
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function upgradeDiagrams(root: HTMLElement, alive: () => boolean) {
|
|
74
|
+
const wraps = [...root.querySelectorAll<HTMLElement>('.ripple-diagram')];
|
|
75
|
+
if (!wraps.length) return;
|
|
76
|
+
const config = diagramConfig(root);
|
|
77
|
+
if (!config) return;
|
|
78
|
+
const key = JSON.stringify(config.themeVariables);
|
|
79
|
+
for (const wrap of wraps) {
|
|
80
|
+
if (wrap.dataset.drawn === key) continue;
|
|
81
|
+
const pre = wrap.querySelector('pre');
|
|
82
|
+
const source = pre?.textContent ?? '';
|
|
83
|
+
if (!pre || !source.trim()) continue;
|
|
84
|
+
renderMermaid(source, config).then(
|
|
85
|
+
(svg) => {
|
|
86
|
+
if (!alive() || !wrap.isConnected) return;
|
|
87
|
+
let canvas = wrap.querySelector<HTMLElement>('.ripple-diagram-canvas');
|
|
88
|
+
if (!canvas) {
|
|
89
|
+
canvas = document.createElement('div');
|
|
90
|
+
canvas.className = 'ripple-diagram-canvas';
|
|
91
|
+
wrap.prepend(canvas);
|
|
92
|
+
}
|
|
93
|
+
canvas.innerHTML = sanitizeSvg(stripOutsideRefs(svg));
|
|
94
|
+
pre.hidden = true;
|
|
95
|
+
wrap.dataset.drawn = key;
|
|
96
|
+
},
|
|
97
|
+
(e: unknown) => console.warn('[ripple/markdown] mermaid diagram failed to render:', e)
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
</script>
|
|
102
|
+
|
|
1
103
|
<script lang="ts">
|
|
2
|
-
import {
|
|
104
|
+
import { getContext } from 'svelte';
|
|
3
105
|
import { cn } from '../../utils.js';
|
|
4
106
|
import { sanitizeHtml } from '../../utils/sanitize-html.js';
|
|
5
107
|
|
|
@@ -7,7 +109,7 @@
|
|
|
7
109
|
id?: string;
|
|
8
110
|
class?: string;
|
|
9
111
|
style?: Record<string, string>;
|
|
10
|
-
/** Markdown source. */
|
|
112
|
+
/** Markdown source. Also renders LaTeX math and ```mermaid fences. */
|
|
11
113
|
content?: string;
|
|
12
114
|
/** Alias for `content`. Some specs author this as `text`. */
|
|
13
115
|
text?: string;
|
|
@@ -17,6 +119,10 @@
|
|
|
17
119
|
|
|
18
120
|
let { id, class: className, style, content, text, gfm = true }: Props = $props();
|
|
19
121
|
|
|
122
|
+
const streamPhase = getContext<(() => 'active' | 'done' | undefined) | undefined>('ui-streaming');
|
|
123
|
+
let root = $state<HTMLDivElement>();
|
|
124
|
+
let themeTick = $state(0);
|
|
125
|
+
|
|
20
126
|
const source = $derived(content ?? text ?? '');
|
|
21
127
|
|
|
22
128
|
const html = $derived.by(() => {
|
|
@@ -25,19 +131,46 @@
|
|
|
25
131
|
// Sanitize before {@html}: markdown source is spec-controlled and may be
|
|
26
132
|
// untrusted (LLM-authored), and marked passes raw HTML through by default,
|
|
27
133
|
// so `<img src=x onerror=…>` in the source would otherwise run as XSS.
|
|
28
|
-
return sanitizeHtml(
|
|
134
|
+
return sanitizeHtml(md.parse(source, { gfm, breaks: true, async: false }) as string);
|
|
29
135
|
} catch (e) {
|
|
30
136
|
console.warn('[ripple/markdown] parse failed:', e);
|
|
31
137
|
return '';
|
|
32
138
|
}
|
|
33
139
|
});
|
|
34
140
|
|
|
141
|
+
$effect(() => {
|
|
142
|
+
if (!root) return;
|
|
143
|
+
// The page's theme lives on <html>; an embed's on its shadow host (data-theme)
|
|
144
|
+
// and .ripple-embed-root (.dark), and <html> never changes there.
|
|
145
|
+
const host = (root.getRootNode() as Partial<ShadowRoot>).host;
|
|
146
|
+
const themeRoots = new Set([document.documentElement, host, root.closest('.ripple-embed-root')]);
|
|
147
|
+
const mo = new MutationObserver(() => themeTick++);
|
|
148
|
+
for (const el of themeRoots) {
|
|
149
|
+
if (el) mo.observe(el, { attributes: true, attributeFilter: ['class', 'data-theme'] });
|
|
150
|
+
}
|
|
151
|
+
return () => mo.disconnect();
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
$effect(() => {
|
|
155
|
+
void html;
|
|
156
|
+
if (!root) return;
|
|
157
|
+
upgradeMath(root);
|
|
158
|
+
if (streamPhase?.() === 'active') return;
|
|
159
|
+
void themeTick;
|
|
160
|
+
let alive = true;
|
|
161
|
+
upgradeDiagrams(root, () => alive);
|
|
162
|
+
return () => {
|
|
163
|
+
alive = false;
|
|
164
|
+
};
|
|
165
|
+
});
|
|
166
|
+
|
|
35
167
|
const styleString = $derived(
|
|
36
168
|
style ? Object.entries(style).map(([k, v]) => `${k}:${v}`).join(';') : undefined
|
|
37
169
|
);
|
|
38
170
|
</script>
|
|
39
171
|
|
|
40
172
|
<div
|
|
173
|
+
bind:this={root}
|
|
41
174
|
{id}
|
|
42
175
|
class={cn(
|
|
43
176
|
'ripple-markdown text-sm leading-relaxed',
|
|
@@ -56,9 +189,20 @@
|
|
|
56
189
|
'[&_th]:border-b [&_th]:border-border [&_th]:p-2 [&_th]:text-left [&_th]:font-semibold',
|
|
57
190
|
'[&_td]:border-b [&_td]:border-border/50 [&_td]:p-2',
|
|
58
191
|
'[&_hr]:my-4 [&_hr]:border-border',
|
|
192
|
+
'[&_.ripple-math-display]:block [&_.ripple-math-display]:my-3 [&_.ripple-math-display]:overflow-x-auto [&_.ripple-math-display]:py-1',
|
|
193
|
+
'[&_.ripple-diagram-canvas]:my-3 [&_.ripple-diagram-canvas]:overflow-x-auto',
|
|
194
|
+
'[&_.ripple-diagram-canvas_svg]:mx-auto [&_.ripple-diagram-canvas_svg]:h-auto [&_.ripple-diagram-canvas_svg]:max-w-full',
|
|
59
195
|
className
|
|
60
196
|
)}
|
|
61
197
|
style={styleString}
|
|
62
198
|
>
|
|
63
199
|
{@html html}
|
|
64
200
|
</div>
|
|
201
|
+
|
|
202
|
+
<style>
|
|
203
|
+
/* System math fonts; temml's own CSS and woff2 are not shipped. */
|
|
204
|
+
.ripple-markdown :global(math) {
|
|
205
|
+
font-family: 'STIX Two Math', 'Cambria Math', 'Latin Modern Math', math;
|
|
206
|
+
font-size: 1.1em;
|
|
207
|
+
}
|
|
208
|
+
</style>
|
|
@@ -2,7 +2,7 @@ interface Props {
|
|
|
2
2
|
id?: string;
|
|
3
3
|
class?: string;
|
|
4
4
|
style?: Record<string, string>;
|
|
5
|
-
/** Markdown source. */
|
|
5
|
+
/** Markdown source. Also renders LaTeX math and ```mermaid fences. */
|
|
6
6
|
content?: string;
|
|
7
7
|
/** Alias for `content`. Some specs author this as `text`. */
|
|
8
8
|
text?: string;
|