@rathnasgala2/template 2.2.0 → 3.0.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.
@@ -138,14 +138,15 @@ export const APPEARANCE_DEFAULT_MODE = APPEARANCE_MODE_SYSTEM;
138
138
  export const APPEARANCE_STORAGE_KEY = 'gala:appearance:color-mode:v1';
139
139
 
140
140
  /**
141
- * The `id` the server-rendered `<select>` control carries, so the bootstrap
142
- * script's post-`DOMContentLoaded` wiring can find it with a single
143
- * `document.getElementById` call. Also used as the paired `<label for>`
144
- * target.
141
+ * The `id` the server-rendered colour-mode toggle `<button>` carries, so
142
+ * the site script's wiring can find it with a single
143
+ * `document.getElementById` call. The button is rendered `hidden`; the
144
+ * script reveals it (with JavaScript off the site simply follows the
145
+ * server-rendered default palette).
145
146
  *
146
147
  * @type {string}
147
148
  */
148
- export const APPEARANCE_SELECT_ID = 'gala-appearance-color-mode';
149
+ export const APPEARANCE_TOGGLE_ID = 'gala-appearance-color-mode';
149
150
 
150
151
  /**
151
152
  * The candidate-output-directory-relative path the deterministic pre-paint
@@ -1,44 +1,31 @@
1
1
  /**
2
- * The server-rendered Light/Dark/System appearance control, placed in the
3
- * header-actions slot. The control must be a native `select`, a radio
4
- * group, or a conforming menu/radio pattern; a three-state unlabeled icon
5
- * cycle is rejected.
2
+ * The server-rendered colour-mode toggle, placed in the header-actions slot:
3
+ * one icon button that the site script reveals and wires to cycle
4
+ * system, light, dark (with a circular-reveal view transition where the
5
+ * browser supports it). It is rendered `hidden` because it does nothing
6
+ * without the script; with JavaScript off the site simply follows the
7
+ * server-rendered default palette and no dead control is shown.
6
8
  *
7
- * Scoped decision (documented, not silently assumed): of those three
8
- * admitted patterns, this module implements a native `<select>` bound to a
9
- * visible, programmatically associated `<label>`. A native select needs no
10
- * `aria-pressed`/`aria-checked` bookkeeping of its own — its accessible
11
- * state is the browser-native selected-`<option>` relationship, exposed to
12
- * every platform accessibility API without any ARIA authored here — and it
13
- * is keyboard-operable (arrow keys, type-ahead, Enter/Space to open) by
14
- * native semantics alone, with no custom widget code to keep synchronized.
15
- * The select's initial selected option is always
16
- * {@link APPEARANCE_DEFAULT_MODE} (`system`): the actual stored/resolved
17
- * value is applied to `select.value` by the pre-paint bootstrap script's own
18
- * `DOMContentLoaded` phase once it runs (see `bootstrap-script.js`), so this
19
- * server-rendered default is only ever visible for the brief instant before
20
- * that phase completes, or permanently with JavaScript disabled — exactly
21
- * the required no-JS fallback: with JavaScript disabled, `system` is
22
- * preserved.
23
- *
24
- * This markup carries no inline `on*` handler and no `javascript:` URL, so
25
- * it needs no CSP allowance beyond the renderer's existing
26
- * `script-src 'self'` baseline; the bootstrap script (an external, same-
27
- * origin file) is what makes it interactive.
9
+ * The button's accessible name carries the current mode (for example
10
+ * "Appearance: System"); the script keeps it current from the three
11
+ * `data-label-*` attributes, which are the only localized text it needs.
12
+ * Which of the three mode icons shows is pure CSS, keyed off the root's
13
+ * selection attribute. The markup carries no inline `on*` handler and no
14
+ * `javascript:` URL, so it needs no CSP allowance beyond `script-src 'self'`.
28
15
  */
29
16
 
30
17
  import { escapeHtml } from '../skeleton.js';
18
+ import { icon } from '../icons.js';
31
19
  import {
32
20
  APPEARANCE_DEFAULT_MODE,
33
21
  APPEARANCE_MODE_DARK,
34
22
  APPEARANCE_MODE_LIGHT,
35
23
  APPEARANCE_MODE_SYSTEM,
36
- APPEARANCE_SELECT_ID,
24
+ APPEARANCE_TOGGLE_ID,
37
25
  } from './contract.js';
