@rathnasgala2/template 3.2.1 → 4.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/README.md CHANGED
@@ -629,7 +629,7 @@ npm run verify
629
629
  hand-maintained) runs, in order: Prettier format check, ESLint,
630
630
  `tsc --checkJs --noEmit`, the dependency-cruiser
631
631
  architecture/module-tree-absence gate, `jscpd` duplication scan, the
632
- `contracts/*.jcs` canonical-form check, the no-local-schema-pin check,
632
+ `contracts/*.jcs` canonical-form check, the no-local-schema-pin check, the
633
633
  `node --test`, the license inventory check, the SBOM currency check
634
634
  (`sbom:check` compares `sbom.cdx.json` against the current lockfile — it is not
635
635
  a presence check), the workflow-pin check, and `npm audit --audit-level=high`.
@@ -645,7 +645,7 @@ npm run architecture # dependency-cruiser: core never imports src/modules, no
645
645
  npm run duplication # jscpd, 3% / 50 tokens
646
646
  npm run contracts:generate # (re)write contracts/*.jcs from their reviewed source modules
647
647
  npm run contracts:check # fail if committed contracts drifted
648
- npm run schema-pin:check # fail on a file: @rathnasgala2/schemas specifier
648
+ npm run schema-pin:check # require the exact registry @rathnasgala2/schemas release
649
649
  npm test # node --test
650
650
  npm run licenses:generate # (re)write THIRD_PARTY_LICENSES.json
651
651
  npm run licenses:check # fail if it drifted from package-lock.json
@@ -656,6 +656,11 @@ npm run workflows:drift # fail if workflow files diverge from their own base
656
656
  npm run audit # npm audit --audit-level=high
657
657
  ```
658
658
 
659
+ Source development does not require a sibling schema checkout. The exact
660
+ `@rathnasgala2/schemas@3.8.0` release is installed from the npm registry, and
661
+ the lockfile carries its registry integrity. The exact peer is the public
662
+ consumer contract.
663
+
659
664
  ## Package layout
660
665
 
661
666
  ```text
@@ -679,17 +684,16 @@ editions: they are a rendered content kind (see Editions).
679
684
 
680
685
  ## Consuming `@rathnasgala2/schemas`
681
686
 
682
- This repository consumes `@rathnasgala2/schemas@3.3.0` from the public npm
683
- registry, declared as `"@rathnasgala2/schemas": "3.3.0"` (exact pin, no range)
684
- so `package-lock.json` records the resolved version and integrity hash. The
685
- LOCAL-1/LOCAL-39 local-tarball convention
686
- (`file:../../local-packages/rathnasgala2-schemas-*.tgz`) is retired for this
687
- package now that it publishes to the registry. The 2.8.0->2.11.0 convergence
688
- changes none of the four roots this renderer validates, for the same reason the
689
- 2.7.0-2.8.0 delta below did not. Only the `"."` export (`validateGalaDocument`,
690
- `GALA_SCHEMA_IDS`) is consumed; the 2.6.0 `./runtime-origins` narrow export and
691
- every other addition through 2.6.1 (new OpenAPI operations, App route/catalog
692
- copy, the `PrincipalSummary`/`AuthenticatorSummary`/keyset field additions) are
687
+ This source targets `@rathnasgala2/schemas@3.8.0` with an exact manifest pin.
688
+ Generation-4 development verifies against the schema sibling/packed candidate.
689
+ The historical `package-lock.json` is not a 3.8.0 registry claim: it must be
690
+ regenerated, together with the SBOM, from the authoritative published 3.8.0
691
+ artifact before release. The 2.8.0->2.11.0 convergence changes none of the four
692
+ roots this renderer validates, for the same reason the 2.7.0-2.8.0 delta below
693
+ did not. Only the `"."` export (`validateGalaDocument`, `GALA_SCHEMA_IDS`) is
694
+ consumed; the 2.6.0 `./runtime-origins` narrow export and every other addition
695
+ through 2.6.1 (new OpenAPI operations, App route/catalog copy, the
696
+ `PrincipalSummary`/`AuthenticatorSummary`/keyset field additions) are
693
697
  App/API-surface changes this renderer does not read. The `"."` export's 2.2.2
694
698
  browser-safety refactor (Node builtins replaced with browser-safe equivalents
695
699
  behind the same public API) is byte-identical in behaviour for a Node consumer
