Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
53475ad
Settings become the default theme (#65)
minimaldesign Aug 14, 2026
8eb7fce
Wireframe becomes a composed entry; add theme.starter.css
minimaldesign Aug 14, 2026
51f0b17
Evict --gradient-primary to its one consumer
minimaldesign Aug 14, 2026
ba27947
Dist: bake the default theme into the core bundle
minimaldesign Aug 14, 2026
a3de328
Docs: the default theme replaces settings, interface tokens become ui…
minimaldesign Aug 14, 2026
801315e
Theme entries pin the layer order
minimaldesign Aug 15, 2026
8c8e73c
Default theme parts are layered by their entry, not self-layered
minimaldesign Aug 15, 2026
b9196f8
Rename theme sublayers: theme.default and theme.user
minimaldesign Aug 15, 2026
d249c68
Collapse the feedback alias tier
minimaldesign Aug 15, 2026
82434ce
Wireframe: clarify that the file holds only deviations
minimaldesign Aug 15, 2026
d9ff2a9
Major "Getting started" doc rewrite + small tweaks
minimaldesign Aug 16, 2026
159a802
more copy editing for the Getting Started page
minimaldesign Aug 16, 2026
8cb14e8
make it clearer for LLMs that ITCSS methodology is important in mCSS
minimaldesign Aug 16, 2026
257e82a
Add an "external" layer to make npm packages, plugins, etc. CSS easie…
minimaldesign Aug 16, 2026
02378fa
make sure all instances of layer stack in the code in consistent
minimaldesign Aug 16, 2026
82fad3e
Enhance CI checks for CSS layers and bundles
minimaldesign Aug 16, 2026
b9706ed
Major "Getting started" rewrite.
minimaldesign Aug 16, 2026
d3cb4fc
Getting started fixes
minimaldesign Aug 16, 2026
341d52d
skin → theme
minimaldesign Aug 16, 2026
7f9021c
restructuring of the docs
minimaldesign Aug 16, 2026
999492a
Template: activate the default theme and component library
minimaldesign Aug 16, 2026
1b777f5
more docs tightening
minimaldesign Aug 17, 2026
c043712
Simplify .claude launch configs
minimaldesign Aug 17, 2026
215fa97
Theme docs rewrite
minimaldesign Aug 17, 2026
798fc81
change headings for raw tokens in theme docs
minimaldesign Aug 17, 2026
04df276
Fix - Adding H4's to the TOC in the docs - not components
minimaldesign Aug 17, 2026
8bc934f
Default Theme docs final sweep
minimaldesign Aug 17, 2026
2d10540
Layout docs updates
minimaldesign Aug 17, 2026
0d8749c
Layout page docs update
minimaldesign Aug 17, 2026
20fea35
Layout page final sweep
minimaldesign Aug 17, 2026
4e3155c
Helpers docs final sweep
minimaldesign Aug 17, 2026
26891e3
installation docs final sweep
minimaldesign Aug 17, 2026
096b683
browser support docs final sweep
minimaldesign Aug 17, 2026
e73a5d5
Offset anchor scrolling for sticky header
minimaldesign Aug 17, 2026
6575e2d
restoring the Component docs page
minimaldesign Aug 17, 2026
5b0a82f
Component docs final sweep
minimaldesign Aug 17, 2026
6f5e7d0
Docs final sweep
minimaldesign Aug 17, 2026
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
35 changes: 13 additions & 22 deletions .claude/launch.json
Original file line number Diff line number Diff line change
@@ -1,35 +1,26 @@
{
"version": "0.0.1",
"configurations": [
{
"name": "preview",
"runtimeExecutable": "npm",
"runtimeArgs": ["run", "preview"],
"port": 4321
},
{
"name": "preview-4331",
"runtimeExecutable": "npm",
"runtimeArgs": ["run", "preview", "--", "--port", "4331"],
"port": 4331
},
{
"name": "dev",
"runtimeExecutable": "npm",
"runtimeArgs": ["run", "dev"],
"runtimeArgs": [
"run",
"dev"
],
"port": 4321
},
{
"name": "dev-4330",
"runtimeExecutable": "npm",
"runtimeArgs": ["run", "dev", "--", "--port", "4330"],
"port": 4330
},
{
"name": "dev-4340",
"name": "preview",
"runtimeExecutable": "npm",
"runtimeArgs": ["run", "dev", "--", "--port", "4340"],
"port": 4340
"runtimeArgs": [
"run",
"preview",
"--",
"--port",
"4400"
],
"port": 4400
}
]
}
4 changes: 4 additions & 0 deletions .githooks/pre-commit
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,7 @@ if git diff --cached --name-only | grep -qE '^(src/styles/framework/|src/tools/b
npm run --silent build:css || exit 1
git add dist
fi

# Every copy of the cascade-layer statement (theme file pins, docs code
# blocks, dist) must agree with the canonical one in framework/mcss.css.
npm run --silent check:layers || exit 1
33 changes: 20 additions & 13 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,23 +36,30 @@ jobs:
git diff --exit-code dist/ || { echo '::error::dist/ is stale — run `npm run build:css` and commit the result'; exit 1; }
test -z "$(git status --porcelain dist/)" || { echo '::error::dist/ has uncommitted files — run `npm run build:css` and commit the result'; exit 1; }

- name: Layer statements agree everywhere
run: npm run check:layers

- name: Verify dist invariants
run: |
set -e
# layers survived the build (the preset-env polyfill must stay off)
grep -q '@layer settings, base, elements, global, components, theme, helpers;' dist/mcss.css
# no cascade-layers or light-dark polyfill artifacts (the layer
# statement itself is covered by check:layers above)
! grep -q ':not(#' dist/mcss.css
# the bundle ships zero-opinion: no theme or component content in
# mcss.css ('@layer theme'/'@layer components' appear in comments;
# check real block syntax)
! grep -q '@layer theme {' dist/mcss.css
! grep -q '@layer components {' dist/mcss.css
# the component library lives in its own bundle
grep -q '@layer components {' dist/mcss.components.css
! grep -q '@layer theme {' dist/mcss.components.css
# per-file themes self-layer so any import method lands them right
grep -q '@layer theme {' dist/css/theme.wireframe.css
grep -q '@layer theme {' dist/css/theme.default.css
! grep -q 'csstools' dist/mcss.css
! grep -q 'csstools' dist/mcss.components.css
# the core bundle bakes in the default theme (it must paint alone)
# but ships no components and no user theme
grep -qE '@layer theme\.default ?\{' dist/mcss.css
! grep -qE '@layer theme\.user ?\{' dist/mcss.css
! grep -qE '@layer components ?\{' dist/mcss.css
# the component library lives in its own bundle, with no theme
grep -qE '@layer components ?\{' dist/mcss.components.css
! grep -qE '@layer theme\.(default|user) ?\{' dist/mcss.components.css
# standalone themes self-layer into theme.user, and the default
# theme's entry layers its parts, so any import method lands right
grep -qE '@layer theme\.user ?\{' dist/css/theme.wireframe.css
grep -qE '@layer theme\.user ?\{' dist/css/theme.starter.css
grep -qF 'layer(theme.default)' dist/css/theme.default.css
# no unresolved custom media or mixins shipped
# (plain '@custom-media' appears in a comment; check real syntax)
! grep -qE '@custom-media --|media \(--' dist/mcss.css
Expand Down
12 changes: 6 additions & 6 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@ mCSS is a CSS framework (ITCSS-based) paired with an Astro documentation site, c

## Commands

| Command | Purpose |
| ------------------- | -------------------------------- |
| `npm run dev` | Start Astro dev server |
| `npm run build` | Production static build |
| `npm run preview` | Preview the production build |
| Command | Purpose |
| ----------------- | ---------------------------- |
| `npm run dev` | Start Astro dev server |
| `npm run build` | Production static build |
| `npm run preview` | Preview the production build |

## Architecture overview

Expand All @@ -25,7 +25,7 @@ mCSS is a CSS framework (ITCSS-based) paired with an Astro documentation site, c

## CSS cascade layers are load-bearing

The framework uses native `@layer`; the layer name (declared in `src/styles/framework/mcss.css`) decides priority, and unlayered site CSS beats every layer. Import new framework files with `layer(<name>)`, except `theme.*.css` files: those wrap themselves in `@layer theme` and are activated from the consumer entry (`_global.css`), never imported by `mcss.css`. Never re-enable preset-env's `cascade-layers` polyfill in `postcss.config.cjs`. Always consult [agents/css.md](agents/css.md) before adding or moving CSS files.
The framework uses native `@layer`; the layer name (declared in `src/styles/framework/mcss.css`) decides priority, and unlayered site CSS beats every layer. Import new framework files with `layer(<name>)`, except standalone `theme.*.css` files (starter, skins): those self-layer via `@layer theme.user` and are activated from the consumer entry (`_global.css`), never imported by `mcss.css`. The default theme's parts are plain CSS layered by their entry (`theme.default.css`, with `layer(theme.default)`). Every design value lives in the default theme (`theme.default.css`); the framework does not paint without it. Never re-enable preset-env's `cascade-layers` polyfill in `postcss.config.cjs`. Always consult [agents/css.md](agents/css.md) before adding or moving CSS files.

## Detailed reference docs

Expand Down
2 changes: 1 addition & 1 deletion agents/components.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
4. **Rest spread**: `{...rest}` on the root element so consumers can set any native attribute.
5. **`data-testid`**: accepted as a prop with a sensible per-component default.
6. **States vs modifiers**: runtime states are `.is-*` classes (`.is-active`, `.is-online`); build-time variants are block modifiers (`.card-filled`). Prefer styling ARIA attributes when one exists (`[aria-current]`, `[aria-disabled]`).
7. **Interface tokens**: every themable knob has a declared default in `settings.ui.css`; local-only custom properties use descriptive names (`--avatar-size`, not `--w`).
7. **UI tokens**: every themable knob has a declared default in `theme.default.ui.css`; local-only custom properties use descriptive names (`--avatar-size`, not `--w`).
8. **Slots**: default slot for main content; named slots documented on the component's docs page.
9. **A11y**: keyboard operable, labelled, JS motion gated on `prefers-reduced-motion`, no live-region roles on static content.

Expand Down
36 changes: 19 additions & 17 deletions agents/css.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,25 +9,27 @@ mCSS uses **native CSS cascade layers** (`@layer`) on top of an ITCSS-inspired f
## Cascade rules (load-bearing)

1. Framework layers, in priority order (later wins):
`settings, base, elements, global, components, theme, helpers`
The order runs **from default to deliberate**: `settings` through `components` are the framework's defaults, `theme` is a deliberate override of those defaults, helpers are the last word. (Not "generic to specific": a theme is broad in reach but late in intent, and intent decides cascade order.)
`base, elements, global, components, theme.default, theme.user, helpers`
The order runs **from default to deliberate**: `base` through `components` are the framework's structure, `theme.default` holds the default theme's design values, `theme.user` holds the consumer's deliberate deviations, helpers are the last word. (Not "generic to specific": a theme is broad in reach but late in intent, and intent decides cascade order.)
2. **Unlayered CSS beats every layer for normal declarations.** That's the consumer guarantee ("your CSS wins") and why site CSS is unlayered.
3. **The `theme` layer holds at most one active theme** (`theme.*.css`): token overrides plus optional style rules. Theme files are self-layered (each wraps its own content in `@layer theme`) and are **never imported by `mcss.css`**; the consumer entry activates one (see `_global.css`). No theme = the default look. Unlayered consumer CSS still beats themes.
3. **Every design value lives in the default theme** (`theme.default.css` entry importing `theme.default.tokens.css` + `theme.default.ui.css` with `layer(theme.default)`; the parts are plain CSS, the entry assigns their layer, and the dist build pre-wraps the dist copies for lone `<link>` use). It is **never imported by `mcss.css`**; the consumer entry activates it right after `mcss.css` (see `_global.css`), and the framework does not paint without it. A consumer theme (self-layered into `theme.user`, see `theme.starter.css`) beats the default regardless of import order; a full skin like `theme.wireframe.css` is an entry that composes `theme.default.css` and overrides on top. Activate exactly one theme entry. Unlayered consumer CSS still beats all themes.
4. **Helpers are the exception: every helper declaration carries `!important`.** Important layered declarations beat unlayered CSS (the cascade inverts for important), so helpers win over site and consumer CSS too. They're element-level overrides, like inline styling. For important declarations layer order also inverts (earlier layer wins), which is why the reduced-motion block in `base.reset.css` still beats helpers.
5. `!important` is REQUIRED in every helper declaration and banned everywhere else in the framework, except the reduced-motion block in `base.reset.css` and third-party override files (see `site/external.astro.css`). `help.colors.css` and `help.spacing.css` get it from their generators.
6. `postcss.config.cjs` disables preset-env's `cascade-layers` polyfill. **Never remove that option** — the polyfill strips every `@layer` rule and silently replaces the cascade with specificity hacks. The browser floor is set in `.browserslistrc` (`defaults and supports css-cascade-layers`).

## Layer table

| Layer | Prefix | Purpose |
| ---------- | ------------- | ----------------------------------------------------------- |
| settings | `settings.*` | Primitive tokens (`settings.tokens.css`) and interface tokens (`settings.ui.css`); media queries + mixins import unlayered: build-time only |
| base | `base.*` | Reset only (`base.reset.css`) |
| elements | `elements.*` | Bare HTML element styles (text, form, media) |
| global | `global.*` | Structural patterns (a11y, grid, layout, prose, wrap) |
| components | `component.*` | Library components (card, hero, notice, …), including CSS-only single-class ones (badge, button, toggle); imported by `mcss.components.css`, never by `mcss.css` |
| theme | `theme.*` | Swappable themes: token overrides + style rules, self-layered via `@layer theme`, activated by the consumer entry (never by `mcss.css`) |
| helpers | `help.*` | Utility overrides (colors, spacing, typography), all `!important`: beats everything, including unlayered CSS |
| Layer | Prefix | Purpose |
| ------------- | ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| base | `base.*` | Reset only (`base.reset.css`) |
| elements | `elements.*` | Bare HTML element styles (text, form, media) |
| global | `global.*` | Structural patterns (a11y, grid, layout, prose, wrap) |
| components | `component.*` | Library components (card, hero, notice, …), including CSS-only single-class ones (badge, button, toggle); imported by `mcss.components.css`, never by `mcss.css` |
| theme.default | `theme.default.*` | The default theme: scale tokens (`theme.default.tokens.css`) and ui tokens (`theme.default.ui.css`), imported by the `theme.default.css` entry, activated by the consumer entry (never by `mcss.css`); the framework does not paint without it |
| theme.user | `theme.*` | Your theme and swappable skins: token overrides + style rules, self-layered via `@layer theme.user`; a full skin (`theme.wireframe.css`) is an entry composing `theme.default.css` and overriding on top |
| helpers | `help.*` | Utility overrides (colors, spacing, typography), all `!important`: beats everything, including unlayered CSS |

`settings.media-queries.css` and `settings.mixins.css` are the only remaining `settings.*` files: build-time material (`@custom-media` + mixins), imported unlayered, no runtime output.

`page.*` is a file-naming convention only (page-specific styles, e.g. site `page.docs.css`); those files are plain unlayered consumer CSS, there is no `pages` layer.

Expand All @@ -46,7 +48,7 @@ Site-only files keep the same prefixes but live in `src/styles/site/` and import
- Block modifiers (`.card-filled`, `.bt-primary`) for **build-time variants** chosen by props/markup.
- Prefer styling an ARIA attribute when one exists: `[aria-current="true"]`, `[aria-expanded="true"]`, `[aria-disabled="true"]` beat inventing a parallel class.
- Nested states compile to two-class selectors (`.avatar.is-online`) that out-specify single-class modifiers (`.avatar-xl`). So declare custom-property **defaults on the block**, let modifiers override them, and only **consume** the properties inside state blocks — never declare defaults there (that's how the avatar status-dot regression happened).
- Element chains may go as deep as the block's own structure needs (`.home_morph_static_arrow` = arrow inside the morph's static state). The underscore marks hierarchy **within one block**; multi-word single names stay camelCase (`.home_themer_formRow`). A nested *component* always starts its own block (`.themeToggle_text`, never `.header_navMobile_themeToggle_text`).
- Element chains may go as deep as the block's own structure needs (`.home_morph_static_arrow` = arrow inside the morph's static state). The underscore marks hierarchy **within one block**; multi-word single names stay camelCase (`.home_themer_formRow`). A nested _component_ always starts its own block (`.themeToggle_text`, never `.header_navMobile_themeToggle_text`).