38
26
 
39
27
  /**
40
- * Render the appearance control's inner markup (a `<label>` plus its bound
41
- * `<select>`), for insertion into the header-actions slot.
28
+ * Render the appearance toggle button.
42
29
  *
43
30
  * @param {object} options rendering options
44
31
  * @param {Readonly<Record<string, string | ((...args: string[]) => string)>>} options.messages
@@ -46,34 +33,38 @@ import {
46
33
  * @returns {string} the control markup
47
34
  */
48
35
  export function renderAppearanceControl({ messages }) {
49
- const label = escapeHtml(
50
- /** @type {string} */ (messages.appearanceControlLabel),
51
- );
52
- const selectId = escapeHtml(APPEARANCE_SELECT_ID);
53
- /** @type {readonly {value: string, text: string}[]} */
54
- const modes = [
55
- {
56
- value: APPEARANCE_MODE_LIGHT,
57
- text: /** @type {string} */ (messages.appearanceModeLightLabel),
58
- },
59
- {
60
- value: APPEARANCE_MODE_DARK,
61
- text: /** @type {string} */ (messages.appearanceModeDarkLabel),
62
- },
63
- {
64
- value: APPEARANCE_MODE_SYSTEM,
65
- text: /** @type {string} */ (messages.appearanceModeSystemLabel),
66
- },
67
- ];
68
- const options = modes
69
- .map(({ value, text }) => {
70
- const selectedAttribute =
71
- value === APPEARANCE_DEFAULT_MODE ? ' selected' : '';
72
- return `<option value="${escapeHtml(value)}"${selectedAttribute}>${escapeHtml(text)}</option>`;
73
- })
36
+ const control = /** @type {string} */ (messages.appearanceControlLabel);
37
+ /** @type {Record<string, string>} */
38
+ const modeText = {
39
+ [APPEARANCE_MODE_LIGHT]: /** @type {string} */ (
40
+ messages.appearanceModeLightLabel
41
+ ),
42
+ [APPEARANCE_MODE_DARK]: /** @type {string} */ (
43
+ messages.appearanceModeDarkLabel
44
+ ),
45
+ [APPEARANCE_MODE_SYSTEM]: /** @type {string} */ (
46
+ messages.appearanceModeSystemLabel
47
+ ),
48
+ };
49
+ const labelOf = (/** @type {string} */ mode) =>
50
+ escapeHtml(`${control}: ${modeText[mode]}`);
51
+ const labelAttributes = [
52
+ APPEARANCE_MODE_SYSTEM,
53
+ APPEARANCE_MODE_LIGHT,
54
+ APPEARANCE_MODE_DARK,
55
+ ]
56
+ .map((mode) => ` data-label-${mode}="${labelOf(mode)}"`)
74
57
  .join('');
58
+ const modeIcon = (
59
+ /** @type {string} */ mode,
60
+ /** @type {string} */ iconName,
61
+ ) => `<span class="g-mode-icon g-mode-${mode}">${icon(iconName)}</span>`;
75
62
  return (
76
- `<label for="${selectId}">${label}</label>` +
77
- `<select id="${selectId}" name="${selectId}">${options}</select>`
63
+ `<button class="g-icon-btn" id="${escapeHtml(APPEARANCE_TOGGLE_ID)}" type="button" hidden` +
64
+ ` data-action="mode"${labelAttributes} aria-label="${labelOf(APPEARANCE_DEFAULT_MODE)}" title="${labelOf(APPEARANCE_DEFAULT_MODE)}">` +
65
+ modeIcon(APPEARANCE_MODE_SYSTEM, 'monitor') +
66
+ modeIcon(APPEARANCE_MODE_LIGHT, 'sun') +
67
+ modeIcon(APPEARANCE_MODE_DARK, 'moon') +
68
+ `</button>`
78
69
  );
79
70
  }
