From f4d68ea838f8ebf6eff8cacde27c605e29242a56 Mon Sep 17 00:00:00 2001 From: Foscat Date: Thu, 24 Sep 2026 20:51:51 -0500 Subject: [PATCH] Prepare Layout Style 3.2.1 release candidate [skip ci] --- .github/workflows/npm-publish.yml | 2 +- CHANGELOG.md | 10 + README.md | 8 +- demo/demo.css | 39 +--- demo/demo.js | 226 ++------------------ demo/index.html | 133 ++++-------- demo/sitemap.xml | 2 +- docs/wiki/Getting-Started.md | 4 +- docs/wiki/Home.md | 2 +- docs/wiki/Installation-And-CDN.md | 6 +- docs/wiki/Release-And-Publishing.md | 8 +- manifest.json | 2 +- package-lock.json | 16 +- package.json | 10 +- scripts/release-fixture-contract.mjs | 4 + test/demo-smoke.test.mjs | 266 ++++++++---------------- test/layout-css-contract.test.mjs | 2 +- test/manifest-contract.test.mjs | 2 +- test/package-contract.test.mjs | 13 +- test/pages-artifact.test.mjs | 6 +- test/release-docs-contract.test.mjs | 16 +- test/release-preflight.test.mjs | 4 +- test/responsive-region-fixture.test.mjs | 145 +++++++++++++ 23 files changed, 369 insertions(+), 557 deletions(-) create mode 100644 test/responsive-region-fixture.test.mjs diff --git a/.github/workflows/npm-publish.yml b/.github/workflows/npm-publish.yml index e5b341e..c5647ad 100644 --- a/.github/workflows/npm-publish.yml +++ b/.github/workflows/npm-publish.yml @@ -6,7 +6,7 @@ on: workflow_dispatch: inputs: release_tag: - description: "Release tag to publish, for example v3.2.0" + description: "Release tag to publish, for example v3.2.1" required: true type: string diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d57df1..21f1444 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,16 @@ All notable changes to `layout-style-css` are documented here. This project follows semantic versioning. +## [3.2.1] - 2026-09-24 + +### Security + +- Pinned patched `colord` and `js-yaml` transitive tooling releases after the final release audit identified newly disclosed denial-of-service advisories. + +### Tests + +- Added real-browser fixtures for 320–390px containers, short landscape shells, sticky fallbacks, nested internal scrolling, bounded wide tables, manual and automatic Mosaic coexistence, long-content minimum sizing, and positioned controls that remain outside document overflow. + ## [3.2.0] - 2026-09-08 ### Added diff --git a/README.md b/README.md index 9277f0f..affa8dd 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Zero-configuration, dependency-free structural CSS that responds to the width and height a layout actually receives. -`layout-style-css@3.2.0` is the current v3 release candidate. It provides intrinsic wrappers, Mosaic and Action Bar compositions, seven semantic recipes, explicit local density controls, and twenty spatial personalities. Layout owns structure; UI Style Kit owns paint; Interactive Surface owns interaction styling. +`layout-style-css@3.2.1` is the current v3 release candidate. It provides intrinsic wrappers, Mosaic and Action Bar compositions, seven semantic recipes, explicit local density controls, and twenty spatial personalities. Layout owns structure; UI Style Kit owns paint; Interactive Surface owns interaction styling. ## Requirements @@ -13,7 +13,7 @@ Zero-configuration, dependency-free structural CSS that responds to the width an ## Install ```bash -npm install layout-style-css@3.2.0 +npm install layout-style-css@3.2.1 ``` Most applications need one import: @@ -200,8 +200,8 @@ This order lets UI Style Kit establish paint and theme roles, Interactive Surfac ## CDN ```html - - + + ``` ## Clean-Break Migration diff --git a/demo/demo.css b/demo/demo.css index 66f171a..cbe373b 100644 --- a/demo/demo.css +++ b/demo/demo.css @@ -20,31 +20,6 @@ color-scheme: light; } -body[data-mode="dark"] { - --demo-bg: #101827; - --demo-panel: #172033; - --demo-panel-strong: #273449; - --demo-text: #f8fafc; - --demo-muted: #cbd5e1; - --demo-line: #46556d; - --demo-accent: #a5b4fc; - --demo-accent-soft: #312e81; - --demo-shadow: 0 1rem 3rem rgb(0 0 0 / 0.38); - color-scheme: dark; -} - -body[data-mode="contrast"] { - --demo-bg: #000000; - --demo-panel: #000000; - --demo-panel-strong: #1a1a1a; - --demo-text: #ffffff; - --demo-muted: #ffffff; - --demo-line: #ffffff; - --demo-accent: #ffeb3b; - --demo-accent-soft: #322f00; - color-scheme: dark; -} - body[data-demo-density="compact"] { --demo-density-gap: 0.5rem; --demo-region-padding: 0.75rem; @@ -212,6 +187,11 @@ h3 { font-size: 0.75rem; } +.demo-bridge-note a { + color: var(--demo-accent); + font-weight: 700; +} + .demo-readouts { display: grid; justify-items: end; @@ -225,15 +205,6 @@ h3 { background: var(--demo-accent-soft); } -body[data-ecosystem="layout-only"] .demo-status { - border-style: dashed; - background: var(--demo-panel-strong); -} - -body[data-ecosystem="all-three"] .demo-status { - box-shadow: inset 0 0 0 0.125rem color-mix(in srgb, var(--demo-accent) 30%, transparent); -} - .demo-page { display: grid; inline-size: min(calc(100% - 2rem), 112rem); diff --git a/demo/demo.js b/demo/demo.js index 7ddc821..e0d222a 100644 --- a/demo/demo.js +++ b/demo/demo.js @@ -1,92 +1,4 @@ -const UI_STYLE_KIT_VERSION = "2.4.0"; -const UI_STYLE_KIT_MANIFEST_URL = `https://unpkg.com/ui-style-kit-css@${UI_STYLE_KIT_VERSION}/manifest.json`; const PERSONALITY_METADATA_URL = document.querySelector("script[data-personalities-url]")?.dataset.personalitiesUrl; -const UI_STYLE_KIT_MANIFEST_FALLBACK = Object.freeze({ - version: UI_STYLE_KIT_VERSION, - presets: Object.freeze([ - Object.freeze({ id: "minimal-saas", label: "Minimal SaaS", prefix: "saas" }), - Object.freeze({ id: "bento", label: "Bento", prefix: "bento" }), - Object.freeze({ id: "maximalist", label: "Maximalist", prefix: "max" }), - Object.freeze({ id: "bauhaus", label: "Bauhaus", prefix: "bau" }), - Object.freeze({ id: "tactile", label: "Tactile", prefix: "tactile" }), - Object.freeze({ id: "neumorphism", label: "Neumorphism", prefix: "neo" }), - Object.freeze({ id: "retrofuturism", label: "Retrofuturism", prefix: "retro" }), - Object.freeze({ id: "brutalism", label: "Brutalism", prefix: "brutal" }), - Object.freeze({ id: "cyberpunk", label: "Cyberpunk", prefix: "cyber" }), - Object.freeze({ id: "y2k", label: "Y2K", prefix: "y2k" }), - Object.freeze({ id: "retro-glass", label: "Retro Glass", prefix: "rg" }), - Object.freeze({ id: "editorial-luxe", label: "Editorial Luxe", prefix: "luxe" }), - Object.freeze({ id: "organic-modern", label: "Organic Modern", prefix: "organic" }), - Object.freeze({ id: "industrial-utility", label: "Industrial Utility", prefix: "utility" }), - Object.freeze({ id: "technical-blueprint", label: "Technical Blueprint", prefix: "blueprint" }), - Object.freeze({ id: "art-deco", label: "Art Deco", prefix: "deco" }), - Object.freeze({ id: "clay", label: "Clay", prefix: "clay" }), - Object.freeze({ id: "data-terminal", label: "Data Terminal", prefix: "terminal" }), - Object.freeze({ id: "paper-editorial", label: "Paper Editorial", prefix: "paper" }), - Object.freeze({ id: "neo-noir", label: "Neo Noir", prefix: "noir" }) - ]), - themes: Object.freeze([ - "midnight-gold", - "ocean-steel", - "forest-moss", - "sunset-ember", - "royal-plum", - "graphite-cyan", - "desert-sage", - "rose-quartz", - "cyber-lime", - "arctic-indigo" - ]), - modes: Object.freeze(["light", "dark", "contrast"]) -}); - -function normalizeUiStyleKitManifest(manifest) { - const normalized = { - version: String(manifest?.version ?? UI_STYLE_KIT_VERSION), - presets: Array.isArray(manifest?.presets) ? manifest.presets : [], - themes: Array.isArray(manifest?.themes) ? manifest.themes : [], - modes: Array.isArray(manifest?.modes) ? manifest.modes : [] - }; - const presets = normalized.presets - .map((preset) => ({ - id: String(preset?.id ?? ""), - label: String(preset?.label ?? preset?.id ?? ""), - prefix: String(preset?.prefix ?? "") - })) - .filter((preset) => preset.id && preset.prefix); - - if (presets.length === 0 || normalized.themes.length === 0 || normalized.modes.length === 0) { - throw new Error("UI Style Kit manifest is missing presets, themes, or modes."); - } - - return Object.freeze({ - version: normalized.version, - presets: Object.freeze(presets.map((preset) => Object.freeze(preset))), - themes: Object.freeze(normalized.themes.map(String)), - modes: Object.freeze(normalized.modes.map(String)) - }); -} - -async function loadUiStyleKitManifest() { - try { - const response = await fetch(UI_STYLE_KIT_MANIFEST_URL, { cache: "force-cache" }); - - if (!response.ok) { - throw new Error(`HTTP ${response.status}`); - } - - return normalizeUiStyleKitManifest(await response.json()); - } catch (error) { - /* - The published demo can render before the companion UI release reaches every CDN edge. - The fallback mirrors the 2.3 manifest contract so layout behavior stays testable. - */ - console.warn("UI Style Kit 2.3 manifest unavailable; using the packaged fallback.", error); - return UI_STYLE_KIT_MANIFEST_FALLBACK; - } -} - -const UI_STYLE_KIT_MANIFEST = await loadUiStyleKitManifest(); function normalizePersonalityMetadata(metadata) { const personalities = Array.isArray(metadata?.personalities) ? metadata.personalities : []; const validCompatibility = new Set(["native", "any", "recommended"]); @@ -212,11 +124,7 @@ const ALLOWLISTS = Object.freeze({ ]), height: Object.freeze(["auto", "29rem", "31rem", "43rem", "45rem", "50rem"]), responsive: Object.freeze(["auto", "manual"]), - density: Object.freeze(["compact", "normal", "spacious"]), - ui: Object.freeze(UI_STYLE_KIT_MANIFEST.presets.map((preset) => preset.id)), - theme: Object.freeze(UI_STYLE_KIT_MANIFEST.themes), - mode: Object.freeze(UI_STYLE_KIT_MANIFEST.modes), - ecosystem: Object.freeze(["layout-only", "layout-ui", "all-three"]) + density: Object.freeze(["compact", "normal", "spacious"]) }); const DEFAULT_STATE = Object.freeze({ @@ -227,11 +135,7 @@ const DEFAULT_STATE = Object.freeze({ container: "auto", height: "auto", responsive: "auto", - density: "normal", - ui: "minimal-saas", - theme: "arctic-indigo", - mode: "light", - ecosystem: "all-three" + density: "normal" }); const CONTAINER_WIDTHS = Object.freeze({ @@ -270,10 +174,6 @@ const DEVICE_PRESETS = Object.freeze({ "desktop-portrait": Object.freeze({ width: 900, height: 1440 }) }); -const UI_CLASS_PREFIXES = Object.freeze( - Object.fromEntries(UI_STYLE_KIT_MANIFEST.presets.map((preset) => [preset.id, preset.prefix])) -); - const RECIPE_AREAS = Object.freeze({ "app-shell": Object.freeze(["header", "sidebar", "main", "aside", "footer"]), dashboard: Object.freeze(["header", "nav", "main", "aside", "footer"]), @@ -282,25 +182,7 @@ const RECIPE_AREAS = Object.freeze({ "split-hero": Object.freeze(["content", "media", "actions"]) }); -const ECOSYSTEM_IMPORTS = Object.freeze({ - "layout-only": Object.freeze(['import "layout-style-css";']), - "layout-ui": Object.freeze([ - 'import "ui-style-kit-css/visual.css";', - 'import "layout-style-css";' - ]), - "all-three": Object.freeze([ - 'import "ui-style-kit-css/visual.css";', - 'import "ui-style-kit-css/interactive-surface-theme.css";', - 'import "interactive-surface-css/state-core.css";', - 'import "layout-style-css";' - ]) -}); - -const ECOSYSTEM_LABELS = Object.freeze({ - "layout-only": "Layout only", - "layout-ui": "Layout + UI", - "all-three": "All three libraries" -}); +const LAYOUT_IMPORT = 'import "layout-style-css";'; const body = document.body; const controls = Object.fromEntries( @@ -316,13 +198,8 @@ const copyStatus = document.querySelector("#copyStatus"); const personalityMetadataStatus = document.querySelector("#personalityMetadataStatus"); const recommendedUiGuidance = document.querySelector("#recommendedUiGuidance"); const compatibleUiGuidance = document.querySelector("#compatibleUiGuidance"); -const ecosystemStatus = document.querySelector("#ecosystemStatus"); const containerReadout = document.querySelector("#containerReadout"); const topologyReadout = document.querySelector("#topologyReadout"); -const uiKitStylesheet = document.querySelector("#uiKitStylesheet"); -const uiKitInteractiveThemeStylesheet = document.querySelector("#uiKitInteractiveThemeStylesheet"); -const interactiveSurfaceStylesheet = document.querySelector("#interactiveSurfaceStylesheet"); -const layoutCoreStylesheet = document.querySelector("#layoutCoreStylesheet"); const drawerToggle = document.querySelector("#demoControlsToggle"); const drawer = document.querySelector("#demoControlsDrawer"); const drawerClose = document.querySelector("#demoControlsClose"); @@ -332,14 +209,11 @@ const actionBarStickyToggle = document.querySelector("#actionBarStickyToggle"); const fixtureActionBar = document.querySelector("#fixtureActionBar"); const mobileControlsQuery = window.matchMedia("(max-width: 63.999rem)"); -body.dataset.uiManifestVersion = UI_STYLE_KIT_MANIFEST.version; body.dataset.personalityMetadataVersion = String(PERSONALITY_METADATA.schemaVersion); syncPersonalityMetadataSelectOptions(); if (personalityMetadataStatus) { personalityMetadataStatus.textContent = PERSONALITY_METADATA_LOAD.status; } -syncUiManifestSelectOptions(); - let state = readStateFromQuery(); let drawerReturnFocus = null; let querySyncTimer = null; @@ -419,11 +293,16 @@ function createElement(tagName, { className, text, attributes = {}, data = {} } return element; } +/** + * Creates one locally styled action used to verify semantic focus order. + * + * @param {string} sequence Region or item sequence identifier. + * @returns {HTMLButtonElement} Demo action with stable focus metadata. + */ function createAction(sequence) { return createElement("button", { - className: "interactive-surface", text: `${formatLabel(sequence)} action`, - attributes: { type: "button", "data-ui-kit": "button" }, + attributes: { type: "button" }, data: { demoButton: "", demoFocus: sequence } }); } @@ -447,7 +326,6 @@ function createRegion(area) { const structuralClass = area === "header" ? "ly-header ly-header--sticky " : ""; const element = createElement(semanticTags[area] ?? "section", { className: `${structuralClass}demo-region`, - attributes: { "data-ui-kit": "card" }, data: { lyArea: area, demoSequence: area } }); const heading = createElement("strong", { text: formatLabel(area) }); @@ -480,7 +358,6 @@ function createGridItem(index) { const sequence = `item-${index}`; const item = createElement("article", { className: "demo-region demo-grid-item", - attributes: { "data-ui-kit": "card" }, data: { demoSequence: sequence } }); item.append(createElement("strong", { text: `Item ${index}` }), createAction(sequence)); @@ -510,40 +387,6 @@ function formatLabel(value) { .join(" "); } -function syncUiManifestSelectOptions() { - const optionGroups = { - ui: UI_STYLE_KIT_MANIFEST.presets.map((preset) => ({ - value: preset.id, - label: preset.label || formatLabel(preset.id) - })), - theme: UI_STYLE_KIT_MANIFEST.themes.map((theme) => ({ - value: theme, - label: formatLabel(theme) - })), - mode: UI_STYLE_KIT_MANIFEST.modes.map((mode) => ({ - value: mode, - label: mode === "contrast" ? "High contrast" : formatLabel(mode) - })) - }; - - for (const [key, options] of Object.entries(optionGroups)) { - const select = controls[key]; - - if (!select) { - continue; - } - - select.replaceChildren( - ...options.map(({ value, label }) => { - const option = document.createElement("option"); - option.value = value; - option.textContent = label; - return option; - }) - ); - } -} - function syncPersonalityMetadataSelectOptions() { const select = controls.personality; @@ -564,8 +407,7 @@ function syncPersonalityMetadataSelectOptions() { } /** - * Renders advisory visual-preset pairings for the selected layout without - * mutating the independently selected UI preset. + * Renders advisory visual-system pairings for the selected layout personality. * * @returns {void} */ @@ -581,50 +423,16 @@ function syncPairingGuidance() { const describePresets = (presets, fallback) => presets.length > 0 ? presets.map(formatLabel).join(", ") : fallback; - recommendedUiGuidance.textContent = `Recommended UI: ${describePresets( + recommendedUiGuidance.textContent = `Recommended visual pairing: ${describePresets( pairing?.recommendedVisualPresets ?? [], pairing?.visualCompatibility === "any" ? "Any visual preset" : "None listed" )}`; - compatibleUiGuidance.textContent = `Compatible UI: ${describePresets( + compatibleUiGuidance.textContent = `Compatible visual pairing: ${describePresets( pairing?.compatibleVisualPresets ?? [], "None listed" )}`; } -function syncUiKitClasses() { - const prefix = UI_CLASS_PREFIXES[state.ui]; - - document.querySelectorAll("[data-ui-kit]").forEach((element) => { - const previousClasses = element.dataset.uiKitApplied?.split(" ").filter(Boolean) ?? []; - - if (previousClasses.length > 0) { - element.classList.remove(...previousClasses); - } - - const nextClasses = element.dataset.uiKit - .split(/\s+/) - .filter(Boolean) - .map((role) => `${prefix}-${role}`); - - if (nextClasses.length > 0) { - element.classList.add(...nextClasses); - } - - element.dataset.uiKitApplied = nextClasses.join(" "); - }); -} - -function syncEcosystem() { - const includesUi = state.ecosystem !== "layout-only"; - const includesInteractiveSurface = state.ecosystem === "all-three"; - - uiKitStylesheet.disabled = !includesUi; - uiKitInteractiveThemeStylesheet.disabled = !includesInteractiveSurface; - interactiveSurfaceStylesheet.disabled = !includesInteractiveSurface; - layoutCoreStylesheet.disabled = false; - ecosystemStatus.textContent = ECOSYSTEM_LABELS[state.ecosystem]; -} - function buildMarkupSnippet() { const wrapperClass = state.wrapper === "default" ? "ly-wrapper" : `ly-wrapper ly-wrapper--${state.wrapper}`; const areas = RECIPE_AREAS[state.recipe]; @@ -647,7 +455,7 @@ function buildMarkupSnippet() { function syncSnippets() { // Snippets are rendered as inert text from allowlisted state, never parsed as HTML. - importsSnippet.textContent = ECOSYSTEM_IMPORTS[state.ecosystem].join("\n"); + importsSnippet.textContent = LAYOUT_IMPORT; markupSnippet.textContent = buildMarkupSnippet(); } @@ -740,12 +548,8 @@ function applyState(options = {}) { control.value = state[key]; } - body.dataset.ui = state.ui; - body.dataset.theme = state.theme; - body.dataset.mode = state.mode; body.dataset.lyLayout = state.personality; body.dataset.demoDensity = state.density; - body.dataset.ecosystem = state.ecosystem; previewRoot.dataset.lyLayout = state.personality; previewRoot.dataset.lyDensity = state.density; @@ -761,8 +565,6 @@ function applyState(options = {}) { renderRecipe(); syncPairingGuidance(); - syncUiKitClasses(); - syncEcosystem(); syncSnippets(); updatePreviewReadout(); diff --git a/demo/index.html b/demo/index.html index 613b3a2..7c64571 100644 --- a/demo/index.html +++ b/demo/index.html @@ -6,9 +6,9 @@ - + - + @@ -57,8 +57,6 @@ - - @@ -68,18 +66,15 @@ Layout Style CSS v3.2 — Intrinsic Responsive Demo - - - - - + + - - + + - + Skip to the layout lab
@@ -103,8 +98,8 @@ Intrinsic Responsive Lab
- All three libraries -
@@ -119,14 +114,14 @@ Workbench

