@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.
Files changed (71) hide show
  1. package/dist/discover/CreatorAvatar.svelte +37 -0
  2. package/dist/discover/CreatorAvatar.svelte.d.ts +10 -0
  3. package/dist/discover/CreatorLine.svelte +18 -0
  4. package/dist/discover/CreatorLine.svelte.d.ts +8 -0
  5. package/dist/discover/DiscoverChips.svelte +15 -2
  6. package/dist/discover/DiscoverFilters.svelte +56 -41
  7. package/dist/discover/DiscoverHeader.svelte +33 -10
  8. package/dist/discover/DiscoverHeader.svelte.d.ts +3 -0
  9. package/dist/discover/DiscoverRow.svelte +36 -10
  10. package/dist/discover/DiscoverRow.svelte.d.ts +7 -0
  11. package/dist/discover/DiscoverSection.svelte +18 -10
  12. package/dist/discover/DiscoverSkeleton.svelte +5 -3
  13. package/dist/discover/DiscoverTile.svelte +52 -17
  14. package/dist/discover/DiscoverTile.svelte.d.ts +7 -0
  15. package/dist/discover/ItemActions.svelte +46 -17
  16. package/dist/discover/ItemActions.svelte.d.ts +6 -0
  17. package/dist/discover/ItemDetail.svelte +280 -0
  18. package/dist/discover/ItemDetail.svelte.d.ts +32 -0
  19. package/dist/discover/ItemMenu.svelte +61 -0
  20. package/dist/discover/ItemMenu.svelte.d.ts +13 -0
  21. package/dist/discover/LiveFrame.svelte +162 -0
  22. package/dist/discover/LiveFrame.svelte.d.ts +21 -0
  23. package/dist/discover/WhatsAppQrCard.svelte +302 -0
  24. package/dist/discover/WhatsAppQrCard.svelte.d.ts +18 -0
  25. package/dist/discover/frame.d.ts +7 -0
  26. package/dist/discover/frame.js +27 -0
  27. package/dist/discover/href.d.ts +7 -1
  28. package/dist/discover/href.js +16 -6
  29. package/dist/discover/index.d.ts +7 -2
  30. package/dist/discover/index.js +15 -7
  31. package/dist/discover/item.d.ts +14 -0
  32. package/dist/discover/item.js +41 -2
  33. package/dist/discover/layout.d.ts +1 -1
  34. package/dist/discover/layout.js +1 -1
  35. package/dist/discover/sections.d.ts +39 -0
  36. package/dist/discover/sections.js +84 -0
  37. package/dist/discover/types.d.ts +17 -0
  38. package/dist/discover/types.js +5 -2
  39. package/dist/discover/whatsapp-card-export.d.ts +9 -0
  40. package/dist/discover/whatsapp-card-export.js +72 -0
  41. package/dist/discover/whatsapp-card.d.ts +106 -0
  42. package/dist/discover/whatsapp-card.js +226 -0
  43. package/dist/discover/whatsapp-qr.d.ts +7 -0
  44. package/dist/discover/whatsapp-qr.js +20 -0
  45. package/dist/figures/bike-gears/figure.js +2 -6
  46. package/dist/figures/fission-reactor/figure.js +2 -6
  47. package/dist/figures/heart/figure.js +2 -6
  48. package/dist/figures/ion-thruster/figure.js +2 -6
  49. package/dist/figures/particle-collider/figure.js +2 -6
  50. package/dist/figures/rocket-engine/figure.js +2 -6
  51. package/dist/figures/style.d.ts +2 -0
  52. package/dist/figures/style.js +18 -0
  53. package/dist/figures/tokamak/figure.js +2 -6
  54. package/dist/manifest/entries/chart.js +1 -1
  55. package/dist/manifest/entries/markdown.js +2 -2
  56. package/dist/manifest.json +5 -5
  57. package/dist/styles.css +11 -10
  58. package/dist/utils/markdown-ext.d.ts +6 -0
  59. package/dist/utils/markdown-ext.js +68 -0
  60. package/dist/utils/mermaid.d.ts +94 -0
  61. package/dist/utils/mermaid.js +144 -0
  62. package/dist/utils/sanitize-html.d.ts +6 -0
  63. package/dist/utils/sanitize-html.js +15 -0
  64. package/dist/version.d.ts +2 -2
  65. package/dist/version.js +2 -2
  66. package/dist/widgets/data/Chart.svelte +34 -6
  67. package/dist/widgets/data/chart-layout.d.ts +4 -4
  68. package/dist/widgets/data/chart-layout.js +9 -8
  69. package/dist/widgets/display/Markdown.svelte +147 -3
  70. package/dist/widgets/display/Markdown.svelte.d.ts +1 -1
  71. 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.8.2 with core 0.6.0, embed 0.1.0 and react 0.1.0. A bump
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.8.2";
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.8.2 with core 0.6.0, embed 0.1.0 and react 0.1.0. A bump
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.8.2';
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: series take the categorical
5
- palette in order, one series is one colour, `reference` series draw muted and
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 the palette in order. */
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 the palette in order. */
15
- export function seriesColor(i, colors) {
16
- return colors[i] || CATEGORICAL[i % CATEGORICAL.length];
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 { marked } from 'marked';
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(marked.parse(source, { gfm, breaks: true, async: false }) as string);
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;