pageship 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/README.md +18 -108
  2. package/dist/cli.mjs +1887 -479
  3. package/dist/skills/LICENSE +21 -0
  4. package/dist/skills/pageship/SKILL.md +150 -0
  5. package/dist/skills/pageship/references/download.md +9 -0
  6. package/dist/skills/pageship/references/list-and-history.md +15 -0
  7. package/dist/skills/pageship/references/restore.md +9 -0
  8. package/dist/skills/pageship/references/visibility.md +12 -0
  9. package/dist/skills/pageship-chart/SKILL.md +110 -0
  10. package/dist/skills/pageship-code-block/SKILL.md +113 -0
  11. package/dist/skills/pageship-code-block/references/highlight-lock.json +542 -0
  12. package/dist/skills/pageship-code-block/references/highlight.mjs +138 -0
  13. package/dist/skills/pageship-decision-record/SKILL.md +43 -0
  14. package/dist/skills/pageship-diagram/SKILL.md +92 -0
  15. package/dist/skills/pageship-diagram/references/check-diagram.js +301 -0
  16. package/dist/skills/pageship-diagram/references/computed-layout.md +49 -0
  17. package/dist/skills/pageship-diagram/references/layout.mjs +310 -0
  18. package/dist/skills/pageship-diagram/references/mermaid.md +89 -0
  19. package/dist/skills/pageship-diagram/references/svg.md +205 -0
  20. package/dist/skills/pageship-docs/NOTICES.md +35 -0
  21. package/dist/skills/pageship-docs/SKILL.md +67 -0
  22. package/dist/skills/pageship-docs/references/ask.md +18 -0
  23. package/dist/skills/pageship-docs/references/check.md +39 -0
  24. package/dist/skills/pageship-docs/references/content.md +29 -0
  25. package/dist/skills/pageship-docs/references/design.md +60 -0
  26. package/dist/skills/pageship-docs/references/file.md +52 -0
  27. package/dist/skills/pageship-docs/references/plain-default.md +331 -0
  28. package/dist/skills/pageship-explainer/SKILL.md +92 -0
  29. package/dist/skills/pageship-interaction/SKILL.md +115 -0
  30. package/dist/skills/pageship-interaction/references/pointer.md +271 -0
  31. package/dist/skills/pageship-interaction/references/step-through.md +146 -0
  32. package/dist/skills/pageship-key-figures/SKILL.md +127 -0
  33. package/dist/skills/pageship-media/SKILL.md +122 -0
  34. package/dist/skills/pageship-motion/SKILL.md +58 -0
  35. package/dist/skills/pageship-plan/SKILL.md +55 -0
  36. package/dist/skills/pageship-report/SKILL.md +29 -0
  37. package/dist/skills/pageship-review-report/SKILL.md +90 -0
  38. package/dist/skills/pageship-slide-deck/SKILL.md +53 -0
  39. package/dist/skills/pageship-slide-deck/references/engine.md +362 -0
  40. package/dist/skills/pageship-spec/SKILL.md +58 -0
  41. package/dist/skills/pageship-table/SKILL.md +243 -0
  42. package/dist/skills/pageship-table/references/sort-and-filter.md +235 -0
  43. package/package.json +1 -1
