Skip to content
Open
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
1 change: 1 addition & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ jobs:
node-version: ${{ matrix.node-version }}
check-latest: true
- run: npm i
- run: npx playwright install --with-deps chromium
- name: Run tests
run: |
node_major=$(node -p "process.versions.node.split('.')[0]")
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ package-lock.json
public
coverage
lcov.info
test-results
.tap
.tmp-*

Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1846,6 +1846,8 @@ Key changes at a glance:

- **Default layout**: The bundled and ejected `root.layout.js` now uses [`fragtml`][fragtml].
- **Included dependencies**: `domstack --eject` adds `mine.css`, `fragtml`, and `highlight.js`; Preact, HTM, and `preact-render-to-string` are no longer included for the default template.
- **mine.css v11**: The default styles now use mine.css v11's CSS-only API, browser-controlled color preference, native CSS nesting, and intentional visual changes. Ejected and customized sites must follow the mine.css v11 migration steps.
- **CSS cascade layers**: DOMStack’s default stylesheet establishes the `mine`, `domstack.global`, `domstack.layout`, and `domstack.page` layer order. Following the same layer pattern in custom global, layout, and page stylesheets is recommended for explicit global → layout → page precedence.
- **JSX runtime**: Client `.jsx` and `.tsx` bundles still work through esbuild, but DOMStack no longer configures Preact as the default runtime. Install React, Preact, or another runtime in your own project and configure it with `esbuild.settings`.
- **Preact examples**: Examples that actually mount Preact in the browser still use Preact and configure it locally.

Expand Down
26 changes: 26 additions & 0 deletions browser-tests/cascade-layers.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { expect, test } from './support.js'

async function cascadeValue (page) {
return page.getByTestId('cascade-layer-fixture').evaluate(element => {
return getComputedStyle(element).getPropertyValue('--domstack-cascade-layer').trim()
})
}

test('orders mine, global, layout, and page cascade layers', async ({ page, siteURL }) => {
await page.goto(`${siteURL}/`, { waitUntil: 'domcontentloaded' })
await page.addStyleTag({
content: '@layer mine { .cascade-layer-fixture { --domstack-cascade-layer: mine; } }'
})

await expect(page.getByTestId('cascade-layer-fixture')).toHaveCount(1)
await expect.poll(() => cascadeValue(page)).toBe('page')

await page.locator('link[rel="stylesheet"][href*="style-"]').evaluate(element => element.remove())
await expect.poll(() => cascadeValue(page)).toBe('layout')

await page.locator('link[rel="stylesheet"][href*="root.layout-"]').evaluate(element => element.remove())
await expect.poll(() => cascadeValue(page)).toBe('global')

await page.locator('link[rel="stylesheet"][href*="global-"]').evaluate(element => element.remove())
await expect.poll(() => cascadeValue(page)).toBe('mine')
})
71 changes: 71 additions & 0 deletions browser-tests/support.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { expect, test as base } from '@playwright/test'
import { readFile } from 'node:fs/promises'
import { createServer } from 'node:http'
import { extname, resolve, sep } from 'node:path'
import { testBuild } from '../index.js'

const fixtureSrc = resolve(import.meta.dirname, '../test-cases/general-features/src')
const contentTypes = new Map([
['.css', 'text/css; charset=utf-8'],
['.html', 'text/html; charset=utf-8'],
['.js', 'text/javascript; charset=utf-8'],
['.woff2', 'font/woff2']
])

export const test = base.extend({
siteURL: async ({ context }, use) => {
const build = await testBuild(fixtureSrc)
const publicDir = build.dest
const server = createServer(async (request, response) => {
try {
const url = new URL(request.url ?? '/', 'http://127.0.0.1')
const pathname = decodeURIComponent(url.pathname)
const relativePath = pathname.endsWith('/')
? `${pathname}index.html`
: pathname
const filePath = resolve(publicDir, `.${relativePath}`)

if (filePath !== publicDir && !filePath.startsWith(`${publicDir}${sep}`)) {
response.writeHead(403).end('Forbidden')
return
}

const body = await readFile(filePath)
response.writeHead(200, {
'cache-control': 'no-store',
'content-type': contentTypes.get(extname(filePath)) ?? 'application/octet-stream'
})
response.end(body)
} catch (error) {
const status = error?.code === 'ENOENT' ? 404 : 500
response.writeHead(status).end(status === 404 ? 'Not found' : 'Server error')
}
})

await new Promise((resolve, reject) => {
server.once('error', reject)
server.listen(0, '127.0.0.1', () => {
server.off('error', reject)
resolve()
})
})

const address = server.address()
if (!address || typeof address === 'string') throw new Error('Static server did not bind to a TCP port')

await context.route(/^https?:\/\/(?!127\.0\.0\.1(?::\d+)?(?:\/|$))/, route => route.abort())
await use(`http://127.0.0.1:${address.port}`)

for (const page of context.pages()) {
if (!page.isClosed()) await page.close().catch(() => {})
}

await new Promise((resolve, reject) => {
server.close(error => error ? reject(error) : resolve())
server.closeAllConnections()
})
await build.cleanup()
}
})