@@ -1 +1 @@
1
- {"attributes":[{"match":"exact-value","name":"data-gala-page-kind","role":"semantic","values":["archive","article","author","error","index","page","profile","series","tag"]},{"match":"exact-value","name":"data-gala-resolved-color-mode","role":"state","values":["dark","light"]},{"match":"exact-value","name":"data-gala-slot","role":"semantic","values":["account-intent","article-end","article-footer-ad","article-preamble","conversation","edition-selector","footer-auxiliary","footer-profile","header-actions","newsletter"]},{"match":"presence","name":"data-gala-publication-root","role":"root","values":[]}],"catalogDigest":"sha256:a10fecf6d43e4392312158f15b66d55a9e2bd3c7816a0747eb2084735d336de5","classSelectors":["g-actions","g-article","g-article-cover","g-article-foot","g-article-grid","g-article-head","g-article-labels","g-article-meta","g-author-bio","g-author-card","g-author-name","g-avatar","g-badge","g-brand","g-brand-name","g-brand-tag","g-brand-text","g-btn","g-byline","g-byline-text","g-card","g-card-body","g-card-excerpt","g-card-foot","g-card-link","g-card-media","g-card-placeholder","g-card-row","g-card-top","g-chip","g-codebar","g-codebar-lang","g-codeblock","g-copy","g-copy-done","g-copy-idle","g-crumbs","g-dek","g-dot","g-eyebrow","g-footer","g-footer-about","g-footer-base","g-footer-brand","g-footer-col","g-footer-grid","g-grid","g-header","g-header-row","g-hero","g-hero-body","g-hero-media","g-icon","g-icon-btn","g-kbd","g-label","g-list","g-main","g-mark","g-menu","g-meta","g-mode-dark","g-mode-icon","g-mode-light","g-mode-system","g-more","g-nav","g-newsletter","g-pager","g-pager-link","g-pager-next","g-pagination","g-panel","g-panel-copy","g-pill","g-pill-count","g-pills","g-progress","g-prose","g-search","g-search-box","g-search-btn","g-search-empty","g-search-hit","g-search-hit-meta","g-search-hit-text","g-search-hit-title","g-search-input","g-search-results","g-section","g-section-head","g-series-box","g-series-card","g-series-here","g-series-list","g-series-n","g-series-tag","g-share","g-skip","g-social","g-sr","g-tag","g-tags","g-toast","g-toc","g-toc-list","g-toc-mobile","g-topic-head","g-topic-hero","g-topic-icon","g-topic-strip","g-wrap","language-bash","language-css","language-diff","language-java","language-javascript","language-json","language-jsx","language-markdown","language-markup","language-python","language-sql","language-tsx","language-typescript","language-yaml","token"],"combinators":[" "," > "," + "," ~ "],"composition":{"compoundOrder":["type","id","class","attribute","pseudo-class","pseudo-element"],"functionalPseudoKeywordArguments":["even","odd"],"functionalSelectorArguments":"compound-only","maximumFunctionalDepth":1,"nthExpressionProfile":"gala-positive-an-plus-b-v2","rootScope":"first-compound-required"},"contractVersion":"3.0.0","functionalPseudos":["is","where","not","nth-child","nth-last-child"],"idSelectors":["gala-appearance-color-mode","main-content"],"orderedLayers":["gala-base","gala-tokens","gala-components","gala-utilities","gala-print"],"profile":"gala-template-styling-contract-v2","pseudoClasses":["active","disabled","focus-visible","hover","visited"],"pseudoElements":["after","before","marker","selection"],"publicThemeSlotHooks":[{"hookId":"code-block","selectorAtom":"pre"},{"hookId":"code-inline","selectorAtom":"code"},{"hookId":"code-language-bash","selectorAtom":".language-bash"},{"hookId":"code-language-css","selectorAtom":".language-css"},{"hookId":"code-language-diff","selectorAtom":".language-diff"},{"hookId":"code-language-java","selectorAtom":".language-java"},{"hookId":"code-language-javascript","selectorAtom":".language-javascript"},{"hookId":"code-language-json","selectorAtom":".language-json"},{"hookId":"code-language-jsx","selectorAtom":".language-jsx"},{"hookId":"code-language-markdown","selectorAtom":".language-markdown"},{"hookId":"code-language-markup","selectorAtom":".language-markup"},{"hookId":"code-language-python","selectorAtom":".language-python"},{"hookId":"code-language-sql","selectorAtom":".language-sql"},{"hookId":"code-language-tsx","selectorAtom":".language-tsx"},{"hookId":"code-language-typescript","selectorAtom":".language-typescript"},{"hookId":"code-language-yaml","selectorAtom":".language-yaml"},{"hookId":"code-token","selectorAtom":".token"},{"hookId":"content-article","selectorAtom":"article"},{"hookId":"control-appearance-toggle","selectorAtom":"#gala-appearance-color-mode"},{"hookId":"control-button","selectorAtom":"button"},{"hookId":"disclosure-details","selectorAtom":"details"},{"hookId":"disclosure-summary","selectorAtom":"summary"},{"hookId":"heading-1","selectorAtom":"h1"},{"hookId":"heading-2","selectorAtom":"h2"},{"hookId":"heading-3","selectorAtom":"h3"},{"hookId":"heading-4","selectorAtom":"h4"},{"hookId":"heading-5","selectorAtom":"h5"},{"hookId":"heading-6","selectorAtom":"h6"},{"hookId":"landmark-footer","selectorAtom":"footer"},{"hookId":"landmark-header","selectorAtom":"header"},{"hookId":"landmark-main","selectorAtom":"main"},{"hookId":"landmark-main-content","selectorAtom":"#main-content"},{"hookId":"landmark-navigation","selectorAtom":"nav"},{"hookId":"landmark-section","selectorAtom":"section"},{"hookId":"media-figure","selectorAtom":"figure"},{"hookId":"media-image","selectorAtom":"img"},{"hookId":"page-archive","selectorAtom":"[data-gala-page-kind=\"archive\"]"},{"hookId":"page-article","selectorAtom":"[data-gala-page-kind=\"article\"]"},{"hookId":"page-author","selectorAtom":"[data-gala-page-kind=\"author\"]"},{"hookId":"page-error","selectorAtom":"[data-gala-page-kind=\"error\"]"},{"hookId":"page-index","selectorAtom":"[data-gala-page-kind=\"index\"]"},{"hookId":"page-page","selectorAtom":"[data-gala-page-kind=\"page\"]"},{"hookId":"page-profile","selectorAtom":"[data-gala-page-kind=\"profile\"]"},{"hookId":"page-series","selectorAtom":"[data-gala-page-kind=\"series\"]"},{"hookId":"page-tag","selectorAtom":"[data-gala-page-kind=\"tag\"]"},{"hookId":"prose-blockquote","selectorAtom":"blockquote"},{"hookId":"prose-divider","selectorAtom":"hr"},{"hookId":"prose-emphasis","selectorAtom":"em"},{"hookId":"prose-link","selectorAtom":"a"},{"hookId":"prose-list-item","selectorAtom":"li"},{"hookId":"prose-list-ordered","selectorAtom":"ol"},{"hookId":"prose-list-unordered","selectorAtom":"ul"},{"hookId":"prose-paragraph","selectorAtom":"p"},{"hookId":"prose-span","selectorAtom":"span"},{"hookId":"prose-strong","selectorAtom":"strong"},{"hookId":"prose-time","selectorAtom":"time"},{"hookId":"slot-account-intent","selectorAtom":"[data-gala-slot=\"account-intent\"]"},{"hookId":"slot-article-end","selectorAtom":"[data-gala-slot=\"article-end\"]"},{"hookId":"slot-article-footer-ad","selectorAtom":"[data-gala-slot=\"article-footer-ad\"]"},{"hookId":"slot-article-preamble","selectorAtom":"[data-gala-slot=\"article-preamble\"]"},{"hookId":"slot-conversation","selectorAtom":"[data-gala-slot=\"conversation\"]"},{"hookId":"slot-edition-selector","selectorAtom":"[data-gala-slot=\"edition-selector\"]"},{"hookId":"slot-footer-auxiliary","selectorAtom":"[data-gala-slot=\"footer-auxiliary\"]"},{"hookId":"slot-footer-profile","selectorAtom":"[data-gala-slot=\"footer-profile\"]"},{"hookId":"slot-header-actions","selectorAtom":"[data-gala-slot=\"header-actions\"]"},{"hookId":"slot-newsletter","selectorAtom":"[data-gala-slot=\"newsletter\"]"},{"hookId":"ui-actions","selectorAtom":".g-actions"},{"hookId":"ui-article","selectorAtom":".g-article"},{"hookId":"ui-article-cover","selectorAtom":".g-article-cover"},{"hookId":"ui-article-foot","selectorAtom":".g-article-foot"},{"hookId":"ui-article-grid","selectorAtom":".g-article-grid"},{"hookId":"ui-article-head","selectorAtom":".g-article-head"},{"hookId":"ui-article-labels","selectorAtom":".g-article-labels"},{"hookId":"ui-article-meta","selectorAtom":".g-article-meta"},{"hookId":"ui-author-bio","selectorAtom":".g-author-bio"},{"hookId":"ui-author-card","selectorAtom":".g-author-card"},{"hookId":"ui-author-name","selectorAtom":".g-author-name"},{"hookId":"ui-avatar","selectorAtom":".g-avatar"},{"hookId":"ui-badge","selectorAtom":".g-badge"},{"hookId":"ui-brand","selectorAtom":".g-brand"},{"hookId":"ui-brand-name","selectorAtom":".g-brand-name"},{"hookId":"ui-brand-tag","selectorAtom":".g-brand-tag"},{"hookId":"ui-brand-text","selectorAtom":".g-brand-text"},{"hookId":"ui-btn","selectorAtom":".g-btn"},{"hookId":"ui-byline","selectorAtom":".g-byline"},{"hookId":"ui-byline-text","selectorAtom":".g-byline-text"},{"hookId":"ui-card","selectorAtom":".g-card"},{"hookId":"ui-card-body","selectorAtom":".g-card-body"},{"hookId":"ui-card-excerpt","selectorAtom":".g-card-excerpt"},{"hookId":"ui-card-foot","selectorAtom":".g-card-foot"},{"hookId":"ui-card-link","selectorAtom":".g-card-link"},{"hookId":"ui-card-media","selectorAtom":".g-card-media"},{"hookId":"ui-card-placeholder","selectorAtom":".g-card-placeholder"},{"hookId":"ui-card-row","selectorAtom":".g-card-row"},{"hookId":"ui-card-top","selectorAtom":".g-card-top"},{"hookId":"ui-chip","selectorAtom":".g-chip"},{"hookId":"ui-codebar","selectorAtom":".g-codebar"},{"hookId":"ui-codebar-lang","selectorAtom":".g-codebar-lang"},{"hookId":"ui-codeblock","selectorAtom":".g-codeblock"},{"hookId":"ui-copy","selectorAtom":".g-copy"},{"hookId":"ui-copy-done","selectorAtom":".g-copy-done"},{"hookId":"ui-copy-idle","selectorAtom":".g-copy-idle"},{"hookId":"ui-crumbs","selectorAtom":".g-crumbs"},{"hookId":"ui-dek","selectorAtom":".g-dek"},{"hookId":"ui-dot","selectorAtom":".g-dot"},{"hookId":"ui-eyebrow","selectorAtom":".g-eyebrow"},{"hookId":"ui-footer","selectorAtom":".g-footer"},{"hookId":"ui-footer-about","selectorAtom":".g-footer-about"},{"hookId":"ui-footer-base","selectorAtom":".g-footer-base"},{"hookId":"ui-footer-brand","selectorAtom":".g-footer-brand"},{"hookId":"ui-footer-col","selectorAtom":".g-footer-col"},{"hookId":"ui-footer-grid","selectorAtom":".g-footer-grid"},{"hookId":"ui-grid","selectorAtom":".g-grid"},{"hookId":"ui-header","selectorAtom":".g-header"},{"hookId":"ui-header-row","selectorAtom":".g-header-row"},{"hookId":"ui-hero","selectorAtom":".g-hero"},{"hookId":"ui-hero-body","selectorAtom":".g-hero-body"},{"hookId":"ui-hero-media","selectorAtom":".g-hero-media"},{"hookId":"ui-icon","selectorAtom":".g-icon"},{"hookId":"ui-icon-btn","selectorAtom":".g-icon-btn"},{"hookId":"ui-kbd","selectorAtom":".g-kbd"},{"hookId":"ui-label","selectorAtom":".g-label"},{"hookId":"ui-list","selectorAtom":".g-list"},{"hookId":"ui-main","selectorAtom":".g-main"},{"hookId":"ui-mark","selectorAtom":".g-mark"},{"hookId":"ui-menu","selectorAtom":".g-menu"},{"hookId":"ui-meta","selectorAtom":".g-meta"},{"hookId":"ui-mode-dark","selectorAtom":".g-mode-dark"},{"hookId":"ui-mode-icon","selectorAtom":".g-mode-icon"},{"hookId":"ui-mode-light","selectorAtom":".g-mode-light"},{"hookId":"ui-mode-system","selectorAtom":".g-mode-system"},{"hookId":"ui-more","selectorAtom":".g-more"},{"hookId":"ui-nav","selectorAtom":".g-nav"},{"hookId":"ui-newsletter","selectorAtom":".g-newsletter"},{"hookId":"ui-pager","selectorAtom":".g-pager"},{"hookId":"ui-pager-link","selectorAtom":".g-pager-link"},{"hookId":"ui-pager-next","selectorAtom":".g-pager-next"},{"hookId":"ui-pagination","selectorAtom":".g-pagination"},{"hookId":"ui-panel","selectorAtom":".g-panel"},{"hookId":"ui-panel-copy","selectorAtom":".g-panel-copy"},{"hookId":"ui-pill","selectorAtom":".g-pill"},{"hookId":"ui-pill-count","selectorAtom":".g-pill-count"},{"hookId":"ui-pills","selectorAtom":".g-pills"},{"hookId":"ui-progress","selectorAtom":".g-progress"},{"hookId":"ui-prose","selectorAtom":".g-prose"},{"hookId":"ui-search","selectorAtom":".g-search"},{"hookId":"ui-search-box","selectorAtom":".g-search-box"},{"hookId":"ui-search-btn","selectorAtom":".g-search-btn"},{"hookId":"ui-search-empty","selectorAtom":".g-search-empty"},{"hookId":"ui-search-hit","selectorAtom":".g-search-hit"},{"hookId":"ui-search-hit-meta","selectorAtom":".g-search-hit-meta"},{"hookId":"ui-search-hit-text","selectorAtom":".g-search-hit-text"},{"hookId":"ui-search-hit-title","selectorAtom":".g-search-hit-title"},{"hookId":"ui-search-input","selectorAtom":".g-search-input"},{"hookId":"ui-search-results","selectorAtom":".g-search-results"},{"hookId":"ui-section","selectorAtom":".g-section"},{"hookId":"ui-section-head","selectorAtom":".g-section-head"},{"hookId":"ui-series-box","selectorAtom":".g-series-box"},{"hookId":"ui-series-card","selectorAtom":".g-series-card"},{"hookId":"ui-series-here","selectorAtom":".g-series-here"},{"hookId":"ui-series-list","selectorAtom":".g-series-list"},{"hookId":"ui-series-n","selectorAtom":".g-series-n"},{"hookId":"ui-series-tag","selectorAtom":".g-series-tag"},{"hookId":"ui-share","selectorAtom":".g-share"},{"hookId":"ui-skip","selectorAtom":".g-skip"},{"hookId":"ui-social","selectorAtom":".g-social"},{"hookId":"ui-sr","selectorAtom":".g-sr"},{"hookId":"ui-tag","selectorAtom":".g-tag"},{"hookId":"ui-tags","selectorAtom":".g-tags"},{"hookId":"ui-toast","selectorAtom":".g-toast"},{"hookId":"ui-toc","selectorAtom":".g-toc"},{"hookId":"ui-toc-list","selectorAtom":".g-toc-list"},{"hookId":"ui-toc-mobile","selectorAtom":".g-toc-mobile"},{"hookId":"ui-topic-head","selectorAtom":".g-topic-head"},{"hookId":"ui-topic-hero","selectorAtom":".g-topic-hero"},{"hookId":"ui-topic-icon","selectorAtom":".g-topic-icon"},{"hookId":"ui-topic-strip","selectorAtom":".g-topic-strip"},{"hookId":"ui-wrap","selectorAtom":".g-wrap"}],"publicationRootSelector":"[data-gala-publication-root]","resolvedPaletteSelectors":{"dark":"[data-gala-publication-root][data-gala-resolved-color-mode=\"dark\"]","light":"[data-gala-publication-root][data-gala-resolved-color-mode=\"light\"]"},"stylesheetLayers":{"components":"gala-components","print":"gala-print","tokens":"gala-tokens","utilities":"gala-utilities"},"templatePackage":"@rathnasgala2/template","templateVersion":"3.0.0","typeSelectors":["a","article","blockquote","button","code","details","em","figure","footer","h1","h2","h3","h4","h5","h6","header","hr","img","li","main","nav","ol","p","pre","section","span","strong","summary","time","ul"]}
1
+ {"attributes":[{"match":"exact-value","name":"data-gala-article-layout","role":"semantic","values":["editorial","full-bleed","issue","prose","technical"]},{"match":"exact-value","name":"data-gala-author-layout","role":"semantic","values":["biography","portfolio","portrait","profile"]},{"match":"exact-value","name":"data-gala-density","role":"semantic","values":["comfortable","compact","spacious"]},{"match":"exact-value","name":"data-gala-footer-layout","role":"semantic","values":["compact","expanded"]},{"match":"exact-value","name":"data-gala-header-layout","role":"semantic","values":["compact","masthead","minimal","publication","utility"]},{"match":"exact-value","name":"data-gala-home-layout","role":"semantic","values":["chronological","gallery","magazine","newspaper","topic-index"]},{"match":"exact-value","name":"data-gala-listing-layout","role":"semantic","values":["dated-rows","image-grid","issues","rows"]},{"match":"exact-value","name":"data-gala-media-ratio","role":"semantic","values":["landscape","mixed"]},{"match":"exact-value","name":"data-gala-page-kind","role":"semantic","values":["archive","article","author","error","index","page","profile","series","tag"]},{"match":"exact-value","name":"data-gala-profile","role":"semantic","values":["developer","editorial","newsletter","personal","visual"]},{"match":"exact-value","name":"data-gala-resolved-color-mode","role":"state","values":["dark","light"]},{"match":"exact-value","name":"data-gala-slot","role":"semantic","values":["account-intent","article-end","article-footer-ad","article-preamble","conversation","edition-selector","footer-auxiliary","footer-profile","header-actions","newsletter"]},{"match":"presence","name":"data-gala-publication-root","role":"root","values":[]}],"catalogDigest":"sha256:63a76754b591a3cda91a4be1f43bdb3fe1d44cf4f9be766154f3944a043399b5","classSelectors":["g-actions","g-article","g-article-cover","g-article-cover-full-bleed","g-article-foot","g-article-grid","g-article-head","g-article-labels","g-article-meta","g-author-bio","g-author-card","g-author-name","g-avatar","g-badge","g-brand","g-brand-name","g-brand-tag","g-brand-text","g-btn","g-byline","g-byline-text","g-card","g-card-body","g-card-excerpt","g-card-foot","g-card-link","g-card-media","g-card-placeholder","g-card-row","g-card-top","g-chip","g-codebar","g-codebar-lang","g-codeblock","g-copy","g-copy-done","g-copy-idle","g-crumbs","g-date-stamp","g-dated-list","g-dated-row","g-dek","g-dot","g-eyebrow","g-footer","g-footer-about","g-footer-base","g-footer-brand","g-footer-col","g-footer-grid","g-grid","g-header","g-header-row","g-hero","g-hero-body","g-hero-media","g-icon","g-icon-btn","g-kbd","g-label","g-list","g-main","g-mark","g-menu","g-meta","g-mode-dark","g-mode-icon","g-mode-light","g-mode-system","g-more","g-nav","g-newsletter","g-pager","g-pager-link","g-pager-next","g-pagination","g-panel","g-panel-copy","g-pill","g-pill-count","g-pills","g-progress","g-prose","g-search","g-search-box","g-search-btn","g-search-empty","g-search-hit","g-search-hit-meta","g-search-hit-text","g-search-hit-title","g-search-input","g-search-results","g-section","g-section-head","g-series-box","g-series-card","g-series-here","g-series-list","g-series-n","g-series-tag","g-share","g-skip","g-social","g-sr","g-tag","g-tags","g-toast","g-toc","g-toc-list","g-toc-mobile","g-topic-head","g-topic-hero","g-topic-icon","g-topic-strip","g-wrap","language-bash","language-css","language-diff","language-java","language-javascript","language-json","language-jsx","language-markdown","language-markup","language-python","language-sql","language-tsx","language-typescript","language-yaml","token"],"combinators":[" "," > "," + "," ~ "],"composition":{"compoundOrder":["type","id","class","attribute","pseudo-class","pseudo-element"],"functionalPseudoKeywordArguments":["even","odd"],"functionalSelectorArguments":"compound-only","maximumFunctionalDepth":1,"nthExpressionProfile":"gala-positive-an-plus-b-v2","rootScope":"first-compound-required"},"contractVersion":"4.0.0","functionalPseudos":["is","where","not","nth-child","nth-last-child"],"idSelectors":["gala-appearance-color-mode","main-content"],"orderedLayers":["gala-base","gala-tokens","gala-components","gala-utilities","gala-print"],"profile":"gala-template-styling-contract-v2","pseudoClasses":["active","disabled","focus-visible","hover","visited"],"pseudoElements":["after","before","marker","selection"],"publicThemeSlotHooks":[{"hookId":"code-block","selectorAtom":"pre"},{"hookId":"code-inline","selectorAtom":"code"},{"hookId":"code-language-bash","selectorAtom":".language-bash"},{"hookId":"code-language-css","selectorAtom":".language-css"},{"hookId":"code-language-diff","selectorAtom":".language-diff"},{"hookId":"code-language-java","selectorAtom":".language-java"},{"hookId":"code-language-javascript","selectorAtom":".language-javascript"},{"hookId":"code-language-json","selectorAtom":".language-json"},{"hookId":"code-language-jsx","selectorAtom":".language-jsx"},{"hookId":"code-language-markdown","selectorAtom":".language-markdown"},{"hookId":"code-language-markup","selectorAtom":".language-markup"},{"hookId":"code-language-python","selectorAtom":".language-python"},{"hookId":"code-language-sql","selectorAtom":".language-sql"},{"hookId":"code-language-tsx","selectorAtom":".language-tsx"},{"hookId":"code-language-typescript","selectorAtom":".language-typescript"},{"hookId":"code-language-yaml","selectorAtom":".language-yaml"},{"hookId":"code-token","selectorAtom":".token"},{"hookId":"composition-article-layout-editorial","selectorAtom":"[data-gala-article-layout=\"editorial\"]"},{"hookId":"composition-article-layout-full-bleed","selectorAtom":"[data-gala-article-layout=\"full-bleed\"]"},{"hookId":"composition-article-layout-issue","selectorAtom":"[data-gala-article-layout=\"issue\"]"},{"hookId":"composition-article-layout-prose","selectorAtom":"[data-gala-article-layout=\"prose\"]"},{"hookId":"composition-article-layout-technical","selectorAtom":"[data-gala-article-layout=\"technical\"]"},{"hookId":"composition-author-layout-biography","selectorAtom":"[data-gala-author-layout=\"biography\"]"},{"hookId":"composition-author-layout-portfolio","selectorAtom":"[data-gala-author-layout=\"portfolio\"]"},{"hookId":"composition-author-layout-portrait","selectorAtom":"[data-gala-author-layout=\"portrait\"]"},{"hookId":"composition-author-layout-profile","selectorAtom":"[data-gala-author-layout=\"profile\"]"},{"hookId":"composition-density-comfortable","selectorAtom":"[data-gala-density=\"comfortable\"]"},{"hookId":"composition-density-compact","selectorAtom":"[data-gala-density=\"compact\"]"},{"hookId":"composition-density-spacious","selectorAtom":"[data-gala-density=\"spacious\"]"},{"hookId":"composition-footer-layout-compact","selectorAtom":"[data-gala-footer-layout=\"compact\"]"},{"hookId":"composition-footer-layout-expanded","selectorAtom":"[data-gala-footer-layout=\"expanded\"]"},{"hookId":"composition-header-layout-compact","selectorAtom":"[data-gala-header-layout=\"compact\"]"},{"hookId":"composition-header-layout-masthead","selectorAtom":"[data-gala-header-layout=\"masthead\"]"},{"hookId":"composition-header-layout-minimal","selectorAtom":"[data-gala-header-layout=\"minimal\"]"},{"hookId":"composition-header-layout-publication","selectorAtom":"[data-gala-header-layout=\"publication\"]"},{"hookId":"composition-header-layout-utility","selectorAtom":"[data-gala-header-layout=\"utility\"]"},{"hookId":"composition-home-layout-chronological","selectorAtom":"[data-gala-home-layout=\"chronological\"]"},{"hookId":"composition-home-layout-gallery","selectorAtom":"[data-gala-home-layout=\"gallery\"]"},{"hookId":"composition-home-layout-magazine","selectorAtom":"[data-gala-home-layout=\"magazine\"]"},{"hookId":"composition-home-layout-newspaper","selectorAtom":"[data-gala-home-layout=\"newspaper\"]"},{"hookId":"composition-home-layout-topic-index","selectorAtom":"[data-gala-home-layout=\"topic-index\"]"},{"hookId":"composition-listing-layout-dated-rows","selectorAtom":"[data-gala-listing-layout=\"dated-rows\"]"},{"hookId":"composition-listing-layout-image-grid","selectorAtom":"[data-gala-listing-layout=\"image-grid\"]"},{"hookId":"composition-listing-layout-issues","selectorAtom":"[data-gala-listing-layout=\"issues\"]"},{"hookId":"composition-listing-layout-rows","selectorAtom":"[data-gala-listing-layout=\"rows\"]"},{"hookId":"composition-media-ratio-landscape","selectorAtom":"[data-gala-media-ratio=\"landscape\"]"},{"hookId":"composition-media-ratio-mixed","selectorAtom":"[data-gala-media-ratio=\"mixed\"]"},{"hookId":"composition-profile-developer","selectorAtom":"[data-gala-profile=\"developer\"]"},{"hookId":"composition-profile-editorial","selectorAtom":"[data-gala-profile=\"editorial\"]"},{"hookId":"composition-profile-newsletter","selectorAtom":"[data-gala-profile=\"newsletter\"]"},{"hookId":"composition-profile-personal","selectorAtom":"[data-gala-profile=\"personal\"]"},{"hookId":"composition-profile-visual","selectorAtom":"[data-gala-profile=\"visual\"]"},{"hookId":"content-article","selectorAtom":"article"},{"hookId":"control-appearance-toggle","selectorAtom":"#gala-appearance-color-mode"},{"hookId":"control-button","selectorAtom":"button"},{"hookId":"disclosure-details","selectorAtom":"details"},{"hookId":"disclosure-summary","selectorAtom":"summary"},{"hookId":"heading-1","selectorAtom":"h1"},{"hookId":"heading-2","selectorAtom":"h2"},{"hookId":"heading-3","selectorAtom":"h3"},{"hookId":"heading-4","selectorAtom":"h4"},{"hookId":"heading-5","selectorAtom":"h5"},{"hookId":"heading-6","selectorAtom":"h6"},{"hookId":"landmark-footer","selectorAtom":"footer"},{"hookId":"landmark-header","selectorAtom":"header"},{"hookId":"landmark-main","selectorAtom":"main"},{"hookId":"landmark-main-content","selectorAtom":"#main-content"},{"hookId":"landmark-navigation","selectorAtom":"nav"},{"hookId":"landmark-section","selectorAtom":"section"},{"hookId":"media-figure","selectorAtom":"figure"},{"hookId":"media-image","selectorAtom":"img"},{"hookId":"page-archive","selectorAtom":"[data-gala-page-kind=\"archive\"]"},{"hookId":"page-article","selectorAtom":"[data-gala-page-kind=\"article\"]"},{"hookId":"page-author","selectorAtom":"[data-gala-page-kind=\"author\"]"},{"hookId":"page-error","selectorAtom":"[data-gala-page-kind=\"error\"]"},{"hookId":"page-index","selectorAtom":"[data-gala-page-kind=\"index\"]"},{"hookId":"page-page","selectorAtom":"[data-gala-page-kind=\"page\"]"},{"hookId":"page-profile","selectorAtom":"[data-gala-page-kind=\"profile\"]"},{"hookId":"page-series","selectorAtom":"[data-gala-page-kind=\"series\"]"},{"hookId":"page-tag","selectorAtom":"[data-gala-page-kind=\"tag\"]"},{"hookId":"prose-blockquote","selectorAtom":"blockquote"},{"hookId":"prose-divider","selectorAtom":"hr"},{"hookId":"prose-emphasis","selectorAtom":"em"},{"hookId":"prose-link","selectorAtom":"a"},{"hookId":"prose-list-item","selectorAtom":"li"},{"hookId":"prose-list-ordered","selectorAtom":"ol"},{"hookId":"prose-list-unordered","selectorAtom":"ul"},{"hookId":"prose-paragraph","selectorAtom":"p"},{"hookId":"prose-span","selectorAtom":"span"},{"hookId":"prose-strong","selectorAtom":"strong"},{"hookId":"prose-time","selectorAtom":"time"},{"hookId":"slot-account-intent","selectorAtom":"[data-gala-slot=\"account-intent\"]"},{"hookId":"slot-article-end","selectorAtom":"[data-gala-slot=\"article-end\"]"},{"hookId":"slot-article-footer-ad","selectorAtom":"[data-gala-slot=\"article-footer-ad\"]"},{"hookId":"slot-article-preamble","selectorAtom":"[data-gala-slot=\"article-preamble\"]"},{"hookId":"slot-conversation","selectorAtom":"[data-gala-slot=\"conversation\"]"},{"hookId":"slot-edition-selector","selectorAtom":"[data-gala-slot=\"edition-selector\"]"},{"hookId":"slot-footer-auxiliary","selectorAtom":"[data-gala-slot=\"footer-auxiliary\"]"},{"hookId":"slot-footer-profile","selectorAtom":"[data-gala-slot=\"footer-profile\"]"},{"hookId":"slot-header-actions","selectorAtom":"[data-gala-slot=\"header-actions\"]"},{"hookId":"slot-newsletter","selectorAtom":"[data-gala-slot=\"newsletter\"]"},{"hookId":"ui-actions","selectorAtom":".g-actions"},{"hookId":"ui-article","selectorAtom":".g-article"},{"hookId":"ui-article-cover","selectorAtom":".g-article-cover"},{"hookId":"ui-article-cover-full-bleed","selectorAtom":".g-article-cover-full-bleed"},{"hookId":"ui-article-foot","selectorAtom":".g-article-foot"},{"hookId":"ui-article-grid","selectorAtom":".g-article-grid"},{"hookId":"ui-article-head","selectorAtom":".g-article-head"},{"hookId":"ui-article-labels","selectorAtom":".g-article-labels"},{"hookId":"ui-article-meta","selectorAtom":".g-article-meta"},{"hookId":"ui-author-bio","selectorAtom":".g-author-bio"},{"hookId":"ui-author-card","selectorAtom":".g-author-card"},{"hookId":"ui-author-name","selectorAtom":".g-author-name"},{"hookId":"ui-avatar","selectorAtom":".g-avatar"},{"hookId":"ui-badge","selectorAtom":".g-badge"},{"hookId":"ui-brand","selectorAtom":".g-brand"},{"hookId":"ui-brand-name","selectorAtom":".g-brand-name"},{"hookId":"ui-brand-tag","selectorAtom":".g-brand-tag"},{"hookId":"ui-brand-text","selectorAtom":".g-brand-text"},{"hookId":"ui-btn","selectorAtom":".g-btn"},{"hookId":"ui-byline","selectorAtom":".g-byline"},{"hookId":"ui-byline-text","selectorAtom":".g-byline-text"},{"hookId":"ui-card","selectorAtom":".g-card"},{"hookId":"ui-card-body","selectorAtom":".g-card-body"},{"hookId":"ui-card-excerpt","selectorAtom":".g-card-excerpt"},{"hookId":"ui-card-foot","selectorAtom":".g-card-foot"},{"hookId":"ui-card-link","selectorAtom":".g-card-link"},{"hookId":"ui-card-media","selectorAtom":".g-card-media"},{"hookId":"ui-card-placeholder","selectorAtom":".g-card-placeholder"},{"hookId":"ui-card-row","selectorAtom":".g-card-row"},{"hookId":"ui-card-top","selectorAtom":".g-card-top"},{"hookId":"ui-chip","selectorAtom":".g-chip"},{"hookId":"ui-codebar","selectorAtom":".g-codebar"},{"hookId":"ui-codebar-lang","selectorAtom":".g-codebar-lang"},{"hookId":"ui-codeblock","selectorAtom":".g-codeblock"},{"hookId":"ui-copy","selectorAtom":".g-copy"},{"hookId":"ui-copy-done","selectorAtom":".g-copy-done"},{"hookId":"ui-copy-idle","selectorAtom":".g-copy-idle"},{"hookId":"ui-crumbs","selectorAtom":".g-crumbs"},{"hookId":"ui-date-stamp","selectorAtom":".g-date-stamp"},{"hookId":"ui-dated-list","selectorAtom":".g-dated-list"},{"hookId":"ui-dated-row","selectorAtom":".g-dated-row"},{"hookId":"ui-dek","selectorAtom":".g-dek"},{"hookId":"ui-dot","selectorAtom":".g-dot"},{"hookId":"ui-eyebrow","selectorAtom":".g-eyebrow"},{"hookId":"ui-footer","selectorAtom":".g-footer"},{"hookId":"ui-footer-about","selectorAtom":".g-footer-about"},{"hookId":"ui-footer-base","selectorAtom":".g-footer-base"},{"hookId":"ui-footer-brand","selectorAtom":".g-footer-brand"},{"hookId":"ui-footer-col","selectorAtom":".g-footer-col"},{"hookId":"ui-footer-grid","selectorAtom":".g-footer-grid"},{"hookId":"ui-grid","selectorAtom":".g-grid"},{"hookId":"ui-header","selectorAtom":".g-header"},{"hookId":"ui-header-row","selectorAtom":".g-header-row"},{"hookId":"ui-hero","selectorAtom":".g-hero"},{"hookId":"ui-hero-body","selectorAtom":".g-hero-body"},{"hookId":"ui-hero-media","selectorAtom":".g-hero-media"},{"hookId":"ui-icon","selectorAtom":".g-icon"},{"hookId":"ui-icon-btn","selectorAtom":".g-icon-btn"},{"hookId":"ui-kbd","selectorAtom":".g-kbd"},{"hookId":"ui-label","selectorAtom":".g-label"},{"hookId":"ui-list","selectorAtom":".g-list"},{"hookId":"ui-main","selectorAtom":".g-main"},{"hookId":"ui-mark","selectorAtom":".g-mark"},{"hookId":"ui-menu","selectorAtom":".g-menu"},{"hookId":"ui-meta","selectorAtom":".g-meta"},{"hookId":"ui-mode-dark","selectorAtom":".g-mode-dark"},{"hookId":"ui-mode-icon","selectorAtom":".g-mode-icon"},{"hookId":"ui-mode-light","selectorAtom":".g-mode-light"},{"hookId":"ui-mode-system","selectorAtom":".g-mode-system"},{"hookId":"ui-more","selectorAtom":".g-more"},{"hookId":"ui-nav","selectorAtom":".g-nav"},{"hookId":"ui-newsletter","selectorAtom":".g-newsletter"},{"hookId":"ui-pager","selectorAtom":".g-pager"},{"hookId":"ui-pager-link","selectorAtom":".g-pager-link"},{"hookId":"ui-pager-next","selectorAtom":".g-pager-next"},{"hookId":"ui-pagination","selectorAtom":".g-pagination"},{"hookId":"ui-panel","selectorAtom":".g-panel"},{"hookId":"ui-panel-copy","selectorAtom":".g-panel-copy"},{"hookId":"ui-pill","selectorAtom":".g-pill"},{"hookId":"ui-pill-count","selectorAtom":".g-pill-count"},{"hookId":"ui-pills","selectorAtom":".g-pills"},{"hookId":"ui-progress","selectorAtom":".g-progress"},{"hookId":"ui-prose","selectorAtom":".g-prose"},{"hookId":"ui-search","selectorAtom":".g-search"},{"hookId":"ui-search-box","selectorAtom":".g-search-box"},{"hookId":"ui-search-btn","selectorAtom":".g-search-btn"},{"hookId":"ui-search-empty","selectorAtom":".g-search-empty"},{"hookId":"ui-search-hit","selectorAtom":".g-search-hit"},{"hookId":"ui-search-hit-meta","selectorAtom":".g-search-hit-meta"},{"hookId":"ui-search-hit-text","selectorAtom":".g-search-hit-text"},{"hookId":"ui-search-hit-title","selectorAtom":".g-search-hit-title"},{"hookId":"ui-search-input","selectorAtom":".g-search-input"},{"hookId":"ui-search-results","selectorAtom":".g-search-results"},{"hookId":"ui-section","selectorAtom":".g-section"},{"hookId":"ui-section-head","selectorAtom":".g-section-head"},{"hookId":"ui-series-box","selectorAtom":".g-series-box"},{"hookId":"ui-series-card","selectorAtom":".g-series-card"},{"hookId":"ui-series-here","selectorAtom":".g-series-here"},{"hookId":"ui-series-list","selectorAtom":".g-series-list"},{"hookId":"ui-series-n","selectorAtom":".g-series-n"},{"hookId":"ui-series-tag","selectorAtom":".g-series-tag"},{"hookId":"ui-share","selectorAtom":".g-share"},{"hookId":"ui-skip","selectorAtom":".g-skip"},{"hookId":"ui-social","selectorAtom":".g-social"},{"hookId":"ui-sr","selectorAtom":".g-sr"},{"hookId":"ui-tag","selectorAtom":".g-tag"},{"hookId":"ui-tags","selectorAtom":".g-tags"},{"hookId":"ui-toast","selectorAtom":".g-toast"},{"hookId":"ui-toc","selectorAtom":".g-toc"},{"hookId":"ui-toc-list","selectorAtom":".g-toc-list"},{"hookId":"ui-toc-mobile","selectorAtom":".g-toc-mobile"},{"hookId":"ui-topic-head","selectorAtom":".g-topic-head"},{"hookId":"ui-topic-hero","selectorAtom":".g-topic-hero"},{"hookId":"ui-topic-icon","selectorAtom":".g-topic-icon"},{"hookId":"ui-topic-strip","selectorAtom":".g-topic-strip"},{"hookId":"ui-wrap","selectorAtom":".g-wrap"}],"publicationRootSelector":"[data-gala-publication-root]","resolvedPaletteSelectors":{"dark":"[data-gala-publication-root][data-gala-resolved-color-mode=\"dark\"]","light":"[data-gala-publication-root][data-gala-resolved-color-mode=\"light\"]"},"stylesheetLayers":{"components":"gala-components","print":"gala-print","tokens":"gala-tokens","utilities":"gala-utilities"},"templatePackage":"@rathnasgala2/template","templateVersion":"4.0.0","typeSelectors":["a","article","blockquote","button","code","details","em","figure","footer","h1","h2","h3","h4","h5","h6","header","hr","img","li","main","nav","ol","p","pre","section","span","strong","summary","time","ul"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rathnasgala2/template",
3
- "version": "3.2.1",
3
+ "version": "4.0.0",
4
4
  "description": "Deterministic Eleventy-backed static publication renderer for Galascribe portable repositories.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -72,11 +72,13 @@
72
72
  "dependencies": {
73
73
  "@11ty/eleventy": "3.1.6",
74
74
  "@11ty/eleventy-plugin-syntaxhighlight": "5.0.2",
75
- "@rathnasgala2/schemas": "3.3.0",
76
75
  "jpeg-js": "0.4.4",
77
76
  "markdown-it": "14.3.1",
78
77
  "sanitize-html": "2.17.7"
79
78
  },
79
+ "peerDependencies": {
80
+ "@rathnasgala2/schemas": "3.8.0"
81
+ },
80
82
  "devDependencies": {
81
83
  "@cyclonedx/cyclonedx-npm": "6.0.1",
82
84
  "@eslint/js": "10.0.1",
package/src/core/index.js CHANGED
@@ -139,6 +139,7 @@ import {
139
139
  defaultThemeStylesheetLinksHtml,
140
140
  loadThemeAssets,
141
141
  } from './internal/theme-assets.js';
142
+ import { EDITORIAL_COMPOSITION } from './internal/template-composition.js';
142
143
  import { resolveTextDirection } from './internal/text-direction.js';
143
144
  import {
144
145
  escapeHtml,
@@ -189,7 +190,7 @@ export const TEMPLATE_PACKAGE_NAME = '@rathnasgala2/template';
189
190
  *
190
191
  * @type {string}
191
192
  */
192
- export const TEMPLATE_PACKAGE_VERSION = '2.1.0';
193
+ export const TEMPLATE_PACKAGE_VERSION = '4.0.0';
193
194
 
194
195
  /**
195
196
  * Validate the caller-supplied render options, independent of build-input
@@ -391,6 +392,22 @@ export async function renderPublication(buildInput, options) {
391
392
  const urlPrefix = derivePublicBasePath(validatedInput.baseUrl, '/');
392
393
  const homeRoute = joinPublicRoute(publicBasePath, '/');
393
394
 
395
+ // Admit the selected generation-4 theme and its complete structural
396
+ // composition before any output directory is created. Theme-less calls
397
+ // use the renderer's Editorial composition and the conventional print
398
+ // stylesheet link; a supplied theme never receives that fallback.
399
+ const themeAssetsResult = options.themeDirectory
400
+ ? await loadThemeAssets({
401
+ themeDirectory: path.resolve(options.themeDirectory),
402
+ basePath: validatedInput.basePath,
403
+ publicBasePath,
404
+ })
405
+ : undefined;
406
+ const themeStylesheetLinksHtml = themeAssetsResult
407
+ ? themeAssetsResult.linkTagsHtml
408
+ : defaultThemeStylesheetLinksHtml(publicBasePath);
409
+ const composition = themeAssetsResult?.composition ?? EDITORIAL_COMPOSITION;
410
+
394
411
  const appearanceControlHtml = renderAppearanceControl({
395
412
  messages: siteMessages,
396
413
  });
@@ -495,6 +512,8 @@ export async function renderPublication(buildInput, options) {
495
512
  showAttribution:
496
513
  validatedInput.appearance.attribution?.showMadeWith !== false,
497
514
  messages: siteMessages,
515
+ profile: composition.profile,
516
+ footerLayout: composition.footerLayout,
498
517
  });
499
518
 
500
519
  /**
@@ -522,6 +541,8 @@ export async function renderPublication(buildInput, options) {
522
541
  searchIndexHref,
523
542
  appearanceControlHtml,
524
543
  messages: siteMessages,
544
+ profile: composition.profile,
545
+ headerLayout: composition.headerLayout,
525
546
  });
526
547
 
527
548
  // The site-wide feeds, resolved once so every page's feed-discovery
@@ -540,22 +561,6 @@ export async function renderPublication(buildInput, options) {
540
561
  `/${GALA_BASE_STYLESHEET_PATH}`,
541
562
  );
542
563
 
543
- // The selected theme package's copied stylesheets/passive assets and the
544
- // ordered `<link>` markup every generated page's `<head>` inserts.
545
- // Run before page assembly (same reasoning as the media pipeline above):
546
- // every page's `data.themeStylesheetLinksHtml` needs the same one
547
- // already-rendered fragment.
548
- const themeAssetsResult = options.themeDirectory
549
- ? await loadThemeAssets({
550
- themeDirectory: path.resolve(options.themeDirectory),
551
- basePath: validatedInput.basePath,
552
- publicBasePath,
553
- })
554
- : undefined;
555
- const themeStylesheetLinksHtml = themeAssetsResult
556
- ? themeAssetsResult.linkTagsHtml
557
- : defaultThemeStylesheetLinksHtml(publicBasePath);
558
-
559
564
  /** @type {{virtualPath: string, content: string, permalink: string, layout?: string, data?: Record<string, unknown>}[]} */
560
565
  const contentPages = [];
561
566
  /** @type {{joinedSource: string, joinedTarget: string, publicTarget: string, filePath: string}[]} */
@@ -567,6 +572,7 @@ export async function renderPublication(buildInput, options) {
567
572
  mediaAssets,
568
573
  mediaDimensions,
569
574
  interactions: interactionsOn ? interactionsModule : undefined,
575
+ composition,
570
576
  });
571
577
  /** @type {Set<string>} output paths of the interaction-bearing routes */
572
578
  const interactionBearingPaths = new Set();
@@ -629,6 +635,7 @@ export async function renderPublication(buildInput, options) {
629
635
  cspMetaTag,
630
636
  baseStylesheetHref: baseStylesheetHrefValue,
631
637
  themeStylesheetLinksHtml,
638
+ composition,
632
639
  },
633
640
  });
634
641
  });