@@ -0,0 +1,69 @@
1
+ /**
2
+ * A deliberately small, dependency-free, safe minifier for the one site
3
+ * script: it removes comments and indentation and drops blank lines. It
4
+ * never renames, reorders or joins statements, and it keeps every newline
5
+ * that remains, so automatic semicolon insertion behaves exactly as in the
6
+ * readable source. String literals and regular-expression literals are
7
+ * copied byte for byte.
8
+ */
9
+
10
+ /**
11
+ * @param {string} previous the last non-whitespace character already
12
+ * emitted ('' at the start of the source)
13
+ * @returns {boolean} whether a `/` here starts a regular-expression literal
14
+ * (rather than a division)
15
+ */
16
+ function regexAllowed(previous) {
17
+ return previous === '' || '(,=:[!&|?{};+-*%<>~^'.includes(previous);
18
+ }
19
+
20
+ /**
21
+ * @param {string} source readable JavaScript source (no template literals)
22
+ * @returns {string} the same program without comments, indentation or blank
23
+ * lines
24
+ */
25
+ export function minifyScript(source) {
26
+ let out = '';
27
+ let previous = '';
28
+ let index = 0;
29
+ const length = source.length;
30
+ while (index < length) {
31
+ const character = source[index];
32
+ const next = source[index + 1];
33
+ if (character === "'" || character === '"') {
34
+ let end = index + 1;
35
+ while (end < length && source[end] !== character) {
36
+ end += source[end] === '\\' ? 2 : 1;
37
+ }
38
+ out += source.slice(index, end + 1);
39
+ previous = character;
40
+ index = end + 1;
41
+ } else if (character === '/' && next === '/') {
42
+ while (index < length && source[index] !== '\n') index += 1;
43
+ } else if (character === '/' && next === '*') {
44
+ const end = source.indexOf('*/', index + 2);
45
+ index = end === -1 ? length : end + 2;
46
+ } else if (character === '/' && regexAllowed(previous)) {
47
+ let end = index + 1;
48
+ let inClass = false;
49
+ while (end < length && (inClass || source[end] !== '/')) {
50
+ if (source[end] === '\\') end += 1;
51
+ else if (source[end] === '[') inClass = true;
52
+ else if (source[end] === ']') inClass = false;
53
+ end += 1;
54
+ }
55
+ out += source.slice(index, end + 1);
56
+ previous = '/';
57
+ index = end + 1;
58
+ } else {
59
+ out += character;
60
+ if (!/\s/.test(character)) previous = character;
61
+ index += 1;
62
+ }
63
+ }
64
+ return `${out
65
+ .split('\n')
66
+ .map((line) => line.trim())
67
+ .filter((line) => line !== '')
68
+ .join('\n')}\n`;
69
+ }
@@ -13,9 +13,10 @@
13
13
  * `internal/appearance/contract.js` — never a hand-typed literal
14
14
  * independent of what those modules emit.
15
15
  *
16
- * Public-hook budget (`publicThemeSlotHooks` is a `[1..64]`-entry set):
17
- * every catalog leaf below (28 type + 15 class + 2 id + 19 attribute-value)
18
- * has exactly one corresponding public hook, for exactly 64 entries — this
16
+ * Public-hook budget (`publicThemeSlotHooks` is a `[1..256]`-entry set since
17
+ * contract 3.0.0): every catalog leaf below (30 type + 127 class + 2 id + 19
18
+ * attribute-value) has exactly one corresponding public hook, for exactly
19
+ * 178 entries — this
19
20
  * renderer's complete public theming surface. A leaf with no matching hook
20
21
  * would be a catalog member no theme could ever validly select (a
21
22
  * used-but-undeclared public slot hook rejects), so catalogs and hooks are
@@ -48,7 +49,7 @@
48
49
  * or a `background-image`.
49
50
  *
50
51
  * Contract 2.1.0 (TPL-H2/TPL-H3/TPL-M7 fix): S2.0's empty `pseudoClasses`
51
- * catalog made every interaction state unstylable while the closed 35-token
52
+ * catalog made every interaction state unstylable while the closed 116-token
52
53
  * catalog already required `color-focus` and `color-link-visited`, tokens
53
54
  * only a pseudo-class can ever apply. {@link PSEUDO_CLASSES} publishes a
54
55
  * small closed catalog instead of widening indefinitely; the module
@@ -61,7 +62,7 @@
61
62
  * markdown-it's own CommonMark grammar (with `html:false`) never actually
62
63
  * produces — `b`, `i`, `u`, `s`, `del`, `ins`, `sub`, `sup`, `mark`, `small`,
63
64
  * `wbr`, `table`, `thead`, `tbody`, `tr`, `th`, `td`, `dl`, `dt`, `dd`,