export { expect }
102 changes: 100 additions & 2 deletions docs/v12-migration.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ Then apply the v12 changes below.
3. [Default Layout Uses fragtml](#3-default-layout-uses-fragtml)
4. [Keep Layout Dependencies Explicit](#4-keep-layout-dependencies-explicit)
5. [JSX Runtime Is Opt-In](#5-jsx-runtime-is-opt-in)
6. [Migration Checklist](#6-migration-checklist)
6. [mine.css v11 and CSS Cascade Layers](#6-minecss-v11-and-css-cascade-layers)
7. [Migration Checklist](#7-migration-checklist)

---

Expand Down Expand Up @@ -123,7 +124,96 @@ export default async function esbuildSettingsOverride (esbuildSettings) {

---

## 6. Migration Checklist
## 6. mine.css v11 and CSS Cascade Layers

DOMStack v12 upgrades its bundled default styles and ejected projects from mine.css v10 to v11.
This is a downstream breaking change even when your project does not import mine.css directly, because the bundled default layout uses it.
Review the complete [mine.css v11 migration guide](https://github.com/bcomnes/mine.css/blob/master/MIGRATION.md) before upgrading a customized or ejected site.

mine.css v11 is CSS-only.
The package root now resolves to the main stylesheet, and the JavaScript theme switcher is no longer published.
Replace the old deep import with the package root:

```css
@import 'mine.css';
```

Remove imports of `mine.css` or `mine.css/dist/theme-switcher.js` from JavaScript, calls to `toggleTheme()`, stored theme state, theme controls, and `.light-mode` or `.dark-mode` rules.
Add `<meta name="color-scheme" content="light dark">` to custom root layouts and use `prefers-color-scheme` for application-specific dark styles.
If you use Highlight.js, load a light theme normally and a dark theme conditionally instead of applying one dark theme in both modes.

The optional mine.css layout remains a separate import.
DOMStack's default stylesheet imports it explicitly; ejected sites must continue to do the same.

### DOMStack cascade layer order

DOMStack v12's default stylesheet establishes this low-to-high-priority author-layer order:

```css
@layer mine, domstack.global, domstack.layout, domstack.page;
```

The order reflects DOMStack's existing stylesheet scopes:

- `mine` contains mine.css framework defaults.
- `domstack.global` contains site-wide rules and syntax themes.
- `domstack.layout` contains optional mine.css layout rules and `*.layout.css` rules.
- `domstack.page` contains page-local `style.css` rules.

Declare the complete order before imports or other layer blocks in the global stylesheet.
Import mine.css normally because its distributed stylesheet already defines the `mine` layer.
Do not write `@import 'mine.css' layer(mine)`.
Optional sidecars and syntax themes are not pre-layered, so assign them to the appropriate DOMStack layer:

```css
@layer mine, domstack.global, domstack.layout, domstack.page;

@import 'mine.css';
@import 'mine.css/dist/layout.css' layer(domstack.layout);
@import 'highlight.js/styles/github.css' layer(domstack.global);
@import url('highlight.js/styles/github-dark-dimmed.css') layer(domstack.global) (prefers-color-scheme: dark);

@layer domstack.global {
:root {
--brand-color: rebeccapurple;
}
}
```

Following this pattern in custom stylesheets is recommended.
Scope layout and page rules in their matching files:

```css
/* article.layout.css */
@layer domstack.layout {
.article-shell {
max-inline-size: 72rem;
}
}
```

```css
/* style.css */
@layer domstack.page {
.article-introduction {
font-size: 1.125em;
}
}
```

DOMStack loads the global stylesheet first, so its order declaration establishes precedence before layout and page layer blocks are encountered.
Unlayered author rules outrank every layered normal rule, so existing unlayered overrides will still win but bypass the DOMStack scope contract.
Move them into the matching layer when you want predictable global → layout → page precedence.
CSS Modules may remain unlayered when component-local precedence is intentional, or be wrapped in the appropriate scope layer when they participate in this cascade.

mine.css v11 also intentionally changes typography, forms, tables, media framing, motion, focus treatment, and the optional `.mine-layout` width.
It preserves native table display; wrap wide tables in an accessible, named, keyboard-focusable overflow region rather than restoring `display: block` on the table.
The distributed CSS uses native CSS nesting, and the package requires Node.js 22 or newer and npm 10 or newer for installation.
Visually verify representative pages in light and dark browser modes, narrow and wide viewports, reduced-motion mode, and print preview where relevant.

---

## 7. Migration Checklist

- [ ] If you import public types from `@domstack/static`, update those imports to `@domstack/static/types.js`.
- [ ] If you rely on BrowserSync-specific dev-server behavior, test watch mode with `@domstack/sync`.
Expand All @@ -133,3 +223,11 @@ export default async function esbuildSettingsOverride (esbuildSettings) {
- [ ] If you want your ejected server-side layout to match the v12 default, migrate its templates to `fragtml` and install `fragtml`.
- [ ] If you use `.jsx` or `.tsx` browser clients, add an `esbuild.settings` file that configures your JSX runtime.
- [ ] If you use Preact browser clients, keep `preact` in your project dependencies.
- [ ] Read the mine.css v11 migration guide and account for its intentional visual and browser-support changes.
- [ ] Replace `@import 'mine.css/dist/mine.css'` with `@import 'mine.css'` and keep optional sidecars explicit.
- [ ] Remove `toggleTheme()`, theme-switcher imports, persisted theme state, theme controls, and light/dark mode classes.
- [ ] Add `<meta name="color-scheme" content="light dark">` to custom root layouts and use `prefers-color-scheme` for dark styles.
- [ ] Declare `@layer mine, domstack.global, domstack.layout, domstack.page;` and scope global, layout, and page rules in their matching layers.
- [ ] Load light and dark syntax-highlighting themes with matching `prefers-color-scheme` behavior.
- [ ] Confirm the deployment and install environment uses Node.js 22+ and npm 10+ and that target browsers support native CSS nesting.
- [ ] Visually and interactively check both color schemes, keyboard focus, reduced motion, forms, wide tables, media, print, and responsive layouts.
2 changes: 1 addition & 1 deletion examples/basic/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
"dependencies": {
"@domstack/static": "file:../../.",
"fragtml": "^0.0.9",
"mine.css": "^9.0.1",
"mine.css": "^11.0.6",
"highlight.js": "^11.9.0"
}
}
11 changes: 1 addition & 10 deletions examples/basic/src/global.client.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,6 @@
// @ts-expect-error
import { toggleTheme } from 'mine.css'

declare global {
interface Window {
toggleTheme: typeof toggleTheme;
}
}

window.toggleTheme = toggleTheme

console.log('The global client is loaded on every page.')

// Try to keep this file as small as possible.
// Use if for things like global theme switchers or analytics scripts
// Use it for things like analytics scripts or other site-wide behavior
21 changes: 13 additions & 8 deletions examples/basic/src/global.css
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
@import 'mine.css/dist/mine.css';
@import 'mine.css/dist/layout.css';
@import 'highlight.js/styles/github-dark-dimmed.css';
@layer mine, domstack.global, domstack.layout, domstack.page;

/* The global.css in the root directory of the site is loaded on every page. */
@import 'mine.css';
@import 'mine.css/dist/layout.css' layer(domstack.layout);
@import 'highlight.js/styles/github.css' layer(domstack.global);
@import url('highlight.js/styles/github-dark-dimmed.css') layer(domstack.global) (prefers-color-scheme: dark);

/* You can import styles out of node_modules by referencing their bare-name */
/* You can import styles from a relative path as well, though usually that is
only used in page scpped style.css files */
/* See https://github.com/postcss/postcss-import for details */
@layer domstack.global {
/* The global.css in the root directory of the site is loaded on every page. */

/* You can import styles out of node_modules by referencing their bare-name */
/* You can import styles from a relative path as well, though usually that is
only used in page scpped style.css files */
/* See https://github.com/postcss/postcss-import for details */
}
8 changes: 5 additions & 3 deletions examples/basic/src/html-page/style.css
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
/* html pages can also have their own style bundles */
@layer domstack.page {
/* html pages can also have their own style bundles */

.html-page-class {
background-color: red;
.html-page-class {
background-color: red;
}
}
3 changes: 1 addition & 2 deletions examples/basic/src/js-page/loose-assets/style.css
Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
@import './local-import.css';

@import './local-import.css' layer(domstack.page);
Loading