dsh-claude-data 0.0.0-stage → 2.14.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/LICENSE +26 -0
- package/NOTICE.md +62 -0
- package/README.md +43 -2
- package/cordis.patch.yml +13 -0
- package/docs/DETAIL.md +27 -0
- package/docs/DETAIL.zh.md +16 -0
- package/icon.svg +9 -0
- package/lib/index.js +167 -0
- package/locale/en.json +7 -0
- package/locale/zh.json +7 -0
- package/package.json +104 -4
- package/skills/claude-data/SKILL.md +536 -0
- package/skills/claude-data/artifact-type/reference/admin-setup.md +64 -0
- package/skills/claude-data/artifact-type/reference/dependent-calls.md +33 -0
- package/skills/claude-data/artifact-type/reference/design-defaults.md +51 -0
- package/skills/claude-data/artifact-type/reference/design-system.md +65 -0
- package/skills/claude-data/artifact-type/reference/exporting.md +23 -0
- package/skills/claude-data/artifact-type/reference/features.md +54 -0
- package/skills/claude-data/artifact-type/reference/looks.md +55 -0
- package/skills/claude-data/artifact-type/reference/one-writer.md +51 -0
- package/skills/claude-data/artifact-type/reference/questions.md +45 -0
- package/skills/claude-data/artifact-type/reference/read-aloud.md +61 -0
- package/skills/claude-data/artifact-type/reference/remix.md +43 -0
- package/skills/claude-data/artifact-type/reference/sources.md +63 -0
- package/skills/claude-data/artifact-type/reference/visual-short-script.md +146 -0
- package/skills/claude-data/artifact-type/reference/visual-short-skeleton.md +131 -0
- package/skills/claude-data/artifact-type/reference/visual-short.md +63 -0
- package/skills/claude-data/scripts/artifact.mjs +5688 -0
- package/skills/claude-data/scripts/editor/assets/anthropicons-fizzy-lake-CPTdBYLV.woff2 +0 -0
- package/skills/claude-data/scripts/editor/assets/index-B1CKpH_P.css +1 -0
- package/skills/claude-data/scripts/editor/assets/index-BWg-0IkN.js +569 -0
- package/skills/claude-data/scripts/editor/assets/working-mark-sheets-dAm26n99.js +1 -0
- package/skills/claude-data/scripts/editor/comments.js +103 -0
- package/skills/claude-data/scripts/editor/index.html +27 -0
- package/skills/claude-data/scripts/editor/local-host.js +481 -0
- package/skills/claude-data/scripts/editor/share-ui.js +208 -0
- package/skills/claude-data/scripts/editor/third-party-licenses.txt +1213 -0
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# A Visual short's skeleton: the script
|
|
2
|
+
|
|
3
|
+
Goes with the HTML and CSS in `artifact-type/reference/visual-short-skeleton.md`,
|
|
4
|
+
as `files/vs.js`.
|
|
5
|
+
Yours to write: the calcs, `fmt`, a `dash.draw` per chart and `scenes`. The rest
|
|
6
|
+
is the engine: copy it as it stands, unless
|
|
7
|
+
the person asks for something it doesn't do.
|
|
8
|
+
|
|
9
|
+
```js
|
|
10
|
+
// Figures as the captions say them: rounded for the ear, still numbers from the data, so the mark check compares them.
|
|
11
|
+
dash.calc('said', {inputs: ['orders'], title: 'Figures as the captions say them', description: 'Rounded to two significant figures.',
|
|
12
|
+
fn: (orders) => orders.map((r) => ({channel: r.channel, cost: r.cost == null || !Number.isFinite(+r.cost) ? null : +(+r.cost).toPrecision(2)}))});
|
|
13
|
+
|
|
14
|
+
const root = document.querySelector('#dash-root'), vs = root.querySelector('.vs'), stage = vs.querySelector('.vs-stage');
|
|
15
|
+
const [chs, caps, mks] = ['.vs-ch', '.vs-cap', '.vs-mk'].map((q) => [...vs.querySelectorAll(q)]); // one of each, and one scene, per chapter
|
|
16
|
+
const still = matchMedia('(prefers-reduced-motion: reduce)');
|
|
17
|
+
const words = (el) => el.textContent.replace(/\s+/g, ' ').trim();
|
|
18
|
+
const write = (el, s) => { if (el && el.textContent !== s) el.textContent = s; }; // text only when it changes
|
|
19
|
+
const says = (i) => [...(caps[i]?.querySelectorAll('.vs-s:not([hidden])') ?? [])]; // a chapter's sentences, less any the data ruled out
|
|
20
|
+
const called = (i) => ['.vs-label', 'h1, h2, h3'].map((q) => chs[i]?.querySelector(q)).map((el) => (el ? words(el) : '')).find(Boolean) ?? String(i + 1);
|
|
21
|
+
// How a figure is written, by source and field; any other as it comes.
|
|
22
|
+
const fmt = {'orders.cost': d3.format('$,.2f'), 'said.cost': d3.format('$,~f')};
|
|
23
|
+
let said = [], durs = [], cur = 0, t = Infinity, playing = false, begun = false, raf = 0, last = 0, heard = null, was = null, pace = 1;
|
|
24
|
+
const phase = () => root.getAttribute('data-dash-read-phase'); // there while a reading is open, and for a moment as a reader edits by hand
|
|
25
|
+
|
|
26
|
+
// While a reading is open every time is scaled by `pace`: how long the voice took over the last caption it read through, against 15 characters a second.
|
|
27
|
+
const paced = (at) => (phase() ? at * pace : at);
|
|
28
|
+
// The second at which the mark named by `word` arrives in chapter i: where the word starts in the caption; 0 if it isn't there.
|
|
29
|
+
const cue = (i, word) => paced(Math.max(0, said[i].indexOf(word)) / 15);
|
|
30
|
+
// How far a move that starts at second `from` has got at time t, 0 to 1.
|
|
31
|
+
const moved = (t, from, len = 0.6) => d3.easeCubicOut(Math.max(0, Math.min(1, (t - from) / len)));
|
|
32
|
+
// A figure that counts up: `real` is marked and keeps its value; `copy` (data-draw="copy", unmarked, aria-hidden) lies over it and counts.
|
|
33
|
+
const places = (v) => { const [m, e = 0] = String(v).split('e'); return Math.min(100, Math.max(0, (m.split('.')[1] ?? '').length - e)) || 0; };
|
|
34
|
+
const shown = (s) => (String(s).match(/\.(\d+)/)?.[1] ?? '').length; // the decimals in a figure as written
|
|
35
|
+
const count = (real, copy, p, value, f) => {
|
|
36
|
+
real.style.opacity = p < 1 ? 0 : 1; copy.style.opacity = p > 0 && p < 1 ? 1 : 0;
|
|
37
|
+
if (p >= 1) return;
|
|
38
|
+
let s = f(value * p); // on the way it is never written with more decimals than the finished figure
|
|
39
|
+
if (shown(s) > shown(f(value))) s = f(+(value * p).toFixed(places(value)));
|
|
40
|
+
write(copy, s);
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// One per chart: make its elements from its rows, whole, and leave a — when there are none or the status isn't "ok"; fill each mark it makes with fig(mark), but no header cell, which is a label. Runs on new data and on a new width.
|
|
44
|
+
dash.draw('d1', (el) => { /* el.replaceChildren(...) */ });
|
|
45
|
+
// One function per chapter. It finds what the drawings made at each call, skips what is missing, sets styles from t and makes nothing; t = Infinity is the chapter whole. Take every time from cue(), so that a voice's pace reaches it.
|
|
46
|
+
const scenes = [(t) => { /* for (const bar of chs[0].querySelectorAll('.bar')) bar.style.width = bar.dataset.share * moved(t, cue(0, 'email')) * 100 + '%' */ }];
|
|
47
|
+
|
|
48
|
+
function fig(el) { // the one place marked text is written: fills one mark, its row found as the dashboard finds it
|
|
49
|
+
const dec = (s) => { try { return decodeURIComponent(s); } catch { return s; } };
|
|
50
|
+
const {source, field, where, row: n} = el.dataset, d = dash.data(source), rows = d.status === 'ok' && Array.isArray(d.data) ? d.data : [];
|
|
51
|
+
const row = where == null ? rows[+(n ?? 0)] : rows.find((r) => where.split('&').every((p) => {
|
|
52
|
+
const i = p.indexOf('='); return i > 0 && String(r[dec(p.slice(0, i)).trim()] ?? '') === dec(p.slice(i + 1)); }));
|
|
53
|
+
const v = row?.[field];
|
|
54
|
+
write(el, v == null || v === '' ? '—' : (fmt[source + '.' + field] ?? String)(v));
|
|
55
|
+
}
|
|
56
|
+
dash.draw('fig', fig); // every marked figure in the HTML is data-draw="fig"
|
|
57
|
+
for (const name of ['runs', 'now', 'copy']) dash.draw(name, () => {}); // drawings the engine writes in at its own times
|
|
58
|
+
const due = new Map(); // the second each sentence starts at, by its span; not a property of the span, since Safari has a span.cue of its own
|
|
59
|
+
function timing() { // after the figures: filling moves where the words fall
|
|
60
|
+
said = chs.map((_, i) => says(i).map(words).join(' ')); durs = said.map((s) => s.length / 15 + 1);
|
|
61
|
+
chs.forEach((_, i) => { let at = 0; for (const s of says(i)) { const w = words(s), j = said[i].indexOf(w, at); due.set(s, (j < 0 ? at : j) / 15); if (j >= 0) at = j + w.length; } });
|
|
62
|
+
write(vs.querySelector('.vs-runs'), String(Math.round(d3.sum(durs) / 5) * 5));
|
|
63
|
+
}
|
|
64
|
+
function draw(i, t) {
|
|
65
|
+
scenes[i]?.(still.matches || t >= paced(durs[i]) ? Infinity : t); // under reduced motion the drawing is whole, and the sentences still take their turns
|
|
66
|
+
const ss = says(i), k = begun ? ss.findLastIndex((s) => t >= paced(due.get(s))) : 0; // the sentence being said; before the first Play, the first
|
|
67
|
+
ss.forEach((s, j) => s.classList.toggle('vs-on', j === Math.max(0, k)));
|
|
68
|
+
}
|
|
69
|
+
function frame() {
|
|
70
|
+
draw(cur, t);
|
|
71
|
+
mks.forEach((m, j) => { for (const bar of m.children) bar.style.width = (j < cur ? 1 : j > cur ? 0 : t === Infinity ? +begun : Math.min(1, t / paced(durs[cur]))) * 100 + '%'; }); // whole: full, but for the title before the first Play
|
|
72
|
+
}
|
|
73
|
+
function show(i, from) {
|
|
74
|
+
if (i !== cur) { draw(cur, Infinity); if (!phase()) write(vs.querySelector('.vs-now'), called(i)); } // what leaves is left whole: its marks are still checked
|
|
75
|
+
[cur, t] = [i, from];
|
|
76
|
+
for (const set of [chs, caps]) set.forEach((el, j) => el.classList.toggle('vs-on', j === i));
|
|
77
|
+
chs.forEach((ch, j) => ch.setAttribute('aria-hidden', String(j !== i))); // not the captions: Listen skips what is aria-hidden
|
|
78
|
+
for (const set of [mks, caps]) set.forEach((el, j) => (j === i ? el.setAttribute('aria-current', 'true') : el.removeAttribute('aria-current')));
|
|
79
|
+
mks.forEach((m, j) => m.getAttribute('aria-label') === called(j) || m.setAttribute('aria-label', called(j))); // so that a hand edit of the label reaches the chapter mark
|
|
80
|
+
frame();
|
|
81
|
+
}
|
|
82
|
+
function tick(now) {
|
|
83
|
+
raf = 0;
|
|
84
|
+
if (!vs.isConnected || (phase() ? phase() !== 'playing' : !playing)) return;
|
|
85
|
+
t += last ? Math.min(0.1, (now - last) / 1000) : 0; last = now;
|
|
86
|
+
if (phase() || t < durs[cur]) frame(); // in a reading the chapter waits, whole, for the voice, and the clock runs on to time it
|
|
87
|
+
else if (cur < chs.length - 1) show(cur + 1, 0);
|
|
88
|
+
else return play(false);
|
|
89
|
+
raf = requestAnimationFrame(tick);
|
|
90
|
+
}
|
|
91
|
+
const kick = () => { if (!raf) { last = 0; raf = requestAnimationFrame(tick); } };
|
|
92
|
+
function look() { const on = phase() ? /starting|playing/.test(phase()) : playing; vs.classList.toggle('vs-playing', on); vs.querySelector('.vs-controls .vs-toggle')?.setAttribute('aria-label', on ? 'Pause' : 'Play'); } // Play or Pause by the voice's phase, or by the page's own clock when there is no voice; the dashboard reads vs-playing too
|
|
93
|
+
function play(on) { playing = on; begun ||= on; look(); frame(); if (on) kick(); }
|
|
94
|
+
// While a reading is open, the dashboard hears the same presses (data-read-pause, data-read-move) and the voice keeps time: a press here shows its chapter, and starts and stops no clock.
|
|
95
|
+
const from = (i) => (show(Math.max(0, Math.min(chs.length - 1, i)), 0), phase() || play(true));
|
|
96
|
+
const on = (q, f) => { const b = vs.querySelector('.vs-controls ' + q); if (b) b.onclick = f; }; // a piece may leave a control out
|
|
97
|
+
on('.vs-toggle', () => phase() || (playing ? play(false) : t < durs[cur] ? play(true) : from(cur === chs.length - 1 ? 0 : cur)));
|
|
98
|
+
on('.vs-back', () => from(cur - 1));
|
|
99
|
+
on('.vs-next', () => from(cur + 1));
|
|
100
|
+
mks.forEach((m, j) => (m.onclick = () => from(j)));
|
|
101
|
+
on('.vs-cc', (e) => e.currentTarget.setAttribute('aria-pressed', String(!vs.classList.toggle('vs-hush')))); // captions on or off, until the page is loaded again
|
|
102
|
+
// vs-idle: a mouse still for 3 s counts as gone, unless its last move was onto the controls (vs-held); vs-touch: a finger or pen, which may leave no hover, counts as on the stage
|
|
103
|
+
stage.onpointermove = stage.onpointerdown = (e) => {
|
|
104
|
+
const m = e.pointerType === 'mouse', move = e.type === 'pointermove', l = stage.classList;
|
|
105
|
+
if (move && Math.abs(e.clientX - stage.x) + Math.abs(e.clientY - stage.y) < 4) return; // no move: a click can drift a pixel, and a still mouse gets a move when the page changes under it
|
|
106
|
+
[stage.x, stage.y] = [e.clientX, e.clientY];
|
|
107
|
+
l.toggle('vs-idle', false); l.toggle('vs-touch', !m); l.toggle('vs-held', move && !!e.target.closest('.vs-controls'));
|
|
108
|
+
clearTimeout(stage.idle); if (m) stage.idle = setTimeout(() => l.add('vs-idle'), 3000);
|
|
109
|
+
};
|
|
110
|
+
still.onchange = () => (vs.isConnected ? frame() : (still.onchange = null)); // switched while paused in mid-chapter
|
|
111
|
+
|
|
112
|
+
const mo = new MutationObserver(() => {
|
|
113
|
+
if (!vs.isConnected) return mo.disconnect();
|
|
114
|
+
const now = vs.querySelector('[data-dash-read-now]'), p = phase(), again = p === 'starting' && was !== 'starting'; // each start reads its caption from the top
|
|
115
|
+
const through = heard && p === 'playing' && was === 'playing'; // the voice went on by itself, so t is how long the caption took
|
|
116
|
+
const before = was; was = p;
|
|
117
|
+
if (!p) { if (heard) t /= pace; heard = null; if (before) play(/starting|playing/.test(before)); return; } // the voice is off: go on without it from here, playing if it was starting or playing
|
|
118
|
+
playing = false; begun = true; look(); // the voice keeps time now
|
|
119
|
+
write(vs.querySelector('.vs-now'), ''); // else a later move to the chapter it names would not be announced, since write() skips unchanged text
|
|
120
|
+
if (now && (now !== heard || again)) {
|
|
121
|
+
if (through) pace = Math.max(0.5, Math.min(1.5, t / (durs[cur] - 1) || 1));
|
|
122
|
+
const i = caps.indexOf((heard = now));
|
|
123
|
+
if (i >= 0) show(i, 0); // a stamp on anything but a caption moves nothing
|
|
124
|
+
}
|
|
125
|
+
if (p === 'ended') show(cur, Infinity); // read to its end, or a reader edits by hand: whole
|
|
126
|
+
kick();
|
|
127
|
+
});
|
|
128
|
+
mo.observe(root, {subtree: true, attributes: true, attributeFilter: ['data-dash-read-phase', 'data-dash-read-now']});
|
|
129
|
+
|
|
130
|
+
// Lays each caption over the row its chapter keeps for it under the headline; with no row, over the top of the drawing.
|
|
131
|
+
function seat() {
|
|
132
|
+
const top = vs.querySelector('.vs-in').getBoundingClientRect().top;
|
|
133
|
+
chs.forEach((ch, i) => { const row = ch.querySelector('.vs-say, .vs-draw'); if (row && caps[i]) caps[i].style.top = row.getBoundingClientRect().top - top + 'px'; }); // a person may have deleted either
|
|
134
|
+
if (durs.length) frame();
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
let box = vs.parentElement; // the box the page scrolls in
|
|
138
|
+
while (box.parentElement && !/auto|scroll/.test(getComputedStyle(box).overflowY)) box = box.parentElement;
|
|
139
|
+
const fit = () => vs.style.setProperty('--room', Math.max(240, box.getBoundingClientRect().bottom - box.scrollTop - vs.getBoundingClientRect().top
|
|
140
|
+
- parseFloat(getComputedStyle(root).paddingBottom) - parseFloat(getComputedStyle(box).paddingBottom)) + 'px'); // the box may be padded for a bar that floats over its foot
|
|
141
|
+
const ro = new ResizeObserver(() => (vs.isConnected ? (fit(), seat()) : ro.disconnect())); for (const el of [box, stage, ...vs.querySelectorAll('.vs-draw')]) ro.observe(el); // the stage too: when only its width changes, the dashboard draws each chart again, whole, without settle, and this puts the chapter showing back at t; each drawing's box, which changes height when a headline above it rewraps, as under Translate
|
|
142
|
+
document.fonts.ready.then(() => vs.isConnected && seat()); // a web font that comes late moves the rows
|
|
143
|
+
|
|
144
|
+
dash.onData(fit);
|
|
145
|
+
dash.draw('settle', () => { timing(); show(cur, t); seat(); }); // the last drawing in the HTML, so it runs once the others have
|
|
146
|
+
```
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# A Visual short's skeleton: HTML and CSS
|
|
2
|
+
|
|
3
|
+
Working HTML and CSS that, with the script in
|
|
4
|
+
`artifact-type/reference/visual-short-script.md`, already do the engine's
|
|
5
|
+
part of `visual-short.md`: one writer for each part, the fit, the
|
|
6
|
+
containers, filling each marked figure, cues worked out after filling, one
|
|
7
|
+
clock, keeping step with Listen, captions that have a row of their own, and the
|
|
8
|
+
controls. Start from them and change whatever the piece needs,
|
|
9
|
+
keeping the class names the script looks for. What is left to write is the piece
|
|
10
|
+
itself: the chapters and captions, the calcs, a `dash.draw` per chart, one
|
|
11
|
+
function in `scenes` per chapter, `fmt`, and the look of the drawings.
|
|
12
|
+
|
|
13
|
+
## One reply has a limit
|
|
14
|
+
|
|
15
|
+
One reply of yours can only be so long, thinking included, and a call that the
|
|
16
|
+
limit cuts short is never run: all of it is lost. Thinking is often the larger
|
|
17
|
+
part of a long reply. Calls sent together are still one reply; your next reply
|
|
18
|
+
starts once they have been answered. A Visual short planned and written in one
|
|
19
|
+
reply can come near the limit, so it need not be the one batch of `SKILL.md`'s
|
|
20
|
+
step 3. Whether to split the writes, and how, is yours to judge.
|
|
21
|
+
|
|
22
|
+
The code is three files: `files/vs.css`, the CSS below and the look of your
|
|
23
|
+
drawings; `files/vs.js`, the script; and `files/index.html`, the HTML below,
|
|
24
|
+
which names them. A remix writes them under its draft, as
|
|
25
|
+
`artifact-type/reference/remix.md` says.
|
|
26
|
+
|
|
27
|
+
However you write the piece:
|
|
28
|
+
|
|
29
|
+
- **The script is best left as one file.** A page's scripts each run in a scope
|
|
30
|
+
of their own and this script's parts share names, so two files would need all
|
|
31
|
+
of those on `window`.
|
|
32
|
+
- **Each file written draws an open page again, and the page is checked as it
|
|
33
|
+
then stands.** Stored before its script, `index.html` shows the person an
|
|
34
|
+
unfinished page and can fail the check. On a new dashboard, once a file lands
|
|
35
|
+
more than a few seconds after `index.html`, the check stops listing errors
|
|
36
|
+
left uncaught in a timer, a listener or a promise. Until the last file is in,
|
|
37
|
+
the check says nothing of the finished page, so read it after that write. A
|
|
38
|
+
new dashboard with no `index.html` says it is missing once your turn ends.
|
|
39
|
+
- **A later change that touches several files goes as one batch**, so that
|
|
40
|
+
nobody sees half of it. In one too long for a reply, each write shows at once
|
|
41
|
+
to everyone looking at that page, and a file `index.html` names that isn't
|
|
42
|
+
there yet is left out, with no note on the page. Read its check only after the
|
|
43
|
+
last write.
|
|
44
|
+
|
|
45
|
+
```html
|
|
46
|
+
<meta name="dash-one-writer">
|
|
47
|
+
<link rel="stylesheet" href="vs.css">
|
|
48
|
+
<div class="vs">
|
|
49
|
+
<section class="vs-stage" data-read-stage><div class="vs-in">
|
|
50
|
+
<div class="vs-chapters">
|
|
51
|
+
<article class="vs-ch">
|
|
52
|
+
<p class="vs-label">Part of the story</p>
|
|
53
|
+
<h2>A sentence that states the finding</h2>
|
|
54
|
+
<div class="vs-say"></div><!-- on every chapter, and empty: the row where the script lays its caption -->
|
|
55
|
+
<div class="vs-draw"><div data-draw="d1" data-source="orders"></div></div><!-- a chart is a drawing, stored empty; words and single figures beside it are HTML, each figure data-draw="fig" -->
|
|
56
|
+
<p class="vs-src">Measure, who, dates. Runs about <span class="vs-runs" data-draw="runs">—</span> seconds.</p>
|
|
57
|
+
</article>
|
|
58
|
+
<!-- one .vs-ch per chapter; only the first, the title, has "Runs about … seconds." -->
|
|
59
|
+
</div>
|
|
60
|
+
<div class="vs-caps" data-read lang="en"><!-- lang: the language the captions are written in -->
|
|
61
|
+
<p class="vs-cap" data-read-step><span class="vs-s">An email order cost about <span data-draw="fig" data-source="said" data-field="cost" data-where="channel=Email">—</span>.</span> <span class="vs-s">The next sentence.</span></p>
|
|
62
|
+
<!-- one .vs-cap per chapter, in the same order; one .vs-s per sentence; nothing else in here: whatever is, is read aloud too -->
|
|
63
|
+
</div>
|
|
64
|
+
<div class="vs-line">
|
|
65
|
+
<button class="vs-mk" data-read-move aria-label="Part of the story"><i></i></button>
|
|
66
|
+
<!-- one .vs-mk per chapter, in the same order, named for its label; the script names it again, in case the label has been edited since -->
|
|
67
|
+
</div>
|
|
68
|
+
<div class="vs-controls">
|
|
69
|
+
<button class="vs-toggle" data-read-pause aria-label="Play"><svg viewBox="0 0 16 16" aria-hidden="true"><path d="M6.44 3.682L11.59 6.765C12.022 7.024 12.286 7.497 12.286 8C12.286 8.503 12.022 8.976 11.59 9.235L6.44 12.318C6.241 12.437 6.014 12.5 5.782 12.5C5.074 12.5 4.5 11.926 4.5 11.218L4.5 4.782C4.5 4.074 5.074 3.5 5.782 3.5C6.014 3.5 6.241 3.563 6.44 3.682Z"/></svg><svg viewBox="0 0 16 16" aria-hidden="true"><rect x="3.5" y="3.5" width="3" height="9" rx=".75"/><rect x="9.5" y="3.5" width="3" height="9" rx=".75"/></svg></button>
|
|
70
|
+
<button class="vs-back" data-read-move aria-label="Back"><svg viewBox="0 0 16 16" aria-hidden="true"><path d="M10 3.5L5.5 8L10 12.5"/></svg></button><button class="vs-next" data-read-move aria-label="Next"><svg viewBox="0 0 16 16" aria-hidden="true"><path d="M6 3.5L10.5 8L6 12.5"/></svg></button>
|
|
71
|
+
<button data-read-sound aria-label="Read aloud" aria-pressed="true"><svg viewBox="0 0 16 16" aria-hidden="true"><path d="M12.9 5.5C13.284 6.25 13.5 7.1 13.5 8C13.5 8.9 13.284 9.75 12.9 10.5M10.599 6.5C10.854 6.941 11 7.454 11 8C11 8.546 10.854 9.059 10.599 9.5M8.5 12.899C8.5 13.328 8 13.558 7.671 13.275L4.783 10.748C4.6 10.588 4.367 10.5 4.124 10.5L3.5 10.5C2.948 10.5 2.5 10.053 2.5 9.5L2.5 6.75M5.768 4.391L7.671 2.726C8 2.443 8.5 2.672 8.5 3.102L8.5 7.25M2.5 4L11 12.5"/></svg><svg viewBox="0 0 16 16" aria-hidden="true"><path d="M12.9 5.5C13.284 6.25 13.5 7.1 13.5 8C13.5 8.9 13.284 9.75 12.9 10.5M10.599 6.5C10.854 6.941 11 7.454 11 8C11 8.546 10.854 9.059 10.599 9.5M3.5 5.5L4.124 5.5C4.366 5.5 4.6 5.412 4.783 5.253L7.671 2.726C8 2.443 8.5 2.672 8.5 3.102L8.5 12.899C8.5 13.328 8 13.558 7.671 13.275L4.783 10.748C4.6 10.588 4.367 10.5 4.124 10.5L3.5 10.5C2.948 10.5 2.5 10.053 2.5 9.5L2.5 6.5C2.5 5.948 2.948 5.5 3.5 5.5Z"/></svg></button><!-- on at first, struck out once the voice is off. The dashboard keeps aria-pressed, and hides the button where there is no voice -->
|
|
72
|
+
<button class="vs-cc" aria-label="CC, captions" aria-pressed="true">CC</button>
|
|
73
|
+
</div>
|
|
74
|
+
<p class="vs-now" aria-live="polite" data-draw="now"></p>
|
|
75
|
+
</div></section>
|
|
76
|
+
<i data-draw="settle"></i><!-- keep it last: drawings run in order, and this one needs the others filled -->
|
|
77
|
+
</div>
|
|
78
|
+
<script src="vs.js"></script>
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
```css
|
|
82
|
+
.vs, .vs-stage { container-type: inline-size; }
|
|
83
|
+
.vs-stage { box-sizing: border-box; aspect-ratio: 16 / 9; width: min(100%, calc(var(--room, 70dvh) * 16 / 9)); margin-inline: auto; overflow: clip;
|
|
84
|
+
background: var(--color-panel); border: 1px solid var(--color-border-line); border-radius: var(--cds-radius); }
|
|
85
|
+
@container (max-width: 800px) { .vs-stage { aspect-ratio: 1; width: min(100%, var(--room, 70dvh)); } }
|
|
86
|
+
@container (max-width: 560px) { .vs-stage { aspect-ratio: 4 / 5; width: min(100%, calc(var(--room, 70dvh) * 4 / 5)); } }
|
|
87
|
+
.vs-in { box-sizing: border-box; height: 100%; padding: 0 3.6cqi 3cqi; display: grid; grid-template-rows: auto minmax(0, 1fr); position: relative; }
|
|
88
|
+
.vs-chapters { display: grid; box-sizing: border-box; isolation: isolate; } /* isolated, or a z-index in a drawing lifts it over the caption and the controls */
|
|
89
|
+
.vs-in, .vs-chapters, .vs-ch { grid-template-columns: minmax(0, 1fr); } /* Without this, one wide child widens every column. */
|
|
90
|
+
.vs-ch, .vs-cap { margin: 0; opacity: 0; transition: opacity 400ms var(--cds-ease-out); }
|
|
91
|
+
.vs-ch.vs-on, .vs-cap.vs-on { opacity: 1; } .vs-ch:not(.vs-on) { pointer-events: none; }
|
|
92
|
+
.vs-ch { grid-area: 1 / 1; display: grid; grid-template-rows: auto auto auto minmax(0, 1fr) auto; }
|
|
93
|
+
.vs-say { grid-row: 3; } .vs-draw { grid-row: 4; } .vs-src { grid-row: 5; } /* by name: a chapter that has lost a row keeps its drawing's */
|
|
94
|
+
.vs-draw { container-type: size; display: flex; flex-direction: column; justify-content: safe center; font-size: max(12px, 1.8cqi); } /* a container of its own: inside it, cqh is 1% of the drawing's height, which comes from .vs-ch's 1fr row, not from what it holds */
|
|
95
|
+
/* The chapter marks: a band of bars across the top of the stage, which stays up. Each is a button as tall as the band that draws a 3px bar. --marks is the band's height, --row the controls', --ctl their width. */
|
|
96
|
+
.vs-in { --marks: max(24px, 4cqi); --row: 32px; --ctl: 176px; }
|
|
97
|
+
@media (pointer: coarse) { .vs-in { --marks: max(32px, 4cqi); --row: 52px; --ctl: 240px; } }
|
|
98
|
+
.vs-line { grid-area: 1 / 1; height: var(--marks); display: flex; gap: 6px; }
|
|
99
|
+
.vs-mk { flex: 1; min-inline-size: 0; position: relative; display: flex; align-items: center; padding: 0; border: 0; background: none; cursor: pointer; forced-color-adjust: none; outline-offset: -3px; } /* the stage would clip a focus ring drawn outside it */
|
|
100
|
+
.vs-mk::before { content: ''; position: absolute; inset-inline: 0; background: var(--cds-chart-muted); }
|
|
101
|
+
.vs-mk i { position: relative; display: block; width: 0; background: var(--color-fg); }
|
|
102
|
+
.vs-mk::before, .vs-mk i { height: 3px; border-radius: 3px; }
|
|
103
|
+
@media (forced-colors: active) { .vs-mk::before { background: GrayText; } .vs-mk i { background: CanvasText; } }
|
|
104
|
+
/* The caption is a line of plain words under the headline, where it covers nothing drawn. Each chapter keeps a row for it, .vs-say, as tall whatever is being said and with captions off,
|
|
105
|
+
so the drawing never moves; the script lays the caption over that row. It is placed against .vs-in, so .vs-caps must stay unpositioned. Presses pass through it. */
|
|
106
|
+
.vs-chapters, .vs-caps { grid-area: 2 / 1; }
|
|
107
|
+
.vs-caps, .vs-say { font-size: max(14px, 2cqi); line-height: 1.4; }
|
|
108
|
+
.vs-say { block-size: 2lh; } .vs-ch h2 { margin: 0.1em 0 0.3em; } /* the caption stands close under the headline */
|
|
109
|
+
.vs-cap { position: absolute; inset-inline: 3.6cqi; display: grid; font-weight: var(--cds-font-weight-medium); }
|
|
110
|
+
/* One sentence shows at a time. The rest, and all of them with captions off, are hidden from the eye alone. */
|
|
111
|
+
.vs-s { grid-area: 1 / 1; opacity: 0; } .vs-s.vs-on { opacity: 1; }
|
|
112
|
+
.vs-caps { pointer-events: none; } .vs-hush .vs-caps { opacity: 0; }
|
|
113
|
+
/* The controls: bare icons and CC at the top right, on the label's line, away from claude.ai's own bars under the page. On a narrow stage they have a row of their own above the label. */
|
|
114
|
+
.vs-label { margin: 0; padding-block: calc((var(--row) - 1lh) / 2); padding-inline-end: var(--ctl); text-transform: uppercase; } /* one line of it stands level with the controls, and clear of them */
|
|
115
|
+
.vs-controls { position: absolute; inset-block-start: var(--marks); inset-inline-end: calc(3.6cqi - 6px); height: var(--row); display: flex; align-items: center; gap: 8px; } /* 6px out, so that CC's letters end where the marks do */
|
|
116
|
+
/* vs-idle, vs-held and vs-touch come from the script. The controls fade by opacity, not display, so a keyboard and a screen reader still reach them.
|
|
117
|
+
:focus-visible, not :focus-within: in most browsers a button pressed with the mouse keeps focus, which would hold them up. */
|
|
118
|
+
.vs-controls { transition: opacity 200ms var(--cds-ease-out); }
|
|
119
|
+
@media (hover: hover) { .vs-playing .vs-stage:is(.vs-idle:not(.vs-held), :not(:hover, .vs-touch)):not(:has(:focus-visible)) .vs-controls { opacity: 0; transition: opacity var(--cds-dur-slow) var(--cds-ease-out) 300ms; } }
|
|
120
|
+
@media (prefers-reduced-motion: reduce) { .vs-controls { transition: none !important; } }
|
|
121
|
+
.vs-controls > button { flex: none; height: calc(100% - 8px); min-inline-size: 24px; padding-inline: 6px; font: inherit; font-size: 13px; color: var(--color-fg); background: none; border: 0; border-radius: 6px; cursor: pointer; }
|
|
122
|
+
@media (hover: hover) { .vs-controls > button:hover { background: var(--cds-border); } .vs-mk:hover::before, .vs-mk:hover i { height: 5px; } } /* on touch, hover sticks after a tap */
|
|
123
|
+
.vs-controls > button:has(svg) { display: grid; place-items: center; padding-inline: 4px; }
|
|
124
|
+
.vs-controls svg { grid-area: 1 / 1; inline-size: 16px; block-size: 16px; fill: none; stroke: currentColor; stroke-linecap: round; stroke-linejoin: round; } /* CDS's Play, Pause, CaretLeft, CaretRight, SpeakerSlash and Speaker, 16px, as its Icon draws them: a page has no icon font */
|
|
125
|
+
@media (pointer: coarse) { .vs-controls > button:has(svg) { min-inline-size: 40px; } .vs-controls svg { inline-size: 20px; block-size: 20px; } }
|
|
126
|
+
.vs-cc[aria-pressed="false"] { color: var(--color-fg-muted); text-decoration: line-through; }
|
|
127
|
+
.vs.vs-playing .vs-toggle > :first-child, .vs:not(.vs-playing) .vs-toggle > :last-child { visibility: hidden; }
|
|
128
|
+
[data-read-sound][aria-pressed="true"] > :first-child, [data-read-sound]:not([aria-pressed="true"]) > :last-child { visibility: hidden; }
|
|
129
|
+
@container (max-width: 560px) { .vs-line { gap: 4px; } .vs-say { block-size: 3lh; } .vs-chapters { padding-top: calc(var(--row) + 4px); } .vs-label { padding: 0; } }
|
|
130
|
+
.vs-now { position: absolute; width: 1px; height: 1px; margin: 0; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
|
|
131
|
+
```
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# Visual short
|
|
2
|
+
|
|
3
|
+
A Visual short is a dashboard that plays, for people who would rather watch than read. It tells one finding in a few chapters on one stage, with nothing around it. Each chapter is a drawing that builds while a caption says what it shows, and the whole piece runs a minute or two. It works when one idea is on screen at a time and each mark arrives with the words that name it.
|
|
4
|
+
|
|
5
|
+
`artifact-type/reference/visual-short-skeleton.md` (HTML and CSS) and `artifact-type/reference/visual-short-script.md` are working code for what follows. Read both before you write, and start from them; what is left is the piece itself.
|
|
6
|
+
|
|
7
|
+
## Chapters
|
|
8
|
+
|
|
9
|
+
The script finds elements by class name and pairs them in order: each `.vs-ch` with one `.vs-cap`, one `.vs-mk` and one scene. An empty `.vs-say` in each chapter keeps a row under the headline for the caption to lie over. Without it the caption covers the drawing. A chapter's label names its `.vs-mk` button and, on a wide stage, shares a line with the controls, so every chapter, the title too, has a short one.
|
|
10
|
+
|
|
11
|
+
The first chapter is the title, which says what the piece is, the period it covers and how long it runs. The piece rests on it, whole, and a thumbnail shows it too. It plays only when someone presses Play or picks a chapter, with the voice or without: motion nobody asked for distracts, and no voice can start before a press. What is faded in the last chapter stays faded, since it stays up when the piece ends.
|
|
12
|
+
|
|
13
|
+
A waiting chapter is faded with `opacity`, since under `display: none` its drawings measure 0 wide and its marks go unchecked. It is also `aria-hidden` and takes no pointer. None of that stops the keyboard, so nothing in a chapter takes focus: no link, control or `tabindex`.
|
|
14
|
+
|
|
15
|
+
The dashboard pauses the piece when someone opens Sources, picks a figure there or comments, and has each chapter stand whole while they edit by hand, so the script has no pause of its own for these.
|
|
16
|
+
|
|
17
|
+
## Drawings and motion
|
|
18
|
+
|
|
19
|
+
The dashboard compares each rendered mark that names one value with its data, at any opacity and at any moment, so such a mark holds its real value from the first draw. Drawings are made whole, and scenes only style them, but for a number that counts up: that is an unmarked `aria-hidden="true"` copy, a drawing of its own, laid over the marked one, which shows again once the count ends; `count` in the script does this.
|
|
20
|
+
|
|
21
|
+
Each chapter is a function of its own time `t` in seconds, whole at `t` = `Infinity`, so it can be held, replayed or jumped to. A CSS animation, a transition or a timer can't be, since the engine doesn't know of it. A scene takes each time from `cue(i, word)`, the moment the caption reaches that word, so motion follows the words and, where there is one, a voice's pace. With no voice, a chapter lasts a second for every 15 characters of its caption, plus one. Under `prefers-reduced-motion` the script draws each drawing whole and only the fade moves, while the sentences take their turns. Motion written anywhere but in `scenes` would still play.
|
|
22
|
+
|
|
23
|
+
A scene sets state such as `class` and `style`, and writes no text or element outside a drawing. Any text or element written under `#dash-root` has the dashboard check every mark again, so a drawing's text is written only when it changes, as `write` in the script does.
|
|
24
|
+
|
|
25
|
+
The stage is a container, so sizes inside it are in `cqi` (`max(14px, 2cqi)`). Nothing above the page is one, so outside `.vs` `cqi` is the window's. `.vs-draw` is a container of both its sizes. Inside it `cqh` and `cqi` are 1% of its height and width and `1em` is the size of the labels on the marks, and a drawing sized from those fills it, where a fixed number of pixels doesn't. The skeleton sizes only the caption and the drawing. A headline that your CSS doesn't size is 14px, smaller than the caption on a wide stage. `max(18px, 3.4cqi)` suits it, 12cqi a big number and `max(11px, 1.3cqi)` the source line. The longest bar leaves room for its value.
|
|
26
|
+
|
|
27
|
+
Values are labeled on their marks. Where they don't all fit, the chart gets axis numbers. Every chart gets a tooltip, inside its drawing. A bar or dot painted with `background` vanishes in forced colors unless it has `forced-color-adjust: none` and a system color there, as the skeleton's chapter marks have.
|
|
28
|
+
|
|
29
|
+
## Captions
|
|
30
|
+
|
|
31
|
+
Each sentence is its own `.vs-s` span and shows alone, in a row two lines high (three on a narrow stage). One that needs more lines lies over the drawing; under about 100 characters it doesn't.
|
|
32
|
+
|
|
33
|
+
Captions are hidden by `opacity` alone, so that a screen reader and a voice still get every word. Both lose the words under `display: none`, `visibility: hidden`, `hidden` or `aria-hidden`, and under `display: none` or `hidden` a figure in the caption goes unchecked too. A sentence the data rules out is the one that gets `hidden`: the script leaves it out of the timing and a voice doesn't read it.
|
|
34
|
+
|
|
35
|
+
Every number in a caption is marked and filled from the data. Words in the HTML reach everyone who opens the dashboard and never change, while each reader's page runs a live query with that reader's own access, unless someone shares its connector. A typed number could show a figure to someone with no access to it, or contradict their own, and it would go stale. A word that turns on the data (up, down, more) is filled too, from a `dash.calc`. What you type is the gist, which holds whatever the numbers turn out to be. A Visual short usually tells of a period that has ended, and unless its queries pin their dates, the numbers move on and the words don't. A certified query keeps its mark whatever its filters are set to, and loses it if its call changes. So its date is pinned by its date filter's value in `dash/params`, which the page first runs with unless a script sets another. One with no date filter can't be pinned, but a query of your own beside it can, and isn't certified.
|
|
36
|
+
|
|
37
|
+
By default a caption rounds its figures while the stage shows the exact ones. A mark that shows $100 for 102.08 differs from its data, so the rounding is a `dash.calc` and the caption's mark names it (`said` in the script). A calc can return words too ("almost half"), which the dashboard can't check.
|
|
38
|
+
|
|
39
|
+
## What makes a good one
|
|
40
|
+
|
|
41
|
+
Suggestions, which give way to what the person asks for.
|
|
42
|
+
|
|
43
|
+
**The piece.** On the title, the one figure the story turns on can stand under a headline, and how long the piece runs is worked out from the captions, not typed, so it stays true as they change. The last chapter is what to do next, and reads best ending with every mark at full strength, since it stays up. The stage stands alone: tiles, filters, tabs, tables or text around it pull the eye from what is playing.
|
|
44
|
+
|
|
45
|
+
**A chapter's frame** is the same each time, so only the content changes: a small uppercase label for the part of the story, a one-sentence headline that states the finding and not the topic, one drawing, and a small muted source line (measure, who, dates).
|
|
46
|
+
|
|
47
|
+
**The drawing** is the one that shows the sentence. A share of people: a 10 by 10 grid of dots that fills, then recolors. A total: one big number that counts, with what made it up beside it. A comparison: horizontal bars from zero, slightly rounded. A gap between two: two bars and a bracket naming the gap. A run of chapters that are all bars is a sign another drawing would fit somewhere. It fills its box, which is most of the stage: bars share its height, each row 100cqh over their number with some air around the bar, and stay bars where they are few; columns stand its full height, less their labels; a dot grid is the largest square it holds, `min(100cqh, 100cqi)` a side. What can't fill the box sits in its middle.
|
|
48
|
+
|
|
49
|
+
**How marks arrive.** Bars grow from zero, a value fades in once its bar lands, dots come in a quick wave. Quick and eased out (`d3.easeCubicOut`) feels right; camera moves, 3D and bounce pull the eye from it. Marks are `--cds-chart-muted` first, with one accent color on what the sentence is about. To focus, fade the rest rather than add a color, since another color reads as another meaning.
|
|
50
|
+
|
|
51
|
+
**The words.** A caption is one paragraph of a few short sentences, since a chapter lasts as long as its caption. It is written as if said aloud, with no symbols a voice would trip on, and each sentence reads well around a filled figure. The figure is rounded as a person would say it ($100 for 102.08, 1,900 for 1,937), with "about" before it where it helps. A caption can also leave the number out and let the stage show it.
|
|
52
|
+
|
|
53
|
+
## The voice
|
|
54
|
+
|
|
55
|
+
Some readers have a voice that reads the captions; many don't, and the piece plays whole without it. It reads only the captions, so each caption says what its chapter shows. The skeleton marks them to be read, and `lang` on their holder, `.vs-caps`, names the language they are written in. Whatever else is in it is read aloud too, and words in `[brackets]` are shown but not said, so a caption has none. The skeleton's controls and script already follow the voice: read `artifact-type/reference/read-aloud.md` before you change either.
|
|
56
|
+
|
|
57
|
+
## Telling the person
|
|
58
|
+
|
|
59
|
+
The person will want to know that Play starts the piece, and how long it runs. A reply mentions the voice only if the person asked to listen.
|
|
60
|
+
|
|
61
|
+
## A retired player
|
|
62
|
+
|
|
63
|
+
A page whose `.vs-controls` holds a `data-read-play` button has a player that is no longer made. Whenever you change such a page and it stays a Visual short, read the skeleton and the script, swap in their controls, CSS and engine, and keep the page's chapters, captions, calcs, `fmt`, drawings and `scenes`.
|