@@ -0,0 +1,92 @@
1
+ ---
2
+ name: pageship-explainer
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
+ license: MIT
5
+ metadata:
6
+ version: "0.2.0"
7
+ ---
8
+
9
+ # Explainer
10
+
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
+
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).
14
+
15
+ ## 1. Gather
16
+
17
+ When the source is code or configuration, what its lines do is the source. Read the files the request points to, then any project file they call that you need to explain them.
18
+
19
+ - Code you can't read is a gap: give its name and what the calling code shows about it, such as the arguments it receives and what happens after it returns, and nothing about what it does inside, whatever its name or file suggests.
20
+ - A setting no code you read uses is only declared: say what the file sets and that the code you read doesn't use it, never what it does.
21
+
22
+ ## 2. Trace the mechanism
23
+
24
+ Write down the reader's question in their words, and the answer in one or two sentences from the source. The answer opens the document. It holds for every case the source covers: name an exception in a clause rather than leave it out.
25
+
26
+ Trace only what the answer needs. Other behaviour in the same files, such as metrics, logging or a neighbouring feature, stays out.
27
+
28
+ Then write yourself a trace:
29
+
30
+ - **Parts.** Each thing that holds a state or acts, such as a queue, a function, a ticket status or a team. A part with no move or rule is only a name: find in the source what moves it on and where it goes, or make the missing rule an open question.
31
+ - **Moves.** What passes or changes between parts, and in what order.
32
+ - **Rules.** Each condition that decides which way a move goes or what value it gets, with its numbers and where they live, such as "statuses 408, 425, 429, 500, 502, 503 and 504 are retried (`retryableStatuses`, line 9)". Keep every condition the source checks, and which one wins when two overlap, such as an error that makes the status irrelevant.
33
+ - **Ends.** Every way the source says the process stops or settles. Say that something stops only where the source says so. A branch whose next step the source doesn't give ends at the last thing the source says, such as "the ticket is a breach", with an open question when the reader would ask what happens next.
34
+
35
+ A statement that depends on something the source doesn't show, such as whether a count includes the current attempt, says so where you make it and points to the open question.
36
+
37
+ A value the source computes keeps the source's form: the formula or rule, and where each input comes from. A random value stays a range. Add a worked example only when every input comes from the source, labelled as a worked example, with the arithmetic shown.
38
+
39
+ Done when every part has its moves and rules or an open question for each one the source leaves out, every branch reaches an end the source gives, and each entry names where in the source it comes from.
40
+
41
+ ## 3. Order the concepts
42
+
43
+ List every concept the reader needs: each glossary term, part, state and rule, and each name from the code or configuration the explainer will use. A word you coin, such as "allowlist" for a set the code names otherwise, is a concept too; prefer the source's name. Mark the ones the reader already knows from who the request says they are, such as "ticket" for new responders. Introduce every other concept where the text or a figure first names it, in one clause or sentence. Order the list so each concept comes after every concept its introduction uses.
44
+
45
+ The answer comes first, so it introduces each concept it uses, for example "Every ticket gets a target time for its first response (the first public reply a responder sends)". When that makes the answer long, use fewer concepts in it.
46
+
47
+ Plan the sections from that order:
48
+
49
+ 1. An overview of the whole mechanism, introducing its parts.
50
+ 2. One section per stage or rule, in the order the mechanism runs.
51
+ 3. Exceptions and limits the source gives that fit no stage.
52
+
53
+ Done when every concept's first use, text inside figures included, comes at or after its introduction.
54
+
55
+ ## 4. Plan the figures
56
+
57
+ Give each section one figure that makes one claim. Before planning the first diagram, load `pageship-diagram`.
58
+
59
+ - **The claim** is one sentence about the mechanism, in the source's words, that the figure's shapes, arrows, rows or marks show and none of them contradicts, such as "A delivery is dead-lettered as soon as an attempt fails with a status that isn't retryable." A topic, such as "Retry logic", is not a claim, and neither is a list of the figure's rules. The claim is the caption. When a caption would join two statements with "and" or a semicolon, keep the one the drawing shows and move the other to the text.
60
+ - **Only what the claim needs.** A second claim gets a second figure. A worked example's caption states what the example shows, not the rules around it.
61
+ - **Every arrow labelled** with what moves along it or the condition that sends things that way, from the source, such as "status 429", "fewer than 8 attempts" or "customer replies". Labels, box text and captions use the glossary's terms in full, such as "first response" rather than "response" or "reply". An arrow the source gives no label for is a gap: leave it out and add an open question. Where `pageship-diagram` would leave connectors unlabelled, such as a large Mermaid graph, split the figure until every connector carries a label.
62
+
63
+ The overview is always a figure, and its claim is the answer's main point. Any other section takes a list, sentence or table when [the form of each figure](../pageship-docs/references/design.md#the-form-of-each-figure) gives one.
64
+
65
+ A code block shows the line a rule lives in beside the figure, never instead of it: at most one excerpt per section. It keeps the label `pageship-code-block` gives it, not a claim.
66
+
67
+ Done when every figure has one claim and every arrow has a label from the source.
68
+
69
+ ## 5. Order of the file
70
+
71
+ Headings, text and figure labels use the glossary's terms.
72
+
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
+ 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.
76
+ 4. **Exceptions and limits**, when there are any.
77
+ 5. **Open questions.**
78
+ 6. **Sources.** Each file, with line numbers for code, and each document the explainer draws on, as the source names them.
79
+
80
+ Done when every part, move, rule and end of the trace is in a figure or the text beside it.
81
+
82
+ ## Check
83
+
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
+
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.
87
+
88
+ Done when every quote is fixed or its reason is in the hand-over.
89
+
90
+ ## Hand over
91
+
92
+ 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.
@@ -0,0 +1,115 @@
1
+ ---
2
+ name: pageship-interaction
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
+ license: MIT
5
+ metadata:
6
+ version: "0.2.0"
7
+ ---
8
+
9
+ # Interaction
10
+
11
+ A control lets the reader ask the document a question and see the answer: what comes after this stage, which items are mine, what sits behind this summary. The document reads in full without it.
12
+
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 a control, the file holds the control and the content it works on.
14
+
15
+ ## 1. Name the reader's question
16
+
17
+ For each control you plan, write down the question it answers in the reader's words, such as "Which follow-ups are mine?", and take its pattern from this table.
18
+
19
+ | The reader asks | Pattern |
20
+ | ------------------------------------------------- | --------------------------------- |
21
+ | What comes next, and in what order? | [Step-through](#step-through) |
22
+ | How does something get from one place to another? | [Path trace](#path-trace) |
23
+ | Which items match what I care about? | [Filter](#filter) |
24
+ | Show me one of several parallel versions | [Tabs](#tabs) |
25
+ | What is behind this line? | [Reveal detail](#reveal-detail) |
26
+ | What exactly is this mark, and what links here? | [Pointer states](#pointer-states) |
27
+
28
+ Every chart and every diagram with nodes takes [pointer states](#pointer-states). For the other patterns, add no control when:
29
+
30
+ - the reader compares the items, such as Q2 against Q3: show them side by side in a table, because tabs or a toggle would hide one side;
31
+ - the content fits on one screen and reads well in order;
32
+ - a list is short enough to scan, about ten items or fewer;
33
+ - the answer is one value;
34
+ - the control would only decorate: carousels, a second view of the same data the reader didn't ask for.
35
+
36
+ When the user asks for a control this list rules out, add it, keep everything visible as step 2 says, and say in your hand-over what the control hides.
37
+
38
+ ## 2. Build each control on complete content
39
+
40
+ Write the full content in the HTML first, then add the controls with script on top of it. Every pattern keeps these three states complete:
41
+
42
+ - **Before the reader touches anything**, every item shows, with every number, name, date, owner and decision the source gives for it. A control may hold back only the sentences that explain them.
43
+ - **In print**, every step, panel, item and detail shows, in reading order, and the controls are hidden. Hide anything with classes that apply only under `@media screen`, so the print stylesheet never has to undo them. When a name doubles as a button, such as a stage name in the overview, print it as plain text instead of hiding it.
44
+ - **Without script**, the page reads as a plain document with everything in it. Script creates or reveals the controls themselves, so a reader without script never sees a button that does nothing.
45
+
46
+ Then build each control by its pattern below and the rules under [Controls](#controls).
47
+
48
+ ## Check
49
+
50
+ - 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
+ - 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
+ - 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.
54
+ - 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
+
56
+ ## Patterns
57
+
58
+ ### Step-through
59
+
60
+ For a sequence of three to eight stages the reader follows one at a time.
61
+
62
+ - 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
+ - The current stage carries `aria-current="step"` and a mark that is not colour alone, such as weight and a border.
64
+ - Previous and Next buttons move one stage, and Next after the last stage shows every stage again. A "Show all steps" button returns to every stage at once. A status line beside them, such as "Step 2 of 5: Triage", has `aria-live="polite"`.
65
+ - The URL fragment records the current stage, and the page opens on the stage it names.
66
+ - Left and Right arrow keys step while focus is on the step-through's buttons. Nothing advances by itself; for a timed sequence, see `pageship-motion`.
67
+ - Copy [the step-through markup, style and script](references/step-through.md) unchanged.
68
+
69
+ ### Path trace
70
+
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.
72
+
73
+ ### Filter
74
+
75
+ For a list or table the reader narrows to what concerns them.
76
+
77
+ - Every item shows on load, with a line such as "Showing all 14 follow-ups".
78
+ - Use one toggle button per value, with `aria-pressed`, and an "All" button that resets them. Group the buttons by field, such as owner and status, each group with a visible label.
79
+ - After each change, update the line, such as "Showing 4 of 14: Priya", with `aria-live="polite"`. When nothing matches, say so in place of the empty list.
80
+ - Totals and summary figures stay computed on the full set. A figure that follows the filter says so in its label.
81
+
82
+ ### Tabs
83
+
84
+ For parallel versions of the same content that the reader reads one at a time, such as the same steps for two platforms.
85
+
86
+ - Follow the ARIA tabs pattern: a `role="tablist"` of `button` elements with `role="tab"` and `aria-selected`, each controlling a `role="tabpanel"`. Left and Right arrows move between tabs, and only the selected tab is in the Tab order.
87
+ - Each panel starts with its own heading, so print and the no-script page read as plain sections.
88
+
89
+ ### Reveal detail
90
+
91
+ For supporting detail behind a summary line.
92
+
93
+ - Use `details` and `summary`.
94
+ - The summary line holds the essential value, such as "Finance review: 1 day 4 h median wait". The detail holds the supporting text only.
95
+
96
+ ### Pointer states
97
+
98
+ For chart marks, diagram nodes and connectors, so the reader can point at one for its detail.
99
+
100
+ - Hover, keyboard focus and a tap show one state: the mark, node or connector stands out by more than colour, such as a thicker outline, the other series or the unconnected parts of a diagram dim, and a tooltip shows.
101
+ - Each chart and diagram is one tab stop. From it, the arrow keys move focus between its marks or nodes, and focus shows the same state as hover.
102
+ - Connectors take hover and tap only. Keyboard users reach the same detail from the tooltips of the nodes at either end.
103
+ - A tooltip may add detail the figure leaves out, such as an exact value or a node's connections, when the same detail is on the page or in the figure's table.
104
+ - Under reduced motion, states switch without transitions.
105
+ - For charts and diagrams, copy [the pointer style and script](references/pointer.md) unchanged.
106
+
107
+ ## Controls
108
+
109
+ - **Real elements.** Use `button type="button"` for actions, `a href="#..."` for jumps, `summary` for revealed detail, and an `input` with a label, outside any `form`, for typed text such as a search. Click handlers go on buttons and links only, never on a `div`, `span`, list item or SVG shape; the pointer script's tap is the one exception.
110
+ - **Keyboard.** Use no positive `tabindex`. Arrow keys work only inside the widget that owns them.
111
+ - **Focus.** Every control keeps its `:focus-visible` outline.
112
+ - **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
+ - **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.
115
+ - **Script.** Put the script at the end of `body`.
@@ -0,0 +1,271 @@
1
+ # Pointer states for charts and diagrams
2
+
3
+ The style and script for [pointer states](../SKILL.md#pointer-states). Copy both unchanged.
4
+
5
+ ## Markup
6
+
7
+ The script finds its targets from the markup the chart and diagram rules already give:
8
+
9
+ - **Chart.** Each data mark inside a series' `<g data-series="…">`, a bar or a point's marker, holds a `<title>` with its tooltip text. The script makes the mark reachable by the arrow keys, names it with the title, shows the title as the tooltip and dims the other series; a chart of one series dims nothing.
10
+ - **Diagram, hand-drawn or computed.** Each `<g class="node" id="…">`, each `<path class="edge">` and each `<text class="edge-label">` with `data-from` and `data-to`. A node's `<title>`, such as a timeline event's, becomes its name. The script makes each labelled node reachable by the arrow keys and builds its tooltip from its label and connections, such as "Help desk / → Survey tool (closed tickets) / ← Web chat". Its connectors, their labels and the nodes at their other ends stay bright. A connector takes hover and tap through a wider invisible line laid over it, and its tooltip names both ends and its label.
11
+ - **Mermaid.** The same as a diagram for a `flowchart`. Other Mermaid types get the node's label as the tooltip and no connection highlight, and a `sequenceDiagram`, which draws no nodes, gets no pointer states.
12
+
13
+ ## Style
14
+
15
+ Add this to the document's `<style>`.
16
+
17
+ ```css
18
+ .figure-tip {
19
+ position: absolute;
20
+ z-index: 10;
21
+ max-width: min(20rem, calc(100vw - 16px));
22
+ padding: 0.375rem 0.625rem;
23
+ border-radius: 6px;
24
+ font:
25
+ 0.875rem/1.4 system-ui,
26
+ sans-serif;
27
+ white-space: pre-line;
28
+ color: var(--bg);
29
+ background: var(--fg);
30
+ pointer-events: none;
31
+ }
32
+ .diagram svg .hit {
33
+ fill: transparent;
34
+ stroke: none;
35
+ }
36
+ .diagram svg .edge-hit {
37
+ fill: none;
38
+ stroke: transparent;
39
+ stroke-width: 12px;
40
+ pointer-events: stroke;
41
+ }
42
+ @media screen {
43
+ .chart svg [data-tip].is-active,
44
+ .diagram svg .node.is-active > :is(rect, polygon, circle, path):not(.hit),
45
+ .diagram svg .is-linked:is(.edge, .flowchart-link) {
46
+ stroke: var(--fg) !important;
47
+ stroke-width: 3px !important;
48
+ }
49
+ .chart svg.has-active [data-series]:not(:has(.is-active)),
50
+ .diagram
51
+ svg.has-active
52
+ :is(.node, .edge, .edge-label, .flowchart-link, g.edgeLabel):not(.is-active, .is-linked) {
53
+ opacity: 0.25;
54
+ }
55
+ }
56
+ @media screen and (prefers-reduced-motion: no-preference) {
57
+ :is(.chart, .diagram)
58
+ svg
59
+ :is([data-series], .node, .edge, .edge-label, .flowchart-link, g.edgeLabel) {
60
+ transition: opacity 0.15s;
61
+ }
62
+ }
63
+ ```
64
+
65
+ ## Script
66
+
67
+ Copy this script once, at the end of the `<body>`.
68
+
69
+ ```html
70
+ <script>
71
+ // Hover, keyboard focus and a tap on a chart mark or a diagram node show the same state.
72
+ // A connector takes hover and tap only; keyboard users reach its detail from its nodes.
73
+ // Each figure is one tab stop, and the arrow keys move focus between its marks or nodes.
74
+ const pointer = { links: new Map(), active: null, tip: document.createElement("div") };
75
+ pointer.tip.className = "figure-tip";
76
+ pointer.tip.setAttribute("role", "tooltip");
77
+ pointer.tip.hidden = true;
78
+ document.body.append(pointer.tip);
79
+
80
+ function preparePointer() {
81
+ // Joins every text node with a space, so the lines of a label in <tspan>s stay apart.
82
+ const words = (element) => {
83
+ const parts = [];
84
+ const walker = element ? document.createTreeWalker(element, NodeFilter.SHOW_TEXT) : null;
85
+ while (walker?.nextNode()) parts.push(walker.currentNode.textContent);
86
+ return parts.join(" ").replace(/\s+/g, " ").trim();
87
+ };
88
+ // The figure's tab stop is its scroll box, or its SVG when it has none. The script
89
+ // focuses each mark or node itself, so none is a tab stop of its own.
90
+ const enlist = (target) => {
91
+ target.tabIndex = -1;
92
+ const svg = target.closest("svg");
93
+ const stop = svg.closest(".scroll") ?? svg;
94
+ stop.tabIndex = 0;
95
+ stop.dataset.marks = "";
96
+ if (!stop.hasAttribute("role")) stop.setAttribute("role", "group");
97
+ if (!stop.hasAttribute("aria-label") && !stop.hasAttribute("aria-labelledby")) {
98
+ stop.setAttribute("aria-label", words(svg.querySelector(":scope > title")) || "Figure");
99
+ }
100
+ };
101
+ // A chart mark's tooltip is its <title>, which the browser shows without script too.
102
+ for (const title of document.querySelectorAll(".chart [data-series] title")) {
103
+ const mark = title.parentElement;
104
+ if (mark.matches("g, .target, .forecast")) continue;
105
+ mark.dataset.tip = words(title);
106
+ mark.setAttribute("aria-label", mark.dataset.tip);
107
+ enlist(mark);
108
+ title.remove();
109
+ }
110
+ for (const svg of document.querySelectorAll(".diagram svg")) {
111
+ // A node's <title>, such as a timeline event's, becomes its name and its one tooltip.
112
+ for (const title of svg.querySelectorAll(".node > title")) {
113
+ title.parentElement.dataset.label = words(title);
114
+ title.parentElement.setAttribute("aria-label", words(title));
115
+ title.remove();
116
+ }
117
+ const label = (node) => node.dataset.label || words(node);
118
+ const nodes = [...svg.querySelectorAll(".node")].filter((node) => label(node));
119
+ // Mermaid names a flowchart node "<svg id>-flowchart-<id>-<n>" and a link "L_<from>_<to>_<n>".
120
+ const prefix = `${svg.id}-flowchart-`;
121
+ const named = new Map(
122
+ nodes
123
+ .filter((node) => svg.id && node.id.startsWith(prefix))
124
+ .map((node) => [node.id.slice(prefix.length).replace(/-\d+$/, ""), node]),
125
+ );
126
+ const ends = (line) => {
127
+ if (line.dataset.from) {
128
+ return [line.dataset.from, line.dataset.to].map((id) =>
129
+ nodes.find((node) => node.id === id),
130
+ );
131
+ }
132
+ for (const [name, node] of named) {
133
+ if (!line.dataset.id?.startsWith(`L_${name}_`)) continue;
134
+ const other = named.get(line.dataset.id.slice(name.length + 3).replace(/_\d+$/, ""));
135
+ if (other) return [node, other];
136
+ }
137
+ return [];
138
+ };
139
+ const links = [...svg.querySelectorAll(".edge, .flowchart-link")].map((line) => {
140
+ const [from, to] = ends(line);
141
+ const text = line.dataset.from
142
+ ? svg.querySelector(
143
+ `.edge-label[data-from="${line.dataset.from}"][data-to="${line.dataset.to}"]`,
144
+ )
145
+ : svg.querySelector(`.edgeLabel [data-id="${line.dataset.id}"]`)?.closest(".edgeLabel");
146
+ return { line, from, to, text, both: line.hasAttribute("marker-start") };
147
+ });
148
+ for (const old of svg.querySelectorAll(".edge-hit")) old.remove();
149
+ for (const { line, from, to, text, both } of links) {
150
+ if (!from || !to) continue;
151
+ const hit = document.createElementNS("http://www.w3.org/2000/svg", "path");
152
+ hit.setAttribute("class", "edge-hit");
153
+ hit.setAttribute("d", line.getAttribute("d"));
154
+ hit.dataset.tip = `${label(from)} ${both ? "↔" : "→"} ${label(to)}${words(text) ? ` (${words(text)})` : ""}`;
155
+ line.after(hit);
156
+ pointer.links.set(hit, [line, from, to, ...(text ? [text] : [])]);
157
+ }
158
+ for (const node of nodes) {
159
+ const lines = [label(node)];
160
+ const parts = [];
161
+ for (const { line, from, to, text, both } of links) {
162
+ if (!from || !to || (from !== node && to !== node)) continue;
163
+ const other = from === node ? to : from;
164
+ const arrow = both ? "↔" : from === node ? "→" : "←";
165
+ lines.push(`${arrow} ${label(other)}${words(text) ? ` (${words(text)})` : ""}`);
166
+ parts.push(line, other, ...(text ? [text] : []));
167
+ }
168
+ node.dataset.tip = lines.join("\n");
169
+ enlist(node);
170
+ pointer.links.set(node, parts);
171
+ }
172
+ }
173
+ }
174
+
175
+ const showPointer = (target) => {
176
+ if (target === pointer.active) return;
177
+ hidePointer();
178
+ pointer.active = target;
179
+ target.closest("svg").classList.add("has-active");
180
+ target.classList.add("is-active");
181
+ for (const part of pointer.links.get(target) ?? []) part.classList.add("is-linked");
182
+ const { tip } = pointer;
183
+ tip.textContent = target.dataset.tip;
184
+ tip.hidden = false;
185
+ // A chart mark's tooltip clears the mark's own printed value: a text of its series within
186
+ // 24px of the mark that the tooltip repeats, other than the series' name.
187
+ const mark = target.getBoundingClientRect();
188
+ const gap = (a) =>
189
+ Math.hypot(
190
+ Math.max(0, a.left - mark.right, mark.left - a.right),
191
+ Math.max(0, a.top - mark.bottom, mark.top - a.bottom),
192
+ );
193
+ const series = target.closest(".chart [data-series]");
194
+ const value =
195
+ [...(series?.querySelectorAll("text") ?? [])]
196
+ .map((text) => [text.textContent.trim(), text.getBoundingClientRect()])
197
+ .filter(([words, box]) => words && words !== series.dataset.series.trim() && gap(box) <= 24)
198
+ .filter(([words]) => tip.textContent.includes(words))
199
+ .map(([, box]) => box)
200
+ .sort((a, b) => gap(a) - gap(b))[0] ?? mark;
201
+ const box = {
202
+ left: Math.min(mark.left, value.left),
203
+ top: Math.min(mark.top, value.top),
204
+ right: Math.max(mark.right, value.right),
205
+ bottom: Math.max(mark.bottom, value.bottom),
206
+ };
207
+ const width = document.documentElement.clientWidth;
208
+ const left = Math.min(
209
+ Math.max((box.left + box.right) / 2 - tip.offsetWidth / 2, 8),
210
+ width - tip.offsetWidth - 8,
211
+ );
212
+ const above = box.top - tip.offsetHeight - 8;
213
+ tip.style.left = `${left + scrollX}px`;
214
+ tip.style.top = `${(above >= 0 ? above : box.bottom + 8) + scrollY}px`;
215
+ };
216
+ const hidePointer = () => {
217
+ const target = pointer.active;
218
+ if (!target) return;
219
+ target.closest("svg").classList.remove("has-active");
220
+ target.classList.remove("is-active");
221
+ for (const part of pointer.links.get(target) ?? []) part.classList.remove("is-linked");
222
+ pointer.tip.hidden = true;
223
+ pointer.active = null;
224
+ };
225
+ const targetOf = (event) => event.target.closest?.("[data-tip]");
226
+ document.addEventListener("pointerover", (event) => {
227
+ if (event.pointerType !== "touch" && targetOf(event)) showPointer(targetOf(event));
228
+ });
229
+ document.addEventListener("pointerout", (event) => {
230
+ const target = targetOf(event);
231
+ if (event.pointerType === "touch") return;
232
+ if (target && target === pointer.active && !target.contains(event.relatedTarget)) {
233
+ if (document.activeElement !== target) hidePointer();
234
+ }
235
+ });
236
+ document.addEventListener("focusin", (event) =>
237
+ targetOf(event) ? showPointer(targetOf(event)) : hidePointer(),
238
+ );
239
+ document.addEventListener("focusout", (event) => {
240
+ const target = targetOf(event);
241
+ if (target && target === pointer.active && !target.matches(":hover")) hidePointer();
242
+ });
243
+ // A tap shows what hover shows until the reader taps elsewhere; a mark or node takes focus.
244
+ document.addEventListener("click", (event) => {
245
+ const target = targetOf(event);
246
+ if (target) {
247
+ if (target.hasAttribute("tabindex")) target.focus();
248
+ showPointer(target);
249
+ } else if (pointer.active && !pointer.active.matches(":focus")) hidePointer();
250
+ });
251
+ // From the figure's tab stop, an arrow key or Home focuses its first mark or node, and End
252
+ // its last; from a mark, the arrow keys move to the next or previous one in drawing order.
253
+ const moves = { ArrowRight: 1, ArrowDown: 1, ArrowLeft: -1, ArrowUp: -1, Home: -1e9, End: 1e9 };
254
+ document.addEventListener("keydown", (event) => {
255
+ if (event.key === "Escape") hidePointer();
256
+ const stop = event.target.closest?.("[data-marks]");
257
+ const move = moves[event.key];
258
+ if (!stop || !move) return;
259
+ const targets = [...stop.querySelectorAll("[data-tip][tabindex]")];
260
+ const at = targets.indexOf(event.target);
261
+ const next = at === -1 && move < 1e9 ? 0 : Math.min(Math.max(at + move, 0), targets.length - 1);
262
+ event.preventDefault();
263
+ targets[next]?.focus();
264
+ });
265
+ preparePointer();
266
+ </script>
267
+ ```
268
+
269
+ ## Check
270
+
271
+ Hover the first mark or node of each figure: it stands out and its tooltip shows. Tab to the figure, which is one tab stop, press the Right arrow key and confirm the same state on the first mark or node, then emulate reduced motion and confirm the state switches without a fade.
@@ -0,0 +1,146 @@
1
+ # Step-through
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.
4
+
5
+ ## Markup
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.
8
+
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>
18
+ <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>
23
+ </ol>
24
+ <div class="step-controls" role="toolbar" aria-label="Step through the path" hidden>
25
+ <button type="button" data-go="previous">Previous step</button>
26
+ <button type="button" data-go="next">Next step</button>
27
+ <button type="button" data-go="all">Show all steps</button>
28
+ </div>
29
+ <p class="step-status" role="status" aria-live="polite" hidden></p>
30
+ <figcaption>...</figcaption>
31
+ </figure>
32
+ ```
33
+
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
+ ## Style
37
+
38
+ ```css
39
+ .stepper .is-current rect,
40
+ .stepper .edge.is-current {
41
+ stroke: var(--accent);
42
+ stroke-width: 3;
43
+ }
44
+ .stepper li.is-current {
45
+ font-weight: 600;
46
+ border-left: 3px solid var(--accent);
47
+ padding-left: 0.5rem;
48
+ }
49
+ .step-controls {
50
+ display: flex;
51
+ flex-wrap: wrap;
52
+ gap: 0.5rem;
53
+ margin: 0.75rem 0;
54
+ }
55
+ .step-controls[hidden],
56
+ .step-status[hidden] {
57
+ display: none;
58
+ }
59
+ .step-controls button {
60
+ font: inherit;
61
+ min-height: 44px;
62
+ padding: 0.25rem 0.875rem;
63
+ color: var(--fg);
64
+ background: var(--surface);
65
+ border: 1px solid var(--muted);
66
+ border-radius: 6px;
67
+ }
68
+ @media (prefers-reduced-motion: no-preference) {
69
+ .stepper [data-step] {
70
+ transition: opacity 0.2s;
71
+ }
72
+ .stepper .is-later {
73
+ opacity: 0.25;
74
+ }
75
+ }
76
+ @media print {
77
+ .stepper .is-later {
78
+ opacity: 1;
79
+ }
80
+ .step-controls,
81
+ .step-status {
82
+ display: none;
83
+ }
84
+ }
85
+ ```
86
+
87
+ ## Script
88
+
89
+ Copy this script once, at the end of the `<body>`. It drives every `.stepper` on the page.
90
+
91
+ ```html
92
+ <script>
93
+ for (const stepper of document.querySelectorAll(".stepper")) {
94
+ const parts = stepper.querySelectorAll("[data-step]");
95
+ const steps = stepper.querySelectorAll("ol.steps > li");
96
+ const status = stepper.querySelector(".step-status");
97
+ const controls = stepper.querySelector(".step-controls");
98
+ let current = 0; // 0 shows every step.
99
+ const show = (step, record = true) => {
100
+ current = step;
101
+ for (const part of parts) {
102
+ const n = Number(part.dataset.step);
103
+ part.classList.toggle("is-current", current > 0 && n === current);
104
+ part.classList.toggle("is-later", current > 0 && n > current);
105
+ if (part.tagName === "LI") {
106
+ if (current > 0 && n === current) part.setAttribute("aria-current", "step");
107
+ else part.removeAttribute("aria-current");
108
+ }
109
+ }
110
+ const name = (li) => (li.querySelector("strong") ?? li).textContent.trim();
111
+ status.textContent =
112
+ current === 0
113
+ ? `Showing all ${steps.length} steps.`
114
+ : `Step ${current} of ${steps.length}: ${name(steps[current - 1])}`;
115
+ if (record) {
116
+ const url = current ? `#${stepper.id}-${current}` : location.href.split("#")[0];
117
+ history.replaceState(null, "", url);
118
+ }
119
+ };
120
+ // Next runs from every step shown through each step and back; Previous runs the other way.
121
+ const go = {
122
+ next: () => (current + 1) % (steps.length + 1),
123
+ previous: () => (current + steps.length) % (steps.length + 1),
124
+ all: () => 0,
125
+ };
126
+ for (const button of controls.querySelectorAll("[data-go]")) {
127
+ button.addEventListener("click", () => show(go[button.dataset.go]()));
128
+ }
129
+ controls.addEventListener("keydown", (event) => {
130
+ const move = { ArrowRight: "next", ArrowLeft: "previous" }[event.key];
131
+ if (!move) return;
132
+ event.preventDefault();
133
+ show(go[move]());
134
+ });
135
+ controls.hidden = false;
136
+ status.hidden = false;
137
+ const opened = Number(location.hash.slice(stepper.id.length + 2));
138
+ const named = location.hash.startsWith(`#${stepper.id}-`) && opened >= 1;
139
+ show(named && opened <= steps.length ? opened : 0, false);
140
+ }
141
+ </script>
142
+ ```
143
+
144
+ ## Check
145
+
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.