Layout controls

-

-

Recommended UI: Minimal SaaS

-

Compatible UI: Organic Modern

+

Recommended visual pairing: Minimal SaaS

+

Compatible visual pairing: Organic Modern

+

+ Companion-library bridges are demonstrated in + Interface Systems Lab. +

- - - - - - -
@@ -277,8 +230,8 @@

Compose for the width and height a layout actually has.

- Interactive Surface 1.5.0 state -
@@ -302,23 +255,23 @@

Responsive recipe stage

Header - +
Main - +
Footer - +
@@ -328,7 +281,7 @@

Responsive recipe stage

Intrinsic workflow fixtures

-
+

Mosaic

1 / 6 / 12 tracks @@ -343,23 +296,23 @@

Mosaic

-
+

Action Bar

- +
- - + +
- +
-
+

Content resilience

Legitimate two-axis overflow @@ -394,18 +347,18 @@

Imports and semantic markup

Ready to copy.

-
+

Imports

- +
-
+

Markup

- +
diff --git a/demo/sitemap.xml b/demo/sitemap.xml index 73485d8..896fec4 100644 --- a/demo/sitemap.xml +++ b/demo/sitemap.xml @@ -2,7 +2,7 @@ https://foscat.github.io/Layout-Style-CSS/ - 2026-09-08 + 2026-09-24 weekly 0.8 diff --git a/docs/wiki/Getting-Started.md b/docs/wiki/Getting-Started.md index 14de933..a1e53a9 100644 --- a/docs/wiki/Getting-Started.md +++ b/docs/wiki/Getting-Started.md @@ -1,9 +1,9 @@ # Getting Started -Install `layout-style-css@3.2.0` and import the root bundle: +Install `layout-style-css@3.2.1` and import the root bundle: ```bash -npm install layout-style-css@3.2.0 +npm install layout-style-css@3.2.1 ``` ```js diff --git a/docs/wiki/Home.md b/docs/wiki/Home.md index f075e26..df70575 100644 --- a/docs/wiki/Home.md +++ b/docs/wiki/Home.md @@ -1,6 +1,6 @@ # Layout Style CSS v3 -`layout-style-css@3.2.0` is the current release candidate. It remains a dependency-free, CSS-only structural system for layouts that adapt to available width and height. +`layout-style-css@3.2.1` is the current release candidate. It remains a dependency-free, CSS-only structural system for layouts that adapt to available width and height. ## Contract diff --git a/docs/wiki/Installation-And-CDN.md b/docs/wiki/Installation-And-CDN.md index c49ac15..f7125af 100644 --- a/docs/wiki/Installation-And-CDN.md +++ b/docs/wiki/Installation-And-CDN.md @@ -9,7 +9,7 @@ ## Package ```bash -npm install layout-style-css@3.2.0 +npm install layout-style-css@3.2.1 ``` ```js @@ -36,11 +36,11 @@ Package and bundler defaults (`main`, `style`, and the root export) use the read ## CDN ```html - + ``` ```html - + ``` The CDN paths include `/dist/` because CDN clients address files in the published tarball, while package import maps use the public exports. diff --git a/docs/wiki/Release-And-Publishing.md b/docs/wiki/Release-And-Publishing.md index 0782abc..9510dd8 100644 --- a/docs/wiki/Release-And-Publishing.md +++ b/docs/wiki/Release-And-Publishing.md @@ -1,6 +1,6 @@ # Release And Publishing -This guide describes the release gate for `layout-style-css@3.2.0`. Running verification does not publish, tag, push, create a GitHub Release, or change the npm registry. +This guide describes the release gate for `layout-style-css@3.2.1`. Running verification does not publish, tag, push, create a GitHub Release, or change the npm registry. ## Local Candidate Gate @@ -35,8 +35,8 @@ The UI fixture SHA is deliberately stable: companion workflows use it to load th Confirm: -- package version is `3.2.0` -- intended tag would be `v3.2.0` +- package version is `3.2.1` +- intended tag would be `v3.2.1` - runtime and peer dependencies are empty - exact exports and tarball files match the v3 contract - generated CSS matches authored sources @@ -50,7 +50,7 @@ Only after explicit approval: 1. Commit the verified candidate. 2. Push the intended branch. 3. Merge through the repository's normal review policy. -4. Create the exact `v3.2.0` tag from the protected release commit. +4. Create the exact `v3.2.1` tag from the protected release commit. 5. Publish the GitHub Release. 6. Let the protected npm workflow verify the tag and publish with provenance. 7. Confirm the GitHub Release, tag, and npm registry state independently. diff --git a/manifest.json b/manifest.json index bac80f2..cebd318 100644 --- a/manifest.json +++ b/manifest.json @@ -5,7 +5,7 @@ "breakingChange": "increment-schemaVersion-before-removing-or-renaming-fields" }, "name": "layout-style-css", - "version": "3.2.0", + "version": "3.2.1", "entrypoints": { "default": "./dist/layout-style-css.css", "minified": "./dist/layout-style-css.min.css", diff --git a/package-lock.json b/package-lock.json index c5b692c..5146629 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "layout-style-css", - "version": "3.2.0", + "version": "3.2.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "layout-style-css", - "version": "3.2.0", + "version": "3.2.1", "license": "MIT", "devDependencies": { "@playwright/test": "1.61.1", @@ -442,9 +442,9 @@ "license": "MIT" }, "node_modules/colord": { - "version": "2.9.3", - "resolved": "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz", - "integrity": "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==", + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/colord/-/colord-2.10.0.tgz", + "integrity": "sha512-AidJptpBJmjTclAp9BkLwJi0T93fo5epJnbaZslpg6QVzpHjAiveF55mE9AcUJiGMqRHgMDY8soMsQtuNYMHfw==", "dev": true, "license": "MIT" }, @@ -936,9 +936,9 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.1.tgz", - "integrity": "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz", + "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==", "dev": true, "funding": [ { diff --git a/package.json b/package.json index 3c6892f..72af19e 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "layout-style-css", "author": "Kyle Foster", - "version": "3.2.0", + "version": "3.2.1", "description": "Dependency-free structural CSS for responsive wrappers, composition primitives, recipes, utilities, and layout personalities.", "type": "module", "license": "MIT", @@ -40,9 +40,10 @@ "test:demo:firefox": "node test/demo-smoke.test.mjs --browser=firefox", "test:demo:webkit": "node test/demo-smoke.test.mjs --browser=webkit", "test:demo:all": "npm run test:demo:chromium && npm run test:demo:firefox && npm run test:demo:webkit", + "test:responsive-regions": "node test/responsive-region-fixture.test.mjs", "test:pages": "node test/pages-artifact.test.mjs", - "test": "npm run test:static && npm run test:demo:quick && npm run test:pages", - "test:full": "npm run test:static && npm run test:demo:all && npm run test:pages", + "test": "npm run test:static && npm run test:demo:quick && npm run test:responsive-regions && npm run test:pages", + "test:full": "npm run test:static && npm run test:demo:all && npm run test:responsive-regions && npm run test:pages", "check": "npm run build && npm run check:ownership && npm run lint && npm run check:demo-js && npm test && npm run pack:dry-run", "check:full": "npm run build && npm run check:ownership && npm run lint && npm run check:demo-js && npm run test:full && npm run pack:dry-run", "pack:dry-run": "npm pack --dry-run --ignore-scripts", @@ -63,8 +64,9 @@ "ui-style-kit-css": "2.4.0" }, "overrides": { + "colord": "2.10.0", "fast-uri": "3.1.7", - "js-yaml": "4.3.1", + "js-yaml": "4.3.2", "nanoid": "3.3.18", "postcss": "8.5.23" }, diff --git a/scripts/release-fixture-contract.mjs b/scripts/release-fixture-contract.mjs index 049c3fe..87d4b50 100644 --- a/scripts/release-fixture-contract.mjs +++ b/scripts/release-fixture-contract.mjs @@ -333,6 +333,10 @@ async function runCli(args) { rootDir, "--candidate-package", packageName, + "--companion-candidate-root", + stagedFixture.fixtureRoot, + "--companion-candidate-root", + siblingInteractive, "--interactive-repo", siblingInteractive, "--interactive-docs-repo", diff --git a/test/demo-smoke.test.mjs b/test/demo-smoke.test.mjs index 20bc435..c1aa1b3 100644 --- a/test/demo-smoke.test.mjs +++ b/test/demo-smoke.test.mjs @@ -9,21 +9,7 @@ const root = normalize(fileURLToPath(new URL("..", import.meta.url))).replace(/[ const demoHtml = readFileSync(join(root, "demo", "index.html"), "utf8"); const demoCss = readFileSync(join(root, "demo", "demo.css"), "utf8"); const demoJs = readFileSync(join(root, "demo", "demo.js"), "utf8"); -const uiManifest = JSON.parse( - readFileSync(join(root, "node_modules", "ui-style-kit-css", "manifest.json"), "utf8") -); const personalityMetadata = JSON.parse(readFileSync(join(root, "personalities.json"), "utf8")); -const companionCssFixtures = Object.freeze({ - "ui-style-kit.visual.min.css": readFileSync( - join(root, "node_modules", "ui-style-kit-css", "dist", "ui-style-kit.visual.min.css") - ), - "interactive-surface-theme.css": readFileSync( - join(root, "node_modules", "ui-style-kit-css", "styles", "interactive-surface-theme.css") - ), - "state-core.css": readFileSync( - join(root, "node_modules", "interactive-surface-css", "state-core.css") - ) -}); const browserName = process.argv.find((argument) => argument.startsWith("--browser="))?.split("=")[1] ?? @@ -46,15 +32,6 @@ const recipes = [ "card-grid" ]; const personalities = personalityMetadata.personalities.map(({ id }) => id); -const expectedUiPresetPairs = [ - ["minimal-saas", "saas"], ["bento", "bento"], ["maximalist", "max"], - ["bauhaus", "bau"], ["tactile", "tactile"], ["neumorphism", "neo"], - ["retrofuturism", "retro"], ["brutalism", "brutal"], ["cyberpunk", "cyber"], - ["y2k", "y2k"], ["retro-glass", "rg"], ["editorial-luxe", "luxe"], - ["organic-modern", "organic"], ["industrial-utility", "utility"], - ["technical-blueprint", "blueprint"], ["art-deco", "deco"], ["clay", "clay"], - ["data-terminal", "terminal"], ["paper-editorial", "paper"], ["neo-noir", "noir"] -]; const wrappers = [ "default", "compact", @@ -148,7 +125,20 @@ const topologyEdges = [ const assertStaticDemoContract = () => { assert.match(demoHtml, /Layout Style CSS v3\.2/); - assert.match(demoHtml, /content="3\.2\.0"/); + assert.match(demoHtml, /content="3\.2\.1"/); + for (const companionPackage of ["ui-style-kit-css", "interactive-surface-css"]) { + assert.doesNotMatch( + `${demoHtml}\n${demoJs}`, + new RegExp(companionPackage), + `The standalone demo must not load or configure ${companionPackage}.` + ); + } + assert.doesNotMatch(demoHtml, /(?:unpkg\.com|data-ui-kit|data-ecosystem)/); + assert.match( + demoHtml, + /class="demo-bridge-note"[\s\S]*href="https:\/\/sanderson-technology-enterprises\.github\.io\/interface-systems-lab\/"[\s\S]*Interface Systems Lab/, + "Bridge guidance must direct people to Interface Systems Lab." + ); assert.match(demoHtml, /id="deviceSelect"/); assert.match(demoHtml, /id="containerSelect"/); assert.match(demoHtml, /id="heightSelect"/); @@ -156,17 +146,17 @@ const assertStaticDemoContract = () => { assert.match(demoHtml, /id="topologyReadout"/); assert.match( demoHtml, - /href="\.\.\/dist\/layout-style-css\.css\?v=3\.2\.0"/, + /href="\.\.\/dist\/layout-style-css\.css\?v=3\.2\.1"/, "The demo should cache-bust its v3 layout bundle." ); assert.match( demoHtml, - /href="\.\/demo\.css\?v=3\.2\.0"/, + /href="\.\/demo\.css\?v=3\.2\.1"/, "The demo should cache-bust its v3 presentation styles." ); assert.match( demoHtml, - /src="\.\/demo\.js\?v=3\.2\.0"/, + /src="\.\/demo\.js\?v=3\.2\.1"/, "The demo should cache-bust its v3 controller." ); assert.doesNotMatch(demoHtml, /integrations\/ui-style-kit\.css/); @@ -187,23 +177,6 @@ const assertStaticDemoContract = () => { assert.match(demoJs, /URLSearchParams/); assert.doesNotMatch(demoJs, /RECIPE_CLASSES/); assert.doesNotMatch(demoJs, /layoutIntegrationStylesheet/); - for (const uiPreset of [ - "editorial-luxe", "organic-modern", "industrial-utility", "technical-blueprint", - "art-deco", "clay", "data-terminal", "paper-editorial", "neo-noir" - ]) { - assert.match(demoJs, new RegExp(`id: "${uiPreset}"`)); - } - const fallbackPresetSource = demoJs.slice( - demoJs.indexOf("presets: Object.freeze(["), - demoJs.indexOf("themes: Object.freeze([") - ); - assert.deepEqual( - [...fallbackPresetSource.matchAll(/id: "([a-z0-9-]+)"[^}]+prefix: "([a-z0-9-]+)"/g)] - .map(([, id, prefix]) => [id, prefix]), - expectedUiPresetPairs, - "The packaged UI manifest fallback must mirror the approved twenty-preset inventory." - ); - assert.match(demoCss, /--demo-container-block-size/); assert.match(demoCss, /data-demo-height-tier="short"/); assert.match(demoCss, /data-demo-height-tier="shallow"/); @@ -330,31 +303,26 @@ const contrastRatio = (foreground, background) => { const verifyCodeBlockContrast = async (page, baseUrl) => { await page.setViewportSize({ width: 1440, height: 900 }); - await page.goto(`${baseUrl}?ecosystem=all-three&mode=light`, { - waitUntil: "domcontentloaded" - }); + await page.goto(baseUrl, { waitUntil: "domcontentloaded" }); await page.waitForFunction(() => document.body.dataset.demoReady === "true"); - for (const mode of ["light", "dark", "contrast"]) { - await setControl(page, "modeSelect", mode); - const blocks = await page.locator(".demo-code-card pre").evaluateAll((preElements) => - preElements.map((preElement) => { - const codeElement = preElement.querySelector("code"); - return { - background: getComputedStyle(preElement).backgroundColor, - color: getComputedStyle(codeElement).color - }; - }) - ); + const blocks = await page.locator(".demo-code-card pre").evaluateAll((preElements) => + preElements.map((preElement) => { + const codeElement = preElement.querySelector("code"); + return { + background: getComputedStyle(preElement).backgroundColor, + color: getComputedStyle(codeElement).color + }; + }) + ); - assert.equal(blocks.length, 2, `${mode}: expected both copy-ready code blocks.`); - for (const [index, block] of blocks.entries()) { - const ratio = contrastRatio(parseRgbColor(block.color), parseRgbColor(block.background)); - assert( - ratio >= 4.5, - `${mode} code block ${index + 1} has ${ratio.toFixed(2)}:1 contrast; expected at least 4.5:1.` - ); - } + assert.equal(blocks.length, 2, "Expected both copy-ready code blocks."); + for (const [index, block] of blocks.entries()) { + const ratio = contrastRatio(parseRgbColor(block.color), parseRgbColor(block.background)); + assert( + ratio >= 4.5, + `Code block ${index + 1} has ${ratio.toFixed(2)}:1 contrast; expected at least 4.5:1.` + ); } }; @@ -443,7 +411,7 @@ const assertNoHorizontalFailures = (snapshot, label) => { /** * Verifies usable automatic recipe floors and accessible two-axis Scroll - * behavior in both the standalone and complete ecosystem modes. + * behavior in the standalone Layout Style CSS demo. * * @param {import("@playwright/test").Page} page Active browser page. * @param {string} baseUrl Demo server URL. @@ -452,8 +420,8 @@ const assertNoHorizontalFailures = (snapshot, label) => { const verifyContentResilience = async (page, baseUrl) => { await page.setViewportSize({ width: 1440, height: 900 }); - for (const ecosystem of ["layout-only", "all-three"]) { - await page.goto(`${baseUrl}?ecosystem=${ecosystem}&wrapper=full`, { + for (const demoMode of ["standalone"]) { + await page.goto(`${baseUrl}?wrapper=full`, { waitUntil: "domcontentloaded" }); await page.waitForFunction(() => document.body.dataset.demoReady === "true"); @@ -546,35 +514,35 @@ const verifyContentResilience = async (page, baseUrl) => { const measurement = result.measurements[recipeName]; assert( measurement.areaWidths.main + 1 >= measurement.mainFloor, - `${ecosystem} ${recipeName} main width ${measurement.areaWidths.main}px did not meet ${measurement.mainFloor}px.` + `${demoMode} ${recipeName} main width ${measurement.areaWidths.main}px did not meet ${measurement.mainFloor}px.` ); } for (const areaName of ["primary", "secondary"]) { const measurement = result.measurements["list-detail"]; assert( measurement.areaWidths[areaName] + 1 >= measurement.paneFloor, - `${ecosystem} List Detail ${areaName} did not meet its pane floor.` + `${demoMode} List Detail ${areaName} did not meet its pane floor.` ); } for (const areaName of ["content", "media"]) { const measurement = result.measurements["split-hero"]; assert( measurement.areaWidths[areaName] + 1 >= measurement.splitFloor, - `${ecosystem} Split Hero ${areaName} did not meet its split floor.` + `${demoMode} Split Hero ${areaName} did not meet its split floor.` ); } assert( result.scrollMetrics.horizontalOverflow > 0, - `${ecosystem} Scroll lacked inline overflow.` + `${demoMode} Scroll lacked inline overflow.` ); assert( result.scrollMetrics.verticalOverflow > 0, - `${ecosystem} Scroll lacked block overflow.` + `${demoMode} Scroll lacked block overflow.` ); - assert(result.scrollMetrics.scrollLeft > 0, `${ecosystem} Scroll could not move inline.`); + assert(result.scrollMetrics.scrollLeft > 0, `${demoMode} Scroll could not move inline.`); assert( result.scrollMetrics.scrollTop > 0, - `${ecosystem} Scroll could not move in block flow.` + `${demoMode} Scroll could not move in block flow.` ); assert.equal(result.scrollMetrics.overflowX, "auto"); assert.equal(result.scrollMetrics.overflowY, "auto"); @@ -591,7 +559,7 @@ const verifyContentResilience = async (page, baseUrl) => { */ const verifyMosaicComposition = async (page, baseUrl) => { await page.setViewportSize({ width: 1920, height: 1080 }); - await page.goto(`${baseUrl}?ecosystem=layout-only&wrapper=full`, { + await page.goto(`${baseUrl}?wrapper=full`, { waitUntil: "domcontentloaded" }); await page.waitForFunction(() => document.body.dataset.demoReady === "true"); @@ -706,7 +674,7 @@ const verifyMosaicComposition = async (page, baseUrl) => { */ const verifyActionBarComposition = async (page, baseUrl) => { await page.setViewportSize({ width: 1440, height: 900 }); - await page.goto(`${baseUrl}?ecosystem=layout-only&wrapper=full`, { + await page.goto(`${baseUrl}?wrapper=full`, { waitUntil: "domcontentloaded" }); await page.waitForFunction(() => document.body.dataset.demoReady === "true"); @@ -793,7 +761,7 @@ const verifyActionBarComposition = async (page, baseUrl) => { */ const verifyAreaAwareAppShell = async (page, baseUrl) => { await page.setViewportSize({ width: 1920, height: 1080 }); - await page.goto(`${baseUrl}?ecosystem=layout-only&wrapper=full`, { + await page.goto(`${baseUrl}?wrapper=full`, { waitUntil: "domcontentloaded" }); await page.waitForFunction(() => document.body.dataset.demoReady === "true"); @@ -911,25 +879,6 @@ const verifyAreaAwareAppShell = async (page, baseUrl) => { assert.match(manual.areas, /"header" "sidebar" "main" "aside" "footer"/); }; -const installExternalFixtures = async (page) => { - await page.route("https://unpkg.com/**", async (route) => { - const url = route.request().url(); - if (url.endsWith("/manifest.json")) { - await route.fulfill({ - body: JSON.stringify(uiManifest), - contentType: "application/json", - status: 200 - }); - return; - } - - const [fixtureName, fixtureBody] = - Object.entries(companionCssFixtures).find(([name]) => url.endsWith(`/${name}`)) ?? []; - assert(fixtureName && fixtureBody, `No local fixture exists for ${url}.`); - await route.fulfill({ body: fixtureBody, contentType: "text/css", status: 200 }); - }); -}; - /** * Verifies demo identity, required controls, durable query state, and legacy * density URL normalization. @@ -939,7 +888,7 @@ const installExternalFixtures = async (page) => { * @returns {Promise} */ const verifyIdentityAndControls = async (page, baseUrl) => { - await page.goto(`${baseUrl}?ecosystem=layout-only`, { waitUntil: "domcontentloaded" }); + await page.goto(baseUrl, { waitUntil: "domcontentloaded" }); await page.waitForFunction(() => document.body.dataset.demoReady === "true"); await page.waitForFunction(() => /\d+\s*×\s*\d+/.test(document.querySelector("#containerReadout")?.textContent)); @@ -961,6 +910,14 @@ const verifyIdentityAndControls = async (page, baseUrl) => { ]) { assert.equal(await page.locator(`#${id}`).count(), 1, `Missing #${id}.`); } + for (const removedId of ["uiSelect", "themeSelect", "modeSelect", "ecosystemSelect"]) { + assert.equal(await page.locator(`#${removedId}`).count(), 0, `Unexpected #${removedId}.`); + } + assert.equal( + await page.locator(".demo-bridge-note a").getAttribute("href"), + "https://sanderson-technology-enterprises.github.io/interface-systems-lab/" + ); + assert.equal(await page.locator("#importsSnippet").textContent(), 'import "layout-style-css";'); assert.deepEqual( await page.locator("#containerSelect option").evaluateAll((options) => options.map(({ value }) => value)), @@ -969,7 +926,7 @@ const verifyIdentityAndControls = async (page, baseUrl) => { ); await page.goto( - `${baseUrl}?device=custom&container=49rem&height=31rem&responsive=manual&wrapper=wide&recipe=docs&personality=bauhaus&ecosystem=layout-only`, + `${baseUrl}?device=custom&container=49rem&height=31rem&responsive=manual&wrapper=wide&recipe=docs&personality=bauhaus`, { waitUntil: "domcontentloaded" } ); await page.waitForFunction(() => document.body.dataset.demoReady === "true"); @@ -998,7 +955,7 @@ const verifyIdentityAndControls = async (page, baseUrl) => { */ const verifyCompositionFixtures = async (page, baseUrl) => { await page.setViewportSize({ width: 1920, height: 1080 }); - await page.goto(`${baseUrl}?ecosystem=layout-only&wrapper=full`, { + await page.goto(`${baseUrl}?wrapper=full`, { waitUntil: "domcontentloaded" }); await page.waitForFunction(() => document.body.dataset.demoReady === "true"); @@ -1032,7 +989,7 @@ const verifyCompositionFixtures = async (page, baseUrl) => { }; const verifyPersonalityOptionsUsePairingMetadata = async (page, baseUrl) => { - const metadataUrl = new URL("../personalities.json?v=3.2.0", baseUrl).toString(); + const metadataUrl = new URL("../personalities.json?v=3.2.1", baseUrl).toString(); const pairingFixture = { schemaVersion: 1, personalities: [ @@ -1060,7 +1017,7 @@ const verifyPersonalityOptionsUsePairingMetadata = async (page, baseUrl) => { status: 200 }) ); - await page.goto(`${baseUrl}?ecosystem=layout-only`, { waitUntil: "domcontentloaded" }); + await page.goto(baseUrl, { waitUntil: "domcontentloaded" }); await page.waitForFunction(() => document.body.dataset.demoReady === "true"); assert.deepEqual( @@ -1080,76 +1037,29 @@ const verifyPersonalityOptionsUsePairingMetadata = async (page, baseUrl) => { assert.match(await page.locator("#recommendedUiGuidance").textContent(), /Minimal SaaS/i); assert.match(await page.locator("#compatibleUiGuidance").textContent(), /Organic Modern/i); - const selectedUi = await page.locator("#uiSelect").inputValue(); await setControl(page, "personalitySelect", "synthwave"); - assert.equal(await page.locator("#uiSelect").inputValue(), selectedUi); assert.match(await page.locator("#recommendedUiGuidance").textContent(), /Cyberpunk.*Retrofuturism/i); await page.unroute(metadataUrl); }; -const verifySynthwaveVisualRecommendations = async (page, baseUrl) => { - const synthwave = personalityMetadata.personalities.find(({ id }) => id === "synthwave"); - - assert.deepEqual(synthwave?.recommendedVisualPresets, ["cyberpunk", "retrofuturism"]); - assert.deepEqual(synthwave?.visualVerification?.computedProperties, { - cyberpunk: { boxShadow: "0px 0px 12px" }, - retrofuturism: { boxShadow: "0px 2.88px 6.72px" } - }); - for (const ui of synthwave.recommendedVisualPresets) { - await page.goto( - `${baseUrl}?ecosystem=layout-ui&personality=synthwave&ui=${ui}&theme=cyber-lime&mode=dark`, - { waitUntil: "domcontentloaded" } - ); - await page.waitForFunction(() => document.body.dataset.demoReady === "true"); - - const rendered = await page.evaluate(() => { - /* A dedicated visible article isolates UI paint from the layout demo's own chrome. */ - const pairingFixture = document.createElement("article"); - pairingFixture.id = "pairingVisualFixture"; - pairingFixture.textContent = "Visual pairing verification"; - pairingFixture.style.position = "fixed"; - pairingFixture.style.inset = "1rem 1rem auto auto"; - pairingFixture.style.zIndex = "1000"; - document.body.append(pairingFixture); - - return { - layout: document.querySelector("#previewRoot")?.dataset.lyLayout, - ui: document.body.dataset.ui, - fixtureVisible: pairingFixture.getClientRects().length > 0, - pairingFixtureShadow: getComputedStyle(pairingFixture).boxShadow - }; - }); - - assert.equal(rendered.layout, "synthwave", `${ui} must not override the independent layout selector.`); - assert.equal(rendered.ui, ui); - assert.equal(rendered.fixtureVisible, true, "The visual pairing fixture must participate in rendering."); - assert.match( - rendered.pairingFixtureShadow, - new RegExp(synthwave.visualVerification.computedProperties[ui].boxShadow), - `${ui} must retain its distinct rendered article shadow treatment.` - ); - } -}; - /** * Verifies that metadata failure falls back to the packaged personality list - * inside a hermetic browser context with local companion stylesheet fixtures. + * inside a hermetic browser context. * * @param {import("@playwright/test").Page} page Active demo page. * @param {string} baseUrl Demo server URL. * @returns {Promise} */ const verifyPersonalityMetadataFailureRecovery = async (page, baseUrl) => { - const metadataUrl = new URL("../personalities.json?v=3.2.0", baseUrl).toString(); + const metadataUrl = new URL("../personalities.json?v=3.2.1", baseUrl).toString(); const recoveryContext = await page.context().browser().newContext(); try { const recoveryPage = await recoveryContext.newPage(); - await installExternalFixtures(recoveryPage); await recoveryPage.route(metadataUrl, (route) => route.fulfill({ status: 503, body: "Unavailable" }) ); - await recoveryPage.goto(`${baseUrl}?ecosystem=layout-only`, { + await recoveryPage.goto(baseUrl, { waitUntil: "domcontentloaded" }); await recoveryPage.waitForFunction( @@ -1175,7 +1085,7 @@ const verifyPersonalityMetadataFailureRecovery = async (page, baseUrl) => { const verifyTopologyEdges = async (page, baseUrl) => { await page.setViewportSize({ width: 1920, height: 1080 }); - await page.goto(`${baseUrl}?ecosystem=layout-only&wrapper=full`, { + await page.goto(`${baseUrl}?wrapper=full`, { waitUntil: "domcontentloaded" }); await page.waitForFunction(() => document.body.dataset.demoReady === "true"); @@ -1231,7 +1141,7 @@ const verifyAppShellRowGeometry = async (page, baseUrl) => { await page.setViewportSize({ width: 1920, height: 1080 }); for (const testCase of cases) { await page.goto( - `${baseUrl}?ecosystem=layout-only&wrapper=full&recipe=app-shell&container=${testCase.width}&height=50rem&personality=${testCase.personality}`, + `${baseUrl}?wrapper=full&recipe=app-shell&container=${testCase.width}&height=50rem&personality=${testCase.personality}`, { waitUntil: "domcontentloaded" } ); await page.waitForFunction(() => document.body.dataset.demoReady === "true"); @@ -1266,7 +1176,7 @@ const verifyAppShellRowGeometry = async (page, baseUrl) => { const verifyManualAndNearestContainer = async (page, baseUrl) => { await page.setViewportSize({ width: 1920, height: 1080 }); await page.goto( - `${baseUrl}?ecosystem=layout-only&device=custom&container=73rem&wrapper=full&recipe=docs&responsive=manual`, + `${baseUrl}?device=custom&container=73rem&wrapper=full&recipe=docs&responsive=manual`, { waitUntil: "domcontentloaded" } ); await page.waitForFunction(() => document.body.dataset.demoReady === "true"); @@ -1344,7 +1254,7 @@ const verifyManualAndNearestContainer = async (page, baseUrl) => { const verifyHeightBehavior = async (page, baseUrl) => { await page.goto( - `${baseUrl}?ecosystem=layout-only&wrapper=full&recipe=app-shell&container=73rem`, + `${baseUrl}?wrapper=full&recipe=app-shell&container=73rem`, { waitUntil: "domcontentloaded" } ); await page.waitForFunction(() => document.body.dataset.demoReady === "true"); @@ -1436,8 +1346,7 @@ const verifySectionAndGutterContracts = async (browser, baseUrl) => { for (const height of [1080, 704, 480]) { const contractPage = await browser.newPage({ viewport: { width: 1440, height } }); try { - await installExternalFixtures(contractPage); - await contractPage.goto(`${baseUrl}?ecosystem=layout-only`, { + await contractPage.goto(baseUrl, { waitUntil: "domcontentloaded" }); await contractPage.waitForFunction(() => document.body.dataset.demoReady === "true"); @@ -1499,8 +1408,7 @@ const verifyDensityContexts = async (browser, baseUrl) => { for (const height of [900, 600, 480]) { const densityPage = await browser.newPage({ viewport: { width: 1440, height } }); try { - await installExternalFixtures(densityPage); - await densityPage.goto(`${baseUrl}?ecosystem=layout-only`, { + await densityPage.goto(baseUrl, { waitUntil: "domcontentloaded" }); await densityPage.waitForFunction(() => document.body.dataset.demoReady === "true"); @@ -1592,7 +1500,6 @@ const verifyDefaultFontHeightTiers = async (baseUrl) => { const fontPage = await fontBrowser.newPage(); try { - await installExternalFixtures(fontPage); for (const sample of [ { height: 550, tier: "shallow" }, @@ -1601,7 +1508,7 @@ const verifyDefaultFontHeightTiers = async (baseUrl) => { ]) { await fontPage.setViewportSize({ width: 1440, height: sample.height }); await fontPage.goto( - `${baseUrl}?ecosystem=layout-only&wrapper=full&recipe=app-shell&container=73rem`, + `${baseUrl}?wrapper=full&recipe=app-shell&container=73rem`, { waitUntil: "domcontentloaded" } ); await fontPage.waitForFunction(() => document.body.dataset.demoReady === "true"); @@ -1622,7 +1529,7 @@ const verifyDefaultFontHeightTiers = async (baseUrl) => { }; const verifyDeviceMatrix = async (page, baseUrl) => { - await page.goto(`${baseUrl}?ecosystem=layout-only&wrapper=full`, { + await page.goto(`${baseUrl}?wrapper=full`, { waitUntil: "domcontentloaded" }); await page.waitForFunction(() => document.body.dataset.demoReady === "true"); @@ -1649,7 +1556,7 @@ const verifyDeviceMatrix = async (page, baseUrl) => { const verifyPersonalityMatrix = async (page, baseUrl) => { await page.setViewportSize({ width: 1920, height: 1080 }); - await page.goto(`${baseUrl}?ecosystem=layout-only&wrapper=full`, { + await page.goto(`${baseUrl}?wrapper=full`, { waitUntil: "domcontentloaded" }); await page.waitForFunction(() => document.body.dataset.demoReady === "true"); @@ -1690,7 +1597,7 @@ const verifyPersonalityMatrix = async (page, baseUrl) => { const verifyPersonalityGeometry = async (page, baseUrl) => { await page.setViewportSize({ width: 1920, height: 1080 }); await page.goto( - `${baseUrl}?ecosystem=layout-only&wrapper=full&recipe=app-shell&container=73rem&height=50rem`, + `${baseUrl}?wrapper=full&recipe=app-shell&container=73rem&height=50rem`, { waitUntil: "domcontentloaded" } ); await page.waitForFunction(() => document.body.dataset.demoReady === "true"); @@ -1760,7 +1667,7 @@ const verifyPersonalityGeometry = async (page, baseUrl) => { const verifySpecializedPersonalityGeometry = async (page, baseUrl) => { await page.setViewportSize({ width: 1920, height: 1080 }); await page.goto( - `${baseUrl}?ecosystem=layout-only&wrapper=full&recipe=split-hero&container=73rem&height=50rem`, + `${baseUrl}?wrapper=full&recipe=split-hero&container=73rem&height=50rem`, { waitUntil: "domcontentloaded" } ); await page.waitForFunction(() => document.body.dataset.demoReady === "true"); @@ -1818,7 +1725,7 @@ const verifySpecializedPersonalityGeometry = async (page, baseUrl) => { const verifyMinimumWidth = async (page, baseUrl) => { await page.setViewportSize({ width: 320, height: 800 }); - await page.goto(`${baseUrl}?ecosystem=layout-only&device=custom&container=auto`, { + await page.goto(`${baseUrl}?device=custom&container=auto`, { waitUntil: "domcontentloaded" }); await page.waitForFunction(() => document.body.dataset.demoReady === "true"); @@ -1845,7 +1752,7 @@ const verifyMinimumWidth = async (page, baseUrl) => { */ const verifyWorkspaceMeasure = async (page, baseUrl) => { await page.setViewportSize({ width: 1920, height: 1080 }); - await page.goto(`${baseUrl}?ecosystem=layout-only`, { waitUntil: "domcontentloaded" }); + await page.goto(baseUrl, { waitUntil: "domcontentloaded" }); await page.waitForFunction(() => document.body.dataset.demoReady === "true"); const widths = await page.evaluate(() => { @@ -1891,7 +1798,7 @@ const verifyWorkspaceMeasure = async (page, baseUrl) => { const verifyBreakoutGeometry = async (page, baseUrl) => { await page.setViewportSize({ width: 1920, height: 1080 }); - await page.goto(`${baseUrl}?ecosystem=layout-only`, { waitUntil: "domcontentloaded" }); + await page.goto(baseUrl, { waitUntil: "domcontentloaded" }); await page.waitForFunction(() => document.body.dataset.demoReady === "true"); const widths = await page.evaluate(() => { @@ -1934,7 +1841,7 @@ const verifyBreakoutGeometry = async (page, baseUrl) => { */ const verifyProfileAndUtilityIsolation = async (page, baseUrl) => { await page.setViewportSize({ width: 1280, height: 800 }); - await page.goto(`${baseUrl}?ecosystem=layout-only&wrapper=full&personality=minimal-saas`, { + await page.goto(`${baseUrl}?wrapper=full&personality=minimal-saas`, { waitUntil: "domcontentloaded" }); await page.waitForFunction(() => document.body.dataset.demoReady === "true"); @@ -2026,7 +1933,7 @@ const verifyPrimitiveOverflow = async (page, baseUrl) => { "scroll" ]; - await page.goto(`${baseUrl}?ecosystem=layout-only&wrapper=full`, { + await page.goto(`${baseUrl}?wrapper=full`, { waitUntil: "domcontentloaded" }); await page.waitForFunction(() => document.body.dataset.demoReady === "true"); @@ -2167,7 +2074,7 @@ const verifyPrimitiveOverflow = async (page, baseUrl) => { const verifyInteractions = async (page, baseUrl) => { await page.setViewportSize({ width: 360, height: 800 }); - await page.goto(`${baseUrl}?ecosystem=layout-only`, { waitUntil: "domcontentloaded" }); + await page.goto(baseUrl, { waitUntil: "domcontentloaded" }); await page.waitForFunction(() => document.body.dataset.demoReady === "true"); const drawerToggle = page.locator("#demoControlsToggle"); @@ -2205,16 +2112,20 @@ const browser = await browserType.launch({ headless: true }); const page = await browser.newPage(); const pageErrors = []; const consoleErrors = []; +const companionPackageRequests = []; page.on("pageerror", (error) => pageErrors.push(error.message)); page.on("console", (message) => { if (message.type() === "error") consoleErrors.push(message.text()); }); +page.on("request", (request) => { + if (/(?:ui-style-kit-css|interactive-surface-css)/.test(request.url())) { + companionPackageRequests.push(request.url()); + } +}); try { - await installExternalFixtures(page); await verifyPersonalityOptionsUsePairingMetadata(page, server.baseUrl); await verifyPersonalityMetadataFailureRecovery(page, server.baseUrl); - await verifySynthwaveVisualRecommendations(page, server.baseUrl); await verifyIdentityAndControls(page, server.baseUrl); await verifyCompositionFixtures(page, server.baseUrl); await verifyTopologyEdges(page, server.baseUrl); @@ -2240,6 +2151,11 @@ try { await verifyCodeBlockContrast(page, server.baseUrl); await verifyInteractions(page, server.baseUrl); + assert.deepEqual( + companionPackageRequests, + [], + "The standalone demo must not request companion-package assets." + ); assert.deepEqual(pageErrors, [], `Page errors:\n${pageErrors.join("\n")}`); assert.deepEqual(consoleErrors, [], `Console errors:\n${consoleErrors.join("\n")}`); console.log( diff --git a/test/layout-css-contract.test.mjs b/test/layout-css-contract.test.mjs index ef4ce98..6401de0 100644 --- a/test/layout-css-contract.test.mjs +++ b/test/layout-css-contract.test.mjs @@ -126,7 +126,7 @@ function customPropertyValue(css, property) { return value.replace(/\s+/g, " ").trim(); } -assert.equal(packageJson.version, "3.2.0", "The minor branch must expose version 3.2.0"); +assert.equal(packageJson.version, "3.2.1", "The patch branch must expose version 3.2.1"); assert.equal(packageJson.engines?.node, ">=20", "Development must retain the Node 20 floor"); assert.deepEqual(packageJson.exports, expectedExports, "Package exports must match the clean v3 API"); assert.deepEqual( diff --git a/test/manifest-contract.test.mjs b/test/manifest-contract.test.mjs index 4294cdf..97b2858 100644 --- a/test/manifest-contract.test.mjs +++ b/test/manifest-contract.test.mjs @@ -171,7 +171,7 @@ function runtimeConsumedCustomProperties(css) { test("ecosystem manifest publishes the structural API and package export", () => { assert.equal(manifest.schemaVersion, 1); assert.equal(manifest.name, packageJson.name); - assert.equal(manifest.version, "3.2.0"); + assert.equal(manifest.version, "3.2.1"); assert.equal(manifest.version, packageJson.version); assert.equal(manifest.schemaPolicy.compatibility, "additive-within-major"); assert.equal( diff --git a/test/package-contract.test.mjs b/test/package-contract.test.mjs index 2caf092..8dbf643 100644 --- a/test/package-contract.test.mjs +++ b/test/package-contract.test.mjs @@ -19,8 +19,9 @@ const ecosystemManifest = JSON.parse( ); // Exact overrides keep the release audit deterministic without promoting transitive tooling to direct dependencies. const expectedSecurityOverrides = { + colord: "2.10.0", "fast-uri": "3.1.7", - "js-yaml": "4.3.1", + "js-yaml": "4.3.2", nanoid: "3.3.18", postcss: "8.5.23", }; @@ -130,11 +131,11 @@ test("package defaults and homepage expose the intended distribution contract", } }); -test("release metadata stays synchronized for the 3.2.0 minor", () => { - assert.equal(manifest.version, "3.2.0"); - assert.equal(packageLock.version, "3.2.0"); - assert.equal(packageLock.packages[""].version, "3.2.0"); - assert.equal(ecosystemManifest.version, "3.2.0"); +test("release metadata stays synchronized for the 3.2.1 patch", () => { + assert.equal(manifest.version, "3.2.1"); + assert.equal(packageLock.version, "3.2.1"); + assert.equal(packageLock.packages[""].version, "3.2.1"); + assert.equal(ecosystemManifest.version, "3.2.1"); assert.equal(manifest.devDependencies["ui-style-kit-css"], "2.4.0"); assert.equal(packageLock.packages[""].devDependencies["ui-style-kit-css"], "2.4.0"); assert.equal(packageLock.packages["node_modules/ui-style-kit-css"].version, "2.4.0"); diff --git a/test/pages-artifact.test.mjs b/test/pages-artifact.test.mjs index 8977e49..65a3b0f 100644 --- a/test/pages-artifact.test.mjs +++ b/test/pages-artifact.test.mjs @@ -81,7 +81,7 @@ for (const { attribute, path } of [ const contents = readFileSync(join(outputDir, ...path.split("/"))); const fingerprint = createHash("sha256").update(contents).digest("hex").slice(0, 12); assert( - index.includes(`${attribute}="./${path}?v=3.2.0-${fingerprint}"`), + index.includes(`${attribute}="./${path}?v=3.2.1-${fingerprint}"`), `Pages should fingerprint ${path} from its deployed contents.` ); } @@ -101,7 +101,7 @@ assert( "Pages demo should preserve the source demo canonical URL" ); assert( - index.includes('"version": "3.2.0"') && index.includes("Layout Style CSS v3"), + index.includes('"version": "3.2.1"') && index.includes("Layout Style CSS v3"), "Pages metadata should identify the v3 intrinsic responsive lab" ); assert( @@ -124,7 +124,7 @@ assert( sitemap.includes(`${expectedPagesBaseUrl}`), "The sitemap must use the case-sensitive Pages canonical URL." ); -assert(sitemap.includes("2026-09-08"), "Pages sitemap should carry 3.2.0 metadata"); +assert(sitemap.includes("2026-09-24"), "Pages sitemap should carry 3.2.1 metadata"); const pagesWorkflow = readFileSync(pagesWorkflowPath, "utf8"); const pagesPreflightStep = pagesWorkflow.indexOf("- name: Verify Pages configuration"); diff --git a/test/release-docs-contract.test.mjs b/test/release-docs-contract.test.mjs index c43543e..2da4c36 100644 --- a/test/release-docs-contract.test.mjs +++ b/test/release-docs-contract.test.mjs @@ -100,7 +100,7 @@ assert( ); for (const requiredText of [ - "3.2.0", + "3.2.1", "Node.js 20", "dependency-free", "zero-configuration", @@ -327,9 +327,11 @@ const minorReleaseHeadings = changelog.match(/^## \[3\.1\.0\] - 2026-08-25$/gm) assert.equal(minorReleaseHeadings.length, 1, "Changelog needs one dated 3.1.0 section."); const nextMinorReleaseHeadings = changelog.match(/^## \[3\.2\.0\] - 2026-09-08$/gm) ?? []; assert.equal(nextMinorReleaseHeadings.length, 1, "Changelog needs one dated 3.2.0 section."); +const responsivePatchHeadings = changelog.match(/^## \[3\.2\.1\] - 2026-09-24$/gm) ?? []; +assert.equal(responsivePatchHeadings.length, 1, "Changelog needs one dated 3.2.1 section."); assert( - release.includes("layout-style-css@3.2.0") && - release.includes("v3.2.0") && + release.includes("layout-style-css@3.2.1") && + release.includes("v3.2.1") && release.includes("does not publish"), "Release documentation must distinguish verification from publication." ); @@ -357,6 +359,12 @@ assert( "Static contract files must run sequentially to keep generated dist reads deterministic." ); assert.equal(packageJson.scripts["test:demo:quick"], "node test/demo-smoke.test.mjs --quick --browser=chromium"); +assert.equal( + packageJson.scripts["test:responsive-regions"], + "node test/responsive-region-fixture.test.mjs" +); +assert(packageJson.scripts.test.includes("test:responsive-regions")); +assert(packageJson.scripts["test:full"].includes("test:responsive-regions")); for (const browser of ["chromium", "firefox", "webkit"]) { assert.equal( packageJson.scripts[`test:demo:${browser}`], @@ -390,7 +398,7 @@ for (const browser of ["chromium", "firefox", "webkit"]) { const publishWorkflow = read(".github", "workflows", "npm-publish.yml"); const demoSmoke = read("test", "demo-smoke.test.mjs"); -assert(publishWorkflow.includes("for example v3.2.0")); +assert(publishWorkflow.includes("for example v3.2.1")); assert(publishWorkflow.includes("playwright install --with-deps chromium firefox webkit")); assert( !/^\s*run:\s+npm\s+run\s+release:verify\s*$/m.test(publishWorkflow) && diff --git a/test/release-preflight.test.mjs b/test/release-preflight.test.mjs index 8980a2f..138ac90 100644 --- a/test/release-preflight.test.mjs +++ b/test/release-preflight.test.mjs @@ -96,7 +96,7 @@ test("stages active candidate version without mutating the reviewed UI fixture", const staged = releaseContract.stageFixtureForCandidate(sourceRoot, { candidatePackage: "layout-style-css", - candidateVersion: "3.2.0", + candidateVersion: "3.2.1", }); try { assert.notEqual(staged.fixtureRoot, sourceRoot); @@ -111,7 +111,7 @@ test("stages active candidate version without mutating the reviewed UI fixture", "utf8", ), ).supportedCombinations.current["layout-style-css"], - "3.2.0", + "3.2.1", ); assert.equal( JSON.parse( diff --git a/test/responsive-region-fixture.test.mjs b/test/responsive-region-fixture.test.mjs new file mode 100644 index 0000000..ba7ddc6 --- /dev/null +++ b/test/responsive-region-fixture.test.mjs @@ -0,0 +1,145 @@ +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import { join } from "node:path"; +import { fileURLToPath } from "node:url"; + +import { chromium } from "@playwright/test"; + +const root = fileURLToPath(new URL("..", import.meta.url)); +const layoutCss = readFileSync(join(root, "dist", "layout-style-css.css"), "utf8"); + +/** Build a standalone consumer fixture for internal scrolling and positioning contracts. */ +function fixtureHtml() { + return ` + + + + + + + + +
+
Responsive fixture
+
+
A-legitimate-unbroken-identifier-that-must-not-force-document-overflow
+
Automatic item
+
+
+
Manual one
+
Manual two
+
+
+
+
+ + + +
Inventory identifierLocation
component-with-a-long-but-valid-operational-identifierRack 12 / Row 8
+
+
+
+
+ +
+ +
+ + `; +} + +/** Return rendered measurements that distinguish document overflow from usable region overflow. */ +async function measureFixture(page) { + return page.evaluate(() => { + const tracks = (selector) => + getComputedStyle(document.querySelector(selector)).gridTemplateColumns + .split(/\s+/u) + .filter(Boolean).length; + const region = (selector) => { + const element = document.querySelector(selector); + element.scrollLeft = 64; + element.scrollTop = 64; + return { + inlineOverflow: element.scrollWidth - element.clientWidth, + blockOverflow: element.scrollHeight - element.clientHeight, + scrollLeft: element.scrollLeft, + scrollTop: element.scrollTop, + }; + }; + + return { + absolutePosition: getComputedStyle(document.querySelector("#absoluteControl")).position, + autoTracks: tracks("#autoMosaic"), + documentOverflow: + document.documentElement.scrollWidth - document.documentElement.clientWidth, + fixedPosition: getComputedStyle(document.querySelector("#fixedControl")).position, + inner: region("#innerScroll"), + manualTracks: tracks("#manualMosaic"), + outer: region("#outerScroll"), + stickyPosition: getComputedStyle(document.querySelector(".ly-header--sticky")).position, + }; + }); +} + +const browser = await chromium.launch({ headless: true }); + +try { + for (const viewport of [ + { height: 568, label: "320px portrait", width: 320 }, + { height: 844, label: "390px portrait", width: 390 }, + { height: 360, label: "short landscape", width: 800 }, + ]) { + const page = await browser.newPage({ viewport }); + try { + await page.setContent(fixtureHtml()); + const metrics = await measureFixture(page); + + assert(metrics.documentOverflow <= 1, `${viewport.label} overflowed the document.`); + assert.equal(metrics.absolutePosition, "absolute"); + assert.equal(metrics.fixedPosition, "fixed"); + assert.equal(metrics.manualTracks, 2, "Manual grid ownership must remain stable."); + assert(metrics.autoTracks >= 1, "Automatic Mosaic must retain a usable track."); + assert( + metrics.outer.inlineOverflow > 0 && metrics.outer.blockOverflow > 0, + `${viewport.label} outer metrics: ${JSON.stringify(metrics.outer)}`, + ); + assert(metrics.outer.scrollLeft > 0 && metrics.outer.scrollTop > 0); + assert(metrics.inner.inlineOverflow > 0); + assert(metrics.inner.scrollLeft > 0); + assert( + ["static", "sticky"].includes(metrics.stickyPosition), + `${viewport.label} produced an invalid sticky fallback.`, + ); + } finally { + await page.close(); + } + } + + console.log("Responsive internal-region fixture passed in Chromium."); +} finally { + await browser.close(); +}