pageship 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/dist/cli.mjs +31 -21
  2. package/dist/skills/pageship/SKILL.md +8 -8
  3. package/dist/skills/pageship-chart/SKILL.md +18 -2
  4. package/dist/skills/pageship-code-block/SKILL.md +1 -1
  5. package/dist/skills/pageship-decision-record/SKILL.md +5 -1
  6. package/dist/skills/pageship-diagram/SKILL.md +16 -14
  7. package/dist/skills/pageship-diagram/references/check-diagram.js +3 -2
  8. package/dist/skills/pageship-docs/SKILL.md +32 -8
  9. package/dist/skills/pageship-docs/references/axe.js +34 -0
  10. package/dist/skills/pageship-docs/references/check.md +35 -8
  11. package/dist/skills/pageship-docs/references/design.md +38 -18
  12. package/dist/skills/pageship-docs/references/html-validate-rules.mjs +326 -0
  13. package/dist/skills/pageship-docs/references/html-validate.json +13 -0
  14. package/dist/skills/pageship-docs/references/palette.mjs +791 -0
  15. package/dist/skills/pageship-docs/references/plain-default.md +1 -1
  16. package/dist/skills/pageship-explainer/SKILL.md +13 -5
  17. package/dist/skills/pageship-interaction/SKILL.md +7 -5
  18. package/dist/skills/pageship-interaction/references/pointer.md +3 -0
  19. package/dist/skills/pageship-interaction/references/step-through.md +10 -25
  20. package/dist/skills/pageship-key-figures/SKILL.md +1 -1
  21. package/dist/skills/pageship-media/SKILL.md +2 -2
  22. package/dist/skills/pageship-motion/SKILL.md +31 -28
  23. package/dist/skills/pageship-motion/references/figure-motion.js +252 -0
  24. package/dist/skills/pageship-motion/references/figure-motion.md +294 -0
  25. package/dist/skills/pageship-plan/SKILL.md +3 -3
  26. package/dist/skills/pageship-report/SKILL.md +5 -1
  27. package/dist/skills/pageship-review-report/SKILL.md +5 -1
  28. package/dist/skills/pageship-slide-deck/SKILL.md +5 -3
  29. package/dist/skills/pageship-slide-deck/references/engine.md +1 -1
  30. package/dist/skills/pageship-spec/SKILL.md +5 -1
  31. package/dist/skills/pageship-table/SKILL.md +5 -3
  32. package/package.json +1 -1
package/dist/cli.mjs CHANGED
@@ -705,7 +705,7 @@ async function updateWorkspaceAddress(baseUrl, action, options = {}) {
705
705
  credentials: options.token ? "omit" : "include",
706
706
  headers: options.token ? { Authorization: `Bearer ${options.token}` } : void 0,
707
707
  redirect: "error",
708
- cache: "no-store",
708
+ cache: "default",
709
709
  throwOnError: false,
710
710
  signal: options.signal ? AbortSignal.any([timeout, options.signal]) : timeout
711
711
  });
@@ -724,7 +724,7 @@ function isRevisionId(value) {
724
724
  return typeof value === "string" && /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}(?![\s\S])/iu.test(value);
725
725
  }
726
726
  /** Same readable normalized Git remote grammar as the Effect contract. */
