pageship 0.3.0 → 0.4.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/cli.mjs +15 -5
- package/dist/skills/pageship/SKILL.md +1 -1
- package/dist/skills/pageship-chart/SKILL.md +18 -2
- package/dist/skills/pageship-code-block/SKILL.md +1 -1
- package/dist/skills/pageship-decision-record/SKILL.md +5 -1
- package/dist/skills/pageship-diagram/SKILL.md +16 -14
- package/dist/skills/pageship-diagram/references/check-diagram.js +3 -2
- package/dist/skills/pageship-docs/SKILL.md +32 -8
- package/dist/skills/pageship-docs/references/axe.js +34 -0
- package/dist/skills/pageship-docs/references/check.md +35 -8
- package/dist/skills/pageship-docs/references/design.md +38 -18
- package/dist/skills/pageship-docs/references/html-validate-rules.mjs +326 -0
- package/dist/skills/pageship-docs/references/html-validate.json +13 -0
- package/dist/skills/pageship-docs/references/palette.mjs +791 -0
- package/dist/skills/pageship-docs/references/plain-default.md +1 -1
- package/dist/skills/pageship-explainer/SKILL.md +13 -5
- package/dist/skills/pageship-interaction/SKILL.md +7 -5
- package/dist/skills/pageship-interaction/references/pointer.md +3 -0
- package/dist/skills/pageship-interaction/references/step-through.md +10 -25
- package/dist/skills/pageship-key-figures/SKILL.md +1 -1
- package/dist/skills/pageship-media/SKILL.md +2 -2
- package/dist/skills/pageship-motion/SKILL.md +31 -28
- package/dist/skills/pageship-motion/references/figure-motion.js +252 -0
- package/dist/skills/pageship-motion/references/figure-motion.md +294 -0
- package/dist/skills/pageship-plan/SKILL.md +3 -3
- package/dist/skills/pageship-report/SKILL.md +5 -1
- package/dist/skills/pageship-review-report/SKILL.md +5 -1
- package/dist/skills/pageship-slide-deck/SKILL.md +5 -3
- package/dist/skills/pageship-slide-deck/references/engine.md +1 -1
- package/dist/skills/pageship-spec/SKILL.md +5 -1
- package/dist/skills/pageship-table/SKILL.md +5 -3
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# The plain default
|
|
2
2
|
|
|
3
|
-
A document look: serif prose with sans headings, labels and figure text, a
|
|
3
|
+
A document look: serif prose with sans headings, labels and figure text, a title band and section headings in the first accent, links in the second, framed figures with a caption strip, and tinted callouts. Extend this stylesheet with the same variables and font stacks; keep its sizes, widths and print rules. Its colour values are a plum and steel-blue set that [the palette calculator](design.md#the-palette-calculator) replaces.
|
|
4
4
|
|
|
5
5
|
```css
|
|
6
6
|
:root {
|
|
@@ -3,14 +3,14 @@ name: pageship-explainer
|
|
|
3
3
|
description: HTML explainers, as the second step of pageship-docs. For any request to explain how something works, or to write, regenerate, edit or move an explainer, load pageship-docs first; load this skill first only when the user names it.
|
|
4
4
|
license: MIT
|
|
5
5
|
metadata:
|
|
6
|
-
version: "0.
|
|
6
|
+
version: "0.4.0"
|
|
7
7
|
---
|
|
8
8
|
|
|
9
9
|
# Explainer
|
|
10
10
|
|
|
11
11
|
An explainer shows the reader how something works: the parts, what passes between them, and the rules that decide what happens next. Figures carry the explanation and short prose sits around them. A reader who looks only at the figures and their captions still learns the mechanism.
|
|
12
12
|
|
|
13
|
-
Write it by [step 4 of pageship-docs](../pageship-docs/SKILL.md#4-write-the-file), with steps 2 to 4 below in place of its Sort. Unless `pageship-docs` loaded this skill, start as [it says](../pageship-docs/SKILL.md#starting-at-a-kind-or-pattern-skill).
|
|
13
|
+
Write it by [step 4 of pageship-docs](../pageship-docs/SKILL.md#4-write-the-file), with steps 2 to 4 below in place of its Sort and Figures. Unless `pageship-docs` loaded this skill, start as [it says](../pageship-docs/SKILL.md#starting-at-a-kind-or-pattern-skill).
|
|
14
14
|
|
|
15
15
|
## 1. Gather
|
|
16
16
|
|
|
@@ -72,7 +72,7 @@ Headings, text and figure labels use the glossary's terms.
|
|
|
72
72
|
|
|
73
73
|
1. **Title and answer.** An `h1` naming what the explainer covers, then the answer, with the date and author when the source gives them.
|
|
74
74
|
2. **Overview.** The overview figure, after the sentences that introduce its parts.
|
|
75
|
-
3. **One section per stage or rule.** Each holds at most two short paragraphs around its figure: one before it introducing what the figure adds, and one after it only for a number or exception the figure doesn't show. A section that needs more prose becomes two sections, each with its own claim and figure.
|
|
75
|
+
3. **One section per stage or rule.** Each holds at most two short paragraphs around its figure: one before it introducing what the figure adds and why it matters to the answer, and one after it only for a number or exception the figure doesn't show. A section that needs more prose becomes two sections, each with its own claim and figure.
|
|
76
76
|
4. **Exceptions and limits**, when there are any.
|
|
77
77
|
5. **Open questions.**
|
|
78
78
|
6. **Sources.** Each file, with line numbers for code, and each document the explainer draws on, as the source names them.
|
|
@@ -83,10 +83,18 @@ Done when every part, move, rule and end of the trace is in a figure or the text
|
|
|
83
83
|
|
|
84
84
|
Read the file from the top against your concept list, your trace and each figure's claim, and fix what breaks a rule above. The answer still holds against each section: a condition, exception or open question that changes it, such as a check that runs on one branch only, is in the answer too, or the answer says less.
|
|
85
85
|
|
|
86
|
-
Then get a fresh reading. Your own reading follows your trace, so it passes the sentences you meant to write.
|
|
86
|
+
Then get a fresh reading. Your own reading follows your trace, so it passes the sentences you meant to write. The reader task, with every path absolute, is: "Read-only. Read [file] beside [each source file] and the Claims section of [the path of `pageship-docs/references/check.md`]. Quote every sentence, caption, label and tooltip that claims more than the source's words state, each with the source line it goes past. Then compare each figure's drawing with its caption and the source: quote every caption the drawing doesn't show, such as an arrow that starts at a part the source doesn't give that move, and every caption that makes two claims."
|
|
87
87
|
|
|
88
|
-
|
|
88
|
+
- **When you can start a subagent**, start one that has seen neither this conversation nor your trace, give it only the reader task, and wait for its answer before you fix anything or hand over.
|
|
89
|
+
- **When you are a subagent that can't start one**, such as a Claude Code subagent, hand over with the reader task, as the hand-over says. The agent that started you runs the reader and brings you its quotes.
|
|
90
|
+
- **Without subagents at all**, read the file once more from the source's side: for each sentence, find the source line before you read the next, then compare each figure's drawing with its caption.
|
|
91
|
+
|
|
92
|
+
Fix each quote the fresh reading returns, or keep it and say in the hand-over why the source states it. Quotes that the agent that started you brings are the fresh reading's answer, so handling them completes it.
|
|
93
|
+
|
|
94
|
+
Done when every quote is fixed or its reason is in the hand-over, or, as a subagent that couldn't start the reader, when your hand-over gives the reader task.
|
|
89
95
|
|
|
90
96
|
## Hand over
|
|
91
97
|
|
|
92
98
|
The structure you introduced includes the sections and their order, each figure's form and why, and any worked example. Also list the behaviour you left out as outside the question.
|
|
99
|
+
|
|
100
|
+
As a subagent that couldn't start the fresh reading's reader, also list "Fresh reading still to run", followed by the reader task word for word, with every path in it absolute.
|
|
@@ -3,7 +3,7 @@ name: pageship-interaction
|
|
|
3
3
|
description: Later step of pageship-docs: for a request to write or edit a document, load pageship-docs first; load this skill first only when the user names it. Pattern for controls in a Pageship HTML document, such as stepping through stages, tracing a path, tabs, filters, revealing detail and pointer states on charts and diagrams, loaded by a pageship- kind skill before it adds any control.
|
|
4
4
|
license: MIT
|
|
5
5
|
metadata:
|
|
6
|
-
version: "0.
|
|
6
|
+
version: "0.4.0"
|
|
7
7
|
---
|
|
8
8
|
|
|
9
9
|
# Interaction
|
|
@@ -47,17 +47,19 @@ Then build each control by its pattern below and the rules under [Controls](#con
|
|
|
47
47
|
|
|
48
48
|
## Check
|
|
49
49
|
|
|
50
|
+
Run the browser steps with [the check's browser tool](../pageship-docs/references/check.md#checks-in-a-browser).
|
|
51
|
+
|
|
50
52
|
- Before any input, take a screenshot and confirm that every item, and every number, name and date the source gives for it, is on screen.
|
|
51
53
|
- Tab through the page. Every control and each chart and diagram takes focus in reading order. Tabs, and the marks or nodes of a figure, also move with the arrow keys.
|
|
52
54
|
- Use each control that selects or toggles, and confirm its state changed in its attribute and on screen.
|
|
53
|
-
-
|
|
55
|
+
- In print, confirm every step, panel, item and detail shows.
|
|
54
56
|
- Without a browser, read the script and confirm every control is a `button`, link, `summary` or labelled input with the state its pattern names.
|
|
55
57
|
|
|
56
58
|
## Patterns
|
|
57
59
|
|
|
58
60
|
### Step-through
|
|
59
61
|
|
|
60
|
-
For a sequence of three to eight stages the reader follows one at a time.
|
|
62
|
+
For a sequence of three to eight stages the reader follows one at a time. A diagram's steps play as a sequence by `pageship-motion` instead.
|
|
61
63
|
|
|
62
64
|
- All stages are an ordered list in the HTML, each with its number, its name and its essential values. Every stage shows on load and in print; stepping marks one stage as current and dims the later ones.
|
|
63
65
|
- The current stage carries `aria-current="step"` and a mark that is not colour alone, such as weight and a border.
|
|
@@ -68,7 +70,7 @@ For a sequence of three to eight stages the reader follows one at a time.
|
|
|
68
70
|
|
|
69
71
|
### Path trace
|
|
70
72
|
|
|
71
|
-
A
|
|
73
|
+
A sequence over a diagram, for something that travels: a ticket through queues, a request through services. Draw the diagram as inline SVG by `pageship-diagram`, with every node keeping its text label at all times, and play it as a sequence by `pageship-motion`, which gives the controls and the list of steps. The same current step drives the list and the diagram: the current node and the connector into it take a thicker stroke.
|
|
72
74
|
|
|
73
75
|
### Filter
|
|
74
76
|
|
|
@@ -111,5 +113,5 @@ For chart marks, diagram nodes and connectors, so the reader can point at one fo
|
|
|
111
113
|
- **Focus.** Every control keeps its `:focus-visible` outline.
|
|
112
114
|
- **Active state.** A control that selects or toggles something shows its state twice: in the attribute its pattern names (`aria-current`, `aria-pressed`, `aria-selected`, or `open` on `details`) and on screen by more than colour, such as weight, a border, an underline or a word. Style the active look from the attribute, such as `[aria-pressed="true"]`.
|
|
113
115
|
- **Size.** Every control is at least 24 by 24 pixels, and its label says what it does ("Next stage", not "›" alone).
|
|
114
|
-
- **State.** Keep it in page variables and the URL fragment.
|
|
116
|
+
- **State.** Keep it in page variables, and in the URL fragment where the pattern records it.
|
|
115
117
|
- **Script.** Put the script at the end of `body`.
|
|
@@ -103,6 +103,8 @@ Copy this script once, at the end of the `<body>`.
|
|
|
103
103
|
const mark = title.parentElement;
|
|
104
104
|
if (mark.matches("g, .target, .forecast")) continue;
|
|
105
105
|
mark.dataset.tip = words(title);
|
|
106
|
+
// An SVG shape takes a name only with a role.
|
|
107
|
+
mark.setAttribute("role", "img");
|
|
106
108
|
mark.setAttribute("aria-label", mark.dataset.tip);
|
|
107
109
|
enlist(mark);
|
|
108
110
|
title.remove();
|
|
@@ -111,6 +113,7 @@ Copy this script once, at the end of the `<body>`.
|
|
|
111
113
|
// A node's <title>, such as a timeline event's, becomes its name and its one tooltip.
|
|
112
114
|
for (const title of svg.querySelectorAll(".node > title")) {
|
|
113
115
|
title.parentElement.dataset.label = words(title);
|
|
116
|
+
title.parentElement.setAttribute("role", "img");
|
|
114
117
|
title.parentElement.setAttribute("aria-label", words(title));
|
|
115
118
|
title.remove();
|
|
116
119
|
}
|
|
@@ -1,46 +1,31 @@
|
|
|
1
1
|
# Step-through
|
|
2
2
|
|
|
3
|
-
The markup, style and script for [the step-through](../SKILL.md#step-through)
|
|
3
|
+
The markup, style and script for [the step-through](../SKILL.md#step-through). Copy them unchanged; change only the ids, the steps and the labels. A diagram's steps are a sequence by `pageship-motion`, not a step-through.
|
|
4
4
|
|
|
5
5
|
## Markup
|
|
6
6
|
|
|
7
|
-
Give the step-through an `id` and the class `stepper
|
|
7
|
+
Give the step-through an `id` and the class `stepper`, with a heading. List the steps as an ordered list, numbered with `data-step` from 1. Each step starts with its name in a `<strong>`, then its essential values. The controls and the status line carry `hidden`, so a reader without script never sees them.
|
|
8
8
|
|
|
9
9
|
```html
|
|
10
|
-
<
|
|
11
|
-
<
|
|
12
|
-
<svg ...>
|
|
13
|
-
...
|
|
14
|
-
<path class="edge" data-step="2" data-from="path-triage" data-to="path-billing" ... />
|
|
15
|
-
<g class="node" data-step="1" id="path-triage">...</g>
|
|
16
|
-
</svg>
|
|
17
|
-
</div>
|
|
10
|
+
<section class="stepper" id="rollout">
|
|
11
|
+
<h3>The rollout, step by step</h3>
|
|
18
12
|
<ol class="steps">
|
|
19
|
-
<li data-step="1"><strong>
|
|
20
|
-
<li data-step="2">
|
|
21
|
-
|
|
22
|
-
</li>
|
|
13
|
+
<li data-step="1"><strong>Pilot</strong>: two teams, 4 to 18 March.</li>
|
|
14
|
+
<li data-step="2"><strong>Half the fleet</strong>: 310 devices from 25 March.</li>
|
|
15
|
+
<li data-step="3"><strong>Everyone</strong>: 8 April.</li>
|
|
23
16
|
</ol>
|
|
24
|
-
<div class="step-controls" role="toolbar" aria-label="Step through the
|
|
17
|
+
<div class="step-controls" role="toolbar" aria-label="Step through the rollout" hidden>
|
|
25
18
|
<button type="button" data-go="previous">Previous step</button>
|
|
26
19
|
<button type="button" data-go="next">Next step</button>
|
|
27
20
|
<button type="button" data-go="all">Show all steps</button>
|
|
28
21
|
</div>
|
|
29
22
|
<p class="step-status" role="status" aria-live="polite" hidden></p>
|
|
30
|
-
|
|
31
|
-
</figure>
|
|
23
|
+
</section>
|
|
32
24
|
```
|
|
33
25
|
|
|
34
|
-
A step-through without a diagram is the same markup with a `<section class="stepper" id="...">` in place of the figure, a heading in place of the caption, and no SVG.
|
|
35
|
-
|
|
36
26
|
## Style
|
|
37
27
|
|
|
38
28
|
```css
|
|
39
|
-
.stepper .is-current rect,
|
|
40
|
-
.stepper .edge.is-current {
|
|
41
|
-
stroke: var(--accent);
|
|
42
|
-
stroke-width: 3;
|
|
43
|
-
}
|
|
44
29
|
.stepper li.is-current {
|
|
45
30
|
font-weight: 600;
|
|
46
31
|
border-left: 3px solid var(--accent);
|
|
@@ -143,4 +128,4 @@ Copy this script once, at the end of the `<body>`. It drives every `.stepper` on
|
|
|
143
128
|
|
|
144
129
|
## Check
|
|
145
130
|
|
|
146
|
-
Press each control by keyboard, and the Left and Right arrow keys on them. Confirm the page opens showing every step, Next walks each step in turn and then shows every step again, the current step's list item carries `aria-current="step"`, and with reduced motion emulated every step stays at full strength. Open the page with a step's fragment, such as `#
|
|
131
|
+
Press each control by keyboard, and the Left and Right arrow keys on them. Confirm the page opens showing every step, Next walks each step in turn and then shows every step again, the current step's list item carries `aria-current="step"`, and with reduced motion emulated every step stays at full strength. Open the page with a step's fragment, such as `#rollout-2`, and confirm it opens on that step.
|
|
@@ -3,7 +3,7 @@ name: pageship-key-figures
|
|
|
3
3
|
description: Later step of pageship-docs: for a request to write or edit a document, load pageship-docs first; load this skill first only when the user names it. Pattern for the few numbers and callouts a Pageship HTML document opens with, loaded by a pageship- kind skill before it writes key figures.
|
|
4
4
|
license: MIT
|
|
5
5
|
metadata:
|
|
6
|
-
version: "0.
|
|
6
|
+
version: "0.4.0"
|
|
7
7
|
---
|
|
8
8
|
|
|
9
9
|
# Key figures
|
|
@@ -3,7 +3,7 @@ name: pageship-media
|
|
|
3
3
|
description: Later step of pageship-docs: for a request to write or edit a document, load pageship-docs first; load this skill first only when the user names it. Pattern for images, video and audio in a Pageship HTML document, loaded by a pageship- kind skill before it shows media.
|
|
4
4
|
license: MIT
|
|
5
5
|
metadata:
|
|
6
|
-
version: "0.
|
|
6
|
+
version: "0.4.0"
|
|
7
7
|
---
|
|
8
8
|
|
|
9
9
|
# Media
|
|
@@ -98,7 +98,7 @@ audio {
|
|
|
98
98
|
|
|
99
99
|
## Check
|
|
100
100
|
|
|
101
|
-
Run this in the browser
|
|
101
|
+
Run this in the page with [the check's browser tool](../pageship-docs/references/check.md#checks-in-a-browser) and fix every item it returns:
|
|
102
102
|
|
|
103
103
|
```js
|
|
104
104
|
[
|
|
@@ -1,58 +1,61 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: pageship-motion
|
|
3
|
-
description: Later step of pageship-docs: for a request to write or edit a document, load pageship-docs first; load this skill first only when the user names it. Pattern for animation and transitions in a Pageship HTML document, such as
|
|
3
|
+
description: Later step of pageship-docs: for a request to write or edit a document, load pageship-docs first; load this skill first only when the user names it. Pattern for animation and transitions in a Pageship HTML document, such as a chart that grows into view, a sequence that plays through a diagram, or a marker moving along a path, loaded by a pageship- kind skill before it draws a chart, a graphic or a sequence, or before anything else on the page moves.
|
|
4
4
|
license: MIT
|
|
5
5
|
metadata:
|
|
6
|
-
version: "0.
|
|
6
|
+
version: "0.4.0"
|
|
7
7
|
---
|
|
8
8
|
|
|
9
9
|
# Motion
|
|
10
10
|
|
|
11
|
-
Motion shows the reader
|
|
11
|
+
Motion shows the reader how a figure takes shape: the values a chart reaches, the order a process runs in, what caused what. Every chart, graphic and sequence plays once when it scrolls into view, and everything else on the page holds still.
|
|
12
12
|
|
|
13
13
|
Unless another pageship- skill loaded this one, start as [pageship-docs says](../pageship-docs/SKILL.md#starting-at-a-kind-or-pattern-skill). Named for just an animation, the file holds the animation and the content it moves.
|
|
14
14
|
|
|
15
15
|
## 1. Decide what moves
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
Two kinds of figure play, each once, when it scrolls into view:
|
|
18
18
|
|
|
19
|
-
- **
|
|
20
|
-
- **
|
|
21
|
-
- **Cause.** An event and its effect, such as a marker moving from one stage to the next when the reader presses Next.
|
|
19
|
+
- **Charts and graphics.** Every chart, whatever its form: the forms `pageship-chart` lists and any it adds, small multiples, a graphic beside a key figure, an illustration, and a diagram with no step order, such as a system map, a state diagram or a tree. It opens on its initial state and plays to its values: bars grow from the baseline, a line draws from left to right, points appear, the parts of a share bar fill in, histogram bins rise, a diagram's nodes appear and then its connectors draw, a graphic builds up. `pageship-chart` names each form's entrance.
|
|
20
|
+
- **Sequences.** A diagram that shows a process, a path, an order of events, or a cause and its effect, in steps: a marker moving along a path, stages lighting up in order, a packet travelling through a system, events appearing along a timeline. It plays from the first step to the last.
|
|
22
21
|
|
|
23
22
|
A transition of 300 ms or less may also carry the eye across a change the reader just made, such as the highlight moving to the new stage.
|
|
24
23
|
|
|
25
|
-
Everything else
|
|
24
|
+
Everything else holds still: text, headings and sections appear in full, and numbers show their final value from the start. Emphasis comes from position, size, weight and the accent colour, so nothing pulses, bounces, lifts on hover or drifts in the background, and no figure loops on its own.
|
|
26
25
|
|
|
27
|
-
|
|
28
|
-
- Numbers show their final value and text shows in full from the start.
|
|
29
|
-
- Nothing pulses, bounces, loops, lifts on hover or drifts in the background. Emphasis comes from position, size, weight and the accent colour.
|
|
26
|
+
A request to make a document more engaging, lively or polished asks for clearer content and layout: a stronger summary, key figures up front, better headings, a table or a chart where it helps. The figures' own playback is all the motion it adds. When the user or a design skill asks for motion this section rules out, leave it out and say so in your hand-over.
|
|
30
27
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
Done when every planned motion has its job and the reader action that starts it, or you planned none.
|
|
28
|
+
Done when every chart, graphic and sequence has its kind, and every other motion has its job and the reader action that starts it.
|
|
34
29
|
|
|
35
30
|
## 2. Build it so the still page tells the whole story
|
|
36
31
|
|
|
37
|
-
- **The
|
|
38
|
-
- **
|
|
39
|
-
- **
|
|
40
|
-
- **
|
|
41
|
-
- **
|
|
42
|
-
- **Pace.** A
|
|
43
|
-
- **One thing at a time.** Animate only the element that carries the change,
|
|
32
|
+
- **The end state lives in the markup.** The HTML and SVG hold the final values: every bar at its height, every point, the marker at the end of its path. Script moves the figure to its initial state only when it is about to play. Without script, in print, under reduced motion, and before the figure scrolls into view, the reader sees the complete figure with every value.
|
|
33
|
+
- **Plays once, in view.** When enough of the figure scrolls into view, it plays from its initial state and stops on the end state. A figure in view when the page loads plays at once. Loop, a button the reader turns on, is the only way a figure repeats; off again, it stops on the end state.
|
|
34
|
+
- **The reader stays in control.** A chart has Replay and Loop. A sequence has Play/Pause, whose label says what it will do, Back, Next, Replay and Loop, and a position line naming the current step, such as "Step 2 of 5: Tagging", with `aria-live="polite"`. Script adds the buttons, by the [controls rules](../pageship-interaction/SKILL.md#controls) of `pageship-interaction`, so a reader without script never sees them.
|
|
35
|
+
- **Paused keeps context.** When the reader pauses or steps part way through a sequence, the parts not yet reached stay visible but faint.
|
|
36
|
+
- **The values in text.** Every value a frame shows is also in text on the page: a chart's printed values and its table, and a sequence's ordered list of steps with their values.
|
|
37
|
+
- **Pace.** A chart's entrance takes about one second, two at most. A sequence's step holds long enough to read its value, about 500 to 800 ms, and a full playback takes about 10 seconds or less. Motion eases out, and the transitions of step 1 take 150 to 300 ms.
|
|
38
|
+
- **One thing at a time.** Animate only the element that carries the change, with `transform`, `opacity` or SVG attributes.
|
|
39
|
+
|
|
40
|
+
[The reference script](references/figure-motion.md) gives one timeline where each frame is computed from the step and the time within it, the controls, the start in view, reduced motion, and a marker along an SVG path, with a worked chart and sequence. It is optional: use it, adapt it, or write your own that keeps these rules.
|
|
41
|
+
|
|
42
|
+
Done when each playing figure's markup holds its end state and its script holds its initial state and the way between.
|
|
44
43
|
|
|
45
44
|
## 3. Respect reduced motion
|
|
46
45
|
|
|
47
|
-
Under `prefers-reduced-motion: reduce`, nothing
|
|
46
|
+
Under `prefers-reduced-motion: reduce`, nothing plays on its own: every figure stays on its end state after loading and scrolling. Every control still works and moves between states at once: Next jumps to the next step, Replay shows the first step, and Play shows each step in turn with no movement between them.
|
|
48
47
|
|
|
49
48
|
- In CSS, turn off every animation and transition under that media query.
|
|
50
|
-
- In script, check `matchMedia("(prefers-reduced-motion: reduce)").matches` each time before you
|
|
49
|
+
- In script, check `matchMedia("(prefers-reduced-motion: reduce)").matches` each time before you play, and set the state directly when it is true.
|
|
51
50
|
|
|
52
51
|
## Check
|
|
53
52
|
|
|
54
|
-
-
|
|
55
|
-
|
|
56
|
-
-
|
|
57
|
-
-
|
|
58
|
-
-
|
|
53
|
+
Run the browser steps with [the check's browser tool](../pageship-docs/references/check.md#checks-in-a-browser). `html_preview` screenshots the page right after load, so a figure in view at load is caught at its initial state or part way; a step that waits for playback takes the step for a missing browser.
|
|
54
|
+
|
|
55
|
+
- **Scripts off.** In a copy with the motion script removed, every chart and graphic shows every value and part, every sequence shows its whole picture, and no playback control shows.
|
|
56
|
+
- **Before scrolling.** With script on, load the page at 1280px and look at a figure below the first screen before scrolling to it: a chart shows every value and a sequence its whole picture, with the controls present.
|
|
57
|
+
- **Plays once, in view.** Scroll each figure into view: a chart or graphic plays from its initial state and a sequence from its first step, and each stops on the end state and stays there. Wait twice the playback's length and confirm nothing changed.
|
|
58
|
+
- **Controls.** Replay plays again. Loop repeats the playback until turned off, then it stops on the end state. In a sequence, Play and Pause do what the label says, Back and Next move one step and the position line names it, and the parts not yet reached stay faint. Each works by keyboard and shows focus.
|
|
59
|
+
- **Reduced motion.** After loading and scrolling, every figure is on its end state and nothing moved. Every control moves between states at once.
|
|
60
|
+
- **Print.** Every figure shows its end state with every value, and no control.
|
|
61
|
+
- **Without a browser**, read the markup and script against steps 1 to 3: the markup holds the end state, the script moves to the initial state only to play, nothing loops without the Loop button, and the script checks reduced motion before every play.
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
// Plays a figure once when it scrolls into view and gives the reader its controls, by the
|
|
2
|
+
// pageship-motion rules. Copy the whole file into a <script> at the end of <body>, then call
|
|
3
|
+
// animateFigure(figure, options) once per figure, as figure-motion.md shows.
|
|
4
|
+
//
|
|
5
|
+
// options.steps is a list of { duration, hold } in milliseconds: the motion of a step, then a
|
|
6
|
+
// pause on its end state. options.frame(step, t) draws the figure at a step index and an eased
|
|
7
|
+
// time t from 0 to 1 within it: frame(0, 0) is the initial state, and frame(last, 1) the end
|
|
8
|
+
// state, which equals the markup. options.kind is "chart", with Replay and Loop, or "sequence",
|
|
9
|
+
// with Play/Pause, Back, Next, Replay, Loop and a position line; a sequence also marks the
|
|
10
|
+
// current and later parts on every [data-step] element in the figure. options.ease replaces the
|
|
11
|
+
// ease-out curve. The call returns the controller it also stores on figure.motion.
|
|
12
|
+
|
|
13
|
+
const reducedMotion = matchMedia("(prefers-reduced-motion: reduce)");
|
|
14
|
+
const printing = matchMedia("print");
|
|
15
|
+
|
|
16
|
+
function animateFigure(figure, { kind = "chart", steps, frame, ease = (t) => 1 - (1 - t) ** 3 }) {
|
|
17
|
+
const starts = [];
|
|
18
|
+
let end = 0;
|
|
19
|
+
for (const step of steps) {
|
|
20
|
+
starts.push(end);
|
|
21
|
+
end += (step.duration ?? 0) + (step.hold ?? 0);
|
|
22
|
+
}
|
|
23
|
+
const parts = figure.querySelectorAll("[data-step]");
|
|
24
|
+
const items = figure.querySelectorAll("ol.steps > li");
|
|
25
|
+
const name = (item) => (item?.querySelector("strong") ?? item)?.textContent.trim() ?? "";
|
|
26
|
+
|
|
27
|
+
// The controls, which only a reader with script sees.
|
|
28
|
+
const title = figure.querySelector("svg > title")?.textContent.trim();
|
|
29
|
+
const controls = document.createElement("div");
|
|
30
|
+
controls.className = "motion-controls";
|
|
31
|
+
controls.setAttribute("role", "toolbar");
|
|
32
|
+
controls.setAttribute("aria-label", title ? `Playback of ${title}` : "Playback");
|
|
33
|
+
const button = (label, pressed) => {
|
|
34
|
+
const element = document.createElement("button");
|
|
35
|
+
element.type = "button";
|
|
36
|
+
element.textContent = label;
|
|
37
|
+
if (pressed) element.setAttribute("aria-pressed", "false");
|
|
38
|
+
controls.append(element);
|
|
39
|
+
return element;
|
|
40
|
+
};
|
|
41
|
+
const sequence = kind === "sequence";
|
|
42
|
+
const playButton = sequence ? button("Play") : null;
|
|
43
|
+
const backButton = sequence ? button("Back") : null;
|
|
44
|
+
const nextButton = sequence ? button("Next") : null;
|
|
45
|
+
const replayButton = button("Replay");
|
|
46
|
+
const loopButton = button("Loop", true);
|
|
47
|
+
const status = sequence ? document.createElement("p") : null;
|
|
48
|
+
if (status) {
|
|
49
|
+
status.className = "motion-status";
|
|
50
|
+
status.setAttribute("role", "status");
|
|
51
|
+
status.setAttribute("aria-live", "polite");
|
|
52
|
+
}
|
|
53
|
+
const caption = figure.querySelector(":scope > figcaption");
|
|
54
|
+
if (caption) caption.before(controls, ...(status ? [status] : []));
|
|
55
|
+
else figure.append(controls, ...(status ? [status] : []));
|
|
56
|
+
|
|
57
|
+
// The position along the timeline, in milliseconds; `end` is the markup's end state. What
|
|
58
|
+
// shows is a state: a step, the eased time within it, and whether the whole figure is done.
|
|
59
|
+
let position = end;
|
|
60
|
+
// The state on screen, which Back, Next and a Play after them continue from.
|
|
61
|
+
let shown = { index: steps.length - 1, t: 1, done: true };
|
|
62
|
+
let playing = false;
|
|
63
|
+
let started = false;
|
|
64
|
+
let last = 0;
|
|
65
|
+
let pending = 0;
|
|
66
|
+
|
|
67
|
+
// The step a position falls in. A step's end belongs to it, not to the next step's start.
|
|
68
|
+
const stepAt = (at) => {
|
|
69
|
+
let index = steps.length - 1;
|
|
70
|
+
while (index > 0 && at <= starts[index]) index--;
|
|
71
|
+
return index;
|
|
72
|
+
};
|
|
73
|
+
const stateAt = (at) => {
|
|
74
|
+
if (at >= end) return { index: steps.length - 1, t: 1, done: true };
|
|
75
|
+
const index = stepAt(at);
|
|
76
|
+
const { duration = 0 } = steps[index];
|
|
77
|
+
return { index, t: duration ? Math.min(1, (at - starts[index]) / duration) : 1, done: false };
|
|
78
|
+
};
|
|
79
|
+
const label = () => {
|
|
80
|
+
if (playButton) playButton.textContent = playing ? "Pause" : "Play";
|
|
81
|
+
};
|
|
82
|
+
const render = (state) => {
|
|
83
|
+
const { index, t, done } = state;
|
|
84
|
+
shown = state;
|
|
85
|
+
frame(index, ease(t));
|
|
86
|
+
const current = done ? 0 : index + 1;
|
|
87
|
+
for (const part of parts) {
|
|
88
|
+
const n = Number(part.dataset.step);
|
|
89
|
+
part.classList.toggle("is-current", current > 0 && n === current);
|
|
90
|
+
part.classList.toggle("is-later", current > 0 && n > current);
|
|
91
|
+
if (part.tagName !== "LI") continue;
|
|
92
|
+
if (current > 0 && n === current) part.setAttribute("aria-current", "step");
|
|
93
|
+
else part.removeAttribute("aria-current");
|
|
94
|
+
}
|
|
95
|
+
if (status) {
|
|
96
|
+
status.textContent = current
|
|
97
|
+
? `Step ${current} of ${steps.length}: ${name(items[current - 1])}`
|
|
98
|
+
: `Showing all ${steps.length} steps.`;
|
|
99
|
+
}
|
|
100
|
+
label();
|
|
101
|
+
};
|
|
102
|
+
const looping = () => loopButton.getAttribute("aria-pressed") === "true";
|
|
103
|
+
const pause = () => {
|
|
104
|
+
playing = false;
|
|
105
|
+
cancelAnimationFrame(pending);
|
|
106
|
+
clearTimeout(pending);
|
|
107
|
+
label();
|
|
108
|
+
};
|
|
109
|
+
const tick = (now) => {
|
|
110
|
+
if (!playing) return;
|
|
111
|
+
if (reducedMotion.matches) return finish();
|
|
112
|
+
// A tab in the background catches up at most 80 ms per frame, so it never jumps to the end.
|
|
113
|
+
// The first frame's time can come before the playback started, which adds nothing.
|
|
114
|
+
position = Math.min(end, position + Math.min(Math.max(0, now - last), 80));
|
|
115
|
+
last = now;
|
|
116
|
+
render(stateAt(position));
|
|
117
|
+
if (position < end) {
|
|
118
|
+
pending = requestAnimationFrame(tick);
|
|
119
|
+
} else if (looping()) {
|
|
120
|
+
// The end state shows for one frame before the next playback starts.
|
|
121
|
+
position = 0;
|
|
122
|
+
pending = requestAnimationFrame(tick);
|
|
123
|
+
} else {
|
|
124
|
+
pause();
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
// Under reduced motion, playback shows each step's end state for its time, with nothing between.
|
|
128
|
+
const stepwise = (index) => {
|
|
129
|
+
if (!playing) return;
|
|
130
|
+
if (index >= steps.length) {
|
|
131
|
+
if (!looping()) return finish();
|
|
132
|
+
index = 0;
|
|
133
|
+
}
|
|
134
|
+
const { duration = 0, hold = 0 } = steps[index];
|
|
135
|
+
position = starts[index] + duration;
|
|
136
|
+
render({ index, t: 1, done: false });
|
|
137
|
+
pending = setTimeout(() => stepwise(index + 1), Math.max(duration + hold, 600));
|
|
138
|
+
};
|
|
139
|
+
const play = (from) => {
|
|
140
|
+
started = true;
|
|
141
|
+
playing = true;
|
|
142
|
+
if (reducedMotion.matches) {
|
|
143
|
+
stepwise(from ?? (shown.done ? 0 : shown.t >= 1 ? shown.index + 1 : shown.index));
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
// Playback continues from the state on screen, or starts over after the end.
|
|
147
|
+
if (from === 0 || shown.done) {
|
|
148
|
+
position = 0;
|
|
149
|
+
render(stateAt(0));
|
|
150
|
+
} else {
|
|
151
|
+
position = starts[shown.index] + (steps[shown.index].duration ?? 0) * shown.t;
|
|
152
|
+
render(shown);
|
|
153
|
+
}
|
|
154
|
+
last = performance.now();
|
|
155
|
+
pending = requestAnimationFrame(tick);
|
|
156
|
+
};
|
|
157
|
+
const replay = () => {
|
|
158
|
+
pause();
|
|
159
|
+
position = 0;
|
|
160
|
+
play(0);
|
|
161
|
+
};
|
|
162
|
+
const finish = () => {
|
|
163
|
+
pause();
|
|
164
|
+
position = end;
|
|
165
|
+
render(stateAt(end));
|
|
166
|
+
};
|
|
167
|
+
// Back and Next move between each step's end state and the end of the whole figure.
|
|
168
|
+
const go = (by) => {
|
|
169
|
+
pause();
|
|
170
|
+
started = true;
|
|
171
|
+
const states = steps.length + 1;
|
|
172
|
+
const at = shown.done ? steps.length : shown.index;
|
|
173
|
+
const to = (at + by + states) % states;
|
|
174
|
+
if (to === steps.length) return finish();
|
|
175
|
+
position = starts[to] + (steps[to].duration ?? 0);
|
|
176
|
+
render({ index: to, t: 1, done: false });
|
|
177
|
+
};
|
|
178
|
+
// Shows a step at an eased time t, for a check that needs a state part way.
|
|
179
|
+
const seek = (index, t) => {
|
|
180
|
+
pause();
|
|
181
|
+
started = true;
|
|
182
|
+
position = starts[index] + (steps[index].duration ?? 0) * t;
|
|
183
|
+
render({ index, t, done: false });
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
playButton?.addEventListener("click", () => (playing ? pause() : play()));
|
|
187
|
+
backButton?.addEventListener("click", () => go(-1));
|
|
188
|
+
nextButton?.addEventListener("click", () => go(1));
|
|
189
|
+
replayButton.addEventListener("click", replay);
|
|
190
|
+
loopButton.addEventListener("click", () => {
|
|
191
|
+
const on = !looping();
|
|
192
|
+
loopButton.setAttribute("aria-pressed", String(on));
|
|
193
|
+
if (on && !playing) play();
|
|
194
|
+
});
|
|
195
|
+
controls.addEventListener("keydown", (event) => {
|
|
196
|
+
const by = { ArrowRight: 1, ArrowLeft: -1 }[event.key];
|
|
197
|
+
if (!by || !sequence) return;
|
|
198
|
+
event.preventDefault();
|
|
199
|
+
go(by);
|
|
200
|
+
});
|
|
201
|
+
addEventListener("beforeprint", finish);
|
|
202
|
+
printing.addEventListener("change", () => printing.matches && finish());
|
|
203
|
+
|
|
204
|
+
// The first time enough of the figure is in view, it plays from its initial state. Until
|
|
205
|
+
// then, under reduced motion and in print, the markup's end state shows. A figure taller than
|
|
206
|
+
// the window counts as in view once half the window shows it, so the thresholds run in small
|
|
207
|
+
// steps up to 35% to catch that crossing in a figure many windows tall.
|
|
208
|
+
const enough = (entry) =>
|
|
209
|
+
entry.isIntersecting &&
|
|
210
|
+
(entry.intersectionRatio >= 0.35 || entry.intersectionRect.height >= innerHeight / 2);
|
|
211
|
+
const observer = new IntersectionObserver(
|
|
212
|
+
(entries) => {
|
|
213
|
+
if (started) return observer.disconnect();
|
|
214
|
+
if (!entries.some(enough)) return;
|
|
215
|
+
observer.disconnect();
|
|
216
|
+
if (!reducedMotion.matches) replay();
|
|
217
|
+
},
|
|
218
|
+
{ threshold: [0, 0.01, 0.02, 0.05, 0.1, 0.15, 0.2, 0.25, 0.3, 0.35] },
|
|
219
|
+
);
|
|
220
|
+
if (!printing.matches) observer.observe(figure);
|
|
221
|
+
|
|
222
|
+
render(stateAt(end));
|
|
223
|
+
figure.motion = {
|
|
224
|
+
play: () => play(),
|
|
225
|
+
pause,
|
|
226
|
+
replay,
|
|
227
|
+
back: () => go(-1),
|
|
228
|
+
next: () => go(1),
|
|
229
|
+
finish,
|
|
230
|
+
seek,
|
|
231
|
+
};
|
|
232
|
+
return figure.motion;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// The point a fraction t along an SVG path, in the SVG's own units.
|
|
236
|
+
function alongPath(path, t) {
|
|
237
|
+
const length = path.getTotalLength();
|
|
238
|
+
const point = path.getPointAtLength(Math.min(1, Math.max(0, t)) * length);
|
|
239
|
+
return { x: point.x, y: point.y };
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Draws an SVG path up to a fraction t of its length. At 1 the path is as the markup draws it.
|
|
243
|
+
function drawPath(path, t) {
|
|
244
|
+
if (t >= 1) {
|
|
245
|
+
path.removeAttribute("stroke-dasharray");
|
|
246
|
+
path.removeAttribute("stroke-dashoffset");
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
const length = path.getTotalLength();
|
|
250
|
+
path.setAttribute("stroke-dasharray", `${length} ${length}`);
|
|
251
|
+
path.setAttribute("stroke-dashoffset", `${length * (1 - Math.max(0, t))}`);
|
|
252
|
+
}
|