v0.8.1: fluid math, raw blocks, packaged CLI, build --strict
Browse filesA re-test of 0.8.0 found two bugs; writing the tests found more.
- fluid: the clamp() left out the intercept of its line, so fluid 16 24 was
19.6px on a 375px phone (should be 16.1px) and hit 24px near 840px instead
of 1200px. Same fix repairs shrinking sizes (fluid 24 16 made a backwards
clamp) and rem sizes (slope was 16x too small). Percentages and mixed
units are refused with a clear message.
- raw { }: a multi-line block started with the text "undefined", anything on
the same line as "raw {" was dropped, and the guide example had no
semicolons (invalid CSS). Fixed; a missing semicolon inside raw is now a
warning with the right line number.
- the dssl CLI inside dssl-engine.zip / .tar.gz crashed on start (looked for
public/dssl, the package keeps the engine in dssl/). Finds either layout now.
- dssl build takes --strict, like dssl check.
- tests: new scripts/test-v081.mjs (37 assertions) evaluates the emitted
clamp() at real window widths instead of comparing strings; 24 of them fail
against the 0.8.0 engine. test-guide-examples.mjs no longer reports a green
"0 passed" when its hardcoded /home/z paths are missing.
- docs, changelog, PARITY and llms.txt updated; packages and the 0.8.1 vsix
rebuilt from the synced engine.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
- GUIDE.md +43 -7
- PARITY.md +19 -0
- README.md +9 -7
- cli/dssl.mjs +16 -10
- dssl-vscode/{dssl-vscode-0.8.0.vsix → dssl-vscode-0.8.1.vsix} +0 -0
- dssl-vscode/lib/engine/parser.js +6 -0
- dssl-vscode/lib/engine/resolver.js +47 -12
- dssl-vscode/package.json +1 -1
- engine-package/USAGE.md +5 -4
- llms.txt +4 -4
- public/dssl-engine.tar.gz +0 -0
- public/dssl-engine.zip +0 -0
- public/dssl/index.js +1 -1
- public/dssl/parser.js +6 -0
- public/dssl/resolver.js +47 -12
- public/llms.txt +4 -4
- scripts/build-engine-package.sh +5 -2
- scripts/test-guide-browser.mjs +2 -1
- scripts/test-guide-examples.mjs +11 -3
- scripts/test-v073.mjs +6 -5
- scripts/test-v081.mjs +175 -0
- src/components/dssl/views/ChangelogView.tsx +23 -0
- src/components/dssl/views/DocsView.tsx +1 -1
|
@@ -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.
|
| 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
|
|
@@ -123,9 +123,33 @@ 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 |
-
|
| 128 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 129 |
|
| 130 |
### raw { } — the escape hatch
|
| 131 |
|
|
@@ -136,12 +160,22 @@ page stylesheet, top-level only:
|
|
| 136 |
```dssl
|
| 137 |
raw {
|
| 138 |
@font-face {
|
| 139 |
-
font-family: Inter
|
| 140 |
-
src: url(/fonts/inter.woff2)
|
| 141 |
}
|
| 142 |
}
|
| 143 |
```
|
| 144 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 145 |
Unknown at-rules outside `raw { }` are skipped whole, with one clean error
|
| 146 |
and a pointer here — they never leak their inner rules (that was a real
|
| 147 |
0.7.2 bug: `@supports (display:grid) { .a { … } }` applied `.a` everywhere
|
|
@@ -249,7 +283,7 @@ for liveness. `unfreeze()` re-arms everything.
|
|
| 249 |
|
| 250 |
Three ways to get it:
|
| 251 |
|
| 252 |
-
1. Download `dssl-vscode/dssl-vscode-0.8.
|
| 253 |
Extensions panel, `...`, Install from VSIX. Works in VS Code and VSCodium.
|
| 254 |
2. Build it: `cd dssl-vscode && npx @vscode/vsce package --no-dependencies`.
|
| 255 |
3. Registry listing: staged for Open VSX (the registry VSCodium searches).
|
|
@@ -267,6 +301,7 @@ v0.8.0 answers the honest limits below without giving up the language:
|
|
| 267 |
```sh
|
| 268 |
node cli/dssl.mjs check styles.dssl --strict # CI: real compiler, exit 1 on errors, --json
|
| 269 |
node cli/dssl.mjs build styles.dssl -o styles.css
|
|
|
|
| 270 |
```
|
| 271 |
|
| 272 |
`build` compiles the static subset to a real stylesheet: tokens become
|
|
@@ -281,6 +316,7 @@ The same output is available in the browser as `DSSL.toCSS(source)`.
|
|
| 281 |
|
| 282 |
```sh
|
| 283 |
node cli/dssl.mjs check public/styles/*.dssl # compile everything, exit code for CI
|
|
|
|
| 284 |
node scripts/test-v073.mjs # 0.8.0 regression suite (65 assertions)
|
| 285 |
node scripts/test-guide-examples.mjs # every guide example compiles
|
| 286 |
node scripts/test-guide-browser.mjs # guide examples asserted in headless Chromium
|
|
|
|
| 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.1, 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
|
|
|
|
| 123 |
### Sizing that moves: fluid
|
| 124 |
|
| 125 |
`font-size: fluid 16 24` (and any length-ish property: width, height, gap,
|
| 126 |
+
padding, margin and friends) makes a size that grows smoothly with the window.
|
| 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 |
+
|
| 130 |
+
```dssl
|
| 131 |
+
h1 { font-size: fluid 28 44 } // 28px on a small phone, 44px on a big desktop
|
| 132 |
+
.stack { gap: fluid 8 16 } // works on any length property
|
| 133 |
+
.hero { font-size: fluid 16 24 400 800 } // your own viewport range
|
| 134 |
+
.old { fluid: 16, 24 } // the comma form still works
|
| 135 |
+
```
|
| 136 |
+
|
| 137 |
+
What you get at a few window widths for `fluid 16 24`:
|
| 138 |
+
|
| 139 |
+
| window | 360 | 375 | 600 | 800 | 1200 |
|
| 140 |
+
|---|---|---|---|---|---|
|
| 141 |
+
| size | 16px | 16.1px | 18.3px | 20.2px | 24px |
|
| 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.
|
| 148 |
+
- Going down works too: `fluid 24 16` shrinks as the window grows.
|
| 149 |
+
- If you used `fluid` in 0.8.0, your text will be a little smaller on phones and
|
| 150 |
+
tablets after updating. That is the fix, not a regression. 0.8.0 had the
|
| 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 { } — the escape hatch
|
| 155 |
|
|
|
|
| 160 |
```dssl
|
| 161 |
raw {
|
| 162 |
@font-face {
|
| 163 |
+
font-family: Inter;
|
| 164 |
+
src: url(/fonts/inter.woff2);
|
| 165 |
}
|
| 166 |
}
|
| 167 |
```
|
| 168 |
|
| 169 |
+
One thing to remember: inside `raw { }` you are writing real CSS, so semicolons
|
| 170 |
+
are required there, unlike everywhere else in DSSL. If you leave one out, the
|
| 171 |
+
browser would quietly throw the whole rule away. DSSL cannot fix that for you
|
| 172 |
+
(the text is passed through untouched on purpose), but it does warn you with the
|
| 173 |
+
line number:
|
| 174 |
+
|
| 175 |
+
```
|
| 176 |
+
styles.dssl:3 warning: raw { } is verbatim CSS, so this line needs a ";" at the end
|
| 177 |
+
```
|
| 178 |
+
|
| 179 |
Unknown at-rules outside `raw { }` are skipped whole, with one clean error
|
| 180 |
and a pointer here — they never leak their inner rules (that was a real
|
| 181 |
0.7.2 bug: `@supports (display:grid) { .a { … } }` applied `.a` everywhere
|
|
|
|
| 283 |
|
| 284 |
Three ways to get it:
|
| 285 |
|
| 286 |
+
1. Download `dssl-vscode/dssl-vscode-0.8.1.vsix` from the repo files, then
|
| 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).
|
|
|
|
| 301 |
```sh
|
| 302 |
node cli/dssl.mjs check styles.dssl --strict # CI: real compiler, exit 1 on errors, --json
|
| 303 |
node cli/dssl.mjs build styles.dssl -o styles.css
|
| 304 |
+
node cli/dssl.mjs build styles.dssl -o styles.css --strict # CI: warnings fail the build too
|
| 305 |
```
|
| 306 |
|
| 307 |
`build` compiles the static subset to a real stylesheet: tokens become
|
|
|
|
| 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
|
| 322 |
node scripts/test-guide-browser.mjs # guide examples asserted in headless Chromium
|
|
@@ -57,6 +57,25 @@ 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.0 review-driven corrections
|
| 61 |
|
| 62 |
The second external review (repro snippets, headless browser) found five bugs and
|
|
|
|
| 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:
|
| 63 |
+
|
| 64 |
+
- `fluid` left out the intercept of its line, so `fluid 16 24` was 19.6px on a
|
| 65 |
+
375px phone instead of 16.1px and reached 24px at about 840px instead of 1200px.
|
| 66 |
+
Fixed, and the tests now evaluate the emitted `clamp()` at real window widths.
|
| 67 |
+
The same fix repaired shrinking sizes (`fluid 24 16`, which produced a backwards
|
| 68 |
+
`clamp()`) and rem sizes (the slope was 16 times too small). Percentages and
|
| 69 |
+
mixed units are now refused with a message instead of producing nonsense.
|
| 70 |
+
- A multi-line `raw { }` started with the text `undefined`, and anything written
|
| 71 |
+
on the same line as `raw {` was dropped. The guide's own example also had no
|
| 72 |
+
semicolons, which is not valid CSS. All three fixed, and a missing semicolon
|
| 73 |
+
inside `raw { }` is now a warning with the right line number.
|
| 74 |
+
- `dssl build` now takes `--strict`, like `dssl check`.
|
| 75 |
+
- The `dssl` command in the downloadable package crashed on start (module not found),
|
| 76 |
+
because it looked for the engine in `public/dssl/` and the package keeps it in
|
| 77 |
+
`dssl/`. It now finds it in either place, and a test runs it from the package layout.
|
| 78 |
+
|
| 79 |
## v0.8.0 review-driven corrections
|
| 80 |
|
| 81 |
The second external review (repro snippets, headless browser) found five bugs and
|
|
@@ -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 |
|
|
@@ -387,9 +387,11 @@ overlap) stays out and gets counted in the report, so the boundary is explicit
|
|
| 387 |
instead of folklore.
|
| 388 |
|
| 389 |
Two more tools from v0.8.0: `raw { }` (alias `@css { }`) drops verbatim CSS
|
| 390 |
-
like `@font-face` or `@keyframes` into the page stylesheet,
|
| 391 |
-
|
| 392 |
-
|
|
|
|
|
|
|
| 393 |
|
| 394 |
## Performance: freeze, explain, and the cost of live
|
| 395 |
|
|
@@ -420,7 +422,7 @@ const rep = DSSL.explain(document.querySelector('.doc-card'))
|
|
| 420 |
|
| 421 |
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.
|
| 422 |
|
| 423 |
-
## What works today (v0.8.
|
| 424 |
|
| 425 |
- the full selector surface, including `:nth-child`, `:not`, `:is`, `:where`, `:has`
|
| 426 |
- 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
|
|
@@ -434,7 +436,7 @@ The matcher also pre-filters every rule through a cheap class/tag/id gate extrac
|
|
| 434 |
- pseudo-elements in any spelling (shadow CSS), themed scrollbars included
|
| 435 |
- `raw { }` / `@css { }` for verbatim CSS (`@font-face`, `@keyframes`, `@supports`); unknown at-rules skip their whole block with one clean error
|
| 436 |
- `fluid 16 24` sizing sugar on any length property
|
| 437 |
-
- the CLI: `dssl check` (CI, --json) and `dssl build` (real CSS from the static subset)
|
| 438 |
- compiler warnings for cascade-order footguns and duplicate axis words
|
| 439 |
- `DSSL.freeze()` / `DSSL.unfreeze()` / `DSSL.explain()` / `DSSL.toCSS()`
|
| 440 |
- bare font names, `hyphens: manual`, loud did-you-mean errors
|
|
@@ -455,7 +457,7 @@ The matcher also pre-filters every rule through a cheap class/tag/id gate extrac
|
|
| 455 |
|
| 456 |
## Status
|
| 457 |
|
| 458 |
-
v0.8.
|
| 459 |
|
| 460 |
## License
|
| 461 |
|
|
|
|
| 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.1.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 |
|
|
|
|
| 387 |
instead of folklore.
|
| 388 |
|
| 389 |
Two more tools from v0.8.0: `raw { }` (alias `@css { }`) drops verbatim CSS
|
| 390 |
+
like `@font-face` or `@keyframes` into the page stylesheet (it is real CSS, so
|
| 391 |
+
semicolons are required inside it, and DSSL warns if you forget one), and
|
| 392 |
+
`font-size: fluid 16 24` gives you a size that grows smoothly from 16px on a
|
| 393 |
+
360px wide window to 24px on a 1200px wide one. `dssl build --strict` makes
|
| 394 |
+
warnings fail the build, for CI.
|
| 395 |
|
| 396 |
## Performance: freeze, explain, and the cost of live
|
| 397 |
|
|
|
|
| 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.1)
|
| 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
|
|
|
|
| 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()`
|
| 442 |
- bare font names, `hyphens: manual`, loud did-you-mean errors
|
|
|
|
| 457 |
|
| 458 |
## Status
|
| 459 |
|
| 460 |
+
v0.8.1, 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.
|
| 461 |
|
| 462 |
## License
|
| 463 |
|
|
@@ -8,7 +8,7 @@
|
|
| 8 |
* Exit 1 on errors, 2 on usage problems. --strict makes warnings fatal.
|
| 9 |
* --json emits machine-readable diagnostics for CI and editors.
|
| 10 |
*
|
| 11 |
-
* dssl build styles.dssl [-o out.css] [--json] [--no-banner]
|
| 12 |
* The exit lane: compile the static subset to a real .css file.
|
| 13 |
* Tokens become custom properties, states become :hover, layers become
|
| 14 |
* @layer, motions become @keyframes, raw { } passes through verbatim.
|
|
@@ -18,16 +18,20 @@
|
|
| 18 |
* Files are compiled in the order given; every bundle's tokens fold into the
|
| 19 |
* next (name a tokens.dssl first, exactly like <link> order in the browser).
|
| 20 |
* ========================================================================= */
|
| 21 |
-
import { readFileSync, writeFileSync, statSync } from 'node:fs';
|
| 22 |
import { dirname, resolve as pathResolve, basename, join } from 'node:path';
|
| 23 |
import { fileURLToPath } from 'node:url';
|
| 24 |
|
| 25 |
-
// the engine is plain ESM
|
|
|
|
|
|
|
|
|
|
| 26 |
const here = pathResolve(dirname(fileURLToPath(import.meta.url)), '..');
|
| 27 |
-
const
|
| 28 |
-
const {
|
| 29 |
-
const {
|
| 30 |
-
const {
|
|
|
|
| 31 |
|
| 32 |
const USAGE = `dssl v${VERSION} — the DSSL style language
|
| 33 |
|
|
@@ -38,6 +42,7 @@ const USAGE = `dssl v${VERSION} — the DSSL style language
|
|
| 38 |
dssl build <file> compile the static subset to a real .css file
|
| 39 |
-o <out.css> output path (default: <file>.css)
|
| 40 |
--json machine-readable report
|
|
|
|
| 41 |
--no-banner omit the generated-file header
|
| 42 |
dssl --version print the version`;
|
| 43 |
|
|
@@ -137,7 +142,8 @@ if (cmd === 'check') {
|
|
| 137 |
if (cmd === 'build') {
|
| 138 |
const json = args.includes('--json');
|
| 139 |
const banner = !args.includes('--no-banner');
|
| 140 |
-
|
|
|
|
| 141 |
const outPath = readArg(args, '-o') || readArg(args, '--out');
|
| 142 |
if (!args.length) { console.error('dssl build: give me a .dssl file to build'); process.exit(2); }
|
| 143 |
const input = args[0];
|
|
@@ -150,7 +156,7 @@ if (cmd === 'build') {
|
|
| 150 |
version: VERSION,
|
| 151 |
input: r.file,
|
| 152 |
output: outputPath,
|
| 153 |
-
ok: r.errors.length === 0,
|
| 154 |
errors: r.errors.map(e => ({ file: e.file || r.name, line: e.line ?? 0, message: e.message, hint: e.hint || null })),
|
| 155 |
warnings: r.warnings.map(w => ({ file: w.file || r.name, line: w.line ?? 0, message: w.message, hint: w.hint || null })),
|
| 156 |
report: built.report,
|
|
@@ -168,7 +174,7 @@ if (cmd === 'build') {
|
|
| 168 |
}
|
| 169 |
printDiags(results, true);
|
| 170 |
}
|
| 171 |
-
process.exit(r.errors.length ? 1 : 0);
|
| 172 |
}
|
| 173 |
|
| 174 |
console.error(`dssl: unknown command "${cmd}"\n\n${USAGE}`);
|
|
|
|
| 8 |
* Exit 1 on errors, 2 on usage problems. --strict makes warnings fatal.
|
| 9 |
* --json emits machine-readable diagnostics for CI and editors.
|
| 10 |
*
|
| 11 |
+
* dssl build styles.dssl [-o out.css] [--json] [--strict] [--no-banner]
|
| 12 |
* The exit lane: compile the static subset to a real .css file.
|
| 13 |
* Tokens become custom properties, states become :hover, layers become
|
| 14 |
* @layer, motions become @keyframes, raw { } passes through verbatim.
|
|
|
|
| 18 |
* Files are compiled in the order given; every bundle's tokens fold into the
|
| 19 |
* next (name a tokens.dssl first, exactly like <link> order in the browser).
|
| 20 |
* ========================================================================= */
|
| 21 |
+
import { readFileSync, writeFileSync, statSync, existsSync } from 'node:fs';
|
| 22 |
import { dirname, resolve as pathResolve, basename, join } from 'node:path';
|
| 23 |
import { fileURLToPath } from 'node:url';
|
| 24 |
|
| 25 |
+
// the engine is plain ESM, import it directly, no build step. In a repo checkout
|
| 26 |
+
// it lives in public/dssl/. In the downloaded package (dssl-engine.zip) it sits
|
| 27 |
+
// next to cli/ as dssl/. 0.8.0 only knew the first layout, so the packaged CLI
|
| 28 |
+
// crashed with "module not found" for everyone who installed it.
|
| 29 |
const here = pathResolve(dirname(fileURLToPath(import.meta.url)), '..');
|
| 30 |
+
const engineDir = existsSync(join(here, 'public/dssl/parser.js')) ? join(here, 'public/dssl') : join(here, 'dssl');
|
| 31 |
+
const { parse } = await import('file://' + join(engineDir, 'parser.js'));
|
| 32 |
+
const { resolve } = await import('file://' + join(engineDir, 'resolver.js'));
|
| 33 |
+
const { toCSS } = await import('file://' + join(engineDir, 'static.js'));
|
| 34 |
+
const { VERSION } = await import('file://' + join(engineDir, 'index.js'));
|
| 35 |
|
| 36 |
const USAGE = `dssl v${VERSION} — the DSSL style language
|
| 37 |
|
|
|
|
| 42 |
dssl build <file> compile the static subset to a real .css file
|
| 43 |
-o <out.css> output path (default: <file>.css)
|
| 44 |
--json machine-readable report
|
| 45 |
+
--strict warnings are errors too (exit 1)
|
| 46 |
--no-banner omit the generated-file header
|
| 47 |
dssl --version print the version`;
|
| 48 |
|
|
|
|
| 142 |
if (cmd === 'build') {
|
| 143 |
const json = args.includes('--json');
|
| 144 |
const banner = !args.includes('--no-banner');
|
| 145 |
+
const strict = args.includes('--strict');
|
| 146 |
+
for (const f of ['--json', '--no-banner', '--strict']) { const i = args.indexOf(f); if (i !== -1) args.splice(i, 1); }
|
| 147 |
const outPath = readArg(args, '-o') || readArg(args, '--out');
|
| 148 |
if (!args.length) { console.error('dssl build: give me a .dssl file to build'); process.exit(2); }
|
| 149 |
const input = args[0];
|
|
|
|
| 156 |
version: VERSION,
|
| 157 |
input: r.file,
|
| 158 |
output: outputPath,
|
| 159 |
+
ok: r.errors.length === 0 && (!strict || r.warnings.length === 0),
|
| 160 |
errors: r.errors.map(e => ({ file: e.file || r.name, line: e.line ?? 0, message: e.message, hint: e.hint || null })),
|
| 161 |
warnings: r.warnings.map(w => ({ file: w.file || r.name, line: w.line ?? 0, message: w.message, hint: w.hint || null })),
|
| 162 |
report: built.report,
|
|
|
|
| 174 |
}
|
| 175 |
printDiags(results, true);
|
| 176 |
}
|
| 177 |
+
process.exit(r.errors.length || (strict && r.warnings.length) ? 1 : 0);
|
| 178 |
}
|
| 179 |
|
| 180 |
console.error(`dssl: unknown command "${cmd}"\n\n${USAGE}`);
|
|
Binary files a/dssl-vscode/dssl-vscode-0.8.0.vsix and b/dssl-vscode/dssl-vscode-0.8.1.vsix differ
|
|
|
|
@@ -255,7 +255,13 @@ export function parse(source, file = 'inline.dssl') {
|
|
| 255 |
node.css = rest.slice(open + 1, close);
|
| 256 |
return rest.slice(close + 1);
|
| 257 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 258 |
rawCapture = { node, depth: 1 };
|
|
|
|
| 259 |
return '';
|
| 260 |
}
|
| 261 |
if (meta.kind === 'bogus') {
|
|
|
|
| 255 |
node.css = rest.slice(open + 1, close);
|
| 256 |
return rest.slice(close + 1);
|
| 257 |
}
|
| 258 |
+
// multi-line: start the body empty (it used to start undefined, which
|
| 259 |
+
// put a literal "undefined" in front of the CSS) and feed whatever
|
| 260 |
+
// followed the "{" on this same line, so `raw { @font-face {` keeps
|
| 261 |
+
// its first line.
|
| 262 |
+
node.css = '';
|
| 263 |
rawCapture = { node, depth: 1 };
|
| 264 |
+
feedRawCapture(rest.slice(open + 1), line);
|
| 265 |
return '';
|
| 266 |
}
|
| 267 |
if (meta.kind === 'bogus') {
|
|
@@ -949,19 +949,38 @@ const FLUID_PROPS = new Set(('font-size,width,height,inline-size,block-size,min-
|
|
| 949 |
'gap,row-gap,column-gap,padding,margin,inset,top,right,bottom,left').split(','));
|
| 950 |
|
| 951 |
function buildFluidParts(parts, ctx, prop = 'font-size') {
|
| 952 |
-
const
|
| 953 |
-
|
| 954 |
-
|
| 955 |
-
|
| 956 |
-
|
| 957 |
-
|
| 958 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 959 |
};
|
| 960 |
-
const
|
| 961 |
-
const
|
| 962 |
-
|
| 963 |
-
|
| 964 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 965 |
}
|
| 966 |
|
| 967 |
/* ------------------------------------------------------------------ *
|
|
@@ -1390,6 +1409,22 @@ export function resolve(ast, file = 'inline.dssl', baseTokens = null) {
|
|
| 1390 |
// untouched; it rides the same document stylesheet as the
|
| 1391 |
// pseudo-element rules and survives `dssl build` verbatim.
|
| 1392 |
if ((node.css || '').trim()) raws.push({ css: node.css.trim(), file, line: node.line, order: order++ });
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1393 |
break;
|
| 1394 |
}
|
| 1395 |
case 'behavior':
|
|
|
|
| 949 |
'gap,row-gap,column-gap,padding,margin,inset,top,right,bottom,left').split(','));
|
| 950 |
|
| 951 |
function buildFluidParts(parts, ctx, prop = 'font-size') {
|
| 952 |
+
const bad = (s, why) => {
|
| 953 |
+
ctx.errors.push({ file: ctx.file, line: ctx.line, message: `fluid: "${s}" ${why}`, hint: 'fluid takes two sizes and optionally the viewport bounds, like: fluid 16 24, or fluid 16 24 360 1200' });
|
| 954 |
+
};
|
| 955 |
+
// sizes may be px (bare numbers are px), rem or em. Percentages cannot be
|
| 956 |
+
// interpolated against the viewport, so they are refused instead of
|
| 957 |
+
// silently producing a clamp() that does not do what it says.
|
| 958 |
+
const size = (s) => {
|
| 959 |
+
const m = String(s).match(/^(-?[0-9.]+)\s*(px|rem|em|%)?$/);
|
| 960 |
+
if (!m) { bad(s, 'is not a size'); return null; }
|
| 961 |
+
if (m[2] === '%') { bad(s, 'cannot be a percentage, use px, rem or em'); return null; }
|
| 962 |
+
return { n: parseFloat(m[1]), unit: m[2] || 'px' };
|
| 963 |
};
|
| 964 |
+
const a = size(parts[0]); const b = size(parts[1]);
|
| 965 |
+
const v0 = size(parts[2] || '360'); const v1 = size(parts[3] || '1200');
|
| 966 |
+
if (!a || !b || !v0 || !v1) return [{ prop, value: '0px' }];
|
| 967 |
+
if (a.unit !== b.unit) { bad(`${parts[0]} ${parts[1]}`, 'mixes units, give both sizes the same unit'); return [{ prop, value: '0px' }]; }
|
| 968 |
+
if (v0.unit !== 'px' || v1.unit !== 'px') { bad(`${parts[2]} ${parts[3]}`, 'viewport bounds are px'); return [{ prop, value: '0px' }]; }
|
| 969 |
+
if (v1.n <= v0.n) { bad(`${parts[2]} ${parts[3]}`, 'needs the larger viewport second'); return [{ prop, value: '0px' }]; }
|
| 970 |
+
// a straight line through (v0, a) and (v1, b):
|
| 971 |
+
// size(viewport) = intercept + slope * viewport
|
| 972 |
+
// The vw term is slope * 100, and the intercept is what is left at a
|
| 973 |
+
// viewport of 0. v0.8.0 forgot the intercept, so the size ran ahead of the
|
| 974 |
+
// line and hit its maximum long before the upper viewport. rem and em are
|
| 975 |
+
// worked out against a 16px root for the slope only; the intercept stays
|
| 976 |
+
// in the unit you wrote.
|
| 977 |
+
const f = a.unit === 'px' ? 1 : 16;
|
| 978 |
+
const slopePxPerPx = ((b.n - a.n) * f) / (v1.n - v0.n);
|
| 979 |
+
const vw = slopePxPerPx * 100;
|
| 980 |
+
const intercept = a.n - (slopePxPerPx * v0.n) / f;
|
| 981 |
+
const num = (x) => String(+x.toFixed(3));
|
| 982 |
+
const lo = Math.min(a.n, b.n), hi = Math.max(a.n, b.n); // clamp() needs lower first even for shrinking sizes
|
| 983 |
+
return [{ prop, value: `clamp(${num(lo)}${a.unit}, calc(${num(intercept)}${a.unit} + ${num(vw)}vw), ${num(hi)}${a.unit})` }];
|
| 984 |
}
|
| 985 |
|
| 986 |
/* ------------------------------------------------------------------ *
|
|
|
|
| 1409 |
// untouched; it rides the same document stylesheet as the
|
| 1410 |
// pseudo-element rules and survives `dssl build` verbatim.
|
| 1411 |
if ((node.css || '').trim()) raws.push({ css: node.css.trim(), file, line: node.line, order: order++ });
|
| 1412 |
+
// raw is real CSS, so DSSL's no-semicolon habit does not carry over.
|
| 1413 |
+
// A declaration line that is followed by another declaration or a
|
| 1414 |
+
// closing brace, with no ";" between, is almost always that slip.
|
| 1415 |
+
{
|
| 1416 |
+
const rl = (node.css || '').replace(/\/\*[\s\S]*?\*\//g, (m) => m.replace(/[^\n]/g, ' ')).split('\n');
|
| 1417 |
+
const decl = /^[a-zA-Z-]+\s*:\s*\S/;
|
| 1418 |
+
const nextReal = (i) => { for (let j = i + 1; j < rl.length; j++) { const t = rl[j].trim(); if (t) return t; } return '}'; };
|
| 1419 |
+
for (let i = 0; i < rl.length; i++) {
|
| 1420 |
+
const t = rl[i].trim();
|
| 1421 |
+
if (!decl.test(t) || /[;{},]$/.test(t)) continue;
|
| 1422 |
+
const nx = nextReal(i);
|
| 1423 |
+
if (nx.startsWith('}') || decl.test(nx)) {
|
| 1424 |
+
warnings.push({ file, line: node.line + i, message: `raw { } is verbatim CSS, so this line needs a ";" at the end ("${t.slice(0, 40)}")`, hint: 'unlike the rest of DSSL, semicolons are required inside raw { }' });
|
| 1425 |
+
}
|
| 1426 |
+
}
|
| 1427 |
+
}
|
| 1428 |
break;
|
| 1429 |
}
|
| 1430 |
case 'behavior':
|
|
@@ -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.1",
|
| 6 |
"publisher": "ehs-studio",
|
| 7 |
"icon": "icon.png",
|
| 8 |
"license": "MIT",
|
|
@@ -101,10 +101,11 @@ state (the graceful no-JS look). The report counts what stays runtime-only
|
|
| 101 |
|
| 102 |
In the browser the same output is `DSSL.toCSS(source)` → `{ css, report }`.
|
| 103 |
|
| 104 |
-
Two more
|
| 105 |
-
verbatim CSS like `@font-face` into the page stylesheet,
|
| 106 |
-
`font-size: fluid 16 24`
|
| 107 |
-
|
|
|
|
| 108 |
|
| 109 |
## Files in this package
|
| 110 |
|
|
|
|
| 101 |
|
| 102 |
In the browser the same output is `DSSL.toCSS(source)` → `{ css, report }`.
|
| 103 |
|
| 104 |
+
Two more tools worth knowing: `raw { }` (alias `@css { }`) drops
|
| 105 |
+
verbatim CSS like `@font-face` into the page stylesheet (it is real CSS, so
|
| 106 |
+
semicolons are required inside it), and `font-size: fluid 16 24` gives you a
|
| 107 |
+
size that grows smoothly from 16px on a 360px wide window to 24px on a 1200px
|
| 108 |
+
wide one. `dssl build --strict` makes warnings fail the build, for CI.
|
| 109 |
|
| 110 |
## Files in this package
|
| 111 |
|
|
@@ -5,7 +5,7 @@
|
|
| 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 |
|
|
@@ -30,8 +30,8 @@
|
|
| 30 |
- Gradients as sentences: `gradient: linear 135, color.accent -> color.ok`, `gradient-text:`, `gradient-flow: 6s alternate`. Non-color stops are compile errors.
|
| 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.
|
| 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; `fluid: 16, 24` still works.
|
| 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.
|
|
|
|
| 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.1 (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 |
|
|
|
|
| 30 |
- Gradients as sentences: `gradient: linear 135, color.accent -> color.ok`, `gradient-text:`, `gradient-flow: 6s alternate`. Non-color stops are compile errors.
|
| 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 |
|
| 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.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
|
|
|
|
@@ -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.1';
|
| 23 |
|
| 24 |
let counter = 0;
|
| 25 |
const engines = new Map();
|
|
@@ -255,7 +255,13 @@ export function parse(source, file = 'inline.dssl') {
|
|
| 255 |
node.css = rest.slice(open + 1, close);
|
| 256 |
return rest.slice(close + 1);
|
| 257 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 258 |
rawCapture = { node, depth: 1 };
|
|
|
|
| 259 |
return '';
|
| 260 |
}
|
| 261 |
if (meta.kind === 'bogus') {
|
|
|
|
| 255 |
node.css = rest.slice(open + 1, close);
|
| 256 |
return rest.slice(close + 1);
|
| 257 |
}
|
| 258 |
+
// multi-line: start the body empty (it used to start undefined, which
|
| 259 |
+
// put a literal "undefined" in front of the CSS) and feed whatever
|
| 260 |
+
// followed the "{" on this same line, so `raw { @font-face {` keeps
|
| 261 |
+
// its first line.
|
| 262 |
+
node.css = '';
|
| 263 |
rawCapture = { node, depth: 1 };
|
| 264 |
+
feedRawCapture(rest.slice(open + 1), line);
|
| 265 |
return '';
|
| 266 |
}
|
| 267 |
if (meta.kind === 'bogus') {
|
|
@@ -949,19 +949,38 @@ const FLUID_PROPS = new Set(('font-size,width,height,inline-size,block-size,min-
|
|
| 949 |
'gap,row-gap,column-gap,padding,margin,inset,top,right,bottom,left').split(','));
|
| 950 |
|
| 951 |
function buildFluidParts(parts, ctx, prop = 'font-size') {
|
| 952 |
-
const
|
| 953 |
-
|
| 954 |
-
|
| 955 |
-
|
| 956 |
-
|
| 957 |
-
|
| 958 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 959 |
};
|
| 960 |
-
const
|
| 961 |
-
const
|
| 962 |
-
|
| 963 |
-
|
| 964 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 965 |
}
|
| 966 |
|
| 967 |
/* ------------------------------------------------------------------ *
|
|
@@ -1390,6 +1409,22 @@ export function resolve(ast, file = 'inline.dssl', baseTokens = null) {
|
|
| 1390 |
// untouched; it rides the same document stylesheet as the
|
| 1391 |
// pseudo-element rules and survives `dssl build` verbatim.
|
| 1392 |
if ((node.css || '').trim()) raws.push({ css: node.css.trim(), file, line: node.line, order: order++ });
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1393 |
break;
|
| 1394 |
}
|
| 1395 |
case 'behavior':
|
|
|
|
| 949 |
'gap,row-gap,column-gap,padding,margin,inset,top,right,bottom,left').split(','));
|
| 950 |
|
| 951 |
function buildFluidParts(parts, ctx, prop = 'font-size') {
|
| 952 |
+
const bad = (s, why) => {
|
| 953 |
+
ctx.errors.push({ file: ctx.file, line: ctx.line, message: `fluid: "${s}" ${why}`, hint: 'fluid takes two sizes and optionally the viewport bounds, like: fluid 16 24, or fluid 16 24 360 1200' });
|
| 954 |
+
};
|
| 955 |
+
// sizes may be px (bare numbers are px), rem or em. Percentages cannot be
|
| 956 |
+
// interpolated against the viewport, so they are refused instead of
|
| 957 |
+
// silently producing a clamp() that does not do what it says.
|
| 958 |
+
const size = (s) => {
|
| 959 |
+
const m = String(s).match(/^(-?[0-9.]+)\s*(px|rem|em|%)?$/);
|
| 960 |
+
if (!m) { bad(s, 'is not a size'); return null; }
|
| 961 |
+
if (m[2] === '%') { bad(s, 'cannot be a percentage, use px, rem or em'); return null; }
|
| 962 |
+
return { n: parseFloat(m[1]), unit: m[2] || 'px' };
|
| 963 |
};
|
| 964 |
+
const a = size(parts[0]); const b = size(parts[1]);
|
| 965 |
+
const v0 = size(parts[2] || '360'); const v1 = size(parts[3] || '1200');
|
| 966 |
+
if (!a || !b || !v0 || !v1) return [{ prop, value: '0px' }];
|
| 967 |
+
if (a.unit !== b.unit) { bad(`${parts[0]} ${parts[1]}`, 'mixes units, give both sizes the same unit'); return [{ prop, value: '0px' }]; }
|
| 968 |
+
if (v0.unit !== 'px' || v1.unit !== 'px') { bad(`${parts[2]} ${parts[3]}`, 'viewport bounds are px'); return [{ prop, value: '0px' }]; }
|
| 969 |
+
if (v1.n <= v0.n) { bad(`${parts[2]} ${parts[3]}`, 'needs the larger viewport second'); return [{ prop, value: '0px' }]; }
|
| 970 |
+
// a straight line through (v0, a) and (v1, b):
|
| 971 |
+
// size(viewport) = intercept + slope * viewport
|
| 972 |
+
// The vw term is slope * 100, and the intercept is what is left at a
|
| 973 |
+
// viewport of 0. v0.8.0 forgot the intercept, so the size ran ahead of the
|
| 974 |
+
// line and hit its maximum long before the upper viewport. rem and em are
|
| 975 |
+
// worked out against a 16px root for the slope only; the intercept stays
|
| 976 |
+
// in the unit you wrote.
|
| 977 |
+
const f = a.unit === 'px' ? 1 : 16;
|
| 978 |
+
const slopePxPerPx = ((b.n - a.n) * f) / (v1.n - v0.n);
|
| 979 |
+
const vw = slopePxPerPx * 100;
|
| 980 |
+
const intercept = a.n - (slopePxPerPx * v0.n) / f;
|
| 981 |
+
const num = (x) => String(+x.toFixed(3));
|
| 982 |
+
const lo = Math.min(a.n, b.n), hi = Math.max(a.n, b.n); // clamp() needs lower first even for shrinking sizes
|
| 983 |
+
return [{ prop, value: `clamp(${num(lo)}${a.unit}, calc(${num(intercept)}${a.unit} + ${num(vw)}vw), ${num(hi)}${a.unit})` }];
|
| 984 |
}
|
| 985 |
|
| 986 |
/* ------------------------------------------------------------------ *
|
|
|
|
| 1409 |
// untouched; it rides the same document stylesheet as the
|
| 1410 |
// pseudo-element rules and survives `dssl build` verbatim.
|
| 1411 |
if ((node.css || '').trim()) raws.push({ css: node.css.trim(), file, line: node.line, order: order++ });
|
| 1412 |
+
// raw is real CSS, so DSSL's no-semicolon habit does not carry over.
|
| 1413 |
+
// A declaration line that is followed by another declaration or a
|
| 1414 |
+
// closing brace, with no ";" between, is almost always that slip.
|
| 1415 |
+
{
|
| 1416 |
+
const rl = (node.css || '').replace(/\/\*[\s\S]*?\*\//g, (m) => m.replace(/[^\n]/g, ' ')).split('\n');
|
| 1417 |
+
const decl = /^[a-zA-Z-]+\s*:\s*\S/;
|
| 1418 |
+
const nextReal = (i) => { for (let j = i + 1; j < rl.length; j++) { const t = rl[j].trim(); if (t) return t; } return '}'; };
|
| 1419 |
+
for (let i = 0; i < rl.length; i++) {
|
| 1420 |
+
const t = rl[i].trim();
|
| 1421 |
+
if (!decl.test(t) || /[;{},]$/.test(t)) continue;
|
| 1422 |
+
const nx = nextReal(i);
|
| 1423 |
+
if (nx.startsWith('}') || decl.test(nx)) {
|
| 1424 |
+
warnings.push({ file, line: node.line + i, message: `raw { } is verbatim CSS, so this line needs a ";" at the end ("${t.slice(0, 40)}")`, hint: 'unlike the rest of DSSL, semicolons are required inside raw { }' });
|
| 1425 |
+
}
|
| 1426 |
+
}
|
| 1427 |
+
}
|
| 1428 |
break;
|
| 1429 |
}
|
| 1430 |
case 'behavior':
|
|
@@ -5,7 +5,7 @@
|
|
| 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 |
|
|
@@ -30,8 +30,8 @@
|
|
| 30 |
- Gradients as sentences: `gradient: linear 135, color.accent -> color.ok`, `gradient-text:`, `gradient-flow: 6s alternate`. Non-color stops are compile errors.
|
| 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.
|
| 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; `fluid: 16, 24` still works.
|
| 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.
|
|
|
|
| 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.1 (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 |
|
|
|
|
| 30 |
- Gradients as sentences: `gradient: linear 135, color.accent -> color.ok`, `gradient-text:`, `gradient-flow: 6s alternate`. Non-color stops are compile errors.
|
| 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 |
|
| 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.vsix` from the repo files. Source lives in `dssl-vscode/` in the repo.
|
|
@@ -1,8 +1,8 @@
|
|
| 1 |
#!/usr/bin/env bash
|
| 2 |
-
# Rebuild public/dssl-engine.zip from the current engine + package texts.
|
| 3 |
# Usage: bash scripts/build-engine-package.sh
|
| 4 |
set -euo pipefail
|
| 5 |
-
ROOT=/
|
| 6 |
STAGE=$(mktemp -d)
|
| 7 |
trap 'rm -rf "$STAGE"' EXIT
|
| 8 |
|
|
@@ -26,5 +26,8 @@ with zipfile.ZipFile(out, "w", zipfile.ZIP_DEFLATED) as z:
|
|
| 26 |
full = os.path.join(base, f)
|
| 27 |
z.write(full, os.path.relpath(full, stage))
|
| 28 |
PY
|
|
|
|
|
|
|
|
|
|
| 29 |
echo "built $ROOT/public/dssl-engine.zip:"
|
| 30 |
python3 -m zipfile -l "$ROOT/public/dssl-engine.zip"
|
|
|
|
| 1 |
#!/usr/bin/env bash
|
| 2 |
+
# Rebuild public/dssl-engine.zip and public/dssl-engine.tar.gz from the current engine + package texts.
|
| 3 |
# Usage: bash scripts/build-engine-package.sh
|
| 4 |
set -euo pipefail
|
| 5 |
+
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
| 6 |
STAGE=$(mktemp -d)
|
| 7 |
trap 'rm -rf "$STAGE"' EXIT
|
| 8 |
|
|
|
|
| 26 |
full = os.path.join(base, f)
|
| 27 |
z.write(full, os.path.relpath(full, stage))
|
| 28 |
PY
|
| 29 |
+
rm -f "$ROOT/public/dssl-engine.tar.gz"
|
| 30 |
+
tar -czf "$ROOT/public/dssl-engine.tar.gz" -C "$STAGE" dssl-engine
|
| 31 |
+
echo "built $ROOT/public/dssl-engine.tar.gz"
|
| 32 |
echo "built $ROOT/public/dssl-engine.zip:"
|
| 33 |
python3 -m zipfile -l "$ROOT/public/dssl-engine.zip"
|
|
@@ -9,9 +9,10 @@
|
|
| 9 |
import { readFileSync, writeFileSync, unlinkSync } from 'node:fs';
|
| 10 |
import { createServer } from 'node:http';
|
| 11 |
import { extname, join } from 'node:path';
|
|
|
|
| 12 |
import { chromium } from 'playwright';
|
| 13 |
|
| 14 |
-
const ROOT = '/
|
| 15 |
const PORT = 8931 + Math.floor(Math.random() * 200);
|
| 16 |
|
| 17 |
const MIME = { '.js': 'text/javascript', '.html': 'text/html', '.css': 'text/css', '.json': 'application/json' };
|
|
|
|
| 9 |
import { readFileSync, writeFileSync, unlinkSync } from 'node:fs';
|
| 10 |
import { createServer } from 'node:http';
|
| 11 |
import { extname, join } from 'node:path';
|
| 12 |
+
import { fileURLToPath } from 'node:url';
|
| 13 |
import { chromium } from 'playwright';
|
| 14 |
|
| 15 |
+
const ROOT = fileURLToPath(new URL('..', import.meta.url)).replace(/\/$/, '');
|
| 16 |
const PORT = 8931 + Math.floor(Math.random() * 200);
|
| 17 |
|
| 18 |
const MIME = { '.js': 'text/javascript', '.html': 'text/html', '.css': 'text/css', '.json': 'application/json' };
|
|
@@ -3,18 +3,23 @@
|
|
| 3 |
// and reports README.md blocks (the README may carry teaching examples that
|
| 4 |
// intentionally error; those are tagged with <!-- dssl:has-errors -->).
|
| 5 |
import { readFileSync } from 'node:fs';
|
|
|
|
| 6 |
import { parse } from '../public/dssl/parser.js';
|
| 7 |
import { resolve, DEFAULT_TOKENS } from '../public/dssl/resolver.js';
|
| 8 |
|
|
|
|
|
|
|
| 9 |
const files = [
|
| 10 |
-
{ path: '
|
| 11 |
-
{ path: '
|
| 12 |
];
|
| 13 |
|
| 14 |
let pass = 0, fail = 0;
|
| 15 |
for (const { path, strict } of files) {
|
| 16 |
let text;
|
| 17 |
-
|
|
|
|
|
|
|
| 18 |
const blocks = [...text.matchAll(/```dssl\n([\s\S]*?)```/g)].map(m => m[1]);
|
| 19 |
// whole-file workspace fold: seed with the engine's default tokens, collect
|
| 20 |
// every token defined anywhere in the document, then compile each block
|
|
@@ -31,6 +36,8 @@ for (const { path, strict } of files) {
|
|
| 31 |
const { ast, errors: parseErrors } = parse(src, name);
|
| 32 |
const doc = resolve(ast, name, registry);
|
| 33 |
const errors = [...parseErrors, ...doc.errors];
|
|
|
|
|
|
|
| 34 |
if (errors.length === 0) { pass++; return; }
|
| 35 |
if (!strict && /dssl:has-errors/.test(src)) { pass++; return; }
|
| 36 |
fail++;
|
|
@@ -38,6 +45,7 @@ for (const { path, strict } of files) {
|
|
| 38 |
for (const e of errors.slice(0, 4)) console.error(' line ' + (e.line ?? '?') + ': ' + e.message);
|
| 39 |
});
|
| 40 |
console.log(`${path.split('/').pop()}: ${blocks.length} blocks checked`);
|
|
|
|
| 41 |
}
|
| 42 |
console.log(`${pass} passed, ${fail} failed`);
|
| 43 |
process.exitCode = fail ? 1 : 0;
|
|
|
|
| 3 |
// and reports README.md blocks (the README may carry teaching examples that
|
| 4 |
// intentionally error; those are tagged with <!-- dssl:has-errors -->).
|
| 5 |
import { readFileSync } from 'node:fs';
|
| 6 |
+
import { fileURLToPath } from 'node:url';
|
| 7 |
import { parse } from '../public/dssl/parser.js';
|
| 8 |
import { resolve, DEFAULT_TOKENS } from '../public/dssl/resolver.js';
|
| 9 |
|
| 10 |
+
// paths are relative to the repo, so this works in any checkout
|
| 11 |
+
const repo = (f) => fileURLToPath(new URL('../' + f, import.meta.url));
|
| 12 |
const files = [
|
| 13 |
+
{ path: repo('GUIDE.md'), strict: true },
|
| 14 |
+
{ path: repo('README.md'), strict: false },
|
| 15 |
];
|
| 16 |
|
| 17 |
let pass = 0, fail = 0;
|
| 18 |
for (const { path, strict } of files) {
|
| 19 |
let text;
|
| 20 |
+
// a missing file used to be skipped quietly, which let the whole check
|
| 21 |
+
// report "0 passed, 0 failed" and look green. A missing file is a failure.
|
| 22 |
+
try { text = readFileSync(path, 'utf8'); } catch { console.error('FAIL cannot read ' + path); fail++; continue; }
|
| 23 |
const blocks = [...text.matchAll(/```dssl\n([\s\S]*?)```/g)].map(m => m[1]);
|
| 24 |
// whole-file workspace fold: seed with the engine's default tokens, collect
|
| 25 |
// every token defined anywhere in the document, then compile each block
|
|
|
|
| 36 |
const { ast, errors: parseErrors } = parse(src, name);
|
| 37 |
const doc = resolve(ast, name, registry);
|
| 38 |
const errors = [...parseErrors, ...doc.errors];
|
| 39 |
+
// raw { } needs real CSS, so a missing semicolon in a documented example is a docs bug
|
| 40 |
+
if (strict) for (const w of doc.warnings || []) if (/needs a ";"/.test(w.message)) errors.push({ line: w.line, message: w.message });
|
| 41 |
if (errors.length === 0) { pass++; return; }
|
| 42 |
if (!strict && /dssl:has-errors/.test(src)) { pass++; return; }
|
| 43 |
fail++;
|
|
|
|
| 45 |
for (const e of errors.slice(0, 4)) console.error(' line ' + (e.line ?? '?') + ': ' + e.message);
|
| 46 |
});
|
| 47 |
console.log(`${path.split('/').pop()}: ${blocks.length} blocks checked`);
|
| 48 |
+
if (blocks.length === 0) { console.error('FAIL no dssl blocks found in ' + path); fail++; }
|
| 49 |
}
|
| 50 |
console.log(`${pass} passed, ${fail} failed`);
|
| 51 |
process.exitCode = fail ? 1 : 0;
|
|
@@ -9,6 +9,7 @@ import { execFileSync } from 'node:child_process';
|
|
| 9 |
import { writeFileSync, readFileSync, mkdtempSync, rmSync } from 'node:fs';
|
| 10 |
import { tmpdir } from 'node:os';
|
| 11 |
import { join } from 'node:path';
|
|
|
|
| 12 |
|
| 13 |
let pass = 0, fail = 0;
|
| 14 |
const ok = (name, cond, extra) => {
|
|
@@ -131,13 +132,13 @@ const lastVal = (decls, prop) => decls.filter(d => d.prop === prop).pop()?.value
|
|
| 131 |
/* ---- feature: fluid sugar ---- */
|
| 132 |
{
|
| 133 |
const inline = compileDoc('.a { font-size: fluid 16 24 }');
|
| 134 |
-
ok('7a. font-size: fluid 16 24 → clamp', val(declsOf(inline.doc), 'font-size') === 'clamp(16px, calc(
|
| 135 |
const bounds = compileDoc('.a { font-size: fluid 16 24 400 800 }');
|
| 136 |
-
ok('7b. custom viewport bounds change the slope', val(declsOf(bounds.doc), 'font-size') === 'clamp(16px, calc(
|
| 137 |
const gap = compileDoc('.a { gap: fluid 8 16 }');
|
| 138 |
-
ok('7c. gap takes inline fluid too', val(declsOf(gap.doc), 'gap') === 'clamp(8px, calc(
|
| 139 |
const legacy = compileDoc('.a { fluid: 16, 24 }');
|
| 140 |
-
ok('7d. the fluid: 16, 24 form still works', val(declsOf(legacy.doc), 'font-size') === 'clamp(16px, calc(
|
| 141 |
}
|
| 142 |
|
| 143 |
/* ---- feature: toCSS — the exit lane ---- */
|
|
@@ -209,7 +210,7 @@ raw { @font-face { font-family: X; src: url(x.woff2) } }`;
|
|
| 209 |
const missing = run(['check', join(dir, 'nope.dssl')]);
|
| 210 |
ok('9i. missing file exits 2', missing.code === 2);
|
| 211 |
const v = run(['--version']);
|
| 212 |
-
ok('9j. --version prints
|
| 213 |
rmSync(dir, { recursive: true, force: true });
|
| 214 |
}
|
| 215 |
|
|
|
|
| 9 |
import { writeFileSync, readFileSync, mkdtempSync, rmSync } from 'node:fs';
|
| 10 |
import { tmpdir } from 'node:os';
|
| 11 |
import { join } from 'node:path';
|
| 12 |
+
import { VERSION } from '../public/dssl/index.js';
|
| 13 |
|
| 14 |
let pass = 0, fail = 0;
|
| 15 |
const ok = (name, cond, extra) => {
|
|
|
|
| 132 |
/* ---- feature: fluid sugar ---- */
|
| 133 |
{
|
| 134 |
const inline = compileDoc('.a { font-size: fluid 16 24 }');
|
| 135 |
+
ok('7a. font-size: fluid 16 24 → clamp', val(declsOf(inline.doc), 'font-size') === 'clamp(16px, calc(12.571px + 0.952vw), 24px)');
|
| 136 |
const bounds = compileDoc('.a { font-size: fluid 16 24 400 800 }');
|
| 137 |
+
ok('7b. custom viewport bounds change the slope', val(declsOf(bounds.doc), 'font-size') === 'clamp(16px, calc(8px + 2vw), 24px)');
|
| 138 |
const gap = compileDoc('.a { gap: fluid 8 16 }');
|
| 139 |
+
ok('7c. gap takes inline fluid too', val(declsOf(gap.doc), 'gap') === 'clamp(8px, calc(4.571px + 0.952vw), 16px)');
|
| 140 |
const legacy = compileDoc('.a { fluid: 16, 24 }');
|
| 141 |
+
ok('7d. the fluid: 16, 24 form still works', val(declsOf(legacy.doc), 'font-size') === 'clamp(16px, calc(12.571px + 0.952vw), 24px)');
|
| 142 |
}
|
| 143 |
|
| 144 |
/* ---- feature: toCSS — the exit lane ---- */
|
|
|
|
| 210 |
const missing = run(['check', join(dir, 'nope.dssl')]);
|
| 211 |
ok('9i. missing file exits 2', missing.code === 2);
|
| 212 |
const v = run(['--version']);
|
| 213 |
+
ok('9j. --version prints the engine version', v.out.trim() === VERSION);
|
| 214 |
rmSync(dir, { recursive: true, force: true });
|
| 215 |
}
|
| 216 |
|
|
@@ -0,0 +1,175 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* test-v081.mjs; one assertion per fix in 0.8.1.
|
| 2 |
+
* Source: the 2026-10-23 re-test of 0.8.0 (two bugs and one nit).
|
| 3 |
+
* 1. fluid computed clamp(16px, calc(16px + 0.952vw), 24px): no intercept,
|
| 4 |
+
* so it was already 19.6px at a 375px phone and hit 24px near 840px.
|
| 5 |
+
* 2. the guide's multi-line raw { } example came out as "undefined @font-face"
|
| 6 |
+
* and, with no semicolons, was not valid CSS.
|
| 7 |
+
* 3. dssl build had no --strict, so CI could not fail on warnings.
|
| 8 |
+
* The fluid tests do not compare strings. They evaluate the emitted clamp()
|
| 9 |
+
* at real viewport widths, which is the thing that was wrong. */
|
| 10 |
+
import { parse } from '../public/dssl/parser.js';
|
| 11 |
+
import { resolve } from '../public/dssl/resolver.js';
|
| 12 |
+
import { toCSS } from '../public/dssl/static.js';
|
| 13 |
+
import { VERSION } from '../public/dssl/index.js';
|
| 14 |
+
import { execFileSync } from 'node:child_process';
|
| 15 |
+
import { writeFileSync, mkdtempSync, rmSync, mkdirSync, copyFileSync } from 'node:fs';
|
| 16 |
+
import { tmpdir } from 'node:os';
|
| 17 |
+
import { join, dirname } from 'node:path';
|
| 18 |
+
import { fileURLToPath } from 'node:url';
|
| 19 |
+
|
| 20 |
+
let pass = 0, fail = 0;
|
| 21 |
+
const ok = (name, cond, extra) => {
|
| 22 |
+
if (cond) { pass++; console.log(` ok ${name}`); }
|
| 23 |
+
else { fail++; console.log(`FAIL ${name}${extra ? '; ' + extra : ''}`); }
|
| 24 |
+
};
|
| 25 |
+
|
| 26 |
+
const compileDoc = (src, file = 't.dssl') => {
|
| 27 |
+
const { ast, errors } = parse(src, file);
|
| 28 |
+
const doc = resolve(ast, file);
|
| 29 |
+
return { doc, errors: [...errors, ...doc.errors], ast };
|
| 30 |
+
};
|
| 31 |
+
const declsOf = (doc, i = 0) => doc.rules[i]?.decls || [];
|
| 32 |
+
const val = (decls, prop) => decls.find(d => d.prop === prop)?.value;
|
| 33 |
+
|
| 34 |
+
/** evaluate `clamp(L, calc(I + Wvw), H)` at a viewport width, in px (rem = 16px) */
|
| 35 |
+
const evalClamp = (css, vw) => {
|
| 36 |
+
const m = /^clamp\(\s*(-?[\d.]+)(px|rem|em),\s*calc\(\s*(-?[\d.]+)(px|rem|em)\s*\+\s*(-?[\d.]+)vw\s*\),\s*(-?[\d.]+)(px|rem|em)\s*\)$/.exec(css);
|
| 37 |
+
if (!m) return NaN;
|
| 38 |
+
const f = (u) => (u === 'px' ? 1 : 16);
|
| 39 |
+
const lo = parseFloat(m[1]) * f(m[2]);
|
| 40 |
+
const mid = parseFloat(m[3]) * f(m[4]) + parseFloat(m[5]) * vw / 100;
|
| 41 |
+
const hi = parseFloat(m[6]) * f(m[7]);
|
| 42 |
+
return Math.min(hi, Math.max(lo, mid));
|
| 43 |
+
};
|
| 44 |
+
const near = (a, b, eps = 0.01) => Math.abs(a - b) <= eps;
|
| 45 |
+
const fluidOf = (src) => val(declsOf(compileDoc(src).doc), 'font-size');
|
| 46 |
+
|
| 47 |
+
/* ---- 1: fluid math ---- */
|
| 48 |
+
{
|
| 49 |
+
const c = fluidOf('.a { font-size: fluid 16 24 }');
|
| 50 |
+
ok('1a. fluid 16 24 is exactly 16px at the 360px lower bound', near(evalClamp(c, 360), 16), `got ${evalClamp(c, 360)}`);
|
| 51 |
+
ok('1b. fluid 16 24 is exactly 24px at the 1200px upper bound', near(evalClamp(c, 1200), 24), `got ${evalClamp(c, 1200)}`);
|
| 52 |
+
ok('1c. a 375px phone gets 16.14px, not the 19.57px 0.8.0 gave', near(evalClamp(c, 375), 16.143), `got ${evalClamp(c, 375)}`);
|
| 53 |
+
ok('1d. the middle of the range is the middle of the sizes (780px = 20px)', near(evalClamp(c, 780), 20), `got ${evalClamp(c, 780)}`);
|
| 54 |
+
ok('1e. an 800px laptop window gets 20.19px, not 23.6px', near(evalClamp(c, 800), 20.19), `got ${evalClamp(c, 800)}`);
|
| 55 |
+
ok('1f. it is clamped below the lower bound and above the upper bound', near(evalClamp(c, 200), 16) && near(evalClamp(c, 2000), 24));
|
| 56 |
+
|
| 57 |
+
const b = fluidOf('.a { font-size: fluid 16 24 400 800 }');
|
| 58 |
+
ok('1g. custom bounds: 16px at 400, 24px at 800, 20px at 600', near(evalClamp(b, 400), 16) && near(evalClamp(b, 800), 24) && near(evalClamp(b, 600), 20));
|
| 59 |
+
|
| 60 |
+
const shrink = fluidOf('.a { font-size: fluid 24 16 }');
|
| 61 |
+
ok('1h. a shrinking size works: 24px at 360, 16px at 1200', near(evalClamp(shrink, 360), 24) && near(evalClamp(shrink, 1200), 16), shrink);
|
| 62 |
+
ok('1i. a shrinking size keeps the lower value first in clamp()', /^clamp\(16px,/.test(shrink), shrink);
|
| 63 |
+
|
| 64 |
+
const rem = fluidOf('.a { font-size: fluid 1rem 1.5rem }');
|
| 65 |
+
ok('1j. rem sizes interpolate too: 16px at 360, 24px at 1200', near(evalClamp(rem, 360), 16) && near(evalClamp(rem, 1200), 24), rem);
|
| 66 |
+
|
| 67 |
+
const legacy = fluidOf('.a { fluid: 16, 24 }');
|
| 68 |
+
ok('1k. the old fluid: 16, 24 form gets the same fix', near(evalClamp(legacy, 375), 16.143), legacy);
|
| 69 |
+
|
| 70 |
+
const gap = val(declsOf(compileDoc('.a { gap: fluid 8 16 }').doc), 'gap');
|
| 71 |
+
ok('1l. gap takes fluid with the same math (8px at 360, 16px at 1200)', near(evalClamp(gap, 360), 8) && near(evalClamp(gap, 1200), 16));
|
| 72 |
+
|
| 73 |
+
const pct = compileDoc('.a { width: fluid 50% 100% }');
|
| 74 |
+
ok('1m. a percentage is refused with a clear message', pct.errors.some(e => /percentage/.test(e.message)));
|
| 75 |
+
const mixed = compileDoc('.a { font-size: fluid 16px 2rem }');
|
| 76 |
+
ok('1n. mixed units are refused', mixed.errors.some(e => /mixes units/.test(e.message)));
|
| 77 |
+
const backwards = compileDoc('.a { font-size: fluid 16 24 1200 360 }');
|
| 78 |
+
ok('1o. viewport bounds in the wrong order are refused', backwards.errors.some(e => /larger viewport second/.test(e.message)));
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
/* ---- 2: raw { } ---- */
|
| 82 |
+
{
|
| 83 |
+
const guide = `raw {
|
| 84 |
+
@font-face {
|
| 85 |
+
font-family: Inter;
|
| 86 |
+
src: url(/fonts/inter.woff2);
|
| 87 |
+
}
|
| 88 |
+
}`;
|
| 89 |
+
const g = compileDoc(guide);
|
| 90 |
+
const css = g.doc.raws[0]?.css || '';
|
| 91 |
+
ok('2a. multi-line raw has no "undefined" in front of it', !/undefined/.test(css), css.slice(0, 40));
|
| 92 |
+
ok('2b. multi-line raw starts with the @font-face', /^@font-face\s*\{/.test(css), css.slice(0, 40));
|
| 93 |
+
ok('2c. the guide example compiles with zero errors and zero warnings', g.errors.length === 0 && g.doc.warnings.length === 0);
|
| 94 |
+
ok('2d. the body is kept verbatim, semicolons included', /font-family: Inter;/.test(css) && /src: url\(\/fonts\/inter\.woff2\);/.test(css));
|
| 95 |
+
|
| 96 |
+
const sameLine = compileDoc('raw { @font-face {\n font-family: X;\n src: local(Arial);\n} }');
|
| 97 |
+
ok('2e. content on the same line as "raw {" is not dropped', /^@font-face/.test(sameLine.doc.raws[0]?.css || ''), sameLine.doc.raws[0]?.css);
|
| 98 |
+
ok('2f. a raw block that closes on the same line as its last rule still works', sameLine.doc.raws.length === 1 && sameLine.errors.length === 0);
|
| 99 |
+
|
| 100 |
+
const old = compileDoc(`raw {
|
| 101 |
+
@font-face {
|
| 102 |
+
font-family: Inter
|
| 103 |
+
src: url(/fonts/inter.woff2)
|
| 104 |
+
}
|
| 105 |
+
}`);
|
| 106 |
+
const w = old.doc.warnings.filter(x => /needs a ";"/.test(x.message));
|
| 107 |
+
ok('2g. the old no-semicolon style warns on each bare declaration', w.length === 2, JSON.stringify(old.doc.warnings.map(x => x.message)));
|
| 108 |
+
ok('2h. the warning points at the right source line', w.length === 2 && w[0].line === 3 && w[1].line === 4, w.map(x => x.line).join(','));
|
| 109 |
+
ok('2i. the warning explains why', w.length > 0 && /semicolons are required inside raw/.test(w[0].hint));
|
| 110 |
+
|
| 111 |
+
const one = compileDoc('raw { @font-face { font-family: X; src: local(Arial) } }');
|
| 112 |
+
ok('2j. single-line raw has no warning (the declarations sit inside braces on one line)', one.doc.warnings.length === 0 && one.errors.length === 0);
|
| 113 |
+
const sel = compileDoc('raw {\n a:hover,\n a:focus { color: red; }\n .grid { grid-template-areas: "a b"\n "c d"; }\n}');
|
| 114 |
+
ok('2k. selectors with colons and multi-line values are not flagged', sel.doc.warnings.length === 0, JSON.stringify(sel.doc.warnings.map(x => x.message)));
|
| 115 |
+
const cmt = compileDoc('raw {\n /* color: red */\n .a { color: blue; }\n}');
|
| 116 |
+
ok('2l. a declaration inside a comment is not flagged', cmt.doc.warnings.length === 0);
|
| 117 |
+
|
| 118 |
+
const built = toCSS(g.doc, { file: 't.dssl', banner: false });
|
| 119 |
+
ok('2m. the built CSS carries the raw block clean and valid', !/undefined/.test(built.css) && /font-family: Inter;/.test(built.css));
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
/* ---- 3: dssl build --strict (CLI) ---- */
|
| 123 |
+
{
|
| 124 |
+
const here = dirname(fileURLToPath(import.meta.url));
|
| 125 |
+
const cli = join(here, '..', 'cli', 'dssl.mjs');
|
| 126 |
+
const dir = mkdtempSync(join(tmpdir(), 'dssl-v081-'));
|
| 127 |
+
const warnFile = join(dir, 'warn.dssl');
|
| 128 |
+
const cleanFile = join(dir, 'clean.dssl');
|
| 129 |
+
writeFileSync(warnFile, '.a { row: center center }\n');
|
| 130 |
+
writeFileSync(cleanFile, '.a { color: red }\n');
|
| 131 |
+
const run = (args) => {
|
| 132 |
+
try { return { code: 0, out: execFileSync('node', [cli, ...args], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }) }; }
|
| 133 |
+
catch (e) { return { code: e.status, out: (e.stdout || '') + (e.stderr || '') }; }
|
| 134 |
+
};
|
| 135 |
+
const plain = run(['build', warnFile, '-o', join(dir, 'a.css')]);
|
| 136 |
+
ok('3a. build with a warning still exits 0 without --strict', plain.code === 0);
|
| 137 |
+
const strict = run(['build', warnFile, '-o', join(dir, 'b.css'), '--strict']);
|
| 138 |
+
ok('3b. build --strict exits 1 on a warning', strict.code === 1);
|
| 139 |
+
const clean = run(['build', cleanFile, '-o', join(dir, 'c.css'), '--strict']);
|
| 140 |
+
ok('3c. build --strict exits 0 when there is nothing to warn about', clean.code === 0);
|
| 141 |
+
const js = JSON.parse(run(['build', warnFile, '--json', '--strict']).out);
|
| 142 |
+
ok('3d. build --json --strict reports ok:false', js.ok === false && js.warnings.length === 1);
|
| 143 |
+
const jn = JSON.parse(run(['build', warnFile, '--json']).out);
|
| 144 |
+
ok('3e. build --json without --strict still reports ok:true', jn.ok === true);
|
| 145 |
+
const v = run(['--version']);
|
| 146 |
+
ok('3f. the CLI reports the engine version', v.out.trim() === VERSION);
|
| 147 |
+
rmSync(dir, { recursive: true, force: true });
|
| 148 |
+
}
|
| 149 |
+
|
| 150 |
+
|
| 151 |
+
/* ---- 4: the packaged CLI (dssl-engine.zip layout: cli/ next to dssl/) ---- */
|
| 152 |
+
{
|
| 153 |
+
const here = dirname(fileURLToPath(import.meta.url));
|
| 154 |
+
const root = join(here, '..');
|
| 155 |
+
const dir = mkdtempSync(join(tmpdir(), 'dssl-pkg-'));
|
| 156 |
+
mkdirSync(join(dir, 'cli')); mkdirSync(join(dir, 'dssl'));
|
| 157 |
+
copyFileSync(join(root, 'cli', 'dssl.mjs'), join(dir, 'cli', 'dssl.mjs'));
|
| 158 |
+
for (const f of ['boot', 'engine', 'index', 'parser', 'resolver', 'static', 'utils']) copyFileSync(join(root, 'public', 'dssl', f + '.js'), join(dir, 'dssl', f + '.js'));
|
| 159 |
+
const src = join(dir, 'a.dssl');
|
| 160 |
+
writeFileSync(src, 'h1 { font-size: fluid 16 24 }\n');
|
| 161 |
+
const run = (args) => {
|
| 162 |
+
try { return { code: 0, out: execFileSync('node', [join(dir, 'cli', 'dssl.mjs'), ...args], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }) }; }
|
| 163 |
+
catch (e) { return { code: e.status, out: (e.stdout || '') + (e.stderr || '') }; }
|
| 164 |
+
};
|
| 165 |
+
const v = run(['--version']);
|
| 166 |
+
ok('4a. the packaged CLI starts (0.8.0 crashed with module not found)', v.code === 0 && v.out.trim() === VERSION, v.out.slice(0, 120));
|
| 167 |
+
const b = run(['build', src, '-o', join(dir, 'a.css'), '--no-banner']);
|
| 168 |
+
ok('4b. the packaged CLI builds a stylesheet', b.code === 0, b.out.slice(0, 120));
|
| 169 |
+
const css = b.code === 0 ? execFileSync('cat', [join(dir, 'a.css')], { encoding: 'utf8' }) : '';
|
| 170 |
+
ok('4c. and the output has the corrected fluid math', /calc\(12\.571px \+ 0\.952vw\)/.test(css), css.slice(0, 160));
|
| 171 |
+
rmSync(dir, { recursive: true, force: true });
|
| 172 |
+
}
|
| 173 |
+
|
| 174 |
+
console.log(`\n${pass} passed, ${fail} failed`);
|
| 175 |
+
process.exit(fail ? 1 : 0);
|
|
@@ -11,6 +11,29 @@ const ENTRIES: {
|
|
| 11 |
code?: string;
|
| 12 |
kind?: "spec" | "vote" | "add";
|
| 13 |
}[] = [
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
{
|
| 15 |
version: "v0.8.0",
|
| 16 |
date: "growth phase · the exit drop",
|
|
|
|
| 11 |
code?: string;
|
| 12 |
kind?: "spec" | "vote" | "add";
|
| 13 |
}[] = [
|
| 14 |
+
{
|
| 15 |
+
version: "v0.8.1",
|
| 16 |
+
date: "growth phase · the follow-up",
|
| 17 |
+
title: "Two more bugs from the re-test, both fixed, and the tests now check the math.",
|
| 18 |
+
why: "after 0.8.0 shipped, the same reviewer ran it again. Most things held up. Two did not: the fluid sizing sugar computed the wrong line, and the guide's own raw example produced broken CSS. Both slipped past the tests for the same reason as before: the tests asked whether the code compiled, not whether the result was right.",
|
| 19 |
+
kind: "spec",
|
| 20 |
+
body:
|
| 21 |
+
"fluid 16 24 is supposed to be 16px on a 360px window and 24px on a 1200px one, sliding smoothly in between. The clamp() it produced had the right slope but forgot where the line starts, so it was already 19.6px on a 375px phone and hit its maximum around 840px. It now starts and ends where it says. While writing the tests, three more things turned up and are fixed by the same change: shrinking sizes like fluid 24 16 made a backwards clamp(), rem sizes got a slope 16 times too small, and percentages (which cannot follow the window width) now give a clear error instead of nonsense. If you used fluid in 0.8.0, your text will be a bit smaller on phones and tablets now. That is the fix. The raw { } block had its own problems: a multi-line block started with the word undefined, anything on the same line as raw { was dropped, and the example in the guide had no semicolons, which is not valid CSS. The first two are fixed, the guide example is corrected, and a missing semicolon inside raw { } is now a warning that names the line, since raw is real CSS and DSSL cannot guess what you meant. dssl build also takes --strict now, so CI can fail on warnings the same way dssl check can. The new tests do not compare strings. They work out what the emitted clamp() gives at real window widths (360, 375, 600, 800, 1200 and beyond) and check it against the sizes you asked for, and they were run against the old 0.8.0 engine first, where two dozen of them fail. One more thing turned up while testing the download: the dssl command inside dssl-engine.zip crashed on start with module not found, because it looked for the engine in public/dssl and the package keeps it in dssl. It now finds the engine in either place, and a test runs it from the package layout.",
|
| 22 |
+
code: `// what fluid 16 24 gives you, by window width
|
| 23 |
+
// 360px -> 16px (the lower size, exactly)
|
| 24 |
+
// 375px -> 16.1px (0.8.0 said 19.6px)
|
| 25 |
+
// 800px -> 20.2px (0.8.0 said 23.6px)
|
| 26 |
+
// 1200px -> 24px (the upper size, exactly)
|
| 27 |
+
h1 { font-size: fluid 16 24 }
|
| 28 |
+
|
| 29 |
+
// raw is real CSS, so it needs its semicolons
|
| 30 |
+
raw {
|
| 31 |
+
@font-face {
|
| 32 |
+
font-family: Inter;
|
| 33 |
+
src: url(/fonts/inter.woff2);
|
| 34 |
+
}
|
| 35 |
+
}`,
|
| 36 |
+
},
|
| 37 |
{
|
| 38 |
version: "v0.8.0",
|
| 39 |
date: "growth phase · the exit drop",
|
|
@@ -437,7 +437,7 @@ input::-webkit-slider-thumb { background: color.accent }
|
|
| 437 |
<tr><td><code>cover</code></td><td><code>cover: fixed</code></td><td>position + inset: 0</td></tr>
|
| 438 |
<tr><td><code>sticky</code></td><td><code>sticky: 0</code></td><td>position: sticky; top: 0px</td></tr>
|
| 439 |
<tr><td><code>font</code></td><td><code>font: 16/1.4 ink</code></td><td>font-size / line-height / color</td></tr>
|
| 440 |
-
<tr><td><code>fluid</code></td><td><code>fluid: 16, 24</code></td><td>
|
| 441 |
<tr><td><code>shadow</code> / <code>radius</code></td><td><code>shadow: lg</code></td><td>token presets (sm/md/lg/xl) or raw values</td></tr>
|
| 442 |
<tr><td><code>circle</code></td><td><code>circle: 36</code></td><td>width + height + border-radius: 50%</td></tr>
|
| 443 |
<tr><td><code>grow</code> / <code>shrink</code> / <code>basis</code></td><td><code>grow: 1</code></td><td>flex-grow / flex-shrink / flex-basis</td></tr>
|
|
|
|
| 437 |
<tr><td><code>cover</code></td><td><code>cover: fixed</code></td><td>position + inset: 0</td></tr>
|
| 438 |
<tr><td><code>sticky</code></td><td><code>sticky: 0</code></td><td>position: sticky; top: 0px</td></tr>
|
| 439 |
<tr><td><code>font</code></td><td><code>font: 16/1.4 ink</code></td><td>font-size / line-height / color</td></tr>
|
| 440 |
+
<tr><td><code>fluid</code></td><td><code>fluid: 16, 24</code></td><td>a size that grows smoothly from 16px at a 360px window to 24px at 1200px (px, rem or em)</td></tr>
|
| 441 |
<tr><td><code>shadow</code> / <code>radius</code></td><td><code>shadow: lg</code></td><td>token presets (sm/md/lg/xl) or raw values</td></tr>
|
| 442 |
<tr><td><code>circle</code></td><td><code>circle: 36</code></td><td>width + height + border-radius: 50%</td></tr>
|
| 443 |
<tr><td><code>grow</code> / <code>shrink</code> / <code>basis</code></td><td><code>grow: 1</code></td><td>flex-grow / flex-shrink / flex-basis</td></tr>
|