@@ -701,6 +708,7 @@ export async function renderPublication(buildInput, options) {
701
708
  cspMetaTag,
702
709
  baseStylesheetHref: baseStylesheetHrefValue,
703
710
  themeStylesheetLinksHtml,
711
+ composition,
704
712
  },
705
713
  });
706
714
 
@@ -259,7 +259,7 @@ export const GALA_BASE_COMPONENT_CSS = `
259
259
  outline-offset: 3px;
260
260
  }
261
261
  [data-gala-publication-root] img { display: block; max-width: 100%; height: auto; }
262
- [data-gala-publication-root] a { color: inherit; text-decoration: none; }
262
+ [data-gala-publication-root] :where(a) { color: inherit; text-decoration: none; }
263
263
  [data-gala-publication-root] h1, [data-gala-publication-root] h2, [data-gala-publication-root] h3, [data-gala-publication-root] p { margin: 0; }
264
264
  [data-gala-publication-root] button { font: inherit; color: inherit; }
265
265
  [data-gala-publication-root] ul, [data-gala-publication-root] ol { margin: 0; padding: 0; list-style: none; }
@@ -483,6 +483,12 @@ export const GALA_BASE_COMPONENT_CSS = `
483
483
  .g-card-row .g-card-media, .g-card-row .g-card-placeholder { aspect-ratio: 16 / 9; height: 100%; border-bottom: 0; border-right: var(--gala-border-media-divider); }
