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