@khanglvm/relay 0.2.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.
@@ -0,0 +1,21 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <meta name="color-scheme" content="light dark">
7
+ <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Crect width='16' height='16' rx='4' fill='%23C2674B'/%3E%3Ctext x='8' y='12' font-size='10' text-anchor='middle' fill='%23FCFAF7' font-family='Georgia,serif'%3Er%3C/text%3E%3C/svg%3E">
8
+ <title>__TITLE__ · relay</title>
9
+ <style>/*__CSS__*/
10
+ /*__BLOCKS_CSS__*/
11
+ /*__ANNOTATE_CSS__*/</style>
12
+ </head>
13
+ <body>
14
+ <script id="boot" type="application/json">__BOOT_JSON__</script>
15
+ <div class="banner" id="banner">This board is closed — the server has stopped.</div>
16
+ <div id="app" class="wrap"></div>
17
+ <script>/*__BLOCKS_JS__*/</script>
18
+ <script>/*__ANNOTATE_JS__*/</script>
19
+ <script>/*__APP_JS__*/</script>
20
+ </body>
21
+ </html>
package/src/ui/kit.js ADDED
@@ -0,0 +1,413 @@
1
+ /**
2
+ * relay kit.js — loaded INSIDE sandboxed custom-HTML iframes via <script src="/kit.js">.
3
+ * NO access to the parent DOM (sandbox without allow-same-origin) — only postMessage.
4
+ *
5
+ * Public API (window.relayKit and window.kit):
6
+ *
7
+ * relayKit.theme
8
+ * Parsed from ?theme=light|dark in location.search.
9
+ * { mode: 'light'|'dark',
10
+ * colors: { bg, card, fg, fg2, muted, accent, accentSoft, border, danger, ok } }
11
+ *
12
+ * relayKit.applyBaseStyles()
13
+ * Injects <style> into document.head setting body font/bg/color from current theme.
14
+ *
15
+ * relayKit.chart(el, cfgOrSimple, opts?) -> Promise<ChartInstance>
16
+ * Lazy-loads /vendor/chart.umd.js (cached).
17
+ * Accepts either a full Chart.js config object OR the simplified relay shape:
18
+ * { kind, labels, series:[{label,data,color?}], title?, height? }
19
+ * Creates a <canvas> inside el. opts.height overrides height (default 300).
20
+ * Returns Promise resolving to the Chart instance.
21
+ *
22
+ * relayKit.mermaid(el, code) -> Promise<SVGElement>
23
+ * Lazy-loads /vendor/mermaid.min.js (cached).
24
+ * Theme: dark mode -> 'dark', light -> 'neutral'. securityLevel 'strict'.
25
+ * Renders svg into el. On error renders muted error text inside el.
26
+ * Returns Promise resolving to the svg element.
27
+ *
28
+ * relayKit.table(el, { columns, rows, sortable? })
29
+ * Normalizes and renders an HTML table inside el.
30
+ * columns: string[] | [{key,label,align?}]
31
+ * rows: any[][] | object[]
32
+ * sortable: click column header to sort ascending/descending.
33
+ *
34
+ * relayKit.commentable(el, label, detail?)
35
+ * Hover: 1px dashed accent outline + cursor pointer + title "add a comment".
36
+ * Click: parent.postMessage({relay:'annotate-request', label, detail}, '*').
37
+ */
38
+
39
+ (function () {
40
+ 'use strict';
41
+
42
+ // ---------------------------------------------------------------------------
43
+ // Theme
44
+ // ---------------------------------------------------------------------------
45
+
46
+ const COLORS = {
47
+ light: {
48
+ bg: '#fcfbf9',
49
+ card: '#ffffff',
50
+ fg: '#1c1b19',
51
+ fg2: '#57534e',
52
+ muted: '#8a8580',
53
+ accent: '#c2674b',
54
+ accentSoft: '#f5e9e3',
55
+ border: '#ece9e4',
56
+ danger: '#bc4434',
57
+ ok: '#4d8a66',
58
+ },
59
+ dark: {
60
+ bg: '#201e1c',
61
+ card: '#282624',
62
+ fg: '#edeae4',
63
+ fg2: '#beb9b0',
64
+ muted: '#a29c93',
65
+ accent: '#d08159',
66
+ accentSoft: '#3b2e26',
67
+ border: '#393632',
68
+ danger: '#e06c57',
69
+ ok: '#6fbf92',
70
+ },
71
+ };
72
+
73
+ const CHART_PALETTE = {
74
+ light: ['#c2674b', '#4d8a66', '#5a7ca8', '#b9913f', '#8a6da3', '#57534e'],
75
+ dark: ['#d08159', '#6fbf92', '#7a9dc8', '#d4aa5f', '#a98dc3', '#a29c93'],
76
+ };
77
+
78
+ const SANS = '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif';
79
+
80
+ function parseThemeMode() {
81
+ try {
82
+ const params = new URLSearchParams(location.search);
83
+ const t = params.get('theme');
84
+ if (t === 'dark') return 'dark';
85
+ if (t === 'light') return 'light';
86
+ } catch (_) {}
87
+ return 'light';
88
+ }
89
+
90
+ const mode = parseThemeMode();
91
+
92
+ const theme = {
93
+ mode,
94
+ colors: COLORS[mode],
95
+ };
96
+
97
+ function applyBaseStyles() {
98
+ try {
99
+ const c = theme.colors;
100
+ const style = document.createElement('style');
101
+ style.textContent = [
102
+ `body {`,
103
+ ` background: ${c.bg};`,
104
+ ` color: ${c.fg};`,
105
+ ` font: 16px/1.6 ${SANS};`,
106
+ ` -webkit-font-smoothing: antialiased;`,
107
+ ` margin: 0; padding: 8px;`,
108
+ ` box-sizing: border-box;`,
109
+ `}`,
110
+ `*, *::before, *::after { box-sizing: inherit; }`,
111
+ ].join('\n');
112
+ document.head.appendChild(style);
113
+ } catch (err) {
114
+ console.warn('[relayKit] applyBaseStyles error:', err);
115
+ }
116
+ }
117
+
118
+ // ---------------------------------------------------------------------------
119
+ // Lazy vendor loader
120
+ // ---------------------------------------------------------------------------
121
+
122
+ const _scriptCache = {};
123
+
124
+ function loadScript(src) {
125
+ if (_scriptCache[src]) return _scriptCache[src];
126
+ _scriptCache[src] = new Promise((resolve, reject) => {
127
+ const s = document.createElement('script');
128
+ s.src = src;
129
+ s.onload = resolve;
130
+ s.onerror = () => reject(new Error('Failed to load: ' + src));
131
+ document.head.appendChild(s);
132
+ });
133
+ return _scriptCache[src];
134
+ }
135
+
136
+ // ---------------------------------------------------------------------------
137
+ // chart()
138
+ // ---------------------------------------------------------------------------
139
+
140
+ function normalizeChartConfig(cfgOrSimple) {
141
+ // If it already looks like a full Chart.js config (has .type), pass through
142
+ if (cfgOrSimple && cfgOrSimple.type && cfgOrSimple.data) {
143
+ return cfgOrSimple;
144
+ }
145
+ // Simplified relay shape
146
+ const s = cfgOrSimple || {};
147
+ const kind = s.kind || 'bar';
148
+ const palette = CHART_PALETTE[mode];
149
+ const datasets = (s.series || []).map((serie, i) => ({
150
+ label: serie.label || '',
151
+ data: serie.data || [],
152
+ backgroundColor: serie.color || palette[i % palette.length],
153
+ borderColor: serie.color || palette[i % palette.length],
154
+ borderWidth: kind === 'line' ? 2 : 0,
155
+ fill: false,
156
+ tension: 0.3,
157
+ }));
158
+ const cfg = {
159
+ type: kind,
160
+ data: {
161
+ labels: s.labels || [],
162
+ datasets,
163
+ },
164
+ options: {
165
+ responsive: true,
166
+ maintainAspectRatio: false,
167
+ plugins: {
168
+ legend: { labels: { color: theme.colors.fg, font: { family: SANS } } },
169
+ title: s.title
170
+ ? { display: true, text: s.title, color: theme.colors.fg, font: { family: SANS, size: 14 } }
171
+ : { display: false },
172
+ },
173
+ scales: {},
174
+ },
175
+ };
176
+ // Add scale colors for cartesian charts
177
+ if (['bar', 'line', 'scatter'].includes(kind)) {
178
+ cfg.options.scales = {
179
+ x: { ticks: { color: theme.colors.muted }, grid: { color: theme.colors.border } },
180
+ y: { ticks: { color: theme.colors.muted }, grid: { color: theme.colors.border } },
181
+ };
182
+ }
183
+ return cfg;
184
+ }
185
+
186
+ async function chart(el, cfgOrSimple, opts) {
187
+ try {
188
+ await loadScript('/vendor/chart.umd.js');
189
+ const Chart = window.Chart;
190
+ if (!Chart) throw new Error('Chart.js did not expose window.Chart');
191
+
192
+ const height = (opts && opts.height) || (cfgOrSimple && cfgOrSimple.height) || 300;
193
+ const clampedHeight = Math.min(Math.max(Number(height) || 300, 100), 2400);
194
+
195
+ // Create canvas
196
+ el.innerHTML = '';
197
+ el.style.position = 'relative';
198
+ el.style.height = clampedHeight + 'px';
199
+ const canvas = document.createElement('canvas');
200
+ el.appendChild(canvas);
201
+
202
+ const config = normalizeChartConfig(cfgOrSimple);
203
+ return new Chart(canvas, config);
204
+ } catch (err) {
205
+ console.warn('[relayKit] chart error:', err);
206
+ }
207
+ }
208
+
209
+ // ---------------------------------------------------------------------------
210
+ // mermaid()
211
+ // ---------------------------------------------------------------------------
212
+
213
+ let _mermaidReady = false;
214
+
215
+ async function mermaid(el, code) {
216
+ try {
217
+ await loadScript('/vendor/mermaid.min.js');
218
+ const m = window.mermaid;
219
+ if (!m) throw new Error('mermaid did not expose window.mermaid');
220
+
221
+ if (!_mermaidReady) {
222
+ m.initialize({
223
+ startOnLoad: false,
224
+ theme: mode === 'dark' ? 'dark' : 'neutral',
225
+ securityLevel: 'strict',
226
+ });
227
+ _mermaidReady = true;
228
+ }
229
+
230
+ el.innerHTML = '';
231
+ const id = 'kit-mermaid-' + Math.random().toString(36).slice(2);
232
+ let svgHtml;
233
+ try {
234
+ const result = await m.render(id, code);
235
+ svgHtml = typeof result === 'object' ? result.svg : result;
236
+ } catch (renderErr) {
237
+ el.style.color = theme.colors.muted;
238
+ el.style.fontSize = '0.85rem';
239
+ el.style.padding = '8px';
240
+ el.textContent = 'Diagram error: ' + renderErr.message;
241
+ console.warn('[relayKit] mermaid render error:', renderErr);
242
+ return null;
243
+ }
244
+ el.innerHTML = svgHtml;
245
+ el.style.maxHeight = '1200px';
246
+ el.style.overflow = 'auto';
247
+ return el.querySelector('svg');
248
+ } catch (err) {
249
+ console.warn('[relayKit] mermaid error:', err);
250
+ return null;
251
+ }
252
+ }
253
+
254
+ // ---------------------------------------------------------------------------
255
+ // table()
256
+ // ---------------------------------------------------------------------------
257
+
258
+ function table(el, opts) {
259
+ try {
260
+ const { columns = [], rows = [], sortable = false } = opts || {};
261
+
262
+ // Normalize columns -> [{key, label, align}]
263
+ const cols = columns.map((c, i) => {
264
+ if (typeof c === 'string') return { key: String(i), label: c, align: 'left' };
265
+ return { key: c.key || String(i), label: c.label || c.key || '', align: c.align || 'left' };
266
+ });
267
+
268
+ // Normalize rows -> string[][]
269
+ function normalizeRow(r) {
270
+ if (Array.isArray(r)) return r.map(v => (v == null ? '' : String(v)));
271
+ return cols.map(c => {
272
+ const v = r[c.key];
273
+ return v == null ? '' : String(v);
274
+ });
275
+ }
276
+
277
+ let data = rows.map(normalizeRow);
278
+
279
+ const c = theme.colors;
280
+
281
+ // Build table HTML
282
+ function buildTable(data) {
283
+ const tableEl = document.createElement('table');
284
+ tableEl.style.cssText = [
285
+ 'width:100%', 'border-collapse:collapse',
286
+ `font-family:${SANS}`, 'font-size:0.9rem',
287
+ `color:${c.fg}`,
288
+ ].join(';');
289
+
290
+ // Header
291
+ const thead = document.createElement('thead');
292
+ const hr = document.createElement('tr');
293
+ cols.forEach((col, ci) => {
294
+ const th = document.createElement('th');
295
+ th.textContent = col.label;
296
+ th.style.cssText = [
297
+ `text-align:${col.align}`,
298
+ `padding:8px 12px`,
299
+ `border-bottom:1px solid ${c.border}`,
300
+ `color:${c.muted}`,
301
+ 'font-size:0.78rem',
302
+ 'letter-spacing:0.06em',
303
+ 'text-transform:uppercase',
304
+ 'font-weight:600',
305
+ ].join(';');
306
+ if (sortable) {
307
+ th.style.cursor = 'pointer';
308
+ th.title = 'Sort';
309
+ let dir = 0;
310
+ th.addEventListener('click', () => {
311
+ dir = dir === 1 ? -1 : 1;
312
+ data.sort((a, b) => {
313
+ const av = a[ci] || '', bv = b[ci] || '';
314
+ const an = parseFloat(av), bn = parseFloat(bv);
315
+ if (!isNaN(an) && !isNaN(bn)) return (an - bn) * dir;
316
+ return av.localeCompare(bv) * dir;
317
+ });
318
+ rebuildBody(data);
319
+ });
320
+ }
321
+ hr.appendChild(th);
322
+ });
323
+ thead.appendChild(hr);
324
+ tableEl.appendChild(thead);
325
+
326
+ const tbody = document.createElement('tbody');
327
+ tableEl.appendChild(tbody);
328
+
329
+ function rebuildBody(rows) {
330
+ tbody.innerHTML = '';
331
+ rows.forEach((row, ri) => {
332
+ const tr = document.createElement('tr');
333
+ tr.style.background = ri % 2 === 0 ? 'transparent' : (mode === 'dark' ? 'rgba(255,255,255,0.02)' : 'rgba(0,0,0,0.015)');
334
+ row.forEach((cell, ci) => {
335
+ const td = document.createElement('td');
336
+ td.textContent = cell;
337
+ td.style.cssText = [
338
+ `text-align:${cols[ci].align}`,
339
+ 'padding:8px 12px',
340
+ `border-bottom:1px solid ${c.border}`,
341
+ ].join(';');
342
+ tr.appendChild(td);
343
+ });
344
+ tbody.appendChild(tr);
345
+ });
346
+ }
347
+
348
+ rebuildBody(data);
349
+ return tableEl;
350
+ }
351
+
352
+ el.innerHTML = '';
353
+ el.appendChild(buildTable(data));
354
+ } catch (err) {
355
+ console.warn('[relayKit] table error:', err);
356
+ }
357
+ }
358
+
359
+ // ---------------------------------------------------------------------------
360
+ // commentable()
361
+ // ---------------------------------------------------------------------------
362
+
363
+ function commentable(el, label, detail) {
364
+ try {
365
+ const lStr = String(label || '').slice(0, 200);
366
+ const dStr = detail != null ? String(detail).slice(0, 500) : undefined;
367
+ const accent = theme.colors.accent;
368
+
369
+ el.style.cursor = 'pointer';
370
+ el.title = 'add a comment';
371
+
372
+ el.addEventListener('mouseenter', () => {
373
+ el.style.outline = `1px dashed ${accent}`;
374
+ el.style.outlineOffset = '2px';
375
+ });
376
+ el.addEventListener('mouseleave', () => {
377
+ el.style.outline = '';
378
+ el.style.outlineOffset = '';
379
+ });
380
+ el.addEventListener('click', (e) => {
381
+ e.stopPropagation();
382
+ const msg = { relay: 'annotate-request', label: lStr };
383
+ if (dStr !== undefined) msg.detail = dStr;
384
+ try {
385
+ parent.postMessage(msg, '*');
386
+ } catch (postErr) {
387
+ console.warn('[relayKit] commentable postMessage error:', postErr);
388
+ }
389
+ });
390
+ } catch (err) {
391
+ console.warn('[relayKit] commentable error:', err);
392
+ }
393
+ }
394
+
395
+ // ---------------------------------------------------------------------------
396
+ // Expose global
397
+ // ---------------------------------------------------------------------------
398
+
399
+ const relayKit = {
400
+ theme,
401
+ applyBaseStyles,
402
+ chart,
403
+ mermaid,
404
+ table,
405
+ commentable,
406
+ };
407
+
408
+ window.relayKit = relayKit;
409
+ if (typeof window.kit === 'undefined') {
410
+ window.kit = relayKit;
411
+ }
412
+
413
+ })();
@@ -0,0 +1,231 @@
1
+ /* quest-board theme — editorial minimalism in the spirit of Claude:
2
+ warm near-white canvas, deep warm charcoal ink (never pure black), ONE
3
+ restrained terracotta/clay accent, soft diffused warm shadows instead of
4
+ hard borders, serif display accents, warm graphite dark mode. */
5
+
6
+ :root {
7
+ --bg: #fcfbf9; /* warm off-white canvas */
8
+ --bg-sunken: #f6f4f0; /* tonal band / input fill */
9
+ --card: #ffffff;
10
+ --fg: #1c1b19; /* deep warm charcoal */
11
+ --fg-2: #57534e; /* secondary copy */
12
+ --muted: #8a8580; /* eyebrow / captions */
13
+ --accent: #c2674b; /* terracotta / clay */
14
+ --accent-hover: #b25a40;
15
+ --accent-fg: #fcfaf7; /* text on accent */
16
+ --accent-soft: #f5e9e3; /* soft accent tint surface */
17
+ --border: #ece9e4; /* hairline tonal whisper */
18
+ --border-strong: #e0dcd5;
19
+ --danger: #bc4434;
20
+ --danger-fg: #fcfaf7;
21
+ --ok: #4d8a66;
22
+ --shadow-card: 0 1px 2px rgba(28, 27, 25, 0.04), 0 6px 16px rgba(28, 27, 25, 0.06);
23
+ --shadow-lift: 0 4px 8px rgba(28, 27, 25, 0.05), 0 16px 40px rgba(28, 27, 25, 0.10);
24
+ --serif: ui-serif, "Iowan Old Style", Georgia, Cambria, "Times New Roman", serif;
25
+ --sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
26
+ --ease: cubic-bezier(0.4, 0, 0.2, 1);
27
+ }
28
+ [data-theme="dark"] {
29
+ --bg: #201e1c; /* deep warm graphite, not blue-black */
30
+ --bg-sunken: #262421;
31
+ --card: #282624;
32
+ --fg: #edeae4;
33
+ --fg-2: #beb9b0;
34
+ --muted: #a29c93;
35
+ --accent: #d08159;
36
+ --accent-hover: #d98e67;
37
+ --accent-fg: #201913;
38
+ --accent-soft: #3b2e26;
39
+ --border: #393632;
40
+ --border-strong: #454139;
41
+ --danger: #e06c57;
42
+ --danger-fg: #201913;
43
+ --ok: #6fbf92;
44
+ --shadow-card: 0 1px 2px rgba(0, 0, 0, 0.25), 0 6px 16px rgba(0, 0, 0, 0.30);
45
+ --shadow-lift: 0 4px 8px rgba(0, 0, 0, 0.30), 0 16px 40px rgba(0, 0, 0, 0.38);
46
+ }
47
+ @media (prefers-color-scheme: dark) {
48
+ :root:not([data-theme="light"]) {
49
+ --bg: #201e1c;
50
+ --bg-sunken: #262421;
51
+ --card: #282624;
52
+ --fg: #edeae4;
53
+ --fg-2: #beb9b0;
54
+ --muted: #a29c93;
55
+ --accent: #d08159;
56
+ --accent-hover: #d98e67;
57
+ --accent-fg: #201913;
58
+ --accent-soft: #3b2e26;
59
+ --border: #393632;
60
+ --border-strong: #454139;
61
+ --danger: #e06c57;
62
+ --danger-fg: #201913;
63
+ --ok: #6fbf92;
64
+ --shadow-card: 0 1px 2px rgba(0, 0, 0, 0.25), 0 6px 16px rgba(0, 0, 0, 0.30);
65
+ --shadow-lift: 0 4px 8px rgba(0, 0, 0, 0.30), 0 16px 40px rgba(0, 0, 0, 0.38);
66
+ }
67
+ }
68
+
69
+ * { box-sizing: border-box; }
70
+ html, body { margin: 0; padding: 0; }
71
+ body {
72
+ background: var(--bg);
73
+ color: var(--fg);
74
+ font: 16px/1.6 var(--sans);
75
+ -webkit-font-smoothing: antialiased;
76
+ transition: background 200ms var(--ease), color 200ms var(--ease);
77
+ }
78
+ .wrap { max-width: 860px; margin: 0 auto; padding: 32px 20px 28px; }
79
+
80
+ .qb-header { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; margin-bottom: 4px; }
81
+ h1 {
82
+ font-family: var(--serif);
83
+ font-size: 1.7rem;
84
+ font-weight: 500;
85
+ letter-spacing: -0.02em;
86
+ margin: 0;
87
+ line-height: 1.25;
88
+ text-wrap: balance;
89
+ }
90
+ .intro { color: var(--fg-2); white-space: pre-wrap; margin: 10px 0 18px; }
91
+ .theme-btn {
92
+ background: transparent; color: var(--fg-2);
93
+ border: 1px solid var(--border-strong); border-radius: 999px;
94
+ padding: 6px 14px; cursor: pointer; font-size: 0.8rem; flex: none;
95
+ font-family: var(--sans);
96
+ transition: border-color 150ms var(--ease), color 150ms var(--ease);
97
+ }
98
+ .theme-btn:hover { border-color: var(--accent); color: var(--accent); }
99
+
100
+ .card {
101
+ background: var(--card);
102
+ border: 0;
103
+ border-radius: 12px;
104
+ padding: 20px 22px;
105
+ margin: 16px 0;
106
+ box-shadow: var(--shadow-card);
107
+ transition: box-shadow 200ms var(--ease);
108
+ }
109
+ .card.error { box-shadow: var(--shadow-card), 0 0 0 1.5px var(--danger); }
110
+
111
+ .qnum {
112
+ color: var(--muted);
113
+ font-size: 0.7rem;
114
+ letter-spacing: 0.12em;
115
+ text-transform: uppercase;
116
+ font-weight: 500;
117
+ margin-bottom: 4px;
118
+ }
119
+ .qlabel { font-weight: 600; font-size: 1.02rem; margin: 0; }
120
+ .req { color: var(--accent); margin-left: 2px; }
121
+ .qdesc { color: var(--muted); font-size: 0.9rem; margin: 4px 0 0; white-space: pre-wrap; }
122
+ .control { margin-top: 14px; }
123
+
124
+ .viz {
125
+ width: 100%;
126
+ border: 1px solid var(--border);
127
+ border-radius: 10px;
128
+ background: var(--card);
129
+ display: block;
130
+ margin: 14px 0 0;
131
+ }
132
+
133
+ .opt {
134
+ display: flex; gap: 10px; align-items: flex-start;
135
+ padding: 11px 14px;
136
+ border: 1px solid var(--border); border-radius: 10px;
137
+ margin: 8px 0; cursor: pointer;
138
+ transition: border-color 150ms var(--ease), background 150ms var(--ease);
139
+ }
140
+ .opt:hover { border-color: var(--accent); }
141
+ .opt.sel { border-color: var(--accent); background: var(--accent-soft); }
142
+ .opt input { margin-top: 4px; accent-color: var(--accent); flex: none; }
143
+ .opt .ol { font-weight: 500; }
144
+ .opt .od { color: var(--muted); font-size: 0.85rem; }
145
+ .opt .otherbox { margin-top: 6px; }
146
+
147
+ .seg { display: flex; gap: 8px; flex-wrap: wrap; }
148
+ .seg button, .scale button {
149
+ background: var(--card); color: var(--fg);
150
+ border: 1px solid var(--border-strong); border-radius: 10px;
151
+ padding: 10px 24px; font: inherit; cursor: pointer;
152
+ transition: border-color 150ms var(--ease), background 150ms var(--ease),
153
+ color 150ms var(--ease), transform 200ms var(--ease), box-shadow 200ms var(--ease);
154
+ }
155
+ .seg button:hover, .scale button:hover { border-color: var(--accent); transform: translateY(-1px); }
156
+ .seg button.sel, .scale button.sel {
157
+ background: var(--accent); color: var(--accent-fg); border-color: var(--accent);
158
+ }
159
+
160
+ .scale { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
161
+ .scale button { width: 44px; height: 44px; padding: 0; }
162
+ .scale .slabel { color: var(--muted); font-size: 0.8rem; }
163
+
164
+ input[type="text"], textarea {
165
+ width: 100%;
166
+ background: var(--bg-sunken); color: var(--fg);
167
+ border: 1px solid var(--border); border-radius: 10px;
168
+ padding: 11px 14px; font: inherit;
169
+ transition: border-color 150ms var(--ease), box-shadow 150ms var(--ease);
170
+ }
171
+ input[type="text"]:focus, textarea:focus {
172
+ outline: none;
173
+ border-color: var(--accent);
174
+ box-shadow: 0 0 0 3px var(--accent-soft);
175
+ }
176
+ textarea { min-height: 90px; resize: vertical; }
177
+
178
+ .qnotewrap { margin-top: 10px; }
179
+ .qnotewrap .qnote { font-size: 0.88rem; padding: 8px 12px; }
180
+
181
+ .errmsg { color: var(--danger); font-size: 0.85rem; margin: 8px 0 0; display: none; }
182
+ .card.error .errmsg { display: block; }
183
+
184
+ .submitbar {
185
+ position: sticky; bottom: 0;
186
+ background: linear-gradient(transparent, var(--bg) 38%);
187
+ padding: 22px 0 16px;
188
+ display: flex; align-items: center; gap: 14px;
189
+ }
190
+ .submit {
191
+ background: var(--accent); color: var(--accent-fg);
192
+ border: none; border-radius: 10px;
193
+ padding: 12px 30px; font: inherit; font-size: 1rem; font-weight: 600; cursor: pointer;
194
+ transition: background 150ms var(--ease), transform 200ms var(--ease), box-shadow 200ms var(--ease);
195
+ }
196
+ .submit:hover { background: var(--accent-hover); transform: translateY(-1px); box-shadow: var(--shadow-lift); }
197
+ .submit:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
198
+ .submit:disabled { opacity: 0.55; cursor: default; transform: none; box-shadow: none; }
199
+ .hint { color: var(--muted); font-size: 0.82rem; }
200
+ .savestate { color: var(--muted); font-size: 0.78rem; margin-left: auto; white-space: nowrap; }
201
+
202
+ .banner {
203
+ position: sticky; top: 0;
204
+ background: var(--danger); color: var(--danger-fg);
205
+ text-align: center; padding: 9px 16px; font-size: 0.9rem;
206
+ z-index: 10; display: none;
207
+ }
208
+
209
+ .done { text-align: center; padding: 80px 18px; }
210
+ .done .mark {
211
+ width: 64px; height: 64px; border-radius: 50%;
212
+ background: var(--ok); color: #fcfbf9;
213
+ font-size: 2rem; line-height: 64px;
214
+ margin: 0 auto 20px;
215
+ }
216
+ .done h2 { font-family: var(--serif); font-weight: 500; letter-spacing: -0.015em; margin: 0 0 8px; }
217
+ .done p { color: var(--muted); margin: 0; }
218
+
219
+ .qb-footer { color: var(--muted); font-size: 0.75rem; text-align: center; margin-top: 28px; }
220
+
221
+ @media (max-width: 480px) {
222
+ .wrap { padding: 18px 12px 18px; }
223
+ .card { padding: 16px; border-radius: 10px; }
224
+ h1 { font-size: 1.35rem; }
225
+ .seg button { padding: 10px 18px; }
226
+ .scale button { width: 40px; height: 40px; }
227
+ }
228
+
229
+ @media (prefers-reduced-motion: reduce) {
230
+ * { transition: none !important; animation: none !important; }
231
+ }
package/src/util.js ADDED
@@ -0,0 +1,19 @@
1
+ export class CliError extends Error {
2
+ constructor(message, code = 4) {
3
+ super(message);
4
+ this.code = code;
5
+ }
6
+ }
7
+
8
+ export const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
9
+
10
+ export async function pollFor(fn, timeoutMs, intervalMs = 150) {
11
+ const end = Date.now() + timeoutMs;
12
+ // eslint-disable-next-line no-constant-condition
13
+ while (true) {
14
+ const v = fn();
15
+ if (v) return v;
16
+ if (Date.now() >= end) return null;
17
+ await sleep(intervalMs);
18
+ }
19
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "chart.js": "4.5.1",
3
+ "mermaid": "11.15.0",
4
+ "updatedAt": "2026-06-11T09:20:33.586Z"
5
+ }