@mark1russell7/site-pages 0.1.9 → 0.1.10

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 CHANGED
@@ -297,6 +297,39 @@ The class sets `overflow-x: auto` and the background color of the box. Set `--sc
297
297
  }
298
298
  ```
299
299
 
300
+ ## The page helpers
301
+
302
+ The entry `@mark1russell7/site-pages/chrome` has small helpers for the script of a page. It imports no Node.js module, so a site can bundle it for the browser. The package root does not export these helpers. Thus a Node.js program that imports the root gets no DOM types.
303
+
304
+ | Function | Operation |
305
+ | --- | --- |
306
+ | `h(tag, attrs, ...children)` | Makes an element. The attribute `class` sets the class name. `false`, `null` and `undefined` give no attribute and no child. |
307
+ | `byId(id)` | Gives the element with the ID. It throws when the page has no such element. |
308
+ | `setupTheme(button, storageKey)` | Makes a theme button: auto, light, dark, in turn. It sets `data-theme` on the `html` element and keeps the choice in `localStorage`. |
309
+ | `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
+ | `copyText(button, text, options)` | Copies a text. The button shows "Copied" or "Copy failed" for 1600 ms. |
311
+ | `copyToClipboard(text)` | Copies a text with the Clipboard API, else with `document.execCommand`. It gives `true` after a copy. |
312
+
313
+ ```ts
314
+ import { byId, copyText, h, setupTheme } from "@mark1russell7/site-pages/chrome";
315
+
316
+ setupTheme(byId<HTMLButtonElement>("theme"), "my-site-theme");
317
+ const copy = h("button", { type: "button", class: "copy" }, "Copy");
318
+ copy.addEventListener("click", () => void copyText(copy, "pnpm add my-package"));
319
+ byId("install").append(copy);
320
+ ```
321
+
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:
323
+
324
+ ```html
325
+ <script>
326
+ try {
327
+ const t = localStorage.getItem("my-site-theme");
328
+ if (t === "light" || t === "dark") document.documentElement.dataset.theme = t;
329
+ } catch {}
330
+ </script>
331
+ ```
332
+
300
333
  ## A GitHub Pages workflow
301
334
 
302
335
  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.
@@ -0,0 +1,63 @@
1
+ /**
2
+ * The small helpers of a page in the browser: the element helper, the element lookup, the theme
3
+ * button and the copy button. This module imports no Node.js module, so a site can bundle it.
4
+ * Import it from `@mark1russell7/site-pages/chrome`, because the package root also has Node.js code.
5
+ */
6
+ /** A child of `h`: a node or a text. The values `null`, `undefined` and `false` give no child. */
7
+ export type Child = Node | string | null | undefined | false;
8
+ /** The value of an attribute of `h`. The value `true` gives an empty attribute. */
9
+ export type AttributeValue = string | number | boolean | null | undefined;
10
+ /**
11
+ * This function makes an element with attributes and children. An attribute with the value
12
+ * `false`, `null` or `undefined` is not set. The attribute `class` sets the class name.
13
+ */
14
+ export declare function h<K extends keyof HTMLElementTagNameMap>(tag: K, attrs?: Readonly<Record<string, AttributeValue>>, ...children: readonly (Child | readonly Child[])[]): HTMLElementTagNameMap[K];
15
+ /**
16
+ * This function gives the element with the ID. The page has each ID that the code asks for. Thus
17
+ * a missing ID is an error of the page, and the function throws an `Error`.
18
+ */
19
+ export declare function byId<T extends HTMLElement = HTMLElement>(id: string): T;
20
+ /** The themes of the theme button, in the sequence of the clicks. `auto` follows the setting of the system. */
21
+ export declare const THEMES: readonly ["auto", "light", "dark"];
22
+ /** A theme of the page. */
23
+ export type Theme = (typeof THEMES)[number];
24
+ /**
25
+ * This function gives the theme of the page from the attribute `data-theme` of the `html` element.
26
+ * Without the attribute, or with another value, the theme is `auto`.
27
+ */
28
+ export declare function currentTheme(): Theme;
29
+ /**
30
+ * This function sets the theme of the page, and keeps it in `localStorage` under `storageKey`.
31
+ * The theme `auto` removes the attribute `data-theme` and the stored value.
32
+ */
33
+ export declare function applyTheme(theme: Theme, storageKey: string): void;
34
+ /** This function sets the next theme after the current theme (auto, light, dark, auto), and gives it. */
35
+ export declare function nextTheme(storageKey: string): Theme;
36
+ /**
37
+ * This function makes a theme button. Each click sets the next theme. The text of the button is
38
+ * "Theme: auto", "Theme: light" or "Theme: dark", and its `aria-label` tells the same theme.
39
+ *
40
+ * @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
+ */
43
+ export declare function setupTheme(button: HTMLButtonElement, storageKey: string): void;
44
+ /**
45
+ * This function copies a text to the clipboard, and gives `true` when the copy is done. It uses
46
+ * the Clipboard API. When that API is not available or refuses, it uses `document.execCommand`.
47
+ */
48
+ export declare function copyToClipboard(text: string): Promise<boolean>;
49
+ /** The options of `copyText`. */
50
+ export interface CopyTextOptions {
51
+ /** The text of the button after a copy. The default is "Copied". */
52
+ readonly copied?: string;
53
+ /** The text of the button after a failure. The default is "Copy failed". */
54
+ readonly failed?: string;
55
+ /** The time in milliseconds until the button shows its label again. The default is 1600. */
56
+ readonly ms?: number;
57
+ }
58
+ /**
59
+ * This function copies a text to the clipboard with `copyToClipboard`. The button tells the result
60
+ * for a short time, then it shows its label again. The function gives the result of the copy.
61
+ */
62
+ export declare function copyText(button: HTMLButtonElement, text: string, options?: CopyTextOptions): Promise<boolean>;
63
+ //# sourceMappingURL=chrome.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"chrome.d.ts","names":[],"sources":["../src/chrome.ts"],"mappings":";;;;;;YAQY,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;;;;;;;;wBAa/B,WAAW,QAAQ,mBAAmB;;;;;wBAahC,gBAAgB,eAAe;;iBA0BpC;;WAEN;;WAEA;;WAEA;;;;;;wBAOW,SAAS,QAAQ,mBAAmB,cAAc,UAAS,kBAAuB"}
@@ -0,0 +1,117 @@
1
+ // src/chrome.ts
2
+ /**
3
+ * This function makes an element with attributes and children. An attribute with the value
4
+ * `false`, `null` or `undefined` is not set. The attribute `class` sets the class name.
5
+ */
6
+ function h(tag, attrs = {}, ...children) {
7
+ const el = document.createElement(tag);
8
+ for (const [name, value] of Object.entries(attrs)) {
9
+ if (value === false || value === null || value === void 0) continue;
10
+ if (name === "class") el.className = String(value);
11
+ else el.setAttribute(name, value === true ? "" : String(value));
12
+ }
13
+ for (const child of children.flat()) {
14
+ if (child === null || child === void 0 || child === false) continue;
15
+ el.append(child);
16
+ }
17
+ return el;
18
+ }
19
+ /**
20
+ * This function gives the element with the ID. The page has each ID that the code asks for. Thus
21
+ * a missing ID is an error of the page, and the function throws an `Error`.
22
+ */
23
+ function byId(id) {
24
+ const el = document.getElementById(id);
25
+ if (el === null) throw new Error(`The page has no element "${id}".`);
26
+ return el;
27
+ }
28
+ /** The themes of the theme button, in the sequence of the clicks. `auto` follows the setting of the system. */
29
+ const THEMES = [
30
+ "auto",
31
+ "light",
32
+ "dark"
33
+ ];
34
+ /**
35
+ * This function gives the theme of the page from the attribute `data-theme` of the `html` element.
36
+ * Without the attribute, or with another value, the theme is `auto`.
37
+ */
38
+ function currentTheme() {
39
+ const theme = document.documentElement.dataset["theme"];
40
+ return theme === "light" || theme === "dark" ? theme : "auto";
41
+ }
42
+ /**
43
+ * This function sets the theme of the page, and keeps it in `localStorage` under `storageKey`.
44
+ * The theme `auto` removes the attribute `data-theme` and the stored value.
45
+ */
46
+ function applyTheme(theme, storageKey) {
47
+ const root = document.documentElement;
48
+ if (theme === "auto") delete root.dataset["theme"];
49
+ else root.dataset["theme"] = theme;
50
+ try {
51
+ if (theme === "auto") localStorage.removeItem(storageKey);
52
+ else localStorage.setItem(storageKey, theme);
53
+ } catch {}
54
+ }
55
+ /** This function sets the next theme after the current theme (auto, light, dark, auto), and gives it. */
56
+ function nextTheme(storageKey) {
57
+ const next = THEMES[(THEMES.indexOf(currentTheme()) + 1) % THEMES.length] ?? "auto";
58
+ applyTheme(next, storageKey);
59
+ return next;
60
+ }
61
+ /**
62
+ * This function makes a theme button. Each click sets the next theme. The text of the button is
63
+ * "Theme: auto", "Theme: light" or "Theme: dark", and its `aria-label` tells the same theme.
64
+ *
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.
67
+ */
68
+ function setupTheme(button, storageKey) {
69
+ const show = (theme) => {
70
+ button.textContent = `Theme: ${theme}`;
71
+ button.setAttribute("aria-label", `Color theme: ${theme}. Select to change.`);
72
+ };
73
+ show(currentTheme());
74
+ button.addEventListener("click", () => show(nextTheme(storageKey)));
75
+ }
76
+ /**
77
+ * This function copies a text to the clipboard, and gives `true` when the copy is done. It uses
78
+ * the Clipboard API. When that API is not available or refuses, it uses `document.execCommand`.
79
+ */
80
+ async function copyToClipboard(text) {
81
+ try {
82
+ await navigator.clipboard.writeText(text);
83
+ return true;
84
+ } catch {}
85
+ try {
86
+ const area = document.createElement("textarea");
87
+ area.value = text;
88
+ area.setAttribute("readonly", "");
89
+ area.style.position = "fixed";
90
+ area.style.opacity = "0";
91
+ document.body.append(area);
92
+ area.select();
93
+ try {
94
+ return document.execCommand("copy");
95
+ } finally {
96
+ area.remove();
97
+ }
98
+ } catch {
99
+ return false;
100
+ }
101
+ }
102
+ /**
103
+ * This function copies a text to the clipboard with `copyToClipboard`. The button tells the result
104
+ * for a short time, then it shows its label again. The function gives the result of the copy.
105
+ */
106
+ async function copyText(button, text, options = {}) {
107
+ const label = button.textContent;
108
+ const ok = await copyToClipboard(text);
109
+ button.textContent = ok ? options.copied ?? "Copied" : options.failed ?? "Copy failed";
110
+ window.setTimeout(() => {
111
+ button.textContent = label;
112
+ }, options.ms ?? 1600);
113
+ return ok;
114
+ }
115
+ export { THEMES, applyTheme, byId, copyText, copyToClipboard, currentTheme, h, nextTheme, setupTheme };
116
+
117
+ //# sourceMappingURL=chrome.js.map
@@ -0,0 +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 copy button. This module imports no Node.js module, so a site can bundle it.\n * Import it from `@mark1russell7/site-pages/chrome`, because the package root also has 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.\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/**\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":";;;;;AAiBA,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;;;;;;;;AASA,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;;;;;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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package",
3
3
  "name": "@mark1russell7/site-pages",
4
- "version": "0.1.9",
4
+ "version": "0.1.10",
5
5
  "description": "A release timeline page and a live status page for the site of a GitHub repository: JSON data and standalone HTML, made at deploy time.",
6
6
  "keywords": [
7
7
  "github-pages",
@@ -31,6 +31,10 @@
31
31
  "types": "./build/index.d.ts",
32
32
  "import": "./build/index.js"
33
33
  },
34
+ "./chrome": {
35
+ "types": "./build/chrome.d.ts",
36
+ "import": "./build/chrome.js"
37
+ },
34
38
  "./scroll-cue.css": "./styles/scroll-cue.css",
35
39
  "./package.json": "./package.json"
36
40
  },