484
484
  .g-card-row .g-card-body { padding: clamp(1rem, 2.5vw, 1.75rem); }
485
485
  .g-card-row h2, .g-card-row h3 { font-size: clamp(1.3rem, 1rem + 1vw, 1.75rem); }
486
+ .g-dated-list { gap: 0; }
487
+ .g-dated-row { display: grid; grid-template-columns: minmax(8rem, 1fr) minmax(0, 5fr); gap: clamp(1rem, 3vw, 2rem); align-items: start; padding-block: var(--gala-space-4); border-top: var(--gala-border-row-divider); }
488
+ .g-date-stamp { padding-top: var(--gala-space-3); color: var(--gala-color-text-muted); font-family: var(--gala-font-ui); font-size: .8125rem; font-variant-numeric: tabular-nums; }
489
+ .g-dated-row .g-card-row { grid-template-columns: minmax(0, 1fr); padding: 0; border-bottom: 0; box-shadow: none; }
490
+ .g-dated-row .g-card-row .g-card-media, .g-dated-row .g-card-row .g-card-placeholder { display: none; }
491
+ .g-dated-row .g-card-foot .g-meta > time:first-child, .g-dated-row .g-card-foot .g-meta > time:first-child + .g-dot { display: none; }
486
492
 
487
493
  /* ---------- series ---------- */