### Never couple classes from different components (CRITICAL)

Expand All @@ -55,18 +57,18 @@ A selector must only contain classes from its own block. Never write a descendan
To style a component from outside:

- **Its root**: mix your own class onto the element in markup (`<Section class="home_section …">`) and style that class.
- **Its internals**: use the component's `<part>Class` props (`headerClass`, `titleClass`, …) to mix a class onto the part, or set the component's interface tokens on your own hook class. If neither exists yet, add the prop or token to the component. Do not reach in with a selector.
- **Its internals**: use the component's `<part>Class` props (`headerClass`, `titleClass`, …) to mix a class onto the part, or set the component's ui tokens on your own hook class. If neither exists yet, add the prop or token to the component. Do not reach in with a selector.
- **Bare HTML tags** (`.home_themer_formRow > button`), `.is-*` states, and ARIA attribute selectors are fine inside your own block.
- **Context blocks are not components**: a component may reference the environment it sits in (`@scope (.prose) to (.not-prose)` in `component.notice.css`, `:root.theme-dark`), but the context's own file must never name specific components (that's why `global.prose.css` lists only bare tags).
- **Theme files are the one sanctioned exception**: a `theme.*.css` file may select component classes from outside (`.card::after`, `.bt`) because a theme is by definition a style over the whole system, versioned with the framework. Even there, prefer token overrides; reach for selectors only for what tokens can't express (pseudo-elements, `nth-child` rhythm, `text-decoration`).

