diff --git a/.gitignore b/.gitignore
index ade8815df7a4457dbcb2b221bea5eceb19411591..4ba7ffbd641590aa2f52a4178071a954763ec65f 100755
--- a/.gitignore
+++ b/.gitignore
@@ -58,3 +58,6 @@ server.log
.env
mini-services/
+
+# local screenshots, never published
+.zscripts/
diff --git a/.vercelignore b/.vercelignore
new file mode 100755
index 0000000000000000000000000000000000000000..5febd01d6ef9ed214be67aa349cebd0af386689a
--- /dev/null
+++ b/.vercelignore
@@ -0,0 +1,16 @@
+/download/
+/upload/
+/tool-results/
+/scripts/
+/tests/
+/examples/
+/mini-services/
+/db/
+/prisma/
+/node_modules/
+/worklog.md
+/*.tar.gz
+/dev.log
+/server.log
+/.vercel
+/package/
diff --git a/AGENTS.md b/AGENTS.md
old mode 100644
new mode 100755
diff --git a/Caddyfile b/Caddyfile
new file mode 100755
index 0000000000000000000000000000000000000000..e4fb2a1abdc3542f710cab68b4b9d342865238f1
--- /dev/null
+++ b/Caddyfile
@@ -0,0 +1,23 @@
+:81 {
+ @transform_port_query {
+ query XTransformPort=*
+ }
+
+ handle @transform_port_query {
+ reverse_proxy localhost:{query.XTransformPort} {
+ header_up Host {host}
+ header_up X-Forwarded-For {remote_host}
+ header_up X-Forwarded-Proto {scheme}
+ header_up X-Real-IP {remote_host}
+ }
+ }
+
+ handle {
+ reverse_proxy localhost:3000 {
+ header_up Host {host}
+ header_up X-Forwarded-For {remote_host}
+ header_up X-Forwarded-Proto {scheme}
+ header_up X-Real-IP {remote_host}
+ }
+ }
+}
diff --git a/GUIDE.md b/GUIDE.md
old mode 100644
new mode 100755
index 62edb977e60110d1af26b4fb24b2dfc24295144a..6aac3439290a04c1797e2b840574a8e02412732b
--- a/GUIDE.md
+++ b/GUIDE.md
@@ -109,6 +109,44 @@ Token overrides can be scoped under `@media` or `@signal` for themes.
.hero { center: both }
```
+Each place word owns an axis (v0.8.0 — the example above used to silently
+lose the `between` because every word wrote both axes):
+
+- `between`, `around`, `evenly` are main-axis words (`justify-content`)
+- `stretch`, `baseline` are cross-axis words (`align-items`)
+- `center`, `start`, `end` fill whichever axes are still free
+
+One distribution word + one alignment word is the whole grammar. A word
+whose axes are already taken warns loudly ("has nowhere to go") instead of
+overwriting, and a word written twice warns too.
+
+### Sizing that moves: fluid
+
+`font-size: fluid 16 24` (and any length-ish property: width, height, gap,
+padding, margin…) compiles to the linear `clamp()` between 360px and 1200px
+viewports — the formula everyone writes wrong by hand. Custom bounds:
+`font-size: fluid 16 24 400 800`. The comma form still works: `fluid: 16, 24`.
+
+### raw { } — the escape hatch
+
+DSSL has no sugar for `@font-face`, `@keyframes`, `@supports`, `@import` or
+`@property` yet. `raw { }` (alias `@css { }`) drops verbatim CSS into the
+page stylesheet, top-level only:
+
+```dssl
+raw {
+ @font-face {
+ font-family: Inter
+ src: url(/fonts/inter.woff2)
+ }
+}
+```
+
+Unknown at-rules outside `raw { }` are skipped whole, with one clean error
+and a pointer here — they never leak their inner rules (that was a real
+0.7.2 bug: `@supports (display:grid) { .a { … } }` applied `.a` everywhere
+after erroring).
+
### The cascade is (layer, order)
No specificity, no `!important`. Later wins inside a layer. Layers sort
@@ -211,7 +249,7 @@ for liveness. `unfreeze()` re-arms everything.
Three ways to get it:
-1. Download `dssl-vscode/dssl-vscode-0.7.2.vsix` from the repo files, then
+1. Download `dssl-vscode/dssl-vscode-0.8.0.vsix` from the repo files, then
Extensions panel, `...`, Install from VSIX. Works in VS Code and VSCodium.
2. Build it: `cd dssl-vscode && npx @vscode/vsce package --no-dependencies`.
3. Registry listing: staged for Open VSX (the registry VSCodium searches).
@@ -222,10 +260,31 @@ did-you-mean included, line numbers accurate since the v0.7.1 parser fix),
completions folded from your workspace token registry, hover docs, a TextMate
grammar for `.dssl`.
+### The exit lane: dssl check + dssl build
+
+v0.8.0 answers the honest limits below without giving up the language:
+
+```sh
+node cli/dssl.mjs check styles.dssl --strict # CI: real compiler, exit 1 on errors, --json
+node cli/dssl.mjs build styles.dssl -o styles.css
+```
+
+`build` compiles the static subset to a real stylesheet: tokens become
+custom properties, states become `:hover`, `@media`/`@container` pass
+through, layers become `@layer`, motions become `@keyframes`, `pin` becomes
+`!important`, `raw { }` passes through verbatim, and `seen` applies its
+final state unconditionally (the graceful no-JS look). What needs the
+runtime stays out and gets counted: `@signal` rules and `animate:`.
+The same output is available in the browser as `DSSL.toCSS(source)`.
+
## 6. Tooling and tests
```sh
-node scripts/test-v072.mjs # latest regression suite (40 assertions)
+node cli/dssl.mjs check public/styles/*.dssl # compile everything, exit code for CI
+node scripts/test-v073.mjs # 0.8.0 regression suite (65 assertions)
+node scripts/test-guide-examples.mjs # every guide example compiles
+node scripts/test-guide-browser.mjs # guide examples asserted in headless Chromium
+node scripts/test-v072.mjs # 0.7.2 regression suite (40 assertions)
node scripts/test-v071.mjs # parser line-number suite
node scripts/test-v070.mjs # v0.7 feature suite (60)
node scripts/test-v050.mjs # v0.5 feature suite (61)
@@ -242,16 +301,24 @@ run the sync script so the editor extension ships the same compiler.
Read this before choosing DSSL for a real site.
-- **JavaScript is required.** No JS, no styles. The engine ships no CSS
- fallback because there is no CSS. If a page must work without JS, use CSS.
+- **JavaScript is required** for the live engine. No JS, no styles at runtime.
+ v0.8.0 adds the exit lane: `dssl build styles.dssl -o styles.css` compiles
+ the static subset to a real stylesheet you can `` instead of the
+ engine — tokens as custom properties, states as `:hover`, layers as
+ `@layer`, motions as `@keyframes`, `raw { }` verbatim. What genuinely needs
+ JS (`@signal`, `animate:`, deform, overlap) stays out and is counted in the
+ report, so you always know what a no-JS page gives up. If a page must work
+ without JS AND uses the dynamic features, use CSS.
- **First paint.** Styles are applied by the engine after it loads, so large
- pages can flash unstyled content (CLS). The docs site solves this by
- inlining its stylesheets into the SSR HTML. The planned prerender mode
- (server renders the final inline styles) is the real fix and is on the
- PARITY roadmap as the next big item.
+ pages can flash unstyled content (CLS). The build output fixes this for
+ static pages (real CSS paints before any JS). For live pages, the docs site
+ solves it by inlining its stylesheets into the SSR HTML. The planned
+ prerender mode (server renders the final inline styles) is the real fix
+ and is on the PARITY roadmap as the next big item.
- **CSP.** Pseudo-elements compile to one real stylesheet the engine injects;
strict `style-src 'self'` blocks inline style attributes and that sheet.
- DSSL wants `unsafe-inline` for styles today.
+ DSSL wants `unsafe-inline` for styles today — or use the `dssl build`
+ output, which is a normal external stylesheet and CSP-clean.
- **Interop is invasive.** DSSL normal declarations beat external CSS, and
`pin` beats external `!important`. Mixing with an existing CSS design
system works but the engine wins by design.
diff --git a/PARITY.md b/PARITY.md
old mode 100644
new mode 100755
index c1b0449decf1d8bc6c94f7cddec60d45305700eb..cf7ff956bfb6be5a570f7f4122e8296a483bde13
--- a/PARITY.md
+++ b/PARITY.md
@@ -28,9 +28,9 @@ The point of this table is to stop two bad arguments: "DSSL is missing everythin
| print styles (`@media print`) | not implemented | planned | |
| `!important` | by design no | by-design-no | `pin { }` is the must-win block; specificity wars are the thing DSSL deletes |
| Cascade | (layer, source order) | shipped | later wins; identical selectors resolve by order, deterministically |
-| Static extraction / view-source styling | live engine only today; prerender snapshot planned | planned | freeze() already makes big static pages zero-cost after first paint |
+| Static extraction / view-source styling | `dssl build` emits real CSS (v0.8.0); prerender snapshot for live pages planned | partially shipped | freeze() already makes big static pages zero-cost after first paint |
| DevTools story | `DSSL.explain(el)` returns the cascade as data | shipped | matched rules with file/line, winning declaration per property, pseudo rules; browser-extension panel planned |
-| Work without JavaScript | not possible for first paint (today) | planned (prerender) | the honest trade; content pages should wait for prerender or pick another tool |
+| Work without JavaScript | `dssl build` emits the static subset as real CSS (v0.8.0); `@signal`/`animate:` stay runtime-only and counted | shipped for the static subset; prerender snapshot still planned for live pages | the honest trade has an exit now |
## The exit story
@@ -39,6 +39,14 @@ Abandoning DSSL leaves you: a DOM of valid inline styles, one small ``.
+- When booting is done the page fires a `dssl-ready` event.
+
+## 3. Write some DSSL
+
+`examples/hello.dssl` in this package:
+
+```dssl
+body {
+ background: #f5f0e4
+ color: #26201a
+ padding: 40
+}
+
+.card {
+ background: #fdfaf2
+ border: 1 solid #ddd2bc
+ radius: 6
+ padding: 24
+
+ hover {
+ box-shadow: 0 6 18 #26201a/16
+ }
+}
+```
+
+Bare numbers are pixels. States like `hover` are nested blocks. The engine keeps them working forever, even for elements added to the page later.
+
+## The JavaScript API
+
+`window.DSSL` is available after boot:
+
+- `DSSL.load(urls)` fetch and apply one or more `.dssl` files (fetched in parallel, applied in order)
+- `DSSL.apply(source, { name, scope })` apply a source string
+- `DSSL.compile(source)` parse and resolve without touching the DOM, returns `ok`, `errors` and stats
+- `DSSL.signal(name, value)` drive styles from JS with `@signal` conditions in your `.dssl`
+- `DSSL.refresh()` force a full recompute
+- `DSSL.replay(selector)` replay entrance motions on matching elements
+- `DSSL.errors` the last reported errors (also grouped in the console, with did-you-mean hints)
+- `DSSL.version` engine version
+
+## Scoped engines
+
+```js
+DSSL.apply(source, { scope: '#widget' })
+```
+
+gives one subtree its own engine, its own tokens and its own cascade. The playground on the live site is built exactly this way.
+
+## The exit lane (v0.8.0)
+
+The engine is a JS runtime. When a page needs to work without JavaScript, sit
+behind a strict CSP, or paint before any script, build the static subset to a
+real stylesheet:
+
+```sh
+node cli/dssl.mjs check styles.dssl --strict # CI: the real compiler, exit 1 on errors, --json for machines
+node cli/dssl.mjs build styles.dssl -o styles.css
+```
+
+`build` emits real CSS: tokens become custom properties, states become
+`:hover`, layers become `@layer`, motions become `@keyframes`, `pin` becomes
+`!important`, `raw { }` passes through verbatim, and `seen` applies its final
+state (the graceful no-JS look). The report counts what stays runtime-only
+(`@signal` rules, `animate:`), so the boundary is explicit.
+
+In the browser the same output is `DSSL.toCSS(source)` → `{ css, report }`.
+
+Two more v0.8.0 tools worth knowing: `raw { }` (alias `@css { }`) drops
+verbatim CSS like `@font-face` into the page stylesheet, and
+`font-size: fluid 16 24` compiles to the linear `clamp()` between 360 and
+1200px viewports.
+
+## Files in this package
+
+- `dssl/` the engine. `boot.js` is the entry, `index.js` the public API, `static.js` the build-target compiler.
+- `cli/dssl.mjs` the CLI (`check` + `build`), no dependencies, Node 18+
+- `styles/tokens.dssl` the starter token set (colors, fonts, radii, shadows)
+- `examples/hello.dssl` a tiny starter stylesheet
+- `USAGE.md` this file
+- `LICENSE` MIT
+
+Live site: https://dssl-sable.vercel.app
+Source and full language docs: https://huggingface.co/EHS-Studio/dssl
diff --git a/engine-package/hello.dssl b/engine-package/hello.dssl
new file mode 100755
index 0000000000000000000000000000000000000000..a6e8a58936a57806fc2d688ffb868fcc9b05ad80
--- /dev/null
+++ b/engine-package/hello.dssl
@@ -0,0 +1,39 @@
+// hello.dssl — a tiny starter stylesheet
+// bare numbers are px, hover is a nested state, /14 at the end of a color is alpha
+
+body {
+ background: #f5f0e4
+ color: #26201a
+ padding: 40
+ font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif
+}
+
+h1 {
+ color: #7c2d2d
+}
+
+.card {
+ background: #fdfaf2
+ border: 1 solid #ddd2bc
+ radius: 6
+ padding: 24
+ box-shadow: 0 2 6 #26201a/14
+ transition: 200
+
+ hover {
+ box-shadow: 0 6 18 #26201a/16
+ }
+}
+
+button {
+ background: #7c2d2d
+ color: #f9efdd
+ padding: 10 18
+ radius: 6
+ border: 0
+ transition: 200
+
+ hover {
+ background: #5e1f1f
+ }
+}
diff --git a/engine-package/install.sh b/engine-package/install.sh
new file mode 100755
index 0000000000000000000000000000000000000000..8a1df49227376920e414c909c36ed2d74cdb8c9a
--- /dev/null
+++ b/engine-package/install.sh
@@ -0,0 +1,19 @@
+#!/bin/sh
+# DSSL engine installer
+# curl -fsSL https://dssl-sable.vercel.app/install.sh | sh
+set -e
+BASE="https://dssl-sable.vercel.app"
+ZIP="/tmp/dssl-engine-$$.zip"
+
+command -v curl >/dev/null 2>&1 || { echo "DSSL installer: 'curl' is required."; exit 1; }
+command -v unzip >/dev/null 2>&1 || { echo "DSSL installer: 'unzip' is required."; exit 1; }
+
+echo "fetching the DSSL engine..."
+curl -fsSL "$BASE/dssl-engine.zip" -o "$ZIP"
+rm -rf dssl-engine
+unzip -q "$ZIP" -d .
+rm -f "$ZIP"
+
+echo ""
+echo "DSSL engine installed to ./dssl-engine"
+echo "read dssl-engine/USAGE.md to wire it into a page."
diff --git a/examples/websocket/frontend.tsx b/examples/websocket/frontend.tsx
new file mode 100755
index 0000000000000000000000000000000000000000..dcc2aff5fb6cdc44c371b66d3b86493d31b2ee79
--- /dev/null
+++ b/examples/websocket/frontend.tsx
@@ -0,0 +1,196 @@
+'use client';
+
+import { useEffect, useState } from 'react';
+import { io } from 'socket.io-client';
+import { Button } from '@/components/ui/button';
+import { Input } from '@/components/ui/input';
+import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
+import { ScrollArea } from '@/components/ui/scroll-area';
+
+type User = {
+ id: string;
+ username: string;
+}
+
+type Message = {
+ id: string;
+ username: string;
+ content: string;
+ timestamp: Date | string;
+ type: 'user' | 'system';
+}
+
+export default function SocketDemo() {
+ const [messages, setMessages] = useState([]);
+ const [inputMessage, setInputMessage] = useState('');
+ const [username, setUsername] = useState('');
+ const [isUsernameSet, setIsUsernameSet] = useState(false);
+ const [socket, setSocket] = useState(null);
+ const [isConnected, setIsConnected] = useState(false);
+ const [users, setUsers] = useState([]);
+
+ useEffect(() => {
+ // Connect to websocket server
+ // Never use PORT in the URL, alyways use XTransformPort
+ // DO NOT change the path, it is used by Caddy to forward the request to the correct port
+ const socketInstance = io('/?XTransformPort=3003', {
+ transports: ['websocket', 'polling'],
+ forceNew: true,
+ reconnection: true,
+ reconnectionAttempts: 5,
+ reconnectionDelay: 1000,
+ timeout: 10000
+ })
+
+ setSocket(socketInstance);
+
+ socketInstance.on('connect', () => {
+ setIsConnected(true);
+ });
+
+ socketInstance.on('disconnect', () => {
+ setIsConnected(false);
+ });
+
+ socketInstance.on('message', (msg: Message) => {
+ setMessages(prev => [...prev, msg]);
+ });
+
+ socketInstance.on('user-joined', (data: { user: User; message: Message }) => {
+ setMessages(prev => [...prev, data.message]);
+ setUsers(prev => {
+ if (!prev.find(u => u.id === data.user.id)) {
+ return [...prev, data.user];
+ }
+ return prev;
+ });
+ });
+
+ socketInstance.on('user-left', (data: { user: User; message: Message }) => {
+ setMessages(prev => [...prev, data.message]);
+ setUsers(prev => prev.filter(u => u.id !== data.user.id));
+ });
+
+ socketInstance.on('users-list', (data: { users: User[] }) => {
+ setUsers(data.users);
+ });
+
+ return () => {
+ socketInstance.disconnect();
+ };
+ }, []);
+
+ const handleJoin = () => {
+ if (socket && username.trim() && isConnected) {
+ socket.emit('join', { username: username.trim() });
+ setIsUsernameSet(true);
+ }
+ };
+
+ const sendMessage = () => {
+ if (socket && inputMessage.trim() && username.trim()) {
+ socket.emit('message', {
+ content: inputMessage.trim(),
+ username: username.trim()
+ });
+ setInputMessage('');
+ }
+ };
+
+ const handleKeyPress = (e: React.KeyboardEvent) => {
+ if (e.key === 'Enter') {
+ sendMessage();
+ }
+ };
+
+ return (
+
+ );
+}
diff --git a/examples/websocket/server.ts b/examples/websocket/server.ts
new file mode 100755
index 0000000000000000000000000000000000000000..4db70dfb1e9ccca1a7f106ef4fadab7ed649e73a
--- /dev/null
+++ b/examples/websocket/server.ts
@@ -0,0 +1,138 @@
+import { createServer } from 'http'
+import { Server } from 'socket.io'
+
+const httpServer = createServer()
+const io = new Server(httpServer, {
+ // DO NOT change the path, it is used by Caddy to forward the request to the correct port
+ path: '/',
+ cors: {
+ origin: "*",
+ methods: ["GET", "POST"]
+ },
+ pingTimeout: 60000,
+ pingInterval: 25000,
+})
+
+interface User {
+ id: string
+ username: string
+}
+
+interface Message {
+ id: string
+ username: string
+ content: string
+ timestamp: Date
+ type: 'user' | 'system'
+}
+
+const users = new Map()
+
+const generateMessageId = () => Math.random().toString(36).substr(2, 9)
+
+const createSystemMessage = (content: string): Message => ({
+ id: generateMessageId(),
+ username: 'System',
+ content,
+ timestamp: new Date(),
+ type: 'system'
+})
+
+const createUserMessage = (username: string, content: string): Message => ({
+ id: generateMessageId(),
+ username,
+ content,
+ timestamp: new Date(),
+ type: 'user'
+})
+
+io.on('connection', (socket) => {
+ console.log(`User connected: ${socket.id}`)
+
+ // Add test event handler
+ socket.on('test', (data) => {
+ console.log('Received test message:', data)
+ socket.emit('test-response', {
+ message: 'Server received test message',
+ data: data,
+ timestamp: new Date().toISOString()
+ })
+ })
+
+ socket.on('join', (data: { username: string }) => {
+ const { username } = data
+
+ // Create user object
+ const user: User = {
+ id: socket.id,
+ username
+ }
+
+ // Add to user list
+ users.set(socket.id, user)
+
+ // Send join message to all users
+ const joinMessage = createSystemMessage(`${username} joined the chat room`)
+ io.emit('user-joined', { user, message: joinMessage })
+
+ // Send current user list to new user
+ const usersList = Array.from(users.values())
+ socket.emit('users-list', { users: usersList })
+
+ console.log(`${username} joined the chat room, current online users: ${users.size}`)
+ })
+
+ socket.on('message', (data: { content: string; username: string }) => {
+ const { content, username } = data
+ const user = users.get(socket.id)
+
+ if (user && user.username === username) {
+ const message = createUserMessage(username, content)
+ io.emit('message', message)
+ console.log(`${username}: ${content}`)
+ }
+ })
+
+ socket.on('disconnect', () => {
+ const user = users.get(socket.id)
+
+ if (user) {
+ // Remove from user list
+ users.delete(socket.id)
+
+ // Send leave message to all users
+ const leaveMessage = createSystemMessage(`${user.username} left the chat room`)
+ io.emit('user-left', { user: { id: socket.id, username: user.username }, message: leaveMessage })
+
+ console.log(`${user.username} left the chat room, current online users: ${users.size}`)
+ } else {
+ console.log(`User disconnected: ${socket.id}`)
+ }
+ })
+
+ socket.on('error', (error) => {
+ console.error(`Socket error (${socket.id}):`, error)
+ })
+})
+
+const PORT = 3003
+httpServer.listen(PORT, () => {
+ console.log(`WebSocket server running on port ${PORT}`)
+})
+
+// Graceful shutdown
+process.on('SIGTERM', () => {
+ console.log('Received SIGTERM signal, shutting down server...')
+ httpServer.close(() => {
+ console.log('WebSocket server closed')
+ process.exit(0)
+ })
+})
+
+process.on('SIGINT', () => {
+ console.log('Received SIGINT signal, shutting down server...')
+ httpServer.close(() => {
+ console.log('WebSocket server closed')
+ process.exit(0)
+ })
+})
\ No newline at end of file
diff --git a/llms.txt b/llms.txt
index 1e185cb44368ab7be89bc4c8d8c148cef355abaa..789eb21fe169cdd270a4174b238f5be131a724ef 100755
--- a/llms.txt
+++ b/llms.txt
@@ -1,28 +1,28 @@
# DSSL
-> DSSL is a style language for the web. For JS-guaranteed pages it replaces writing CSS; it does not replace the CSS platform (the engine ends in inline styles the browser parses). You write `.dssl` files, the page loads a ~20 kB engine, and the engine walks the DOM in real time applying every matched rule as inline styles. Nothing compiles to CSS. Elements added later are styled the moment they appear; signals, states and motion share one grammar.
+> 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 a ~20 kB engine, 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.
- Site (docs, playground, changelog, bench): https://dssl-sable.vercel.app
- Repo + full docs in the README: https://huggingface.co/EHS-Studio/dssl
- Install: `curl -fsSL https://dssl-sable.vercel.app/install.sh | sh` (or the zip: https://dssl-sable.vercel.app/dssl-engine.zip)
-- Version: 0.7.2 (engine), MIT license
+- Version: 0.8.0 (engine), MIT license
- Feature matrix vs CSS: see PARITY.md in the repo
- For agents working ON the codebase: see AGENTS.md in the repo
## The five things reviewers ask about
1. No build step. Styles are source files linked with `` or embedded in the page as `
+
+