64
- * `figure`, `figcaption` — is likewise excluded from `typeSelectors`: this
65
+ * `figcaption` — is likewise excluded from `typeSelectors`: this
65
66
  * catalog is every hook the template actually renders, not the sanitizer's
66
67
  * own defence-in-depth superset.
67
68
  */
@@ -70,7 +71,7 @@ import { HIGHLIGHT_GRAMMARS } from '../render-policy-content.js';
70
71
  import {
71
72
  APPEARANCE_RESOLVED_MODE_ATTRIBUTE,
72
73
  APPEARANCE_ROOT_ATTRIBUTE,
73
- APPEARANCE_SELECT_ID,
74
+ APPEARANCE_TOGGLE_ID,
74
75
  } from './contract.js';
75
76
  import { PAGE_KIND_ATTRIBUTE, PAGE_KIND_VALUES } from '../page-kinds.js';
76
77
  import { SLOT_NAMES } from '../skeleton.js';
@@ -79,9 +80,9 @@ import { canonicalizeJcs, domainDigest } from '../canonical-jcs.js';
79
80
  /** @type {string} */
80
81
  export const TEMPLATE_STYLING_CONTRACT_PACKAGE = '@rathnasgala2/template';
81
82
  /** @type {string} */
82
- export const TEMPLATE_STYLING_CONTRACT_VERSION = '2.1.0';
83
+ export const TEMPLATE_STYLING_CONTRACT_VERSION = '3.0.0';
83
84
  /** @type {string} */
84
- export const TEMPLATE_STYLING_CONTRACT_TEMPLATE_VERSION = '2.1.0';
85
+ export const TEMPLATE_STYLING_CONTRACT_TEMPLATE_VERSION = '3.0.0';
85
86
 
86
87
  /** @type {string} the `main-content` landmark's fixed `id`. */
87
88
  const MAIN_CONTENT_ID = 'main-content';
@@ -104,7 +105,7 @@ const MAIN_CONTENT_ID = 'main-content';
104
105
  * `hover`.
105
106
  * - `visited`: the only way to consume the closed token catalog's own
106
107
  * `color-link-visited`, which had no reachable application before this.
107
- * - `disabled`: the appearance `<select>` is a native form control, which
108
+ * - `disabled`: the appearance toggle is a native button control, which
108
109
  * may legitimately be disabled by a later module; themes need a way to
109
110
  * style that state today so it is not a breaking addition later.
110
111
  *
