@mark1russell7/site-pages 0.1.10 → 0.2.1
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 +120 -2
- package/build/chrome.d.ts +22 -3
- package/build/chrome.d.ts.map +1 -1
- package/build/chrome.js +14 -2
- package/build/chrome.js.map +1 -1
- package/build/cli.js +73 -11
- package/build/cli.js.map +1 -1
- package/build/explorer/index.d.ts +212 -0
- package/build/explorer/index.d.ts.map +1 -0
- package/build/explorer/index.js +1342 -0
- package/build/explorer/index.js.map +1 -0
- package/build/vite.d.ts +72 -0
- package/build/vite.d.ts.map +1 -0
- package/build/vite.js +75 -0
- package/build/vite.js.map +1 -0
- package/package.json +16 -2
- package/styles/explorer.css +173 -0
- package/styles/shell.css +166 -0
package/README.md
CHANGED
|
@@ -61,7 +61,9 @@ site-pages check --url https://owner.github.io/name/
|
|
|
61
61
|
|
|
62
62
|
A view fails for each of these problems:
|
|
63
63
|
|
|
64
|
-
- The page is wider than the screen, or an element goes past the right edge.
|
|
64
|
+
- The page is wider than the screen, or an element goes past the right edge. An element in a box that scrolls or clips does not count here.
|
|
65
|
+
- A box with `overflow-x: hidden` or `clip` cuts text at its right edge, at any width. A visitor cannot reach that text.
|
|
66
|
+
- A table goes out of view at a phone width. The table is in a box that scrolls, and a text of the table ends past the edge of the box.
|
|
65
67
|
- A text area hides a part of its text.
|
|
66
68
|
- The page shows no text.
|
|
67
69
|
- The page writes a console error, or a script throws an exception.
|
|
@@ -69,6 +71,41 @@ A view fails for each of these problems:
|
|
|
69
71
|
- A script or a style sheet does not load, from any origin. An example is a module script that imports `node:perf_hooks`.
|
|
70
72
|
- A check of the probe fails, or the probe makes no check.
|
|
71
73
|
|
|
74
|
+
The text of the problem has the element, the box, the two widths, the width of the view and the start of the text. For example:
|
|
75
|
+
|
|
76
|
+
```text
|
|
77
|
+
a table goes out of view at 360 px: table.api-table in div.table-scroll: content of 736 px in a box of 344 px, "Note". Make the table fit, give the box the class scroll-cue, or add data-overflow-ok to the box.
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
The rule for a box that scrolls examines only tables, for these reasons:
|
|
81
|
+
|
|
82
|
+
- A code block, a hex view, a row of tabs and a carousel are meant to scroll, and a visitor sees that they scroll.
|
|
83
|
+
- A table in a box that scrolls looks complete on a phone, with no scroll bar. The visitor does not see the last columns.
|
|
84
|
+
- The class `scroll-cue` shows a shade at the edge of the box, so a table in such a box does not count. The shade shows only when the cells have no background color of their own.
|
|
85
|
+
- A false alarm costs more than a miss, because each false alarm needs an exception in the site.
|
|
86
|
+
|
|
87
|
+
A table is `table`, `role="table"` or `role="grid"`. A desktop view has a scroll bar, so a table in a box that scrolls is not a problem there. `report.json` still shows the table in `measure.cut`.
|
|
88
|
+
|
|
89
|
+
These items do not count as cut text:
|
|
90
|
+
|
|
91
|
+
- Text in a box with `text-overflow: ellipsis`.
|
|
92
|
+
- Text for a screen reader: `.sr`, `.sr-only`, `.visually-hidden`, `.skip`, or an element of 1 px or less, with or without a clip.
|
|
93
|
+
- Hidden text: `display: none`, `visibility: hidden`, `opacity: 0` or `aria-hidden="true"`.
|
|
94
|
+
- A decorative element with no text, and text in an `svg`.
|
|
95
|
+
- An overflow of 1 px or less. This tolerance is for rounding.
|
|
96
|
+
|
|
97
|
+
Vertical overflow does not count, because an accordion and a list with a line clamp use it on purpose.
|
|
98
|
+
|
|
99
|
+
The attribute `data-overflow-ok` marks a box that is meant to scroll or to clip. Put it on the box or on an element around the box:
|
|
100
|
+
|
|
101
|
+
```html
|
|
102
|
+
<div class="result-scroll" data-overflow-ok><table>...</table></div>
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Use the attribute for a data table with many columns, a marquee or a slider. For an API table, the better fix is a layout that fits the phone.
|
|
106
|
+
|
|
107
|
+
A passing check proves what it asserts, not the page. Read the screenshots at full size before a merge. When a read finds a fault that a script can find, add it to this check.
|
|
108
|
+
|
|
72
109
|
The probe is a JavaScript file or a TypeScript file (`.ts`, `.mts` or `.cts`). Its code is the body of an async function. These functions are available:
|
|
73
110
|
|
|
74
111
|
| Function | Operation |
|
|
@@ -309,6 +346,7 @@ The entry `@mark1russell7/site-pages/chrome` has small helpers for the script of
|
|
|
309
346
|
| `currentTheme()`, `nextTheme(storageKey)`, `applyTheme(theme, storageKey)` | Read the theme, set the next theme, or set one theme. A site with its own button, for example a React component, uses them. |
|
|
310
347
|
| `copyText(button, text, options)` | Copies a text. The button shows "Copied" or "Copy failed" for 1600 ms. |
|
|
311
348
|
| `copyToClipboard(text)` | Copies a text with the Clipboard API, else with `document.execCommand`. It gives `true` after a copy. |
|
|
349
|
+
| `showBuild(element, build, repository)` | Adds "Version 1.2.3, build abc1234 of 2026-11-03" to an element. The commit is a link to GitHub. |
|
|
312
350
|
|
|
313
351
|
```ts
|
|
314
352
|
import { byId, copyText, h, setupTheme } from "@mark1russell7/site-pages/chrome";
|
|
@@ -319,7 +357,7 @@ copy.addEventListener("click", () => void copyText(copy, "pnpm add my-package"))
|
|
|
319
357
|
byId("install").append(copy);
|
|
320
358
|
```
|
|
321
359
|
|
|
322
|
-
The theme `auto` removes `data-theme`, and the CSS of the site then obeys `prefers-color-scheme`. Put this script in the `head` of the page, with the same key. It sets the theme before the first paint, so a dark page does not show white for a moment:
|
|
360
|
+
The theme `auto` removes `data-theme`, and the CSS of the site then obeys `prefers-color-scheme`. Put this script in the `head` of the page, with the same key. It sets the theme before the first paint, so a dark page does not show white for a moment. A site with the plugin `siteShell` does not need this script, because the plugin adds it:
|
|
323
361
|
|
|
324
362
|
```html
|
|
325
363
|
<script>
|
|
@@ -330,6 +368,86 @@ The theme `auto` removes `data-theme`, and the CSS of the site then obeys `prefe
|
|
|
330
368
|
</script>
|
|
331
369
|
```
|
|
332
370
|
|
|
371
|
+
## The shell of a site
|
|
372
|
+
|
|
373
|
+
The shell is the part of a page that is not its content. The package has two parts of the shell, and each part is optional.
|
|
374
|
+
|
|
375
|
+
The entry `@mark1russell7/site-pages/vite` is for `vite.config.ts`. The plugin `siteShell` sets `base` from `SITE_BASE`, sets `build.target` to `es2022`, defines `__BUILD__`, and adds the theme script to the `head` of each HTML page.
|
|
376
|
+
|
|
377
|
+
```ts
|
|
378
|
+
import { defineConfig } from "vite";
|
|
379
|
+
import { siteShell } from "@mark1russell7/site-pages/vite";
|
|
380
|
+
|
|
381
|
+
export default defineConfig({
|
|
382
|
+
root: "site",
|
|
383
|
+
plugins: [siteShell({ themeKey: "my-site-theme" })],
|
|
384
|
+
});
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
```ts
|
|
388
|
+
import { byId, setupTheme, showBuild, type BuildInfo } from "@mark1russell7/site-pages/chrome";
|
|
389
|
+
|
|
390
|
+
declare const __BUILD__: BuildInfo;
|
|
391
|
+
|
|
392
|
+
setupTheme(byId<HTMLButtonElement>("theme"), "my-site-theme");
|
|
393
|
+
showBuild(byId("build"), __BUILD__, "owner/my-site");
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
| Export of `./vite` | Operation |
|
|
397
|
+
| --- | --- |
|
|
398
|
+
| `siteShell({ themeKey, packageFile })` | Makes the plugin. A `base` or a `build.target` in the config of the site has priority. |
|
|
399
|
+
| `themeScript(storageKey)` | Gives the text of the theme script, for a page without the plugin. |
|
|
400
|
+
| `siteBase(env)` | Gives `SITE_BASE` with one `/` at the end, or `/`. |
|
|
401
|
+
| `buildInfo(version, env, now)` | Gives the data of `__BUILD__`: the short commit, the date and the version. |
|
|
402
|
+
|
|
403
|
+
The version comes from `../package.json`, relative to the folder of the site. Set `packageFile` to another file, or to `false` for a site with no version.
|
|
404
|
+
|
|
405
|
+
The file `@mark1russell7/site-pages/shell.css` has the tokens and the rules of the shell. The rules are for the document, the skip link, the header, the links, the theme button, the main column and the footer. Import it before the style sheet of the site, so the rules of the site come last:
|
|
406
|
+
|
|
407
|
+
```ts
|
|
408
|
+
import "@mark1russell7/site-pages/shell.css";
|
|
409
|
+
import "./style.css";
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
The file sets no look for the content. Buttons, cards, code and tables stay in the site. The header, the navigation, the footer text and each widget also stay in the site, because each site has its own words there.
|
|
413
|
+
|
|
414
|
+
A site with its own look does not import `shell.css`. It uses only the plugin and `showBuild`, or nothing from this section.
|
|
415
|
+
|
|
416
|
+
## The response explorer
|
|
417
|
+
|
|
418
|
+
The entry `@mark1russell7/site-pages/explorer` shows a measured response in the browser. A visitor chooses a measure, varies one or two dimensions, and holds or facets the others. The view shows the measured points, the fitted curve or surface, and its held-out error in plain words. The record has the format `response-records/1` of `@mark1russell7/rigor-statistics`.
|
|
419
|
+
|
|
420
|
+
```ts
|
|
421
|
+
import { mountExplorer } from "@mark1russell7/site-pages/explorer";
|
|
422
|
+
import "@mark1russell7/site-pages/explorer.css";
|
|
423
|
+
import record from "./record.json";
|
|
424
|
+
|
|
425
|
+
mountExplorer(document.getElementById("chart")!, record, { initial: { x: "boxes" } });
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
| Argument | Use |
|
|
429
|
+
| --- | --- |
|
|
430
|
+
| `host` | The element for the explorer |
|
|
431
|
+
| `recordJson` | The record, as JSON text or as parsed data |
|
|
432
|
+
| `options.initial` | The first measure, axes, panels and held values. The explorer corrects a value that does not apply. |
|
|
433
|
+
| `options.budgetMs` | The time limit of one surface, in milliseconds. The default is 50. |
|
|
434
|
+
| `options.now` | The clock, for tests |
|
|
435
|
+
|
|
436
|
+
`mountExplorer` gives a handle: `state()` reads the current state, and `destroy()` removes the view.
|
|
437
|
+
|
|
438
|
+
The explorer obeys these rules:
|
|
439
|
+
|
|
440
|
+
- **A bad record.** The explorer reads a record only through `parseResponseRecord`. It shows each fault with its path, and it does not throw.
|
|
441
|
+
- **Conditions.** A dimension with `when` has its parent at a level inside `when`. The controls disable a choice that a view refuses, and they tell why.
|
|
442
|
+
- **The band.** The band shows `low` to `high`. It fades with the distance from the nearest measured point.
|
|
443
|
+
- **The time of a surface.** The field `nearest` reads each point at each place. So `planSteps` bounds the grid from the point count before the call. After a surface that passes the budget, `nextStepsCap` makes the next grid smaller.
|
|
444
|
+
- **A table model.** A table model has no smooth surface, so the view shows cells.
|
|
445
|
+
- **No model.** A measure with no valid model shows only the points, and the view says so.
|
|
446
|
+
|
|
447
|
+
The view draws SVG, and it needs no framework, no font and no script from another server. It reads the tokens of a site when they exist, and it has its own colors for the light and the dark scheme. The keyboard reaches each control, and the arrow keys move a cursor on a chart. "Show the numbers" opens a table of the values.
|
|
448
|
+
|
|
449
|
+
The entry also exports the pure core, for a site with its own view. These functions are `settle`, `viewOf`, `axisChoices`, `facetChoices`, `holdControls`, `setMeasure`, `setAxis`, `setFacet`, `setHold`, `planSteps`, `nextStepsCap`, `openRecord` and `numberTable`.
|
|
450
|
+
|
|
333
451
|
## A GitHub Pages workflow
|
|
334
452
|
|
|
335
453
|
This workflow builds the site, adds the two pages, and deploys. It also starts every 3 hours, so the status page stays current without a push. A scheduled run builds the newest commit of `main` that passed CI.
|
package/build/chrome.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The small helpers of a page in the browser: the element helper, the element lookup, the theme
|
|
3
|
-
* button and the
|
|
4
|
-
* Import it from `@mark1russell7/site-pages/chrome`, because the package root also has
|
|
3
|
+
* button, the copy button and the build stamp. This module imports no Node.js module, so a site can
|
|
4
|
+
* bundle it. Import it from `@mark1russell7/site-pages/chrome`, because the package root also has
|
|
5
|
+
* Node.js code.
|
|
5
6
|
*/
|
|
6
7
|
/** A child of `h`: a node or a text. The values `null`, `undefined` and `false` give no child. */
|
|
7
8
|
export type Child = Node | string | null | undefined | false;
|
|
@@ -38,9 +39,27 @@ export declare function nextTheme(storageKey: string): Theme;
|
|
|
38
39
|
* "Theme: auto", "Theme: light" or "Theme: dark", and its `aria-label` tells the same theme.
|
|
39
40
|
*
|
|
40
41
|
* @remarks Before the first paint, a script in the `head` of the page reads `storageKey` from
|
|
41
|
-
* `localStorage` and sets `data-theme`. Thus a dark page does not show white for a moment.
|
|
42
|
+
* `localStorage` and sets `data-theme`. Thus a dark page does not show white for a moment. The
|
|
43
|
+
* plugin `siteShell` of the entry `./vite` adds that script.
|
|
42
44
|
*/
|
|
43
45
|
export declare function setupTheme(button: HTMLButtonElement, storageKey: string): void;
|
|
46
|
+
/** The data of a build: `siteShell` writes it, and `showBuild` shows it. */
|
|
47
|
+
export interface BuildInfo {
|
|
48
|
+
/** The first 7 characters of the commit, or `local` for a build outside CI. */
|
|
49
|
+
readonly sha: string;
|
|
50
|
+
/** The date of the build, as `YYYY-MM-DD`. */
|
|
51
|
+
readonly date: string;
|
|
52
|
+
/** The version of the package. A site without a version does not set it. */
|
|
53
|
+
readonly version?: string | undefined;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* This function shows the version and the build of the site in an element. The text is "Version
|
|
57
|
+
* 1.2.3, build abc1234 of 2026-11-03", and the commit is a link to GitHub. A build with no version
|
|
58
|
+
* starts with "Build", and a local build has no link.
|
|
59
|
+
*
|
|
60
|
+
* @param repository - The repository as "owner/name".
|
|
61
|
+
*/
|
|
62
|
+
export declare function showBuild(element: HTMLElement, build: BuildInfo, repository: string): void;
|
|
44
63
|
/**
|
|
45
64
|
* This function copies a text to the clipboard, and gives `true` when the copy is done. It uses
|
|
46
65
|
* the Clipboard API. When that API is not available or refuses, it uses `document.execCommand`.
|
package/build/chrome.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"chrome.d.ts","names":[],"sources":["../src/chrome.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"chrome.d.ts","names":[],"sources":["../src/chrome.ts"],"mappings":";;;;;;;YASY,QAAQ;;YAGR;;;;;wBAMI,EAAE,gBAAgB,uBAChC,KAAK,GACL,QAAO,SAAS,eAAe,qBAC5B,oBAAoB,iBAAiB,aACvC,sBAAsB;;;;;wBAkBT,KAAK,UAAU,cAAc,aAAa,aAAa;;qBAO1D;;YAGD,gBAAgB;;;;;wBAMZ,gBAAgB;;;;;wBAShB,WAAW,OAAO,OAAO;;wBAazB,UAAU,qBAAqB;;;;;;;;;wBAc/B,WAAW,QAAQ,mBAAmB;;iBAUrC;;WAEN;;WAEA;;WAEA;;;;;;;;;wBAUK,UAAU,SAAS,aAAa,OAAO,WAAW;;;;;wBAS5C,gBAAgB,eAAe;;iBA0BpC;;WAEN;;WAEA;;WAEA;;;;;;wBAOW,SAAS,QAAQ,mBAAmB,cAAc,UAAS,kBAAuB"}
|
package/build/chrome.js
CHANGED
|
@@ -63,7 +63,8 @@ function nextTheme(storageKey) {
|
|
|
63
63
|
* "Theme: auto", "Theme: light" or "Theme: dark", and its `aria-label` tells the same theme.
|
|
64
64
|
*
|
|
65
65
|
* @remarks Before the first paint, a script in the `head` of the page reads `storageKey` from
|
|
66
|
-
* `localStorage` and sets `data-theme`. Thus a dark page does not show white for a moment.
|
|
66
|
+
* `localStorage` and sets `data-theme`. Thus a dark page does not show white for a moment. The
|
|
67
|
+
* plugin `siteShell` of the entry `./vite` adds that script.
|
|
67
68
|
*/
|
|
68
69
|
function setupTheme(button, storageKey) {
|
|
69
70
|
const show = (theme) => {
|
|
@@ -74,6 +75,17 @@ function setupTheme(button, storageKey) {
|
|
|
74
75
|
button.addEventListener("click", () => show(nextTheme(storageKey)));
|
|
75
76
|
}
|
|
76
77
|
/**
|
|
78
|
+
* This function shows the version and the build of the site in an element. The text is "Version
|
|
79
|
+
* 1.2.3, build abc1234 of 2026-11-03", and the commit is a link to GitHub. A build with no version
|
|
80
|
+
* starts with "Build", and a local build has no link.
|
|
81
|
+
*
|
|
82
|
+
* @param repository - The repository as "owner/name".
|
|
83
|
+
*/
|
|
84
|
+
function showBuild(element, build, repository) {
|
|
85
|
+
const commit = build.sha === "local" ? "local" : h("a", { href: `https://github.com/${repository}/commit/${build.sha}` }, build.sha);
|
|
86
|
+
element.append(build.version === void 0 ? "Build " : `Version ${build.version}, build `, commit, ` of ${build.date}`);
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
77
89
|
* This function copies a text to the clipboard, and gives `true` when the copy is done. It uses
|
|
78
90
|
* the Clipboard API. When that API is not available or refuses, it uses `document.execCommand`.
|
|
79
91
|
*/
|
|
@@ -112,6 +124,6 @@ async function copyText(button, text, options = {}) {
|
|
|
112
124
|
}, options.ms ?? 1600);
|
|
113
125
|
return ok;
|
|
114
126
|
}
|
|
115
|
-
export { THEMES, applyTheme, byId, copyText, copyToClipboard, currentTheme, h, nextTheme, setupTheme };
|
|
127
|
+
export { THEMES, applyTheme, byId, copyText, copyToClipboard, currentTheme, h, nextTheme, setupTheme, showBuild };
|
|
116
128
|
|
|
117
129
|
//# sourceMappingURL=chrome.js.map
|
package/build/chrome.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"chrome.js","names":[],"sources":["../src/chrome.ts"],"sourcesContent":["/// <reference lib=\"dom\" />\n/**\n * The small helpers of a page in the browser: the element helper, the element lookup, the theme\n * button and the
|
|
1
|
+
{"version":3,"file":"chrome.js","names":[],"sources":["../src/chrome.ts"],"sourcesContent":["/// <reference lib=\"dom\" />\n/**\n * The small helpers of a page in the browser: the element helper, the element lookup, the theme\n * button, the copy button and the build stamp. This module imports no Node.js module, so a site can\n * bundle it. Import it from `@mark1russell7/site-pages/chrome`, because the package root also has\n * Node.js code.\n */\n\n/** A child of `h`: a node or a text. The values `null`, `undefined` and `false` give no child. */\nexport type Child = Node | string | null | undefined | false;\n\n/** The value of an attribute of `h`. The value `true` gives an empty attribute. */\nexport type AttributeValue = string | number | boolean | null | undefined;\n\n/**\n * This function makes an element with attributes and children. An attribute with the value\n * `false`, `null` or `undefined` is not set. The attribute `class` sets the class name.\n */\nexport function h<K extends keyof HTMLElementTagNameMap>(\n tag: K,\n attrs: Readonly<Record<string, AttributeValue>> = {},\n ...children: readonly (Child | readonly Child[])[]\n): HTMLElementTagNameMap[K] {\n const el = document.createElement(tag);\n for (const [name, value] of Object.entries(attrs)) {\n if (value === false || value === null || value === undefined) continue;\n if (name === \"class\") el.className = String(value);\n else el.setAttribute(name, value === true ? \"\" : String(value));\n }\n for (const child of children.flat()) {\n if (child === null || child === undefined || child === false) continue;\n el.append(child);\n }\n return el;\n}\n\n/**\n * This function gives the element with the ID. The page has each ID that the code asks for. Thus\n * a missing ID is an error of the page, and the function throws an `Error`.\n */\nexport function byId<T extends HTMLElement = HTMLElement>(id: string): T {\n const el = document.getElementById(id);\n if (el === null) throw new Error(`The page has no element \"${id}\".`);\n return el as T;\n}\n\n/** The themes of the theme button, in the sequence of the clicks. `auto` follows the setting of the system. */\nexport const THEMES: readonly [\"auto\", \"light\", \"dark\"] = [\"auto\", \"light\", \"dark\"];\n\n/** A theme of the page. */\nexport type Theme = (typeof THEMES)[number];\n\n/**\n * This function gives the theme of the page from the attribute `data-theme` of the `html` element.\n * Without the attribute, or with another value, the theme is `auto`.\n */\nexport function currentTheme(): Theme {\n const theme = document.documentElement.dataset[\"theme\"];\n return theme === \"light\" || theme === \"dark\" ? theme : \"auto\";\n}\n\n/**\n * This function sets the theme of the page, and keeps it in `localStorage` under `storageKey`.\n * The theme `auto` removes the attribute `data-theme` and the stored value.\n */\nexport function applyTheme(theme: Theme, storageKey: string): void {\n const root = document.documentElement;\n if (theme === \"auto\") delete root.dataset[\"theme\"];\n else root.dataset[\"theme\"] = theme;\n try {\n if (theme === \"auto\") localStorage.removeItem(storageKey);\n else localStorage.setItem(storageKey, theme);\n } catch {\n // A browser with no storage keeps the theme until the page closes.\n }\n}\n\n/** This function sets the next theme after the current theme (auto, light, dark, auto), and gives it. */\nexport function nextTheme(storageKey: string): Theme {\n const next = THEMES[(THEMES.indexOf(currentTheme()) + 1) % THEMES.length] ?? \"auto\";\n applyTheme(next, storageKey);\n return next;\n}\n\n/**\n * This function makes a theme button. Each click sets the next theme. The text of the button is\n * \"Theme: auto\", \"Theme: light\" or \"Theme: dark\", and its `aria-label` tells the same theme.\n *\n * @remarks Before the first paint, a script in the `head` of the page reads `storageKey` from\n * `localStorage` and sets `data-theme`. Thus a dark page does not show white for a moment. The\n * plugin `siteShell` of the entry `./vite` adds that script.\n */\nexport function setupTheme(button: HTMLButtonElement, storageKey: string): void {\n const show = (theme: Theme): void => {\n button.textContent = `Theme: ${theme}`;\n button.setAttribute(\"aria-label\", `Color theme: ${theme}. Select to change.`);\n };\n show(currentTheme());\n button.addEventListener(\"click\", () => show(nextTheme(storageKey)));\n}\n\n/** The data of a build: `siteShell` writes it, and `showBuild` shows it. */\nexport interface BuildInfo {\n /** The first 7 characters of the commit, or `local` for a build outside CI. */\n readonly sha: string;\n /** The date of the build, as `YYYY-MM-DD`. */\n readonly date: string;\n /** The version of the package. A site without a version does not set it. */\n readonly version?: string | undefined;\n}\n\n/**\n * This function shows the version and the build of the site in an element. The text is \"Version\n * 1.2.3, build abc1234 of 2026-11-03\", and the commit is a link to GitHub. A build with no version\n * starts with \"Build\", and a local build has no link.\n *\n * @param repository - The repository as \"owner/name\".\n */\nexport function showBuild(element: HTMLElement, build: BuildInfo, repository: string): void {\n const commit = build.sha === \"local\" ? \"local\" : h(\"a\", { href: `https://github.com/${repository}/commit/${build.sha}` }, build.sha);\n element.append(build.version === undefined ? \"Build \" : `Version ${build.version}, build `, commit, ` of ${build.date}`);\n}\n\n/**\n * This function copies a text to the clipboard, and gives `true` when the copy is done. It uses\n * the Clipboard API. When that API is not available or refuses, it uses `document.execCommand`.\n */\nexport async function copyToClipboard(text: string): Promise<boolean> {\n try {\n await navigator.clipboard.writeText(text);\n return true;\n } catch {\n // An insecure page has no Clipboard API, and a browser can refuse the copy. The old command is the fallback.\n }\n try {\n const area = document.createElement(\"textarea\");\n area.value = text;\n area.setAttribute(\"readonly\", \"\");\n area.style.position = \"fixed\";\n area.style.opacity = \"0\";\n document.body.append(area);\n area.select();\n try {\n return document.execCommand(\"copy\");\n } finally {\n area.remove();\n }\n } catch {\n return false;\n }\n}\n\n/** The options of `copyText`. */\nexport interface CopyTextOptions {\n /** The text of the button after a copy. The default is \"Copied\". */\n readonly copied?: string;\n /** The text of the button after a failure. The default is \"Copy failed\". */\n readonly failed?: string;\n /** The time in milliseconds until the button shows its label again. The default is 1600. */\n readonly ms?: number;\n}\n\n/**\n * This function copies a text to the clipboard with `copyToClipboard`. The button tells the result\n * for a short time, then it shows its label again. The function gives the result of the copy.\n */\nexport async function copyText(button: HTMLButtonElement, text: string, options: CopyTextOptions = {}): Promise<boolean> {\n const label = button.textContent;\n const ok = await copyToClipboard(text);\n button.textContent = ok ? (options.copied ?? \"Copied\") : (options.failed ?? \"Copy failed\");\n window.setTimeout(() => {\n button.textContent = label;\n }, options.ms ?? 1600);\n return ok;\n}\n"],"mappings":";;;;;AAkBA,SAAgB,EACd,KACA,QAAkD,CAAC,GACnD,GAAG,UACuB;CAC1B,MAAM,KAAK,SAAS,cAAc,GAAG;CACrC,KAAK,MAAM,CAAC,MAAM,UAAU,OAAO,QAAQ,KAAK,GAAG;EACjD,IAAI,UAAU,SAAS,UAAU,QAAQ,UAAU,KAAA,GAAW;EAC9D,IAAI,SAAS,SAAS,GAAG,YAAY,OAAO,KAAK;OAC5C,GAAG,aAAa,MAAM,UAAU,OAAO,KAAK,OAAO,KAAK,CAAC;CAChE;CACA,KAAK,MAAM,SAAS,SAAS,KAAK,GAAG;EACnC,IAAI,UAAU,QAAQ,UAAU,KAAA,KAAa,UAAU,OAAO;EAC9D,GAAG,OAAO,KAAK;CACjB;CACA,OAAO;AACT;;;;;AAMA,SAAgB,KAA0C,IAAe;CACvE,MAAM,KAAK,SAAS,eAAe,EAAE;CACrC,IAAI,OAAO,MAAM,MAAM,IAAI,MAAM,4BAA4B,GAAG,GAAG;CACnE,OAAO;AACT;;AAGA,MAAa,SAA6C;CAAC;CAAQ;CAAS;AAAM;;;;;AASlF,SAAgB,eAAsB;CACpC,MAAM,QAAQ,SAAS,gBAAgB,QAAQ;CAC/C,OAAO,UAAU,WAAW,UAAU,SAAS,QAAQ;AACzD;;;;;AAMA,SAAgB,WAAW,OAAc,YAA0B;CACjE,MAAM,OAAO,SAAS;CACtB,IAAI,UAAU,QAAQ,OAAO,KAAK,QAAQ;MACrC,KAAK,QAAQ,WAAW;CAC7B,IAAI;EACF,IAAI,UAAU,QAAQ,aAAa,WAAW,UAAU;OACnD,aAAa,QAAQ,YAAY,KAAK;CAC7C,QAAQ,CAER;AACF;;AAGA,SAAgB,UAAU,YAA2B;CACnD,MAAM,OAAO,QAAQ,OAAO,QAAQ,aAAa,CAAC,IAAI,KAAK,OAAO,WAAW;CAC7E,WAAW,MAAM,UAAU;CAC3B,OAAO;AACT;;;;;;;;;AAUA,SAAgB,WAAW,QAA2B,YAA0B;CAC9E,MAAM,QAAQ,UAAuB;EACnC,OAAO,cAAc,UAAU;EAC/B,OAAO,aAAa,cAAc,gBAAgB,MAAM,oBAAoB;CAC9E;CACA,KAAK,aAAa,CAAC;CACnB,OAAO,iBAAiB,eAAe,KAAK,UAAU,UAAU,CAAC,CAAC;AACpE;;;;;;;;AAmBA,SAAgB,UAAU,SAAsB,OAAkB,YAA0B;CAC1F,MAAM,SAAS,MAAM,QAAQ,UAAU,UAAU,EAAE,KAAK,EAAE,MAAM,sBAAsB,WAAW,UAAU,MAAM,MAAM,GAAG,MAAM,GAAG;CACnI,QAAQ,OAAO,MAAM,YAAY,KAAA,IAAY,WAAW,WAAW,MAAM,QAAQ,WAAW,QAAQ,OAAO,MAAM,MAAM;AACzH;;;;;AAMA,eAAsB,gBAAgB,MAAgC;CACpE,IAAI;EACF,MAAM,UAAU,UAAU,UAAU,IAAI;EACxC,OAAO;CACT,QAAQ,CAER;CACA,IAAI;EACF,MAAM,OAAO,SAAS,cAAc,UAAU;EAC9C,KAAK,QAAQ;EACb,KAAK,aAAa,YAAY,EAAE;EAChC,KAAK,MAAM,WAAW;EACtB,KAAK,MAAM,UAAU;EACrB,SAAS,KAAK,OAAO,IAAI;EACzB,KAAK,OAAO;EACZ,IAAI;GACF,OAAO,SAAS,YAAY,MAAM;EACpC,UAAU;GACR,KAAK,OAAO;EACd;CACF,QAAQ;EACN,OAAO;CACT;AACF;;;;;AAgBA,eAAsB,SAAS,QAA2B,MAAc,UAA2B,CAAC,GAAqB;CACvH,MAAM,QAAQ,OAAO;CACrB,MAAM,KAAK,MAAM,gBAAgB,IAAI;CACrC,OAAO,cAAc,KAAM,QAAQ,UAAU,WAAa,QAAQ,UAAU;CAC5E,OAAO,iBAAiB;EACtB,OAAO,cAAc;CACvB,GAAG,QAAQ,MAAM,IAAI;CACrB,OAAO;AACT"}
|
package/build/cli.js
CHANGED
|
@@ -199,6 +199,22 @@ async function connect(url) {
|
|
|
199
199
|
* - an element in `.sr`, `.sr-only`, `.visually-hidden` or `.skip` (text for screen readers and skip links),
|
|
200
200
|
* - an element in a `pre`, other than the `pre` itself.
|
|
201
201
|
* Of a chain of elements past the edge, only the outermost one counts.
|
|
202
|
+
*
|
|
203
|
+
* `past` skips each element in a container that scrolls or clips, so `cut` examines those containers.
|
|
204
|
+
* A box is the nearest element around a text, below `body`, with `overflow-x` other than `visible`. Its content is wider
|
|
205
|
+
* than the box by more than 1 px. An element with `overflow-x: clip` is always a box, because it can report no overflow.
|
|
206
|
+
* The text counts when a line of it ends more than 1 px past the right edge of the box.
|
|
207
|
+
*
|
|
208
|
+
* A box that scrolls counts only for text in a table (`table`, `role="table"` or `role="grid"`).
|
|
209
|
+
* A code block, a hex view, a row of tabs and a carousel in such a box are meant to scroll.
|
|
210
|
+
* A box that clips counts for each text, because a visitor cannot reach that text. These texts do not count:
|
|
211
|
+
* - a text in a box with `text-overflow` other than `clip` (a person chose the ellipsis),
|
|
212
|
+
* - a text in an element with `data-overflow-ok`, or in that element itself (a person chose the overflow),
|
|
213
|
+
* - a text in a box with the class `scroll-cue` that scrolls (a shade shows the visitor that it scrolls),
|
|
214
|
+
* - a text in `.sr`, `.sr-only`, `.visually-hidden`, `.skip`, `[aria-hidden="true"]`, `select`, `textarea` or `svg`,
|
|
215
|
+
* - a text in a fixed element, which the box does not clip,
|
|
216
|
+
* - a text that is not visible, or with an element around it of 1 px or less or with `opacity: 0`.
|
|
217
|
+
* Only the first text of a box counts. An element with no text does not count. Vertical overflow does not count.
|
|
202
218
|
*/
|
|
203
219
|
const MEASURE = `(() => {
|
|
204
220
|
const root = document.documentElement;
|
|
@@ -227,6 +243,46 @@ const MEASURE = `(() => {
|
|
|
227
243
|
const clipped = [...document.querySelectorAll("textarea")]
|
|
228
244
|
.filter((e) => { const s = getComputedStyle(e); return (s.overflowY === "hidden" || s.overflowY === "clip") && e.getBoundingClientRect().height > 0 && e.scrollHeight > e.clientHeight + 1; })
|
|
229
245
|
.map((e) => name(e) + " " + e.scrollHeight + ">" + e.clientHeight);
|
|
246
|
+
const boxOf = (e) => {
|
|
247
|
+
for (let p = e; p && p !== document.body && p !== root; p = p.parentElement) {
|
|
248
|
+
const s = getComputedStyle(p);
|
|
249
|
+
if (s.overflowX === "clip" || (s.overflowX !== "visible" && p.scrollWidth > p.clientWidth + 1)) return p;
|
|
250
|
+
if (s.position === "fixed") return undefined;
|
|
251
|
+
}
|
|
252
|
+
return undefined;
|
|
253
|
+
};
|
|
254
|
+
const shown = (e) => {
|
|
255
|
+
if (e.closest(".sr,.sr-only,.visually-hidden,.skip,[aria-hidden=true],select,textarea,svg")) return false;
|
|
256
|
+
if (getComputedStyle(e).visibility !== "visible") return false;
|
|
257
|
+
for (let p = e; p && p !== root; p = p.parentElement) {
|
|
258
|
+
const s = getComputedStyle(p);
|
|
259
|
+
if (s.opacity === "0") return false;
|
|
260
|
+
if (s.display === "contents") continue;
|
|
261
|
+
const r = p.getBoundingClientRect();
|
|
262
|
+
if (r.width <= 1 || r.height <= 1) return false;
|
|
263
|
+
}
|
|
264
|
+
return true;
|
|
265
|
+
};
|
|
266
|
+
const cuts = new Map();
|
|
267
|
+
const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT);
|
|
268
|
+
const range = document.createRange();
|
|
269
|
+
for (let t = walker.nextNode(); t; t = walker.nextNode()) {
|
|
270
|
+
const e = t.parentElement;
|
|
271
|
+
if (!e || !t.data.trim()) continue;
|
|
272
|
+
const box = boxOf(e);
|
|
273
|
+
if (!box || cuts.has(box)) continue;
|
|
274
|
+
const s = getComputedStyle(box);
|
|
275
|
+
if (s.textOverflow !== "clip") continue;
|
|
276
|
+
const scrolls = s.overflowX === "auto" || s.overflowX === "scroll";
|
|
277
|
+
const table = e.closest("table,[role=table],[role=grid]");
|
|
278
|
+
if (scrolls && !(table && box.contains(table))) continue;
|
|
279
|
+
const left = box.getBoundingClientRect().left + box.clientLeft;
|
|
280
|
+
range.selectNodeContents(t);
|
|
281
|
+
const end = Math.max(...[...range.getClientRects()].filter((r) => r.width > 0).map((r) => r.right));
|
|
282
|
+
if (end <= left + box.clientWidth + 1) continue;
|
|
283
|
+
if (e.closest("[data-overflow-ok]") || (scrolls && box.classList.contains("scroll-cue")) || !shown(e)) continue;
|
|
284
|
+
cuts.set(box, { kind: scrolls ? "table" : "clip", box: name(box), element: name(scrolls ? table : e), scrollWidth: Math.max(box.scrollWidth, Math.ceil(end - left)), clientWidth: box.clientWidth, text: t.data.trim().replace(/\\s+/g, " ").slice(0, 40) });
|
|
285
|
+
}
|
|
230
286
|
return {
|
|
231
287
|
innerWidth,
|
|
232
288
|
scrollWidth: root.scrollWidth,
|
|
@@ -234,6 +290,7 @@ const MEASURE = `(() => {
|
|
|
234
290
|
text: (document.body ? document.body.innerText : "").trim().length,
|
|
235
291
|
past: past.slice(0, 8),
|
|
236
292
|
clipped: clipped.slice(0, 8),
|
|
293
|
+
cut: [...cuts.values()].slice(0, 8),
|
|
237
294
|
};
|
|
238
295
|
})()`;
|
|
239
296
|
/**
|
|
@@ -360,17 +417,19 @@ async function serve(root, base) {
|
|
|
360
417
|
close: () => new Promise((done) => server.close(() => done()))
|
|
361
418
|
};
|
|
362
419
|
}
|
|
363
|
-
// src/check/check.ts
|
|
364
420
|
/**
|
|
365
|
-
*
|
|
366
|
-
*
|
|
367
|
-
*
|
|
368
|
-
* A view fails when the page is wider than the screen or an element goes past the right edge. It
|
|
369
|
-
* also fails when a text area hides its text or the page shows no text. A console error, an
|
|
370
|
-
* exception and a failed request of the site also fail it. A script that does not load and a failed check of the probe
|
|
371
|
-
* fail it too. The command
|
|
372
|
-
* writes a screenshot of each view.
|
|
421
|
+
* This function gives the problems of the cuts of a view with the given width. A box that clips text is a problem at each width.
|
|
422
|
+
* A table in a box that scrolls is a problem only on a phone. A phone shows no scroll bar, but a desktop shows one.
|
|
423
|
+
* The summary and the log give the page of each problem.
|
|
373
424
|
*/
|
|
425
|
+
function cutProblems(cuts, width) {
|
|
426
|
+
const widths = (cut) => `${cut.element} in ${cut.box}: content of ${cut.scrollWidth} px in a box of ${cut.clientWidth} px, "${cut.text}"`;
|
|
427
|
+
return cuts.flatMap((cut) => {
|
|
428
|
+
if (cut.kind === "clip") return [`a box cuts text at ${width} px: ${widths(cut)}. Make the content fit, or add data-overflow-ok to the box.`];
|
|
429
|
+
if (width < 768) return [`a table goes out of view at ${width} px: ${widths(cut)}. Make the table fit, give the box the class scroll-cue, or add data-overflow-ok to the box.`];
|
|
430
|
+
return [];
|
|
431
|
+
});
|
|
432
|
+
}
|
|
374
433
|
/** This function gives the file name part of a page: `/` is `home`, `/releases/` is `releases`. */
|
|
375
434
|
function pageSlug(page) {
|
|
376
435
|
const slug = page.replace(/^\/+|\/+$/g, "").replace(/[^A-Za-z0-9._-]+/g, "_");
|
|
@@ -517,6 +576,7 @@ async function checkView(page, events, input) {
|
|
|
517
576
|
if (measure.scrollWidth > width) view.problems.push(`the page is wider than the screen: ${measure.scrollWidth} > ${width}`);
|
|
518
577
|
for (const past of measure.past) view.problems.push(`past the right edge: ${past}`);
|
|
519
578
|
for (const clipped of measure.clipped) view.problems.push(`a text area hides its text: ${clipped}`);
|
|
579
|
+
view.problems.push(...cutProblems(measure.cut, width));
|
|
520
580
|
if (measure.text === 0) view.problems.push("the page shows no text");
|
|
521
581
|
if (options.shots) {
|
|
522
582
|
const file = join(options.shots, `${pageSlug(path)}-${width}-${scheme}.png`);
|
|
@@ -570,8 +630,10 @@ const CHECK_HELP = `Usage: site-pages check (--out <dir> | --url <url>) [options
|
|
|
570
630
|
|
|
571
631
|
Opens the pages of the site in a headless Chromium at phone and desktop widths, in the light and
|
|
572
632
|
the dark scheme, and fails when a page is wider than the screen, an element goes past the right
|
|
573
|
-
edge, a text area hides its text, a
|
|
574
|
-
|
|
633
|
+
edge, a text area hides its text, a box cuts text, a table goes out of view on a phone, a page
|
|
634
|
+
shows no text, or on a console error, an exception, a failed request of the site, a script that
|
|
635
|
+
does not load or a failed check of the probe. Add data-overflow-ok to a box that is meant to
|
|
636
|
+
scroll or to clip.
|
|
575
637
|
|
|
576
638
|
Options:
|
|
577
639
|
--out <dir> The built site folder. The check serves it at --base.
|