Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/npm-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ on:
workflow_dispatch:
inputs:
release_tag:
description: "Release tag to publish, for example v3.0.2"
description: "Release tag to publish, for example v3.1.0"
required: true
type: string

Expand Down
20 changes: 20 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,26 @@

All notable changes to `layout-style-css` are documented here. This project follows semantic versioning.

## [3.1.0] - 2026-08-25

### Added

- Added compact, normal, and spacious density contexts that can be applied at the root or to a nested layout subtree.
- Added the `96rem` workspace Wrapper, all ten local gap utilities, explicit bounded and viewport-relative Scroll modifiers, and recipe-owned threshold metadata.

### Changed

- Made normal section spacing more conservative while retaining the former marketing rhythm through spacious density.
- Made gap utilities local to the element carrying the class and made base Scroll rely on its containing layout for height constraints.

### Migration

- Use spacious density for the former section rhythm, explicit inherited gap tokens for a shared spacing context, and `ly-scroll--viewport` for the former viewport-relative Scroll behavior.

### Tests

- Added density, workspace utilization, nested-gap isolation, Scroll-mode, public-token liveness, threshold metadata, cross-height, generated-output, package, and Pages contracts.

## [3.0.2] - 2026-08-25

### Fixed
Expand Down
33 changes: 25 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.0.2` is the current v3 patch release and preserves the clean-break structural contract introduced in 3.0.0. It provides intrinsic wrappers, composition primitives, seven semantic recipes, and sixteen spatial personalities. Layout owns structure; UI Style Kit owns paint; Interactive Surface owns interaction styling.
`layout-style-css@3.1.0` is the current additive v3 release. It provides intrinsic wrappers, composition primitives, seven semantic recipes, explicit local density controls, and sixteen spatial personalities. Layout owns structure; UI Style Kit owns paint; Interactive Surface owns interaction styling.

## Requirements

Expand All @@ -13,7 +13,7 @@ Zero-configuration, dependency-free structural CSS that responds to the width an
## Install

```bash
npm install layout-style-css@3.0.2
npm install layout-style-css@3.1.0
```

Most applications need one import:
Expand Down Expand Up @@ -44,6 +44,17 @@ Use `.ly-root` as the responsive boundary and keep the mobile DOM order authorit

`.ly-root`, wrappers, and recipe roots establish the named `ly-scope` inline-size container. A recipe works directly in the root; when it is nested in a wrapper, it responds to that nearest wrapper.

## Density Contexts

Normal density is the zero-configuration default. Use `data-ly-density="compact"`, `data-ly-density="normal"`, or `data-ly-density="spacious"` on `.ly-root` or any nested layout subtree. A nested density explicitly resets inherited gaps and section rhythm, so task controls can remain compact inside a spacious marketing page.

```html
<main class="ly-root" data-ly-density="normal">
<section data-ly-density="compact">Dense application controls</section>
<section data-ly-density="spacious">Campaign presentation</section>
</main>
```

## Wrappers

Wrappers are optional measure and nesting controls. Every wrapper uses logical properties, fluid container-relative gutters, safe-area compensation, and shrink-safe sizing.
Expand All @@ -54,6 +65,7 @@ Wrappers are optional measure and nesting controls. Every wrapper uses logical p
| `.ly-wrapper--compact` | `40rem` compact measure |
| `.ly-wrapper--prose` | `68ch` reading measure |
| `.ly-wrapper--content` | `72rem` content measure |
| `.ly-wrapper--workspace` | `96rem` task-oriented application measure |
| `.ly-wrapper--wide` | `112rem` wide measure |
| `.ly-wrapper--full` | Full available inline size |
| `.ly-wrapper--breakout` | Clamped content, feature, and full lanes |
Expand All @@ -79,7 +91,9 @@ The core includes:
- `.ly-panes` and `.ly-media`
- `.ly-reel`, `.ly-frame`, and `.ly-scroll`

Grid and flex primitives wrap intrinsically whenever track wrapping can replace a query. Page, cover, and bounded-scroll sizing use `vh` fallbacks followed by `100dvh`-aware behavior. Only `.ly-reel` deliberately scrolls horizontally; only `.ly-scroll` deliberately creates a bounded vertical scroll region.
Grid and flex primitives wrap intrinsically whenever track wrapping can replace a query. Base `.ly-scroll` owns overflow but no height cap. Add `.ly-scroll--bounded` for the stable `--ly-scroll-max` cap or `.ly-scroll--viewport` for height-aware `--ly-scroll-viewport-max` behavior. Only `.ly-reel` deliberately scrolls horizontally; only the Scroll family deliberately creates vertical scrolling.

Gap utilities `.ly-gap-0` through `.ly-gap-9` assign `gap` only on the element carrying the class. They do not alter inherited primitive gap variables. Override `--ly-gap`, `--ly-grid-gap`, `--ly-stack-gap`, or `--ly-cluster-gap` when a shared spacing context is intentional.

## Automatic Recipe Engine

Expand All @@ -95,6 +109,8 @@ Recipes use attributes only. The stacked semantic source order is always safe, a
| `data-ly-recipe="gallery"` | Intrinsic tracks; no topology breakpoint |
| `data-ly-recipe="card-grid"` | Intrinsic tracks; no topology breakpoint |

The same ownership is available in `manifest.json` through `thresholds.recipes`, alongside the generic container-width and viewport-height arrays. Release checks verify every mapping against authored queries and require every public geometry token to reach a runtime CSS declaration.

App Shell base, medium, and wide topologies own matching row definitions. This keeps the primary workspace flexible while header and footer tracks remain intrinsic, including personality-specific four-row topologies.

Canonical regions use `data-ly-area`, including `header`, `sidebar`, `nav`, `main`, `aside`, `footer`, `content`, `media`, `actions`, `primary`, and `secondary`.
Expand Down Expand Up @@ -162,7 +178,7 @@ Each personality is a token/topology profile consumed by the shared recipe engin
| `layout-style-css/personalities.json` | Public layout-to-visual pairing recommendations |
| `layout-style-css/package.json` | Package metadata |

The cascade order is `ly.reset`, `ly.tokens`, `ly.wrappers`, `ly.primitives`, `ly.recipes`, `ly.utilities`, and `ly.personalities`.
The cascade order is `ly.reset`, `ly.tokens`, `ly.wrappers`, `ly.primitives`, `ly.recipes`, `ly.utilities`, `ly.personalities`, and `ly.context`. Unlayered application CSS remains stronger than every library layer.

## Ecosystem Imports

Expand All @@ -177,18 +193,18 @@ import "layout-style-css";

This order lets UI Style Kit establish paint and theme roles, Interactive Surface add interaction states, and Layout apply structure.

`data-ly-layout`, `data-ui`, `data-theme`, and `data-mode` are independently selectable. See [Layout Styles](docs/wiki/Layout-Styles.md#visual-pairing-guidance) for the full recommendation matrix; pairings are never dependencies.
`data-ly-layout`, `data-ly-density`, `data-ui`, `data-theme`, and `data-mode` are independently selectable. See [Layout Styles](docs/wiki/Layout-Styles.md#visual-pairing-guidance) for the full recommendation matrix; pairings are never dependencies.

## CDN

```html
<link rel="stylesheet" href="https://unpkg.com/layout-style-css@3.0.2/dist/layout-style-css.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/layout-style-css@3.0.2/dist/layout-style-css.min.css">
<link rel="stylesheet" href="https://unpkg.com/layout-style-css@3.1.0/dist/layout-style-css.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/layout-style-css@3.1.0/dist/layout-style-css.min.css">
```

## Clean-Break Migration

v3 has no compatibility bundle. It removes `legacy.css`, the deprecated UI Style Kit structural bridge, v1/v2 aliases, responsive `ly-md-*` and `ly-lg-*` utilities, and all visual-order utilities. See [Migrating To 3.0](docs/wiki/Migrating-To-3.0.md) for exact mappings.
v3 has no compatibility bundle. It removes `legacy.css`, the deprecated UI Style Kit structural bridge, v1/v2 aliases, responsive `ly-md-*` and `ly-lg-*` utilities, and all visual-order utilities. See [Migrating To 3.0](docs/wiki/Migrating-To-3.0.md) for the major-version mappings and [Migrating To 3.1](docs/wiki/Migrating-To-3.1.md) for density, local gap, and Scroll behavior changes.

## Demo And Verification

Expand All @@ -214,6 +230,7 @@ npm run release:verify
- [Layout Recipes](docs/wiki/Layout-Recipes.md)
- [Layout Styles](docs/wiki/Layout-Styles.md)
- [Migrating To 3.0](docs/wiki/Migrating-To-3.0.md)
- [Migrating To 3.1](docs/wiki/Migrating-To-3.1.md)
- [Demo And GitHub Pages](docs/wiki/Demo-And-GitHub-Pages.md)
- [Release And Publishing](docs/wiki/Release-And-Publishing.md)
- [Security And Support](docs/wiki/Security-And-Support.md)
Expand Down
8 changes: 4 additions & 4 deletions demo/demo.css
Original file line number Diff line number Diff line change
Expand Up @@ -45,12 +45,12 @@ body[data-mode="contrast"] {
color-scheme: dark;
}

body[data-density="compact"] {
body[data-demo-density="compact"] {
--demo-density-gap: 0.5rem;
--demo-region-padding: 0.75rem;
}

body[data-density="spacious"] {
body[data-demo-density="spacious"] {
--demo-density-gap: 1.5rem;
--demo-region-padding: 1.5rem;
}
Expand Down Expand Up @@ -367,15 +367,15 @@ body[data-ecosystem="all-three"] .demo-status {
--ly-grid-gap: var(--ly-gap);
--ly-section-padding-block: clamp(1.5rem, 6%, 3rem);
--ly-header-height: 3.5rem;
--ly-scroll-max: min(62%, 34rem);
--ly-scroll-viewport-max: min(62%, 34rem);
}

.demo-preview-root[data-demo-height-tier="shallow"] {
--ly-cover-min: auto;
--ly-shell-min: auto;
--ly-sticky-position: static;
--ly-section-padding-block: 1.5rem;
--ly-scroll-max: max(12rem, calc(100% - 8rem));
--ly-scroll-viewport-max: max(12rem, calc(100% - 8rem));
}

.demo-preview-root[data-demo-height-tier="shallow"] .demo-recipe {
Expand Down
62 changes: 47 additions & 15 deletions demo/demo.js
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,16 @@ const ALLOWLISTS = Object.freeze({
"desktop-landscape",
"desktop-portrait"
]),
wrapper: Object.freeze(["default", "compact", "prose", "content", "wide", "full", "breakout"]),
wrapper: Object.freeze([
"default",
"compact",
"prose",
"content",
"workspace",
"wide",
"full",
"breakout"
]),
recipe: Object.freeze([
"app-shell",
"dashboard",
Expand Down Expand Up @@ -190,7 +199,7 @@ const ALLOWLISTS = Object.freeze({
]),
height: Object.freeze(["auto", "29rem", "31rem", "43rem", "45rem", "50rem"]),
responsive: Object.freeze(["auto", "manual"]),
density: Object.freeze(["compact", "comfortable", "spacious"]),
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),
Expand All @@ -205,7 +214,7 @@ const DEFAULT_STATE = Object.freeze({
container: "auto",
height: "auto",
responsive: "auto",
density: "comfortable",
density: "normal",
ui: "minimal-saas",
theme: "arctic-indigo",
mode: "light",
Expand Down Expand Up @@ -248,12 +257,6 @@ const DEVICE_PRESETS = Object.freeze({
"desktop-portrait": Object.freeze({ width: 900, height: 1440 })
});

const DENSITY_GAPS = Object.freeze({
compact: "0.5rem",
comfortable: "1rem",
spacious: "1.5rem"
});

const UI_CLASS_PREFIXES = Object.freeze(
Object.fromEntries(UI_STYLE_KIT_MANIFEST.presets.map((preset) => [preset.id, preset.prefix]))
);
Expand Down Expand Up @@ -326,12 +329,28 @@ let querySyncTimer = null;
let hasSynchronizedQuery = false;
let readoutFrame = null;

/**
* Maps durable demo query aliases to current allowlisted values.
*
* @param {string} key Query-state key.
* @param {string | null} value Raw query value.
* @returns {string | null} Current value or the unchanged input.
*/
function normalizeQueryValue(key, value) {
return key === "density" && value === "comfortable" ? "normal" : value;
}

/**
* Reads allowlisted demo state from the current query string.
*
* @returns {Record<string, string>} Normalized demo state.
*/
function readStateFromQuery() {
const query = new URLSearchParams(window.location.search);

return Object.fromEntries(
Object.entries(ALLOWLISTS).map(([key, values]) => {
const candidate = query.get(key);
const candidate = normalizeQueryValue(key, query.get(key));
return [key, candidate !== null && values.includes(candidate) ? candidate : DEFAULT_STATE[key]];
})
);
Expand Down Expand Up @@ -392,6 +411,12 @@ function createAction(sequence) {
});
}

/**
* Creates one semantic recipe region and its representative preview content.
*
* @param {string} area Canonical recipe area name.
* @returns {HTMLElement} Populated preview region.
*/
function createRegion(area) {
const semanticTags = {
header: "header",
Expand Down Expand Up @@ -420,7 +445,7 @@ function createRegion(area) {

if (area === "secondary") {
const scroll = createElement("div", {
className: "ly-scroll demo-list-scroll",
className: "ly-scroll ly-scroll--bounded demo-list-scroll",
attributes: { tabindex: "0", "aria-label": "Scrollable detail activity" }
});

Expand Down Expand Up @@ -657,7 +682,15 @@ function updatePreviewReadout() {
});
}

function applyState({ updateQuery = true } = {}) {
/**
* Applies current demo state to controls, public layout attributes, preview
* allocation, rendered recipe content, snippets, and URL state.
*
* @param {{updateQuery?: boolean}} [options] State-application options.
* @returns {void}
*/
function applyState(options = {}) {
const { updateQuery = true } = options;
for (const [key, control] of Object.entries(controls)) {
control.value = state[key];
}
Expand All @@ -666,11 +699,10 @@ function applyState({ updateQuery = true } = {}) {
body.dataset.theme = state.theme;
body.dataset.mode = state.mode;
body.dataset.lyLayout = state.personality;
body.dataset.density = state.density;
body.dataset.demoDensity = state.density;
body.dataset.ecosystem = state.ecosystem;
previewRoot.dataset.lyLayout = state.personality;
previewRoot.style.setProperty("--ly-gap", DENSITY_GAPS[state.density]);
previewRoot.style.setProperty("--ly-grid-gap", DENSITY_GAPS[state.density]);
previewRoot.dataset.lyDensity = state.density;

previewWrapper.className =
state.wrapper === "default" ? "ly-wrapper" : `ly-wrapper ly-wrapper--${state.wrapper}`;
Expand Down
19 changes: 10 additions & 9 deletions demo/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<meta http-equiv="content-language" content="en-US">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<meta name="title" content="Layout Style CSS v3 Intrinsic Responsive Demo">
<meta name="version" content="3.0.2">
<meta name="version" content="3.1.0">
<meta name="description" content="Explore zero-configuration intrinsic wrappers, responsive recipes, vertical adaptations, and sixteen layout personalities in Layout Style CSS v3.">
<meta name="keywords" content="CSS layout library, container queries, layout recipes, UI Style Kit CSS, Interactive Surface CSS, responsive CSS">
<meta name="author" content="Foscat">
Expand Down Expand Up @@ -71,15 +71,15 @@
<link id="uiKitStylesheet" rel="stylesheet" href="https://unpkg.com/ui-style-kit-css@2.1.0/dist/ui-style-kit.visual.min.css" integrity="sha384-DUhB5MkNLJffjy7NRg13prdEcPTUR6zdPtameCyfq3tId/cM8o3ojpyvU/xdxZ8t" crossorigin="anonymous">
<link id="uiKitInteractiveThemeStylesheet" rel="stylesheet" href="https://unpkg.com/ui-style-kit-css@2.1.0/styles/interactive-surface-theme.css" integrity="sha384-t7ZuirMM/Qoeoz+/5+S4JxIGXs076pqlgMxv6FymZW3tGj7snT8J2ZlV2+sH6++j" crossorigin="anonymous">
<link id="interactiveSurfaceStylesheet" rel="stylesheet" href="https://unpkg.com/interactive-surface-css@1.5.0/state-core.css" integrity="sha384-v2NwNOzOcQ5ed7W7OHahaKHXspqfD0p8XO+kueqrsyfEcuBO2izsAQb/J/PxF0eX" crossorigin="anonymous">
<link id="layoutCoreStylesheet" rel="stylesheet" href="../dist/layout-style-css.css?v=3.0.2">
<link rel="stylesheet" href="./demo.css?v=3.0.2">
<link id="layoutCoreStylesheet" rel="stylesheet" href="../dist/layout-style-css.css?v=3.1.0">
<link rel="stylesheet" href="./demo.css?v=3.1.0">

<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "SoftwareSourceCode",
"name": "layout-style-css",
"version": "3.0.2",
"version": "3.1.0",
"description": "A dependency-free CSS layout library for intrinsic wrappers, composition primitives, width and height responsive recipes, and spatial personalities.",
"programmingLanguage": "CSS",
"applicationCategory": "DeveloperApplication",
Expand All @@ -91,10 +91,10 @@
"publisher": { "@type": "Organization", "name": "Foscat" }
}
</script>
<script src="./personality-metadata.js?v=3.0.2"></script>
<script type="module" src="./demo.js?v=3.0.2" data-personalities-url="../personalities.json?v=3.0.2"></script>
<script src="./personality-metadata.js?v=3.1.0"></script>
<script type="module" src="./demo.js?v=3.1.0" data-personalities-url="../personalities.json?v=3.1.0"></script>
</head>
<body class="ly-root" data-ui="minimal-saas" data-theme="arctic-indigo" data-mode="light" data-ly-layout="minimal-saas" data-density="comfortable" data-ecosystem="all-three">
<body class="ly-root" data-ui="minimal-saas" data-theme="arctic-indigo" data-mode="light" data-ly-layout="minimal-saas" data-demo-density="normal" data-ecosystem="all-three">
<a class="demo-skip-link" href="#layoutLab">Skip to the layout lab</a>

<header class="demo-topbar">
Expand Down Expand Up @@ -143,6 +143,7 @@ <h2 id="controlsTitle">Layout controls</h2>
<option value="compact">Compact · 40rem</option>
<option value="prose">Prose · 68ch</option>
<option value="content">Content · 72rem</option>
<option value="workspace">Workspace · 96rem</option>
<option value="wide">Wide · 112rem</option>
<option value="full">Full width</option>
<option value="breakout">Breakout lanes</option>
Expand Down Expand Up @@ -208,7 +209,7 @@ <h2 id="controlsTitle">Layout controls</h2>
<label for="densitySelect">Density
<select id="densitySelect" data-query-key="density" data-ui-kit="select">
<option value="compact">Compact</option>
<option value="comfortable">Comfortable</option>
<option value="normal">Normal</option>
<option value="spacious">Spacious</option>
</select>
</label>
Expand Down Expand Up @@ -292,7 +293,7 @@ <h2 id="stageTitle">Responsive recipe stage</h2>
</div>

<div id="previewFrame" class="demo-preview-frame" data-container-width="auto" data-container-height="auto">
<div id="previewRoot" class="ly-root demo-preview-root" data-ly-layout="minimal-saas" data-demo-height-tier="regular">
<div id="previewRoot" class="ly-root demo-preview-root" data-ly-layout="minimal-saas" data-ly-density="normal" data-demo-height-tier="regular">
<div id="previewWrapper" class="ly-wrapper">
<div id="recipePreview" class="demo-recipe" data-ly-recipe="app-shell">
<header class="ly-header ly-header--sticky demo-region" data-ly-area="header" data-demo-sequence="header">
Expand Down
Loading
Loading