@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,226 @@
1
+ // discover/whatsapp-card.ts — the WhatsApp QR card (WhatsAppQrCard) as pure logic,
2
+ // shared by the app's /discover hero and the public Discover page:
3
+ // - the calling codes offered (sorted by country name), the default country from
4
+ // the browser's languages (en-GB → GB, else India), and the country a pasted
5
+ // international number starts with (longest code wins; a code several countries
6
+ // share, like +1, keeps the country already picked);
7
+ // - the number: a pasted "+44 7700 900123" or "0044…" splits into country and
8
+ // number; separators and trunk zeros go; the whole E.164 number is 8-15 digits;
9
+ // - the wa.me link (with the optional pre-filled message), the line printed under
10
+ // the shop name, and the download file names;
11
+ // - the printed card: a 148 × 105 mm counter card laid out once in millimetres
12
+ // (cardLayout) and drawn from that layout as SVG (cardSvg) and print page
13
+ // (cardPrintHtml) here, and as a PNG by ./whatsapp-card-export. The card is
14
+ // always dark ink on white paper, whatever the host theme.
15
+ // Nothing here touches the DOM, so it runs on the server too.
16
+ import { qrPath } from './whatsapp-qr.js';
17
+ export const DIAL_CODES = [
18
+ { iso: 'AF', name: 'Afghanistan', code: '93' },
19
+ { iso: 'DZ', name: 'Algeria', code: '213' },
20
+ { iso: 'AR', name: 'Argentina', code: '54' },
21
+ { iso: 'AU', name: 'Australia', code: '61' },
22
+ { iso: 'AT', name: 'Austria', code: '43' },
23
+ { iso: 'BH', name: 'Bahrain', code: '973' },
24
+ { iso: 'BD', name: 'Bangladesh', code: '880' },
25
+ { iso: 'BE', name: 'Belgium', code: '32' },
26
+ { iso: 'BR', name: 'Brazil', code: '55' },
27
+ { iso: 'CA', name: 'Canada', code: '1' },
28
+ { iso: 'CL', name: 'Chile', code: '56' },
29
+ { iso: 'CN', name: 'China', code: '86' },
30
+ { iso: 'CO', name: 'Colombia', code: '57' },
31
+ { iso: 'CZ', name: 'Czechia', code: '420' },
32
+ { iso: 'DK', name: 'Denmark', code: '45' },
33
+ { iso: 'EG', name: 'Egypt', code: '20' },
34
+ { iso: 'ET', name: 'Ethiopia', code: '251' },
35
+ { iso: 'FI', name: 'Finland', code: '358' },
36
+ { iso: 'FR', name: 'France', code: '33' },
37
+ { iso: 'DE', name: 'Germany', code: '49' },
38
+ { iso: 'GH', name: 'Ghana', code: '233' },
39
+ { iso: 'GR', name: 'Greece', code: '30' },
40
+ { iso: 'HK', name: 'Hong Kong', code: '852' },
41
+ { iso: 'HU', name: 'Hungary', code: '36' },
42
+ { iso: 'IN', name: 'India', code: '91' },
43
+ { iso: 'ID', name: 'Indonesia', code: '62' },
44
+ { iso: 'IQ', name: 'Iraq', code: '964' },
45
+ { iso: 'IE', name: 'Ireland', code: '353' },
46
+ { iso: 'IL', name: 'Israel', code: '972' },
47
+ { iso: 'IT', name: 'Italy', code: '39' },
48
+ { iso: 'JP', name: 'Japan', code: '81' },
49
+ { iso: 'JO', name: 'Jordan', code: '962' },
50
+ { iso: 'KE', name: 'Kenya', code: '254' },
51
+ { iso: 'KW', name: 'Kuwait', code: '965' },
52
+ { iso: 'LB', name: 'Lebanon', code: '961' },
53
+ { iso: 'MY', name: 'Malaysia', code: '60' },
54
+ { iso: 'MX', name: 'Mexico', code: '52' },
55
+ { iso: 'MA', name: 'Morocco', code: '212' },
56
+ { iso: 'NP', name: 'Nepal', code: '977' },
57
+ { iso: 'NL', name: 'Netherlands', code: '31' },
58
+ { iso: 'NZ', name: 'New Zealand', code: '64' },
59
+ { iso: 'NG', name: 'Nigeria', code: '234' },
60
+ { iso: 'NO', name: 'Norway', code: '47' },
61
+ { iso: 'OM', name: 'Oman', code: '968' },
62
+ { iso: 'PK', name: 'Pakistan', code: '92' },
63
+ { iso: 'PE', name: 'Peru', code: '51' },
64
+ { iso: 'PH', name: 'Philippines', code: '63' },
65
+ { iso: 'PL', name: 'Poland', code: '48' },
66
+ { iso: 'PT', name: 'Portugal', code: '351' },
67
+ { iso: 'QA', name: 'Qatar', code: '974' },
68
+ { iso: 'RO', name: 'Romania', code: '40' },
69
+ { iso: 'RU', name: 'Russia', code: '7' },
70
+ { iso: 'SA', name: 'Saudi Arabia', code: '966' },
71
+ { iso: 'SG', name: 'Singapore', code: '65' },
72
+ { iso: 'ZA', name: 'South Africa', code: '27' },
73
+ { iso: 'KR', name: 'South Korea', code: '82' },
74
+ { iso: 'ES', name: 'Spain', code: '34' },
75
+ { iso: 'LK', name: 'Sri Lanka', code: '94' },
76
+ { iso: 'SE', name: 'Sweden', code: '46' },
77
+ { iso: 'CH', name: 'Switzerland', code: '41' },
78
+ { iso: 'TW', name: 'Taiwan', code: '886' },
79
+ { iso: 'TZ', name: 'Tanzania', code: '255' },
80
+ { iso: 'TH', name: 'Thailand', code: '66' },
81
+ { iso: 'TR', name: 'Türkiye', code: '90' },
82
+ { iso: 'UG', name: 'Uganda', code: '256' },
83
+ { iso: 'UA', name: 'Ukraine', code: '380' },
84
+ { iso: 'AE', name: 'United Arab Emirates', code: '971' },
85
+ { iso: 'GB', name: 'United Kingdom', code: '44' },
86
+ { iso: 'US', name: 'United States', code: '1' },
87
+ { iso: 'VN', name: 'Vietnam', code: '84' },
88
+ { iso: 'ZW', name: 'Zimbabwe', code: '263' },
89
+ ];
90
+ export const DEFAULT_DIAL_ISO = 'IN';
91
+ export const dialCodeFor = (iso) => DIAL_CODES.find((d) => d.iso === iso) ?? DIAL_CODES.find((d) => d.iso === DEFAULT_DIAL_ISO);
92
+ /** The first browser language with a region we list (en-GB → GB), else India. */
93
+ export function defaultDialIso(languages) {
94
+ for (const tag of languages ?? []) {
95
+ const region = tag.split(/[-_]/)[1]?.toUpperCase();
96
+ if (region && DIAL_CODES.some((d) => d.iso === region))
97
+ return region;
98
+ }
99
+ return DEFAULT_DIAL_ISO;
100
+ }
101
+ /** The listed country an international number's digits start with, longest code
102
+ * first; `current` wins among countries that share the matched code. */
103
+ export function matchDialCode(digits, current) {
104
+ for (let len = 3; len >= 1; len--) {
105
+ const code = digits.slice(0, len);
106
+ const hits = DIAL_CODES.filter((d) => d.code === code);
107
+ if (hits.length)
108
+ return hits.find((d) => d.iso === current) ?? hits[0];
109
+ }
110
+ return null;
111
+ }
112
+ // ── The number and the link ─────────────────────────────────────────────────
113
+ export const SHOP_MAX = 28;
114
+ export const MESSAGE_MAX = 200;
115
+ export const DEFAULT_SHOP = 'Sharma Sweets';
116
+ export const DEFAULT_MESSAGE = "Hi! I'd like to place an order.";
117
+ /** The example number the server renders, so a page with JS off shows a real card. */
118
+ export const EXAMPLE_PHONE = '98765 43210';
119
+ /** E.164 allows at most 15 digits; below 8 no real number is complete. */
120
+ export const E164_MIN = 8;
121
+ export const E164_MAX = 15;
122
+ /** The national part as digits: separators dropped, trunk zeros dropped. */
123
+ export const nationalDigits = (raw) => raw.replace(/\D/g, '').replace(/^0+/, '');
124
+ /** A pasted international number ("+44 7700 900123", "0044 7700 900123") split into
125
+ * the listed country it starts with and the rest. Null for a national number, or an
126
+ * international one whose code we don't list. */
127
+ export function splitInternational(raw, currentIso) {
128
+ const t = raw.trim();
129
+ if (!t.startsWith('+') && !t.startsWith('00'))
130
+ return null;
131
+ const digits = t.replace(/\D/g, '').replace(/^00/, '');
132
+ const match = matchDialCode(digits, currentIso);
133
+ if (!match)
134
+ return null;
135
+ return { iso: match.iso, national: nationalDigits(digits.slice(match.code.length)) };
136
+ }
137
+ /** "+91 98765 43210": ten-digit numbers split 5 + 5, others in fours from the end. */
138
+ export function formatLine(code, national) {
139
+ const groups = national.length === 10 ? [national.slice(0, 5), national.slice(5)] : (national.match(/\d{1,4}(?=(\d{4})*$)/g) ?? []);
140
+ return `+${code} ${groups.join(' ')}`;
141
+ }
142
+ export function checkPhone(code, raw) {
143
+ const national = nationalDigits(raw);
144
+ if (!national)
145
+ return { ok: false, error: raw.trim() ? 'Enter the number in digits.' : null };
146
+ const total = code.length + national.length;
147
+ if (total < E164_MIN || national.length < 4)
148
+ return { ok: false, error: 'That number looks too short.' };
149
+ if (total > E164_MAX)
150
+ return { ok: false, error: 'That number is too long.' };
151
+ return { ok: true, digits: code + national, line: formatLine(code, national) };
152
+ }
153
+ /** https://wa.me/<digits>, with ?text= when there is a message. */
154
+ export function waLink(digits, message) {
155
+ const text = message.trim().slice(0, MESSAGE_MAX);
156
+ return `https://wa.me/${digits}${text ? `?text=${encodeURIComponent(text)}` : ''}`;
157
+ }
158
+ /** `<shop>-whatsapp-qr.<ext>`, slugified; `whatsapp-qr.<ext>` for a name with no letters. */
159
+ export function cardFilename(shop, ext) {
160
+ const slug = shop
161
+ .toLowerCase()
162
+ .replace(/[^a-z0-9]+/g, '-')
163
+ .replace(/^-+|-+$/g, '')
164
+ .slice(0, 50);
165
+ return `${slug ? `${slug}-` : ''}whatsapp-qr.${ext}`;
166
+ }
167
+ // ── The printed card ────────────────────────────────────────────────────────
168
+ export const CARD_MM = { width: 148, height: 105 };
169
+ export const PNG_DPI = 300;
170
+ /** Pixels per millimetre at the PNG's resolution. */
171
+ export const PX_PER_MM = PNG_DPI / 25.4;
172
+ /** 148 × 105 mm at 300 dpi. */
173
+ export const CARD_PX = {
174
+ width: Math.round(CARD_MM.width * PX_PER_MM),
175
+ height: Math.round(CARD_MM.height * PX_PER_MM),
176
+ };
177
+ export const CARD_INK = '#111111';
178
+ export const CARD_PAPER = '#ffffff';
179
+ export const CARD_MUTED = '#555555';
180
+ export const CARD_EDGE = '#d4d4d4';
181
+ export const CARD_FONT = 'Helvetica, Arial, sans-serif';
182
+ export const CARD_CTA = 'Scan to order on WhatsApp →';
183
+ export const DEFAULT_BRAND = 'Paw';
184
+ /** The small print at the card's foot, naming the host's product. */
185
+ export const cardFooter = (brand = DEFAULT_BRAND) => `Made with ${brand} · pocketpaw.xyz`;
186
+ const MARGIN = 10;
187
+ const QR_SIDE = 56;
188
+ const COLUMN = CARD_MM.width - MARGIN * 3 - QR_SIDE;
189
+ /** A rough average glyph width for a bold sans, as a share of the font size. */
190
+ const BOLD_GLYPH = 0.6;
191
+ export function cardLayout(input) {
192
+ const shop = input.shop.trim() || 'Your shop';
193
+ const shopSize = Math.min(10, COLUMN / (BOLD_GLYPH * Math.max(1, shop.length)));
194
+ return {
195
+ width: CARD_MM.width,
196
+ height: CARD_MM.height,
197
+ texts: [
198
+ { text: shop, x: MARGIN, y: 34, size: shopSize, weight: 700, color: CARD_INK, maxWidth: COLUMN },
199
+ { text: input.line, x: MARGIN, y: 46, size: 5, weight: 400, color: CARD_INK, maxWidth: COLUMN },
200
+ { text: CARD_CTA, x: MARGIN, y: 62, size: 3.6, weight: 700, color: CARD_INK, maxWidth: COLUMN },
201
+ { text: cardFooter(input.brand), x: MARGIN, y: CARD_MM.height - MARGIN, size: 3, weight: 400, color: CARD_MUTED, maxWidth: COLUMN },
202
+ ],
203
+ qr: { x: CARD_MM.width - MARGIN - QR_SIDE, y: (CARD_MM.height - QR_SIDE) / 2, size: QR_SIDE, path: qrPath(input.link) },
204
+ };
205
+ }
206
+ const xml = (s) => s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
207
+ const n = (v) => String(Math.round(v * 1000) / 1000);
208
+ /** The card as a standalone SVG document, sized in millimetres for print. */
209
+ export function cardSvg(layout) {
210
+ const { qr } = layout;
211
+ const texts = layout.texts
212
+ .map((t) => `<text x="${n(t.x)}" y="${n(t.y)}" font-family="${CARD_FONT}" font-size="${n(t.size)}" font-weight="${t.weight}" fill="${t.color}">${xml(t.text)}</text>`)
213
+ .join('');
214
+ return (`<svg xmlns="http://www.w3.org/2000/svg" width="${layout.width}mm" height="${layout.height}mm" viewBox="0 0 ${layout.width} ${layout.height}">` +
215
+ `<rect x="0.15" y="0.15" width="${n(layout.width - 0.3)}" height="${n(layout.height - 0.3)}" rx="3" fill="${CARD_PAPER}" stroke="${CARD_EDGE}" stroke-width="0.3"/>` +
216
+ texts +
217
+ `<g transform="translate(${n(qr.x)} ${n(qr.y)}) scale(${n(qr.size / qr.path.size)})"><path d="${qr.path.d}" fill="${CARD_INK}" shape-rendering="crispEdges"/></g>` +
218
+ `</svg>`);
219
+ }
220
+ /** The print document: one card on a 148 × 105 mm page, nothing else. */
221
+ export function cardPrintHtml(layout, title) {
222
+ const safeTitle = title.replace(/[<&]/g, '');
223
+ return (`<!doctype html><html><head><meta charset="utf-8"><title>${safeTitle}</title>` +
224
+ `<style>@page{size:148mm 105mm;margin:0}html,body{margin:0;padding:0;background:${CARD_PAPER}}svg{display:block;width:148mm;height:105mm}</style>` +
225
+ `</head><body>${cardSvg(layout)}</body></html>`);
226
+ }
@@ -0,0 +1,7 @@
1
+ export interface QrPath {
2
+ /** Modules per side. */
3
+ size: number;
4
+ /** SVG path data in module units. */
5
+ d: string;
6
+ }
7
+ export declare function qrPath(text: string): QrPath;
@@ -0,0 +1,20 @@
1
+ // discover/whatsapp-qr.ts — the one QR encoder behind WhatsAppQrCard: text in, an
2
+ // SVG path out (one 1×1 square per dark module, in module units, no quiet zone). The
3
+ // same path draws the on-screen preview, the downloadable SVG and, through Path2D,
4
+ // the PNG, so all three come from the same code. Synchronous and DOM-free, so a
5
+ // $derived can call it and the server renders a real QR.
6
+ //
7
+ // Uses the package's existing `qrcode-svg` dependency for the module matrix only
8
+ // (its own SVG writer is not used), error correction M. Its matrix is indexed
9
+ // [row][col]; the path draws x = col, y = row (qrcode-svg's own writer transposes).
10
+ import QRCode from 'qrcode-svg';
11
+ export function qrPath(text) {
12
+ const modules = new QRCode({ content: text, ecl: 'M', padding: 0 }).qrcode.modules;
13
+ let d = '';
14
+ for (let r = 0; r < modules.length; r++) {
15
+ for (let c = 0; c < modules.length; c++)
16
+ if (modules[r][c])
17
+ d += `M${c} ${r}h1v1h-1z`;
18
+ }
19
+ return { size: modules.length, d };
20
+ }
@@ -24,6 +24,7 @@
24
24
  // `scripts/capture-figure.ts` and `scripts/score-figure.ts` read.
