pageship 0.1.0 → 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.
- package/README.md +18 -107
- package/dist/cli.mjs +1886 -505
- package/dist/skills/LICENSE +21 -0
- package/dist/skills/pageship/SKILL.md +150 -0
- package/dist/skills/pageship/references/download.md +9 -0
- package/dist/skills/pageship/references/list-and-history.md +15 -0
- package/dist/skills/pageship/references/restore.md +9 -0
- package/dist/skills/pageship/references/visibility.md +12 -0
- package/dist/skills/pageship-chart/SKILL.md +110 -0
- package/dist/skills/pageship-code-block/SKILL.md +113 -0
- package/dist/skills/pageship-code-block/references/highlight-lock.json +542 -0
- package/dist/skills/pageship-code-block/references/highlight.mjs +138 -0
- package/dist/skills/pageship-decision-record/SKILL.md +43 -0
- package/dist/skills/pageship-diagram/SKILL.md +92 -0
- package/dist/skills/pageship-diagram/references/check-diagram.js +301 -0
- package/dist/skills/pageship-diagram/references/computed-layout.md +49 -0
- package/dist/skills/pageship-diagram/references/layout.mjs +310 -0
- package/dist/skills/pageship-diagram/references/mermaid.md +89 -0
- package/dist/skills/pageship-diagram/references/svg.md +205 -0
- package/dist/skills/pageship-docs/NOTICES.md +35 -0
- package/dist/skills/pageship-docs/SKILL.md +67 -0
- package/dist/skills/pageship-docs/references/ask.md +18 -0
- package/dist/skills/pageship-docs/references/check.md +39 -0
- package/dist/skills/pageship-docs/references/content.md +29 -0
- package/dist/skills/pageship-docs/references/design.md +60 -0
- package/dist/skills/pageship-docs/references/file.md +52 -0
- package/dist/skills/pageship-docs/references/plain-default.md +331 -0
- package/dist/skills/pageship-explainer/SKILL.md +92 -0
- package/dist/skills/pageship-interaction/SKILL.md +115 -0
- package/dist/skills/pageship-interaction/references/pointer.md +271 -0
- package/dist/skills/pageship-interaction/references/step-through.md +146 -0
- package/dist/skills/pageship-key-figures/SKILL.md +127 -0
- package/dist/skills/pageship-media/SKILL.md +122 -0
- package/dist/skills/pageship-motion/SKILL.md +58 -0
- package/dist/skills/pageship-plan/SKILL.md +55 -0
- package/dist/skills/pageship-report/SKILL.md +29 -0
- package/dist/skills/pageship-review-report/SKILL.md +90 -0
- package/dist/skills/pageship-slide-deck/SKILL.md +53 -0
- package/dist/skills/pageship-slide-deck/references/engine.md +362 -0
- package/dist/skills/pageship-spec/SKILL.md +58 -0
- package/dist/skills/pageship-table/SKILL.md +243 -0
- package/dist/skills/pageship-table/references/sort-and-filter.md +235 -0
- package/package.json +1 -1
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: pageship-decision-record
|
|
3
|
+
description: HTML decision records, as the second step of pageship-docs. For any request to record, write, regenerate, edit or move a decision, 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
|
+
# Decision record
|
|
10
|
+
|
|
11
|
+
A decision record tells the reader what was decided, what else was considered and why it lost, and what follows. A reader who stops after the first lines knows the decision and its status. A reader who goes on compares the options on the same points.
|
|
12
|
+
|
|
13
|
+
Write it by [step 4 of pageship-docs](../pageship-docs/SKILL.md#4-write-the-file). Unless `pageship-docs` loaded this skill, start as [it says](../pageship-docs/SKILL.md#starting-at-a-kind-or-pattern-skill).
|
|
14
|
+
|
|
15
|
+
## Parts
|
|
16
|
+
|
|
17
|
+
- **Decision.** What was chosen, in the source's words, with its status exactly as the source gives it, such as decided, agreed, proposed, or awaiting sign-off. Add the date, scope, duration, owner and approver when the source gives them. When the source chooses nothing yet, the decision says so.
|
|
18
|
+
- **Context.** The problem that called for a decision, and the facts behind it.
|
|
19
|
+
- **Options.** Every option the source considers, the chosen one included, under the source's name. For each: what it is, who raised it when the source says, what the source says for and against it, and its outcome in the source's words, such as chosen, dropped or kept in part. A rejected option carries the reason the source gives, in its words. A drawback the source lists is a reason only when the source ties it to the outcome, as in "dropped because ..." or "rejected as too costly". When the source drops or changes an option without a reason, say that it gives none, and add an open question asking why.
|
|
20
|
+
- **Consequences.** What the source says follows from the decision: what changes, who must act, costs, risks and expected results. Each keeps the standing the source gives it, such as expected, a target, agreed or still to confirm. One the source says nobody has checked yet is still to confirm. One the source gives no standing is written as stated, with no label.
|
|
21
|
+
- **Assumptions**, when the source states any.
|
|
22
|
+
- **Open questions.**
|
|
23
|
+
|
|
24
|
+
## The comparison
|
|
25
|
+
|
|
26
|
+
Build one comparison of the options:
|
|
27
|
+
|
|
28
|
+
- The points are the criteria the source weighs at least one option on, such as cost, effort, effect, readiness or risk, plus the outcome and the reason, named in the source's words.
|
|
29
|
+
- Every option gets a value on every point: what the source says about that option on that point, in its units and wording. Where the source says nothing, write "Not stated", never a value you would have to infer, such as "low" for an option whose cost the source never gives.
|
|
30
|
+
- Keep the source's order of options, and mark the chosen one in words.
|
|
31
|
+
|
|
32
|
+
## Order
|
|
33
|
+
|
|
34
|
+
1. **Title and decision.** An `h1` naming what was decided, then at once the decision in one or two sentences in the source's words, and a line with its status and the date, owner and scope the source gives.
|
|
35
|
+
2. **Context.**
|
|
36
|
+
3. **Options considered.** The comparison as a table. Anything longer than a short phrase goes under the table, in a list by option.
|
|
37
|
+
4. **Consequences.** Each with its standing, and who checks and when if the source says.
|
|
38
|
+
5. **Open questions.**
|
|
39
|
+
6. **Sources.** As the source names them.
|
|
40
|
+
|
|
41
|
+
## Hand over
|
|
42
|
+
|
|
43
|
+
The structure you introduced includes the comparison points you chose.
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: pageship-diagram
|
|
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 diagrams in a Pageship HTML document, such as a process, a timeline, a system map or states, loaded by a pageship- kind skill before it draws a diagram.
|
|
4
|
+
license: MIT
|
|
5
|
+
metadata:
|
|
6
|
+
version: "0.2.0"
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Diagram
|
|
10
|
+
|
|
11
|
+
A diagram shows the reader something a paragraph can't show at once: a path with branches, how far apart events were, what connects to what. Each diagram makes one claim, stated in its caption, and draws only what the source says.
|
|
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).
|
|
14
|
+
|
|
15
|
+
## 1. Pick the diagram
|
|
16
|
+
|
|
17
|
+
Write down the reader's question the figure answers, in a few words, and take the row closest to it.
|
|
18
|
+
|
|
19
|
+
| The reader asks | Diagram | Drawn with |
|
|
20
|
+
| ----------------------------------------------- | --------------------------------------- | ---------------------------------- |
|
|
21
|
+
| What happens next, and where does it branch? | Flowchart, top to bottom | SVG, with a [step-through][steps] |
|
|
22
|
+
| Who hands what to whom, in what order? | Swimlane or sequence | SVG, with a [step-through][steps] |
|
|
23
|
+
| When did each thing happen, and how far apart? | [Timeline](references/svg.md#timelines) | SVG, always |
|
|
24
|
+
| What states can it be in, and what moves it on? | State diagram | SVG, [computed][layout] when large |
|
|
25
|
+
| What connects to what? | System map or dependency graph | SVG, [computed][layout] when large |
|
|
26
|
+
| What is part of what? | Tree or nested boxes | SVG |
|
|
27
|
+
|
|
28
|
+
A process or a change over time of three to eight steps gets the [step-through][steps], unless its layout is computed.
|
|
29
|
+
|
|
30
|
+
## 2. Fit the limits
|
|
31
|
+
|
|
32
|
+
Count what the figure will hold against these limits:
|
|
33
|
+
|
|
34
|
+
| Drawn with | Nodes | Connectors | Accent elements | Steps |
|
|
35
|
+
| ----------------------- | ----- | ---------- | --------------- | ----- |
|
|
36
|
+
| SVG, laid out by hand | 9 | 12 | 2 | 8 |
|
|
37
|
+
| SVG, [computed][layout] | 30 | 40 | 2 | none |
|
|
38
|
+
| [Mermaid][mermaid] | 30 | 40 | 2 | none |
|
|
39
|
+
|
|
40
|
+
A node is a box or other shape that stands for one thing. A timeline counts its events against a limit of 12.
|
|
41
|
+
|
|
42
|
+
When the count is over, cut in this order:
|
|
43
|
+
|
|
44
|
+
1. Merge nodes that always go together, and drop a connector the layout already shows. Each node stays one idea from the source, named in the source's words.
|
|
45
|
+
2. Split the figure: an overview with one node per group, then one figure per group.
|
|
46
|
+
3. When the reader needs the whole graph in one picture, such as a map of every system or every state a ticket can be in, and it fits the computed limits, [compute the layout][layout]. Use [Mermaid][mermaid] for it only when you can't run Node or the layout script can't get ELK. Over these limits, split by area.
|
|
47
|
+
|
|
48
|
+
Content you merge or move stays in the document, in a figure or the text beside it. Leave nothing out to make the figure fit.
|
|
49
|
+
|
|
50
|
+
## 3. Draw
|
|
51
|
+
|
|
52
|
+
- With SVG, follow [the SVG rules](references/svg.md).
|
|
53
|
+
- With a computed layout, follow [the computed layout steps][layout].
|
|
54
|
+
- With Mermaid, follow [the Mermaid rules][mermaid].
|
|
55
|
+
- For a process or change over time laid out by hand, add [the step-through][steps].
|
|
56
|
+
|
|
57
|
+
Every figure, however drawn:
|
|
58
|
+
|
|
59
|
+
- Sits in a `<figure>` with a `<figcaption>` stating its one claim in the source's words, such as "Every ticket passes through triage before it reaches a queue". The caption and the description say only what the source states: a rule you read off the graph's shape, such as "only" or "never", stays out unless the source says it.
|
|
60
|
+
- Has a `<title>` naming the subject and a `<desc>` saying in one or two sentences what the figure shows.
|
|
61
|
+
- Labels every node, and every connector the [Mermaid rules][mermaid] or [computed layout steps][layout] don't leave unlabelled, in the source's and the glossary's terms. A node that several paths reach says only what the source gives for every one of them.
|
|
62
|
+
- Takes its colours as [Colours](#colours) says.
|
|
63
|
+
- Takes [the pointer states](../pageship-interaction/references/pointer.md), so hovering, focusing or tapping a node or a timeline's event shows its detail.
|
|
64
|
+
|
|
65
|
+
## Colours
|
|
66
|
+
|
|
67
|
+
Colour a diagram from [the look's colour variables](../pageship-docs/references/design.md#the-looks-colour-variables) and no other colour. [The SVG stylesheet](references/svg.md#markup) sets these for hand-drawn and computed figures, and [the Mermaid script](references/mermaid.md#markup) for Mermaid:
|
|
68
|
+
|
|
69
|
+
| Part | Variable |
|
|
70
|
+
| ------------------------------------------------------------------------ | ------------------------------------------ |
|
|
71
|
+
| Node boxes | `--surface` fill, `--fg` border |
|
|
72
|
+
| Text | `--fg` |
|
|
73
|
+
| Connectors, arrowheads and connector labels | `--muted` |
|
|
74
|
+
| Accent elements: the one or two nodes or connectors the caption is about | `--accent`, with a thicker line |
|
|
75
|
+
| Node kinds | `--series-1` to `--series-4` on the border |
|
|
76
|
+
|
|
77
|
+
When the source sorts the nodes into two to four named groups, such as the team or queue that handles each step, or kinds of system, give each group a kind. Number the kinds in the order they first appear: kind 1 takes `series-1` and a solid border, kind 2 `series-2` and dashed, kind 3 `series-3` and dotted, kind 4 `series-4` and dash-dot. A legend under the figure names each kind and its border, so colour never carries the kind alone. An accent node keeps its kind's border style.
|
|
78
|
+
|
|
79
|
+
## Check
|
|
80
|
+
|
|
81
|
+
- In a browser, run [check-diagram.js](references/check-diagram.js) in the page at 1280px and at 390px wide, for example `agent-browser eval "$(cat <this skill's folder>/references/check-diagram.js)"`. It returns a list of problems with every SVG you drew or computed. Fix each one and run it again until the list is empty. A line starting with "note" is a crossing to remove if you can.
|
|
82
|
+
- For a step-through, run [its check](../pageship-interaction/references/step-through.md#check).
|
|
83
|
+
- For Mermaid, look at the screenshot at both widths and confirm every node and label is readable, no two lines run together, and no line runs through a node or a label.
|
|
84
|
+
- Without a browser, check your layout plan by arithmetic as [the SVG rules](references/svg.md#plan-the-layout) say.
|
|
85
|
+
|
|
86
|
+
## Hand over
|
|
87
|
+
|
|
88
|
+
The structure you introduced includes the form you chose for each figure and why, and any merges, splits or overviews. When a graph over the hand-drawn limits went to Mermaid instead of a computed layout, say why.
|
|
89
|
+
|
|
90
|
+
[steps]: ../pageship-interaction/references/step-through.md
|
|
91
|
+
[mermaid]: references/mermaid.md
|
|
92
|
+
[layout]: references/computed-layout.md
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
// Checks every hand-drawn diagram in the open page against the pageship-diagram rules.
|
|
2
|
+
// Run the whole file in the page, for example with `agent-browser eval "$(cat check-diagram.js)"`
|
|
3
|
+
// or Playwright's `page.evaluate`. It returns a list of problems; an empty list passes.
|
|
4
|
+
// Mermaid diagrams are skipped: Mermaid lays them out itself.
|
|
5
|
+
(() => {
|
|
6
|
+
const problems = [];
|
|
7
|
+
const gap = 4; // How close text may come to a connector, in screen pixels.
|
|
8
|
+
|
|
9
|
+
const rectOf = (element) => {
|
|
10
|
+
const r = element.getBoundingClientRect();
|
|
11
|
+
return { left: r.left, top: r.top, right: r.right, bottom: r.bottom };
|
|
12
|
+
};
|
|
13
|
+
const grow = (r, by) => ({
|
|
14
|
+
left: r.left - by,
|
|
15
|
+
top: r.top - by,
|
|
16
|
+
right: r.right + by,
|
|
17
|
+
bottom: r.bottom + by,
|
|
18
|
+
});
|
|
19
|
+
const overlaps = (a, b) =>
|
|
20
|
+
a.left < b.right && b.left < a.right && a.top < b.bottom && b.top < a.bottom;
|
|
21
|
+
const contains = (outer, inner) =>
|
|
22
|
+
inner.left >= outer.left - 1 &&
|
|
23
|
+
inner.right <= outer.right + 1 &&
|
|
24
|
+
inner.top >= outer.top - 1 &&
|
|
25
|
+
inner.bottom <= outer.bottom + 1;
|
|
26
|
+
// Whether a segment passes through a rectangle, by clipping the segment to it.
|
|
27
|
+
const segmentHits = (s, r) => {
|
|
28
|
+
if (r.left >= r.right || r.top >= r.bottom) return false;
|
|
29
|
+
const dx = s.x2 - s.x1;
|
|
30
|
+
const dy = s.y2 - s.y1;
|
|
31
|
+
let from = 0;
|
|
32
|
+
let to = 1;
|
|
33
|
+
for (const [p, q] of [
|
|
34
|
+
[-dx, s.x1 - r.left],
|
|
35
|
+
[dx, r.right - s.x1],
|
|
36
|
+
[-dy, s.y1 - r.top],
|
|
37
|
+
[dy, r.bottom - s.y1],
|
|
38
|
+
]) {
|
|
39
|
+
if (p === 0) {
|
|
40
|
+
if (q <= 0) return false;
|
|
41
|
+
} else if (p < 0) {
|
|
42
|
+
from = Math.max(from, q / p);
|
|
43
|
+
} else {
|
|
44
|
+
to = Math.min(to, q / p);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return from < to || (from === to && dx === 0 && dy === 0);
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// The points of a path drawn with M, L, H and V, in screen pixels.
|
|
51
|
+
const segmentsOf = (path) => {
|
|
52
|
+
const d = path.getAttribute("d") ?? "";
|
|
53
|
+
if (/[^MmLlHhVvZz\d\s,.+\-eE]/u.test(d)) return null;
|
|
54
|
+
const tokens = d.match(/[MmLlHhVvZz]|[-+]?(?:\d*\.\d+|\d+\.?)(?:[eE][-+]?\d+)?/gu) ?? [];
|
|
55
|
+
const points = [];
|
|
56
|
+
let x = 0;
|
|
57
|
+
let y = 0;
|
|
58
|
+
let command = "M";
|
|
59
|
+
for (let i = 0; i < tokens.length;) {
|
|
60
|
+
if (/^[MmLlHhVvZz]$/u.test(tokens[i])) command = tokens[i++];
|
|
61
|
+
const relative = command === command.toLowerCase();
|
|
62
|
+
const upper = command.toUpperCase();
|
|
63
|
+
if (upper === "Z") {
|
|
64
|
+
if (points.length) points.push({ ...points[0] });
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
const read = () => Number(tokens[i++]);
|
|
68
|
+
if (upper === "M" || upper === "L") {
|
|
69
|
+
const nx = read();
|
|
70
|
+
const ny = read();
|
|
71
|
+
x = relative ? x + nx : nx;
|
|
72
|
+
y = relative ? y + ny : ny;
|
|
73
|
+
if (upper === "M" && points.length) return null; // One connector per path.
|
|
74
|
+
if (upper === "M") command = relative ? "l" : "L";
|
|
75
|
+
} else if (upper === "H") {
|
|
76
|
+
const nx = read();
|
|
77
|
+
x = relative ? x + nx : nx;
|
|
78
|
+
} else if (upper === "V") {
|
|
79
|
+
const ny = read();
|
|
80
|
+
y = relative ? y + ny : ny;
|
|
81
|
+
}
|
|
82
|
+
points.push({ x, y });
|
|
83
|
+
}
|
|
84
|
+
const matrix = path.getScreenCTM();
|
|
85
|
+
const screen = points.map((p) => new DOMPoint(p.x, p.y).matrixTransform(matrix));
|
|
86
|
+
const segments = [];
|
|
87
|
+
for (let i = 1; i < screen.length; i++) {
|
|
88
|
+
const [a, b] = [screen[i - 1], screen[i]];
|
|
89
|
+
if (Math.abs(a.x - b.x) < 0.01 && Math.abs(a.y - b.y) < 0.01) continue;
|
|
90
|
+
segments.push({ x1: a.x, y1: a.y, x2: b.x, y2: b.y });
|
|
91
|
+
}
|
|
92
|
+
return segments;
|
|
93
|
+
};
|
|
94
|
+
const horizontal = (s) => Math.abs(s.y1 - s.y2) < 0.5;
|
|
95
|
+
const vertical = (s) => Math.abs(s.x1 - s.x2) < 0.5;
|
|
96
|
+
|
|
97
|
+
document.querySelectorAll("svg").forEach((svg, index) => {
|
|
98
|
+
if (svg.closest(".mermaid") || svg.hasAttribute("aria-roledescription")) return;
|
|
99
|
+
const nodes = [...svg.querySelectorAll(".node")];
|
|
100
|
+
const edges = [...svg.querySelectorAll(".edge")];
|
|
101
|
+
const texts = [...svg.querySelectorAll("text")].filter((t) => t.getBBox().width > 0);
|
|
102
|
+
if (nodes.length === 0 && edges.length === 0 && texts.length === 0) return;
|
|
103
|
+
const name = svg.querySelector("title")?.textContent.trim() || `SVG ${index + 1}`;
|
|
104
|
+
const report = (message) => problems.push(`${name}: ${message}`);
|
|
105
|
+
|
|
106
|
+
// Accessible title and description.
|
|
107
|
+
const first = svg.firstElementChild;
|
|
108
|
+
const labelled = (svg.getAttribute("aria-labelledby") ?? "")
|
|
109
|
+
.split(/\s+/u)
|
|
110
|
+
.filter(Boolean)
|
|
111
|
+
.map((id) => document.getElementById(id));
|
|
112
|
+
if (svg.getAttribute("role") !== "img") report('the svg needs role="img"');
|
|
113
|
+
if (first?.tagName !== "title" || !first.textContent.trim()) {
|
|
114
|
+
report("the first child must be a <title> with text");
|
|
115
|
+
}
|
|
116
|
+
const desc = svg.querySelector(":scope > desc");
|
|
117
|
+
if (!desc?.textContent.trim()) report("it needs a <desc> with text");
|
|
118
|
+
if (!labelled.includes(first) || !labelled.includes(desc)) {
|
|
119
|
+
report("aria-labelledby must name the <title> and <desc> ids");
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Drawn at its own size, scrolling inside its container when wide.
|
|
123
|
+
const scale = svg.getScreenCTM()?.a ?? 1;
|
|
124
|
+
if (svg.viewBox.baseVal && svg.viewBox.baseVal.width > 0 && Math.abs(scale - 1) > 0.02) {
|
|
125
|
+
report(`it is drawn at ${Math.round(scale * 100)}% of its size; give it the viewBox size`);
|
|
126
|
+
}
|
|
127
|
+
let scroller = svg.parentElement;
|
|
128
|
+
while (scroller && !/(auto|scroll)/u.test(getComputedStyle(scroller).overflowX)) {
|
|
129
|
+
scroller = scroller.parentElement;
|
|
130
|
+
}
|
|
131
|
+
if (!scroller || scroller === document.documentElement || scroller === document.body) {
|
|
132
|
+
report('it needs a wrapper with overflow-x: auto, such as <div class="scroll">');
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Width: the page's column.
|
|
136
|
+
const width = svg.width.baseVal.value;
|
|
137
|
+
if (width > 1088) {
|
|
138
|
+
const fix =
|
|
139
|
+
svg.dataset.layout === "computed"
|
|
140
|
+
? "try the other direction or split the figure"
|
|
141
|
+
: "redraw it narrower or split the figure";
|
|
142
|
+
report(`it is ${Math.round(width)}px wide; the limit is 1088px, so ${fix}`);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Counts. A figure laid out by layout.mjs has the computed-layout limits, and a timeline,
|
|
146
|
+
// marked data-layout="timeline", counts its events against 12.
|
|
147
|
+
const timeline = svg.dataset.layout === "timeline";
|
|
148
|
+
const [nodeLimit, edgeLimit] =
|
|
149
|
+
svg.dataset.layout === "computed" ? [30, 40] : [timeline ? 12 : 9, 12];
|
|
150
|
+
if (nodes.length > nodeLimit) {
|
|
151
|
+
report(`${nodes.length} ${timeline ? "events" : "nodes"}; the limit is ${nodeLimit}`);
|
|
152
|
+
}
|
|
153
|
+
if (edges.length > edgeLimit) report(`${edges.length} connectors; the limit is ${edgeLimit}`);
|
|
154
|
+
const accents = svg.querySelectorAll(".accent").length;
|
|
155
|
+
if (accents > 2) report(`${accents} accent elements; the limit is 2`);
|
|
156
|
+
|
|
157
|
+
// Type size.
|
|
158
|
+
for (const text of texts) {
|
|
159
|
+
const matrix = text.getScreenCTM();
|
|
160
|
+
const size =
|
|
161
|
+
parseFloat(getComputedStyle(text).fontSize) * (matrix ? Math.hypot(matrix.a, matrix.b) : 1);
|
|
162
|
+
if (size < 12.5)
|
|
163
|
+
report(`"${text.textContent.trim()}" is ${size.toFixed(1)}px; the minimum is 13px`);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Boxes.
|
|
167
|
+
const boxes = nodes.map((node) => ({
|
|
168
|
+
node,
|
|
169
|
+
id: node.id,
|
|
170
|
+
rect: rectOf(node.querySelector("rect, circle, ellipse, polygon, path") ?? node),
|
|
171
|
+
}));
|
|
172
|
+
for (const [i, a] of boxes.entries()) {
|
|
173
|
+
for (const b of boxes.slice(i + 1)) {
|
|
174
|
+
if (overlaps(a.rect, b.rect)) report(`nodes ${a.id || i} and ${b.id || "?"} overlap`);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Connectors.
|
|
179
|
+
const routes = [];
|
|
180
|
+
for (const edge of edges) {
|
|
181
|
+
const label = `connector ${edge.dataset.from ?? "?"} → ${edge.dataset.to ?? "?"}`;
|
|
182
|
+
const ends = [edge.dataset.from, edge.dataset.to];
|
|
183
|
+
if (ends.some((id) => !id || !boxes.some((box) => box.id === id))) {
|
|
184
|
+
report(`${label} needs data-from and data-to naming node ids`);
|
|
185
|
+
}
|
|
186
|
+
const segments = edge.tagName === "path" ? segmentsOf(edge) : null;
|
|
187
|
+
if (!segments?.length) {
|
|
188
|
+
report(`${label} must be one path drawn with M, H and V only`);
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
for (const s of segments) {
|
|
192
|
+
if (!horizontal(s) && !vertical(s)) report(`${label} has a slanted segment`);
|
|
193
|
+
}
|
|
194
|
+
for (const box of boxes) {
|
|
195
|
+
if (ends.includes(box.id)) continue;
|
|
196
|
+
if (segments.some((s) => segmentHits(s, grow(box.rect, -1)))) {
|
|
197
|
+
report(`${label} passes through or behind node ${box.id}`);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
routes.push({ label, segments });
|
|
201
|
+
}
|
|
202
|
+
let crossings = 0;
|
|
203
|
+
for (const [i, a] of routes.entries()) {
|
|
204
|
+
for (const b of routes.slice(i + 1)) {
|
|
205
|
+
for (const s of a.segments) {
|
|
206
|
+
for (const t of b.segments) {
|
|
207
|
+
const bothH = horizontal(s) && horizontal(t);
|
|
208
|
+
const bothV = vertical(s) && vertical(t);
|
|
209
|
+
if (bothH || bothV) {
|
|
210
|
+
const apart = bothH ? Math.abs(s.y1 - t.y1) : Math.abs(s.x1 - t.x1);
|
|
211
|
+
const [s1, s2] = bothH ? [s.x1, s.x2] : [s.y1, s.y2];
|
|
212
|
+
const [t1, t2] = bothH ? [t.x1, t.x2] : [t.y1, t.y2];
|
|
213
|
+
const shared =
|
|
214
|
+
Math.min(Math.max(s1, s2), Math.max(t1, t2)) -
|
|
215
|
+
Math.max(Math.min(s1, s2), Math.min(t1, t2));
|
|
216
|
+
if (apart < 12 && shared > 1) {
|
|
217
|
+
report(`${a.label} and ${b.label} run within 12px of each other`);
|
|
218
|
+
}
|
|
219
|
+
} else if ((horizontal(s) && vertical(t)) || (vertical(s) && horizontal(t))) {
|
|
220
|
+
const [h, v] = horizontal(s) ? [s, t] : [t, s];
|
|
221
|
+
const within = (value, a, b, margin) =>
|
|
222
|
+
value > Math.min(a, b) + margin && value < Math.max(a, b) - margin;
|
|
223
|
+
if (within(v.x1, h.x1, h.x2, 1) && within(h.y1, v.y1, v.y2, 1)) {
|
|
224
|
+
crossings++;
|
|
225
|
+
} else if (within(v.x1, h.x1, h.x2, -1) && within(h.y1, v.y1, v.y2, -1)) {
|
|
226
|
+
report(`${a.label} and ${b.label} meet at a bend or an end`);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
if (crossings > 0) {
|
|
234
|
+
problems.push(
|
|
235
|
+
`${name}: note, ${crossings} connector crossing(s); reorder nodes to remove them`,
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Other straight lines, such as a timeline's axis, ticks and leader lines.
|
|
240
|
+
const lines = [];
|
|
241
|
+
for (const element of svg.querySelectorAll("line, polyline, path")) {
|
|
242
|
+
if (element.closest(".node, marker, defs, .edge") || element.matches(".edge, .edge-hit"))
|
|
243
|
+
continue;
|
|
244
|
+
let segments = null;
|
|
245
|
+
if (element.tagName === "line") {
|
|
246
|
+
const matrix = element.getScreenCTM();
|
|
247
|
+
const [a, b] = [
|
|
248
|
+
[element.x1, element.y1],
|
|
249
|
+
[element.x2, element.y2],
|
|
250
|
+
].map(([x, y]) => new DOMPoint(x.baseVal.value, y.baseVal.value).matrixTransform(matrix));
|
|
251
|
+
segments = [{ x1: a.x, y1: a.y, x2: b.x, y2: b.y }];
|
|
252
|
+
} else if (element.tagName === "polyline") {
|
|
253
|
+
const matrix = element.getScreenCTM();
|
|
254
|
+
const points = [...element.points].map((p) =>
|
|
255
|
+
new DOMPoint(p.x, p.y).matrixTransform(matrix),
|
|
256
|
+
);
|
|
257
|
+
segments = points
|
|
258
|
+
.slice(1)
|
|
259
|
+
.map((b, i) => ({ x1: points[i].x, y1: points[i].y, x2: b.x, y2: b.y }));
|
|
260
|
+
} else {
|
|
261
|
+
segments = segmentsOf(element);
|
|
262
|
+
}
|
|
263
|
+
if (segments?.length) lines.push(segments);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Text: clear of every line, of other text and of boxes it doesn't belong to.
|
|
267
|
+
const textRects = texts.map((text) => ({
|
|
268
|
+
text,
|
|
269
|
+
rect: rectOf(text),
|
|
270
|
+
words: text.textContent.trim(),
|
|
271
|
+
box: boxes.find((box) => box.node.contains(text)),
|
|
272
|
+
}));
|
|
273
|
+
for (const [i, t] of textRects.entries()) {
|
|
274
|
+
for (const route of routes) {
|
|
275
|
+
if (route.segments.some((s) => segmentHits(s, grow(t.rect, gap)))) {
|
|
276
|
+
report(`"${t.words}" touches ${route.label}`);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
if (lines.some((segments) => segments.some((s) => segmentHits(s, grow(t.rect, gap))))) {
|
|
280
|
+
report(`"${t.words}" touches a line`);
|
|
281
|
+
}
|
|
282
|
+
if (
|
|
283
|
+
t.text.matches(".edge-label") &&
|
|
284
|
+
!routes.some((route) => route.segments.some((s) => segmentHits(s, grow(t.rect, 16))))
|
|
285
|
+
) {
|
|
286
|
+
report(`"${t.words}" is a connector label more than 12px from any connector`);
|
|
287
|
+
}
|
|
288
|
+
for (const box of boxes) {
|
|
289
|
+
if (t.box === box) {
|
|
290
|
+
if (!contains(box.rect, t.rect)) report(`"${t.words}" spills out of node ${box.id}`);
|
|
291
|
+
} else if (!t.box && overlaps(t.rect, box.rect)) {
|
|
292
|
+
report(`"${t.words}" overlaps node ${box.id}`);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
for (const u of textRects.slice(i + 1)) {
|
|
296
|
+
if (overlaps(t.rect, u.rect)) report(`"${t.words}" overlaps "${u.words}"`);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
});
|
|
300
|
+
return problems;
|
|
301
|
+
})();
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Computed layout
|
|
2
|
+
|
|
3
|
+
[layout.mjs](layout.mjs) runs ELK on a list of nodes and connectors while you write, and prints the figure as inline SVG in [the SVG markup](svg.md#markup). The file loads no library.
|
|
4
|
+
|
|
5
|
+
## What it needs
|
|
6
|
+
|
|
7
|
+
- **Node.js 18 or later.**
|
|
8
|
+
- **ELK.** The script downloads elkjs 0.12.0's single-file build once into the temporary folder. To skip that, set `PAGESHIP_ELK` to the path of a copy you already have, such as a project's `node_modules/elkjs/lib/elk.bundled.js`.
|
|
9
|
+
|
|
10
|
+
Without Node, or when the script says it could not download ELK, draw the graph with [Mermaid](mermaid.md) and tell the user why in the hand-over.
|
|
11
|
+
|
|
12
|
+
## 1. Write the spec
|
|
13
|
+
|
|
14
|
+
The spec is JSON:
|
|
15
|
+
|
|
16
|
+
| Field | Holds |
|
|
17
|
+
| ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
18
|
+
| `slug` | The figure's slug, lowercase, which prefixes every `id` in it. |
|
|
19
|
+
| `title` | The `<title>`, naming the subject. |
|
|
20
|
+
| `description` | The `<desc>`, saying in one or two sentences what the figure shows. |
|
|
21
|
+
| `caption` | The `<figcaption>`, the figure's one claim in the source's words. |
|
|
22
|
+
| `direction` | `"down"` (the default) or `"right"`. |
|
|
23
|
+
| `kinds` | The names of the node [kinds](../SKILL.md#colours), in order, when the nodes have kinds. The script prints the legend. |
|
|
24
|
+
| `nodes` | `{ "id", "label", "accent", "kind" }` for each node. `"\n"` in a label starts a second line. Two accents at most. `kind` names one of `kinds`. |
|
|
25
|
+
| `edges` | `{ "from", "to", "label", "both" }` for each connector. `label` and `both` are optional. |
|
|
26
|
+
|
|
27
|
+
- List nodes and connectors in the order the reader follows them, starting point first. The layout keeps that order where it can, so in a cycle the start stays at the top and the connector back to it runs upwards.
|
|
28
|
+
- Label each connector whose meaning the reader can't tell from its two ends, such as a state change or a branch's condition, in three words at most. When every line means the same thing, such as "sends data to", leave them unlabelled and let the table say what flows.
|
|
29
|
+
- When two nodes send something both ways and neither direction carries a label, give one connector `"both": true`, which draws arrowheads at both ends. When each direction has its own label, such as two state changes, give each direction its own connector and label.
|
|
30
|
+
|
|
31
|
+
## 2. Run the script
|
|
32
|
+
|
|
33
|
+
Pass the spec on standard input:
|
|
34
|
+
|
|
35
|
+
```sh
|
|
36
|
+
node <this skill's folder>/references/layout.mjs <<'EOF'
|
|
37
|
+
{ "slug": "states", "title": "...", "description": "...", "caption": "...", "nodes": [...], "edges": [...] }
|
|
38
|
+
EOF
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
It prints the whole `<figure>`. Put it in the document as printed. To change the drawing, change the spec and run the script again; the coordinates come only from the script.
|
|
42
|
+
|
|
43
|
+
"Note: the figure is …px wide" means it is over the column's 1088px, which check-diagram.js reports. Try the other `direction`. When both are over, split the figure by area.
|
|
44
|
+
|
|
45
|
+
## 3. Finish the figure
|
|
46
|
+
|
|
47
|
+
- Style it with the CSS in [the SVG rules](svg.md#markup), kinds and accents included.
|
|
48
|
+
- After the figure, give every connection as a table by `pageship-table`, one row each, with what flows or what moves it on in the source's words, in `<details>` when the connectors carry labels and `<details open>` when they don't.
|
|
49
|
+
- A crossing note from check-diagram.js that reordering the spec did not remove may stay.
|