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.
Files changed (32) hide show
  1. package/dist/cli.mjs +15 -5
  2. package/dist/skills/pageship/SKILL.md +1 -1
  3. package/dist/skills/pageship-chart/SKILL.md +18 -2
  4. package/dist/skills/pageship-code-block/SKILL.md +1 -1
  5. package/dist/skills/pageship-decision-record/SKILL.md +5 -1
  6. package/dist/skills/pageship-diagram/SKILL.md +16 -14
  7. package/dist/skills/pageship-diagram/references/check-diagram.js +3 -2
  8. package/dist/skills/pageship-docs/SKILL.md +32 -8
  9. package/dist/skills/pageship-docs/references/axe.js +34 -0
  10. package/dist/skills/pageship-docs/references/check.md +35 -8
  11. package/dist/skills/pageship-docs/references/design.md +38 -18
  12. package/dist/skills/pageship-docs/references/html-validate-rules.mjs +326 -0
  13. package/dist/skills/pageship-docs/references/html-validate.json +13 -0
  14. package/dist/skills/pageship-docs/references/palette.mjs +791 -0
  15. package/dist/skills/pageship-docs/references/plain-default.md +1 -1
  16. package/dist/skills/pageship-explainer/SKILL.md +13 -5
  17. package/dist/skills/pageship-interaction/SKILL.md +7 -5
  18. package/dist/skills/pageship-interaction/references/pointer.md +3 -0
  19. package/dist/skills/pageship-interaction/references/step-through.md +10 -25
  20. package/dist/skills/pageship-key-figures/SKILL.md +1 -1
  21. package/dist/skills/pageship-media/SKILL.md +2 -2
  22. package/dist/skills/pageship-motion/SKILL.md +31 -28
  23. package/dist/skills/pageship-motion/references/figure-motion.js +252 -0
  24. package/dist/skills/pageship-motion/references/figure-motion.md +294 -0
  25. package/dist/skills/pageship-plan/SKILL.md +3 -3
  26. package/dist/skills/pageship-report/SKILL.md +5 -1
  27. package/dist/skills/pageship-review-report/SKILL.md +5 -1
  28. package/dist/skills/pageship-slide-deck/SKILL.md +5 -3
  29. package/dist/skills/pageship-slide-deck/references/engine.md +1 -1
  30. package/dist/skills/pageship-spec/SKILL.md +5 -1
  31. package/dist/skills/pageship-table/SKILL.md +5 -3
  32. 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 plum title band and section headings, steel-blue links, 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.
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.3.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. Start a subagent that has seen neither this conversation nor your trace, and give it only this task: "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." Wait for its answer before you fix anything or hand over. Fix each quote it returns, or keep it and say in the hand-over why the source states it. Without subagents, 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.
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
- Done when every quote is fixed or its reason is in the hand-over.
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.3.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
- - Emulate print media and confirm every step, panel, item and detail shows.
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 step-through 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 build the step-through around it as [the step-through reference](references/step-through.md) shows. The same current stage drives the list and the diagram: the current node and the connector into it take a thicker stroke.
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) and [the path trace](../SKILL.md#path-trace). Copy them unchanged; change only the ids, the steps and the labels.
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`. 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. A path trace or a diagram's step-through puts its diagram, drawn by `pageship-diagram`, before the list, and marks each node and connector with the step that brings it in, using the same numbers. The controls and the status line carry `hidden`, so a reader without script never sees them.
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
- <figure class="diagram stepper" id="path">
11
- <div class="scroll" tabindex="0" role="region" aria-label="Ticket path diagram">
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>Triage</strong>: a ticket arrives in the triage queue.</li>
20
- <li data-step="2">
21
- <strong>Tagging</strong>: the triage responder tags it billing, bug or other.
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 path" hidden>
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
- <figcaption>...</figcaption>
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 `#path-2`, and confirm it opens on that step.
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.3.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.3.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 console and fix every item it returns:
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 playing a change over time or moving a marker along a path, loaded by a pageship- kind skill before anything on the page moves.
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.3.0"
6
+ version: "0.4.0"
7
7
  ---
8
8
 
9
9
  # Motion
10
10
 
11
- Motion shows the reader something they would otherwise have to work out: what caused what, the order things happen in, or how a value changes over time. Everything else on the page holds still.
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
- Motion has exactly three jobs, and each starts when the reader acts:
17
+ Two kinds of figure play, each once, when it scrolls into view:
18
18
 
19
- - **Change.** A value or series changing over time, such as a backlog building week by week, played from a Play button.
20
- - **Order.** Stages or a path appearing in sequence as the reader steps or plays through them.
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 is still:
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
- - Nothing moves on page load or as the reader scrolls.
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
- 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. It adds no motion. When the user or a design skill asks for motion this section rules out, leave it out and say so in your hand-over.
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 whole picture first.** Before the reader presses anything, the page shows the complete result: every week of the series, every stage of the path, with its values.
38
- - **Ends complete.** Playback stops at the end and shows the full picture again. It never loops.
39
- - **Paused keeps context.** When the reader pauses or steps part way, the parts not yet reached stay visible but faint, such as the later weeks drawn in a pale line.
40
- - **The values in text.** Every value a frame shows is also in text on the page, such as a table under the chart or labels on the path. A position line, such as "Week 5 of 13: 2 Aug, 162 open tickets", names the current frame, with `aria-live="polite"`.
41
- - **Controls.** Playback has a Play/Pause button whose label says what it will do, and Previous and Next buttons that step one frame, by the [controls rules](../pageship-interaction/SKILL.md#controls) of `pageship-interaction`.
42
- - **Pace.** A frame holds long enough to read its value: about 500 to 800 ms. A full playback takes about 10 seconds or less. Transitions between states take 150 to 300 ms with an ease-out curve.
43
- - **One thing at a time.** Animate only the element that carries the change, using `transform`, `opacity` or SVG attributes.
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 tweens. Every control still works and changes the state at once: Next jumps to the next frame, and Play advances frame by frame with no movement between them.
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 animate, and set the end state directly when it is true.
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
- - Load the page and wait two seconds without touching it. Nothing moved, and the screenshot shows the whole picture.
55
- - Press Play and let it run to the end. It stops on the full picture. Play again and pause part way: the parts not yet reached are still visible.
56
- - Emulate `prefers-reduced-motion: reduce` and use every control. Each state appears at once.
57
- - Emulate print media. The full picture prints, with no controls.
58
- - Without a browser, read the CSS and script against steps 1 to 3.
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
+ }