488
494
  .g-series-card {
@@ -527,6 +533,12 @@ export const GALA_BASE_COMPONENT_CSS = `
527
533
  [data-gala-slot="edition-selector"] { display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--gala-space-2); min-width: 0; max-width: 100%; }
528
534
  .g-article-cover { margin-block: 0; }
529
535
  .g-article-cover img { width: 100%; height: auto; aspect-ratio: 21 / 9; object-fit: cover; border-radius: var(--gala-radius-large); border: var(--gala-border-card); box-shadow: var(--gala-shadow-card); filter: var(--gala-media-filter); }
536
+ .g-article-cover-full-bleed { width: 100%; max-width: none; margin: 0; padding-inline: 0; }
537
+ .g-article-cover-full-bleed img { aspect-ratio: 16 / 9; border-inline: 0; border-radius: 0; }
538
+ [data-gala-media-ratio="mixed"] .g-hero-media { aspect-ratio: 3 / 2; }
539
+ [data-gala-media-ratio="mixed"] .g-grid > .g-card:nth-child(3n + 1) :is(.g-card-media, .g-card-placeholder) { aspect-ratio: 4 / 5; }
540
+ [data-gala-media-ratio="mixed"] .g-grid > .g-card:nth-child(3n + 2) :is(.g-card-media, .g-card-placeholder) { aspect-ratio: 3 / 2; }
541
+ [data-gala-media-ratio="mixed"] .g-grid > .g-card:nth-child(3n) :is(.g-card-media, .g-card-placeholder) { aspect-ratio: 1; }
530
542
  .g-article-grid { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, var(--gala-content-measure)) minmax(0, 1fr); gap: clamp(1.5rem, 4vw, 3.5rem); padding-block: clamp(2rem, 5vw, 3.5rem) var(--gala-space-4); }
531
543
  .g-article-grid > .g-prose { grid-column: 2; }
532
544
  .g-toc { position: sticky; top: 6rem; align-self: start; justify-self: end; width: min(100%, 15rem); display: grid; gap: var(--gala-space-3); }
@@ -810,6 +822,8 @@ export const GALA_BASE_COMPONENT_CSS = `
810
822
  .g-grid { grid-template-columns: minmax(0, 1fr); }
811
823
  .g-card-row { grid-template-columns: minmax(0, 1fr); }
812
824
  .g-card-row .g-card-media, .g-card-row .g-card-placeholder { border-right: 0; border-bottom: var(--gala-border-media-divider); height: auto; }
825
+ .g-dated-row { grid-template-columns: minmax(0, 1fr); gap: var(--gala-space-2); }
826
+ .g-date-stamp { padding-top: 0; }
813
827
  .g-pager { grid-template-columns: minmax(0, 1fr); }
814
828
  .g-pager-next { grid-column: 1; }
815
829
  .g-article-cover img { aspect-ratio: 16 / 10; }
@@ -832,7 +846,7 @@ export const GALA_BASE_COMPONENT_CSS = `
832
846
  @keyframes g-read { from { transform: scaleX(0); } to { transform: scaleX(1); } }
833
847
  }
834
848
  @supports (animation-timeline: view()) {
835
- .g-grid > .g-card, .g-list > .g-card, .g-panel, .g-series-card, .g-author-card, .g-series-box, .g-prose > pre, .g-codeblock, .g-prose > figure, .g-prose > blockquote {
849
+ .g-grid > .g-card, .g-list > .g-card, .g-dated-row, .g-panel, .g-series-card, .g-author-card, .g-series-box, .g-prose > pre, .g-codeblock, .g-prose > figure, .g-prose > blockquote {
836
850
  animation: g-reveal linear both; animation-timeline: view(); animation-range: entry 0% entry 55%;
837
851
  }
838
852
  @keyframes g-reveal { from { opacity: .15; transform: translateY(28px) scale(.985); } }
@@ -74,15 +74,19 @@ import {
74
74
  APPEARANCE_TOGGLE_ID,
75
75
  } from './contract.js';
76
76
  import { PAGE_KIND_ATTRIBUTE, PAGE_KIND_VALUES } from '../page-kinds.js';
77
+ import {
78
+ COMPOSITION_ATTRIBUTES,
79
+ COMPOSITION_ATTRIBUTE_CATALOG,
80
+ } from '../template-composition.js';
77
81
  import { SLOT_NAMES } from '../skeleton.js';
78
82
  import { canonicalizeJcs, domainDigest } from '../canonical-jcs.js';
79
83
 
80
84
  /** @type {string} */
81
85
  export const TEMPLATE_STYLING_CONTRACT_PACKAGE = '@rathnasgala2/template';
82
86
  /** @type {string} */
83
- export const TEMPLATE_STYLING_CONTRACT_VERSION = '3.0.0';
87
+ export const TEMPLATE_STYLING_CONTRACT_VERSION = '4.0.0';
84
88
  /** @type {string} */
85
- export const TEMPLATE_STYLING_CONTRACT_TEMPLATE_VERSION = '3.0.0';
89
+ export const TEMPLATE_STYLING_CONTRACT_TEMPLATE_VERSION = '4.0.0';
86
90
 
87
91
  /** @type {string} the `main-content` landmark's fixed `id`. */
88
92
  const MAIN_CONTENT_ID = 'main-content';
@@ -278,6 +282,9 @@ export const COMPONENT_CLASS_GROUPS = Object.freeze({
278
282
  'g-section-head',
279
283
  'g-grid',
280
284
  'g-list',
285
+ 'g-dated-list',
286
+ 'g-dated-row',
287
+ 'g-date-stamp',
281
288
  'g-pills',
282
289
  'g-pill',
283
290
  'g-pill-count',
@@ -343,6 +350,7 @@ export const COMPONENT_CLASS_GROUPS = Object.freeze({
343
350
  'g-article-meta',
344
351
  'g-share',
345
352
  'g-article-cover',
353
+ 'g-article-cover-full-bleed',
346
354
  'g-article-grid',
347
355
  'g-toc',
348
356
  'g-toc-list',
@@ -468,6 +476,13 @@ const ATTRIBUTE_VALUE_HOOKS = Object.freeze([
468
476
  kind: /** @type {const} */ ('attribute'),
469
477
  }),
470
478
  ),
479
+ ...COMPOSITION_ATTRIBUTES.map(({ field, name, value }) =>
480
+ Object.freeze({
481
+ hookId: `composition-${field.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`)}-${value}`,
482
+ selectorAtom: `[${name}="${value}"]`,
483
+ kind: /** @type {const} */ ('attribute'),
484
+ }),
485
+ ),
471
486
  ]);
