v0.8.2: one cascade across all stylesheets, fluid on the longhands
Browse filesFound while rebuilding the website on its own engine.
- "Later wins" did not hold between stylesheets. Every sheet numbers its
rules from 0 and the engine sorted by (layer, order) comparing those raw
numbers across sheets, so a later sheet lost to an earlier one whenever
the earlier one had more rules above the rule in question. Reproduced with
a 30 rule sheet followed by a 1 rule sheet: the earlier colour won.
rebuild() now renumbers every rule into one global sequence in load order
(assignGlobalOrder, idempotent via order0). Layers still outrank order.
- Consequence: a tokens { } block in a later sheet now overrides a token an
earlier sheet set inside @media or @signal .
- fluid works on padding-top/right/bottom/left/inline/block, margin-*,
letter-spacing and border-radius, not only the four-sided shorthands.
- scripts/test-v082.mjs (27 assertions) runs the engine's own sort on rules
from real compiled sheets; the first check in each group reproduces the
old bug.
- docs: no em dashes in the markdown docs; GUIDE, README, PARITY, llms.txt
and the package USAGE updated; extension synced; zip, tarball and the
0.8.2 vsix rebuilt.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
- GUIDE.md +18 -8
- PARITY.md +14 -3
- README.md +11 -10
- dssl-vscode/{dssl-vscode-0.8.1.vsix → dssl-vscode-0.8.2.vsix} +0 -0
- dssl-vscode/lib/engine/resolver.js +6 -1
- dssl-vscode/package.json +1 -1
- engine-package/USAGE.md +1 -1
- llms.txt +5 -5
- public/dssl-engine.tar.gz +0 -0
- public/dssl-engine.zip +0 -0
- public/dssl/engine.js +31 -0
- public/dssl/index.js +1 -1
- public/dssl/resolver.js +6 -1
- public/llms.txt +5 -5
- scripts/test-v082.mjs +116 -0
- src/app/api/route.ts +0 -5
|
@@ -6,7 +6,7 @@ example in this guide is compiled by `scripts/test-guide-examples.mjs` on every
|
|
| 6 |
release, so the examples cannot drift from the compiler. If an example here
|
| 7 |
stops compiling, that is a release blocker, not a docs bug.
|
| 8 |
|
| 9 |
-
Status up front: DSSL is a real, tested style engine at v0.8.
|
| 10 |
honest alpha. It needs JavaScript, it writes inline styles, and it has a
|
| 11 |
first-paint story you should read before shipping anything serious (see
|
| 12 |
"Honest limits"). Within those limits, it is genuinely nicer to write than
|
|
@@ -109,7 +109,7 @@ Token overrides can be scoped under `@media` or `@signal` for themes.
|
|
| 109 |
.hero { center: both }
|
| 110 |
```
|
| 111 |
|
| 112 |
-
Each place word owns an axis (v0.8.0
|
| 113 |
lose the `between` because every word wrote both axes):
|
| 114 |
|
| 115 |
- `between`, `around`, `evenly` are main-axis words (`justify-content`)
|
|
@@ -123,7 +123,8 @@ overwriting, and a word written twice warns too.
|
|
| 123 |
### Sizing that moves: fluid
|
| 124 |
|
| 125 |
`font-size: fluid 16 24` (and any length-ish property: width, height, gap,
|
| 126 |
-
padding, margin
|
|
|
|
| 127 |
It is 16px on a 360px wide screen, 24px on a 1200px wide one, and in between it
|
| 128 |
slides along a straight line. Past either end it stays put.
|
| 129 |
|
|
@@ -142,6 +143,8 @@ What you get at a few window widths for `fluid 16 24`:
|
|
| 142 |
|
| 143 |
A few things worth knowing:
|
| 144 |
|
|
|
|
|
|
|
| 145 |
- Sizes can be px (bare numbers are px), rem or em. Both sizes need the same unit.
|
| 146 |
- Percentages are refused with an error, because a percentage cannot follow the
|
| 147 |
window width.
|
|
@@ -151,7 +154,7 @@ A few things worth knowing:
|
|
| 151 |
right slope but forgot where the line starts, so sizes ran ahead of the
|
| 152 |
window and hit their maximum around 840px instead of 1200px.
|
| 153 |
|
| 154 |
-
### raw { }
|
| 155 |
|
| 156 |
DSSL has no sugar for `@font-face`, `@keyframes`, `@supports`, `@import` or
|
| 157 |
`@property` yet. `raw { }` (alias `@css { }`) drops verbatim CSS into the
|
|
@@ -177,12 +180,18 @@ styles.dssl:3 warning: raw { } is verbatim CSS, so this line needs a ";" at the
|
|
| 177 |
```
|
| 178 |
|
| 179 |
Unknown at-rules outside `raw { }` are skipped whole, with one clean error
|
| 180 |
-
and a pointer here
|
| 181 |
0.7.2 bug: `@supports (display:grid) { .a { … } }` applied `.a` everywhere
|
| 182 |
after erroring).
|
| 183 |
|
| 184 |
### The cascade is (layer, order)
|
| 185 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 186 |
No specificity, no `!important`. Later wins inside a layer. Layers sort
|
| 187 |
`tokens < base < components < utilities < pin`. Nested blocks do what the
|
| 188 |
indentation says they do (fixed in v0.7.2, it used to be the engine's worst
|
|
@@ -283,7 +292,7 @@ for liveness. `unfreeze()` re-arms everything.
|
|
| 283 |
|
| 284 |
Three ways to get it:
|
| 285 |
|
| 286 |
-
1. Download `dssl-vscode/dssl-vscode-0.8.
|
| 287 |
Extensions panel, `...`, Install from VSIX. Works in VS Code and VSCodium.
|
| 288 |
2. Build it: `cd dssl-vscode && npx @vscode/vsce package --no-dependencies`.
|
| 289 |
3. Registry listing: staged for Open VSX (the registry VSCodium searches).
|
|
@@ -316,6 +325,7 @@ The same output is available in the browser as `DSSL.toCSS(source)`.
|
|
| 316 |
|
| 317 |
```sh
|
| 318 |
node cli/dssl.mjs check public/styles/*.dssl # compile everything, exit code for CI
|
|
|
|
| 319 |
node scripts/test-v081.mjs # 0.8.1 regression suite (37 assertions)
|
| 320 |
node scripts/test-v073.mjs # 0.8.0 regression suite (65 assertions)
|
| 321 |
node scripts/test-guide-examples.mjs # every guide example compiles
|
|
@@ -340,7 +350,7 @@ Read this before choosing DSSL for a real site.
|
|
| 340 |
- **JavaScript is required** for the live engine. No JS, no styles at runtime.
|
| 341 |
v0.8.0 adds the exit lane: `dssl build styles.dssl -o styles.css` compiles
|
| 342 |
the static subset to a real stylesheet you can `<link>` instead of the
|
| 343 |
-
engine
|
| 344 |
`@layer`, motions as `@keyframes`, `raw { }` verbatim. What genuinely needs
|
| 345 |
JS (`@signal`, `animate:`, deform, overlap) stays out and is counted in the
|
| 346 |
report, so you always know what a no-JS page gives up. If a page must work
|
|
@@ -353,7 +363,7 @@ Read this before choosing DSSL for a real site.
|
|
| 353 |
and is on the PARITY roadmap as the next big item.
|
| 354 |
- **CSP.** Pseudo-elements compile to one real stylesheet the engine injects;
|
| 355 |
strict `style-src 'self'` blocks inline style attributes and that sheet.
|
| 356 |
-
DSSL wants `unsafe-inline` for styles today
|
| 357 |
output, which is a normal external stylesheet and CSP-clean.
|
| 358 |
- **Interop is invasive.** DSSL normal declarations beat external CSS, and
|
| 359 |
`pin` beats external `!important`. Mixing with an existing CSS design
|
|
|
|
| 6 |
release, so the examples cannot drift from the compiler. If an example here
|
| 7 |
stops compiling, that is a release blocker, not a docs bug.
|
| 8 |
|
| 9 |
+
Status up front: DSSL is a real, tested style engine at v0.8.2, and it is
|
| 10 |
honest alpha. It needs JavaScript, it writes inline styles, and it has a
|
| 11 |
first-paint story you should read before shipping anything serious (see
|
| 12 |
"Honest limits"). Within those limits, it is genuinely nicer to write than
|
|
|
|
| 109 |
.hero { center: both }
|
| 110 |
```
|
| 111 |
|
| 112 |
+
Each place word owns an axis (v0.8.0, the example above used to silently
|
| 113 |
lose the `between` because every word wrote both axes):
|
| 114 |
|
| 115 |
- `between`, `around`, `evenly` are main-axis words (`justify-content`)
|
|
|
|
| 123 |
### Sizing that moves: fluid
|
| 124 |
|
| 125 |
`font-size: fluid 16 24` (and any length-ish property: width, height, gap,
|
| 126 |
+
padding, margin, their `-top` / `-bottom` / `-inline` / `-block` longhands,
|
| 127 |
+
letter-spacing and border-radius) makes a size that grows smoothly with the window.
|
| 128 |
It is 16px on a 360px wide screen, 24px on a 1200px wide one, and in between it
|
| 129 |
slides along a straight line. Past either end it stays put.
|
| 130 |
|
|
|
|
| 143 |
|
| 144 |
A few things worth knowing:
|
| 145 |
|
| 146 |
+
- `padding: fluid 8 16` sets all four sides. To make just one side fluid, use the
|
| 147 |
+
longhand: `padding-top: fluid 36 80`. (0.8.1 had no way to say that.)
|
| 148 |
- Sizes can be px (bare numbers are px), rem or em. Both sizes need the same unit.
|
| 149 |
- Percentages are refused with an error, because a percentage cannot follow the
|
| 150 |
window width.
|
|
|
|
| 154 |
right slope but forgot where the line starts, so sizes ran ahead of the
|
| 155 |
window and hit their maximum around 840px instead of 1200px.
|
| 156 |
|
| 157 |
+
### raw { }: the escape hatch
|
| 158 |
|
| 159 |
DSSL has no sugar for `@font-face`, `@keyframes`, `@supports`, `@import` or
|
| 160 |
`@property` yet. `raw { }` (alias `@css { }`) drops verbatim CSS into the
|
|
|
|
| 180 |
```
|
| 181 |
|
| 182 |
Unknown at-rules outside `raw { }` are skipped whole, with one clean error
|
| 183 |
+
and a pointer here, they never leak their inner rules (that was a real
|
| 184 |
0.7.2 bug: `@supports (display:grid) { .a { … } }` applied `.a` everywhere
|
| 185 |
after erroring).
|
| 186 |
|
| 187 |
### The cascade is (layer, order)
|
| 188 |
|
| 189 |
+
(This holds across stylesheets too: a sheet loaded later beats one loaded before it.
|
| 190 |
+
Versions up to 0.8.1 got that wrong. Each sheet counted its own rules from zero and
|
| 191 |
+
the engine compared those numbers across sheets, so a later sheet could lose just
|
| 192 |
+
because the earlier one was longer. 0.8.2 gives every rule one position in load
|
| 193 |
+
order.)
|
| 194 |
+
|
| 195 |
No specificity, no `!important`. Later wins inside a layer. Layers sort
|
| 196 |
`tokens < base < components < utilities < pin`. Nested blocks do what the
|
| 197 |
indentation says they do (fixed in v0.7.2, it used to be the engine's worst
|
|
|
|
| 292 |
|
| 293 |
Three ways to get it:
|
| 294 |
|
| 295 |
+
1. Download `dssl-vscode/dssl-vscode-0.8.2.vsix` from the repo files, then
|
| 296 |
Extensions panel, `...`, Install from VSIX. Works in VS Code and VSCodium.
|
| 297 |
2. Build it: `cd dssl-vscode && npx @vscode/vsce package --no-dependencies`.
|
| 298 |
3. Registry listing: staged for Open VSX (the registry VSCodium searches).
|
|
|
|
| 325 |
|
| 326 |
```sh
|
| 327 |
node cli/dssl.mjs check public/styles/*.dssl # compile everything, exit code for CI
|
| 328 |
+
node scripts/test-v082.mjs # 0.8.2 regression suite (27 assertions)
|
| 329 |
node scripts/test-v081.mjs # 0.8.1 regression suite (37 assertions)
|
| 330 |
node scripts/test-v073.mjs # 0.8.0 regression suite (65 assertions)
|
| 331 |
node scripts/test-guide-examples.mjs # every guide example compiles
|
|
|
|
| 350 |
- **JavaScript is required** for the live engine. No JS, no styles at runtime.
|
| 351 |
v0.8.0 adds the exit lane: `dssl build styles.dssl -o styles.css` compiles
|
| 352 |
the static subset to a real stylesheet you can `<link>` instead of the
|
| 353 |
+
engine, tokens as custom properties, states as `:hover`, layers as
|
| 354 |
`@layer`, motions as `@keyframes`, `raw { }` verbatim. What genuinely needs
|
| 355 |
JS (`@signal`, `animate:`, deform, overlap) stays out and is counted in the
|
| 356 |
report, so you always know what a no-JS page gives up. If a page must work
|
|
|
|
| 363 |
and is on the PARITY roadmap as the next big item.
|
| 364 |
- **CSP.** Pseudo-elements compile to one real stylesheet the engine injects;
|
| 365 |
strict `style-src 'self'` blocks inline style attributes and that sheet.
|
| 366 |
+
DSSL wants `unsafe-inline` for styles today, or use the `dssl build`
|
| 367 |
output, which is a normal external stylesheet and CSP-clean.
|
| 368 |
- **Interop is invasive.** DSSL normal declarations beat external CSS, and
|
| 369 |
`pin` beats external `!important`. Mixing with an existing CSS design
|
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
# PARITY.md
|
| 2 |
|
| 3 |
The point of this table is to stop two bad arguments: "DSSL is missing everything" and
|
| 4 |
"DSSL replaces all of CSS". Both are false. Here is the ledger. Status: shipped / partial
|
|
@@ -40,7 +40,7 @@ keeps working with the engine removed. No other styling replacement can say that
|
|
| 40 |
no other replacement styles with the platform's own primitives.
|
| 41 |
|
| 42 |
And since v0.8.0 there is a second exit: `dssl build styles.dssl -o styles.css`
|
| 43 |
-
compiles the static subset to a real stylesheet before you ever ship
|
| 44 |
no inline styles, CSP-clean, works without JavaScript. Tokens become custom properties,
|
| 45 |
states become pseudo-classes, layers become `@layer`, motions become `@keyframes`,
|
| 46 |
`pin` becomes `!important`, `raw { }` passes through verbatim, `seen` applies its final
|
|
@@ -57,6 +57,17 @@ so the boundary is explicit, not folklore.
|
|
| 57 |
- loud errors with did-you-mean, and a warnings channel for cascade-order footguns
|
| 58 |
- `freeze()` / `unfreeze()`: a live engine that can be parked at zero cost
|
| 59 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 60 |
## v0.8.1 follow-up corrections
|
| 61 |
|
| 62 |
A re-test of 0.8.0 found two bugs, and more showed up while writing the tests:
|
|
@@ -81,7 +92,7 @@ A re-test of 0.8.0 found two bugs, and more showed up while writing the tests:
|
|
| 81 |
The second external review (repro snippets, headless browser) found five bugs and
|
| 82 |
asked for the static build first. All fixed or shipped:
|
| 83 |
|
| 84 |
-
- `row: gap 8 between center`
|
| 85 |
(every place word used to write both axes; the later word overwrote
|
| 86 |
`justify-content` silently). Place words now own an axis: between/around/evenly
|
| 87 |
are main-axis, stretch/baseline cross-axis, center/start/end fill free axes;
|
|
|
|
| 1 |
+
# PARITY.md: DSSL vs CSS, honestly
|
| 2 |
|
| 3 |
The point of this table is to stop two bad arguments: "DSSL is missing everything" and
|
| 4 |
"DSSL replaces all of CSS". Both are false. Here is the ledger. Status: shipped / partial
|
|
|
|
| 40 |
no other replacement styles with the platform's own primitives.
|
| 41 |
|
| 42 |
And since v0.8.0 there is a second exit: `dssl build styles.dssl -o styles.css`
|
| 43 |
+
compiles the static subset to a real stylesheet before you ever ship, no runtime,
|
| 44 |
no inline styles, CSP-clean, works without JavaScript. Tokens become custom properties,
|
| 45 |
states become pseudo-classes, layers become `@layer`, motions become `@keyframes`,
|
| 46 |
`pin` becomes `!important`, `raw { }` passes through verbatim, `seen` applies its final
|
|
|
|
| 57 |
- loud errors with did-you-mean, and a warnings channel for cascade-order footguns
|
| 58 |
- `freeze()` / `unfreeze()`: a live engine that can be parked at zero cost
|
| 59 |
|
| 60 |
+
## v0.8.2: what rebuilding the site found
|
| 61 |
+
|
| 62 |
+
- Cross-stylesheet cascade. "Later wins" held inside one sheet and not between
|
| 63 |
+
sheets: every sheet numbered its rules from zero and the engine compared those
|
| 64 |
+
numbers across sheets, so a later sheet lost to an earlier one whenever the
|
| 65 |
+
earlier one had more rules above the rule in question. Every rule now has one
|
| 66 |
+
position in load order. A consequence: a later `tokens { }` block now overrides
|
| 67 |
+
a token an earlier sheet set inside `@media` or `@signal`.
|
| 68 |
+
- `fluid` works on the longhands (`padding-top`, `margin-block`, `letter-spacing`,
|
| 69 |
+
`border-radius` and the rest), not only on the four-sided shorthands.
|
| 70 |
+
|
| 71 |
## v0.8.1 follow-up corrections
|
| 72 |
|
| 73 |
A re-test of 0.8.0 found two bugs, and more showed up while writing the tests:
|
|
|
|
| 92 |
The second external review (repro snippets, headless browser) found five bugs and
|
| 93 |
asked for the static build first. All fixed or shipped:
|
| 94 |
|
| 95 |
+
- `row: gap 8 between center`, the guide's own example, kept its `between`
|
| 96 |
(every place word used to write both axes; the later word overwrote
|
| 97 |
`justify-content` silently). Place words now own an axis: between/around/evenly
|
| 98 |
are main-axis, stretch/baseline cross-axis, center/start/end fill free axes;
|
|
@@ -6,7 +6,7 @@ license: mit
|
|
| 6 |
|
| 7 |
DSSL is a style language for the web. For the pages it fits, it replaces writing CSS entirely. It does not replace the CSS platform: the engine ends in inline styles that the browser parses, and it needs JavaScript. Where it wins is the authoring experience: tokens, words-as-layout, states and motion in one grammar, loud errors, zero build. PARITY.md keeps the honest feature ledger.
|
| 8 |
|
| 9 |
-
Not a preprocessor that spits out stylesheets. Not a utility class graveyard. You write `.dssl` files, the page loads a small engine, and the engine walks your DOM in real time and applies every rule it matches straight onto the elements it finds. Nothing compiles at runtime
|
| 10 |
|
| 11 |
Elements that show up later get styled the moment they appear. Change a rule and the page updates on the next pass. The language is the runtime.
|
| 12 |
|
|
@@ -68,7 +68,7 @@ Both drop a `dssl-engine/` folder next to you: the engine, a starter token set,
|
|
| 68 |
|
| 69 |
Prefer to click: [dssl-engine.zip](https://dssl-sable.vercel.app/dssl-engine.zip) or [dssl-engine.tar.gz](https://dssl-sable.vercel.app/dssl-engine.tar.gz). Same package, straight from the browser.
|
| 70 |
|
| 71 |
-
Writing your own .dssl? The official editor extension (`ehs-studio.dssl-vscode`) puts the real compiler in your editor: the squiggles are the same errors the browser console prints, completions come from your own token registry, and hovers explain the semantic vocabulary. One package for VS Code and VSCodium (same extension API): download `dssl-vscode/dssl-vscode-0.8.
|
| 72 |
|
| 73 |
AI agents and machine reviewers: this repo ships `llms.txt` (dense, structured facts), `AGENTS.md` (how to work on the codebase) and `PARITY.md` (the honest feature matrix against CSS). The site serves the same `llms.txt` at https://dssl-sable.vercel.app/llms.txt
|
| 74 |
|
|
@@ -346,7 +346,7 @@ v0.7 added warnings: things that compile, run, and are still probably not what y
|
|
| 346 |
.nav-links {
|
| 347 |
display: none
|
| 348 |
column: gap 2 // warning: "column:" expands to display: flex and
|
| 349 |
-
// overrides the earlier "display: none"
|
| 350 |
}
|
| 351 |
```
|
| 352 |
|
|
@@ -415,17 +415,17 @@ DSSL.load(['/styles/app.dssl']).then(() => {
|
|
| 415 |
|
| 416 |
```js
|
| 417 |
const rep = DSSL.explain(document.querySelector('.doc-card'))
|
| 418 |
-
// rep.matched
|
| 419 |
-
// rep.wins
|
| 420 |
-
// rep.pseudo
|
| 421 |
```
|
| 422 |
|
| 423 |
The matcher also pre-filters every rule through a cheap class/tag/id gate extracted from the selector's rightmost compound, so a recompute skips obvious non-matches instead of asking the browser rule by rule. The gate only ever says no; anything it does not fully understand falls back to native matching.
|
| 424 |
|
| 425 |
-
## What works today (v0.8.
|
| 426 |
|
| 427 |
- the full selector surface, including `:nth-child`, `:not`, `:is`, `:where`, `:has`
|
| 428 |
-
- flexbox and grid with `row:` / `column:` / `grid:` / `stack:` / `center:` sugar
|
| 429 |
- behaviors and `use`
|
| 430 |
- design tokens, numeric keys included, scoped per file and per subtree
|
| 431 |
- states, engine-native `seen`, `stagger` choreography
|
|
@@ -435,7 +435,8 @@ The matcher also pre-filters every rule through a cheap class/tag/id gate extrac
|
|
| 435 |
- the overlap policy and `pointer-events` pass-through for veils
|
| 436 |
- pseudo-elements in any spelling (shadow CSS), themed scrollbars included
|
| 437 |
- `raw { }` / `@css { }` for verbatim CSS (`@font-face`, `@keyframes`, `@supports`); unknown at-rules skip their whole block with one clean error
|
| 438 |
-
- `fluid 16 24` sizing sugar on any length property
|
|
|
|
| 439 |
- the CLI: `dssl check` (CI, --json, --strict) and `dssl build` (real CSS from the static subset, also with --strict)
|
| 440 |
- compiler warnings for cascade-order footguns and duplicate axis words
|
| 441 |
- `DSSL.freeze()` / `DSSL.unfreeze()` / `DSSL.explain()` / `DSSL.toCSS()`
|
|
@@ -457,7 +458,7 @@ The matcher also pre-filters every rule through a cheap class/tag/id gate extrac
|
|
| 457 |
|
| 458 |
## Status
|
| 459 |
|
| 460 |
-
v0.8.
|
| 461 |
|
| 462 |
## License
|
| 463 |
|
|
|
|
| 6 |
|
| 7 |
DSSL is a style language for the web. For the pages it fits, it replaces writing CSS entirely. It does not replace the CSS platform: the engine ends in inline styles that the browser parses, and it needs JavaScript. Where it wins is the authoring experience: tokens, words-as-layout, states and motion in one grammar, loud errors, zero build. PARITY.md keeps the honest feature ledger.
|
| 8 |
|
| 9 |
+
Not a preprocessor that spits out stylesheets. Not a utility class graveyard. You write `.dssl` files, the page loads a small engine, and the engine walks your DOM in real time and applies every rule it matches straight onto the elements it finds. Nothing compiles at runtime, and since v0.8.0 there is also an exit lane: `dssl build` compiles the static subset of the same source to a real `.css` file for pages that must work without JavaScript or sit behind a strict CSP. You choose per page: live engine, built CSS, or both.
|
| 10 |
|
| 11 |
Elements that show up later get styled the moment they appear. Change a rule and the page updates on the next pass. The language is the runtime.
|
| 12 |
|
|
|
|
| 68 |
|
| 69 |
Prefer to click: [dssl-engine.zip](https://dssl-sable.vercel.app/dssl-engine.zip) or [dssl-engine.tar.gz](https://dssl-sable.vercel.app/dssl-engine.tar.gz). Same package, straight from the browser.
|
| 70 |
|
| 71 |
+
Writing your own .dssl? The official editor extension (`ehs-studio.dssl-vscode`) puts the real compiler in your editor: the squiggles are the same errors the browser console prints, completions come from your own token registry, and hovers explain the semantic vocabulary. One package for VS Code and VSCodium (same extension API): download `dssl-vscode/dssl-vscode-0.8.2.vsix` from the repo files, then Extensions panel -> ... -> Install from VSIX. An Open VSX listing (so it appears in VSCodium's Extensions tab, searchable) is staged and publishes with one Open VSX token; see `dssl-vscode/PUBLISHING.md`.
|
| 72 |
|
| 73 |
AI agents and machine reviewers: this repo ships `llms.txt` (dense, structured facts), `AGENTS.md` (how to work on the codebase) and `PARITY.md` (the honest feature matrix against CSS). The site serves the same `llms.txt` at https://dssl-sable.vercel.app/llms.txt
|
| 74 |
|
|
|
|
| 346 |
.nav-links {
|
| 347 |
display: none
|
| 348 |
column: gap 2 // warning: "column:" expands to display: flex and
|
| 349 |
+
// overrides the earlier "display: none", menu stays open
|
| 350 |
}
|
| 351 |
```
|
| 352 |
|
|
|
|
| 415 |
|
| 416 |
```js
|
| 417 |
const rep = DSSL.explain(document.querySelector('.doc-card'))
|
| 418 |
+
// rep.matched: [{ id, sel, layer, file, line, states, media, signals, decls }]
|
| 419 |
+
// rep.wins : [{ prop, value, by: { sel, file, line } }]
|
| 420 |
+
// rep.pseudo : the ::before / ::after rules touching this element
|
| 421 |
```
|
| 422 |
|
| 423 |
The matcher also pre-filters every rule through a cheap class/tag/id gate extracted from the selector's rightmost compound, so a recompute skips obvious non-matches instead of asking the browser rule by rule. The gate only ever says no; anything it does not fully understand falls back to native matching.
|
| 424 |
|
| 425 |
+
## What works today (v0.8.2)
|
| 426 |
|
| 427 |
- the full selector surface, including `:nth-child`, `:not`, `:is`, `:where`, `:has`
|
| 428 |
+
- flexbox and grid with `row:` / `column:` / `grid:` / `stack:` / `center:` sugar, place words now own an axis (`between` is main-axis, `stretch` is cross-axis) and conflicts warn instead of silently overwriting
|
| 429 |
- behaviors and `use`
|
| 430 |
- design tokens, numeric keys included, scoped per file and per subtree
|
| 431 |
- states, engine-native `seen`, `stagger` choreography
|
|
|
|
| 435 |
- the overlap policy and `pointer-events` pass-through for veils
|
| 436 |
- pseudo-elements in any spelling (shadow CSS), themed scrollbars included
|
| 437 |
- `raw { }` / `@css { }` for verbatim CSS (`@font-face`, `@keyframes`, `@supports`); unknown at-rules skip their whole block with one clean error
|
| 438 |
+
- `fluid 16 24` sizing sugar on any length property, including `padding-top`, `margin-block` and the other longhands
|
| 439 |
+
- one cascade across every stylesheet: a sheet loaded later beats an earlier one (fixed in 0.8.2)
|
| 440 |
- the CLI: `dssl check` (CI, --json, --strict) and `dssl build` (real CSS from the static subset, also with --strict)
|
| 441 |
- compiler warnings for cascade-order footguns and duplicate axis words
|
| 442 |
- `DSSL.freeze()` / `DSSL.unfreeze()` / `DSSL.explain()` / `DSSL.toCSS()`
|
|
|
|
| 458 |
|
| 459 |
## Status
|
| 460 |
|
| 461 |
+
v0.8.2, experimental, moving fast. The language grows whenever the site needs something it cannot express yet, so the site doubles as the test suite, and the docs examples are rendered and asserted in a real browser, so the guide cannot lie. Honest ledger: great for creative sites, prototypes, docs, playgrounds and internal tools; when a page must work without JavaScript, build the static subset with `dssl build` or pick another tool for that page. Freeze makes big static pages viable; live interaction is what the engine is for.
|
| 462 |
|
| 463 |
## License
|
| 464 |
|
|
Binary files a/dssl-vscode/dssl-vscode-0.8.1.vsix and b/dssl-vscode/dssl-vscode-0.8.2.vsix differ
|
|
|
|
@@ -946,7 +946,12 @@ const SEMANTIC = {
|
|
| 946 |
* (360px and 1200px by default) — the formula everyone writes by hand and
|
| 947 |
* gets wrong. */
|
| 948 |
const FLUID_PROPS = new Set(('font-size,width,height,inline-size,block-size,min-width,max-width,min-height,max-height,' +
|
| 949 |
-
'gap,row-gap,column-gap,padding,margin,inset,top,right,bottom,left
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 950 |
|
| 951 |
function buildFluidParts(parts, ctx, prop = 'font-size') {
|
| 952 |
const bad = (s, why) => {
|
|
|
|
| 946 |
* (360px and 1200px by default) — the formula everyone writes by hand and
|
| 947 |
* gets wrong. */
|
| 948 |
const FLUID_PROPS = new Set(('font-size,width,height,inline-size,block-size,min-width,max-width,min-height,max-height,' +
|
| 949 |
+
'gap,row-gap,column-gap,padding,margin,inset,top,right,bottom,left,' +
|
| 950 |
+
// 0.8.2: the longhands. The site's own hero wanted a fluid padding-top and
|
| 951 |
+
// there was no way to say it, because `padding: fluid ...` is all four sides.
|
| 952 |
+
'padding-top,padding-right,padding-bottom,padding-left,padding-inline,padding-block,' +
|
| 953 |
+
'margin-top,margin-right,margin-bottom,margin-left,margin-inline,margin-block,' +
|
| 954 |
+
'letter-spacing,border-radius').split(','));
|
| 955 |
|
| 956 |
function buildFluidParts(parts, ctx, prop = 'font-size') {
|
| 957 |
const bad = (s, why) => {
|
|
@@ -2,7 +2,7 @@
|
|
| 2 |
"name": "dssl-vscode",
|
| 3 |
"displayName": "DSSL Language Support",
|
| 4 |
"description": "The DSSL style language in your editor: syntax highlighting, real-compiler diagnostics (the same errors the browser console shows), completions from the live token registry, hover docs. Works in VS Code and VSCodium.",
|
| 5 |
-
"version": "0.8.
|
| 6 |
"publisher": "ehs-studio",
|
| 7 |
"icon": "icon.png",
|
| 8 |
"license": "MIT",
|
|
|
|
| 2 |
"name": "dssl-vscode",
|
| 3 |
"displayName": "DSSL Language Support",
|
| 4 |
"description": "The DSSL style language in your editor: syntax highlighting, real-compiler diagnostics (the same errors the browser console shows), completions from the live token registry, hover docs. Works in VS Code and VSCodium.",
|
| 5 |
+
"version": "0.8.2",
|
| 6 |
"publisher": "ehs-studio",
|
| 7 |
"icon": "icon.png",
|
| 8 |
"license": "MIT",
|
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
# DSSL engine
|
| 2 |
|
| 3 |
DSSL is a style language that runs in the browser. You write `.dssl` files, add two tags to your page, and the engine does the rest. No compiler, no build step, no CSS output. The engine walks your DOM live and writes styles straight onto the elements it matches, including elements that show up later.
|
| 4 |
|
|
|
|
| 1 |
+
# DSSL engine: quickstart
|
| 2 |
|
| 3 |
DSSL is a style language that runs in the browser. You write `.dssl` files, add two tags to your page, and the engine does the rest. No compiler, no build step, no CSS output. The engine walks your DOM live and writes styles straight onto the elements it matches, including elements that show up later.
|
| 4 |
|
|
@@ -1,18 +1,18 @@
|
|
| 1 |
# DSSL
|
| 2 |
|
| 3 |
-
> DSSL is a style language for the web. For JS-guaranteed pages it replaces writing CSS; it does not replace the CSS platform. You write `.dssl` files, the page loads the engine (about 58 kB gzipped, about 190 kB as plain source), and the engine walks the DOM in real time applying every matched rule as inline styles. Nothing compiles to CSS at runtime
|
| 4 |
|
| 5 |
- Site (docs, playground, changelog, bench): https://dssl-sable.vercel.app
|
| 6 |
- Repo + full docs in the README: https://huggingface.co/EHS-Studio/dssl
|
| 7 |
- Install: `curl -fsSL https://dssl-sable.vercel.app/install.sh | sh` (or the zip: https://dssl-sable.vercel.app/dssl-engine.zip)
|
| 8 |
-
- Version: 0.8.
|
| 9 |
- Feature matrix vs CSS: see PARITY.md in the repo
|
| 10 |
- For agents working ON the codebase: see AGENTS.md in the repo
|
| 11 |
|
| 12 |
## The five things reviewers ask about
|
| 13 |
|
| 14 |
1. No build step. Styles are source files linked with `<link rel="stylesheet/dssl">` or embedded in the page as `<style type="text/dssl">` / `<script type="text/dssl">` blocks (auto-booted in document order). The engine restyles the live DOM, including nodes added later.
|
| 15 |
-
2. Honest trade: it is a JS runtime. If JavaScript is unavailable the page renders unstyled
|
| 16 |
3. Exit cost is zero. Abandoning DSSL leaves you a page of valid inline styles and one small shadow stylesheet for pseudo-elements. There is nothing to un-install from your markup.
|
| 17 |
4. Speed has a handbrake. `DSSL.freeze()` detaches every observer and listener after first paint and leaves the resolved look at zero engine cost; `unfreeze()` re-arms. A gate prefilter skips obvious non-matches before native matching. The Bench page on the site measures 100 / 1,000 / 10,000 nodes on your machine: CSS `<style>` vs DSSL live vs DSSL frozen.
|
| 18 |
5. The cascade is (layer, source order). No specificity, no !important. Nested state blocks sort after the parent rule's own declarations (v0.7.2 fix: they used to lose), and nested `@media`/`@signal`/`@layer` keep their condition. `pin { }` is the only must-win block. The compiler refuses to guess: unknown tokens, properties and values fail loudly with did-you-mean; `--*` custom properties pass through unvalidated on purpose.
|
|
@@ -31,7 +31,7 @@
|
|
| 31 |
- Overlap policy: nothing below 100% opacity overlaps anything unless `overlap: allow`; `deny` is unconditional; pairs with `pointer-events: none` so veils stop eating clicks and text selection.
|
| 32 |
- Behaviors: named rule groups applied with `use focus-ring`.
|
| 33 |
- Verbatim escape hatch (v0.8.0): `raw { }` / `@css { }` drops real CSS (@font-face, @keyframes, @supports, @import, @property) into the page stylesheet untouched; unknown at-rules skip their whole block with one clean error. Inside raw { } semicolons are required (it is real CSS); a missing one is a warning with a line number.
|
| 34 |
-
- Fluid sugar (v0.8.0): `font-size: fluid 16 24` (and any length-ish property) compiles to the linear clamp() between 360px and 1200px viewports (exactly the first size at 360px and the second at 1200px; px, rem or em, no percentages; shrinking sizes work); `fluid: 16, 24` still works. 0.8.1 fixed the math: 0.8.0 left out the intercept, so sizes were too large on small screens.
|
| 35 |
- v0.8.0 review-driven fixes, all regression-tested incl. headless-Chromium computed-style tests of the docs: row/column axis roles (the documented `between center` example laid out wrong before); @font-face no longer crashes the compiler (unknown at-rules skip balanced); @supports inner rules no longer leak unconditionally; `url(...)` is atomic (unquoted form no longer false-errors); `!important` is a loud error that emits nothing; the animation shorthand warns when its motion is undefined instead of suggesting "span"; CSS-order transition values get the exact DSSL conversion in the error hint.
|
| 36 |
- v0.7.2 review-driven fixes, all regression-tested: gate prefilter runs before native matches(); hover state events scope candidate recomputation to the touched subtree plus ancestors (was page-wide); checkbox/details states listen to `change`/`toggle`; raw hex alpha (`#ff0000/50` -> color-mix); bare numbers on time properties are ms; `::part()` supported, `::slotted()` rejected with an explanation; SVG properties (`fill`, `stroke*`) and `text-wrap`/`content-visibility` in the vocabulary.
|
| 37 |
- Warnings channel (v0.7): things that compile but are probably wrong, e.g. a layout shorthand expanding to `display` and clobbering an earlier explicit `display:` in the same rule.
|
|
@@ -62,4 +62,4 @@ Wait for the `dssl-ready` event on `window` before measuring layout: the engine
|
|
| 62 |
|
| 63 |
## Editor support
|
| 64 |
|
| 65 |
-
The official extension (VS Code and VSCodium, same .vsix) bundles the real parser and resolver: diagnostics identical to the browser console, completions from the workspace token registry, hover docs for the semantic vocabulary. Install `dssl-vscode/dssl-vscode-0.8.
|
|
|
|
| 1 |
# DSSL
|
| 2 |
|
| 3 |
+
> DSSL is a style language for the web. For JS-guaranteed pages it replaces writing CSS; it does not replace the CSS platform. You write `.dssl` files, the page loads the engine (about 58 kB gzipped, about 190 kB as plain source), and the engine walks the DOM in real time applying every matched rule as inline styles. Nothing compiles to CSS at runtime, but v0.8.0 adds the exit lane: `dssl build` compiles the static subset to a real .css file for no-JS, strict-CSP or size-critical pages. Elements added later are styled the moment they appear; signals, states and motion share one grammar.
|
| 4 |
|
| 5 |
- Site (docs, playground, changelog, bench): https://dssl-sable.vercel.app
|
| 6 |
- Repo + full docs in the README: https://huggingface.co/EHS-Studio/dssl
|
| 7 |
- Install: `curl -fsSL https://dssl-sable.vercel.app/install.sh | sh` (or the zip: https://dssl-sable.vercel.app/dssl-engine.zip)
|
| 8 |
+
- Version: 0.8.2 (engine), MIT license
|
| 9 |
- Feature matrix vs CSS: see PARITY.md in the repo
|
| 10 |
- For agents working ON the codebase: see AGENTS.md in the repo
|
| 11 |
|
| 12 |
## The five things reviewers ask about
|
| 13 |
|
| 14 |
1. No build step. Styles are source files linked with `<link rel="stylesheet/dssl">` or embedded in the page as `<style type="text/dssl">` / `<script type="text/dssl">` blocks (auto-booted in document order). The engine restyles the live DOM, including nodes added later.
|
| 15 |
+
2. Honest trade: it is a JS runtime. If JavaScript is unavailable the page renders unstyled, unless you build the static subset: `dssl build styles.dssl -o styles.css` emits real CSS (tokens → custom properties, states → :hover, layers → @layer, motions → @keyframes, `raw { }` verbatim, `seen` applied as its no-JS final state) and the report counts what stays runtime-only (`@signal` rules, `animate:`). For content pages that must not flash, use the build output or choose another tool.
|
| 16 |
3. Exit cost is zero. Abandoning DSSL leaves you a page of valid inline styles and one small shadow stylesheet for pseudo-elements. There is nothing to un-install from your markup.
|
| 17 |
4. Speed has a handbrake. `DSSL.freeze()` detaches every observer and listener after first paint and leaves the resolved look at zero engine cost; `unfreeze()` re-arms. A gate prefilter skips obvious non-matches before native matching. The Bench page on the site measures 100 / 1,000 / 10,000 nodes on your machine: CSS `<style>` vs DSSL live vs DSSL frozen.
|
| 18 |
5. The cascade is (layer, source order). No specificity, no !important. Nested state blocks sort after the parent rule's own declarations (v0.7.2 fix: they used to lose), and nested `@media`/`@signal`/`@layer` keep their condition. `pin { }` is the only must-win block. The compiler refuses to guess: unknown tokens, properties and values fail loudly with did-you-mean; `--*` custom properties pass through unvalidated on purpose.
|
|
|
|
| 31 |
- Overlap policy: nothing below 100% opacity overlaps anything unless `overlap: allow`; `deny` is unconditional; pairs with `pointer-events: none` so veils stop eating clicks and text selection.
|
| 32 |
- Behaviors: named rule groups applied with `use focus-ring`.
|
| 33 |
- Verbatim escape hatch (v0.8.0): `raw { }` / `@css { }` drops real CSS (@font-face, @keyframes, @supports, @import, @property) into the page stylesheet untouched; unknown at-rules skip their whole block with one clean error. Inside raw { } semicolons are required (it is real CSS); a missing one is a warning with a line number.
|
| 34 |
+
- Fluid sugar (v0.8.0): `font-size: fluid 16 24` (and any length-ish property) compiles to the linear clamp() between 360px and 1200px viewports (exactly the first size at 360px and the second at 1200px; px, rem or em, no percentages; shrinking sizes work); `fluid: 16, 24` still works; it also works on the longhands (`padding-top`, `margin-block`, `letter-spacing`, `border-radius`) since 0.8.2. 0.8.2 also made the cascade global: a stylesheet loaded later always beats an earlier one, and a later `tokens { }` block overrides an earlier conditional token. 0.8.1 fixed the math: 0.8.0 left out the intercept, so sizes were too large on small screens.
|
| 35 |
- v0.8.0 review-driven fixes, all regression-tested incl. headless-Chromium computed-style tests of the docs: row/column axis roles (the documented `between center` example laid out wrong before); @font-face no longer crashes the compiler (unknown at-rules skip balanced); @supports inner rules no longer leak unconditionally; `url(...)` is atomic (unquoted form no longer false-errors); `!important` is a loud error that emits nothing; the animation shorthand warns when its motion is undefined instead of suggesting "span"; CSS-order transition values get the exact DSSL conversion in the error hint.
|
| 36 |
- v0.7.2 review-driven fixes, all regression-tested: gate prefilter runs before native matches(); hover state events scope candidate recomputation to the touched subtree plus ancestors (was page-wide); checkbox/details states listen to `change`/`toggle`; raw hex alpha (`#ff0000/50` -> color-mix); bare numbers on time properties are ms; `::part()` supported, `::slotted()` rejected with an explanation; SVG properties (`fill`, `stroke*`) and `text-wrap`/`content-visibility` in the vocabulary.
|
| 37 |
- Warnings channel (v0.7): things that compile but are probably wrong, e.g. a layout shorthand expanding to `display` and clobbering an earlier explicit `display:` in the same rule.
|
|
|
|
| 62 |
|
| 63 |
## Editor support
|
| 64 |
|
| 65 |
+
The official extension (VS Code and VSCodium, same .vsix) bundles the real parser and resolver: diagnostics identical to the browser console, completions from the workspace token registry, hover docs for the semantic vocabulary. Install `dssl-vscode/dssl-vscode-0.8.2.vsix` from the repo files. Source lives in `dssl-vscode/` in the repo.
|
|
Binary files a/public/dssl-engine.tar.gz and b/public/dssl-engine.tar.gz differ
|
|
|
|
Binary files a/public/dssl-engine.zip and b/public/dssl-engine.zip differ
|
|
|
|
@@ -23,6 +23,36 @@ import { composeDeform, arcLayout } from './utils.js';
|
|
| 23 |
|
| 24 |
const LAYER_RANK = { tokens: 0, base: 1, components: 2, utilities: 3, pin: 4 };
|
| 25 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
// v0.3.4 — alignment implies a formatting context.
|
| 27 |
// place-items / place-content / align-* / justify-* only arrange children
|
| 28 |
// inside flex or grid containers. On a plain <span> or <button> they used to
|
|
@@ -175,6 +205,7 @@ export class DsslEngine {
|
|
| 175 |
const shadowRules = [];
|
| 176 |
const raws = [];
|
| 177 |
const bundles = [...this.bundles.values()].sort((a, b) => a.seq - b.seq);
|
|
|
|
| 178 |
for (const b of bundles) {
|
| 179 |
for (const r of b.rules) {
|
| 180 |
rules.push(r); // scoped engines map :root rules onto the scope element
|
|
|
|
| 23 |
|
| 24 |
const LAYER_RANK = { tokens: 0, base: 1, components: 2, utilities: 3, pin: 4 };
|
| 25 |
|
| 26 |
+
/* 0.8.2: one cascade position per rule, across every stylesheet on the page.
|
| 27 |
+
*
|
| 28 |
+
* A rule's own `order` is just its place inside the sheet it was written in:
|
| 29 |
+
* every sheet counts from 0. The cascade sorts by (layer, order), so when two
|
| 30 |
+
* sheets were combined their numbers were compared as if they were one count,
|
| 31 |
+
* and a later sheet's rule could lose to an earlier sheet's rule purely
|
| 32 |
+
* because the earlier sheet had more rules above it. "Later wins" held inside
|
| 33 |
+
* a sheet and not between them. That is also why a tokens block in a later
|
| 34 |
+
* sheet could not override a conditional token from an earlier one.
|
| 35 |
+
*
|
| 36 |
+
* This puts the bundles in load order and shifts each one's numbers past the
|
| 37 |
+
* end of the one before. The sheet's own numbers are kept in `order0`, so
|
| 38 |
+
* calling it again after a rebuild gives the same result instead of drifting.
|
| 39 |
+
*/
|
| 40 |
+
export function assignGlobalOrder(bundlesInLoadOrder) {
|
| 41 |
+
let base = 0;
|
| 42 |
+
for (const b of bundlesInLoadOrder) {
|
| 43 |
+
let max = -1;
|
| 44 |
+
for (const list of [b.rules, b.shadowRules, b.raws]) {
|
| 45 |
+
if (!list) continue;
|
| 46 |
+
for (const r of list) {
|
| 47 |
+
if (r.order0 === undefined) r.order0 = r.order;
|
| 48 |
+
r.order = base + r.order0;
|
| 49 |
+
if (r.order0 > max) max = r.order0;
|
| 50 |
+
}
|
| 51 |
+
}
|
| 52 |
+
base += max + 1;
|
| 53 |
+
}
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
// v0.3.4 — alignment implies a formatting context.
|
| 57 |
// place-items / place-content / align-* / justify-* only arrange children
|
| 58 |
// inside flex or grid containers. On a plain <span> or <button> they used to
|
|
|
|
| 205 |
const shadowRules = [];
|
| 206 |
const raws = [];
|
| 207 |
const bundles = [...this.bundles.values()].sort((a, b) => a.seq - b.seq);
|
| 208 |
+
assignGlobalOrder(bundles);
|
| 209 |
for (const b of bundles) {
|
| 210 |
for (const r of b.rules) {
|
| 211 |
rules.push(r); // scoped engines map :root rules onto the scope element
|
|
@@ -19,7 +19,7 @@ import { resolve, DEFAULT_TOKENS } from './resolver.js';
|
|
| 19 |
import { toCSS as docToCSS } from './static.js';
|
| 20 |
import { DsslEngine } from './engine.js';
|
| 21 |
|
| 22 |
-
export const VERSION = '0.8.
|
| 23 |
|
| 24 |
let counter = 0;
|
| 25 |
const engines = new Map();
|
|
|
|
| 19 |
import { toCSS as docToCSS } from './static.js';
|
| 20 |
import { DsslEngine } from './engine.js';
|
| 21 |
|
| 22 |
+
export const VERSION = '0.8.2';
|
| 23 |
|
| 24 |
let counter = 0;
|
| 25 |
const engines = new Map();
|
|
@@ -946,7 +946,12 @@ const SEMANTIC = {
|
|
| 946 |
* (360px and 1200px by default) — the formula everyone writes by hand and
|
| 947 |
* gets wrong. */
|
| 948 |
const FLUID_PROPS = new Set(('font-size,width,height,inline-size,block-size,min-width,max-width,min-height,max-height,' +
|
| 949 |
-
'gap,row-gap,column-gap,padding,margin,inset,top,right,bottom,left
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 950 |
|
| 951 |
function buildFluidParts(parts, ctx, prop = 'font-size') {
|
| 952 |
const bad = (s, why) => {
|
|
|
|
| 946 |
* (360px and 1200px by default) — the formula everyone writes by hand and
|
| 947 |
* gets wrong. */
|
| 948 |
const FLUID_PROPS = new Set(('font-size,width,height,inline-size,block-size,min-width,max-width,min-height,max-height,' +
|
| 949 |
+
'gap,row-gap,column-gap,padding,margin,inset,top,right,bottom,left,' +
|
| 950 |
+
// 0.8.2: the longhands. The site's own hero wanted a fluid padding-top and
|
| 951 |
+
// there was no way to say it, because `padding: fluid ...` is all four sides.
|
| 952 |
+
'padding-top,padding-right,padding-bottom,padding-left,padding-inline,padding-block,' +
|
| 953 |
+
'margin-top,margin-right,margin-bottom,margin-left,margin-inline,margin-block,' +
|
| 954 |
+
'letter-spacing,border-radius').split(','));
|
| 955 |
|
| 956 |
function buildFluidParts(parts, ctx, prop = 'font-size') {
|
| 957 |
const bad = (s, why) => {
|
|
@@ -1,18 +1,18 @@
|
|
| 1 |
# DSSL
|
| 2 |
|
| 3 |
-
> DSSL is a style language for the web. For JS-guaranteed pages it replaces writing CSS; it does not replace the CSS platform. You write `.dssl` files, the page loads the engine (about 58 kB gzipped, about 190 kB as plain source), and the engine walks the DOM in real time applying every matched rule as inline styles. Nothing compiles to CSS at runtime
|
| 4 |
|
| 5 |
- Site (docs, playground, changelog, bench): https://dssl-sable.vercel.app
|
| 6 |
- Repo + full docs in the README: https://huggingface.co/EHS-Studio/dssl
|
| 7 |
- Install: `curl -fsSL https://dssl-sable.vercel.app/install.sh | sh` (or the zip: https://dssl-sable.vercel.app/dssl-engine.zip)
|
| 8 |
-
- Version: 0.8.
|
| 9 |
- Feature matrix vs CSS: see PARITY.md in the repo
|
| 10 |
- For agents working ON the codebase: see AGENTS.md in the repo
|
| 11 |
|
| 12 |
## The five things reviewers ask about
|
| 13 |
|
| 14 |
1. No build step. Styles are source files linked with `<link rel="stylesheet/dssl">` or embedded in the page as `<style type="text/dssl">` / `<script type="text/dssl">` blocks (auto-booted in document order). The engine restyles the live DOM, including nodes added later.
|
| 15 |
-
2. Honest trade: it is a JS runtime. If JavaScript is unavailable the page renders unstyled
|
| 16 |
3. Exit cost is zero. Abandoning DSSL leaves you a page of valid inline styles and one small shadow stylesheet for pseudo-elements. There is nothing to un-install from your markup.
|
| 17 |
4. Speed has a handbrake. `DSSL.freeze()` detaches every observer and listener after first paint and leaves the resolved look at zero engine cost; `unfreeze()` re-arms. A gate prefilter skips obvious non-matches before native matching. The Bench page on the site measures 100 / 1,000 / 10,000 nodes on your machine: CSS `<style>` vs DSSL live vs DSSL frozen.
|
| 18 |
5. The cascade is (layer, source order). No specificity, no !important. Nested state blocks sort after the parent rule's own declarations (v0.7.2 fix: they used to lose), and nested `@media`/`@signal`/`@layer` keep their condition. `pin { }` is the only must-win block. The compiler refuses to guess: unknown tokens, properties and values fail loudly with did-you-mean; `--*` custom properties pass through unvalidated on purpose.
|
|
@@ -31,7 +31,7 @@
|
|
| 31 |
- Overlap policy: nothing below 100% opacity overlaps anything unless `overlap: allow`; `deny` is unconditional; pairs with `pointer-events: none` so veils stop eating clicks and text selection.
|
| 32 |
- Behaviors: named rule groups applied with `use focus-ring`.
|
| 33 |
- Verbatim escape hatch (v0.8.0): `raw { }` / `@css { }` drops real CSS (@font-face, @keyframes, @supports, @import, @property) into the page stylesheet untouched; unknown at-rules skip their whole block with one clean error. Inside raw { } semicolons are required (it is real CSS); a missing one is a warning with a line number.
|
| 34 |
-
- Fluid sugar (v0.8.0): `font-size: fluid 16 24` (and any length-ish property) compiles to the linear clamp() between 360px and 1200px viewports (exactly the first size at 360px and the second at 1200px; px, rem or em, no percentages; shrinking sizes work); `fluid: 16, 24` still works. 0.8.1 fixed the math: 0.8.0 left out the intercept, so sizes were too large on small screens.
|
| 35 |
- v0.8.0 review-driven fixes, all regression-tested incl. headless-Chromium computed-style tests of the docs: row/column axis roles (the documented `between center` example laid out wrong before); @font-face no longer crashes the compiler (unknown at-rules skip balanced); @supports inner rules no longer leak unconditionally; `url(...)` is atomic (unquoted form no longer false-errors); `!important` is a loud error that emits nothing; the animation shorthand warns when its motion is undefined instead of suggesting "span"; CSS-order transition values get the exact DSSL conversion in the error hint.
|
| 36 |
- v0.7.2 review-driven fixes, all regression-tested: gate prefilter runs before native matches(); hover state events scope candidate recomputation to the touched subtree plus ancestors (was page-wide); checkbox/details states listen to `change`/`toggle`; raw hex alpha (`#ff0000/50` -> color-mix); bare numbers on time properties are ms; `::part()` supported, `::slotted()` rejected with an explanation; SVG properties (`fill`, `stroke*`) and `text-wrap`/`content-visibility` in the vocabulary.
|
| 37 |
- Warnings channel (v0.7): things that compile but are probably wrong, e.g. a layout shorthand expanding to `display` and clobbering an earlier explicit `display:` in the same rule.
|
|
@@ -62,4 +62,4 @@ Wait for the `dssl-ready` event on `window` before measuring layout: the engine
|
|
| 62 |
|
| 63 |
## Editor support
|
| 64 |
|
| 65 |
-
The official extension (VS Code and VSCodium, same .vsix) bundles the real parser and resolver: diagnostics identical to the browser console, completions from the workspace token registry, hover docs for the semantic vocabulary. Install `dssl-vscode/dssl-vscode-0.8.
|
|
|
|
| 1 |
# DSSL
|
| 2 |
|
| 3 |
+
> DSSL is a style language for the web. For JS-guaranteed pages it replaces writing CSS; it does not replace the CSS platform. You write `.dssl` files, the page loads the engine (about 58 kB gzipped, about 190 kB as plain source), and the engine walks the DOM in real time applying every matched rule as inline styles. Nothing compiles to CSS at runtime, but v0.8.0 adds the exit lane: `dssl build` compiles the static subset to a real .css file for no-JS, strict-CSP or size-critical pages. Elements added later are styled the moment they appear; signals, states and motion share one grammar.
|
| 4 |
|
| 5 |
- Site (docs, playground, changelog, bench): https://dssl-sable.vercel.app
|
| 6 |
- Repo + full docs in the README: https://huggingface.co/EHS-Studio/dssl
|
| 7 |
- Install: `curl -fsSL https://dssl-sable.vercel.app/install.sh | sh` (or the zip: https://dssl-sable.vercel.app/dssl-engine.zip)
|
| 8 |
+
- Version: 0.8.2 (engine), MIT license
|
| 9 |
- Feature matrix vs CSS: see PARITY.md in the repo
|
| 10 |
- For agents working ON the codebase: see AGENTS.md in the repo
|
| 11 |
|
| 12 |
## The five things reviewers ask about
|
| 13 |
|
| 14 |
1. No build step. Styles are source files linked with `<link rel="stylesheet/dssl">` or embedded in the page as `<style type="text/dssl">` / `<script type="text/dssl">` blocks (auto-booted in document order). The engine restyles the live DOM, including nodes added later.
|
| 15 |
+
2. Honest trade: it is a JS runtime. If JavaScript is unavailable the page renders unstyled, unless you build the static subset: `dssl build styles.dssl -o styles.css` emits real CSS (tokens → custom properties, states → :hover, layers → @layer, motions → @keyframes, `raw { }` verbatim, `seen` applied as its no-JS final state) and the report counts what stays runtime-only (`@signal` rules, `animate:`). For content pages that must not flash, use the build output or choose another tool.
|
| 16 |
3. Exit cost is zero. Abandoning DSSL leaves you a page of valid inline styles and one small shadow stylesheet for pseudo-elements. There is nothing to un-install from your markup.
|
| 17 |
4. Speed has a handbrake. `DSSL.freeze()` detaches every observer and listener after first paint and leaves the resolved look at zero engine cost; `unfreeze()` re-arms. A gate prefilter skips obvious non-matches before native matching. The Bench page on the site measures 100 / 1,000 / 10,000 nodes on your machine: CSS `<style>` vs DSSL live vs DSSL frozen.
|
| 18 |
5. The cascade is (layer, source order). No specificity, no !important. Nested state blocks sort after the parent rule's own declarations (v0.7.2 fix: they used to lose), and nested `@media`/`@signal`/`@layer` keep their condition. `pin { }` is the only must-win block. The compiler refuses to guess: unknown tokens, properties and values fail loudly with did-you-mean; `--*` custom properties pass through unvalidated on purpose.
|
|
|
|
| 31 |
- Overlap policy: nothing below 100% opacity overlaps anything unless `overlap: allow`; `deny` is unconditional; pairs with `pointer-events: none` so veils stop eating clicks and text selection.
|
| 32 |
- Behaviors: named rule groups applied with `use focus-ring`.
|
| 33 |
- Verbatim escape hatch (v0.8.0): `raw { }` / `@css { }` drops real CSS (@font-face, @keyframes, @supports, @import, @property) into the page stylesheet untouched; unknown at-rules skip their whole block with one clean error. Inside raw { } semicolons are required (it is real CSS); a missing one is a warning with a line number.
|
| 34 |
+
- Fluid sugar (v0.8.0): `font-size: fluid 16 24` (and any length-ish property) compiles to the linear clamp() between 360px and 1200px viewports (exactly the first size at 360px and the second at 1200px; px, rem or em, no percentages; shrinking sizes work); `fluid: 16, 24` still works; it also works on the longhands (`padding-top`, `margin-block`, `letter-spacing`, `border-radius`) since 0.8.2. 0.8.2 also made the cascade global: a stylesheet loaded later always beats an earlier one, and a later `tokens { }` block overrides an earlier conditional token. 0.8.1 fixed the math: 0.8.0 left out the intercept, so sizes were too large on small screens.
|
| 35 |
- v0.8.0 review-driven fixes, all regression-tested incl. headless-Chromium computed-style tests of the docs: row/column axis roles (the documented `between center` example laid out wrong before); @font-face no longer crashes the compiler (unknown at-rules skip balanced); @supports inner rules no longer leak unconditionally; `url(...)` is atomic (unquoted form no longer false-errors); `!important` is a loud error that emits nothing; the animation shorthand warns when its motion is undefined instead of suggesting "span"; CSS-order transition values get the exact DSSL conversion in the error hint.
|
| 36 |
- v0.7.2 review-driven fixes, all regression-tested: gate prefilter runs before native matches(); hover state events scope candidate recomputation to the touched subtree plus ancestors (was page-wide); checkbox/details states listen to `change`/`toggle`; raw hex alpha (`#ff0000/50` -> color-mix); bare numbers on time properties are ms; `::part()` supported, `::slotted()` rejected with an explanation; SVG properties (`fill`, `stroke*`) and `text-wrap`/`content-visibility` in the vocabulary.
|
| 37 |
- Warnings channel (v0.7): things that compile but are probably wrong, e.g. a layout shorthand expanding to `display` and clobbering an earlier explicit `display:` in the same rule.
|
|
|
|
| 62 |
|
| 63 |
## Editor support
|
| 64 |
|
| 65 |
+
The official extension (VS Code and VSCodium, same .vsix) bundles the real parser and resolver: diagnostics identical to the browser console, completions from the workspace token registry, hover docs for the semantic vocabulary. Install `dssl-vscode/dssl-vscode-0.8.2.vsix` from the repo files. Source lives in `dssl-vscode/` in the repo.
|
|
@@ -0,0 +1,116 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* test-v082.mjs: one assertion per change in 0.8.2.
|
| 2 |
+
* Found while rebuilding the site on its own engine:
|
| 3 |
+
* 1. "later wins" did not hold BETWEEN stylesheets. Each sheet numbers its
|
| 4 |
+
* rules from 0 and the cascade compared those numbers across sheets, so a
|
| 5 |
+
* later sheet lost to an earlier one that simply had more rules above the
|
| 6 |
+
* rule in question. Fixed by giving every rule one global position.
|
| 7 |
+
* 2. a later tokens { } block could not override a token an earlier sheet
|
| 8 |
+
* set inside @media or @signal (same cause).
|
| 9 |
+
* 3. fluid did not work on padding-top, margin-block and the other longhands.
|
| 10 |
+
* The cascade tests use the engine's own sort (layer rank, then order) on rules
|
| 11 |
+
* from real compiled sheets, so they check the thing that was wrong. */
|
| 12 |
+
import { assignGlobalOrder } from '../public/dssl/engine.js';
|
| 13 |
+
import { compile } from '../public/dssl/index.js';
|
| 14 |
+
|
| 15 |
+
let pass = 0, fail = 0;
|
| 16 |
+
const ok = (name, cond, extra) => {
|
| 17 |
+
if (cond) { pass++; console.log(` ok ${name}`); }
|
| 18 |
+
else { fail++; console.log(`FAIL ${name}${extra ? '; ' + extra : ''}`); }
|
| 19 |
+
};
|
| 20 |
+
|
| 21 |
+
const LAYER_RANK = { tokens: 0, base: 1, components: 2, utilities: 3, pin: 4 };
|
| 22 |
+
const cascade = (rules) => [...rules].sort((a, b) => (LAYER_RANK[a.layer] ?? 2) - (LAYER_RANK[b.layer] ?? 2) || a.order - b.order);
|
| 23 |
+
const bundleOf = (src) => { const { doc } = compile(src, 't.dssl'); return { rules: doc.rules, shadowRules: doc.shadowRules, raws: doc.raws || [] }; };
|
| 24 |
+
const winner = (rules, sel) => cascade(rules.filter((r) => r.sel === sel)).pop();
|
| 25 |
+
const val = (rule, prop) => rule.decls.filter((d) => d.prop === prop).pop()?.value;
|
| 26 |
+
|
| 27 |
+
/* ---- 1: later sheets win ---- */
|
| 28 |
+
{
|
| 29 |
+
let filler = '';
|
| 30 |
+
for (let i = 0; i < 30; i++) filler += `.zz${i} { color: green }\n`;
|
| 31 |
+
const first = bundleOf(filler + '.x { color: red }');
|
| 32 |
+
const second = bundleOf('.x { color: blue }');
|
| 33 |
+
const before = winner([...first.rules, ...second.rules], '.x');
|
| 34 |
+
ok('1a. the bug: without a global order the EARLIER sheet wins', val(before, 'color') === 'red', val(before, 'color'));
|
| 35 |
+
|
| 36 |
+
const a = bundleOf(filler + '.x { color: red }');
|
| 37 |
+
const b = bundleOf('.x { color: blue }');
|
| 38 |
+
assignGlobalOrder([a, b]);
|
| 39 |
+
const after = winner([...a.rules, ...b.rules], '.x');
|
| 40 |
+
ok('1b. with a global order the LATER sheet wins', val(after, 'color') === 'blue', val(after, 'color'));
|
| 41 |
+
|
| 42 |
+
const lastOfFirst = Math.max(...a.rules.map((r) => r.order));
|
| 43 |
+
const firstOfSecond = Math.min(...b.rules.map((r) => r.order));
|
| 44 |
+
ok('1c. every rule of a later sheet sorts after every rule of an earlier one', firstOfSecond > lastOfFirst, `${firstOfSecond} vs ${lastOfFirst}`);
|
| 45 |
+
|
| 46 |
+
const snapshot = [...a.rules, ...b.rules].map((r) => r.order).join(',');
|
| 47 |
+
assignGlobalOrder([a, b]);
|
| 48 |
+
assignGlobalOrder([a, b]);
|
| 49 |
+
ok('1d. it is idempotent: rebuilding does not make the numbers drift', [...a.rules, ...b.rules].map((r) => r.order).join(',') === snapshot);
|
| 50 |
+
|
| 51 |
+
// replacing a sheet in place: new rules get numbered from the same slot
|
| 52 |
+
const b2 = bundleOf('.x { color: purple }\n.y { color: teal }');
|
| 53 |
+
assignGlobalOrder([a, b2]);
|
| 54 |
+
ok('1e. a replaced sheet still sorts after the sheet before it', val(winner([...a.rules, ...b2.rules], '.x'), 'color') === 'purple');
|
| 55 |
+
|
| 56 |
+
// three sheets in a row
|
| 57 |
+
const c = bundleOf('.x { color: orange }');
|
| 58 |
+
assignGlobalOrder([a, b2, c]);
|
| 59 |
+
ok('1f. with three sheets the last one wins', val(winner([...a.rules, ...b2.rules, ...c.rules], '.x'), 'color') === 'orange');
|
| 60 |
+
|
| 61 |
+
// within one sheet nothing changed: the later line still wins
|
| 62 |
+
const one = bundleOf('.x { color: red }\n.x { color: green }');
|
| 63 |
+
assignGlobalOrder([one]);
|
| 64 |
+
ok('1g. inside one sheet the later rule still wins', val(winner(one.rules, '.x'), 'color') === 'green');
|
| 65 |
+
|
| 66 |
+
// layers still outrank order: a utilities rule in an EARLY sheet beats a components rule in a late one
|
| 67 |
+
const early = bundleOf('@layer utilities { .x { margin-top: 0 } }');
|
| 68 |
+
const late = bundleOf('.x { margin-top: 30 }');
|
| 69 |
+
assignGlobalOrder([early, late]);
|
| 70 |
+
ok('1h. layers still outrank load order', val(winner([...early.rules, ...late.rules], '.x'), 'margin-top') === '0px');
|
| 71 |
+
|
| 72 |
+
// shadow (pseudo-element) rules and raw blocks are renumbered with their sheet
|
| 73 |
+
const sh1 = bundleOf('.a::before { content: "a" }');
|
| 74 |
+
const sh2 = bundleOf('.a::before { content: "b" }');
|
| 75 |
+
assignGlobalOrder([sh1, sh2]);
|
| 76 |
+
ok('1i. pseudo-element rules follow the same global order', sh2.shadowRules[0].order > sh1.shadowRules[0].order);
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
/* ---- 2: a later tokens block beats an earlier conditional token ---- */
|
| 80 |
+
{
|
| 81 |
+
const palettes = bundleOf(`tokens { color { accent: red } }
|
| 82 |
+
@media (prefers-color-scheme: dark) { tokens { color { accent: pink } } }`);
|
| 83 |
+
const later = bundleOf('tokens { color { accent: blue } }');
|
| 84 |
+
const tokenRules = (b) => b.rules.filter((r) => r.layer === 'tokens' && r.decls.some((d) => d.prop === '--dssl-color-accent'));
|
| 85 |
+
const before = cascade([...tokenRules(palettes), ...tokenRules(later)]).pop();
|
| 86 |
+
ok('2a. the bug: an earlier conditional token out-ranked a later tokens block', before.decls.some((d) => d.value === 'pink'));
|
| 87 |
+
|
| 88 |
+
const p = bundleOf(`tokens { color { accent: red } }
|
| 89 |
+
@media (prefers-color-scheme: dark) { tokens { color { accent: pink } } }`);
|
| 90 |
+
const l = bundleOf('tokens { color { accent: blue } }');
|
| 91 |
+
assignGlobalOrder([p, l]);
|
| 92 |
+
const after = cascade([...tokenRules(p), ...tokenRules(l)]).pop();
|
| 93 |
+
ok('2b. now the later tokens block wins, even in dark mode', after.decls.some((d) => d.value === 'blue'));
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
+
/* ---- 3: fluid on the longhands ---- */
|
| 97 |
+
{
|
| 98 |
+
const evalClamp = (css, vw) => {
|
| 99 |
+
const m = /^clamp\(\s*(-?[\d.]+)px,\s*calc\(\s*(-?[\d.]+)px\s*\+\s*(-?[\d.]+)vw\s*\),\s*(-?[\d.]+)px\s*\)$/.exec(css);
|
| 100 |
+
if (!m) return NaN;
|
| 101 |
+
return Math.min(+m[4], Math.max(+m[1], +m[2] + (+m[3] * vw) / 100));
|
| 102 |
+
};
|
| 103 |
+
const near = (x, y) => Math.abs(x - y) < 0.01;
|
| 104 |
+
for (const prop of ['padding-top', 'padding-bottom', 'padding-left', 'padding-right', 'padding-inline', 'padding-block', 'margin-top', 'margin-bottom', 'margin-left', 'margin-right', 'margin-inline', 'margin-block', 'letter-spacing', 'border-radius']) {
|
| 105 |
+
const { doc, ok: good } = compile(`.a { ${prop}: fluid 8 24 }`);
|
| 106 |
+
const v = doc.rules[0]?.decls.find((d) => d.prop === prop)?.value;
|
| 107 |
+
ok(`3. ${prop}: fluid 8 24 is 8px at 360 and 24px at 1200`, good && near(evalClamp(v, 360), 8) && near(evalClamp(v, 1200), 24), v);
|
| 108 |
+
}
|
| 109 |
+
const still = compile('.a { padding: fluid 8 16 }');
|
| 110 |
+
ok('3b. the shorthand still works', still.ok && /^clamp\(/.test(still.doc.rules[0].decls[0].value));
|
| 111 |
+
const nope = compile('.a { opacity: fluid 0 1 }');
|
| 112 |
+
ok('3c. fluid on a property that cannot take it is still an error, not silence', !nope.ok);
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
console.log(`\n${pass} passed, ${fail} failed`);
|
| 116 |
+
process.exit(fail ? 1 : 0);
|
|
@@ -1,5 +0,0 @@
|
|
| 1 |
-
import { NextResponse } from "next/server";
|
| 2 |
-
|
| 3 |
-
export async function GET() {
|
| 4 |
-
return NextResponse.json({ message: "Hello, world!" });
|
| 5 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|