@@ -213,11 +214,13 @@ const TYPE_HOOKS = Object.freeze(
213
214
  ['prose-link', 'a'],
214
215
  ['media-image', 'img'],
215
216
  ['prose-divider', 'hr'],
216
- ['control-label', 'label'],
217
- ['control-select', 'select'],
218
- ['control-option', 'option'],
219
217
  ['prose-span', 'span'],
220
218
  ['prose-time', 'time'],
219
+ ['control-button', 'button'],
220
+ ['disclosure-details', 'details'],
221
+ ['disclosure-summary', 'summary'],
222
+ ['landmark-section', 'section'],
223
+ ['media-figure', 'figure'],
221
224
  ].map(([hookId, tag]) =>
222
225
  Object.freeze({
223
226
  hookId,
@@ -228,13 +231,188 @@ const TYPE_HOOKS = Object.freeze(
228
231
  );
229
232
 
230
233
  /**
231
- * Every class-selector hook: the base Prism `.token` class plus one
232
- * `.language-<grammar>` hook per admitted highlight grammar (S2 scope: no
234
+ * The most public hooks a theme contract may declare
235
+ * (`theme-contract.slotHooks.maxItems`, raised from 64 to 256 in contract
236
+ * 3.0.0).
237
+ *
238
+ * @type {number}
239
+ */
240
+ export const MAX_PUBLIC_HOOKS = 256;
241
+
242
+ /**
243
+ * Contract 3.0.0: every `g-*` component class the template's own markup
244
+ * (`internal/chrome.js`, `internal/components.js`, `internal/page-kinds.js`
245
+ * and `internal/skeleton.js`) emits, grouped by component. The naming
246
+ * convention is the approved design reference's: `g-<component>` for the
247
+ * block and `g-<component>-<part>` for its parts; a variant adds a second
248
+ * class (`g-card g-card-row`). One hook per class, published as
249
+ * `ui-<name>`; `test/theme-styling-contract.test.js` fails if a rendered
250
+ * `g-*` class is missing here, or a class listed here is never rendered.
251
+ * Pure-layout needs a component can meet with an element/descendant
252
+ * selector (`.g-card h3`) deliberately get no class of their own, to stay
253
+ * under the 256-hook cap.
254
+ *
255
+ * @type {Readonly<Record<string, readonly string[]>>}
256
+ */
257
+ export const COMPONENT_CLASS_GROUPS = Object.freeze({
258
+ shared: [
259
+ 'g-wrap',
260
+ 'g-main',
261
+ 'g-skip',
262
+ 'g-sr',
263
+ 'g-icon',
264
+ 'g-dot',
265
+ 'g-label',
266
+ 'g-dek',
267
+ 'g-btn',
268
+ 'g-icon-btn',
269
+ 'g-more',
270
+ 'g-chip',
271
+ 'g-badge',
272
+ 'g-eyebrow',
273
+ 'g-meta',
274
+ 'g-avatar',
275
+ 'g-byline',
276
+ 'g-byline-text',
277
+ 'g-section',
278
+ 'g-section-head',
279
+ 'g-grid',
280
+ 'g-list',
281
+ 'g-pills',
282
+ 'g-pill',
283
+ 'g-pill-count',
284
+ 'g-crumbs',
285
+ 'g-pagination',
286
+ 'g-toast',
287
+ ],
288
+ header: [
289
+ 'g-header',
290
+ 'g-header-row',
291
+ 'g-brand',
292
+ 'g-mark',
293
+ 'g-brand-text',
294
+ 'g-brand-name',
295
+ 'g-brand-tag',
296
+ 'g-nav',
297
+ 'g-actions',
298
+ 'g-search-btn',
299
+ 'g-menu',
300
+ 'g-mode-icon',
301
+ 'g-mode-system',
302
+ 'g-mode-light',
303
+ 'g-mode-dark',
304
+ ],
305
+ footer: [
306
+ 'g-footer',
307
+ 'g-footer-grid',
308
+ 'g-footer-brand',
309
+ 'g-footer-about',
310
+ 'g-footer-col',
311
+ 'g-footer-base',
312
+ 'g-social',
313
+ ],
314
+ card: [
315
+ 'g-card',
316
+ 'g-card-row',
317
+ 'g-card-link',
318
+ 'g-card-media',
319
+ 'g-card-placeholder',
320
+ 'g-card-body',
321
+ 'g-card-top',
322
+ 'g-card-excerpt',
323
+ 'g-card-foot',
324
+ 'g-series-tag',
325
+ ],
326
+ home: [
327
+ 'g-hero',
328
+ 'g-hero-media',
329
+ 'g-hero-body',
330
+ 'g-topic-strip',
331
+ 'g-series-card',
332
+ 'g-series-list',
333
+ 'g-series-n',
334
+ 'g-newsletter',
335
+ 'g-panel',
336
+ 'g-panel-copy',
337
+ ],
338
+ article: [
339
+ 'g-article',
340
+ 'g-progress',
341
+ 'g-article-head',
342
+ 'g-article-labels',
343
+ 'g-article-meta',
344
+ 'g-share',
345
+ 'g-article-cover',
346
+ 'g-article-grid',
347
+ 'g-toc',
348
+ 'g-toc-list',
349
+ 'g-toc-mobile',
350
+ 'g-prose',
351
+ 'g-tags',
352
+ 'g-tag',
353
+ 'g-article-foot',
354
+ 'g-author-card',
355
+ 'g-author-name',
356
+ 'g-author-bio',
357
+ 'g-series-box',
358
+ 'g-series-here',
359
+ 'g-pager',
360
+ 'g-pager-link',
361
+ 'g-pager-next',
362
+ ],
363
+ listing: ['g-topic-hero', 'g-topic-head', 'g-topic-icon'],
364
+ // Created at runtime by the optional site script (never server-rendered):
365
+ // the search dialog and its results, and the code block chrome. Published
366
+ // all the same so a theme can restyle them.
367
+ script: [
368
+ 'g-kbd',
369
+ 'g-search',
370
+ 'g-search-box',
371
+ 'g-search-input',
372
+ 'g-search-results',
373
+ 'g-search-hit',
374
+ 'g-search-hit-text',
375
+ 'g-search-hit-title',
376
+ 'g-search-hit-meta',
377
+ 'g-search-empty',
378
+ 'g-codeblock',
379
+ 'g-codebar',
380
+ 'g-codebar-lang',
381
+ 'g-copy',
382
+ 'g-copy-idle',
383
+ 'g-copy-done',
384
+ ],
385
+ });
386
+
387
+ /**
388
+ * The classes only the site script creates (the `script` group of
389
+ * {@link COMPONENT_CLASS_GROUPS}); the drift test checks these against the
390
+ * script source instead of the server-rendered HTML.
391
+ *
392
+ * @type {readonly string[]}
393
+ */
394
+ export const SCRIPT_CREATED_CLASSES = Object.freeze([
395
+ ...COMPONENT_CLASS_GROUPS.script,
396
+ ]);
397
+
398
+ /**
399
+ * Every class-selector hook: the `g-*` component classes
400
+ * ({@link COMPONENT_CLASS_GROUPS}), the base Prism `.token` class and one
401
+ * `.language-<grammar>` hook per admitted highlight grammar (no
233
402
  * per-token-kind class is public — see module documentation).
234
403
  *
235
404
  * @type {readonly StyleHookDefinition[]}
236
405
  */
237
406
  const CLASS_HOOKS = Object.freeze([
407
+ ...Object.values(COMPONENT_CLASS_GROUPS)
408
+ .flat()
409
+ .map((className) =>
410
+ Object.freeze({
411
+ hookId: `ui-${className.slice(2)}`,
412
+ selectorAtom: `.${className}`,
413
+ kind: /** @type {const} */ ('class'),
414
+ }),
415
+ ),
238
416
  Object.freeze({
239
417
  hookId: 'code-token',
240
418
  selectorAtom: '.token',
@@ -262,8 +440,8 @@ const ID_HOOKS = Object.freeze([
262
440
  kind: /** @type {const} */ ('id'),
263
441
  }),
264
442
  Object.freeze({
265
- hookId: 'control-appearance-select',
266
- selectorAtom: `#${APPEARANCE_SELECT_ID}`,
443
+ hookId: 'control-appearance-toggle',
444
+ selectorAtom: `#${APPEARANCE_TOGGLE_ID}`,
267
445
  kind: /** @type {const} */ ('id'),
268
446
  }),
269
447
  ]);
@@ -450,7 +628,7 @@ export const TEMPLATE_STYLING_CONTRACT_INVALID =
450
628
  * Structural self-validation of a `templateStylingContract` object against
451
629
  * its closed shape (the checks this repository can run
452
630
  * without a published `urn:gala:schema:template-styling-contract` schema
453
- * entry — no such schema ID is registered in `@rathnasgala2/schemas@2.11.0`;
631
+ * entry — no such schema ID is registered in `@rathnasgala2/schemas@3.0.0`;
454
632
  * only `urn:gala:schema:theme-contract:2.0.0` is, which this module's sibling
455
633
  * theme fixtures are validated against with the schema package's own
456
634
  * exported `validateGalaDocument`, see `test/theme-styling-contract.test.js`).
@@ -470,7 +648,7 @@ export function assertTemplateStylingContractShape(contract) {
470
648
  if (contract.profile !== 'gala-template-styling-contract-v2') {
471
649
  fail('profile');
472
650
  }
473
- if (contract.contractVersion !== '2.1.0') fail('contractVersion');
651
+ if (contract.contractVersion !== '3.0.0') fail('contractVersion');
474
652
  if (contract.templatePackage !== '@rathnasgala2/template') {
475
653
  fail('templatePackage');
476
654
  }
@@ -585,7 +763,11 @@ export function assertTemplateStylingContractShape(contract) {
585
763
  const hooks = /** @type {{hookId: string, selectorAtom: string}[]} */ (
586
764
  contract.publicThemeSlotHooks
587
765
  );
588
- if (!Array.isArray(hooks) || hooks.length < 1 || hooks.length > 64) {
766
+ if (
767
+ !Array.isArray(hooks) ||
768
+ hooks.length < 1 ||
769
+ hooks.length > MAX_PUBLIC_HOOKS
770
+ ) {
589
771
  fail('publicThemeSlotHooks length');
590
772
  }
591
773
  const seenAtoms = new Set();