### Token naming grammar

Interface tokens (`settings.ui.css`) are the public API of every component; they follow `--component-part-property`, longhand, kebab-case:
UI tokens (`theme.default.ui.css`) are the public API of every component; they follow `--component-part-property`, longhand, kebab-case:

- `--bt-background-color-hover`, `--notice-border-width`, `--avatar-status-dot-color-online`
- No abbreviations (`--card-bg-color` is legacy; new tokens spell out `background-color`), no camelCase, no underscores.
- Feedback colors: prefer the semantic aliases `--success-*` / `--danger-*` / `--warning-*` over the raw `--yes/no/maybe-*` scales.
- Feedback colors come straight from the `--yes-*` / `--no-*` / `--maybe-*` palettes; there is no separate success/danger/warning alias tier.

### Logical vs physical direction

Expand All @@ -85,7 +87,7 @@ Never `transition: all` — it also transitions layout properties, so any late-a

- **One block per file.** Every block gets its own file named after it (`.featureItem` lives in `component.featureItem.css`, never inside `component.featureGrid.css`), even for small companion blocks (`component.fieldRow.css`, site `component.webring.css`).
- Framework file: create `src/styles/framework/<prefix>.<name>.css` and add `@import url(./<file>) layer(<layer>);` in the matching block of `framework/mcss.css`, EXCEPT `component.*` files: those import in `framework/mcss.components.css` (the component library's own entry; `mcss.css` only declares the `components` slot).
- Theme file: create `src/styles/framework/theme.<name>.css` wrapping its own content in `@layer theme { … }`; do NOT import it in `mcss.css` (the consumer entry activates it; the dist index lists it commented out).
- Theme file: copy `src/styles/framework/theme.starter.css` to `theme.<name>.css` (its content self-layers via `@layer theme.user { … }`); do NOT import it in `mcss.css` (the consumer entry activates it after `theme.default.css`). For a full swappable skin, write an entry that imports `theme.default.css` and overrides on top, like `theme.wireframe.css`. The dist index lists non-default themes commented out.
- Site file: create `src/styles/site/<prefix>.<name>.css` and add a plain `@import` in `_global.css` (unlayered).
- Framework CSS must never reference site-only selectors (e.g. `.expressive-code`); the site file mirrors any shared pattern itself.

Expand Down
Loading
Loading