@rathnasgala2/template 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +515 -0
- package/contracts/render-policy.jcs +1 -0
- package/contracts/theme-styling-contract.jcs +1 -0
- package/package.json +96 -0
- package/src/core/errors.js +136 -0
- package/src/core/index.js +758 -0
- package/src/core/internal/appearance/bootstrap-script.js +159 -0
- package/src/core/internal/appearance/contract.js +193 -0
- package/src/core/internal/appearance/controller-markup.js +80 -0
- package/src/core/internal/appearance/styling-contract.js +509 -0
- package/src/core/internal/canonical-jcs.js +0 -0
- package/src/core/internal/content-security.js +0 -0
- package/src/core/internal/eleventy-render.js +220 -0
- package/src/core/internal/feeds.js +161 -0
- package/src/core/internal/fs-walk.js +38 -0
- package/src/core/internal/media/avif-probe.js +169 -0
- package/src/core/internal/media/font-policy.js +56 -0
- package/src/core/internal/media/jpeg-codec.js +0 -0
- package/src/core/internal/media/limits.js +129 -0
- package/src/core/internal/media/pipeline.js +353 -0
- package/src/core/internal/media/png-codec.js +406 -0
- package/src/core/internal/media/resize.js +147 -0
- package/src/core/internal/media/sniff.js +109 -0
- package/src/core/internal/media/webp-probe.js +139 -0
- package/src/core/internal/messages.js +78 -0
- package/src/core/internal/page-kinds.js +709 -0
- package/src/core/internal/provenance.js +49 -0
- package/src/core/internal/render-policy-content.js +295 -0
- package/src/core/internal/route-labels.js +68 -0
- package/src/core/internal/route.js +167 -0
- package/src/core/internal/search-index.js +119 -0
- package/src/core/internal/seo.js +114 -0
- package/src/core/internal/sitemap.js +77 -0
- package/src/core/internal/skeleton.js +413 -0
- package/src/core/internal/source-inventory.js +145 -0
- package/src/core/internal/text-direction.js +59 -0
- package/src/core/internal/theme-assets.js +387 -0
- package/src/core/internal/uuid.js +51 -0
- package/src/core/manifest.js +0 -0
- package/types/eleventy-plugin-syntaxhighlight.d.ts +26 -0
- package/types/eleventy.d.ts +38 -0
- package/types/index.d.ts +347 -0
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The exact deterministic pre-paint appearance bootstrap script (brief S2
|
|
3
|
+
* section 3: "a versioned deterministic pre-paint bootstrap owned by
|
|
4
|
+
* `template`/`core` resolves local selection plus system preference and sets
|
|
5
|
+
* the root state before first themed paint. It is covered by the artifact
|
|
6
|
+
* digest, is CSP-compatible under `script-src 'self'` (therefore an external
|
|
7
|
+
* file, not inline), touches no network, account, content or unrelated
|
|
8
|
+
* storage key, and cannot hide the document indefinitely").
|
|
9
|
+
*
|
|
10
|
+
* This is the one and only browser bootstrap S2 ever emits ("Module absence
|
|
11
|
+
* and CSP equality": "the appearance controller is the only browser
|
|
12
|
+
* bootstrap"). Every name and value it embeds is read from
|
|
13
|
+
* `internal/appearance/contract.js`, the one module that owns them, so the
|
|
14
|
+
* server-rendered control markup and this script can never drift apart on a
|
|
15
|
+
* literal.
|
|
16
|
+
*
|
|
17
|
+
* Two independent phases, both inside the same one external file so the
|
|
18
|
+
* template never ships a second script:
|
|
19
|
+
*
|
|
20
|
+
* 1. A synchronous, top-level phase that runs immediately when this
|
|
21
|
+
* blocking, non-`defer`/non-`async`/non-`module` `<script src>` executes
|
|
22
|
+
* in `<head>` — before `<body>` is parsed, and so before any themed
|
|
23
|
+
* paint. It resolves the stored selection (or `system`, gracefully
|
|
24
|
+
* substituted for a missing/unreadable/invalid stored value — storage
|
|
25
|
+
* failure never leaves the session unusable) against
|
|
26
|
+
* `prefers-color-scheme` and sets both root attributes.
|
|
27
|
+
* 2. A `DOMContentLoaded`-deferred phase that wires the server-rendered
|
|
28
|
+
* `<select>` control's `change` event once `<body>` exists (it cannot run
|
|
29
|
+
* any earlier: the control has not been parsed yet at phase 1's own
|
|
30
|
+
* execution point) and subscribes to live `prefers-color-scheme` changes,
|
|
31
|
+
* updating the resolved attribute only while the current selection is
|
|
32
|
+
* exactly `system` (brief: "an explicit `light`/`dark` choice is not
|
|
33
|
+
* overridden by later system changes").
|
|
34
|
+
*
|
|
35
|
+
* Deliberately absent: no `document.documentElement.style` mutation of any
|
|
36
|
+
* kind (in particular, no hide-until-ready visibility toggle — brief:
|
|
37
|
+
* "cannot hide the document indefinitely" — this script never needs one,
|
|
38
|
+
* since it only ever sets attributes, never inline style), no network
|
|
39
|
+
* request, no cookie, no account/tracking identifier, no storage key other
|
|
40
|
+
* than {@link APPEARANCE_STORAGE_KEY}.
|
|
41
|
+
*/
|
|
42
|
+
|
|
43
|
+
import {
|
|
44
|
+
APPEARANCE_MODE_DARK,
|
|
45
|
+
APPEARANCE_MODE_LIGHT,
|
|
46
|
+
APPEARANCE_MODE_SYSTEM,
|
|
47
|
+
APPEARANCE_RESOLVED_MODE_ATTRIBUTE,
|
|
48
|
+
APPEARANCE_SELECTION_ATTRIBUTE,
|
|
49
|
+
APPEARANCE_SELECT_ID,
|
|
50
|
+
APPEARANCE_STORAGE_KEY,
|
|
51
|
+
} from './contract.js';
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* The exact bootstrap script source, byte-for-byte deterministic across
|
|
55
|
+
* builds: every interpolated value below is a fixed literal from
|
|
56
|
+
* `contract.js`, never a per-publication or per-build input.
|
|
57
|
+
*
|
|
58
|
+
* @type {string}
|
|
59
|
+
*/
|
|
60
|
+
export const APPEARANCE_BOOTSTRAP_SCRIPT_SOURCE = `(function () {
|
|
61
|
+
'use strict';
|
|
62
|
+
var STORAGE_KEY = ${JSON.stringify(APPEARANCE_STORAGE_KEY)};
|
|
63
|
+
var RESOLVED_ATTRIBUTE = ${JSON.stringify(APPEARANCE_RESOLVED_MODE_ATTRIBUTE)};
|
|
64
|
+
var SELECTION_ATTRIBUTE = ${JSON.stringify(APPEARANCE_SELECTION_ATTRIBUTE)};
|
|
65
|
+
var SELECT_ID = ${JSON.stringify(APPEARANCE_SELECT_ID)};
|
|
66
|
+
var MODE_LIGHT = ${JSON.stringify(APPEARANCE_MODE_LIGHT)};
|
|
67
|
+
var MODE_DARK = ${JSON.stringify(APPEARANCE_MODE_DARK)};
|
|
68
|
+
var MODE_SYSTEM = ${JSON.stringify(APPEARANCE_MODE_SYSTEM)};
|
|
69
|
+
|
|
70
|
+
function isKnownMode(value) {
|
|
71
|
+
return value === MODE_LIGHT || value === MODE_DARK || value === MODE_SYSTEM;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function readStoredSelection() {
|
|
75
|
+
try {
|
|
76
|
+
var value = window.localStorage.getItem(STORAGE_KEY);
|
|
77
|
+
if (isKnownMode(value)) {
|
|
78
|
+
return value;
|
|
79
|
+
}
|
|
80
|
+
} catch (error) {
|
|
81
|
+
// Storage blocked (private mode, disabled site data, quota): the
|
|
82
|
+
// session stays usable and this load falls back to MODE_SYSTEM below.
|
|
83
|
+
}
|
|
84
|
+
return MODE_SYSTEM;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function writeStoredSelection(mode) {
|
|
88
|
+
try {
|
|
89
|
+
window.localStorage.setItem(STORAGE_KEY, mode);
|
|
90
|
+
} catch (error) {
|
|
91
|
+
// Storage blocked: the selection simply does not persist past this
|
|
92
|
+
// load; the visible page state this call site also applies still
|
|
93
|
+
// updates normally.
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function systemPrefersDark() {
|
|
98
|
+
try {
|
|
99
|
+
return !!(
|
|
100
|
+
window.matchMedia &&
|
|
101
|
+
window.matchMedia('(prefers-color-scheme: dark)').matches
|
|
102
|
+
);
|
|
103
|
+
} catch (error) {
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function resolveMode(selection) {
|
|
109
|
+
if (selection === MODE_LIGHT || selection === MODE_DARK) {
|
|
110
|
+
return selection;
|
|
111
|
+
}
|
|
112
|
+
return systemPrefersDark() ? MODE_DARK : MODE_LIGHT;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function applySelection(selection) {
|
|
116
|
+
var root = document.documentElement;
|
|
117
|
+
root.setAttribute(SELECTION_ATTRIBUTE, selection);
|
|
118
|
+
root.setAttribute(RESOLVED_ATTRIBUTE, resolveMode(selection));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Phase 1 (synchronous, pre-paint): resolve and apply immediately.
|
|
122
|
+
applySelection(readStoredSelection());
|
|
123
|
+
|
|
124
|
+
// Live system-preference changes, only while the reader's own selection is
|
|
125
|
+
// still exactly "system" (an explicit light/dark choice is never
|
|
126
|
+
// overridden by a later system change).
|
|
127
|
+
if (window.matchMedia) {
|
|
128
|
+
try {
|
|
129
|
+
var media = window.matchMedia('(prefers-color-scheme: dark)');
|
|
130
|
+
var onSystemChange = function () {
|
|
131
|
+
if (readStoredSelection() === MODE_SYSTEM) {
|
|
132
|
+
applySelection(MODE_SYSTEM);
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
if (typeof media.addEventListener === 'function') {
|
|
136
|
+
media.addEventListener('change', onSystemChange);
|
|
137
|
+
} else if (typeof media.addListener === 'function') {
|
|
138
|
+
media.addListener(onSystemChange);
|
|
139
|
+
}
|
|
140
|
+
} catch (error) {
|
|
141
|
+
// No live system-change subscription available: the resolved mode
|
|
142
|
+
// this load already applied simply stays static until next load.
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Phase 2 (deferred): wire the server-rendered control once it exists.
|
|
147
|
+
document.addEventListener('DOMContentLoaded', function () {
|
|
148
|
+
var select = document.getElementById(SELECT_ID);
|
|
149
|
+
if (!select) return;
|
|
150
|
+
select.value = readStoredSelection();
|
|
151
|
+
select.addEventListener('change', function () {
|
|
152
|
+
var next = select.value;
|
|
153
|
+
if (!isKnownMode(next)) return;
|
|
154
|
+
writeStoredSelection(next);
|
|
155
|
+
applySelection(next);
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
})();
|
|
159
|
+
`;
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The single module `contracts/theme-styling-contract.jcs` (S2-T12) targets
|
|
3
|
+
* for every Light/Dark/System appearance name this renderer owns (task
|
|
4
|
+
* packet S2-T07: "the `data-` attribute/class names the controller sets must
|
|
5
|
+
* be fixed constants exported from one module"; DEC-097's closed
|
|
6
|
+
* `templateStylingContract` catalog, section "Closed theme-package and
|
|
7
|
+
* stylesheet admission": the required root attribute row
|
|
8
|
+
* `{name:data-gala-publication-root,match:presence,values:[],role:root}` and
|
|
9
|
+
* the required palette attribute row
|
|
10
|
+
* `{name:data-gala-resolved-color-mode,match:exact-value,values:[dark,light],role:state}`;
|
|
11
|
+
* "The template renderer emits the exact root attribute and maintains
|
|
12
|
+
* exactly one `data-gala-resolved-color-mode="light"|"dark"` value on that
|
|
13
|
+
* same element before theme CSS is observed").
|
|
14
|
+
*
|
|
15
|
+
* Two attributes are exposed, deliberately kept separate (brief S2 section 3:
|
|
16
|
+
* "selection and resolved state are exposed separately through
|
|
17
|
+
* template-owned root attributes; theme CSS consumes only resolved state"):
|
|
18
|
+
*
|
|
19
|
+
* - {@link APPEARANCE_ROOT_ATTRIBUTE} and {@link APPEARANCE_RESOLVED_MODE_ATTRIBUTE}
|
|
20
|
+
* are the two DEC-097-fixed theme-facing attributes above — part of the
|
|
21
|
+
* published styling contract S2-T12 emits, never renamed here independent
|
|
22
|
+
* of that contract.
|
|
23
|
+
* - {@link APPEARANCE_SELECTION_ATTRIBUTE} is this renderer's own
|
|
24
|
+
* template-internal bookkeeping attribute (the reader's raw `light` /
|
|
25
|
+
* `dark` / `system` choice, as opposed to `system`'s *resolved* value). It
|
|
26
|
+
* is not a theme-styling-contract hook — DEC-097's attribute catalog does
|
|
27
|
+
* not name it, and no theme selector may target it — but its name is still
|
|
28
|
+
* fixed here, in this one module, exactly like the two contract-owned
|
|
29
|
+
* attributes, so no other module ever invents its own literal for it.
|
|
30
|
+
*
|
|
31
|
+
* Both the pre-paint bootstrap script ({@link ../appearance/bootstrap-script.js})
|
|
32
|
+
* and the server-rendered control markup ({@link ../appearance/controller-markup.js})
|
|
33
|
+
* import every name/value from here; neither ever writes an attribute name,
|
|
34
|
+
* a mode value or the storage key as its own literal.
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
import { joinBasePathAndRoute } from '../route.js';
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The DEC-097 `templateStylingContract.publicationRootSelector` attribute:
|
|
41
|
+
* a bare-presence attribute the template always renders on the document's
|
|
42
|
+
* root element (`<html>`), independent of JavaScript (`match:presence`).
|
|
43
|
+
*
|
|
44
|
+
* @type {string}
|
|
45
|
+
*/
|
|
46
|
+
export const APPEARANCE_ROOT_ATTRIBUTE = 'data-gala-publication-root';
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* The DEC-097 `templateStylingContract.resolvedPaletteSelectors` attribute:
|
|
50
|
+
* an exact-value attribute (`"light"` or `"dark"`, never `"system"`) the
|
|
51
|
+
* pre-paint bootstrap script sets on the root element before first themed
|
|
52
|
+
* paint. Theme CSS consumes only this attribute, never
|
|
53
|
+
* {@link APPEARANCE_SELECTION_ATTRIBUTE}.
|
|
54
|
+
*
|
|
55
|
+
* @type {string}
|
|
56
|
+
*/
|
|
57
|
+
export const APPEARANCE_RESOLVED_MODE_ATTRIBUTE =
|
|
58
|
+
'data-gala-resolved-color-mode';
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Template-internal bookkeeping attribute carrying the reader's raw
|
|
62
|
+
* selection (`light` / `dark` / `system`), as opposed to
|
|
63
|
+
* {@link APPEARANCE_RESOLVED_MODE_ATTRIBUTE}'s resolved value. Not part of
|
|
64
|
+
* the published theme styling contract; no theme selector may target it.
|
|
65
|
+
*
|
|
66
|
+
* @type {string}
|
|
67
|
+
*/
|
|
68
|
+
export const APPEARANCE_SELECTION_ATTRIBUTE = 'data-gala-color-mode-selection';
|
|
69
|
+
|
|
70
|
+
/** @type {'light'} */
|
|
71
|
+
export const APPEARANCE_MODE_LIGHT = 'light';
|
|
72
|
+
/** @type {'dark'} */
|
|
73
|
+
export const APPEARANCE_MODE_DARK = 'dark';
|
|
74
|
+
/** @type {'system'} */
|
|
75
|
+
export const APPEARANCE_MODE_SYSTEM = 'system';
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Every canonical machine-readable selection value (brief S2 section 3:
|
|
79
|
+
* "canonical machine values are `light`, `dark`, `system`; `lite` is not a
|
|
80
|
+
* value").
|
|
81
|
+
*
|
|
82
|
+
* @type {readonly ['light', 'dark', 'system']}
|
|
83
|
+
*/
|
|
84
|
+
export const APPEARANCE_MODE_VALUES = Object.freeze([
|
|
85
|
+
APPEARANCE_MODE_LIGHT,
|
|
86
|
+
APPEARANCE_MODE_DARK,
|
|
87
|
+
APPEARANCE_MODE_SYSTEM,
|
|
88
|
+
]);
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Every value {@link APPEARANCE_RESOLVED_MODE_ATTRIBUTE} may carry. `system`
|
|
92
|
+
* always resolves to one of these two; it is never a third selector value
|
|
93
|
+
* (DEC-097: "`system` is resolved to one of those values rather than
|
|
94
|
+
* becoming a third selector").
|
|
95
|
+
*
|
|
96
|
+
* @type {readonly ['light', 'dark']}
|
|
97
|
+
*/
|
|
98
|
+
export const APPEARANCE_RESOLVED_MODE_VALUES = Object.freeze([
|
|
99
|
+
APPEARANCE_MODE_LIGHT,
|
|
100
|
+
APPEARANCE_MODE_DARK,
|
|
101
|
+
]);
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* The initial reader default (brief S2 section 3: "initial reader default is
|
|
105
|
+
* `system`").
|
|
106
|
+
*
|
|
107
|
+
* @type {'system'}
|
|
108
|
+
*/
|
|
109
|
+
export const APPEARANCE_DEFAULT_MODE = APPEARANCE_MODE_SYSTEM;
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* The one versioned local-storage key the pre-paint bootstrap script and the
|
|
113
|
+
* control's change handler both read/write (brief S2 section 3: "selection
|
|
114
|
+
* is stored under one versioned local key"). The trailing `v1` is this
|
|
115
|
+
* key's own version; a future incompatible storage format change mints a new
|
|
116
|
+
* key rather than reinterpreting old values, so an old browser's already
|
|
117
|
+
* stored `v1` value is simply orphaned (ignored, never migrated or deleted)
|
|
118
|
+
* rather than misread as a new format.
|
|
119
|
+
*
|
|
120
|
+
* @type {string}
|
|
121
|
+
*/
|
|
122
|
+
export const APPEARANCE_STORAGE_KEY = 'gala:appearance:color-mode:v1';
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* The `id` the server-rendered `<select>` control carries, so the bootstrap
|
|
126
|
+
* script's post-`DOMContentLoaded` wiring can find it with a single
|
|
127
|
+
* `document.getElementById` call. Also used as the paired `<label for>`
|
|
128
|
+
* target.
|
|
129
|
+
*
|
|
130
|
+
* @type {string}
|
|
131
|
+
*/
|
|
132
|
+
export const APPEARANCE_SELECT_ID = 'gala-appearance-color-mode';
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* The candidate-output-directory-relative path the deterministic pre-paint
|
|
136
|
+
* bootstrap script is written to (task packet S2-T07: "entered into the
|
|
137
|
+
* manifest through the S2-T03 path with `{path,byteLength,sha256}`"). Not
|
|
138
|
+
* content-addressed (unlike the S2-T05 media pipeline's derivative paths):
|
|
139
|
+
* this file's bytes are a pure function of this module's own constants, not
|
|
140
|
+
* of any per-publication input, so a stable path carries no collision risk.
|
|
141
|
+
* The `v1` component versions the bootstrap script's own behaviour,
|
|
142
|
+
* independent of {@link APPEARANCE_STORAGE_KEY}'s own versioning.
|
|
143
|
+
*
|
|
144
|
+
* @type {string}
|
|
145
|
+
*/
|
|
146
|
+
export const APPEARANCE_BOOTSTRAP_SCRIPT_PATH =
|
|
147
|
+
'assets/gala-appearance-bootstrap-v1.js';
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* The root-absolute `<script src>` reference for a root (`/`) `basePath`
|
|
151
|
+
* publication. Every actual render joins this with the publication's own
|
|
152
|
+
* `basePath` through {@link appearanceBootstrapScriptHref} instead of using
|
|
153
|
+
* this literal directly (S2-T12 basePath fix, LOCAL-7 follow-up: an earlier
|
|
154
|
+
* version of this module used this root-absolute literal unconditionally,
|
|
155
|
+
* which 404s under a non-root `basePath` publication). This constant is
|
|
156
|
+
* retained as the one-place root-`basePath` literal every test and the
|
|
157
|
+
* `joinBasePathAndRoute` call below both agree on.
|
|
158
|
+
*
|
|
159
|
+
* @type {string}
|
|
160
|
+
*/
|
|
161
|
+
export const APPEARANCE_BOOTSTRAP_SCRIPT_HREF = `/${APPEARANCE_BOOTSTRAP_SCRIPT_PATH}`;
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* The `basePath`-joined `<script src>` reference every generated page's
|
|
165
|
+
* skeleton layout actually uses (S2-T12 basePath fix).
|
|
166
|
+
*
|
|
167
|
+
* @param {string} basePath the build input's `basePath`
|
|
168
|
+
* @returns {string} the joined `href`
|
|
169
|
+
*/
|
|
170
|
+
export function appearanceBootstrapScriptHref(basePath) {
|
|
171
|
+
return joinBasePathAndRoute(basePath, APPEARANCE_BOOTSTRAP_SCRIPT_HREF);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* The `manifestAsset`/`manifestRoute` `mediaType` enum member for a
|
|
176
|
+
* JavaScript file (`@rathnasgala2/schemas`' `artifact-manifest.schema.json`).
|
|
177
|
+
*
|
|
178
|
+
* @type {string}
|
|
179
|
+
*/
|
|
180
|
+
export const APPEARANCE_BOOTSTRAP_SCRIPT_MEDIA_TYPE =
|
|
181
|
+
'application/javascript; charset=utf-8';
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* The `<meta name="color-scheme">` content value: both palettes are
|
|
185
|
+
* admitted, so the browser's own user-agent styling (form controls,
|
|
186
|
+
* scrollbars) and initial paint follow `prefers-color-scheme` immediately,
|
|
187
|
+
* independent of this renderer's own resolved-mode attribute (brief S2
|
|
188
|
+
* section 3: "`color-scheme` meta/CSS handling"; "with JavaScript disabled,
|
|
189
|
+
* CSS media queries preserve `system`").
|
|
190
|
+
*
|
|
191
|
+
* @type {string}
|
|
192
|
+
*/
|
|
193
|
+
export const COLOR_SCHEME_META_CONTENT = 'light dark';
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The server-rendered Light/Dark/System appearance control (task packet
|
|
3
|
+
* S2-T07: "the three-mode appearance control in the header-actions slot";
|
|
4
|
+
* brief S2 section 3: "the control is a native `select`, a radio group, or a
|
|
5
|
+
* conforming menu/radio pattern. A three-state unlabeled icon cycle is
|
|
6
|
+
* rejected").
|
|
7
|
+
*
|
|
8
|
+
* Scoped decision (documented, not silently assumed): of the brief's three
|
|
9
|
+
* admitted patterns, this module implements a native `<select>` bound to a
|
|
10
|
+
* visible, programmatically associated `<label>`. A native select needs no
|
|
11
|
+
* `aria-pressed`/`aria-checked` bookkeeping of its own — its accessible
|
|
12
|
+
* state is the browser-native selected-`<option>` relationship, exposed to
|
|
13
|
+
* every platform accessibility API without any ARIA authored here — and it
|
|
14
|
+
* is keyboard-operable (arrow keys, type-ahead, Enter/Space to open) by
|
|
15
|
+
* native semantics alone, with no custom widget code to keep synchronized.
|
|
16
|
+
* The select's initial selected option is always
|
|
17
|
+
* {@link APPEARANCE_DEFAULT_MODE} (`system`): the actual stored/resolved
|
|
18
|
+
* value is applied to `select.value` by the pre-paint bootstrap script's own
|
|
19
|
+
* `DOMContentLoaded` phase once it runs (see `bootstrap-script.js`), so this
|
|
20
|
+
* server-rendered default is only ever visible for the brief instant before
|
|
21
|
+
* that phase completes, or permanently with JavaScript disabled — exactly
|
|
22
|
+
* the no-JS fallback the brief requires ("with JavaScript disabled, ...
|
|
23
|
+
* `system` [is preserved]").
|
|
24
|
+
*
|
|
25
|
+
* This markup carries no inline `on*` handler and no `javascript:` URL, so
|
|
26
|
+
* it needs no CSP allowance beyond the renderer's existing
|
|
27
|
+
* `script-src 'self'` baseline; the bootstrap script (an external, same-
|
|
28
|
+
* origin file) is what makes it interactive.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import { escapeHtml } from '../skeleton.js';
|
|
32
|
+
import {
|
|
33
|
+
APPEARANCE_DEFAULT_MODE,
|
|
34
|
+
APPEARANCE_MODE_DARK,
|
|
35
|
+
APPEARANCE_MODE_LIGHT,
|
|
36
|
+
APPEARANCE_MODE_SYSTEM,
|
|
37
|
+
APPEARANCE_SELECT_ID,
|
|
38
|
+
} from './contract.js';
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Render the appearance control's inner markup (a `<label>` plus its bound
|
|
42
|
+
* `<select>`), for insertion into the header-actions slot.
|
|
43
|
+
*
|
|
44
|
+
* @param {object} options rendering options
|
|
45
|
+
* @param {Readonly<Record<string, string | ((...args: string[]) => string)>>} options.messages
|
|
46
|
+
* the resolved message catalog
|
|
47
|
+
* @returns {string} the control markup
|
|
48
|
+
*/
|
|
49
|
+
export function renderAppearanceControl({ messages }) {
|
|
50
|
+
const label = escapeHtml(
|
|
51
|
+
/** @type {string} */ (messages.appearanceControlLabel),
|
|
52
|
+
);
|
|
53
|
+
const selectId = escapeHtml(APPEARANCE_SELECT_ID);
|
|
54
|
+
/** @type {readonly {value: string, text: string}[]} */
|
|
55
|
+
const modes = [
|
|
56
|
+
{
|
|
57
|
+
value: APPEARANCE_MODE_LIGHT,
|
|
58
|
+
text: /** @type {string} */ (messages.appearanceModeLightLabel),
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
value: APPEARANCE_MODE_DARK,
|
|
62
|
+
text: /** @type {string} */ (messages.appearanceModeDarkLabel),
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
value: APPEARANCE_MODE_SYSTEM,
|
|
66
|
+
text: /** @type {string} */ (messages.appearanceModeSystemLabel),
|
|
67
|
+
},
|
|
68
|
+
];
|
|
69
|
+
const options = modes
|
|
70
|
+
.map(({ value, text }) => {
|
|
71
|
+
const selectedAttribute =
|
|
72
|
+
value === APPEARANCE_DEFAULT_MODE ? ' selected' : '';
|
|
73
|
+
return `<option value="${escapeHtml(value)}"${selectedAttribute}>${escapeHtml(text)}</option>`;
|
|
74
|
+
})
|
|
75
|
+
.join('');
|
|
76
|
+
return (
|
|
77
|
+
`<label for="${selectId}">${label}</label>` +
|
|
78
|
+
`<select id="${selectId}" name="${selectId}">${options}</select>`
|
|
79
|
+
);
|
|
80
|
+
}
|