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,362 @@
|
|
|
1
|
+
# Slide engine
|
|
2
|
+
|
|
3
|
+
Copy the three parts below into the deck unchanged: the markup around the slides, the stylesheet after the look's stylesheet, and the script at the end of `body`.
|
|
4
|
+
|
|
5
|
+
While presenting, the deck owns the arrow keys, Page Up, Page Down, Space, Home and End for the whole page. A widget inside a slide that handles one of those keys itself calls `event.preventDefault()`, and the deck then leaves that key alone.
|
|
6
|
+
|
|
7
|
+
## Markup
|
|
8
|
+
|
|
9
|
+
The slides go inside `main class="deck"`.
|
|
10
|
+
|
|
11
|
+
```html
|
|
12
|
+
<body>
|
|
13
|
+
<nav class="deck-outline" id="deck-outline" aria-label="Outline" hidden><ol></ol></nav>
|
|
14
|
+
<main class="deck">
|
|
15
|
+
<section class="slide" id="slide-1">
|
|
16
|
+
<h1>Deck title</h1>
|
|
17
|
+
<p>The main point, in the source's words.</p>
|
|
18
|
+
</section>
|
|
19
|
+
<section class="slide" id="slide-2">
|
|
20
|
+
<h2>The one idea of this slide</h2>
|
|
21
|
+
<ul>
|
|
22
|
+
<li>An item from the source</li>
|
|
23
|
+
</ul>
|
|
24
|
+
</section>
|
|
25
|
+
</main>
|
|
26
|
+
<nav class="deck-bar" aria-label="Slide controls" hidden>
|
|
27
|
+
<div class="deck-progress" aria-hidden="true"><span></span></div>
|
|
28
|
+
<span class="deck-position" aria-live="polite"></span>
|
|
29
|
+
<button type="button" data-deck="previous">Previous slide</button>
|
|
30
|
+
<button type="button" data-deck="next">Next slide</button>
|
|
31
|
+
<button type="button" data-deck="outline" aria-expanded="false" aria-controls="deck-outline">
|
|
32
|
+
Outline
|
|
33
|
+
</button>
|
|
34
|
+
</nav>
|
|
35
|
+
<script>
|
|
36
|
+
/* the engine script */
|
|
37
|
+
</script>
|
|
38
|
+
</body>
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Stylesheet
|
|
42
|
+
|
|
43
|
+
```css
|
|
44
|
+
/* Slide deck engine: copy unchanged. Without script, or narrower than 900px or shorter
|
|
45
|
+
than 500px, the slides read as one scrolling page. */
|
|
46
|
+
.slide {
|
|
47
|
+
max-width: 72rem;
|
|
48
|
+
margin: 0 auto;
|
|
49
|
+
padding: 2rem 1.25rem;
|
|
50
|
+
border-bottom: 1px solid var(--line);
|
|
51
|
+
font-size: 1rem;
|
|
52
|
+
}
|
|
53
|
+
.slide:focus {
|
|
54
|
+
outline: none;
|
|
55
|
+
}
|
|
56
|
+
.slide :is(h1, h2) {
|
|
57
|
+
margin: 0 0 0.6em;
|
|
58
|
+
padding: 0;
|
|
59
|
+
border: 0;
|
|
60
|
+
line-height: 1.2;
|
|
61
|
+
}
|
|
62
|
+
.slide h1 {
|
|
63
|
+
font-size: 2.25em;
|
|
64
|
+
}
|
|
65
|
+
.slide h1::after {
|
|
66
|
+
content: "";
|
|
67
|
+
display: block;
|
|
68
|
+
width: 2em;
|
|
69
|
+
height: 0.15em;
|
|
70
|
+
margin-top: 0.5em;
|
|
71
|
+
background: var(--accent-2);
|
|
72
|
+
}
|
|
73
|
+
.slide h2 {
|
|
74
|
+
font-size: 1.5em;
|
|
75
|
+
}
|
|
76
|
+
.slide h3 {
|
|
77
|
+
margin: 1em 0 0.4em;
|
|
78
|
+
font-size: 1.125em;
|
|
79
|
+
}
|
|
80
|
+
.slide :is(p, li) {
|
|
81
|
+
max-width: 70ch;
|
|
82
|
+
}
|
|
83
|
+
.slide :is(small, figcaption, .meta, table, pre) {
|
|
84
|
+
font-size: 0.875em;
|
|
85
|
+
}
|
|
86
|
+
.slide code {
|
|
87
|
+
font-size: inherit;
|
|
88
|
+
}
|
|
89
|
+
.deck-bar,
|
|
90
|
+
.deck-outline[hidden] {
|
|
91
|
+
display: none;
|
|
92
|
+
}
|
|
93
|
+
.deck-outline {
|
|
94
|
+
max-width: 72rem;
|
|
95
|
+
margin: 0 auto;
|
|
96
|
+
padding: 1rem 1.25rem;
|
|
97
|
+
}
|
|
98
|
+
@media screen and (min-width: 900px) and (min-height: 500px) {
|
|
99
|
+
.deck-on body {
|
|
100
|
+
overflow: hidden;
|
|
101
|
+
background: var(--surface);
|
|
102
|
+
}
|
|
103
|
+
.deck-on .deck {
|
|
104
|
+
position: fixed;
|
|
105
|
+
inset: 0 0 3.5rem;
|
|
106
|
+
max-width: none;
|
|
107
|
+
margin: 0;
|
|
108
|
+
padding: 0;
|
|
109
|
+
overflow: hidden;
|
|
110
|
+
}
|
|
111
|
+
.deck-on .slide {
|
|
112
|
+
position: absolute;
|
|
113
|
+
top: 50%;
|
|
114
|
+
left: 50%;
|
|
115
|
+
display: flex;
|
|
116
|
+
flex-direction: column;
|
|
117
|
+
width: 1280px;
|
|
118
|
+
max-width: none;
|
|
119
|
+
height: 720px;
|
|
120
|
+
padding: 64px 80px;
|
|
121
|
+
overflow: hidden;
|
|
122
|
+
border: 0;
|
|
123
|
+
background: var(--bg);
|
|
124
|
+
font-size: 28px;
|
|
125
|
+
line-height: 1.4;
|
|
126
|
+
transform: translate(-50%, -50%) scale(var(--deck-scale, 1));
|
|
127
|
+
visibility: hidden;
|
|
128
|
+
}
|
|
129
|
+
.deck-on .slide > * {
|
|
130
|
+
flex-shrink: 0;
|
|
131
|
+
}
|
|
132
|
+
.deck-on .slide.current {
|
|
133
|
+
visibility: visible;
|
|
134
|
+
}
|
|
135
|
+
.deck-on .deck-bar {
|
|
136
|
+
position: fixed;
|
|
137
|
+
inset: auto 0 0;
|
|
138
|
+
display: flex;
|
|
139
|
+
gap: 0.5rem;
|
|
140
|
+
align-items: center;
|
|
141
|
+
height: 3.5rem;
|
|
142
|
+
padding: 0 1rem;
|
|
143
|
+
border-top: 1px solid var(--line);
|
|
144
|
+
background: var(--bg);
|
|
145
|
+
}
|
|
146
|
+
.deck-on .deck-bar button {
|
|
147
|
+
min-height: 2.25rem;
|
|
148
|
+
padding: 0 0.875rem;
|
|
149
|
+
border: 1px solid var(--muted);
|
|
150
|
+
border-radius: 6px;
|
|
151
|
+
background: var(--bg);
|
|
152
|
+
color: var(--fg);
|
|
153
|
+
font: inherit;
|
|
154
|
+
}
|
|
155
|
+
.deck-on .deck-bar button:hover:not(:disabled) {
|
|
156
|
+
background: var(--surface);
|
|
157
|
+
}
|
|
158
|
+
.deck-on .deck-bar button:disabled {
|
|
159
|
+
opacity: 0.5;
|
|
160
|
+
}
|
|
161
|
+
.deck-on .deck-bar [aria-expanded="true"] {
|
|
162
|
+
border-width: 2px;
|
|
163
|
+
font-weight: 600;
|
|
164
|
+
}
|
|
165
|
+
.deck-on .deck-position {
|
|
166
|
+
margin-right: auto;
|
|
167
|
+
}
|
|
168
|
+
.deck-on .deck-progress {
|
|
169
|
+
position: absolute;
|
|
170
|
+
inset: -1px 0 auto;
|
|
171
|
+
height: 4px;
|
|
172
|
+
}
|
|
173
|
+
.deck-on .deck-progress span {
|
|
174
|
+
display: block;
|
|
175
|
+
height: 100%;
|
|
176
|
+
background: var(--accent);
|
|
177
|
+
}
|
|
178
|
+
.deck-on .deck-outline {
|
|
179
|
+
position: fixed;
|
|
180
|
+
z-index: 1;
|
|
181
|
+
right: 1rem;
|
|
182
|
+
bottom: 4rem;
|
|
183
|
+
width: min(28rem, 90vw);
|
|
184
|
+
max-height: calc(100vh - 6rem);
|
|
185
|
+
overflow-y: auto;
|
|
186
|
+
border: 1px solid var(--line);
|
|
187
|
+
border-radius: 6px;
|
|
188
|
+
background: var(--bg);
|
|
189
|
+
}
|
|
190
|
+
.deck-on .deck-outline [aria-current="page"] {
|
|
191
|
+
font-weight: 700;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
@page {
|
|
195
|
+
size: 1280px 720px;
|
|
196
|
+
margin: 0;
|
|
197
|
+
}
|
|
198
|
+
@media print {
|
|
199
|
+
.deck-bar,
|
|
200
|
+
.deck-outline {
|
|
201
|
+
display: none !important;
|
|
202
|
+
}
|
|
203
|
+
.deck {
|
|
204
|
+
max-width: none;
|
|
205
|
+
padding: 0;
|
|
206
|
+
}
|
|
207
|
+
.slide {
|
|
208
|
+
display: flex;
|
|
209
|
+
flex-direction: column;
|
|
210
|
+
width: 1280px;
|
|
211
|
+
max-width: none;
|
|
212
|
+
min-height: 720px;
|
|
213
|
+
margin: 0;
|
|
214
|
+
padding: 64px 80px;
|
|
215
|
+
border: 0;
|
|
216
|
+
font-size: 28px;
|
|
217
|
+
line-height: 1.4;
|
|
218
|
+
break-inside: avoid;
|
|
219
|
+
}
|
|
220
|
+
.slide + .slide {
|
|
221
|
+
break-before: page;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
## Script
|
|
227
|
+
|
|
228
|
+
```js
|
|
229
|
+
// Slide deck engine: copy unchanged. It needs the markup above and keeps its state in the
|
|
230
|
+
// URL fragment (#slide-3), never in storage.
|
|
231
|
+
(() => {
|
|
232
|
+
const root = document.documentElement;
|
|
233
|
+
const slides = [...document.querySelectorAll(".deck > .slide")];
|
|
234
|
+
const bar = document.querySelector(".deck-bar");
|
|
235
|
+
const position = bar.querySelector(".deck-position");
|
|
236
|
+
const progress = bar.querySelector(".deck-progress span");
|
|
237
|
+
const previous = bar.querySelector('[data-deck="previous"]');
|
|
238
|
+
const next = bar.querySelector('[data-deck="next"]');
|
|
239
|
+
const toggle = bar.querySelector('[data-deck="outline"]');
|
|
240
|
+
const outline = document.getElementById("deck-outline");
|
|
241
|
+
const presenting = matchMedia("screen and (min-width: 900px) and (min-height: 500px)");
|
|
242
|
+
const title = (slide) => slide.querySelector("h1, h2")?.textContent.trim() ?? slide.id;
|
|
243
|
+
let current = 0;
|
|
244
|
+
|
|
245
|
+
// The outline lists every slide with a link, built from each slide's heading.
|
|
246
|
+
const list = outline.querySelector("ol");
|
|
247
|
+
for (const slide of slides) {
|
|
248
|
+
const link = document.createElement("a");
|
|
249
|
+
link.href = `#${slide.id}`;
|
|
250
|
+
link.textContent = title(slide);
|
|
251
|
+
list.append(document.createElement("li"));
|
|
252
|
+
list.lastElementChild.append(link);
|
|
253
|
+
}
|
|
254
|
+
const links = [...list.querySelectorAll("a")];
|
|
255
|
+
|
|
256
|
+
const fromHash = () => {
|
|
257
|
+
const index = slides.findIndex((slide) => `#${slide.id}` === location.hash);
|
|
258
|
+
return index < 0 ? 0 : index;
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
function show(index, moveFocus) {
|
|
262
|
+
current = Math.max(0, Math.min(slides.length - 1, index));
|
|
263
|
+
const leaving = slides.find((slide) => slide.contains(document.activeElement));
|
|
264
|
+
slides.forEach((slide, i) => slide.classList.toggle("current", i === current));
|
|
265
|
+
links.forEach((link, i) =>
|
|
266
|
+
i === current
|
|
267
|
+
? link.setAttribute("aria-current", "page")
|
|
268
|
+
: link.removeAttribute("aria-current"),
|
|
269
|
+
);
|
|
270
|
+
position.textContent = `Slide ${current + 1} of ${slides.length}: ${title(slides[current])}`;
|
|
271
|
+
progress.style.width = `${((current + 1) / slides.length) * 100}%`;
|
|
272
|
+
previous.disabled = current === 0;
|
|
273
|
+
next.disabled = current === slides.length - 1;
|
|
274
|
+
if (location.hash !== `#${slides[current].id}`) {
|
|
275
|
+
history.replaceState(null, "", `#${slides[current].id}`);
|
|
276
|
+
}
|
|
277
|
+
if (presenting.matches && (moveFocus || leaving)) slides[current].focus();
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function fit() {
|
|
281
|
+
const scale = Math.min(innerWidth / 1280, (innerHeight - bar.offsetHeight) / 720);
|
|
282
|
+
root.style.setProperty("--deck-scale", String(scale));
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Reports every slide whose content outgrows its 16:9 frame, or scrolls inside a box on the
|
|
286
|
+
// slide. Split such a slide or cut its words; never shrink its text.
|
|
287
|
+
const excess = (box) =>
|
|
288
|
+
Math.max(box.scrollHeight - box.clientHeight, box.scrollWidth - box.clientWidth);
|
|
289
|
+
function checkOverflow() {
|
|
290
|
+
if (!presenting.matches) return;
|
|
291
|
+
slides.forEach((slide, i) => {
|
|
292
|
+
const boxes = [...slide.querySelectorAll("*")].filter(
|
|
293
|
+
(box) => getComputedStyle(box).overflow !== "visible",
|
|
294
|
+
);
|
|
295
|
+
const over = Math.max(excess(slide), ...boxes.map(excess));
|
|
296
|
+
if (over > 1) {
|
|
297
|
+
console.error(
|
|
298
|
+
`Slide ${i + 1} (${title(slide)}) overflows its 16:9 frame by ${over}px. Split it.`,
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function setOutline(open) {
|
|
305
|
+
outline.hidden = presenting.matches && !open;
|
|
306
|
+
toggle.setAttribute("aria-expanded", String(open));
|
|
307
|
+
if (open) (links[current] ?? links[0]).focus();
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function layout() {
|
|
311
|
+
fit();
|
|
312
|
+
setOutline(false);
|
|
313
|
+
show(current, false);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
root.classList.add("deck-on");
|
|
317
|
+
bar.hidden = false;
|
|
318
|
+
slides.forEach((slide) => (slide.tabIndex = -1));
|
|
319
|
+
current = fromHash();
|
|
320
|
+
layout();
|
|
321
|
+
addEventListener("resize", fit);
|
|
322
|
+
presenting.addEventListener("change", () => (layout(), checkOverflow()));
|
|
323
|
+
document.fonts.ready.then(() => (fit(), checkOverflow()));
|
|
324
|
+
addEventListener("hashchange", () => {
|
|
325
|
+
show(fromHash(), true);
|
|
326
|
+
setOutline(false);
|
|
327
|
+
});
|
|
328
|
+
previous.addEventListener("click", () => show(current - 1, false));
|
|
329
|
+
next.addEventListener("click", () => show(current + 1, false));
|
|
330
|
+
toggle.addEventListener("click", () =>
|
|
331
|
+
setOutline(toggle.getAttribute("aria-expanded") !== "true"),
|
|
332
|
+
);
|
|
333
|
+
|
|
334
|
+
// The deck owns the arrow keys for the whole page while it presents. A widget inside a
|
|
335
|
+
// slide that handles a key itself calls preventDefault, and the deck leaves that key alone.
|
|
336
|
+
document.addEventListener("keydown", (event) => {
|
|
337
|
+
if (!presenting.matches || event.defaultPrevented) return;
|
|
338
|
+
if (event.altKey || event.ctrlKey || event.metaKey) return;
|
|
339
|
+
if (event.key === "Escape" && toggle.getAttribute("aria-expanded") === "true") {
|
|
340
|
+
setOutline(false);
|
|
341
|
+
toggle.focus();
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
if (event.target.closest?.("input, select, textarea, [contenteditable]")) return;
|
|
345
|
+
const onControl = event.target.closest?.("a, button, summary");
|
|
346
|
+
const step = {
|
|
347
|
+
ArrowRight: 1,
|
|
348
|
+
ArrowDown: 1,
|
|
349
|
+
PageDown: 1,
|
|
350
|
+
ArrowLeft: -1,
|
|
351
|
+
ArrowUp: -1,
|
|
352
|
+
PageUp: -1,
|
|
353
|
+
" ": onControl ? 0 : event.shiftKey ? -1 : 1,
|
|
354
|
+
}[event.key];
|
|
355
|
+
if (event.key === "Home") show(0, true);
|
|
356
|
+
else if (event.key === "End") show(slides.length - 1, true);
|
|
357
|
+
else if (step) show(current + step, true);
|
|
358
|
+
else return;
|
|
359
|
+
event.preventDefault();
|
|
360
|
+
});
|
|
361
|
+
})();
|
|
362
|
+
```
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: pageship-spec
|
|
3
|
+
description: HTML specs, as the second step of pageship-docs. For any request to write, regenerate, edit or move a spec, 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
|
+
# Spec
|
|
10
|
+
|
|
11
|
+
A spec is a contract between the people who ask for a change and the people who build and test it. Readers hold the builders to every line, so every line is a decision the source made. Everything else is an open question. It describes behaviour, never how it is built, and names no file path or line number anywhere, its sources included.
|
|
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
|
+
- **Problem.** What is wrong today and for whom, with the evidence the source gives.
|
|
18
|
+
- **Solution.** What changes for the people the problem affects, as the source describes it.
|
|
19
|
+
- **User stories.** One for each need the source states, as "As a _actor_, I want _behaviour_, so that _reason_." All three come from the source. When it gives no reason, end the story after the behaviour.
|
|
20
|
+
- **Implementation decisions.** What the source states as agreed or decided about how the change behaves: rules, limits, timings people notice, who sees what, and the contracts between parts. Leave out storage, scheduled jobs and how often they run, and tasks such as "add a column". When such a detail serves an outcome the source states, such as "each invoice is sent once", keep that outcome.
|
|
21
|
+
- **Testing decisions.** How the source says the change will be tested: what, against which data, by whom, and before which step of the rollout. Say which criteria a test covers only when the source says so.
|
|
22
|
+
- **Out of scope.** What the source says the change will not do.
|
|
23
|
+
- **Assumptions.** Only what the source says it takes as given.
|
|
24
|
+
- **Open questions.** Every proposal, wish, disagreement or "we'll see" nobody agreed to, everything the source calls undecided, and every gap a builder would hit before they could finish. Keep who holds which view when the source says.
|
|
25
|
+
|
|
26
|
+
## Acceptance criteria
|
|
27
|
+
|
|
28
|
+
Write a criterion for each outcome of each decision. Open questions, out-of-scope items and testing or rollout steps, such as a replay or a staging week, get none. Use the source's numbers and words and the glossary's terms, not their definitions. Each criterion has:
|
|
29
|
+
|
|
30
|
+
1. **One outcome.** One situation, one event and one result. Two outcomes are two criteria, even when the source puts them in one sentence, such as a list of things a screen shows.
|
|
31
|
+
2. **A visible trigger.** The event is something a user or tester sees happen, such as a customer replying or an invoice's due date passing. A job or check running, or an alert or message being sent, happens inside the system: name where a person sees it, such as "the customer receives the email". Mark the trigger as not given, under rule 3, only when the source names no such event.
|
|
32
|
+
3. **No value the source doesn't give.** It adds no window, threshold, count, place, trigger, display or state. When it can't be tested without one, mark the missing value inside the criterion, such as "within [time not given in the source]" or "shown in [place not given in the source]". In a criterion in which something does not happen, the event is the tester checking, at a moment marked the same way when the source gives none; a time from the situation, such as the target or the end of a snooze, counts only when the source says to check then. List each marked gap under open questions; a criterion refers to an open question in no other way.
|
|
33
|
+
4. **A result, not the event again.** The result is what the tester observes, and the criterion would fail if the change were missing. "The invoice is cancelled" after "a user cancels an invoice" tests nothing. Name what the tester sees when the source states or directly implies it, and otherwise mark it under rule 3.
|
|
34
|
+
5. **Its own words.** It spells out the situation, the event and the result, so it reads without the others. "The same way as" another criterion is not a result.
|
|
35
|
+
|
|
36
|
+
Done when every decided outcome has a criterion.
|
|
37
|
+
|
|
38
|
+
## Order
|
|
39
|
+
|
|
40
|
+
1. **Title and summary.** An `h1` naming the change, then two or three sentences in the source's words: what will change, for whom, and the problem it solves. Add the date, author and status when the source gives them. When there are open questions, give their number as a link to them, such as "4 open questions".
|
|
41
|
+
2. **Problem.**
|
|
42
|
+
3. **Solution.**
|
|
43
|
+
4. **User stories.** Numbered.
|
|
44
|
+
5. **Acceptance criteria.** A numbered list, one criterion per item, each with an `id` such as `ac-1`.
|
|
45
|
+
6. **Implementation decisions.**
|
|
46
|
+
7. **Testing decisions.**
|
|
47
|
+
8. **Out of scope.**
|
|
48
|
+
9. **Assumptions.** Only when the source states some.
|
|
49
|
+
10. **Open questions.**
|
|
50
|
+
11. **Sources.** Each source by its own title, with a date only when it carries one. Call the user's request "the request".
|
|
51
|
+
|
|
52
|
+
## Check
|
|
53
|
+
|
|
54
|
+
- Take the criteria one at a time and test each against the five rules. Fix every criterion that breaks one.
|
|
55
|
+
|
|
56
|
+
## Hand over
|
|
57
|
+
|
|
58
|
+
The structure you introduced includes the user stories you phrased from the source's needs and the criteria you split. Also list the build details you left out, such as file paths and build tasks.
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: pageship-table
|
|
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 tables in a Pageship HTML document, loaded by a pageship- kind skill before it writes a table.
|
|
4
|
+
license: MIT
|
|
5
|
+
metadata:
|
|
6
|
+
version: "0.2.0"
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Table
|
|
10
|
+
|
|
11
|
+
A table lets the reader look up and compare exact values, on a phone and on paper.
|
|
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. Shape the table
|
|
16
|
+
|
|
17
|
+
- **Across and down.** Put the dimension with fewer values across, so the table stays narrow. Five measures for two quarters is one row per measure and one column per quarter.
|
|
18
|
+
- **Width.** A table has at most four columns, the row label and text columns included. For more, put fewer across, turn a column into groups, or split it into one table per question. Only when the reader must compare more than three values in one row, give it more. Up to four columns show in full at phone width too, stacked as the markup below says when they don't fit. A wider table may scroll sideways inside its box, with its row labels and header row in view.
|
|
19
|
+
- **Groups.** When rows fall into a few groups, such as regions or teams, give each group its own run of rows under a heading row, not a column that repeats the group's name.
|
|
20
|
+
- **Order.** Keep the source's order. Sort the rows only when the question is a ranking, and say in the caption what they are sorted by.
|
|
21
|
+
- **Values.** Copy each value as the source writes it, with the same digits, precision and unit; round and convert nothing. A unit every value in a column shares goes in the column header; otherwise it stays on each value.
|
|
22
|
+
- **Kinds of number.** Give the kind as the source does: in the caption when one kind covers the table, otherwise in the row or column label.
|
|
23
|
+
- **Worked-out values.** A change, total or share comes only from plain arithmetic on the source's figures, and the caption or a note under the table says which columns it was worked out from. Totals go in `<tfoot>`.
|
|
24
|
+
- **Gaps.** A cell the source doesn't fill says "Not given", never a blank, a zero or a dash, and the gap joins the open questions.
|
|
25
|
+
- **Rankings.** Every sentence that ranks rows, anywhere in the document (the summary, a caption, a control's hint), names the column in its header's words: a sort on breach rate puts the highest rate first, not the most breaches. When the column has a gap, the sentence says which rows it covers, such as "the slowest of the queues with a median", not "slowest in every region".
|
|
26
|
+
|
|
27
|
+
## 2. Write the markup
|
|
28
|
+
|
|
29
|
+
Every table takes this shape, scroll box included:
|
|
30
|
+
|
|
31
|
+
```html
|
|
32
|
+
<figure class="table-figure">
|
|
33
|
+
<figcaption id="q3-measures">
|
|
34
|
+
Measured figures, Q2 and Q3 2026, from the help desk export.
|
|
35
|
+
</figcaption>
|
|
36
|
+
<div class="table-scroll" role="region" tabindex="0" aria-labelledby="q3-measures">
|
|
37
|
+
<table aria-labelledby="q3-measures">
|
|
38
|
+
<thead>
|
|
39
|
+
<tr>
|
|
40
|
+
<th scope="col">Measure</th>
|
|
41
|
+
<th scope="col" class="num"><span>Q2</span></th>
|
|
42
|
+
<th scope="col" class="num"><span>Q3</span></th>
|
|
43
|
+
</tr>
|
|
44
|
+
</thead>
|
|
45
|
+
<tbody>
|
|
46
|
+
<tr>
|
|
47
|
+
<th scope="row">Tickets opened</th>
|
|
48
|
+
<td class="num">4,350</td>
|
|
49
|
+
<td class="num">4,812</td>
|
|
50
|
+
</tr>
|
|
51
|
+
</tbody>
|
|
52
|
+
</table>
|
|
53
|
+
</div>
|
|
54
|
+
</figure>
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
- The caption comes first and says what the table shows, for which period, from which source, and the kind of number.
|
|
58
|
+
- Each group is its own `<tbody data-group="…">` that opens with a heading row holding one `<th scope="rowgroup" colspan="…">` with the group's name.
|
|
59
|
+
- A number column's header holds its words in one element, a `<span>` or the sort button, so a long header wraps two or more words to a line.
|
|
60
|
+
- Give the table and its columns no width beyond the stylesheet's, which keeps words whole and more than one to a line in text cells and number headers, while `.num` cells take their natural width: no other `min-width`, fixed width or `width: 100%`, and no `overflow-wrap` or `word-break` of your own.
|
|
61
|
+
- A table of three or more columns with words in a column besides the row label stacks at phone width, unless it has sort buttons: give the `table` `class="stack"` and each `td` a `data-label` with its column header's words. Each row then shows its row label over one line per cell, the label before the value.
|
|
62
|
+
- Mark at most one column or row as the one the summary talks about, with weight or a label, never with colour alone.
|
|
63
|
+
|
|
64
|
+
Add this unchanged to the file's stylesheet, after the rules that set the look. Its `max-height` keeps a long table's header row in view at every width, except a stacked table on a phone, which shows in full; the file's own rules keep it too:
|
|
65
|
+
|
|
66
|
+
```css
|
|
67
|
+
.table-figure {
|
|
68
|
+
margin: 1.5rem 0;
|
|
69
|
+
}
|
|
70
|
+
.table-scroll {
|
|
71
|
+
overflow: auto;
|
|
72
|
+
max-width: 100%;
|
|
73
|
+
max-height: 70vh;
|
|
74
|
+
}
|
|
75
|
+
.table-scroll table {
|
|
76
|
+
width: auto;
|
|
77
|
+
border-collapse: separate;
|
|
78
|
+
border-spacing: 0;
|
|
79
|
+
font-size: 0.9375rem;
|
|
80
|
+
}
|
|
81
|
+
.table-scroll :is(th, td) {
|
|
82
|
+
padding: 0.5rem 0.75rem;
|
|
83
|
+
border-bottom: 1px solid var(--line);
|
|
84
|
+
text-align: left;
|
|
85
|
+
vertical-align: top;
|
|
86
|
+
overflow-wrap: normal;
|
|
87
|
+
word-break: normal;
|
|
88
|
+
}
|
|
89
|
+
.table-scroll :is(th, td):not(.num) {
|
|
90
|
+
min-width: 8rem;
|
|
91
|
+
}
|
|
92
|
+
.table-scroll th.num > * {
|
|
93
|
+
display: inline-block;
|
|
94
|
+
width: max-content;
|
|
95
|
+
min-width: min-content;
|
|
96
|
+
max-width: 7rem;
|
|
97
|
+
}
|
|
98
|
+
.table-scroll :is(th, td) code {
|
|
99
|
+
white-space: nowrap;
|
|
100
|
+
overflow-wrap: normal;
|
|
101
|
+
word-break: normal;
|
|
102
|
+
}
|
|
103
|
+
.table-scroll thead th {
|
|
104
|
+
position: sticky;
|
|
105
|
+
top: 0;
|
|
106
|
+
z-index: 2;
|
|
107
|
+
background: var(--bg);
|
|
108
|
+
vertical-align: bottom;
|
|
109
|
+
border-bottom-width: 2px;
|
|
110
|
+
}
|
|
111
|
+
.table-scroll tbody th {
|
|
112
|
+
font-weight: 500;
|
|
113
|
+
}
|
|
114
|
+
.table-scroll th[scope="rowgroup"] {
|
|
115
|
+
font-weight: 600;
|
|
116
|
+
background: var(--surface);
|
|
117
|
+
}
|
|
118
|
+
.table-scroll .num {
|
|
119
|
+
text-align: right;
|
|
120
|
+
font-variant-numeric: tabular-nums;
|
|
121
|
+
}
|
|
122
|
+
.table-scroll td.num {
|
|
123
|
+
white-space: nowrap;
|
|
124
|
+
}
|
|
125
|
+
.table-scroll tfoot :is(th, td) {
|
|
126
|
+
font-weight: 600;
|
|
127
|
+
border-bottom: 0;
|
|
128
|
+
}
|
|
129
|
+
.table-scroll :is(th, td):first-child {
|
|
130
|
+
position: sticky;
|
|
131
|
+
left: 0;
|
|
132
|
+
z-index: 1;
|
|
133
|
+
background: var(--bg);
|
|
134
|
+
}
|
|
135
|
+
.table-scroll thead th:first-child {
|
|
136
|
+
z-index: 3;
|
|
137
|
+
}
|
|
138
|
+
@media (max-width: 40rem) {
|
|
139
|
+
.table-scroll table {
|
|
140
|
+
font-size: 0.875rem;
|
|
141
|
+
}
|
|
142
|
+
.table-scroll :is(th, td) {
|
|
143
|
+
padding: 0.5rem;
|
|
144
|
+
}
|
|
145
|
+
.table-scroll:has(> table.stack) {
|
|
146
|
+
max-height: none;
|
|
147
|
+
}
|
|
148
|
+
.table-scroll table.stack,
|
|
149
|
+
.table-scroll table.stack :is(tbody, tfoot, tr, th, td) {
|
|
150
|
+
display: block;
|
|
151
|
+
}
|
|
152
|
+
.table-scroll table.stack thead {
|
|
153
|
+
display: none;
|
|
154
|
+
}
|
|
155
|
+
.table-scroll table.stack tr {
|
|
156
|
+
padding: 0.5rem 0;
|
|
157
|
+
border-bottom: 1px solid var(--line);
|
|
158
|
+
}
|
|
159
|
+
.table-scroll table.stack :is(th, td) {
|
|
160
|
+
position: static;
|
|
161
|
+
min-width: 0;
|
|
162
|
+
padding: 0.125rem 0;
|
|
163
|
+
border: 0;
|
|
164
|
+
text-align: left;
|
|
165
|
+
}
|
|
166
|
+
.table-scroll table.stack tbody th {
|
|
167
|
+
font-weight: 600;
|
|
168
|
+
}
|
|
169
|
+
.table-scroll table.stack td[data-label]::before {
|
|
170
|
+
content: attr(data-label) ": ";
|
|
171
|
+
color: var(--muted);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
@media print {
|
|
175
|
+
.table-scroll {
|
|
176
|
+
overflow: visible;
|
|
177
|
+
max-height: none;
|
|
178
|
+
}
|
|
179
|
+
.table-scroll :is(th, td) {
|
|
180
|
+
position: static !important;
|
|
181
|
+
}
|
|
182
|
+
.table-scroll table {
|
|
183
|
+
font-size: 0.8125rem;
|
|
184
|
+
}
|
|
185
|
+
.table-scroll td.num {
|
|
186
|
+
white-space: normal;
|
|
187
|
+
}
|
|
188
|
+
.table-scroll tr[hidden] {
|
|
189
|
+
display: table-row !important;
|
|
190
|
+
}
|
|
191
|
+
.table-controls,
|
|
192
|
+
.table-empty {
|
|
193
|
+
display: none !important;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
### Colours
|
|
199
|
+
|
|
200
|
+
The table takes [the look's colour variables](../pageship-docs/references/design.md#the-looks-colour-variables) and no other colour: `--line` for rules, `--surface` for group heading rows and `--fg` for text. A cell with a status the source gives holds it by [the design rules' statuses](../pageship-docs/references/design.md#the-looks-colour-variables), as `<span data-status="warn">At risk</span>`, and gets a dot in the status colour:
|
|
201
|
+
|
|
202
|
+
```css
|
|
203
|
+
.table-scroll [data-status]::before {
|
|
204
|
+
content: "";
|
|
205
|
+
display: inline-block;
|
|
206
|
+
width: 0.625rem;
|
|
207
|
+
height: 0.625rem;
|
|
208
|
+
margin-right: 0.375rem;
|
|
209
|
+
border-radius: 50%;
|
|
210
|
+
background: var(--status);
|
|
211
|
+
}
|
|
212
|
+
.table-scroll [data-status="good"] {
|
|
213
|
+
--status: var(--good);
|
|
214
|
+
}
|
|
215
|
+
.table-scroll [data-status="warn"] {
|
|
216
|
+
--status: var(--warn);
|
|
217
|
+
}
|
|
218
|
+
.table-scroll [data-status="bad"] {
|
|
219
|
+
--status: var(--bad);
|
|
220
|
+
}
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
## 3. Add sorting and filtering only for a real question
|
|
224
|
+
|
|
225
|
+
Controls answer a question the static table can't answer at a glance:
|
|
226
|
+
|
|
227
|
+
- **Sorting**, when a table has more than 15 rows and the reader would rank it by more than one column. When only one ranking matters, sort the rows that way in the markup instead.
|
|
228
|
+
- **Filter buttons**, when a table has more than 15 rows and a reader narrows it to one group or one value of a column, such as their region, a status or an owner: one button per value, up to six.
|
|
229
|
+
- **A search box**, when a table has more than 30 rows and the reader looks up one row by name.
|
|
230
|
+
|
|
231
|
+
When you add any, follow [sort and filter](references/sort-and-filter.md) exactly.
|
|
232
|
+
|
|
233
|
+
## Check
|
|
234
|
+
|
|
235
|
+
- At 390px wide, every table shows its row labels and its first value column without scrolling the table, no cell breaks a word, and no cell of four words or more holds one word per line.
|
|
236
|
+
- In a table taller than the screen, scroll its last row into view and confirm the header row is still visible.
|
|
237
|
+
- Emulate print media (in Playwright, `page.emulateMedia({ media: "print" })`) after applying a filter, and confirm every row of every table shows, with its column labels and without the filter controls.
|
|
238
|
+
- Reread every sentence in the document that names a highest, lowest, slowest or most against the rankings rule.
|
|
239
|
+
- Reset brings back every row in the source's order.
|
|
240
|
+
|
|
241
|
+
## Hand over
|
|
242
|
+
|
|
243
|
+
The structure you introduced includes row order, worked-out values, groups and controls.
|