472
487
 
473
488
  /** @type {readonly StyleHookDefinition[]} every public hook, unsorted. */
@@ -524,6 +539,12 @@ export function buildTemplateStylingContract() {
524
539
  values: sortedUniqueBytes(PAGE_KIND_VALUES),
525
540
  role: 'semantic',
526
541
  },
542
+ ...COMPOSITION_ATTRIBUTE_CATALOG.map(({ name, values }) => ({
543
+ name,
544
+ match: 'exact-value',
545
+ values: sortedUniqueBytes(values),
546
+ role: 'semantic',
547
+ })),
527
548
  ].sort((a, b) => {
528
549
  const bufferA = Buffer.from(canonicalizeJcs(a), 'utf8');
529
550
  const bufferB = Buffer.from(canonicalizeJcs(b), 'utf8');
@@ -648,7 +669,7 @@ export function assertTemplateStylingContractShape(contract) {
648
669
  if (contract.profile !== 'gala-template-styling-contract-v2') {
649
670
  fail('profile');
650
671
  }
651
- if (contract.contractVersion !== '3.0.0') fail('contractVersion');
672
+ if (contract.contractVersion !== '4.0.0') fail('contractVersion');
652
673
  if (contract.templatePackage !== '@rathnasgala2/template') {
653
674
  fail('templatePackage');
654
675
  }
@@ -52,6 +52,8 @@ function renderMark(name, mark) {
52
52
  * @param {string} [options.appearanceControlHtml] the already-rendered
53
53
  * appearance control, inserted into the `header-actions` slot
54
54
  * @param {Readonly<Record<string, string | ((...args: string[]) => string)>>} options.messages
55
+ * @param {string} options.profile the admitted composition profile
56
+ * @param {string} options.headerLayout the admitted composition's header layout
55
57
  * @returns {string} the rendered header
56
58
  */
57
59
  export function renderHeader({
@@ -66,6 +68,8 @@ export function renderHeader({
66
68
  searchIndexHref,
67
69
  appearanceControlHtml,
68
70
  messages,
71
+ profile,
72
+ headerLayout,
69
73
  }) {
70
74
  const label = (/** @type {string} */ key) =>
71
75
  escapeHtml(/** @type {string} */ (messages[key]));
@@ -74,19 +78,28 @@ export function renderHeader({
74
78
  ? `<details class="g-menu"><summary>${icon('menu')}<span class="g-sr">${label('menuSummaryLabel')}</span></summary>` +
75
79
  `<ul>${navItems.map((item) => renderNavItem(item, currentRoute, urlPrefix)).join('')}</ul></details>`
76
80
  : '';
77
- return (
78
- `<header class="g-header"><div class="g-wrap g-header-row">` +
81
+ const brand =
79
82
  `<a class="g-brand" href="${escapeHtml(homeRoute)}">${renderMark(publicationName, brandMark)}` +
80
83
  `<span class="g-brand-text"><span class="g-brand-name">${escapeHtml(publicationName)}</span>` +
81
- `<span class="g-brand-tag">${escapeHtml(tagline)}</span></span></a>` +
82
- navHtml +
84
+ `<span class="g-brand-tag">${escapeHtml(tagline)}</span></span></a>`;
85
+ const actions =
83
86
  `<div class="g-actions">` +
84
87
  `<button class="g-search-btn" type="button" hidden data-action="search" data-search-index="${escapeHtml(searchIndexHref)}" aria-label="${label('searchButtonAriaLabel')}">` +
85
88
  `${icon('search')}<span>${label('searchButtonLabel')}</span></button>` +
86
89
  renderSlot('header-actions', { html: appearanceControlHtml }) +
87
90
  menu +
88
- `</div></div></header>`
89
- );
91
+ `</div>`;
92
+ const content =
93
+ profile === 'newsletter'
94
+ ? actions + brand + navHtml
95
+ : profile === 'visual'
96
+ ? brand + actions + navHtml
97
+ : profile === 'personal'
98
+ ? navHtml + brand + actions
99
+ : profile === 'developer'
100
+ ? navHtml + actions + brand
101
+ : brand + navHtml + actions;
102
+ return `<header class="g-header" data-gala-header-layout="${escapeHtml(headerLayout)}"><div class="g-wrap g-header-row">${content}</div></header>`;
90
103
  }
91
104
 
92
105
  /**
@@ -146,6 +159,8 @@ function renderSocialLinks(socialLinks, rssHref, messages) {
146
159
  * attribution line renders: the build input's
147
160
  * `appearance.attribution.showMadeWith`, shown when absent
148
161
  * @param {Readonly<Record<string, string | ((...args: string[]) => string)>>} options.messages
162
+ * @param {string} options.profile the admitted composition profile
163
+ * @param {string} options.footerLayout the admitted composition's footer layout
149
164
  * @returns {string} the rendered footer
150
165
  */
151
166
  export function renderFooter({
@@ -163,6 +178,8 @@ export function renderFooter({
163
178
  copyrightText,
164
179
  showAttribution,
165
180
  messages,
181
+ profile,
182
+ footerLayout,
166
183
  }) {
167
184
  const label = (/** @type {string} */ key) =>
168
185
  escapeHtml(/** @type {string} */ (messages[key]));
@@ -176,20 +193,31 @@ export function renderFooter({
176
193
  ? ''
177
194
  : `<nav class="g-footer-col" aria-label="${label(labelKey)}"><p class="g-label">${label(labelKey)}</p>` +
178
195
  `<ul>${items.map((item) => renderNavItem(item, undefined, urlPrefix)).join('')}</ul></nav>`;
179
- return (
180
- `<footer class="g-footer"><div class="g-wrap g-footer-grid">` +
196
+ const brand =
181
197
  `<div class="g-footer-brand">` +
182
198
  `<a class="g-brand" href="${escapeHtml(homeRoute)}">${renderMark(publicationName, brandMark)}` +
183
199
  `<span class="g-brand-name">${escapeHtml(publicationName)}</span></a>` +
184
200
  `<p class="g-footer-about">${escapeHtml(description)}</p>` +
185
201
  renderSocialLinks(socialLinks, rssHref, messages) +
186
- `</div>` +
187
- column('exploreColumnLabel', primaryItems) +
188
- column('footerNavigationLabel', footerItems) +
202
+ `</div>`;
203
+ const primary = column('exploreColumnLabel', primaryItems);
204
+ const secondary = column('footerNavigationLabel', footerItems);
205
+ const profileCard =
189
206
  `<div class="g-footer-col">` +
190
207
  renderSlot('footer-profile', { html: authorHtml + footerProfileHtml }) +
191
- `</div>` +
192
- `</div>` +
208
+ `</div>`;
209
+ const grid =
210
+ profile === 'newsletter'
211
+ ? brand + profileCard + primary + secondary
212
+ : profile === 'visual'
213
+ ? brand + primary + profileCard + secondary
214
+ : profile === 'personal'
215
+ ? profileCard + brand + primary + secondary
216
+ : profile === 'developer'
217
+ ? primary + secondary + brand + profileCard
218
+ : brand + primary + secondary + profileCard;
219
+ return (
220
+ `<footer class="g-footer" data-gala-footer-layout="${escapeHtml(footerLayout)}"><div class="g-wrap g-footer-grid">${grid}</div>` +
193
221
  renderSlot('footer-auxiliary') +
194
222
  renderSlot('account-intent') +
195
223
  renderSlot('conversation') +
@@ -172,7 +172,7 @@ const SKELETON_LAYOUT_SOURCE = [
172
172
  '<link rel="stylesheet" href="{{ baseStylesheetHref }}">',
173
173
  '{{ themeStylesheetLinksHtml | safe }}',
174
174
  '</head>',
175
- `<body ${PAGE_KIND_ATTRIBUTE}="{{ pageKind }}">`,
175
+ `<body ${PAGE_KIND_ATTRIBUTE}="{{ pageKind }}" data-gala-profile="{{ composition.profile }}" data-gala-home-layout="{{ composition.homeLayout }}" data-gala-article-layout="{{ composition.articleLayout }}" data-gala-author-layout="{{ composition.authorLayout }}" data-gala-listing-layout="{{ composition.listingLayout }}" data-gala-header-layout="{{ composition.headerLayout }}" data-gala-footer-layout="{{ composition.footerLayout }}" data-gala-media-ratio="{{ composition.mediaRatio }}" data-gala-density="{{ composition.density }}">`,
176
176
  '{{ content | safe }}',
177
177
  '</body>',
178
178
  '</html>',
@@ -88,6 +88,10 @@ import { getMessages } from './messages.js';
88
88
  import { resolveTextDirection } from './text-direction.js';
89
89
  import { deriveAuthorSlugs, routeSegmentForLabel } from './route-labels.js';
90
90
  import { derivePublicBasePath, joinPublicRoute } from './route.js';
91
+ import {
92
+ assertSupportedComposition,
93
+ EDITORIAL_COMPOSITION,
94
+ } from './template-composition.js';
91
95
 
92
96
  /** Listing page size (index and per-year archive pages). Scoped decision:
93
97
  * this renderer has no externally fixed page size; 10 is a deterministic,
@@ -383,11 +387,46 @@ export function homeDocumentTitle(name, tagline) {
383
387
  * @param {import('../../../types/index.d.ts').InteractionsModule} [options.interactions]
384
388
  * the validated interactions module; when set, every article page carries
385
389
  * the interactions section and is interaction-bearing
390
+ * @param {unknown} [options.composition] the schema-validated structural
391
+ * composition selected by the theme; defaults to Editorial only for
392
+ * theme-less renderer use
386
393
  * @returns {GeneratedPage[]} every generated page, in a stable, deterministic
387
394
  * order
388
395
  */
389
396
  export function buildGeneratedPages(validatedInput, options) {
390
397
  const { publication, authors, content, basePath, baseUrl } = validatedInput;
398
+ const composition = assertSupportedComposition(
399
+ options.composition ?? EDITORIAL_COMPOSITION,
400
+ );
401
+ /**
402
+ * Preserve every content region while giving each article direction its
403
+ * own reading sequence and semantic container.
404
+ *
405
+ * @param {{head: string, cover: string, toc: string, prose: string, foot: string}} parts
406
+ * @returns {string}
407
+ */
408
+ const articleStructure = ({ head, cover, toc, prose, foot }) => {
409
+ /**
410
+ * @param {string} contents rendered article regions
411
+ * @param {string} [element] semantic wrapper element
412
+ * @returns {string}
413
+ */
414
+ const grid = (contents, element = 'div') =>
415
+ `<${element} class="g-wrap g-article-grid">${contents}</${element}>`;
416
+ if (composition.profile === 'newsletter') {
417
+ return `<section>${head}${cover}${grid(prose + toc, 'section')}${foot}</section>`;
418
+ }
419
+ if (composition.profile === 'visual') {
420
+ return `<div>${cover}${head}${grid(prose + toc)}</div>${foot}`;
421
+ }
422
+ if (composition.profile === 'personal') {
423
+ return `<section>${head}${cover}${grid(prose)}${toc ? grid(toc, 'section') : ''}${foot}</section>`;
424
+ }
425
+ if (composition.profile === 'developer') {
426
+ return `<div>${head}${cover}${grid(toc + prose, 'section')}${foot}</div>`;
427
+ }
428
+ return head + cover + grid(toc + prose) + foot;
429
+ };
391
430
  const interactionsModule = interactionsActive(options.interactions)
392
431
  ? options.interactions
393
432
  : undefined;
@@ -612,13 +651,32 @@ export function buildGeneratedPages(validatedInput, options) {
612
651
  * @param {string} [afterHtml] markup after the list (pagination)
613
652
  * @returns {string}
614
653
  */
615
- const rowList = (items, afterHtml = '') =>
616
- `<section class="g-wrap g-section">` +
617
- (items.length > 0
618
- ? `<div class="g-list">${items.map((record) => cardOf(record, 'row', 2)).join('')}</div>`
619
- : `<p>${text('emptyListingLabel')}</p>`) +
620
- afterHtml +
621
- `</section>`;
654
+ const rowList = (items, afterHtml = '') => {
655
+ const grid = composition.listingLayout === 'image-grid';
656
+ const issueCards = composition.listingLayout === 'issues';
657
+ const datedRows = composition.listingLayout === 'dated-rows';
658
+ const outer =
659
+ composition.profile === 'personal' || composition.profile === 'developer'
660
+ ? 'div'
661
+ : 'section';
662
+ const contents =
663
+ items.length > 0
664
+ ? datedRows
665
+ ? `<div class="g-list g-dated-list">${items
666
+ .map(
667
+ (record) =>
668
+ `<div class="g-dated-row"><time class="g-date-stamp" datetime="${escapeHtml(record.frontmatter.publishedAt)}">` +
669
+ `${call('formatDate', record.frontmatter.publishedAt)}</time>${cardOf(record, 'row', 2)}</div>`,
670
+ )
671
+ .join('')}</div>`
672
+ : `<div class="${grid ? 'g-grid' : 'g-list'}">${items
673
+ .map((record) =>
674
+ cardOf(record, grid || issueCards ? 'grid' : 'row', 2),
675
+ )
676
+ .join('')}</div>`
677
+ : `<p>${text('emptyListingLabel')}</p>`;
678
+ return `<${outer} class="g-wrap g-section" data-gala-listing-layout="${composition.listingLayout}">${contents}${afterHtml}</${outer}>`;
679
+ };
622
680
 
623
681
  /**
624
682
  * One listing page's metadata: bare title, `<title>`, description,
@@ -710,6 +768,28 @@ export function buildGeneratedPages(validatedInput, options) {
710
768
  ? `<p class="g-dek">${escapeHtml(author.biography)}</p>`
711
769
  : '') +
712
770
  links;
771
+ const authorHead = listingHead({
772
+ breadcrumbHtml: crumbs(
773
+ [{ label: /** @type {string} */ (messages.authorsSectionLabel) }],
774
+ [{ label: author.displayName }],
775
+ ),
776
+ kicker: /** @type {string} */ (messages.authorKickerLabel),
777
+ count: authored.length,
778
+ title: author.displayName,
779
+ descriptionHtml: intro,
780
+ markHtml: avatar(author),
781
+ });
782
+ const authoredList = rowList(authored);
783
+ const authorContents =
784
+ composition.profile === 'newsletter'
785
+ ? `<section>${authorHead}${authoredList}</section>`
786
+ : composition.profile === 'visual'
787
+ ? `<div>${authorHead}<section>${authoredList}</section></div>`
788
+ : composition.profile === 'personal'
789
+ ? `<section>${authorHead}<div>${authoredList}</div></section>`
790
+ : composition.profile === 'developer'
791
+ ? `<div>${authorHead}<div>${authoredList}</div></div>`
792
+ : authorHead + authoredList;
713
793
  pages.push({
714
794
  kind: 'author',
715
795
  route: authorRoute(author),
@@ -737,17 +817,9 @@ export function buildGeneratedPages(validatedInput, options) {
737
817
  }),
738
818
  ),
739
819
  bodyHtml:
740
- listingHead({
741
- breadcrumbHtml: crumbs(
742
- [{ label: /** @type {string} */ (messages.authorsSectionLabel) }],
743
- [{ label: author.displayName }],
744
- ),
745
- kicker: /** @type {string} */ (messages.authorKickerLabel),
746
- count: authored.length,
747
- title: author.displayName,
748
- descriptionHtml: intro,
749
- markHtml: avatar(author),
750
- }) + rowList(authored),
820
+ `<div data-gala-author-layout="${composition.authorLayout}">` +
821
+ authorContents +
822
+ `</div>`,
751
823
  });
752
824
  }
753
825
 
@@ -822,7 +894,7 @@ export function buildGeneratedPages(validatedInput, options) {
822
894
  editionSelector +
823
895
  `</div>`;
824
896
  const cover = hero
825
- ? `<figure class="g-wrap g-article-cover">${renderImage({ image: hero, alt: heroMedia?.role === 'decorative' ? '' : (heroMedia?.alt ?? ''), priority: true })}</figure>`
897
+ ? `<figure class="${composition.articleLayout === 'full-bleed' ? 'g-article-cover g-article-cover-full-bleed' : 'g-wrap g-article-cover'}">${renderImage({ image: hero, alt: heroMedia?.role === 'decorative' ? '' : (heroMedia?.alt ?? ''), priority: true })}</figure>`
826
898
  : '';
827
899
  const tocNav = contents
828
900
  ? `<nav class="g-toc" aria-label="${text('onThisPageLabel')}"><p class="g-label">${icon('list')}${text('onThisPageLabel')}</p>${contents}</nav>`
@@ -1001,8 +1073,8 @@ export function buildGeneratedPages(validatedInput, options) {
1001
1073
  interactionBearing: Boolean(interactions),
1002
1074
  lastModified: contentLastModified(frontmatter),
1003
1075
  bodyHtml:
1004
- `<article class="g-article">${isArticle || isEdition ? '<div class="g-progress" aria-hidden="true"></div>' : ''}${head}${cover}` +
1005
- `<div class="g-wrap g-article-grid">${tocNav}${prose}</div>${foot}</article>` +
1076
+ `<article class="g-article" data-gala-article-layout="${composition.articleLayout}">${isArticle || isEdition ? '<div class="g-progress" aria-hidden="true"></div>' : ''}` +
1077
+ `${articleStructure({ head, cover, toc: tocNav, prose, foot })}</article>` +
1006
1078
  after +
1007
1079
  (isArticle ? newsletterHtml : ''),
1008
1080
  });
@@ -1093,15 +1165,49 @@ export function buildGeneratedPages(validatedInput, options) {
1093
1165
  ? `<section class="g-wrap g-section"><div class="g-section-head"><h2>${text('moreArticlesHeading')}</h2></div>` +
1094
1166
  `<div class="g-list">${items.map((r) => cardOf(r, 'row')).join('')}</div>${pagination}</section>`
1095
1167
  : '';
1096
- const bodyHtml =
1097
- pageNumber === 1
1168
+ const homeParts =
1169
+ composition.profile === 'newsletter'
1098
1170
  ? homeHeading +
1099
- homeFeatured +
1100
1171
  homeTags +
1172
+ homeFeatured +
1101
1173
  homeLatest +
1102
- homeSeries +
1103
1174
  moreList +
1175
+ homeSeries +
1104
1176
  newsletterHtml
1177
+ : composition.profile === 'visual'
1178
+ ? homeHeading +
1179
+ homeFeatured +
1180
+ homeLatest +
1181
+ homeSeries +
1182
+ homeTags +
1183
+ moreList +
1184
+ newsletterHtml
1185
+ : composition.profile === 'personal'
1186
+ ? homeHeading +
1187
+ homeLatest +
1188
+ homeFeatured +
1189
+ moreList +
1190
+ homeTags +
1191
+ homeSeries +
1192
+ newsletterHtml
1193
+ : composition.profile === 'developer'
1194
+ ? homeHeading +
1195
+ homeTags +
1196
+ homeLatest +
1197
+ homeSeries +
1198
+ homeFeatured +
1199
+ moreList +
1200
+ newsletterHtml
1201
+ : homeHeading +
1202
+ homeFeatured +
1203
+ homeTags +
1204
+ homeLatest +
1205
+ homeSeries +
1206
+ moreList +
1207
+ newsletterHtml;
1208
+ const bodyHtml =
1209
+ pageNumber === 1
1210
+ ? `<div data-gala-home-layout="${composition.homeLayout}">${homeParts}</div>`
1105
1211
  : listingHead({
1106
1212
  breadcrumbHtml: crumbs([
1107
1213
  { label: /** @type {string} */ (messages.indexHeading) },
@@ -0,0 +1,185 @@
1
+ /**
2
+ * Generation-4 template composition admission.
3
+ *
4
+ * A composition is one complete, immutable product direction. Admission is
5
+ * deliberately by exact object equality so fields from different directions
6
+ * cannot be combined into renderer states that were never designed or tested.
7
+ */
8
+
9
+ /** The immutable generation-4 composition schema identity. */
10
+ export const TEMPLATE_COMPOSITION_SCHEMA_ID =
11
+ 'urn:gala:schema:template-composition:2.0.0';
12
+
13
+ const SCHEMA_FIELDS = Object.freeze({
14
+ schemaId: TEMPLATE_COMPOSITION_SCHEMA_ID,
15
+ schemaVersion: '2.0.0',
16
+ });
17
+
18
+ /** The complete Editorial composition. */
19
+ export const EDITORIAL_COMPOSITION = Object.freeze({
20
+ ...SCHEMA_FIELDS,
21
+ profile: 'editorial',
22
+ homeLayout: 'magazine',
23
+ articleLayout: 'editorial',
24
+ authorLayout: 'portrait',
25
+ listingLayout: 'rows',
26
+ headerLayout: 'masthead',
27
+ footerLayout: 'expanded',
28
+ mediaRatio: 'landscape',
29
+ density: 'comfortable',
30
+ });
31
+
32
+ /** The complete Newsletter composition. */
33
+ export const NEWSLETTER_COMPOSITION = Object.freeze({
34
+ ...SCHEMA_FIELDS,
35
+ profile: 'newsletter',
36
+ homeLayout: 'newspaper',
37
+ articleLayout: 'issue',
38
+ authorLayout: 'profile',
39
+ listingLayout: 'issues',
40
+ headerLayout: 'publication',
41
+ footerLayout: 'expanded',
42
+ mediaRatio: 'landscape',
43
+ density: 'comfortable',
44
+ });
45
+
46
+ /** The complete Visual composition. */
47
+ export const VISUAL_COMPOSITION = Object.freeze({
48
+ ...SCHEMA_FIELDS,
49
+ profile: 'visual',
50
+ homeLayout: 'gallery',
51
+ articleLayout: 'full-bleed',
52
+ authorLayout: 'portfolio',
53
+ listingLayout: 'image-grid',
54
+ headerLayout: 'minimal',
55
+ footerLayout: 'compact',
56
+ mediaRatio: 'mixed',
57
+ density: 'comfortable',
58
+ });
59
+
60
+ /** The complete Personal composition. */
61
+ export const PERSONAL_COMPOSITION = Object.freeze({
62
+ ...SCHEMA_FIELDS,
63
+ profile: 'personal',
64
+ homeLayout: 'chronological',
65
+ articleLayout: 'prose',
66
+ authorLayout: 'biography',
67
+ listingLayout: 'dated-rows',
68
+ headerLayout: 'compact',
69
+ footerLayout: 'compact',
70
+ mediaRatio: 'landscape',
71
+ density: 'spacious',
72
+ });
73
+
74
+ /** The complete Developer composition. */
75
+ export const DEVELOPER_COMPOSITION = Object.freeze({
76
+ ...SCHEMA_FIELDS,
77
+ profile: 'developer',
78
+ homeLayout: 'topic-index',
79
+ articleLayout: 'technical',
80
+ authorLayout: 'biography',
81
+ listingLayout: 'rows',
82
+ headerLayout: 'utility',
83
+ footerLayout: 'compact',
84
+ mediaRatio: 'landscape',
85
+ density: 'compact',
86
+ });
87
+
88
+ /** Every complete composition implemented by generation 4. */
89
+ export const SUPPORTED_COMPOSITIONS = Object.freeze([
90
+ EDITORIAL_COMPOSITION,
91
+ NEWSLETTER_COMPOSITION,
92
+ VISUAL_COMPOSITION,
93
+ PERSONAL_COMPOSITION,
94
+ DEVELOPER_COMPOSITION,
95
+ ]);
96
+
97
+ /** @type {Readonly<Record<string, (typeof SUPPORTED_COMPOSITIONS)[number]>>} */
98
+ const COMPOSITION_BY_PROFILE = Object.freeze(
99
+ Object.fromEntries(
100
+ SUPPORTED_COMPOSITIONS.map((composition) => [
101
+ composition.profile,
102
+ composition,
103
+ ]),
104
+ ),
105
+ );
106
+
107
+ const COMPOSITION_ATTRIBUTE_NAMES = Object.freeze({
108
+ profile: 'data-gala-profile',
109
+ homeLayout: 'data-gala-home-layout',
110
+ articleLayout: 'data-gala-article-layout',
111
+ authorLayout: 'data-gala-author-layout',
112
+ listingLayout: 'data-gala-listing-layout',
113
+ headerLayout: 'data-gala-header-layout',
114
+ footerLayout: 'data-gala-footer-layout',
115
+ mediaRatio: 'data-gala-media-ratio',
116
+ density: 'data-gala-density',
117
+ });
118
+
119
+ /** @typedef {(typeof SUPPORTED_COMPOSITIONS)[number]} SupportedComposition */
120
+ /** @typedef {Exclude<keyof SupportedComposition, 'schemaId' | 'schemaVersion'>} CompositionField */
121
+
122
+ /** Public semantic attribute catalogs corresponding to composition fields. */
123
+ export const COMPOSITION_ATTRIBUTE_CATALOG = Object.freeze(
124
+ /** @type {readonly [CompositionField, string][]} */ (
125
+ Object.entries(COMPOSITION_ATTRIBUTE_NAMES)
126
+ ).map(([field, name]) =>
127
+ Object.freeze({
128
+ field,
129
+ name,
130
+ values: Object.freeze([
131
+ ...new Set(
132
+ SUPPORTED_COMPOSITIONS.map((composition) => composition[field]),
133
+ ),
134
+ ]),
135
+ }),
136
+ ),
137
+ );
138
+
139
+ /** Every public semantic attribute/value pair themes may select. */
140
+ export const COMPOSITION_ATTRIBUTES = Object.freeze(
141
+ COMPOSITION_ATTRIBUTE_CATALOG.flatMap(({ field, name, values }) =>
142
+ values.map((value) => Object.freeze({ field, name, value })),
143
+ ),
144
+ );
145
+
146
+ /**
147
+ * Admit exactly one complete composition implemented by this renderer.
148
+ *
149
+ * @param {unknown} candidate a complete template-composition document
150
+ * @returns {(typeof SUPPORTED_COMPOSITIONS)[number]} canonical composition
151
+ */
152
+ export function assertSupportedComposition(candidate) {
153
+ if (!candidate || typeof candidate !== 'object' || Array.isArray(candidate)) {
154
+ throw new Error(
155
+ `composition must be a complete ${TEMPLATE_COMPOSITION_SCHEMA_ID} document`,
156
+ );
157
+ }
158
+ const composition = /** @type {Record<string, unknown>} */ (candidate);
159
+ const selected =
160
+ typeof composition.profile === 'string' &&
161
+ Object.hasOwn(COMPOSITION_BY_PROFILE, composition.profile)
162
+ ? COMPOSITION_BY_PROFILE[composition.profile]
163
+ : undefined;
164
+ if (!selected) {
165
+ throw new Error(
166
+ `composition.profile (${JSON.stringify(composition.profile)}) is not implemented by this renderer`,
167
+ );
168
+ }
169
+ const expectedKeys = Object.keys(selected).sort();
170
+ const actualKeys = Object.keys(composition).sort();
171
+ if (JSON.stringify(actualKeys) !== JSON.stringify(expectedKeys)) {
172
+ throw new Error(
173
+ `composition must contain exactly ${expectedKeys.join(', ')}`,
174
+ );
175
+ }
176
+ for (const [key, expected] of Object.entries(selected)) {
177
+ if (composition[key] !== expected) {
178
+ throw new Error(
179
+ `composition.${key} (${JSON.stringify(composition[key])}) is not ` +
180
+ `implemented for ${selected.profile}; expected ${JSON.stringify(expected)}`,
181
+ );
182
+ }
183
+ }
184
+ return selected;
185
+ }
@@ -85,8 +85,9 @@ import { sanitizeThemeSvg } from './media/theme-svg-sanitizer.js';
85
85
  import { projectFixedAssetPath } from './route.js';
86
86
  import { satisfiesTemplateRange } from './semver-range.js';
87
87
  import { escapeHtml } from './skeleton.js';
88
+ import { assertSupportedComposition } from './template-composition.js';
88
89
 
89
- /** @type {string} the `urn:gala:schema:theme-contract:2.0.0` schema id
90
+ /** @type {string} the immutable `urn:gala:schema:theme-contract:2.0.0` schema id
90
91
  * every consumed `theme.json` is validated against (TPL-H1). */
91
92
  const THEME_CONTRACT_SCHEMA_ID = 'urn:gala:schema:theme-contract:2.0.0';
92
93
 
@@ -415,7 +416,7 @@ function assertThemeContractIntegrity(themeJson) {
415
416
  if (!validation.valid) {
416
417
  throw new ThemeAssetError(
417
418
  'THEME_CONTRACT_SCHEMA_INVALID',
418
- `theme.json failed urn:gala:schema:theme-contract:2.0.0 validation ` +
419
+ `theme.json failed ${THEME_CONTRACT_SCHEMA_ID} validation ` +
419
420
  `(${validation.diagnostics.length} diagnostic(s)): ` +
420
421
  JSON.stringify(validation.diagnostics),
421
422
  );
@@ -458,6 +459,14 @@ function assertThemeContractIntegrity(themeJson) {
458
459
  `(${TEMPLATE_STYLING_CONTRACT_TEMPLATE_VERSION})`,
459
460
  );
460
461
  }
462
+ try {
463
+ assertSupportedComposition(themeJson.composition);
464
+ } catch (error) {
465
+ throw new ThemeAssetError(
466
+ 'THEME_COMPOSITION_UNSUPPORTED',
467
+ /** @type {Error} */ (error).message,
468
+ );
469
+ }
461
470
  }
462
471
 
463
472
  /**
@@ -490,6 +499,7 @@ function subresourceIntegrityAttribute(bytes) {
490
499
  * files: {path: string, bytes: Buffer}[],
491
500
  * assets: import('../../../types/index.d.ts').ManifestAssetEntry[],
492
501
  * linkTagsHtml: string,
502
+ * composition: (typeof import('./template-composition.js').SUPPORTED_COMPOSITIONS)[number],
493
503
  * }>} the files to write (already `basePath`-joined), their manifest rows
494
504
  * (same joined paths), and the ready-to-insert `<link>` markup
495
505
  */
@@ -611,7 +621,12 @@ export async function loadThemeAssets({
611
621
  });
612
622
  }
613
623
 
614
- return { files, assets, linkTagsHtml: linkTags.join('') };
624
+ return {
625
+ files,
626
+ assets,
627
+ linkTagsHtml: linkTags.join(''),
628
+ composition: assertSupportedComposition(themeJson.composition),
629
+ };
615
630
  }
616
631
 
617
632
  /**