25
25
  import * as k from '@ripple-ui/core/iso';
26
26
  import { frameOf, framed, percentIn } from '../frame.js';
27
+ import { addFigureStyle } from '../style.js';
27
28
  import { clampParams, crankSpeedOf, describe, poseAt, poseOf, ratioOf, readoutsOf, ringTeethAt, teethAt, CASSETTE, RINGS } from './model.js';
28
29
  export const WIDTH = 640;
29
30
  export const HEIGHT = 420;
@@ -584,12 +585,7 @@ function anchorPoints() {
584
585
  const STYLE_ID = 'ripple-iso-bike-css';
585
586
  export function mount(el, { theme = 'dark', params: initial, onReadout } = {}) {
586
587
  const doc = el.ownerDocument;
587
- if (!doc.getElementById(STYLE_ID)) {
588
- const style = doc.createElement('style');
589
- style.id = STYLE_ID;
590
- style.textContent = k.ISO_CSS + BIKE_CSS;
591
- doc.head.appendChild(style);
592
- }
588
+ addFigureStyle(el, STYLE_ID, k.ISO_CSS + BIKE_CSS);
593
589
  let params = clampParams(initial);
594
590
  const svg = doc.importNode(new DOMParser().parseFromString(bikeSvg(params), 'image/svg+xml').documentElement, true);
595
591
  el.classList.add('iso');
@@ -21,6 +21,7 @@
21
21
  // No DOM or work at module top level.
22
22
  import * as k from '@ripple-ui/core/iso';
23
23
  import { frameOf, framed, percentIn } from '../frame.js';
24
+ import { addFigureStyle } from '../style.js';
24
25
  import { clampParams, describe, readoutsOf, stateOf } from './model.js';
25
26
  export const WIDTH = 640;
26
27
  export const HEIGHT = 420;
@@ -685,12 +686,7 @@ const SPIN = { pump: 5, turbine: 6, fan: 2.4 };
685
686
  const DASH = 7;
686
687
  export function mount(el, { theme = 'dark', params: initial, onReadout } = {}) {
687
688
  const doc = el.ownerDocument;
688
- if (!doc.getElementById(STYLE_ID)) {
689
- const style = doc.createElement('style');
690
- style.id = STYLE_ID;
691
- style.textContent = k.ISO_CSS + REACTOR_CSS;
692
- doc.head.appendChild(style);
693
- }
689
+ addFigureStyle(el, STYLE_ID, k.ISO_CSS + REACTOR_CSS);
694
690
  let params = clampParams(initial);
695
691
  const svg = doc.importNode(new DOMParser().parseFromString(reactorSvg(params), 'image/svg+xml').documentElement, true);
696
692
  el.classList.add('iso');
@@ -15,6 +15,7 @@
15
15
  // oxygen-poor, red oxygen-rich). No DOM at module top level.
16
16
  import * as k from '@ripple-ui/core/iso';
17
17
  import { frameOf, framed, percentIn } from '../frame.js';
18
+ import { addFigureStyle } from '../style.js';
18
19
  import { clampParams, describe, readoutsOf, stateAt, stillTime, timingOf } from './model.js';
19
20
  export const WIDTH = 640;
20
21
  export const HEIGHT = 420;
@@ -442,12 +443,7 @@ const STYLE_ID = 'ripple-iso-heart-css';
442
443
  const FLOW_PX = 0.22; // dash px per ml/s
443
444
  export function mount(el, { theme = 'dark', params: initial, onReadout } = {}) {
444
445
  const doc = el.ownerDocument;
445
- if (!doc.getElementById(STYLE_ID)) {
446
- const style = doc.createElement('style');
447
- style.id = STYLE_ID;
448
- style.textContent = k.ISO_CSS + HEART_CSS;
449
- doc.head.appendChild(style);
450
- }
446
+ addFigureStyle(el, STYLE_ID, k.ISO_CSS + HEART_CSS);
451
447
  let params = clampParams(initial);
452
448
  const parsed = new DOMParser().parseFromString(heartSvg(params), 'image/svg+xml').documentElement;
453
449
  const svg = doc.importNode(parsed, true);
@@ -20,6 +20,7 @@
20
20
  // work at module top level.
21
21
  import * as k from '@ripple-ui/core/iso';
22
22
  import { frameOf, framed, percentIn } from '../frame.js';
23
+ import { addFigureStyle } from '../style.js';
23
24
  import { beamSpeedOf, clampParams, densityOf, describe, readoutsOf, thrustOf, DRIFT, STILL_T } from './model.js';
24
25
  export const WIDTH = 640;
25
26
  export const HEIGHT = 420;
@@ -529,12 +530,7 @@ const STYLE_ID = 'ripple-iso-ion-thruster-css';
529
530
  const XENON_PX = 5; // dash px a second per mg/s
530
531
  export function mount(el, { theme = 'dark', params: initial, onReadout } = {}) {
531
532
  const doc = el.ownerDocument;
532
- if (!doc.getElementById(STYLE_ID)) {
533
- const style = doc.createElement('style');
534
- style.id = STYLE_ID;
535
- style.textContent = k.ISO_CSS + ION_CSS;
536
- doc.head.appendChild(style);
537
- }
533
+ addFigureStyle(el, STYLE_ID, k.ISO_CSS + ION_CSS);
538
534
  let params = clampParams(initial);
539
535
  const svg = doc.importNode(new DOMParser().parseFromString(ionSvg(params), 'image/svg+xml').documentElement, true);
540
536
  el.classList.add('iso');
@@ -20,6 +20,7 @@
20
20
  // work at module top level.
21
21
  import * as k from '@ripple-ui/core/iso';
22
22
  import { frameOf, framed, percentIn } from '../frame.js';
23
+ import { addFigureStyle } from '../style.js';
23
24
  import { bunchesAt, burstsAt, clampParams, describe, fieldOf, readoutsOf, stillTime } from './model.js';
24
25
  export const WIDTH = 640;
25
26
  export const HEIGHT = 420;
@@ -669,12 +670,7 @@ function anchorPoints() {
669
670
  const STYLE_ID = 'ripple-iso-collider-css';
670
671
  export function mount(el, { theme = 'dark', params: initial, onReadout } = {}) {
671
672
  const doc = el.ownerDocument;
672
- if (!doc.getElementById(STYLE_ID)) {
673
- const style = doc.createElement('style');
674
- style.id = STYLE_ID;
675
- style.textContent = k.ISO_CSS + COLLIDER_CSS;
676
- doc.head.appendChild(style);
677
- }
673
+ addFigureStyle(el, STYLE_ID, k.ISO_CSS + COLLIDER_CSS);
678
674
  let params = clampParams(initial);
679
675
  const win = doc.defaultView;
680
676
  const still = win?.matchMedia?.('(prefers-reduced-motion: reduce)');
@@ -19,6 +19,7 @@
19
19
  // needle, and moves the flow dashes. No DOM or work at module top level.
20
20
  import * as k from '@ripple-ui/core/iso';
21
21
  import { frameOf, framed, percentIn } from '../frame.js';
22
+ import { addFigureStyle } from '../style.js';
22
23
  import { clampParams, describe, plumeOf, readoutsOf, shaftSpeedOf, stateOf } from './model.js';
23
24
  export const WIDTH = 640;
24
25
  export const HEIGHT = 420;
@@ -766,12 +767,7 @@ const STYLE_ID = 'ripple-iso-rocket-css';
766
767
  const FLOW_PX = 0.08; // dash px a second per kg/s
767
768
  export function mount(el, { theme = 'dark', params: initial, onReadout } = {}) {
768
769
  const doc = el.ownerDocument;
769
- if (!doc.getElementById(STYLE_ID)) {
770
- const style = doc.createElement('style');
771
- style.id = STYLE_ID;
772
- style.textContent = k.ISO_CSS + ENGINE_CSS;
773
- doc.head.appendChild(style);
774
- }
770
+ addFigureStyle(el, STYLE_ID, k.ISO_CSS + ENGINE_CSS);
775
771
  let params = clampParams(initial);
776
772
  const svg = doc.importNode(new DOMParser().parseFromString(engineSvg(params), 'image/svg+xml').documentElement, true);
777
773
  el.classList.add('iso');
@@ -0,0 +1,2 @@
1
+ /** Adds `css` as `<style id={id}>` to the root `el` renders in, unless that root already has it. */
2
+ export declare function addFigureStyle(el: HTMLElement, id: string, css: string): void;
@@ -0,0 +1,18 @@
1
+ // figures/style.ts: puts a figure's stylesheet where the figure can see it. A
2
+ // figure inside a shadow root (the @ripple-ui/embed card) never reads
3
+ // document.head, so its <style> goes into that shadow root; anywhere else it
4
+ // goes into the document head. One copy per root, keyed by the style id, so two
5
+ // embeds on a page each get their own. The embed's destroy() empties its root,
6
+ // which takes the copy with it; the next mount adds it back.
7
+ /** Adds `css` as `<style id={id}>` to the root `el` renders in, unless that root already has it. */
8
+ export function addFigureStyle(el, id, css) {
9
+ const doc = el.ownerDocument;
10
+ const root = el.getRootNode();
11
+ const shadow = root instanceof ShadowRoot ? root : null;
12
+ if ((shadow ?? doc).getElementById(id))
13
+ return;
14
+ const style = doc.createElement('style');
15
+ style.id = id;
16
+ style.textContent = css;
17
+ (shadow ?? doc.head).appendChild(style);
18
+ }
@@ -26,6 +26,7 @@
26
26
  // from the model's temperature. No DOM or work at module top level.
27
27
  import * as k from '@ripple-ui/core/iso';
28
28
  import { frameOf, framed, percentIn } from '../frame.js';
29
+ import { addFigureStyle } from '../style.js';
29
30
  import { beamSpeedOf, clampParams, describe, glowOf, readoutsOf, solve, streamSpeedOf, twistAt } from './model.js';
30
31
  export const WIDTH = 640;
31
32
  export const HEIGHT = 420;
@@ -667,12 +668,7 @@ function anchorPoints() {
667
668
  const STYLE_ID = 'ripple-iso-tokamak-css';
668
669
  export function mount(el, { theme = 'dark', params: initial, onReadout } = {}) {
669
670
  const doc = el.ownerDocument;
670
- if (!doc.getElementById(STYLE_ID)) {
671
- const style = doc.createElement('style');
672
- style.id = STYLE_ID;
673
- style.textContent = k.ISO_CSS + TOKAMAK_CSS;
674
- doc.head.appendChild(style);
675
- }
671
+ addFigureStyle(el, STYLE_ID, k.ISO_CSS + TOKAMAK_CSS);
676
672
  let params = clampParams(initial);
677
673
  const svg = doc.importNode(new DOMParser().parseFromString(tokamakSvg(params), 'image/svg+xml').documentElement, true);
678
674
  el.classList.add('iso');
@@ -7,7 +7,7 @@ export const chartEntry = {
7
7
  type: { type: '"bar" | "line" | "area" | "pie" | "donut" | "candlestick" | "sparkline" | "heatmap" | "gauge" | "radar"', required: false, description: 'Chart type. Default "bar".' },
8
8
  title: { type: 'string', required: false, description: 'Chart title.' },
9
9
  height: { type: 'number', required: false, description: 'Height in px. Default 200.' },
10
- colors: { type: 'string[]', required: false, description: 'Palette in order: one per series when points carry `series`, one per slice on pie/donut. On one series (bar, line) every point wears the first colour; give two or more to colour points individually. Default palette is categorical, no status colours.' },
10
+ colors: { type: 'string[]', required: false, description: 'Palette in order: one per series when points carry `series`, one per slice on pie/donut. On one series (bar, line) every point wears the first colour; give two or more to colour points individually. Default palette is the theme\'s chart-1 to chart-5 (brand blue first), then categorical; no status colours.' },
11
11
  reference: { type: 'string | string[]', required: false, description: 'Series keys that are a reference, not data (a target, plan or benchmark): drawn muted and dashed, outside the palette order.' },
12
12
  tooltip: { type: 'boolean', required: false, description: 'Show tooltip on hover. Default true.' },
13
13
  },
@@ -1,9 +1,9 @@
1
1
  export const markdownEntry = {
2
2
  type: 'markdown',
3
3
  category: 'display',
4
- description: 'Render markdown with GFM support (tables, strikethrough, autolinks, task lists).',
4
+ description: 'Render markdown with GFM (tables, strikethrough, autolinks, task lists), LaTeX math and mermaid diagrams.',
5
5
  props: {
6
- content: { type: 'string', required: false, description: 'Markdown source.' },
6
+ content: { type: 'string', required: false, description: 'Markdown source. Math: $x$ inline, $$x$$ on its own lines for display; a bare number like $5 stays text. A ```mermaid fence draws as a diagram.' },
7
7
  text: { type: 'string', required: false, description: 'Alias for content.' },
8
8
  gfm: { type: 'boolean', required: false, description: 'Enable GitHub-Flavored Markdown. Default true.' },
9
9
  },
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "schema": "ripple.manifest/v1",
3
- "version": "0.8.2",
4
- "generatedAt": "2026-10-28T15:31:10.672Z",
3
+ "version": "0.9.0-next.0",
4
+ "generatedAt": "2026-10-31T10:56:56.990Z",
5
5
  "spec": {
6
6
  "uiField": "ui",
7
7
  "stateField": "state",
@@ -1907,7 +1907,7 @@
1907
1907
  "colors": {
1908
1908
  "type": "string[]",
1909
1909
  "required": false,
1910
- "description": "Palette in order: one per series when points carry `series`, one per slice on pie/donut. On one series (bar, line) every point wears the first colour; give two or more to colour points individually. Default palette is categorical, no status colours."
1910
+ "description": "Palette in order: one per series when points carry `series`, one per slice on pie/donut. On one series (bar, line) every point wears the first colour; give two or more to colour points individually. Default palette is the theme's chart-1 to chart-5 (brand blue first), then categorical; no status colours."
1911
1911
  },
1912
1912
  "reference": {
1913
1913
  "type": "string | string[]",
@@ -8287,12 +8287,12 @@
8287
8287
  {
8288
8288
  "type": "markdown",
8289
8289
  "category": "display",
8290
- "description": "Render markdown with GFM support (tables, strikethrough, autolinks, task lists).",
8290
+ "description": "Render markdown with GFM (tables, strikethrough, autolinks, task lists), LaTeX math and mermaid diagrams.",
8291
8291
  "props": {
8292
8292
  "content": {
8293
8293
  "type": "string",
8294
8294
  "required": false,
8295
- "description": "Markdown source."
8295
+ "description": "Markdown source. Math: $x$ inline, $$x$$ on its own lines for display; a bare number like $5 stays text. A ```mermaid fence draws as a diagram."
8296
8296
  },
8297
8297
  "text": {
8298
8298
  "type": "string",
package/dist/styles.css CHANGED
@@ -90,11 +90,12 @@
90
90
  --border: hsl(0 0% 89.8%);
91
91
  --input: hsl(0 0% 89.8%);
92
92
  --ring: hsl(0 0% 3.9%);
93
- --chart-1: hsl(12 76% 61%);
94
- --chart-2: hsl(173 58% 39%);
95
- --chart-3: hsl(197 37% 24%);
96
- --chart-4: hsl(43 74% 66%);
97
- --chart-5: hsl(27 87% 67%);
93
+ /* Brand-led categorical slots: Paw blue first, then teal, amber, violet, slate. */
94
+ --chart-1: #1877f2;
95
+ --chart-2: #0d9488;
96
+ --chart-3: #d97706;
97
+ --chart-4: #7c3aed;
98
+ --chart-5: #64748b;
98
99
  --radius: 0.625rem;
99
100
  --sidebar-background: hsl(0 0% 98%);
100
101
  --sidebar-foreground: hsl(240 5.3% 26.1%);
@@ -130,11 +131,11 @@
130
131
  --border: hsl(0 0% 14.9%);
131
132
  --input: hsl(0 0% 14.9%);
132
133
  --ring: hsl(0 0% 83.1%);
133
- --chart-1: hsl(220 70% 50%);
134
- --chart-2: hsl(160 60% 45%);
135
- --chart-3: hsl(30 80% 55%);
136
- --chart-4: hsl(280 65% 60%);
137
- --chart-5: hsl(340 75% 55%);
134
+ --chart-1: #4c96ff;
135
+ --chart-2: #2dd4bf;
136
+ --chart-3: #fbbf24;
137
+ --chart-4: #a78bfa;
138
+ --chart-5: #94a3b8;
138
139
  --sidebar-background: hsl(240 5.9% 10%);
139
140
  --sidebar-foreground: hsl(240 4.8% 95.9%);
140
141
  --sidebar-primary: hsl(224.3 76.3% 48%);
@@ -0,0 +1,6 @@
1
+ import type { MarkedExtension } from 'marked';
2
+ export declare const MATH_CLASS = "ripple-math";
3
+ export declare const escapeHtml: (s: string) => string;
4
+ export declare function markedMath(): MarkedExtension;
5
+ /** Wrap ```mermaid fences for the widget to draw; every other fence renders as usual. */
6
+ export declare function markedMermaid(): MarkedExtension;
@@ -0,0 +1,68 @@
1
+ export const MATH_CLASS = 'ripple-math';
2
+ export const escapeHtml = (s) => s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
3
+ const INLINE = [
4
+ [/^\$\$((?:\\[\s\S]|[^\\$])+?)\$\$/, true],
5
+ [/^\$(?![\s$])((?:\\.|[^\\$\n])+?)(?<!\s)\$(?!\d)/, false],
6
+ [/^\\\(([\s\S]+?)\\\)/, false]
7
+ ];
8
+ const BLOCK = /^(?:\$\$([\s\S]+?)\$\$|\\\[([\s\S]+?)\\\])[ \t]*(?:\n+|$)/;
9
+ const PLAIN_NUMBER = /^[\d\s.,]+$/;
10
+ function placeholder(token, tag) {
11
+ const { tex, display, raw } = token;
12
+ const cls = display ? `${MATH_CLASS} ${MATH_CLASS}-display` : MATH_CLASS;
13
+ return `<${tag} class="${cls}" data-tex="${escapeHtml(tex)}"${display ? ' data-display=""' : ''}>${escapeHtml(raw.trim())}</${tag}>${tag === 'div' ? '\n' : ''}`;
14
+ }
15
+ export function markedMath() {
16
+ return {
17
+ extensions: [
18
+ {
19
+ name: 'mathBlock',
20
+ level: 'block',
21
+ start: (src) => src.match(/^ {0,3}(?:\$\$|\\\[)/m)?.index,
22
+ tokenizer(src) {
23
+ const m = BLOCK.exec(src);
24
+ if (!m)
25
+ return undefined;
26
+ const tex = (m[1] ?? m[2]).trim();
27
+ if (!tex)
28
+ return undefined;
29
+ return { type: 'mathBlock', raw: m[0], tex, display: true };
30
+ },
31
+ renderer: (token) => placeholder(token, 'div')
32
+ },
33
+ {
34
+ name: 'mathInline',
35
+ level: 'inline',
36
+ start(src) {
37
+ const m = /(?<!\\)(?:\$|\\\()/.exec(src);
38
+ return m?.index;
39
+ },
40
+ tokenizer(src) {
41
+ for (const [re, display] of INLINE) {
42
+ const m = re.exec(src);
43
+ if (!m)
44
+ continue;
45
+ const tex = m[1].trim();
46
+ if (!tex || PLAIN_NUMBER.test(tex))
47
+ return undefined;
48
+ return { type: 'mathInline', raw: m[0], tex, display };
49
+ }
50
+ return undefined;
51
+ },
52
+ renderer: (token) => placeholder(token, 'span')
53
+ }
54
+ ]
55
+ };
56
+ }
57
+ /** Wrap ```mermaid fences for the widget to draw; every other fence renders as usual. */
58
+ export function markedMermaid() {
59
+ return {
60
+ renderer: {
61
+ code({ text, lang }) {
62
+ if ((lang ?? '').split(/\s/)[0] !== 'mermaid')
63
+ return false;
64
+ return `<div class="ripple-diagram"><pre><code class="language-mermaid">${escapeHtml(text)}</code></pre></div>\n`;
65
+ }
66
+ }
67
+ };
68
+ }