@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,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, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
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,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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,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
|
-
|
|
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
|
|
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
|
|
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
|
},
|
package/dist/manifest.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schema": "ripple.manifest/v1",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"generatedAt": "2026-10-
|
|
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
|
|
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
|
|
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
|
-
|
|
94
|
-
--chart-
|
|
95
|
-
--chart-
|
|
96
|
-
--chart-
|
|
97
|
-
--chart-
|
|
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:
|
|
134
|
-
--chart-2:
|
|
135
|
-
--chart-3:
|
|
136
|
-
--chart-4:
|
|
137
|
-
--chart-5:
|
|
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, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
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
|
+
}
|