727
- function isProject(value) {
727
+ function isRepository(value) {
728
728
  return typeof value === "string" && value.length <= 512 && /^[a-z0-9._-]+(?::[0-9]{1,5})?(?:\/(?!\.{1,2}(?:\/|$))[^\x00-\x20\x7f/\\?#]+)+(?![\s\S])/u.test(value);
729
729
  }
730
730
  function isCliVersion(value) {
@@ -742,6 +742,16 @@ function requestOptions(baseUrl, options) {
742
742
  throwOnError: false
743
743
  };
744
744
  }
745
+ /**
746
+ * A change keeps the browser's default cache mode. Chromium skips its preflight cache for a
747
+ * `no-store` request, and no browser caches a change's response anyway.
748
+ */
749
+ function changeOptions(baseUrl, options) {
750
+ return {
751
+ ...requestOptions(baseUrl, options),
752
+ cache: "default"
753
+ };
754
+ }
745
755
  function record(value) {
746
756
  return typeof value === "object" && value !== null && !Array.isArray(value);
747
757
  }
@@ -757,7 +767,7 @@ function normalizeDocument(value) {
757
767
  return validDocument(normalized) ? normalized : void 0;
758
768
  }
759
769
  function validDocument(value) {
760
- return record(value) && isDocumentId(value.id) && typeof value.slug === "string" && /^[a-z0-9]+(?:-[a-z0-9]+)*(?![\s\S])/u.test(value.slug) && validTitle(value.title) && typeof value.sourcePath === "string" && value.sourcePath.length <= 1024 && /^(?!\/)(?!.*(?:^|\/)\.{1,2}(?:\/|$))(?!.*\/\/)[^\x00-\x1f\x7f\\:]*\.[hH][tT][mM][lL]?(?![\s\S])/u.test(value.sourcePath) && (value.project === null || isProject(value.project)) && (value.visibility === "private" || value.visibility === "public") && isRevisionId(value.revisionId) && typeof value.byteLength === "number" && Number.isSafeInteger(value.byteLength) && value.byteLength > 0 && typeof value.createdAt === "number" && Number.isSafeInteger(value.createdAt) && value.createdAt >= 0 && typeof value.publishedAt === "number" && Number.isSafeInteger(value.publishedAt) && value.publishedAt >= 0 && typeof value.url === "string" && new RegExp(`^https://[a-z0-9-]+\\.${workspaceDomainPattern}/d/${value.id}-${value.slug}$`, "u").test(value.url);
770
+ return record(value) && isDocumentId(value.id) && typeof value.slug === "string" && /^[a-z0-9]+(?:-[a-z0-9]+)*(?![\s\S])/u.test(value.slug) && validTitle(value.title) && typeof value.sourcePath === "string" && value.sourcePath.length <= 1024 && /^(?!\/)(?!.*(?:^|\/)\.{1,2}(?:\/|$))(?!.*\/\/)[^\x00-\x1f\x7f\\:]*\.[hH][tT][mM][lL]?(?![\s\S])/u.test(value.sourcePath) && (value.repository === null || isRepository(value.repository)) && (value.visibility === "private" || value.visibility === "public") && isRevisionId(value.revisionId) && typeof value.byteLength === "number" && Number.isSafeInteger(value.byteLength) && value.byteLength > 0 && typeof value.createdAt === "number" && Number.isSafeInteger(value.createdAt) && value.createdAt >= 0 && typeof value.publishedAt === "number" && Number.isSafeInteger(value.publishedAt) && value.publishedAt >= 0 && typeof value.url === "string" && new RegExp(`^https://[a-z0-9-]+\\.${workspaceDomainPattern}/d/${value.id}-${value.slug}$`, "u").test(value.url);
761
771
  }
762
772
  function validRevision(value, documentId) {
763
773
  return record(value) && isRevisionId(value.id) && typeof value.createdAt === "number" && Number.isSafeInteger(value.createdAt) && value.createdAt >= 0 && typeof value.byteLength === "number" && Number.isSafeInteger(value.byteLength) && value.byteLength > 0 && typeof value.isCurrent === "boolean" && (value.cliVersion === null || isCliVersion(value.cliVersion)) && typeof value.url === "string" && new RegExp(`^https://[a-z0-9-]+\\.${workspaceDomainPattern}/d/${documentId}-[a-z0-9]+(?:-[a-z0-9]+)*/revisions/${value.id}$`, "u").test(value.url);
@@ -861,7 +871,7 @@ async function getDocumentContent(baseUrl, id, options = {}) {
861
871
  throw new Error("Invalid document content response.");
862
872
  }
863
873
  async function restoreDocumentRevision(baseUrl, id, input, options = {}) {
864
- const request = requestOptions(baseUrl, options);
874
+ const request = changeOptions(baseUrl, options);
865
875
  const { data: received, response, error } = await documentsRestore({
866
876
  ...request,
867
877
  path: { id },
@@ -891,7 +901,7 @@ async function restoreDocumentRevision(baseUrl, id, input, options = {}) {
891
901
  }
892
902
  async function updateDocumentVisibility(baseUrl, id, visibility, options = {}) {
893
903
  const { data: received, response, error } = await documentsUpdateVisibility({
894
- ...requestOptions(baseUrl, options),
904
+ ...changeOptions(baseUrl, options),
895
905
  path: { id },
896
906
  body: { visibility }
897
907
  });
@@ -913,7 +923,7 @@ async function publishDocument(baseUrl, input, options = {}) {
913
923
  if (input.action) {
914
924
  if (!(await getDocumentLimits(baseUrl, options)).publicationActions.includes(input.action)) throw new DocumentRequestError("publication_action_unsupported", "This server does not support explicit publication actions. Ask the operator to update the server. No HTML was uploaded.", 400);
915
925
  }
916
- const request = requestOptions(baseUrl, options);
926
+ const request = changeOptions(baseUrl, options);
917
927
  const { data: received, response, error } = await documentsCreate({
918
928
  ...request,
919
929
  headers: {
@@ -923,7 +933,7 @@ async function publishDocument(baseUrl, input, options = {}) {
923
933
  body: new Blob([new Uint8Array(input.bytes)], { type: "text/html" }),
924
934
  query: {
925
935
  sourcePath: input.sourcePath,
926
- project: input.project,
936
+ repository: input.repository,
927
937
  cliVersion: input.cliVersion,
928
938
  action: input.action,
929
939
  documentId: input.documentId,
@@ -962,7 +972,7 @@ async function getWorkspace(baseUrl, options = {}) {
962
972
  });
963
973
  if (!response?.ok) throw new Error("Could not load your workspace.", { cause: error });
964
974
  if (data?.status === "signed_out" || data?.status === "uninvited") return { status: data.status };
965
- if (data?.status === "admitted" && typeof data.user?.id === "string" && data.user.id.length > 0 && typeof data.user?.name === "string" && (data.user.login === void 0 || typeof data.user.login === "string") && typeof data.workspace?.id === "string" && data.workspace.id.length > 0 && (data.address === void 0 || data.address === null || isWorkspaceAddress(data.address))) return data;
975
+ if (data?.status === "admitted" && typeof data.user?.id === "string" && data.user.id.length > 0 && typeof data.user?.name === "string" && (data.user.login === void 0 || typeof data.user.login === "string") && (data.user.githubAccountId === void 0 || typeof data.user.githubAccountId === "string" && /^[1-9]\d*$/u.test(data.user.githubAccountId)) && typeof data.workspace?.id === "string" && data.workspace.id.length > 0 && (data.address === void 0 || data.address === null || isWorkspaceAddress(data.address))) return data;
966
976
  throw new Error("Invalid workspace response.");
967
977
  }
968
978
  function validateCompatibility(value) {
@@ -1323,7 +1333,7 @@ const outcomes = {
1323
1333
  choice_required: "pending",
1324
1334
  address_required: "pending",
1325
1335
  source_collision: "pending",
1326
- project_mismatch: "pending",
1336
+ repository_mismatch: "pending",
1327
1337
  marker_unavailable: "pending",
1328
1338
  revision_conflict: "pending",
1329
1339
  cancelled: "pending",
@@ -1412,7 +1422,7 @@ function commandLine(command, args) {
1412
1422
  }
1413
1423
  //#endregion
1414
1424
  //#region package.json
1415
- var version = "0.2.0";
1425
+ var version = "0.4.0";
1416
1426
  //#endregion
1417
1427
  //#region src/publish.ts
1418
1428
  const execute = promisify(execFile);
@@ -1424,7 +1434,7 @@ async function sourceMetadata(file) {
1424
1434
  const path = await realpath(resolve(file));
1425
1435
  const cwd = await realpath(process.cwd());
1426
1436
  let root = contained(cwd, path) ? cwd : dirname(path);
1427
- let project;
1437
+ let repository;
1428
1438
  try {
1429
1439
  const git = async (...args) => (await execute("git", [
1430
1440
  "-C",
@@ -1445,8 +1455,8 @@ async function sourceMetadata(file) {
1445
1455
  "ssh:",
1446
1456
  "git:"
1447
1457
  ].includes(url.protocol) && url.hostname) {
1448
- const repository = `${url.hostname.toLowerCase()}${url.port ? `:${url.port}` : ""}/${url.pathname.replace(/^\/+|\/+$/gu, "").replace(/\.git$/u, "")}`;
1449
- if (isProject(repository)) project = repository;
1458
+ const normalized = `${url.hostname.toLowerCase()}${url.port ? `:${url.port}` : ""}/${url.pathname.replace(/^\/+|\/+$/gu, "").replace(/\.git$/u, "")}`;
1459
+ if (isRepository(normalized)) repository = normalized;
1450
1460
  }
1451
1461
  }
1452
1462
  } catch {}
@@ -1455,7 +1465,7 @@ async function sourceMetadata(file) {
1455
1465
  return {
1456
1466
  path,
1457
1467
  sourcePath,
1458
- project
1468
+ repository
1459
1469
  };
1460
1470
  }
1461
1471
  async function fileBytes(path, limit) {
@@ -1653,7 +1663,7 @@ async function publish(options) {
1653
1663
  expectedRevisionId: entry.revisionId
1654
1664
  };
1655
1665
  automatic = "manifest";
1656
- } else if (target.project === (source.project ?? null)) {
1666
+ } else if (target.repository === (source.repository ?? null)) {
1657
1667
  action = {
1658
1668
  action: "revise",
1659
1669
  documentId: target.id,
@@ -1661,9 +1671,9 @@ async function publish(options) {
1661
1671
  };
1662
1672
  automatic = "marker";
1663
1673
  } else return {
1664
- status: "project_mismatch",
1674
+ status: "repository_mismatch",
1665
1675
  document: target,
1666
- message: `This file's marker names ${target.url}, a document from ${target.project ?? "a loose file"}, but this file ${source.project ? `belongs to ${source.project}` : "is a loose file"}. No document changed. To revise that document anyway, publish with --revise ${target.id} --expected-revision ${target.revisionId}, or use --new for a separate document. Use --json for complete follow-up arguments.`,
1676
+ message: `This file's marker names ${target.url}, a document from ${target.repository ?? "a loose file"}, but this file ${source.repository ? `belongs to ${source.repository}` : "is a loose file"}. No document changed. To revise that document anyway, publish with --revise ${target.id} --expected-revision ${target.revisionId}, or use --new for a separate document. Use --json for complete follow-up arguments.`,
1667
1677
  resolutions: resolutions(options.file, contextArgs, [target])
1668
1678
  };
1669
1679
  } else if (detect) {
@@ -1691,7 +1701,7 @@ async function publish(options) {
1691
1701
  const result = await publishDocument(server, {
1692
1702
  bytes: uploaded,
1693
1703
  sourcePath: source.sourcePath,
1694
- ...source.project ? { project: source.project } : {},
1704
+ ...source.repository ? { repository: source.repository } : {},
1695
1705
  cliVersion: version,
1696
1706
  idempotencyKey,
1697
1707
  ...proposal
@@ -2191,7 +2201,7 @@ function listDocuments(options) {
2191
2201
  ...next ? { next } : {},
2192
2202
  message: [
2193
2203
  page.documents.length ? "Documents:" : options.cursor ? "No more documents." : "No documents.",
2194
- ...page.documents.map((document) => `${document.id} ${document.visibility} ${document.title}\n` + (document.project ? ` project ${document.project}\n` : "") + ` source ${document.sourcePath}\n current revision ${document.revisionId}\n ${document.url}`),
2204
+ ...page.documents.map((document) => `${document.id} ${document.visibility} ${document.title}\n` + (document.repository ? ` repository ${document.repository}\n` : "") + ` source ${document.sourcePath}\n current revision ${document.revisionId}\n ${document.url}`),
2195
2205
  ...next ? [`More documents: run ${commandLine(next.command, next.args)}`] : []
2196
2206
  ].join("\n")
2197
2207
  };
@@ -2346,7 +2356,7 @@ const isSkillName = (name) => /^pageship(?:-[a-z0-9-]+)?$/u.test(name);
2346
2356
  const recommendedSkills = ["frontend-design", "design-taste-frontend"];
2347
2357
  /** The oldest Node major that runs the authoring skills' scripts. */
2348
2358
  const authoringNodeMajor = 20;
2349
- const setupGuide = "https://pageship.dev/setup-guide";
2359
+ const setupGuide = "https://pageship.dev/setup-guide.md";
2350
2360
  const installNext = {
2351
2361
  command: "skills",
2352
2362
  args: ["install"]
@@ -2457,7 +2467,7 @@ async function findMissing(environment) {
2457
2467
  function notFound(missing) {
2458
2468
  if (missing.length === 0) return void 0;
2459
2469
  const named = missing.map((entry) => entry === "node" ? `Node ${authoringNodeMajor} or later` : entry);
2460
- return `${named.length > 1 ? `${named.slice(0, -1).join(", ")} and ${named.at(-1)}` : named[0]} ${named.length > 1 ? "were" : "was"} not found. The setup guide at ${setupGuide} covers them.`;
2470
+ return `${named.length > 1 ? `${named.slice(0, -1).join(", ")} and ${named.at(-1)}` : named[0]} ${named.length > 1 ? "were" : "was"} not found. The setup guide covers them: download ${setupGuide} and read all of it.`;
2461
2471
  }
2462
2472
  /**
2463
2473
  * Whether the Pageship skills in ~/.agents/skills are missing, current, or older than this
@@ -3,7 +3,7 @@ name: pageship
3
3
  description: Publish an HTML file to Pageship and manage what is published. Use when the user asks to publish, share or get a link to a document, update its published copy, change its visibility, list, inspect history, download or restore published documents, or sign in to Pageship. To write, edit, rename or move a document, load pageship-docs instead.
4
4
  license: MIT
5
5
  metadata:
6
- version: "0.2.0"
6
+ version: "0.4.0"
7
7
  ---
8
8
 
9
9
  # Pageship
@@ -18,7 +18,7 @@ A document marker in a file (`<!-- pageship:v1 document=<id> -->`) records that
18
18
 
19
19
  ## Write or change a document
20
20
 
21
- When the user asks to write, change, rename or move a document, load `pageship-docs` and follow it, even when the same request asks you to publish. When it hands over the file, publish it if the user asked. If `pageship-docs` isn't installed, say so and offer the setup guide at https://pageship.dev/setup-guide, which installs it. If the user declines, write the document without the authoring skills, then publish it if the user asked.
21
+ When the user asks to write, change, rename or move a document, load `pageship-docs` and follow it, even when the same request asks you to publish. When it hands over the file, publish it if the user asked. If `pageship-docs` isn't installed, say so and offer the setup guide, which installs it. Get it as [Install the CLI](#install-the-cli) describes. If the user declines, write the document without the authoring skills, then publish it if the user asked.
22
22
 
23
23
  ## Reading a result
24
24
 
@@ -38,7 +38,7 @@ A follow-up has a `command` and an `args` array. Run it as `pageship --json <com
38
38
 
39
39
  ## Install the CLI
40
40
 
41
- If the shell can't find `pageship`, tell the user the Pageship CLI isn't installed and offer to install it. Only with their consent, follow the setup guide at https://pageship.dev/setup-guide. It asks before it changes their environment.
41
+ If the shell can't find `pageship`, tell the user the Pageship CLI isn't installed and offer to install it. Only with their consent, follow the setup guide. Download https://pageship.dev/setup-guide.md with curl or a similar command and read all of it, since a fetch tool may hand you only a summary. The guide asks before it changes their environment.
42
42
 
43
43
  ## Check readiness
44
44
 
@@ -62,7 +62,7 @@ pageship --json skills install
62
62
 
63
63
  On `skills_installed`, they are in `~/.agents/skills`, and linked from `~/.claude/skills` when Claude Code is set up. On `skills_modified`, nothing was written: the folders in `folders` were changed after the CLI installed them, or the CLI has no record of them. Show the user those folders. Run `resolutions.force` only if they agree to replace them and lose those changes.
64
64
 
65
- Both results, and `status`, list in `missing` what the CLI looked for and did not find: the recommended skills `frontend-design` and `design-taste-frontend`, and `node`, Node 20 or later, which the authoring skills' scripts need. The CLI never installs them, and they never change the outcome. Mention them, and offer the setup guide at https://pageship.dev/setup-guide.
65
+ Both results, and `status`, list in `missing` what the CLI looked for and did not find: the recommended skills `frontend-design` and `design-taste-frontend`, and `node`, Node 20 or later, which the authoring skills' scripts need. The CLI never installs them, and they never change the outcome. Mention them, and offer the setup guide. Get it as [Install the CLI](#install-the-cli) describes.
66
66
 
67
67
  ## Sign in
68
68
 
@@ -88,9 +88,9 @@ Set the shell placeholder `PAGESHIP_FILE` to one nonempty `.html` or `.htm` file
88
88
  pageship --json publish "$PAGESHIP_FILE"
89
89
  ```
90
90
 
91
- The CLI records each published file's real path in its publish manifest on this machine, so running the same command again for that file revises its document without a choice, and a fresh session needs no remembered document ID. Inside a Git checkout with an origin remote, the file belongs to a project. A project file without a manifest entry, such as one in another checkout, is looked up by its project and repository-relative path on the server. `document.project` shows the readable remote, and it is `null` for a loose file outside any project.
91
+ The CLI records each published file's real path in its publish manifest on this machine, so running the same command again for that file revises its document without a choice, and a fresh session needs no remembered document ID. Inside a Git checkout with an origin remote, the file belongs to a repository. A file in a repository without a manifest entry, such as one in another checkout, is looked up by its repository and repository-relative path on the server. `document.repository` shows the readable remote, and it is `null` for a loose file outside any repository.
92
92
 
93
- After a successful upload the CLI adds the document marker to the end of the local file, so the file and the published revision match. The marker lets the file find its document after a rename or a move, and on another machine in a checkout of the same project. Keep the marker when you edit or regenerate the file, and do not write or change it yourself. To publish a file exactly as it is, add `--no-marker`.
93
+ After a successful upload the CLI adds the document marker to the end of the local file, so the file and the published revision match. The marker lets the file find its document after a rename or a move, and on another machine in a checkout of the same repository. Keep the marker when you edit or regenerate the file, and do not write or change it yourself. To publish a file exactly as it is, add `--no-marker`.
94
94
 
95
95
  ### Report the publish
96
96
 
@@ -104,7 +104,7 @@ Keep `idempotencyKey` only while you resolve an uncertain attempt.
104
104
 
105
105
  ### When the publish stops for a decision
106
106
 
107
- On `project_mismatch`, the file's marker names a document from another project, for example a fork. Nothing changed. Show the user the returned `document` and ask whether to revise it with the `resolutions.revise` entry or create a separate document with `resolutions.new`.
107
+ On `repository_mismatch`, the file's marker names a document from another repository, for example a fork. Nothing changed. Show the user the returned `document` and ask whether to revise it with the `resolutions.revise` entry or create a separate document with `resolutions.new`.
108
108
 
109
109
  On `marker_unavailable`, the marker names a document that was not found or is not the user's, and nothing was uploaded. Ask before running the returned `next`, which creates a separate document.
110
110
 
@@ -120,7 +120,7 @@ An unmarked loose file is not matched by path, so on another machine, or from an
120
120
  pageship --json publish "$PAGESHIP_FILE" --revise "$PAGESHIP_DOCUMENT" --expected-revision "$PAGESHIP_REVISION"
121
121
  ```
122
122
 
123
- This works for any document the user owns, even when the file moved or belongs to another project. Another account's document returns `unavailable`. Later plain publishes of this file revise the named document, and its marker names it. After `--new`, they revise the new one.
123
+ This works for any document the user owns, even when the file moved or belongs to another repository. Another account's document returns `unavailable`. Later plain publishes of this file revise the named document, and its marker names it. After `--new`, they revise the new one.
124
124
 
125
125
  ## Share a document
126
126
 
@@ -3,12 +3,12 @@ name: pageship-chart
3
3
  description: Later step of pageship-docs: for a request to write or edit a document, load pageship-docs first; load this skill first only when the user names it. Pattern for charts in a Pageship HTML document, loaded by a pageship- kind skill before it draws a chart.
4
4
  license: MIT
5
5
  metadata:
6
- version: "0.2.0"
6
+ version: "0.4.0"
7
7
  ---
8
8
 
9
9
  # Chart
10
10
 
11
- A chart answers one question about the source's numbers at a glance. You draw it as inline SVG on an honest scale, with every value the reader needs printed on the page, never with a chart library or Mermaid.
11
+ A chart answers one question about the source's numbers at a glance. You draw it as inline SVG on an honest scale, with every value the reader needs printed on the page, never with a chart library or Mermaid, and it plays once from its initial state to its values when it scrolls into view.
12
12
 
13
13
  Unless another pageship- skill loaded this one, start as [pageship-docs says](../pageship-docs/SKILL.md#starting-at-a-kind-or-pattern-skill).
14
14
 
@@ -85,6 +85,22 @@ Write static SVG markup with the coordinates computed as you write. For each val
85
85
 
86
86
  Buttons that highlight or filter series show every series by default.
87
87
 
88
+ ## 5. Play the entrance
89
+
90
+ Load `pageship-motion` before the first chart: [its playback rules](../pageship-motion/SKILL.md#2-build-it-so-the-still-page-tells-the-whole-story) own when a chart plays, its pace, its controls and reduced motion. The markup you wrote in step 4 is the end state; the entrance starts from each mark's initial state and ends on that markup.
91
+
92
+ | Form | Entrance |
93
+ | -------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
94
+ | Vertical bars, histogram bins | Bars grow up from the baseline to their height; each printed value fades in with its bar. |
95
+ | Horizontal bars | Bars grow from the left axis to their length, top to bottom. |
96
+ | Line chart | Each line draws from left to right; a point and its value appear as the line reaches it; the direct label appears at the end. |
97
+ | Share bar | The parts fill in from left to right, one after the next, each to its share. |
98
+ | Scatter plot, dot plot | Dots appear in order of the horizontal axis, left to right. |
99
+ | Small multiples | Every small chart plays its own form's entrance at the same time, on the same clock. |
100
+ | Target, forecast and other lines | Appear in full when the series they belong to finishes. |
101
+
102
+ [The reference script](../pageship-motion/references/figure-motion.md) shows bars that grow and a line that draws; the other forms scale each mark's size or opacity the same way.
103
+
88
104
  ## Colours
89
105
 
90
106
  Colour the chart in the stylesheet, with `fill` and `stroke`, from [the look's colour variables](../pageship-docs/references/design.md#the-looks-colour-variables) and no other colour:
@@ -3,7 +3,7 @@ name: pageship-code-block
3
3
  description: Later step of pageship-docs: for a request to write or edit a document, load pageship-docs first; load this skill first only when the user names it. Pattern for code in a Pageship HTML document, such as source code, commands, configuration or logs, loaded by a pageship- kind skill before the first code block.
4
4
  license: MIT
5
5
  metadata:
6
- version: "0.2.0"
6
+ version: "0.4.0"
7
7
  ---
8
8
 
9
9
  # Code block
@@ -3,7 +3,7 @@ name: pageship-decision-record
3
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
4
  license: MIT
5
5
  metadata:
6
- version: "0.2.0"
6
+ version: "0.4.0"
7
7
  ---
8
8
 
9
9
  # Decision record
@@ -29,6 +29,10 @@ Build one comparison of the options:
29
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
30
  - Keep the source's order of options, and mark the chosen one in words.
31
31
 
32
+ ## Figures
33
+
34
+ Beyond the comparison table, a decision record's figures come from the numbers behind the context and from the consequences, such as a table of the consequences when most share fields such as standing and who acts. When the source gives a number for every option on one point, such as cost, a chart of that point beside the table is a candidate figure.
35
+
32
36
  ## Order
33
37
 
34
38
  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.
@@ -3,7 +3,7 @@ name: pageship-diagram
3
3
  description: Later step of pageship-docs: for a request to write or edit a document, load pageship-docs first; load this skill first only when the user names it. Pattern for 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
4
  license: MIT
5
5
  metadata:
6
- version: "0.2.0"
6
+ version: "0.4.0"
7
7
  ---
8
8
 
9
9
  # Diagram
@@ -16,16 +16,16 @@ Unless another pageship- skill loaded this one, start as [pageship-docs says](..
16
16
 
17
17
  Write down the reader's question the figure answers, in a few words, and take the row closest to it.
18
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 |
19
+ | The reader asks | Diagram | Drawn with |
20
+ | ----------------------------------------------- | --------------------------------------- | ----------------------------------- |
21
+ | What happens next, and where does it branch? | Flowchart, top to bottom | SVG, played as a [sequence][motion] |
22
+ | Who hands what to whom, in what order? | Swimlane or sequence | SVG, played as a [sequence][motion] |
23
+ | When did each thing happen, and how far apart? | [Timeline](references/svg.md#timelines) | SVG, played as a [sequence][motion] |
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
27
 
28
- A process or a change over time of three to eight steps gets the [step-through][steps], unless its layout is computed.
28
+ Every diagram plays once when it scrolls into view, by `pageship-motion`. A process, a path or events in time play as a [sequence][motion]: a marker travels the path, or the stages or events appear in order, and the reader has the controls. Every other diagram, a computed layout included, is a graphic that builds in: its nodes appear, then its connectors draw. A Mermaid figure, which Mermaid draws at load, holds still.
29
29
 
30
30
  ## 2. Fit the limits
31
31
 
@@ -52,7 +52,7 @@ Content you merge or move stays in the document, in a figure or the text beside
52
52
  - With SVG, follow [the SVG rules](references/svg.md).
53
53
  - With a computed layout, follow [the computed layout steps][layout].
54
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].
55
+ - Load `pageship-motion` before the first SVG figure. A process, a path or events in time laid out by hand plays as a [sequence][motion]: each node and connector carries the `data-step` that brings it in, and an ordered list of the steps with their values follows the drawing. Any other SVG figure builds in as a graphic: the nodes appear, then the connectors draw.
56
56
 
57
57
  Every figure, however drawn:
58
58
 
@@ -78,8 +78,10 @@ When the source sorts the nodes into two to four named groups, such as the team
78
78
 
79
79
  ## Check
80
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).
81
+ Run the browser steps with [the check's browser tool](../pageship-docs/references/check.md#checks-in-a-browser).
82
+
83
+ - Run [check-diagram.js](references/check-diagram.js) in the page at 1280px and at 390px wide. In `html_preview`, the copy's `load` listener runs `const problems = <the file's text>` and logs `JSON.stringify(problems)`. In agent-browser, run `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.
84
+ - For every SVG figure, run [the motion check](../pageship-motion/SKILL.md#check).
83
85
  - 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
86
  - Without a browser, check your layout plan by arithmetic as [the SVG rules](references/svg.md#plan-the-layout) say.
85
87
 
@@ -87,6 +89,6 @@ When the source sorts the nodes into two to four named groups, such as the team
87
89
 
88
90
  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
91
 
90
- [steps]: ../pageship-interaction/references/step-through.md
92
+ [motion]: ../pageship-motion/SKILL.md
91
93
  [mermaid]: references/mermaid.md
92
94
  [layout]: references/computed-layout.md
@@ -1,6 +1,7 @@
1
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.
2
+ // Run the whole file in the page, for example with `agent-browser eval "$(cat check-diagram.js)"`,
3
+ // or in html_preview as `const problems = <this file>` in a load listener that logs them.
4
+ // It returns a list of problems; an empty list passes.
4
5
  // Mermaid diagrams are skipped: Mermaid lays them out itself.
5
6
  (() => {
6
7
  const problems = [];
@@ -3,13 +3,15 @@ name: pageship-docs
3
3
  description: First step for any document people read, such as a report, plan, spec, explainer, review, decision record or slide deck, in HTML or Markdown. Load it before any other pageship- skill. Use when the user asks for one of these documents, asks to regenerate, redesign, edit, rename, move or copy an HTML source file, or asks to record whether they want HTML or Markdown.
4
4
  license: MIT
5
5
  metadata:
6
- version: "0.2.0"
6
+ version: "0.4.0"
7
7
  ---
8
8
 
9
9
  # Pageship documents
10
10
 
11
11
  The result is Markdown or one standalone HTML source file. You never publish yourself; you hand the file to the `pageship` skill, which does.
12
12
 
13
+ When your task says you write the file for `pageship-docs`, you are the subagent of [step 4 in a subagent](#step-4-in-a-subagent): start at step 4 with the format, kind and path it gives. Otherwise, when you can start a subagent, as in Claude Code, Codex and T3 Code, run steps 1 to 3 yourself and send step 4 to one subagent, for a new file and an edit alike. The sources, skill text and check output then stay in its context, not yours. Without subagents, run every step yourself.
14
+
13
15
  ## 1. Settle the format
14
16
 
15
17
  Take the first rule that applies.
@@ -35,7 +37,7 @@ For Markdown, go to step 3.
35
37
  | Decision record | What was decided, the options and the consequences | `pageship-decision-record` |
36
38
  | Slide deck | Slides to present from a browser | `pageship-slide-deck` |
37
39
 
38
- Load the skill of the kind that fits. When none fits exactly, or its skill is not installed, use the closest installed kind and tell the user which.
40
+ Pick the kind that fits. Step 4 loads its skill. When none fits exactly, or its skill is not installed, use the closest installed kind and tell the user which.
39
41
 
40
42
  ## 3. Choose the path
41
43
 
@@ -43,25 +45,47 @@ A new file goes at the path the user names, or else where the project keeps such
43
45
 
44
46
  ## 4. Write the file
45
47
 
48
+ When you send this step to a subagent, follow [step 4 in a subagent](#step-4-in-a-subagent).
49
+
46
50
  For Markdown, write one file by the [content rules](references/content.md) alone, then hand over.
47
51
 
48
- For HTML, the kind skill gives the parts, the order and its own rules, which win over this skeleton:
52
+ For HTML, load the kind skill. It gives the parts, the order and its own rules, which win over this skeleton:
49
53
 
50
54
  1. **Gather.** Read the request, every file it points to and the project's glossary. Where the kind asks for an **inventory**, list every item of the source in its order and words, until an item covers every sentence of the source. Done when you can point to where each fact you plan to write comes from.
51
55
  2. **Sort.** Put every statement of the source into exactly one of the kind's parts.
52
- 3. **Write.** One file by the [content rules](references/content.md), the [file rules](references/file.md) and the [design rules](references/design.md), in the kind's order, under its headings or the closest glossary terms. A part the source leaves empty keeps its heading and one line saying the source gives none. Done when every sorted statement, or every inventory item, is in the file.
53
- 4. **Check.** Run [the check](references/check.md), with the checks the kind and pattern skills add.
56
+ 3. **Figures.** For each section, ask whether a figure carries its point better than prose: a chart, a table, a diagram or key figures. Wherever one does, use it, in the form [the design rules](references/design.md#the-form-of-each-figure) give. Technical or complex content, such as a mechanism, a set of numbers or a comparison of several items, gets a figure and prose. The prose beside each figure says what it shows and why it matters, as the source puts it. Before drawing the first chart, graphic or sequence diagram, load `pageship-motion`, which plays each one as it scrolls into view. Done when you can name, for each section, the figures it gets, or that prose carries its point better.
57
+ 4. **Write.** One file by the [content rules](references/content.md), the [file rules](references/file.md) and the [design rules](references/design.md), in the kind's order, under its headings or the closest glossary terms. A part the source leaves empty keeps its heading and one line saying the source gives none. Done when every sorted statement, or every inventory item, is in the file.
58
+ 5. **Check.** Run [the check](references/check.md), with the checks the kind and pattern skills add.
54
59
 
55
60
  Then hand over. Your final message lists, however briefly you otherwise reply:
56
61
 
57
62
  - the file's path;
58
63
  - the structure you added that the source didn't have, as [the check lists it](references/check.md#the-structure-you-added);
59
64
  - the open questions, marking the ones you added;
60
- - the checks that ran and the ones you skipped;
65
+ - the checks that ran and the ones you skipped, and which tool ran [the browser checks](references/check.md#checks-in-a-browser), or that none did;
61
66
  - what the kind and pattern skills add to this list.
62
67
 
63
- For an HTML file, end the hand-over with the next step for publishing. When the user asked in this conversation to publish this document, or accepted an offer to, load `pageship` and publish it. Otherwise, when the `pageship` skill is installed, offer to publish the file through it; for a file with a document marker, offer to publish the change as a new revision of its document.
68
+ As the subagent, end at the list. Otherwise, for an HTML file, end the hand-over with the next step for publishing. When the user asked in this conversation to publish this document, or accepted an offer to, load `pageship` and publish it. Otherwise, when the `pageship` skill is installed, offer to publish the file through it; for a file with a document marker, offer to publish the change as a new revision of its document.
69
+
70
+ ## Step 4 in a subagent
71
+
72
+ Start one subagent and give it:
73
+
74
+ - the user's request, word for word, and every source that lives only in this conversation, such as pasted text or an answer the user gave, quoted in full;
75
+ - each source file by absolute path, the file being edited, renamed, moved or copied included. The subagent reads them;
76
+ - the format and the path from steps 1 to 3 and, for HTML, the kind or lone pattern skill;
77
+ - this instruction: "You write the file for `pageship-docs`. Load it and the skill named here, and follow step 4 and its hand-over. The format, kind and path are settled. Put each question you would ask the user into the open questions. Your final message is the full hand-over list; I handle publishing."
78
+
79
+ Wait for its final message. When it lacks the hand-over list, ask the subagent for the list.
80
+
81
+ When the list has "Fresh reading still to run", the subagent couldn't start a reader of its own:
82
+
83
+ 1. Start a new subagent that has not seen this conversation and give it only the reader task the list gives, word for word. Wait for its answer.
84
+ 2. Bring its quotes to the writer. When you can continue the writing subagent, send it the quotes in full. Otherwise start a new subagent with the file's path, each source file by absolute path, the kind skill, the quotes in full and this instruction: "You write the file for `pageship-docs`. Load it and the skill named here. The file is written; the quotes below are its fresh reading. Fix each one as the skill's check says, then run the rest of the check again. Your final message is the full hand-over list; I handle publishing."
85
+ 3. Its new list replaces the first. Done when the list you relay has no "Fresh reading still to run".
86
+
87
+ Relay the list to the user in full, open questions included, then, for an HTML file, end with the next step for publishing as step 4 says.
64
88
 
65
89
  ## Starting at a kind or pattern skill
66
90
 
67
- When the user named a kind skill, or named a pattern skill and asked for just that pattern, write HTML unless the request names another format, and start at step 3. A lone pattern is the file's main content, under an `h1`, with no parts to sort. In every other case, start at step 1.
91
+ The subagent always starts at step 4. Otherwise, when the user named a kind skill, or named a pattern skill and asked for just that pattern, write HTML unless the request names another format, and start at step 3. A lone pattern is the file's main content, under an `h1`, with no parts to sort. In every other case, start at step 1.
@@ -0,0 +1,34 @@
1
+ // Runs axe-core's WCAG 2 A and AA rules on the page and logs one line per problem, with the
2
+ // element and how to fix it. Run with `agent-browser eval --stdin < axe.js`, which also prints the
3
+ // lines, or paste into a <script> at the end of <body> for html_preview, which shows the log.
4
+ // The promise stays the script's value, which agent-browser eval waits for and prints.
5
+ // oxlint-disable-next-line typescript/no-floating-promises
6
+ (async () => {
7
+ // Added before the page's load event, the script delays it, so html_preview waits for axe-core.
8
+ if (!window.axe) {
9
+ const script = document.createElement("script");
10
+ script.src = "https://cdn.jsdelivr.net/npm/axe-core@4.14.0/axe.min.js";
11
+ script.integrity = "sha384-lFUXB8tI4BaTE1bqxH9eeAXON2NH1qsdAhs7S5L++nUNP5QOy+hwr0C3TYEJUEl0";
12
+ script.crossOrigin = "anonymous";
13
+ document.head.append(script);
14
+ await new Promise((resolve, reject) => {
15
+ script.onload = resolve;
16
+ script.onerror = () => reject(new Error("axe-core didn't load"));
17
+ });
18
+ }
19
+ if (document.readyState !== "complete") {
20
+ await new Promise((resolve) => addEventListener("load", resolve, { once: true }));
21
+ }
22
+ const { violations } = await window.axe.run(document, {
23
+ runOnly: ["wcag2a", "wcag2aa", "wcag21a", "wcag21aa", "wcag22aa"],
24
+ });
25
+ const problems = violations.flatMap((violation) =>
26
+ violation.nodes.map(
27
+ (node) =>
28
+ `axe ${violation.id}: ${node.target.join(" ")}: ${node.failureSummary.replace(/\s+/g, " ")}`,
29
+ ),
30
+ );
31
+ for (const problem of problems) console.log(problem);
32
+ console.log(`axe: ${problems.length} problems`);
33
+ return problems;
34
+ })();
@@ -2,25 +2,52 @@
2
2
 
3
3
  Run these checks on the finished file and fix what they find.
4
4
 
5
- The files a check makes, such as screenshots, PDFs and scripts, are scratch: save them by absolute path in the system temporary folder, never in the project, and delete them, wherever they landed, once the check is done.
5
+ The files a check makes, such as screenshots, PDFs, scripts and edited copies of the file, are scratch: save them by absolute path in the system temporary folder, never in the project, and delete them, wherever they landed, once the check is done.
6
6
 
7
7
  ## Checks that need no browser
8
8
 
9
- - Every `<script src>` and `<link rel="stylesheet">` tag matches a tag in the [file rules](file.md) exactly: the same URL, `integrity` and `crossorigin="anonymous"`. A script or stylesheet inside the file as a `data:` URL matches none. A hosted font matches by its address instead: a stylesheet link to `https://fonts.googleapis.com/css2?family=...`, with no `integrity`. A font preload (`rel="preload" as="font"`) needs no match.
10
- - No relative paths load anything: search for `src="` and `url(` and confirm each one is `https://`, `data:` or a `#fragment`.
9
+ Run the linter, html-validate with the config and rules in this folder:
10
+
11
+ ```sh
12
+ npx --yes html-validate@11.17.0 --config <pageship-docs folder>/references/html-validate.json <file>
13
+ ```
14
+
15
+ It reports HTML errors, accessibility problems such as a missing `lang` or `alt` or a skipped heading level, and breaks of three rules: [one standalone file](file.md#one-standalone-file) and [trusted libraries only](file.md#trusted-libraries-only) in the file rules, and **Height** in [the design rules](design.md#base-rules-every-file-meets). Fix each problem it reports and run it again. Done when it reports nothing. When `npx` can't run, such as without Node or network, check the file against those three rules by reading it, and say in the hand-over that the linter didn't run.
16
+
17
+ Then:
18
+
11
19
  - The file uses nothing on the sandbox list in the file rules.
12
20
  - The marker is as the file rules say: the same last line as before, or none at all.
13
21
  - Every number, name and date in the file appears in the source. Each gap is an open question.
14
- - Light and dark each set every variable in [the design rules](design.md#the-looks-colour-variables), each colour clears its floor, `--accent-2` sits as far from `--accent` as the table says, and both accents colour something on screen.
22
+ - Light and dark each set every variable in [the design rules](design.md#the-looks-colour-variables), and each colour clears its floor: `node <pageship-docs folder>/references/palette.mjs --check <file>` lists each one that doesn't. Without Node, compare each value with its floor yourself. Both accents colour something on screen.
15
23
 
16
24
  ## Checks in a browser
17
25
 
18
- Use a browser tool when you have one, such as `agent-browser` or Playwright; without one, skip this section. Open the file from disk and:
26
+ These checks, and the browser steps of the kind and pattern skills, are required whenever you have a browser tool. Use the first one you have:
27
+
28
+ 1. **`html_preview`**, in T3 Code. Pass the file's text as `html`, with a `width` and an `appearance`. It returns a screenshot of the whole page and the console output. A file over 512 KB is too large for it: use agent-browser.
29
+ 2. **agent-browser**, in every other harness. `agent-browser open file://<the file's absolute path>`, then `set viewport <width> 800`, `set media light` or `set media dark`, `screenshot <path>`, `console` and `errors`. A step's input uses `click`, `press` and `focus`, and a step's script runs with `eval`.
30
+ 3. **Neither.** Skip this section and the skills' browser steps, and take the step a skill gives for a missing browser, where it gives one.
19
31
 
20
- - At 1280px and at 390px wide, confirm the page doesn't scroll sideways: `document.documentElement.scrollWidth` is no larger than `clientWidth`.
21
- - Look at a screenshot at each width, in light and in dark. Fix overlapping, clipped or unreadable content.
32
+ Some states take an edited copy of the file:
33
+
34
+ - **axe-core in `html_preview`**: at the end of `<body>`, add a `<script>` holding the text of `<pageship-docs folder>/references/axe.js`. Every preview of the file uses a copy with it.
35
+ - **Dark in `html_preview`**, which doesn't set the colour scheme: change every `prefers-color-scheme: dark` to `prefers-color-scheme: light`, add `<style>:root { color-scheme: dark }</style>` at the end of `<head>`, and pass `appearance: "dark"`.
36
+ - **Reduced motion in `html_preview`**: swap `prefers-reduced-motion: reduce` and `prefers-reduced-motion: no-preference` everywhere. In agent-browser, `set media light reduced-motion`.
37
+ - **Print, in both tools**, which can't emulate it: swap `print` and `screen` in every media query, in `@media` rules and `matchMedia` calls alike, so `@media print` reads `@media screen`.
38
+ - **Input and scripts in `html_preview`**: at the end of `<body>`, add a `<script>` with a `load` listener that takes the steps and logs each result with `console.log`. Click with `element.click()`, which also stands for Enter or Space on a button; press a key with `element.dispatchEvent(new KeyboardEvent("keydown", { key: "ArrowRight", bubbles: true, cancelable: true }))`; hover with a `pointerover` event the same way. A script can't press Tab: log each control's `tabIndex`, which is `0` for every control in the tab order, then focus each one in document order with `element.focus()` and log `document.activeElement`. The screenshot shows the page after the listener ran.
39
+ - **A fragment in `html_preview`**, such as `#slide-3`: the copy's first script runs `history.replaceState(null, "", "#slide-3")`.
40
+
41
+ `html_preview` takes its screenshot right after the page loads, before timers and animations run their course, so a figure that plays in view at load may show its initial state or part of its playback; `pageship-motion`'s check covers its end state. A step that waits, such as letting playback run, takes the skill's step for a missing browser, or is skipped where the skill gives none.
42
+
43
+ At 1280px and at 390px wide, each in light and in dark:
44
+
45
+ - Confirm the page doesn't scroll sideways: `document.documentElement.scrollWidth` is no larger than `clientWidth`.
46
+ - Look at the screenshot. Fix overlapping, clipped or unreadable content.
22
47
  - Read the console. Fix every error, including failed integrity checks.
23
- - Use every control by keyboard and confirm it works and shows focus.
48
+ - Run axe-core, which checks accessibility in the rendered page, such as the contrast of each piece of text against what is really behind it. In agent-browser, run `agent-browser eval --stdin < <pageship-docs folder>/references/axe.js`; in `html_preview`, read the lines the copy's script logs. Fix each problem it lists.
49
+
50
+ Then use every control by keyboard and confirm it works and shows focus. Done when, at both widths in both colour schemes, the page doesn't scroll sideways, the screenshot shows nothing overlapping, clipped or unreadable, the console shows no error, and axe-core lists no problem, and every control works by keyboard and shows focus.
24
51
 
25
52
  ## Claims
26
53