Z User commited on
Commit
15d6beb
·
1 Parent(s): a53475b

sync work in progress

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitignore +54 -2
  2. bun.lock +0 -0
  3. components.json +21 -0
  4. eslint.config.mjs +50 -0
  5. examples/websocket/frontend.tsx +196 -0
  6. examples/websocket/server.ts +138 -0
  7. next.config.ts +12 -0
  8. package.json +94 -0
  9. postcss.config.mjs +5 -0
  10. prisma/schema.prisma +32 -0
  11. public/dssl/boot.js +2 -0
  12. public/dssl/engine.js +447 -0
  13. public/dssl/index.js +162 -0
  14. public/dssl/parser.js +228 -0
  15. public/dssl/resolver.js +776 -0
  16. public/dssl/utils.js +34 -0
  17. public/favicon.svg +5 -0
  18. public/logo.svg +29 -0
  19. public/robots.txt +14 -0
  20. public/styles/changelog.dssl +97 -0
  21. public/styles/docs.dssl +321 -0
  22. public/styles/home.dssl +422 -0
  23. public/styles/playground.dssl +180 -0
  24. public/styles/site.dssl +472 -0
  25. public/styles/tokens.dssl +104 -0
  26. scripts/dssl-smoke.mjs +131 -0
  27. scripts/dssl-styles-check.mjs +28 -0
  28. src/app/api/route.ts +5 -0
  29. src/app/globals.css +2 -0
  30. src/app/layout.tsx +35 -0
  31. src/app/page.tsx +5 -0
  32. src/components/dssl/CodeBlock.tsx +46 -0
  33. src/components/dssl/SiteRoot.tsx +189 -0
  34. src/components/dssl/highlight.ts +87 -0
  35. src/components/dssl/icons.tsx +58 -0
  36. src/components/dssl/views/ChangelogView.tsx +178 -0
  37. src/components/dssl/views/DocsView.tsx +400 -0
  38. src/components/dssl/views/HomeView.tsx +331 -0
  39. src/components/dssl/views/PlaygroundView.tsx +387 -0
  40. src/components/dssl/views/SpecView.tsx +204 -0
  41. src/components/ui/accordion.tsx +66 -0
  42. src/components/ui/alert-dialog.tsx +157 -0
  43. src/components/ui/alert.tsx +66 -0
  44. src/components/ui/aspect-ratio.tsx +11 -0
  45. src/components/ui/avatar.tsx +53 -0
  46. src/components/ui/badge.tsx +46 -0
  47. src/components/ui/breadcrumb.tsx +109 -0
  48. src/components/ui/button.tsx +59 -0
  49. src/components/ui/calendar.tsx +213 -0
  50. src/components/ui/card.tsx +92 -0
.gitignore CHANGED
@@ -1,2 +1,54 @@
1
- skills/
2
- node_modules/
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2
+
3
+ # dependencies
4
+ node_modules
5
+ /.pnp
6
+ .pnp.*
7
+ .yarn/*
8
+ !.yarn/patches
9
+ !.yarn/plugins
10
+ !.yarn/releases
11
+ !.yarn/versions
12
+
13
+ # testing
14
+ /coverage
15
+
16
+ # next.js
17
+ /.next/
18
+ /out/
19
+
20
+ # production
21
+ /build
22
+
23
+ # misc
24
+ .DS_Store
25
+ *.pem
26
+
27
+ # debug
28
+ npm-debug.log*
29
+ yarn-debug.log*
30
+ yarn-error.log*
31
+ .pnpm-debug.log*
32
+
33
+ # env files (can opt-in for committing if needed)
34
+ .env*
35
+
36
+ # vercel
37
+ .vercel
38
+
39
+
40
+ # typescript
41
+ *.tsbuildinfo
42
+ next-env.d.ts
43
+ local-*
44
+ .claude
45
+ .z-ai-config
46
+ *.log
47
+ dev.log
48
+ dev.out.log
49
+ test
50
+ prompt
51
+
52
+ server.log
53
+ # Skills directory
54
+ /skills/
bun.lock ADDED
The diff for this file is too large to render. See raw diff
 
components.json ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema.json",
3
+ "style": "new-york",
4
+ "rsc": true,
5
+ "tsx": true,
6
+ "tailwind": {
7
+ "config": "",
8
+ "css": "src/app/globals.css",
9
+ "baseColor": "neutral",
10
+ "cssVariables": true,
11
+ "prefix": ""
12
+ },
13
+ "aliases": {
14
+ "components": "@/components",
15
+ "utils": "@/lib/utils",
16
+ "ui": "@/components/ui",
17
+ "lib": "@/lib",
18
+ "hooks": "@/hooks"
19
+ },
20
+ "iconLibrary": "lucide"
21
+ }
eslint.config.mjs ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import nextCoreWebVitals from "eslint-config-next/core-web-vitals";
2
+ import nextTypescript from "eslint-config-next/typescript";
3
+ import { dirname } from "path";
4
+ import { fileURLToPath } from "url";
5
+
6
+ const __filename = fileURLToPath(import.meta.url);
7
+ const __dirname = dirname(__filename);
8
+
9
+ const eslintConfig = [...nextCoreWebVitals, ...nextTypescript, {
10
+ rules: {
11
+ // TypeScript rules
12
+ "@typescript-eslint/no-explicit-any": "off",
13
+ "@typescript-eslint/no-unused-vars": "off",
14
+ "@typescript-eslint/no-non-null-assertion": "off",
15
+ "@typescript-eslint/ban-ts-comment": "off",
16
+ "@typescript-eslint/prefer-as-const": "off",
17
+ "@typescript-eslint/no-unused-disable-directive": "off",
18
+
19
+ // React rules
20
+ "react-hooks/exhaustive-deps": "off",
21
+ "react-hooks/purity": "off",
22
+ "react/no-unescaped-entities": "off",
23
+ "react/display-name": "off",
24
+ "react/prop-types": "off",
25
+ "react-compiler/react-compiler": "off",
26
+
27
+ // Next.js rules
28
+ "@next/next/no-img-element": "off",
29
+ "@next/next/no-html-link-for-pages": "off",
30
+
31
+ // General JavaScript rules
32
+ "prefer-const": "off",
33
+ "no-unused-vars": "off",
34
+ "no-console": "off",
35
+ "no-debugger": "off",
36
+ "no-empty": "off",
37
+ "no-irregular-whitespace": "off",
38
+ "no-case-declarations": "off",
39
+ "no-fallthrough": "off",
40
+ "no-mixed-spaces-and-tabs": "off",
41
+ "no-redeclare": "off",
42
+ "no-undef": "off",
43
+ "no-unreachable": "off",
44
+ "no-useless-escape": "off",
45
+ },
46
+ }, {
47
+ ignores: ["node_modules/**", ".next/**", "out/**", "build/**", "next-env.d.ts", "examples/**", "skills"]
48
+ }];
49
+
50
+ export default eslintConfig;
examples/websocket/frontend.tsx ADDED
@@ -0,0 +1,196 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client';
2
+
3
+ import { useEffect, useState } from 'react';
4
+ import { io } from 'socket.io-client';
5
+ import { Button } from '@/components/ui/button';
6
+ import { Input } from '@/components/ui/input';
7
+ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
8
+ import { ScrollArea } from '@/components/ui/scroll-area';
9
+
10
+ type User = {
11
+ id: string;
12
+ username: string;
13
+ }
14
+
15
+ type Message = {
16
+ id: string;
17
+ username: string;
18
+ content: string;
19
+ timestamp: Date | string;
20
+ type: 'user' | 'system';
21
+ }
22
+
23
+ export default function SocketDemo() {
24
+ const [messages, setMessages] = useState<Message[]>([]);
25
+ const [inputMessage, setInputMessage] = useState('');
26
+ const [username, setUsername] = useState('');
27
+ const [isUsernameSet, setIsUsernameSet] = useState(false);
28
+ const [socket, setSocket] = useState<any>(null);
29
+ const [isConnected, setIsConnected] = useState(false);
30
+ const [users, setUsers] = useState<User[]>([]);
31
+
32
+ useEffect(() => {
33
+ // Connect to websocket server
34
+ // Never use PORT in the URL, alyways use XTransformPort
35
+ // DO NOT change the path, it is used by Caddy to forward the request to the correct port
36
+ const socketInstance = io('/?XTransformPort=3003', {
37
+ transports: ['websocket', 'polling'],
38
+ forceNew: true,
39
+ reconnection: true,
40
+ reconnectionAttempts: 5,
41
+ reconnectionDelay: 1000,
42
+ timeout: 10000
43
+ })
44
+
45
+ setSocket(socketInstance);
46
+
47
+ socketInstance.on('connect', () => {
48
+ setIsConnected(true);
49
+ });
50
+
51
+ socketInstance.on('disconnect', () => {
52
+ setIsConnected(false);
53
+ });
54
+
55
+ socketInstance.on('message', (msg: Message) => {
56
+ setMessages(prev => [...prev, msg]);
57
+ });
58
+
59
+ socketInstance.on('user-joined', (data: { user: User; message: Message }) => {
60
+ setMessages(prev => [...prev, data.message]);
61
+ setUsers(prev => {
62
+ if (!prev.find(u => u.id === data.user.id)) {
63
+ return [...prev, data.user];
64
+ }
65
+ return prev;
66
+ });
67
+ });
68
+
69
+ socketInstance.on('user-left', (data: { user: User; message: Message }) => {
70
+ setMessages(prev => [...prev, data.message]);
71
+ setUsers(prev => prev.filter(u => u.id !== data.user.id));
72
+ });
73
+
74
+ socketInstance.on('users-list', (data: { users: User[] }) => {
75
+ setUsers(data.users);
76
+ });
77
+
78
+ return () => {
79
+ socketInstance.disconnect();
80
+ };
81
+ }, []);
82
+
83
+ const handleJoin = () => {
84
+ if (socket && username.trim() && isConnected) {
85
+ socket.emit('join', { username: username.trim() });
86
+ setIsUsernameSet(true);
87
+ }
88
+ };
89
+
90
+ const sendMessage = () => {
91
+ if (socket && inputMessage.trim() && username.trim()) {
92
+ socket.emit('message', {
93
+ content: inputMessage.trim(),
94
+ username: username.trim()
95
+ });
96
+ setInputMessage('');
97
+ }
98
+ };
99
+
100
+ const handleKeyPress = (e: React.KeyboardEvent) => {
101
+ if (e.key === 'Enter') {
102
+ sendMessage();
103
+ }
104
+ };
105
+
106
+ return (
107
+ <div className="container mx-auto p-4 max-w-2xl">
108
+ <Card>
109
+ <CardHeader>
110
+ <CardTitle className="flex items-center justify-between">
111
+ WebSocket Demo
112
+ <span className={`text-sm px-2 py-1 rounded ${isConnected ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'}`}>
113
+ {isConnected ? 'Connected' : 'Disconnected'}
114
+ </span>
115
+ </CardTitle>
116
+ </CardHeader>
117
+ <CardContent className="space-y-4">
118
+ {!isUsernameSet ? (
119
+ <div className="space-y-2">
120
+ <Input
121
+ value={username}
122
+ onChange={(e) => setUsername(e.target.value)}
123
+ onKeyPress={(e) => {
124
+ if (e.key === 'Enter') {
125
+ handleJoin();
126
+ }
127
+ }}
128
+ placeholder="Enter your username..."
129
+ disabled={!isConnected}
130
+ className="flex-1"
131
+ />
132
+ <Button
133
+ onClick={handleJoin}
134
+ disabled={!isConnected || !username.trim()}
135
+ className="w-full"
136
+ >
137
+ Join Chat
138
+ </Button>
139
+ </div>
140
+ ) : (
141
+ <>
142
+ <ScrollArea className="h-80 w-full border rounded-md p-4">
143
+ <div className="space-y-2">
144
+ {messages.length === 0 ? (
145
+ <p className="text-gray-500 text-center">No messages yet</p>
146
+ ) : (
147
+ messages.map((msg) => (
148
+ <div key={msg.id} className="border-b pb-2 last:border-b-0">
149
+ <div className="flex justify-between items-start">
150
+ <div className="flex-1">
151
+ <p className={`text-sm font-medium ${msg.type === 'system'
152
+ ? 'text-blue-600 italic'
153
+ : 'text-gray-700'
154
+ }`}>
155
+ {msg.username}
156
+ </p>
157
+ <p className={`${msg.type === 'system'
158
+ ? 'text-blue-500 italic'
159
+ : 'text-gray-900'
160
+ }`}>
161
+ {msg.content}
162
+ </p>
163
+ </div>
164
+ <span className="text-xs text-gray-500">
165
+ {new Date(msg.timestamp).toLocaleTimeString()}
166
+ </span>
167
+ </div>
168
+ </div>
169
+ ))
170
+ )}
171
+ </div>
172
+ </ScrollArea>
173
+
174
+ <div className="flex space-x-2">
175
+ <Input
176
+ value={inputMessage}
177
+ onChange={(e) => setInputMessage(e.target.value)}
178
+ onKeyPress={handleKeyPress}
179
+ placeholder="Type a message..."
180
+ disabled={!isConnected}
181
+ className="flex-1"
182
+ />
183
+ <Button
184
+ onClick={sendMessage}
185
+ disabled={!isConnected || !inputMessage.trim()}
186
+ >
187
+ Send
188
+ </Button>
189
+ </div>
190
+ </>
191
+ )}
192
+ </CardContent>
193
+ </Card>
194
+ </div>
195
+ );
196
+ }
examples/websocket/server.ts ADDED
@@ -0,0 +1,138 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { createServer } from 'http'
2
+ import { Server } from 'socket.io'
3
+
4
+ const httpServer = createServer()
5
+ const io = new Server(httpServer, {
6
+ // DO NOT change the path, it is used by Caddy to forward the request to the correct port
7
+ path: '/',
8
+ cors: {
9
+ origin: "*",
10
+ methods: ["GET", "POST"]
11
+ },
12
+ pingTimeout: 60000,
13
+ pingInterval: 25000,
14
+ })
15
+
16
+ interface User {
17
+ id: string
18
+ username: string
19
+ }
20
+
21
+ interface Message {
22
+ id: string
23
+ username: string
24
+ content: string
25
+ timestamp: Date
26
+ type: 'user' | 'system'
27
+ }
28
+
29
+ const users = new Map<string, User>()
30
+
31
+ const generateMessageId = () => Math.random().toString(36).substr(2, 9)
32
+
33
+ const createSystemMessage = (content: string): Message => ({
34
+ id: generateMessageId(),
35
+ username: 'System',
36
+ content,
37
+ timestamp: new Date(),
38
+ type: 'system'
39
+ })
40
+
41
+ const createUserMessage = (username: string, content: string): Message => ({
42
+ id: generateMessageId(),
43
+ username,
44
+ content,
45
+ timestamp: new Date(),
46
+ type: 'user'
47
+ })
48
+
49
+ io.on('connection', (socket) => {
50
+ console.log(`User connected: ${socket.id}`)
51
+
52
+ // Add test event handler
53
+ socket.on('test', (data) => {
54
+ console.log('Received test message:', data)
55
+ socket.emit('test-response', {
56
+ message: 'Server received test message',
57
+ data: data,
58
+ timestamp: new Date().toISOString()
59
+ })
60
+ })
61
+
62
+ socket.on('join', (data: { username: string }) => {
63
+ const { username } = data
64
+
65
+ // Create user object
66
+ const user: User = {
67
+ id: socket.id,
68
+ username
69
+ }
70
+
71
+ // Add to user list
72
+ users.set(socket.id, user)
73
+
74
+ // Send join message to all users
75
+ const joinMessage = createSystemMessage(`${username} joined the chat room`)
76
+ io.emit('user-joined', { user, message: joinMessage })
77
+
78
+ // Send current user list to new user
79
+ const usersList = Array.from(users.values())
80
+ socket.emit('users-list', { users: usersList })
81
+
82
+ console.log(`${username} joined the chat room, current online users: ${users.size}`)
83
+ })
84
+
85
+ socket.on('message', (data: { content: string; username: string }) => {
86
+ const { content, username } = data
87
+ const user = users.get(socket.id)
88
+
89
+ if (user && user.username === username) {
90
+ const message = createUserMessage(username, content)
91
+ io.emit('message', message)
92
+ console.log(`${username}: ${content}`)
93
+ }
94
+ })
95
+
96
+ socket.on('disconnect', () => {
97
+ const user = users.get(socket.id)
98
+
99
+ if (user) {
100
+ // Remove from user list
101
+ users.delete(socket.id)
102
+
103
+ // Send leave message to all users
104
+ const leaveMessage = createSystemMessage(`${user.username} left the chat room`)
105
+ io.emit('user-left', { user: { id: socket.id, username: user.username }, message: leaveMessage })
106
+
107
+ console.log(`${user.username} left the chat room, current online users: ${users.size}`)
108
+ } else {
109
+ console.log(`User disconnected: ${socket.id}`)
110
+ }
111
+ })
112
+
113
+ socket.on('error', (error) => {
114
+ console.error(`Socket error (${socket.id}):`, error)
115
+ })
116
+ })
117
+
118
+ const PORT = 3003
119
+ httpServer.listen(PORT, () => {
120
+ console.log(`WebSocket server running on port ${PORT}`)
121
+ })
122
+
123
+ // Graceful shutdown
124
+ process.on('SIGTERM', () => {
125
+ console.log('Received SIGTERM signal, shutting down server...')
126
+ httpServer.close(() => {
127
+ console.log('WebSocket server closed')
128
+ process.exit(0)
129
+ })
130
+ })
131
+
132
+ process.on('SIGINT', () => {
133
+ console.log('Received SIGINT signal, shutting down server...')
134
+ httpServer.close(() => {
135
+ console.log('WebSocket server closed')
136
+ process.exit(0)
137
+ })
138
+ })
next.config.ts ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { NextConfig } from "next";
2
+
3
+ const nextConfig: NextConfig = {
4
+ output: "standalone",
5
+ /* config options here */
6
+ typescript: {
7
+ ignoreBuildErrors: true,
8
+ },
9
+ reactStrictMode: false,
10
+ };
11
+
12
+ export default nextConfig;
package.json ADDED
@@ -0,0 +1,94 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "nextjs_tailwind_shadcn_ts",
3
+ "version": "0.2.1",
4
+ "private": true,
5
+ "scripts": {
6
+ "dev": "next dev -p 3000 2>&1 | tee dev.log",
7
+ "build": "next build && cp -r .next/static .next/standalone/.next/ && cp -r public .next/standalone/",
8
+ "start": "NODE_ENV=production bun .next/standalone/server.js 2>&1 | tee server.log",
9
+ "lint": "eslint .",
10
+ "db:push": "prisma db push --accept-data-loss",
11
+ "db:generate": "prisma generate",
12
+ "db:migrate": "prisma migrate dev",
13
+ "db:reset": "prisma migrate reset"
14
+ },
15
+ "dependencies": {
16
+ "@dnd-kit/core": "^6.3.1",
17
+ "@dnd-kit/sortable": "^10.0.0",
18
+ "@dnd-kit/utilities": "^3.2.2",
19
+ "@hookform/resolvers": "^5.1.1",
20
+ "@mdxeditor/editor": "^3.39.1",
21
+ "@prisma/client": "^6.11.1",
22
+ "@radix-ui/react-accordion": "^1.2.11",
23
+ "@radix-ui/react-alert-dialog": "^1.1.14",
24
+ "@radix-ui/react-aspect-ratio": "^1.1.7",
25
+ "@radix-ui/react-avatar": "^1.1.10",
26
+ "@radix-ui/react-checkbox": "^1.3.2",
27
+ "@radix-ui/react-collapsible": "^1.1.11",
28
+ "@radix-ui/react-context-menu": "^2.2.15",
29
+ "@radix-ui/react-dialog": "^1.1.14",
30
+ "@radix-ui/react-dropdown-menu": "^2.1.15",
31
+ "@radix-ui/react-hover-card": "^1.1.14",
32
+ "@radix-ui/react-label": "^2.1.7",
33
+ "@radix-ui/react-menubar": "^1.1.15",
34
+ "@radix-ui/react-navigation-menu": "^1.2.13",
35
+ "@radix-ui/react-popover": "^1.1.14",
36
+ "@radix-ui/react-progress": "^1.1.7",
37
+ "@radix-ui/react-radio-group": "^1.3.7",
38
+ "@radix-ui/react-scroll-area": "^1.2.9",
39
+ "@radix-ui/react-select": "^2.2.5",
40
+ "@radix-ui/react-separator": "^1.1.7",
41
+ "@radix-ui/react-slider": "^1.3.5",
42
+ "@radix-ui/react-slot": "^1.2.3",
43
+ "@radix-ui/react-switch": "^1.2.5",
44
+ "@radix-ui/react-tabs": "^1.1.12",
45
+ "@radix-ui/react-toast": "^1.2.14",
46
+ "@radix-ui/react-toggle": "^1.1.9",
47
+ "@radix-ui/react-toggle-group": "^1.1.10",
48
+ "@radix-ui/react-tooltip": "^1.2.7",
49
+ "@reactuses/core": "^6.0.5",
50
+ "@tanstack/react-query": "^5.82.0",
51
+ "@tanstack/react-table": "^8.21.3",
52
+ "class-variance-authority": "^0.7.1",
53
+ "clsx": "^2.1.1",
54
+ "cmdk": "^1.1.1",
55
+ "date-fns": "^4.1.0",
56
+ "embla-carousel-react": "^8.6.0",
57
+ "framer-motion": "^12.23.2",
58
+ "input-otp": "^1.4.2",
59
+ "lucide-react": "^0.525.0",
60
+ "next": "^16.1.1",
61
+ "next-auth": "^4.24.11",
62
+ "next-intl": "^4.3.4",
63
+ "next-themes": "^0.4.6",
64
+ "prisma": "^6.11.1",
65
+ "react": "^19.0.0",
66
+ "react-day-picker": "^9.8.0",
67
+ "react-dom": "^19.0.0",
68
+ "react-hook-form": "^7.60.0",
69
+ "react-markdown": "^10.1.0",
70
+ "react-resizable-panels": "^3.0.3",
71
+ "react-syntax-highlighter": "^15.6.1",
72
+ "recharts": "^2.15.4",
73
+ "sharp": "^0.34.3",
74
+ "sonner": "^2.0.6",
75
+ "tailwind-merge": "^3.3.1",
76
+ "tailwindcss-animate": "^1.0.7",
77
+ "uuid": "^11.1.0",
78
+ "vaul": "^1.1.2",
79
+ "z-ai-web-dev-sdk": "^0.0.18",
80
+ "zod": "^4.0.2",
81
+ "zustand": "^5.0.6"
82
+ },
83
+ "devDependencies": {
84
+ "@tailwindcss/postcss": "^4",
85
+ "@types/react": "^19",
86
+ "@types/react-dom": "^19",
87
+ "bun-types": "^1.3.4",
88
+ "eslint": "^9",
89
+ "eslint-config-next": "^16.1.1",
90
+ "tailwindcss": "^4",
91
+ "tw-animate-css": "^1.3.5",
92
+ "typescript": "^5"
93
+ }
94
+ }
postcss.config.mjs ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ const config = {
2
+ plugins: ["@tailwindcss/postcss"],
3
+ };
4
+
5
+ export default config;
prisma/schema.prisma ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // This is your Prisma schema file,
2
+ // learn more about it in the docs: https://pris.ly/d/prisma-schema
3
+
4
+ // Looking for ways to speed up your queries, or scale easily with your serverless or edge functions?
5
+ // Try Prisma Accelerate: https://pris.ly/cli/accelerate-init
6
+
7
+ generator client {
8
+ provider = "prisma-client-js"
9
+ }
10
+
11
+ datasource db {
12
+ provider = "sqlite"
13
+ url = env("DATABASE_URL")
14
+ }
15
+
16
+ model User {
17
+ id String @id @default(cuid())
18
+ email String @unique
19
+ name String?
20
+ createdAt DateTime @default(now())
21
+ updatedAt DateTime @updatedAt
22
+ }
23
+
24
+ model Post {
25
+ id String @id @default(cuid())
26
+ title String
27
+ content String?
28
+ published Boolean @default(false)
29
+ authorId String
30
+ createdAt DateTime @default(now())
31
+ updatedAt DateTime @updatedAt
32
+ }
public/dssl/boot.js ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ // boot.js — minimal entry so a plain <script type="module"> can load DSSL
2
+ import './index.js';
public/dssl/engine.js ADDED
@@ -0,0 +1,447 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* =========================================================================
2
+ * DSSL — engine.js
3
+ * The style engine. No CSS output for regular rules: DSSL matches selectors
4
+ * against the live DOM itself and applies winning declarations directly to
5
+ * elements (inline style), running its own cascade:
6
+ *
7
+ * cascade = (layer, source order) — no specificity, ever
8
+ * layers: tokens < base < components < utilities < pin
9
+ *
10
+ * States (hover/focus/active/…) are implemented by the engine with event
11
+ * delegation + live `matches()` checks. `@media` uses matchMedia listeners,
12
+ * `@signal` is driven from JS, `@container` is evaluated against the nearest
13
+ * tracked container. Motion runs through the Web Animations API.
14
+ *
15
+ * Pseudo-elements (::before/::after/…) cannot be expressed inline, so they
16
+ * are the one thing compiled to real CSS in a shadow <style> tag — using the
17
+ * same tokens via var().
18
+ * ========================================================================= */
19
+
20
+ const LAYER_RANK = { tokens: 0, base: 1, components: 2, utilities: 3, pin: 4 };
21
+
22
+ function parseSizeQuery(q) {
23
+ // supports (min-width: 480px) | (max-width: 480px) | (width >= 480px) | (width <= 480px)
24
+ let m = q.match(/min-width\s*:\s*([0-9.]+)(px)?/) || q.match(/width\s*>=\s*([0-9.]+)(px)?/);
25
+ const min = m ? parseFloat(m[1]) : null;
26
+ m = q.match(/max-width\s*:\s*([0-9.]+)(px)?/) || q.match(/width\s*<=\s*([0-9.]+)(px)?/);
27
+ const max = m ? parseFloat(m[1]) : null;
28
+ return { min, max };
29
+ }
30
+
31
+ export class DsslEngine {
32
+ constructor({ scope = null, name = 'global', reset = false } = {}) {
33
+ this.name = name;
34
+ this.scope = scope || null; // null = whole document
35
+ this.reset = reset;
36
+ this.bundles = new Map(); // bundleName -> resolved doc
37
+ this.rules = [];
38
+ this.shadowRules = [];
39
+ this.signalStore = new Map();
40
+ this.mqlCache = new Map();
41
+ this.containerEls = new Set();
42
+ this.ownMap = new WeakMap(); // element -> [props DSSL owns]
43
+ this.animMap = new WeakMap(); // element -> Map(key -> Animation)
44
+ this.stackPrev = new WeakMap(); // element -> [children we grid-area'd]
45
+ this.touched = new Set(); // every element we ever styled (so removals can be cleaned)
46
+ this.pending = new Set();
47
+ this.scheduled = false;
48
+ this.initialized = false;
49
+ this.shadowStyle = null;
50
+ this.seq = 0; // bundle insertion order
51
+ this.lastApply = 0;
52
+ }
53
+
54
+ /* ------------------------------------------------------------ *
55
+ * Bundles
56
+ * ------------------------------------------------------------ */
57
+
58
+ hasBundle(name) { return this.bundles.has(name); }
59
+
60
+ applyBundle(name, doc, { replace = false } = {}) {
61
+ // keep the original insertion order when replacing a bundle in place
62
+ const prev = this.bundles.get(name);
63
+ const seq = prev ? prev.seq : this.seq++;
64
+ if (replace) this.bundles.delete(name);
65
+ this.bundles.set(name, { seq, ...doc });
66
+ this.rebuild();
67
+ }
68
+
69
+ removeBundle(name) {
70
+ if (!this.bundles.has(name)) return;
71
+ this.bundles.delete(name);
72
+ this.rebuild();
73
+ }
74
+
75
+ rebuild() {
76
+ const rules = [];
77
+ const shadowRules = [];
78
+ const bundles = [...this.bundles.values()].sort((a, b) => a.seq - b.seq);
79
+ for (const b of bundles) {
80
+ for (const r of b.rules) {
81
+ rules.push(r); // scoped engines map :root rules onto the scope element
82
+ }
83
+ shadowRules.push(...b.shadowRules);
84
+ }
85
+ this.rules = rules;
86
+ this.shadowRules = shadowRules;
87
+
88
+ // unique media queries → MediaQueryList cache
89
+ for (const r of rules) for (const q of r.media) this.mql(q);
90
+ for (const r of shadowRules) for (const q of r.media) this.mql(q);
91
+
92
+ this.init();
93
+ this.renderShadow();
94
+ this.computeAll();
95
+ }
96
+
97
+ mql(query) {
98
+ let m = this.mqlCache.get(query);
99
+ if (!m) {
100
+ try { m = window.matchMedia(query); } catch { m = { matches: false, addEventListener() {} }; }
101
+ this.mqlCache.set(query, m);
102
+ }
103
+ return m;
104
+ }
105
+
106
+ /* ------------------------------------------------------------ *
107
+ * One-time wiring: events, observers, media listeners
108
+ * ------------------------------------------------------------ */
109
+
110
+ init() {
111
+ if (this.initialized) return;
112
+ this.initialized = true;
113
+
114
+ const root = this.scope || document;
115
+ const doc = this.scope ? this.scope : document.documentElement;
116
+
117
+ // --- element state events ---
118
+ const on = (type, fn) => root.addEventListener(type, fn, { passive: true, capture: type === 'focusin' || type === 'focusout' });
119
+ on('pointerover', (e) => this.stateEvent('hover', e.target));
120
+ on('pointerout', (e) => this.stateEvent('hover', e.target));
121
+ on('focusin', (e) => { this.stateEvent('focus', e.target); this.stateEvent('focus-visible', e.target); });
122
+ on('focusout', (e) => { this.stateEvent('focus', e.target); this.stateEvent('focus-visible', e.target); });
123
+ on('pointerdown', (e) => this.stateEvent('active', e.target));
124
+ on('pointerup', (e) => this.stateEvent('active', e.target));
125
+ on('pointercancel', (e) => this.stateEvent('active', e.target));
126
+
127
+ // --- DOM mutations ---
128
+ const mo = new MutationObserver((muts) => {
129
+ if (this._applying) return; // our own style writes — ignore
130
+ const batch = new Set();
131
+ for (const m of muts) {
132
+ if (m.type === 'attributes') {
133
+ if (m.attributeName === 'style') continue;
134
+ if (m.target === document.documentElement && m.attributeName.startsWith('data-dssl')) continue;
135
+ batch.add(m.target);
136
+ // class/attr changes can affect descendant rules — include small subtrees
137
+ if (m.target.querySelectorAll) {
138
+ const kids = m.target.querySelectorAll('*');
139
+ if (kids.length <= 300) kids.forEach(k => batch.add(k));
140
+ }
141
+ } else {
142
+ if (m.addedNodes) m.addedNodes.forEach(n => {
143
+ if (n.nodeType !== 1) return;
144
+ batch.add(n);
145
+ if (n.querySelectorAll) n.querySelectorAll('*').forEach(k => batch.add(k));
146
+ });
147
+ if (m.removedNodes) m.removedNodes.forEach(n => {
148
+ if (n.nodeType !== 1) return;
149
+ this.touched.delete(n);
150
+ if (n.querySelectorAll) n.querySelectorAll('*').forEach(k => this.touched.delete(k));
151
+ });
152
+ }
153
+ }
154
+ if (batch.size) this.schedule(batch);
155
+ });
156
+ mo.observe(doc, { childList: true, subtree: true, attributes: true, attributeFilter: undefined });
157
+ this._mo = mo;
158
+
159
+ // --- media changes ---
160
+ // (matchMedia listeners are added lazily per query in mql(); rewire here)
161
+ const origMql = this.mql.bind(this);
162
+ this.mql = (query) => {
163
+ const m = origMql(query);
164
+ if (!m._dsslWired) {
165
+ m._dsslWired = true;
166
+ try { m.addEventListener('change', () => this.computeAll()); } catch { /* old browsers */ }
167
+ }
168
+ return m;
169
+ };
170
+
171
+ // --- reduced motion ---
172
+ try {
173
+ const rm = window.matchMedia('(prefers-reduced-motion: reduce)');
174
+ rm.addEventListener('change', () => this.computeAll());
175
+ this.reducedMotion = rm;
176
+ } catch { this.reducedMotion = { matches: false }; }
177
+
178
+ // --- container tracking via ResizeObserver ---
179
+ this.ro = new ResizeObserver(() => this.computeAll());
180
+ }
181
+
182
+ stateEvent(state, target) {
183
+ if (!target || target.nodeType !== 1) return;
184
+ const batch = new Set();
185
+ for (const rule of this.rules) {
186
+ const hit = rule.states.has(state) || (state === 'focus' && rule.states.has('focus-visible')) || (state === 'focus-visible' && rule.states.has('focus'));
187
+ if (!hit) continue;
188
+ let els;
189
+ try { els = (this.scope || document).querySelectorAll(rule.sel); } catch { continue; }
190
+ els.forEach(el => batch.add(el));
191
+ }
192
+ if (batch.size) this.schedule(batch);
193
+ }
194
+
195
+ /* ------------------------------------------------------------ *
196
+ * Scheduling + cascade application
197
+ * ------------------------------------------------------------ */
198
+
199
+ schedule(els) {
200
+ els.forEach(el => this.pending.add(el));
201
+ if (this.scheduled) return;
202
+ this.scheduled = true;
203
+ queueMicrotask(() => {
204
+ this.scheduled = false;
205
+ const batch = [...this.pending];
206
+ this.pending.clear();
207
+ this._applying = true;
208
+ for (const el of batch) this.computeElement(el);
209
+ this._applying = false;
210
+ });
211
+ }
212
+
213
+ matches(el, sel) {
214
+ try { return el.matches(sel); } catch { return false; }
215
+ }
216
+
217
+ inScope(el) {
218
+ if (!this.scope) return true;
219
+ return this.scope === el || this.scope.contains(el);
220
+ }
221
+
222
+ signalsPass(rule) {
223
+ for (const s of rule.signals) {
224
+ const has = this.signalStore.has(s.name);
225
+ const val = this.signalStore.get(s.name);
226
+ if (s.op === 'truthy') { if (!has || val === false || val === null || val === 'false') return false; }
227
+ else if (s.op === '==') { if (!has || String(val) !== String(s.value)) return false; }
228
+ else if (s.op === '!=') { if (has && String(val) === String(s.value)) return false; }
229
+ }
230
+ return true;
231
+ }
232
+
233
+ containerPass(el, rule) {
234
+ if (!rule.containers.length) return true;
235
+ // nearest ancestor (or self) with container-type set via DSSL
236
+ let node = el;
237
+ while (node) {
238
+ if (this.containerEls.has(node)) break;
239
+ node = node.parentElement;
240
+ }
241
+ if (!node) return false;
242
+ const w = node.clientWidth;
243
+ for (const q of rule.containers) {
244
+ const { min, max } = parseSizeQuery(q);
245
+ if (min !== null && w < min) return false;
246
+ if (max !== null && w > max) return false;
247
+ }
248
+ return true;
249
+ }
250
+
251
+ computeAll() {
252
+ this._applying = true;
253
+ const rootEl = this.scope || document.documentElement;
254
+ const els = new Set([rootEl, ...this.touched]); // include previously-styled elements — their rules may have STOPPED matching
255
+ const root = this.scope || document;
256
+ for (const rule of this.rules) {
257
+ if (rule.sel === ':root') { els.add(rootEl); continue; }
258
+ let found;
259
+ try { found = root.querySelectorAll(rule.sel); } catch { continue; }
260
+ found.forEach(el => els.add(el));
261
+ }
262
+ for (const el of els) this.computeElement(el);
263
+ this._applying = false;
264
+ }
265
+
266
+ computeElement(el) {
267
+ if (!el || !el.style || el.nodeType !== 1) return;
268
+ if (!this.inScope(el)) return;
269
+ this.touched.add(el);
270
+
271
+ const matched = [];
272
+ const rootEl = this.scope || document.documentElement;
273
+ for (const rule of this.rules) {
274
+ if (rule.sel === ':root') {
275
+ if (el !== rootEl) continue;
276
+ } else if (!this.matches(el, rule.sel)) continue;
277
+ if (!rule.media.every(q => this.mql(q).matches)) continue;
278
+ if (!this.signalsPass(rule)) continue;
279
+ if (!this.containerPass(el, rule)) continue;
280
+ matched.push(rule);
281
+ }
282
+ matched.sort((a, b) =>
283
+ (LAYER_RANK[a.layer] ?? 2) - (LAYER_RANK[b.layer] ?? 2) || a.order - b.order
284
+ );
285
+
286
+ // wipe what we owned before
287
+ const owned = this.ownMap.get(el) || [];
288
+ for (const p of owned) { try { el.style.removeProperty(p); } catch { } }
289
+
290
+ const newOwned = [];
291
+ const animSpecs = [];
292
+ let cancelAnims = false;
293
+ let stack = false;
294
+
295
+ for (const rule of matched) {
296
+ const imp = rule.layer === 'pin' ? 'important' : '';
297
+ for (const d of rule.decls) {
298
+ if (d.prop === '@animate') {
299
+ if (d.value === null) { cancelAnims = true; continue; }
300
+ animSpecs.push({ ...d.value, key: rule.id + '|' + rule.media.join('&') + '|' + rule.signals.map(s => s.name + s.op + s.value).join('&') });
301
+ continue;
302
+ }
303
+ if (d.prop === '@stack') { stack = true; continue; }
304
+ try {
305
+ el.style.setProperty(d.prop, d.value, imp);
306
+ newOwned.push(d.prop);
307
+ } catch { /* invalid value — resolver should have caught it */ }
308
+ }
309
+ }
310
+
311
+ this.ownMap.set(el, newOwned);
312
+ this.lastApply = Date.now();
313
+
314
+ // container registration
315
+ if (newOwned.includes('container-type')) {
316
+ if (!this.containerEls.has(el)) { this.containerEls.add(el); try { this.ro.observe(el); } catch { } }
317
+ } else if (this.containerEls.has(el)) {
318
+ this.containerEls.delete(el);
319
+ try { this.ro.unobserve(el); } catch { }
320
+ }
321
+
322
+ // stack behavior: children share grid cell
323
+ const prevStack = this.stackPrev.get(el) || [];
324
+ if (stack) {
325
+ for (const child of el.children) {
326
+ if (child.nodeType === 1) { try { child.style.gridArea = '1 / 1'; } catch { } }
327
+ }
328
+ this.stackPrev.set(el, [...el.children]);
329
+ } else if (prevStack.length) {
330
+ for (const child of prevStack) { try { child.style.removeProperty('grid-area'); } catch { } }
331
+ this.stackPrev.delete(el);
332
+ }
333
+
334
+ // animations
335
+ const anims = this.animMap.get(el) || new Map();
336
+ if (cancelAnims) {
337
+ anims.forEach(a => { try { a.cancel(); } catch { } });
338
+ anims.clear();
339
+ }
340
+ const reduced = this.reducedMotion && this.reducedMotion.matches;
341
+ for (const spec of animSpecs) {
342
+ if (spec.broken || !spec.frames || !spec.frames.length) continue;
343
+ if (reduced && !spec.essential) continue;
344
+ if (anims.has(spec.key)) continue;
345
+ let frames = spec.frames.map(f => ({ ...f.props, offset: f.offset / 100 }));
346
+ if (frames.length === 1) {
347
+ if (frames[0].offset === 0) frames = [frames[0], { offset: 1 }];
348
+ else frames = [{ offset: 0 }, frames[0]];
349
+ }
350
+ try {
351
+ const anim = el.animate(frames, {
352
+ duration: spec.duration, easing: spec.easing,
353
+ delay: spec.delay || 0, fill: spec.fill,
354
+ });
355
+ anims.set(spec.key, anim);
356
+ } catch { /* WAAPI unsupported property — skip */ }
357
+ }
358
+ if (animSpecs.length) this.animMap.set(el, anims);
359
+ }
360
+
361
+ /* ------------------------------------------------------------ *
362
+ * Signals (JS → styles bridge)
363
+ * ------------------------------------------------------------ */
364
+
365
+ signal(name, value) {
366
+ if (value === null || value === undefined) this.signalStore.delete(name);
367
+ else this.signalStore.set(name, String(value));
368
+ // DevTools-visible mirror
369
+ try {
370
+ if (value === null || value === undefined) document.documentElement.removeAttribute('data-dssl-signal-' + name);
371
+ else document.documentElement.setAttribute('data-dssl-signal-' + name, String(value));
372
+ } catch { }
373
+ this.computeAll();
374
+ }
375
+
376
+ getSignal(name) { return this.signalStore.get(name); }
377
+
378
+ /* ------------------------------------------------------------ *
379
+ * Pseudo-elements → shadow CSS (the only real CSS we emit)
380
+ * ------------------------------------------------------------ */
381
+
382
+ renderShadow() {
383
+ const idAttr = 'data-dssl-engine';
384
+ let host = this.scope;
385
+ if (this.scope && !this.scope.hasAttribute(idAttr)) this.scope.setAttribute(idAttr, this.name);
386
+
387
+ const prefix = this.scope ? `[${idAttr}="${this.name}"] ` : '';
388
+
389
+ const cssFor = (r) => {
390
+ let sel = prefix + r.sel + '::' + r.pseudo;
391
+ for (const s of r.signals) {
392
+ const attr = 'data-dssl-signal-' + s.name;
393
+ const cond = s.op === 'truthy' ? `html[${attr}]` : `html[${attr}="${s.value}"]`;
394
+ sel = cond + ' ' + sel;
395
+ }
396
+ let body = r.decls.map(d => ` ${d.prop}: ${d.value};`).join('\n');
397
+ let out = `${sel} {\n${body}\n}`;
398
+ for (const q of [...r.media].reverse()) out = `@media ${q} {\n${out}\n}`;
399
+ for (const c of [...r.containers].reverse()) out = `@container ${c} {\n${out}\n}`;
400
+ return out;
401
+ };
402
+
403
+ let css = this.shadowRules.map(cssFor).join('\n\n');
404
+
405
+ if (this.reset) {
406
+ css += `
407
+
408
+ /* dssl reset */
409
+ :where(html) { box-sizing: border-box; -webkit-text-size-adjust: 100%; }
410
+ :where(html *, html *::before, html *::after) { box-sizing: border-box; }
411
+ :where(body) { margin: 0; }
412
+ :where(h1,h2,h3,h4,h5,h6,p,figure,blockquote,dl,dd,pre) { margin: 0; }
413
+ :where(ul,ol) { margin: 0; padding: 0; list-style: none; }
414
+ :where(img,picture,video,canvas,svg) { display: block; max-width: 100%; }
415
+ :where(input,button,textarea,select) { font: inherit; color: inherit; letter-spacing: inherit; }
416
+ :where(a) { color: inherit; text-decoration: none; }
417
+ :where(button) { background: none; border: none; padding: 0; cursor: pointer; }
418
+ :where(table) { border-collapse: collapse; }`;
419
+ }
420
+
421
+ if (!this.shadowStyle) {
422
+ this.shadowStyle = document.createElement('style');
423
+ this.shadowStyle.setAttribute('data-dssl-shadow', this.name);
424
+ document.head.appendChild(this.shadowStyle);
425
+ }
426
+ this.shadowStyle.textContent = `/* generated by DSSL engine "${this.name}" — pseudo-elements + reset only */\n` + css;
427
+ }
428
+
429
+ /* ------------------------------------------------------------ *
430
+ * Replay animations
431
+ * ------------------------------------------------------------ */
432
+
433
+ replay(selector) {
434
+ const root = this.scope || document;
435
+ let els;
436
+ try { els = selector ? root.querySelectorAll(selector) : root.querySelectorAll('*'); } catch { return; }
437
+ this._applying = true;
438
+ for (const el of els) {
439
+ const anims = this.animMap.get(el);
440
+ if (!anims) continue;
441
+ anims.forEach(a => { try { a.cancel(); } catch { } });
442
+ anims.clear();
443
+ this.computeElement(el);
444
+ }
445
+ this._applying = false;
446
+ }
447
+ }
public/dssl/index.js ADDED
@@ -0,0 +1,162 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* =========================================================================
2
+ * DSSL — index.js — public API + auto-boot
3
+ *
4
+ * import '/dssl/index.js' // or <script type="module" src="/dssl/index.js">
5
+ *
6
+ * DSSL.load(['/styles/site.dssl']) // fetch + apply
7
+ * DSSL.apply(source, { name, scope }) // apply a source string
8
+ * DSSL.compile(source) // parse + resolve only (no DOM)
9
+ * DSSL.signal('theme', 'dark') // JS → styles bridge
10
+ * DSSL.refresh() / DSSL.replay(sel) / DSSL.errors
11
+ *
12
+ * Auto-boot: any <link rel="stylesheet/dssl" href="…"> or
13
+ * <style type="text/dssl"> on the page is fetched/applied on load.
14
+ * Add <html data-dssl-reset> to also get the small built-in reset.
15
+ * ========================================================================= */
16
+
17
+ import { parse } from './parser.js';
18
+ import { resolve, DEFAULT_TOKENS } from './resolver.js';
19
+ import { DsslEngine } from './engine.js';
20
+
21
+ export const VERSION = '0.2.0';
22
+
23
+ let counter = 0;
24
+ const engines = new Map();
25
+ let lastErrors = [];
26
+
27
+ // Token registry shared across bundles: tokens.dssl defines the palette,
28
+ // every later stylesheet resolves against it.
29
+ const tokenRegistry = new Map(Object.entries(DEFAULT_TOKENS));
30
+
31
+ function getEngine(opts = {}) {
32
+ if (!opts.scope) {
33
+ if (!engines.has('global')) {
34
+ engines.set('global', new DsslEngine({ scope: null, name: 'global', reset: !!opts.reset || document.documentElement.hasAttribute('data-dssl-reset') }));
35
+ }
36
+ return engines.get('global');
37
+ }
38
+ const el = typeof opts.scope === 'string' ? document.querySelector(opts.scope) : opts.scope;
39
+ if (!el) throw new Error('[DSSL] scope not found: ' + opts.scope);
40
+ const key = 'scope:' + (el.getAttribute('data-dssl-engine') || (el.setAttribute('data-dssl-engine', 'scoped-' + engines.size), 'scoped-' + engines.size));
41
+ if (!engines.has(key)) engines.set(key, new DsslEngine({ scope: el, name: el.getAttribute('data-dssl-engine'), reset: !!opts.reset }));
42
+ return engines.get(key);
43
+ }
44
+
45
+ function reportErrors(errors, file) {
46
+ lastErrors = errors;
47
+ if (!errors.length) return;
48
+ try {
49
+ console.groupCollapsed(`%c[DSSL] ${errors.length} issue(s) in ${file}`, 'color:#b3372e;font-weight:bold');
50
+ for (const e of errors) {
51
+ console.warn(`${e.file || ''}${e.file ? ':' : ''}${e.line ?? ''} — ${e.message}${e.hint ? `\n did you mean "${e.hint}"?` : ''}`);
52
+ }
53
+ console.groupEnd();
54
+ } catch { /* very old browsers */ }
55
+ }
56
+
57
+ /** Parse + resolve without touching the DOM. */
58
+ export function compile(source, file = 'inline.dssl') {
59
+ const { ast, errors: parseErrors } = parse(source, file);
60
+ const doc = resolve(ast, file, Object.fromEntries(tokenRegistry));
61
+ const errors = [...parseErrors, ...doc.errors];
62
+ return {
63
+ ok: errors.length === 0,
64
+ errors,
65
+ stats: {
66
+ rules: doc.rules.length,
67
+ shadowRules: doc.shadowRules.length,
68
+ motions: Object.keys(doc.motions).length,
69
+ tokens: Object.keys(doc.tokens).length,
70
+ mediaQueries: doc.mediaQueries.length,
71
+ },
72
+ tokens: doc.tokens,
73
+ doc,
74
+ };
75
+ }
76
+
77
+ /** Apply a DSSL source string. opts: { name, scope, replace, reset } */
78
+ export function apply(source, opts = {}) {
79
+ const name = opts.name || 'inline-' + (++counter);
80
+ const file = name.endsWith('.dssl') ? name : name + '.dssl';
81
+ const result = compile(source, file);
82
+ // fold this bundle's tokens into the shared registry for later bundles
83
+ let registryDirty = false;
84
+ for (const [k, v] of Object.entries(result.doc.tokens || {})) {
85
+ if (tokenRegistry.get(k) !== v) { tokenRegistry.set(k, v); registryDirty = true; }
86
+ }
87
+ if (!opts.keepErrors) reportErrors(result.errors, file);
88
+ if (result.ok || result.doc.rules.length || result.doc.shadowRules.length) {
89
+ const engine = getEngine(opts);
90
+ if (!opts.scope) {
91
+ // the shared registry is installed once as the first bundle; refresh it
92
+ // whenever any bundle's tokens changed. Scoped bundles keep their own
93
+ // :root rules mapped onto the scope element instead.
94
+ if (!engine.hasBundle('__dssl-tokens__') || registryDirty) {
95
+ engine.applyBundle('__dssl-tokens__', { rules: result.doc.registryRule ? [result.doc.registryRule] : [], shadowRules: [], motions: {}, mediaQueries: [], errors: [] }, { replace: true });
96
+ }
97
+ }
98
+ engine.applyBundle(name, result.doc, { replace: !!opts.replace });
99
+ }
100
+ return result;
101
+ }
102
+
103
+ /** Fetch and apply one or more .dssl files, in order. */
104
+ export async function load(urls, opts = {}) {
105
+ const list = Array.isArray(urls) ? urls : [urls];
106
+ const results = [];
107
+ for (const url of list) {
108
+ const res = await fetch(url);
109
+ if (!res.ok) {
110
+ const errors = [{ file: url, line: 0, message: `could not load stylesheet (HTTP ${res.status})` }];
111
+ reportErrors(errors, url);
112
+ results.push({ ok: false, errors });
113
+ continue;
114
+ }
115
+ const text = await res.text();
116
+ results.push(apply(text, { ...opts, name: opts.name || url }));
117
+ }
118
+ return results;
119
+ }
120
+
121
+ export function signal(name, value) { for (const e of engines.values()) e.signal(name, value); }
122
+ export function getSignal(name) { return engines.get('global')?.getSignal(name); }
123
+ export function refresh() { for (const e of engines.values()) e.computeAll(); }
124
+ export function replay(selector) { for (const e of engines.values()) e.replay(selector); }
125
+ export function errors() { return lastErrors; }
126
+ export function version() { return VERSION; }
127
+
128
+ export const DSSL = {
129
+ version: VERSION, compile, apply, load, signal, getSignal, refresh, replay, errors,
130
+ __debug: { engines },
131
+ };
132
+
133
+ if (typeof window !== 'undefined') {
134
+ window.DSSL = DSSL;
135
+
136
+ // ---- auto-boot -------------------------------------------------------
137
+ const boot = async () => {
138
+ try {
139
+ const links = [...document.querySelectorAll('link[rel="stylesheet/dssl"]')];
140
+ const inlines = [...document.querySelectorAll('style[type="text/dssl"]')];
141
+
142
+ const sources = [];
143
+ for (const l of links) sources.push({ name: l.getAttribute('href'), kind: 'link', el: l });
144
+ for (const s of inlines) sources.push({ name: 'inline-<style>-' + (++counter), kind: 'style', el: s });
145
+
146
+ for (const src of sources) {
147
+ try {
148
+ if (src.kind === 'link') await load(src.name);
149
+ else apply(src.el.textContent, { name: src.name });
150
+ } catch (e) {
151
+ reportErrors([{ file: src.name, line: 0, message: 'boot failed: ' + e.message }], src.name);
152
+ }
153
+ }
154
+ } finally {
155
+ // always announce readiness — even when there was nothing to auto-load
156
+ window.dispatchEvent(new CustomEvent('dssl-ready', { detail: { version: VERSION } }));
157
+ }
158
+ };
159
+
160
+ if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', boot);
161
+ else boot();
162
+ }
public/dssl/parser.js ADDED
@@ -0,0 +1,228 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* =========================================================================
2
+ * DSSL — Domain Specific Style Language
3
+ * parser.js — tokenizer + parser for the braces syntax
4
+ *
5
+ * Line-oriented parser:
6
+ * - blocks open with `header {` and close with `}`
7
+ * - declarations are `prop: value` (newline- or `;`-terminated)
8
+ * - one-liners like `.card { padding: 12 }` are allowed
9
+ * - comments: `// ...` and block comments
10
+ *
11
+ * Produces a node tree; classification of headers happens here, value
12
+ * resolution happens in resolver.js.
13
+ * ========================================================================= */
14
+
15
+ const STATE_WORDS = new Set([
16
+ 'hover', 'focus', 'focus-visible', 'active', 'disabled', 'checked',
17
+ 'empty', 'target', 'required', 'invalid', 'valid', 'open', 'closed',
18
+ 'placeholder-shown', 'read-only', 'read-write', 'enabled',
19
+ 'indeterminate', 'first-child', 'last-child', 'only-child',
20
+ 'first-of-type', 'last-of-type', 'link', 'visited',
21
+ ]);
22
+
23
+ const PSEUDO_ELEMENTS = new Set([
24
+ 'before', 'after', 'placeholder', 'selection', 'marker',
25
+ 'first-line', 'first-letter', 'backdrop', 'file-selector-button',
26
+ ]);
27
+
28
+ /** find index of first top-level occurrence of `ch` (skips strings + parens) */
29
+ function scanFor(str, ch, from = 0) {
30
+ let depth = 0, q = null;
31
+ for (let i = from; i < str.length; i++) {
32
+ const c = str[i];
33
+ if (q) { if (c === q && str[i - 1] !== '\\') q = null; continue; }
34
+ if (c === '"' || c === "'") { q = c; continue; }
35
+ if (c === '(' || c === '[') { depth++; continue; }
36
+ if (c === ')' || c === ']') { depth = Math.max(0, depth - 1); continue; }
37
+ if (depth === 0 && c === ch) return i;
38
+ }
39
+ return -1;
40
+ }
41
+
42
+ /** split a string on a top-level separator */
43
+ function splitTop(str, sep) {
44
+ const out = [];
45
+ let cur = '', depth = 0, q = null;
46
+ for (let i = 0; i < str.length; i++) {
47
+ const c = str[i];
48
+ if (q) { cur += c; if (c === q && str[i - 1] !== '\\') q = null; continue; }
49
+ if (c === '"' || c === "'") { q = c; cur += c; continue; }
50
+ if (c === '(' || c === '[') { depth++; cur += c; continue; }
51
+ if (c === ')' || c === ']') { depth = Math.max(0, depth - 1); cur += c; continue; }
52
+ if (depth === 0 && c === sep) { out.push(cur); cur = ''; continue; }
53
+ cur += c;
54
+ }
55
+ out.push(cur);
56
+ return out;
57
+ }
58
+
59
+ /** strip comments (respecting strings), newlines preserved so lines survive */
60
+ function stripComments(src) {
61
+ let out = '', q = null;
62
+ for (let i = 0; i < src.length; i++) {
63
+ const c = src[i], n = src[i + 1];
64
+ if (q) {
65
+ out += c;
66
+ if (c === q && src[i - 1] !== '\\') q = null;
67
+ continue;
68
+ }
69
+ if (c === '"' || c === "'") { q = c; out += c; continue; }
70
+ if (c === '/' && n === '/') {
71
+ while (i < src.length && src[i] !== '\n') i++;
72
+ out += '\n'; i--; continue;
73
+ }
74
+ if (c === '/' && n === '*') {
75
+ let j = i + 2;
76
+ while (j < src.length && !(src[j] === '*' && src[j + 1] === '/')) {
77
+ out += src[j] === '\n' ? '\n' : ' ';
78
+ j++;
79
+ }
80
+ i = j + 1; // skip past the closing */
81
+ continue;
82
+ }
83
+ out += c;
84
+ }
85
+ return out;
86
+ }
87
+
88
+ export function parse(source, file = 'inline.dssl') {
89
+ const errors = [];
90
+ const err = (line, message, hint) => errors.push({ file, line, message, hint });
91
+ const root = { kind: 'root', children: [] };
92
+ const stack = [root];
93
+
94
+ const clean = stripComments(source);
95
+ const lines = clean.split('\n');
96
+
97
+ const classify = (header, line) => {
98
+ const h = header.trim();
99
+ if (h === 'tokens') return { kind: 'tokens' };
100
+ if (h === 'pin') return { kind: 'pin' };
101
+ if (h.startsWith('behavior ')) return { kind: 'behavior', name: h.slice(9).trim() };
102
+ if (h.startsWith('motion ')) return { kind: 'motion', name: h.slice(7).trim() };
103
+ if (h.startsWith('@layer ')) return { kind: 'layer', name: h.slice(7).trim() };
104
+ if (h.startsWith('@media ')) return { kind: 'media', query: h.slice(7).trim() };
105
+ if (h.startsWith('@container')) return { kind: 'container', query: h.slice(10).trim() };
106
+ if (h.startsWith('@signal ')) return { kind: 'signal', expr: h.slice(8).trim() };
107
+ if (h.startsWith('@')) {
108
+ err(line, `unknown at-rule "${h.split(' ')[0]}"`, 'try @media, @container, @signal or @layer');
109
+ return { kind: 'bogus' };
110
+ }
111
+ if (h.startsWith('::')) {
112
+ const name = h.slice(2).split(/[\s(]/)[0];
113
+ if (!PSEUDO_ELEMENTS.has(name)) {
114
+ err(line, `unknown pseudo-element "::${name}"`, 'try ::before, ::after, ::placeholder, ::selection');
115
+ return { kind: 'bogus' };
116
+ }
117
+ return { kind: 'pseudo', name };
118
+ }
119
+ if (STATE_WORDS.has(h)) return { kind: 'state', name: h };
120
+ return { kind: 'selector', sel: h };
121
+ };
122
+
123
+ /** process a chunk, walking top-level braces so one-liners work */
124
+ const processChunk = (chunk, line) => {
125
+ let rest = chunk;
126
+ while (rest.length) {
127
+ rest = rest.trim();
128
+ if (!rest) return;
129
+
130
+ const open = scanFor(rest, '{');
131
+ const close = scanFor(rest, '}');
132
+
133
+ if (close !== -1 && (open === -1 || close < open)) {
134
+ const before = rest.slice(0, close).trim();
135
+ if (before) processContent(before, line);
136
+ if (stack.length === 1) err(line, 'unexpected "}"');
137
+ else stack.pop();
138
+ rest = rest.slice(close + 1);
139
+ continue;
140
+ }
141
+
142
+ if (open !== -1) {
143
+ const header = rest.slice(0, open).trim();
144
+ if (!header) { err(line, 'block is missing a header before "{"'); rest = rest.slice(open + 1); continue; }
145
+ const meta = classify(header, line);
146
+ const node = { ...meta, line, children: [] };
147
+ stack[stack.length - 1].children.push(node);
148
+ if (meta.kind !== 'bogus') stack.push(node);
149
+ rest = rest.slice(open + 1);
150
+ continue;
151
+ }
152
+
153
+ processContent(rest, line);
154
+ return;
155
+ }
156
+ };
157
+
158
+ const processContent = (content, line) => {
159
+ for (const piece of splitTop(content, ';')) {
160
+ const part = piece.trim();
161
+ if (!part) continue;
162
+ const colon = scanFor(part, ':');
163
+ if (colon === -1) {
164
+ // `use behavior-name` — keyword declaration with a name argument
165
+ const useMatch = part.match(/^use\s+([A-Za-z][\w-]*)$/);
166
+ if (useMatch) {
167
+ stack[stack.length - 1].children.push({ kind: 'decl', prop: 'use', value: useMatch[1], line });
168
+ continue;
169
+ }
170
+ // valueless semantic property (e.g. `truncate`) — resolver decides if legal
171
+ if (/^[a-z][a-z0-9-]*$/i.test(part)) {
172
+ stack[stack.length - 1].children.push({ kind: 'decl', prop: part.toLowerCase(), value: '', line });
173
+ } else {
174
+ err(line, `expected ":" in "${part.slice(0, 40)}"`, 'declarations look like prop: value');
175
+ }
176
+ continue;
177
+ }
178
+ const prop = part.slice(0, colon).trim().toLowerCase();
179
+ const value = part.slice(colon + 1).trim();
180
+ if (!prop) { err(line, 'declaration is missing a property name'); continue; }
181
+ if (!/^(-{1,2}[a-z][a-z0-9-]*|[a-z][a-z0-9-]*)$/i.test(prop)) {
182
+ err(line, `"${prop}" is not a valid property name`);
183
+ continue;
184
+ }
185
+ stack[stack.length - 1].children.push({ kind: 'decl', prop, value, line });
186
+ }
187
+ };
188
+
189
+ // ---- line loop with continuation buffering ----
190
+ // A logical chunk grows across lines while:
191
+ // - parens/brackets are unbalanced (multi-line values)
192
+ // - it ends with a top-level comma (multi-line selector lists)
193
+ // - it ends with a top-level colon (value on the next line)
194
+ let buf = '', bufLine = 0;
195
+
196
+ const needsMore = (s) => {
197
+ let depth = 0, q = null;
198
+ for (let i = 0; i < s.length; i++) {
199
+ const c = s[i];
200
+ if (q) { if (c === q && s[i - 1] !== '\\') q = null; continue; }
201
+ if (c === '"' || c === "'") { q = c; continue; }
202
+ if (c === '(' || c === '[') depth++;
203
+ else if (c === ')' || c === ']') depth--;
204
+ }
205
+ if (depth !== 0) return true;
206
+ const t = s.trimEnd();
207
+ const last = t[t.length - 1];
208
+ return last === ',' || last === ':';
209
+ };
210
+
211
+ lines.forEach((raw, idx) => {
212
+ const line = idx + 1;
213
+ const trimmed = raw.trim();
214
+ if (!trimmed) return; // blank lines never split a pending chunk
215
+ const candidate = buf ? buf + ' ' + trimmed : raw;
216
+ if (buf && needsMore(candidate)) { buf = candidate; return; }
217
+ if (buf) { processChunk(candidate, bufLine || line); buf = ''; return; }
218
+ if (needsMore(candidate)) { buf = candidate; bufLine = line; return; }
219
+ processChunk(candidate, line);
220
+ });
221
+ if (buf.trim()) processChunk(buf, bufLine);
222
+
223
+ if (stack.length > 1) {
224
+ err(lines.length, `${stack.length - 1} block(s) were never closed with "}"`, 'every "{" needs a matching "}"');
225
+ }
226
+
227
+ return { ast: root, errors };
228
+ }
public/dssl/resolver.js ADDED
@@ -0,0 +1,776 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* =========================================================================
2
+ * DSSL — resolver.js
3
+ * Walks the parsed tree and produces flat, engine-ready rules:
4
+ *
5
+ * rule = {
6
+ * id, sel, // full structural selector (may contain native pseudo-classes)
7
+ * media: [query], // all must match
8
+ * signals: [{name, op, value}], // JS-driven conditions
9
+ * containers: [query], // container-query conditions
10
+ * layer, order, // cascade: (layer, order) — no specificity
11
+ * decls: [{prop, value} | special],
12
+ * file, line
13
+ * }
14
+ *
15
+ * Also resolves tokens (→ custom properties on <html>), semantic properties,
16
+ * behaviors (`use`), motions (→ Web Animations keyframes) and pseudo-element
17
+ * rules (→ generated "shadow" CSS, the only thing inline styles can't reach).
18
+ * ========================================================================= */
19
+
20
+ import { levenshtein, didYouMean } from './utils.js';
21
+
22
+ /* ------------------------------------------------------------------ *
23
+ * Vocabulary
24
+ * ------------------------------------------------------------------ */
25
+
26
+ const CSS_KEYWORDS = new Set(('inherit,initial,unset,revert,revert-layer,none,auto,currentcolor,transparent,' +
27
+ 'black,silver,gray,white,maroon,red,purple,fuchsia,green,lime,olive,yellow,navy,blue,teal,aqua,' +
28
+ 'orange,gold,pink,brown,violet,cyan,magenta,indigo,crimson,coral,tomato,goldenrod,khaki,' +
29
+ 'slategray,slategrey,dimgray,dimgrey,lightgray,lightgrey,darkgray,darkgrey,gainsboro,whitesmoke,' +
30
+ 'ghostwhite,ivory,beige,linen,seashell,snow,honeydew,mintcream,azure,aliceblue,lavender,' +
31
+ 'canvas,canvastext,linktext,visitedtext,activetext,buttonface,buttontext,buttonborder,field,fieldtext,highlight,highlighttext,' +
32
+ 'solid,dashed,dotted,double,groove,ridge,inset,outset,hidden,visible,' +
33
+ 'block,inline,inline-block,flex,inline-flex,grid,inline-grid,flow-root,contents,table,table-row,table-cell,list-item,' +
34
+ 'relative,absolute,fixed,sticky,static,sticky,' +
35
+ 'start,end,center,left,right,between,around,evenly,baseline,stretch,normal,space-between,space-around,space-evenly,' +
36
+ 'wrap,nowrap,reverse,row,column,row-reverse,column-reverse,' +
37
+ 'bold,bolder,lighter,italic,oblique,small-caps,uppercase,lowercase,capitalize,underline,overline,line-through,blink,' +
38
+ 'ellipsis,clip,break-word,anywhere,pre,pre-wrap,pre-line,subpixel-antialiased,antialiased,' +
39
+ 'pointer,progress,default,move,text,wait,help,grab,grabbing,not-allowed,zoom-in,zoom-out,copy,alias,crosshair,' +
40
+ 'serif,sans-serif,monospace,cursive,fantasy,system-ui,ui-sans-serif,ui-monospace,ui-serif,ui-rounded,' +
41
+ 'system-ui,-apple-system,Roboto,Helvetica,Arial,Menlo,Consolas,SFMono-Regular,Liberation-Mono,Inter,Noto-Sans,Ubuntu,Cantarell,Oxygen,Fira-Sans,Droid-Sans,' +
42
+ 'border-box,content-box,padding-box,fill-box,stroke-box,view-box,' +
43
+ 'contain,cover,fill,fit-content,min-content,max-content,minmax,repeat,clamp,calc,var,url,env,attr,' +
44
+ 'scaleX,scaleY,translateX,translateY,translate,rotate,skew,skewX,skewY,matrix,' +
45
+ 'ease,ease-in,ease-out,ease-in-out,linear,step-start,step-end,steps,cubic-bezier,spring,bounce,smooth,snap,' +
46
+ 'forwards,backwards,both,infinite,alternate,alternate-reverse,running,paused,reverse,' +
47
+ 'safe,unsafe,legacy,loose,strict,spell-out,letters,isolate,isolation,blend,multiply,screen,overlay,darken,lighten,color-dodge,color-burn,hard-light,soft-light,difference,exclusion,hue,saturation,color,luminosity,plus-lighter,' +
48
+ 'blur,brightness,contrast,grayscale,invert,opacity,sepia,saturate,drop-shadow,' +
49
+ 'pointer coarse fine hover interactive,first,last,odd,even,all,portrait,landscape,print,screen,and,not,only,' +
50
+ 'inline-size,size,scroll-state,normal,snapped,physical,logical,' +
51
+ 'in,oklab,oklch,srgb,srgb-linear,display-p3,a98-rgb,prophoto-rgb,rec2020,xyz,xyz-d50,xyz-d65,shorter,longer,deg,grad,rad,turn,' +
52
+ 'to,top,bottom,left,right,above,below,closest-side,farthest-side,ellipse,circle,at,padding-box,border-box,' +
53
+ 'isolate,plaintext,sideways,upright,mixed,ltr,rtl,transform,auto,' +
54
+ '-webkit-box,-webkit-inline-box,' +
55
+ 'thin,thick,medium,separate,collapse,vertical,horizontal,balance,pretty,orphans,' +
56
+ 'flex-start,flex-end,self-start,self-end,snap-block,snap-inline').split(','));
57
+
58
+ const UNITLESS_PROPS = new Set([
59
+ 'opacity', 'line-height', 'z-index', 'font-weight', 'flex-grow', 'flex-shrink',
60
+ 'flex', 'order', 'scale', 'widows', 'orphans', 'column-count', 'aspect-ratio',
61
+ 'animation-iteration-count', 'flood-opacity', 'fill-opacity', 'stop-opacity',
62
+ 'shape-image-threshold', 'animation-delay', 'transition-delay',
63
+ ]);
64
+
65
+ // A practical whitelist of standard CSS properties (loud errors beat silent ones)
66
+ const KNOWN_PROPS = new Set(('all,appearance,accent-color,align-content,align-items,align-self,align-tracks,aspect-ratio,' +
67
+ 'backdrop-filter,backface-visibility,background,background-attachment,background-blend-mode,background-clip,background-color,background-image,background-origin,background-position,background-repeat,background-size,' +
68
+ 'block-size,border,border-block,border-block-color,border-block-end,border-block-end-color,border-block-end-style,border-block-end-width,border-block-start,border-block-start-color,border-block-start-style,border-block-start-width,border-block-style,border-block-width,' +
69
+ 'border-bottom,border-bottom-color,border-bottom-left-radius,border-bottom-right-radius,border-bottom-style,border-bottom-width,' +
70
+ 'border-collapse,border-color,border-end-end-radius,border-end-start-radius,border-image,border-image-outset,border-image-repeat,border-image-slice,border-image-source,border-image-width,' +
71
+ 'border-inline,border-inline-color,border-inline-end,border-inline-end-color,border-inline-end-style,border-inline-end-width,border-inline-start,border-inline-start-color,border-inline-start-style,border-inline-start-width,border-inline-style,border-inline-width,' +
72
+ 'border-left,border-left-color,border-left-style,border-left-width,border-radius,border-right,border-right-color,border-right-style,border-right-width,' +
73
+ 'border-spacing,border-start-end-radius,border-start-start-radius,border-style,border-top,border-top-color,border-top-left-radius,border-top-right-radius,border-top-style,border-top-width,border-width,' +
74
+ 'bottom,box-decoration-break,box-shadow,box-sizing,break-after,break-before,break-inside,caret-color,caret-shape,clear,clip,clip-path,color,color-scheme,column-count,column-fill,column-gap,column-rule,column-rule-color,column-rule-style,column-rule-width,column-span,column-width,columns,contain,contain-intrinsic-size,container,container-name,container-type,content,cursor,caret,display,empty-cells,filter,flex,flex-basis,flex-direction,flex-flow,flex-grow,flex-shrink,flex-wrap,float,font,font-family,font-feature-settings,font-kerning,font-language-override,font-optical-sizing,font-palette,font-size,font-size-adjust,font-stretch,font-style,font-synthesis,font-variant,font-variant-caps,font-variant-east-asian,font-variant-ligatures,font-variant-numeric,font-variant-position,font-variation-settings,font-weight,forced-color-adjust,gap,grid,grid-area,grid-auto-columns,grid-auto-flow,grid-auto-rows,grid-column,grid-column-end,grid-column-gap,grid-column-start,grid-gap,grid-row,grid-row-end,grid-row-gap,grid-row-start,grid-template,grid-template-areas,grid-template-columns,grid-template-rows,' +
75
+ 'hanging-punctuation,height,hyphenate-character,hyphenate-limit-chars,hyphens,image-orientation,image-rendering,inline-size,inset,inset-block,inset-block-end,inset-block-start,inset-inline,inset-inline-end,inset-inline-start,isolation,justify-content,justify-items,justify-self,justify-tracks,left,letter-spacing,line-break,line-height,list-style,list-style-image,list-style-position,list-style-type,margin,margin-block,margin-block-end,margin-block-start,margin-bottom,margin-inline,margin-inline-end,margin-inline-start,margin-left,margin-right,margin-top,margin-trim,mask,mask-border,mask-border-mode,mask-border-outset,mask-border-repeat,mask-border-slice,mask-border-source,mask-border-width,mask-clip,mask-composite,mask-image,mask-mode,mask-origin,mask-position,mask-repeat,mask-size,max-height,max-inline-size,max-width,min-block-size,min-height,min-inline-size,min-width,mix-blend-mode,object-fit,object-position,offset,offset-anchor,offset-distance,offset-path,offset-rotate,opacity,order,outline,outline-color,outline-offset,outline-style,outline-width,overflow,overflow-anchor,overflow-clip-margin,overflow-wrap,overflow-x,overflow-y,overscroll-behavior,overscroll-behavior-block,overscroll-behavior-inline,overscroll-behavior-x,overscroll-behavior-y,padding,padding-block,padding-block-end,padding-block-start,padding-bottom,padding-inline,padding-inline-end,padding-inline-start,padding-left,padding-right,padding-top,page-break-after,page-break-before,page-break-inside,paint-order,perspective,perspective-origin,place-content,place-items,place-self,pointer-events,position,print-color-adjust,quotes,resize,right,rotate,row-gap,ruby-align,ruby-merge,scale,scroll-behavior,scroll-margin,scroll-margin-block,scroll-margin-block-end,scroll-margin-block-start,scroll-margin-bottom,scroll-margin-inline,scroll-margin-inline-end,scroll-margin-inline-start,scroll-margin-left,scroll-margin-right,scroll-margin-top,scroll-padding,scroll-padding-block,scroll-padding-bottom,scroll-padding-inline,scroll-padding-left,scroll-padding-right,scroll-padding-top,scroll-snap-align,scroll-snap-stop,scroll-snap-type,scrollbar-color,scrollbar-gutter,scrollbar-width,shape-image-threshold,shape-margin,shape-outside,tab-size,table-layout,text-align,text-align-last,text-combine-upright,text-decoration,text-decoration-color,text-decoration-line,text-decoration-skip-ink,text-decoration-style,text-decoration-thickness,text-emphasis,text-emphasis-color,text-emphasis-position,text-emphasis-style,text-indent,text-justify,text-orientation,text-overflow,text-rendering,text-shadow,text-transform,text-underline-offset,text-underline-position,top,touch-action,transform,transform-box,transform-origin,transform-style,transition,transition-behavior,transition-delay,transition-duration,transition-property,transition-timing-function,translate,unicode-bidi,user-select,vertical-align,visibility,white-space,white-space-collapse,width,will-change,word-break,word-spacing,word-wrap,writing-mode,z-index,zoom,' +
76
+ 'animation,animation-composition,animation-delay,animation-direction,animation-duration,animation-fill-mode,animation-iteration-count,animation-name,animation-play-state,animation-range,animation-timeline,animation-timing-function,' +
77
+ 'view-transition-name,interpolate-size,text-box,text-box-edge,text-box-trim,dynamic-range-limit,field-sizing,anchor-name,position-anchor,position-area,scroll-timeline,scroll-timeline-axis,scroll-timeline-name,timeline-scope,view-timeline,view-timeline-axis,view-timeline-name,' +
78
+ '-webkit-background-clip,-webkit-text-fill-color,-webkit-box-orient,-webkit-line-clamp,-webkit-tap-highlight-color,-webkit-font-smoothing,-moz-osx-font-smoothing,-webkit-box-shadow,-webkit-text-stroke').split(','));
79
+
80
+ /* ------------------------------------------------------------------ *
81
+ * Built-in token defaults (overridable via `tokens { }`)
82
+ * ------------------------------------------------------------------ */
83
+
84
+ export const DEFAULT_TOKENS = {
85
+ 'color.ink': '#16161d',
86
+ 'color.paper': '#faf7f2',
87
+ 'color.card': '#ffffff',
88
+ 'color.muted': '#6f6a7a',
89
+ 'color.line': '#e5ded2',
90
+ 'color.accent': 'oklch(62% 0.19 35)',
91
+ 'color.accent-ink': '#fff6ee',
92
+ 'color.ok': '#2e7d4f',
93
+ 'color.err': '#b3372e',
94
+ 'space.1': '4', 'space.2': '8', 'space.3': '12', 'space.4': '16',
95
+ 'space.5': '24', 'space.6': '32', 'space.7': '48', 'space.8': '64',
96
+ 'radius.sm': '8', 'radius.md': '14', 'radius.lg': '22', 'radius.full': '999',
97
+ 'shadow.sm': '0 1 2 color.ink/8, 0 1 3 color.ink/6',
98
+ 'shadow.md': '0 2 4 color.ink/10, 0 4 12 color.ink/8',
99
+ 'shadow.lg': '0 4 8 color.ink/12, 0 12 32 color.ink/12',
100
+ 'shadow.xl': '0 8 16 color.ink/14, 0 24 64 color.ink/16',
101
+ 'font.sans': 'system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
102
+ 'font.mono': 'ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace',
103
+ };
104
+
105
+ const EASINGS = {
106
+ snap: 'cubic-bezier(0.2, 0.9, 0.3, 1)',
107
+ smooth: 'cubic-bezier(0.4, 0, 0.2, 1)',
108
+ bounce: 'cubic-bezier(0.34, 1.56, 0.64, 1)',
109
+ linear: 'linear',
110
+ ease: 'ease',
111
+ 'ease-in': 'ease-in',
112
+ 'ease-out': 'ease-out',
113
+ 'ease-in-out': 'ease-in-out',
114
+ };
115
+
116
+ const PLACE_WORDS = {
117
+ center: ['center', 'center'], start: ['start', 'flex-start'], end: ['end', 'flex-end'],
118
+ between: ['center', 'space-between'], around: ['center', 'space-around'],
119
+ evenly: ['center', 'space-evenly'], stretch: ['stretch', 'flex-start'],
120
+ baseline: ['baseline', 'flex-start'],
121
+ };
122
+
123
+ /* ------------------------------------------------------------------ *
124
+ * Value resolution
125
+ * ------------------------------------------------------------------ */
126
+
127
+ const IDENT_RE = /^-?(?:[A-Za-z_]|--[A-Za-z_])[A-Za-z0-9_.\\-]*/;
128
+
129
+ // functions whose numeric arguments are NOT lengths — numbers stay bare
130
+ const UNITLESS_FN = new Set(('oklch,oklab,lab,lch,hsl,hsla,rgb,rgba,hwb,color,color-mix,light-dark,' +
131
+ 'brightness,saturate,contrast,grayscale,sepia,invert,opacity,' +
132
+ 'scale,scalex,scaley,scalez,scale3d,steps,cubic-bezier,linear,radial,conic').split(','));
133
+ // functions whose bare numbers ARE lengths (px)
134
+ const LENGTH_FN = new Set('calc,min,max,clamp,round,mod,rem,abs,sign,hypot,pow,sqrt,log,exp,blur,drop-shadow'.split(','));
135
+
136
+ /**
137
+ * Resolve a raw value string into its final form:
138
+ * - bare numbers get `px` (unless ctx.unitless)
139
+ * - token references become var(--dssl-…), `token/NN` becomes color-mix
140
+ * - unknown identifiers are errors (with did-you-mean)
141
+ */
142
+ function resolveValue(raw, ctx) {
143
+ let out = '';
144
+ let i = 0;
145
+ const n = raw.length;
146
+
147
+ const err = (msg, hint) => ctx.errors.push({ file: ctx.file, line: ctx.line, message: msg, hint });
148
+
149
+ while (i < n) {
150
+ const c = raw[i];
151
+
152
+ if (/\s/.test(c)) { out += c; i++; continue; }
153
+
154
+ if (c === '"' || c === "'") {
155
+ let j = i + 1;
156
+ while (j < n && raw[j] !== c) j++;
157
+ out += raw.slice(i, j + 1);
158
+ i = j + 1;
159
+ continue;
160
+ }
161
+
162
+ if (/[0-9]/.test(c) || (c === '.' && /[0-9]/.test(raw[i + 1] || '')) || ((c === '-' || c === '+') && /[0-9.]/.test(raw[i + 1] || ''))) {
163
+ let j = i + 1;
164
+ while (j < n && /[0-9.]/.test(raw[j])) j++;
165
+ const num = raw.slice(i, j);
166
+ // optional unit
167
+ let unit = '';
168
+ if (j < n && /[a-z%]/i.test(raw[j])) {
169
+ let k = j;
170
+ while (k < n && /[a-z%]/i.test(raw[k])) k++;
171
+ unit = raw.slice(j, k).toLowerCase();
172
+ j = k;
173
+ }
174
+ out += num + (unit ? unit : (ctx.unitless ? '' : 'px'));
175
+ i = j;
176
+ continue;
177
+ }
178
+
179
+ const idm = raw.slice(i).match(IDENT_RE);
180
+ if (idm) {
181
+ let ident = idm[0];
182
+ i += ident.length;
183
+
184
+ if (ident.startsWith('--')) { out += ident; continue; } // custom property
185
+
186
+ // function call — resolve the inside
187
+ if (raw[i] === '(') {
188
+ let depth = 1, j = i + 1;
189
+ while (j < n && depth > 0) {
190
+ if (raw[j] === '(') depth++;
191
+ if (raw[j] === ')') depth--;
192
+ if (depth > 0 && (raw[j] === '"' || raw[j] === "'")) {
193
+ const q = raw[j]; j++;
194
+ while (j < n && raw[j] !== q) j++;
195
+ }
196
+ j++;
197
+ }
198
+ const inner = raw.slice(i + 1, j - 1);
199
+ const fname = ident.toLowerCase();
200
+ // color-ish functions keep bare numbers bare; calc-family gets px
201
+ const innerUnitless = UNITLESS_FN.has(fname) ? true : LENGTH_FN.has(fname) ? false : ctx.unitless;
202
+ const resolvedInner = resolveValue(inner, { ...ctx, unitless: innerUnitless });
203
+ out += ident + '(' + resolvedInner + ')';
204
+ i = j;
205
+ continue;
206
+ }
207
+
208
+ // token reference?
209
+ const tokenKey = findToken(ctx.tokens, ident);
210
+ if (tokenKey) {
211
+ const varRef = 'var(--dssl-' + tokenKey.replace(/\./g, '-') + ')';
212
+ // alpha modifier: ink/20
213
+ let rest = raw.slice(i).match(/^\s*\/\s*([0-9.]+)\s*%?/);
214
+ if (rest) {
215
+ out += `color-mix(in oklab, ${varRef} ${rest[1]}%, transparent)`;
216
+ i += rest[0].length;
217
+ continue;
218
+ }
219
+ out += varRef;
220
+ continue;
221
+ }
222
+
223
+ // known keyword?
224
+ if (CSS_KEYWORDS.has(ident) || CSS_KEYWORDS.has(ident.toLowerCase())) {
225
+ out += ident;
226
+ continue;
227
+ }
228
+
229
+ err(`unknown value "${ident}" for property "${ctx.prop}"`,
230
+ didYouMean(ident, [...ctx.tokenNames, ...CSS_KEYWORDS]));
231
+ out += ident; // keep going so the rest still resolves
232
+ continue;
233
+ }
234
+
235
+ // punctuation: copy (handles / , + * # etc.)
236
+ if (c === '#') { // hex color
237
+ let j = i + 1;
238
+ while (j < n && /[0-9a-fA-F]/.test(raw[j])) j++;
239
+ out += raw.slice(i, j);
240
+ i = j;
241
+ continue;
242
+ }
243
+ out += c;
244
+ i++;
245
+ }
246
+ return out;
247
+ }
248
+
249
+ function findToken(tokens, ident) {
250
+ if (tokens.has(ident)) return ident;
251
+ // try progressive dot paths (a.b.c defined, user typed a.b.c — exact only, plus flat fallback)
252
+ if (ident.includes('.')) {
253
+ const parts = ident.split('.');
254
+ for (let k = parts.length - 1; k > 0; k--) {
255
+ const key = parts.slice(0, k).join('.');
256
+ if (tokens.has(key)) return key; // hmm — partial match would change meaning; require exact
257
+ }
258
+ }
259
+ return null;
260
+ }
261
+
262
+ /* ------------------------------------------------------------------ *
263
+ * Semantic properties
264
+ * ------------------------------------------------------------------ */
265
+
266
+ const len = (raw, ctx) => resolveValue(raw, { ...ctx, unitless: false });
267
+ const num = (raw, ctx) => resolveValue(raw, { ...ctx, unitless: true });
268
+
269
+ function parseFlexWords(value, ctx, dir) {
270
+ const out = [{ prop: 'display', value: 'flex' }, { prop: 'flex-direction', value: dir }];
271
+ const atoms = value.split(/\s+/).filter(Boolean);
272
+ for (let k = 0; k < atoms.length; k++) {
273
+ const a = atoms[k];
274
+ if (a === 'gap') {
275
+ const v = atoms[++k];
276
+ if (v === undefined) { ctx.errors.push({ file: ctx.file, line: ctx.line, message: 'gap needs a value' }); break; }
277
+ out.push({ prop: 'gap', value: len(v, ctx) });
278
+ continue;
279
+ }
280
+ if (a === 'wrap') { out.push({ prop: 'flex-wrap', value: 'wrap' }); continue; }
281
+ if (a === 'nowrap') { out.push({ prop: 'flex-wrap', value: 'nowrap' }); continue; }
282
+ if (a === 'reverse') { out[1].value = dir + '-reverse'; continue; }
283
+ if (a === 'inline') { out[0].value = 'inline-flex'; continue; }
284
+ if (PLACE_WORDS[a]) {
285
+ out.push({ prop: 'align-items', value: PLACE_WORDS[a][0] });
286
+ out.push({ prop: 'justify-content', value: PLACE_WORDS[a][1] });
287
+ continue;
288
+ }
289
+ ctx.errors.push({ file: ctx.file, line: ctx.line, message: `unknown word "${a}" in ${dir}`, hint: didYouMean(a, ['gap', 'center', 'start', 'end', 'between', 'around', 'evenly', 'stretch', 'baseline', 'wrap', 'nowrap', 'reverse', 'inline']) });
290
+ }
291
+ return out;
292
+ }
293
+
294
+ const SEMANTIC = {
295
+ size(v, ctx) {
296
+ const parts = v.split(/\s+/).filter(Boolean);
297
+ const w = len(parts[0] || 'auto', ctx);
298
+ const h = parts[1] !== undefined ? len(parts[1], ctx) : w;
299
+ return [{ prop: 'width', value: w }, { prop: 'height', value: h }];
300
+ },
301
+ row: (v, ctx) => parseFlexWords(v, ctx, 'row'),
302
+ column: (v, ctx) => parseFlexWords(v, ctx, 'column'),
303
+ grid(v, ctx) {
304
+ const atoms = v.split(/\s+/).filter(Boolean);
305
+ let cols;
306
+ if (atoms.length === 1 && /^[0-9]+$/.test(atoms[0])) {
307
+ cols = `repeat(${atoms[0]}, 1fr)`;
308
+ } else if ((atoms[0] === 'auto-fill' || atoms[0] === 'auto-fit') && atoms[1]) {
309
+ cols = `repeat(${atoms[0]}, minmax(${len(atoms[1], ctx)}, 1fr))`;
310
+ } else {
311
+ cols = atoms.map(a => len(a, ctx)).join(' ');
312
+ }
313
+ return [{ prop: 'display', value: 'grid' }, { prop: 'grid-template-columns', value: cols }];
314
+ },
315
+ stack(v, ctx) {
316
+ const word = (v || 'center').trim();
317
+ const p = PLACE_WORDS[word];
318
+ if (!p) {
319
+ ctx.errors.push({ file: ctx.file, line: ctx.line, message: `unknown place "${word}" in stack`, hint: didYouMean(word, Object.keys(PLACE_WORDS)) });
320
+ return [{ prop: '@stack', value: '1' }];
321
+ }
322
+ return [{ prop: 'display', value: 'grid' }, { prop: 'place-items', value: `${p[0]} ${p[1]}` }, { prop: '@stack', value: '1' }];
323
+ },
324
+ place(v, ctx) {
325
+ const p = PLACE_WORDS[v.trim()];
326
+ if (!p) {
327
+ ctx.errors.push({ file: ctx.file, line: ctx.line, message: `unknown place "${v}"`, hint: didYouMean(v, Object.keys(PLACE_WORDS)) });
328
+ return [];
329
+ }
330
+ return [{ prop: 'place-items', value: `${p[0]} ${p[1]}` }, { prop: 'place-content', value: `${p[0]} ${p[1]}` }];
331
+ },
332
+ cover(v, ctx) {
333
+ const pos = (v || 'absolute').trim();
334
+ return [{ prop: 'position', value: pos }, { prop: 'inset', value: '0' }];
335
+ },
336
+ sticky(v, ctx) {
337
+ return [{ prop: 'position', value: 'sticky' }, { prop: 'top', value: len(v || '0', ctx) }];
338
+ },
339
+ font(v, ctx) {
340
+ const out = [];
341
+ const slash = v.split('/');
342
+ const head = slash[0].trim().split(/\s+/).filter(Boolean);
343
+ out.push({ prop: 'font-size', value: len(head[0] || '16', ctx) });
344
+ if (slash[1]) {
345
+ const tail = slash[1].trim().split(/\s+/).filter(Boolean);
346
+ out.push({ prop: 'line-height', value: num(tail[0] || '1.5', ctx) });
347
+ if (tail.length > 1) out.push({ prop: 'color', value: resolveValue(tail.slice(1).join(' '), ctx) });
348
+ } else if (head.length > 1) {
349
+ out.push({ prop: 'color', value: resolveValue(head.slice(1).join(' '), ctx) });
350
+ }
351
+ return out;
352
+ },
353
+ fluid(v, ctx) {
354
+ const parts = v.split(',').map(s => s.trim());
355
+ const px = (s) => {
356
+ const m = String(s).match(/^([0-9.]+)\s*(px|rem|em|%)?$/);
357
+ if (!m) { ctx.errors.push({ file: ctx.file, line: ctx.line, message: `fluid: "${s}" is not a size` }); return '0px'; }
358
+ return m[2] ? `${m[1]}${m[2]}` : `${m[1]}px`;
359
+ };
360
+ const min = px(parts[0]); const max = px(parts[1]);
361
+ const vmin = px(parts[2] || '360'); const vmax = px(parts[3] || '1200');
362
+ const mv = parseFloat(min), xv = parseFloat(max);
363
+ const slope = ((xv - mv) / (parseFloat(vmax) - parseFloat(vmin))) * 100;
364
+ return [{ prop: 'font-size', value: `clamp(${min}, calc(${min} + ${slope.toFixed(3)}vw), ${max})` }];
365
+ },
366
+ shadow(v, ctx) {
367
+ const t = v.trim();
368
+ if (['sm', 'md', 'lg', 'xl', 'none'].includes(t)) {
369
+ return [{ prop: 'box-shadow', value: t === 'none' ? 'none' : `var(--dssl-shadow-${t})` }];
370
+ }
371
+ return [{ prop: 'box-shadow', value: resolveValue(v, ctx) }];
372
+ },
373
+ radius(v, ctx) {
374
+ const atoms = v.trim().split(/\s+/).filter(Boolean);
375
+ const out = atoms.map(a => {
376
+ if (['sm', 'md', 'lg', 'full', 'xl'].includes(a)) return `var(--dssl-radius-${a})`;
377
+ return resolveValue(a, ctx);
378
+ });
379
+ return [{ prop: 'border-radius', value: out.join(' ') }];
380
+ },
381
+ circle(v, ctx) {
382
+ const s = len(v, ctx);
383
+ return [
384
+ { prop: 'width', value: s },
385
+ { prop: 'height', value: s },
386
+ { prop: 'border-radius', value: '50%' },
387
+ ];
388
+ },
389
+ align(v, ctx) {
390
+ const map = { center: 'center', start: 'start', end: 'end', stretch: 'stretch', baseline: 'baseline' };
391
+ const val = map[v.trim()];
392
+ if (!val) {
393
+ ctx.errors.push({ file: ctx.file, line: ctx.line, message: `unknown align "${v}"`, hint: didYouMean(v, Object.keys(map)) });
394
+ return [];
395
+ }
396
+ return [{ prop: 'align-items', value: val }];
397
+ },
398
+ z(v, ctx) { return [{ prop: 'z-index', value: num(v, ctx) }]; },
399
+ grow(v, ctx) { return [{ prop: 'flex-grow', value: num(v || '1', ctx) }]; },
400
+ shrink(v, ctx) { return [{ prop: 'flex-shrink', value: num(v || '1', ctx) }]; },
401
+ basis(v, ctx) { return [{ prop: 'flex-basis', value: len(v, ctx) }]; },
402
+ truncate() {
403
+ return [
404
+ { prop: 'overflow', value: 'hidden' },
405
+ { prop: 'text-overflow', value: 'ellipsis' },
406
+ { prop: 'white-space', value: 'nowrap' },
407
+ ];
408
+ },
409
+ transition(v, ctx) {
410
+ const t = v.trim();
411
+ if (t === 'none') return [{ prop: 'transition', value: 'none' }];
412
+ const atoms = t.split(/\s+/);
413
+ const dur = atoms[0];
414
+ const easing = atoms[1] && EASINGS[atoms[1]] ? EASINGS[atoms[1]] : (atoms[1] || 'ease');
415
+ const ms = /^\d+$/.test(dur) ? dur + 'ms' : dur;
416
+ return [{ prop: 'transition', value: `all ${ms} ${easing}` }];
417
+ },
418
+ animate(v, ctx) {
419
+ const t = v.trim();
420
+ if (t === 'none') return [{ prop: '@animate', value: null }];
421
+ const atoms = t.split(/\s+/);
422
+ const spec = { motion: atoms[0], duration: 400, easing: EASINGS.smooth, fill: 'both', delay: 0 };
423
+ for (let k = 1; k < atoms.length; k++) {
424
+ const a = atoms[k];
425
+ if (/^\d+(ms|s)?$/.test(a)) {
426
+ const ms = a.endsWith('s') && !a.endsWith('ms') ? parseFloat(a) * 1000 : parseFloat(a);
427
+ if (k === 1) spec.duration = ms; else spec.delay = ms;
428
+ continue;
429
+ }
430
+ if (EASINGS[a]) { spec.easing = EASINGS[a]; continue; }
431
+ if (['both', 'forwards', 'backwards', 'none', 'auto'].includes(a)) { spec.fill = a; continue; }
432
+ ctx.errors.push({ file: ctx.file, line: ctx.line, message: `unknown animate part "${a}"`, hint: didYouMean(a, Object.keys(EASINGS).concat(['both', 'forwards', 'backwards'])) });
433
+ }
434
+ return [{ prop: '@animate', value: spec }];
435
+ },
436
+ };
437
+
438
+ /* ------------------------------------------------------------------ *
439
+ * resolveDecl — one declaration → zero or more engine decls
440
+ * ------------------------------------------------------------------ */
441
+
442
+ function resolveDecl(prop, value, ctx) {
443
+ const errors = ctx.errors;
444
+ const ctx2 = { ...ctx, prop, errors };
445
+
446
+ if (prop.startsWith('--')) return [{ prop, value: resolveValue(value, { ...ctx2, unitless: false }) }];
447
+
448
+ const sem = SEMANTIC[prop];
449
+ if (sem) return sem(value, ctx2);
450
+
451
+ if (!KNOWN_PROPS.has(prop)) {
452
+ errors.push({
453
+ file: ctx.file, line: ctx.line,
454
+ message: `unknown property "${prop}"`,
455
+ hint: didYouMean(prop, [...KNOWN_PROPS, ...Object.keys(SEMANTIC)]),
456
+ });
457
+ return [];
458
+ }
459
+ return [{ prop, value: resolveValue(value, { ...ctx2, unitless: UNITLESS_PROPS.has(prop) }) }];
460
+ }
461
+
462
+ /* ------------------------------------------------------------------ *
463
+ * Media / container / signal normalization
464
+ * ------------------------------------------------------------------ */
465
+
466
+ function normalizeMedia(query, ctx, line) {
467
+ let q = query.trim();
468
+ if (!q) {
469
+ ctx.errors.push({ file: ctx.file, line, message: '@media needs a condition', hint: 'e.g. @media (width >= 768)' });
470
+ return '(min-width: 0)';
471
+ }
472
+ // bare numbers become px: (width >= 768) → (width >= 768px)
473
+ q = q.replace(/([>=<]\s*)(\d+(?:\.\d+)?)\b(?!\s*(?:px|%|em|rem|vh|vw|vmin|vmax|s|ms|deg|dppx|x))/g, '$1$2px');
474
+ return q;
475
+ }
476
+
477
+ function parseSignal(expr, ctx, line) {
478
+ const m = expr.match(/^([A-Za-z_][\w-]*)\s*(==|!=)\s*(.+)$/);
479
+ if (m) {
480
+ let v = m[3].trim();
481
+ if ((v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'"))) v = v.slice(1, -1);
482
+ return { name: m[1], op: m[2], value: v };
483
+ }
484
+ if (/^[A-Za-z_][\w-]*$/.test(expr.trim())) return { name: expr.trim(), op: 'truthy', value: null };
485
+ ctx.errors.push({ file: ctx.file, line, message: `bad @signal expression "${expr}"`, hint: 'try: @signal theme == dark or @signal compact' });
486
+ return { name: expr.trim(), op: 'truthy', value: null };
487
+ }
488
+
489
+ /* ------------------------------------------------------------------ *
490
+ * The flattener
491
+ * ------------------------------------------------------------------ */
492
+
493
+ export function resolve(ast, file = 'inline.dssl', baseTokens = null) {
494
+ const errors = [];
495
+ const rules = [];
496
+ const shadowRules = [];
497
+ const motions = {};
498
+ const behaviors = {};
499
+ let tokenMap = new Map(Object.entries(baseTokens || DEFAULT_TOKENS));
500
+ const tokenLines = new Map(); // for docs/debug
501
+ let order = 0;
502
+ let ruleId = 0;
503
+
504
+ /* ---- pass 1: collect tokens / behaviors / motions ---- */
505
+
506
+ const collectTokens = (node, path, acc) => {
507
+ for (const child of node.children) {
508
+ if (child.kind === 'decl') {
509
+ const key = path ? path + '.' + child.prop : child.prop;
510
+ tokenMap.set(key, child.value);
511
+ tokenLines.set(key, child.line);
512
+ if (acc) acc.push(key);
513
+ } else if (child.kind === 'selector' || child.kind === 'tokens') {
514
+ // nested groups written as bare words: `color { ink: ... }`
515
+ collectTokens(child, path ? path + '.' + child.sel : child.sel, acc);
516
+ } else {
517
+ errors.push({ file, line: child.line, message: `unexpected "${child.kind}" inside tokens` });
518
+ }
519
+ }
520
+ };
521
+
522
+ const prepass = (nodes) => {
523
+ for (const node of nodes) {
524
+ if (node.kind === 'tokens') collectTokens(node, '');
525
+ else if (node.kind === 'behavior') {
526
+ if (behaviors[node.name]) errors.push({ file, line: node.line, message: `behavior "${node.name}" is defined twice` });
527
+ behaviors[node.name] = node;
528
+ } else if (node.kind === 'motion') {
529
+ motions[node.name] = node;
530
+ } else {
531
+ for (const child of node.children) if (child.kind !== 'decl') prepass([child]);
532
+ }
533
+ }
534
+ };
535
+ prepass(ast.children);
536
+
537
+ /* ---- motions → keyframes ---- */
538
+
539
+ const parseMotion = (name) => {
540
+ const node = motions[name];
541
+ if (!node) return null;
542
+ if (node._parsed) return node._parsed;
543
+ node._parsed = { essential: false, frames: [] };
544
+ const frameOf = (label) => {
545
+ if (label === 'from') return 0;
546
+ if (label === 'to') return 100;
547
+ const m = label.match(/^at\s+([0-9.]+)\s*%?$/);
548
+ return m ? parseFloat(m[1]) : null;
549
+ };
550
+ const ctxBase = { file, line: node.line, errors, tokens: tokenMap, tokenNames: [...tokenMap.keys()], prop: 'motion', unitless: false };
551
+ for (const child of node.children) {
552
+ if (child.kind === 'decl' && child.prop === 'essential') {
553
+ node._parsed.essential = child.value === '' || child.value === 'true';
554
+ continue;
555
+ }
556
+ if (child.kind === 'decl') {
557
+ errors.push({ file, line: child.line, message: `"${child.prop}" is not allowed inside motion`, hint: 'motions take from/to/at blocks and `essential`' });
558
+ continue;
559
+ }
560
+ if (child.kind === 'selector') {
561
+ const offset = frameOf(child.sel);
562
+ if (offset === null) {
563
+ errors.push({ file, line: child.line, message: `bad keyframe selector "${child.sel}"`, hint: 'use from, to, or at 40%' });
564
+ continue;
565
+ }
566
+ const props = {};
567
+ for (const d of child.children) {
568
+ if (d.kind !== 'decl') continue;
569
+ const resolved = resolveDecl(d.prop, d.value, { ...ctxBase, line: d.line });
570
+ for (const r of resolved) {
571
+ if (r.prop.startsWith('@') || r.prop.startsWith('--')) continue;
572
+ const camel = r.prop.replace(/-([a-z])/g, (_, ch) => ch.toUpperCase());
573
+ props[camel] = r.value;
574
+ }
575
+ }
576
+ node._parsed.frames.push({ offset, props });
577
+ }
578
+ }
579
+ node._parsed.frames.sort((a, b) => a.offset - b.offset);
580
+ return node._parsed;
581
+ };
582
+
583
+ /* ---- token emission ---- */
584
+
585
+ const tokenNames = () => [...tokenMap.keys()];
586
+
587
+ const resolveTokenDecls = (keys, conds) => {
588
+ const decls = [];
589
+ const ctxBase = { file, line: 0, errors, tokens: tokenMap, tokenNames: tokenNames(), prop: 'token', unitless: false };
590
+ for (const key of keys) {
591
+ const raw = tokenMap.get(key);
592
+ const prop = '--dssl-' + key.replace(/\./g, '-');
593
+ decls.push({ prop, value: resolveValue(raw, { ...ctxBase, prop: key }) });
594
+ }
595
+ if (!decls.length) return null;
596
+ return {
597
+ id: ruleId++, sel: ':root', media: conds.media, signals: conds.signals,
598
+ containers: conds.containers, layer: 'tokens', order: order++,
599
+ decls, file, line: 0, pseudo: null, states: new Set(),
600
+ };
601
+ };
602
+
603
+ /** used by index.js to install the shared registry as a base bundle */
604
+ // Built immediately, before the walk, so its cascade order sorts below every
605
+ // author rule (engine cascade: layer first, then source order).
606
+ const registryRule = resolveTokenDecls([...tokenMap.keys()], { media: [], signals: [], containers: [] });
607
+
608
+ /* ---- main recursive walk ---- */
609
+
610
+ const walk = (children, ctx, ctxRule) => {
611
+ for (const node of children) {
612
+ switch (node.kind) {
613
+ case 'decl': {
614
+ if (node.prop === 'use') {
615
+ const name = node.value.trim();
616
+ const bh = behaviors[name];
617
+ if (!bh) {
618
+ errors.push({ file, line: node.line, message: `unknown behavior "${name}"`, hint: didYouMean(name, Object.keys(behaviors)) });
619
+ break;
620
+ }
621
+ if (bh._active) { errors.push({ file, line: node.line, message: `behavior cycle involving "${name}"` }); break; }
622
+ bh._active = true;
623
+ walk(bh.children, ctx, ctxRule);
624
+ bh._active = false;
625
+ break;
626
+ }
627
+ const resolved = resolveDecl(node.prop, node.value, { file, line: node.line, errors, tokens: tokenMap, tokenNames: tokenNames(), prop: node.prop, unitless: false });
628
+ for (const r of resolved) ctxRule.decls.push({ ...r, line: node.line });
629
+ break;
630
+ }
631
+ case 'selector': {
632
+ const sel = ctx.sel === '' ? node.sel : combine(ctx.sel, node.sel);
633
+ const childCtx = { ...ctx, sel };
634
+ const sub = { decls: [] };
635
+ walk(node.children, childCtx, sub);
636
+ flush(sub, childCtx, node.line);
637
+ break;
638
+ }
639
+ case 'state': {
640
+ if (!ctx.sel) {
641
+ errors.push({ file, line: node.line, message: `"${node.name}" needs a selector around it` });
642
+ break;
643
+ }
644
+ const childCtx = { ...ctx, sel: ctx.sel + ':' + node.name };
645
+ const sub = { decls: [] };
646
+ walk(node.children, childCtx, sub);
647
+ flush(sub, childCtx, node.line);
648
+ break;
649
+ }
650
+ case 'media': {
651
+ const q = normalizeMedia(node.query, { errors, file, line: node.line }, node.line);
652
+ const sub = { decls: [] };
653
+ walk(node.children, { ...ctx, media: [...ctx.media, q] }, sub);
654
+ flush(sub, ctx, node.line);
655
+ break;
656
+ }
657
+ case 'container': {
658
+ const q = normalizeMedia(node.query || '(min-width: 0)', { errors, file, line: node.line }, node.line);
659
+ const sub = { decls: [] };
660
+ walk(node.children, { ...ctx, containers: [...ctx.containers, q] }, sub);
661
+ flush(sub, ctx, node.line);
662
+ break;
663
+ }
664
+ case 'signal': {
665
+ const sig = parseSignal(node.expr, { errors, file, line: node.line }, node.line);
666
+ const sub = { decls: [] };
667
+ walk(node.children, { ...ctx, signals: [...ctx.signals, sig] }, sub);
668
+ flush(sub, ctx, node.line);
669
+ break;
670
+ }
671
+ case 'layer':
672
+ case 'pin': {
673
+ const layer = node.kind === 'pin' ? 'pin' : (['tokens', 'base', 'components', 'utilities', 'pin'].includes(node.name) ? node.name : 'components');
674
+ const sub = { decls: [] };
675
+ walk(node.children, { ...ctx, layer }, sub);
676
+ flush(sub, ctx, node.line);
677
+ break;
678
+ }
679
+ case 'pseudo': {
680
+ const base = ctx.sel || '*'; // top-level ::selection etc. means every element
681
+ const srule = {
682
+ id: ruleId++, sel: base, pseudo: node.name,
683
+ media: ctx.media, signals: ctx.signals, containers: ctx.containers,
684
+ layer: ctx.layer, order: order++, decls: [], file, line: node.line,
685
+ };
686
+ for (const d of node.children) {
687
+ if (d.kind !== 'decl') { errors.push({ file, line: d.line, message: 'only declarations may appear inside a pseudo-element block (v0.2)' }); continue; }
688
+ if (d.prop === 'content' && d.value === '') { srule.decls.push({ prop: 'content', value: '""', line: d.line }); continue; }
689
+ const resolved = resolveDecl(d.prop, d.value, { file, line: d.line, errors, tokens: tokenMap, tokenNames: tokenNames(), prop: d.prop, unitless: false });
690
+ srule.decls.push(...resolved.map(r => ({ ...r, line: d.line })));
691
+ }
692
+ if (srule.decls.length) shadowRules.push(srule);
693
+ break;
694
+ }
695
+ case 'tokens': {
696
+ if (ctx.sel) { errors.push({ file, line: node.line, message: 'tokens must be top-level or inside @media / @signal' }); break; }
697
+ // structural tracking: only keys THIS block declares are emitted,
698
+ // so conditional palettes don't shadow unrelated tokens
699
+ const ownKeys = [];
700
+ collectTokens(node, '', ownKeys);
701
+ const tr = ownKeys.length ? resolveTokenDecls(ownKeys, ctx) : null;
702
+ if (tr) rules.push(tr);
703
+ break;
704
+ }
705
+ case 'behavior':
706
+ case 'motion':
707
+ if (ctx.depth) errors.push({ file, line: node.line, message: `${node.kind} "${node.name}" must be top-level` });
708
+ break;
709
+ default:
710
+ break;
711
+ }
712
+ }
713
+ };
714
+
715
+ function flush(sub, ctx, line) {
716
+ if (!sub.decls.length) return;
717
+ if (!ctx.sel) {
718
+ errors.push({ file, line, message: 'declarations must live inside a selector', hint: 'top-level blocks are tokens, behavior, motion, @layer, @media… — or a selector like .card' });
719
+ return;
720
+ }
721
+ rules.push({
722
+ id: ruleId++, sel: ctx.sel, media: ctx.media, signals: ctx.signals,
723
+ containers: ctx.containers, layer: ctx.layer, order: order++,
724
+ decls: sub.decls, file, line, pseudo: null,
725
+ states: scanStates(ctx.sel),
726
+ });
727
+ }
728
+
729
+ /* ---- go ---- */
730
+
731
+ const topCtx = { sel: '', media: [], signals: [], containers: [], layer: 'components', depth: 0 };
732
+ const rootRule = { decls: [] };
733
+ walk(ast.children, topCtx, rootRule);
734
+ flush(rootRule, topCtx, 0);
735
+
736
+ // parse all motions that were referenced
737
+ for (const r of rules) {
738
+ for (const d of r.decls) {
739
+ if (d.prop === '@animate' && d.value && d.value.motion) {
740
+ const m = parseMotion(d.value.motion);
741
+ if (!m) {
742
+ errors.push({
743
+ file, line: d.line,
744
+ message: `unknown motion "${d.value.motion}"`,
745
+ hint: didYouMean(d.value.motion, Object.keys(motions)),
746
+ });
747
+ d.value.broken = true;
748
+ } else {
749
+ d.value.frames = m.frames;
750
+ d.value.essential = m.essential;
751
+ }
752
+ }
753
+ }
754
+ }
755
+
756
+ const mediaQueries = new Set();
757
+ for (const r of [...rules, ...shadowRules]) r.media.forEach(q => mediaQueries.add(q));
758
+
759
+ return { rules, shadowRules, motions, errors, mediaQueries: [...mediaQueries], tokens: Object.fromEntries(tokenMap), registryRule };
760
+ }
761
+
762
+ /** naive `&` substitution + descendant join */
763
+ function combine(parent, child) {
764
+ if (child.includes('&')) return child.split('&').join(parent);
765
+ return parent + ' ' + child;
766
+ }
767
+
768
+ const STATE_SCAN = /:(hover|focus-visible|focus|active|disabled|checked|empty|target|invalid|valid|required|open|closed|read-only|read-write|placeholder-shown|first-child|last-child|only-child)\b/g;
769
+ function scanStates(sel) {
770
+ const s = new Set();
771
+ if (!sel) return s;
772
+ let m;
773
+ const re = new RegExp(STATE_SCAN.source, 'g');
774
+ while ((m = re.exec(sel))) s.add(m[1]);
775
+ return s;
776
+ }
public/dssl/utils.js ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* DSSL — utils.js — tiny shared helpers */
2
+
3
+ export function levenshtein(a, b) {
4
+ if (a === b) return 0;
5
+ const m = a.length, n = b.length;
6
+ if (!m) return n;
7
+ if (!n) return m;
8
+ let prev = new Array(n + 1);
9
+ let cur = new Array(n + 1);
10
+ for (let j = 0; j <= n; j++) prev[j] = j;
11
+ for (let i = 1; i <= m; i++) {
12
+ cur[0] = i;
13
+ for (let j = 1; j <= n; j++) {
14
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
15
+ cur[j] = Math.min(prev[j] + 1, cur[j - 1] + 1, prev[j - 1] + cost);
16
+ }
17
+ [prev, cur] = [cur, prev];
18
+ }
19
+ return prev[n];
20
+ }
21
+
22
+ /** best "did you mean ..." suggestion, or undefined */
23
+ export function didYouMean(word, candidates) {
24
+ if (!word || !candidates || !candidates.length) return undefined;
25
+ let best = undefined, bestDist = Infinity;
26
+ const w = word.toLowerCase();
27
+ for (const c of candidates) {
28
+ if (!c) continue;
29
+ const d = levenshtein(w, String(c).toLowerCase());
30
+ if (d < bestDist) { bestDist = d; best = c; }
31
+ }
32
+ const tolerance = Math.max(2, Math.floor(w.length / 3));
33
+ return bestDist <= tolerance ? String(best) : undefined;
34
+ }
public/favicon.svg ADDED
public/logo.svg ADDED
public/robots.txt ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ User-agent: Googlebot
2
+ Allow: /
3
+
4
+ User-agent: Bingbot
5
+ Allow: /
6
+
7
+ User-agent: Twitterbot
8
+ Allow: /
9
+
10
+ User-agent: facebookexternalhit
11
+ Allow: /
12
+
13
+ User-agent: *
14
+ Allow: /
public/styles/changelog.dssl ADDED
@@ -0,0 +1,97 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // =========================================================================
2
+ // changelog.dssl — the "grown while building" timeline
3
+ // =========================================================================
4
+
5
+ .tl {
6
+ border-left: 2 solid color.line
7
+ margin-left: 8
8
+ padding-left: 26
9
+ column: gap space.6
10
+ margin-top: space.5
11
+ }
12
+
13
+ .tl-item {
14
+ position: relative
15
+ }
16
+
17
+ .tl-item::before {
18
+ content: ""
19
+ position: absolute
20
+ left: -33
21
+ top: 5
22
+ size: 12
23
+ circle: 12
24
+ background: color.accent
25
+ border: 2 solid color.paper
26
+ box-shadow: 0 0 0 2 color.accent/25
27
+ }
28
+
29
+ .tl-item-add::before {
30
+ background: color.ok
31
+ box-shadow: 0 0 0 2 color.ok/25
32
+ }
33
+
34
+ .tl-date {
35
+ font-family: font.mono
36
+ font-size: 11.5
37
+ color: color.muted
38
+ letter-spacing: 0.06em
39
+ text-transform: uppercase
40
+ }
41
+
42
+ .tl-item h3 {
43
+ font-size: 17
44
+ font-weight: 700
45
+ margin: 3 0 6
46
+ letter-spacing: -0.01em
47
+ }
48
+
49
+ .tl-item p {
50
+ font-size: 14
51
+ color: color.muted
52
+ line-height: 1.65
53
+ margin: 0 0 8
54
+ max-width: 640
55
+ }
56
+
57
+ .tl-why {
58
+ font-family: font.mono
59
+ font-size: 12
60
+ color: color.accent
61
+ background: color.accent/8
62
+ border: 1 solid color.accent/20
63
+ padding: 3 9
64
+ radius: full
65
+ display: inline-block
66
+ margin-bottom: 8
67
+ }
68
+
69
+ .tl-version {
70
+ font-family: font.mono
71
+ font-size: 12
72
+ color: color.muted
73
+ border: 1 solid color.line
74
+ padding: 3 9
75
+ radius: full
76
+ display: inline-block
77
+ margin-bottom: 8
78
+ margin-right: 6
79
+ background: color.card
80
+ }
81
+
82
+ .vote-badge {
83
+ font-family: font.mono
84
+ font-size: 12
85
+ padding: 3 10
86
+ radius: full
87
+ border: 1 solid color.ok/35
88
+ color: color.ok
89
+ background: color.ok/8
90
+ display: inline-block
91
+ }
92
+
93
+ .vote-badge-veto {
94
+ border-color: color.err/35
95
+ color: color.err
96
+ background: color.err/8
97
+ }
public/styles/docs.dssl ADDED
@@ -0,0 +1,321 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // =========================================================================
2
+ // docs.dssl — language reference layout (also used by the spec view)
3
+ // =========================================================================
4
+
5
+ .page-head {
6
+ padding: space.6 0 space.2
7
+ }
8
+
9
+ .page-title {
10
+ fluid: 30, 44
11
+ font-weight: 800
12
+ letter-spacing: -0.03em
13
+ margin: space.2 0 space.2
14
+ }
15
+
16
+ .page-sub {
17
+ color: color.muted
18
+ font-size: 16.5
19
+ line-height: 1.65
20
+ max-width: 640
21
+ }
22
+
23
+ .docs-layout {
24
+ display: grid
25
+ grid-template-columns: 230 minmax(0, 1fr)
26
+ gap: space.6
27
+ align-items: start
28
+ padding-bottom: space.7
29
+ }
30
+
31
+ @media (width <= 920) {
32
+ .docs-layout {
33
+ grid-template-columns: 1fr
34
+ }
35
+ .docs-side {
36
+ display: none
37
+ }
38
+ }
39
+
40
+ .docs-side {
41
+ position: sticky
42
+ top: 88
43
+ column: gap 1
44
+ max-height: calc(100vh - 110)
45
+ overflow-y: auto
46
+ padding-right: 6
47
+ scrollbar-width: thin
48
+ }
49
+
50
+ .side-head {
51
+ font-family: font.mono
52
+ font-size: 10.5
53
+ text-transform: uppercase
54
+ letter-spacing: 0.14em
55
+ color: color.muted
56
+ padding: 0 12
57
+ margin: space.3 0 4
58
+ }
59
+
60
+ .side-head:first-child {
61
+ margin-top: 0
62
+ }
63
+
64
+ .docs-side a {
65
+ padding: 6 12
66
+ radius: 0 sm sm 0
67
+ font-size: 13.5
68
+ color: color.muted
69
+ transition: 130
70
+ border-left: 2 solid transparent
71
+ line-height: 1.4
72
+ }
73
+
74
+ .docs-side a:hover {
75
+ color: color.ink
76
+ background: color.line/50
77
+ border-left-color: color.accent/40
78
+ }
79
+
80
+ .docs-body {
81
+ min-width: 0
82
+ column: gap space.7
83
+ }
84
+
85
+ .doc-block {
86
+ column: gap space.3
87
+ scroll-margin-top: 92
88
+ }
89
+
90
+ .doc-lead {
91
+ color: color.muted
92
+ font-size: 15.5
93
+ line-height: 1.7
94
+ margin: 0
95
+ }
96
+
97
+ // live example well
98
+ .demo-box {
99
+ display: grid
100
+ place: center
101
+ gap: 12
102
+ border: 1 dashed color.line
103
+ radius: md
104
+ padding: space.5
105
+ background:
106
+ radial-gradient(color.line/45 1, transparent 1)
107
+ background-size: 16 16
108
+ min-height: 96
109
+ }
110
+
111
+ // tables (semantic property reference, spec votes, …)
112
+ .tbl {
113
+ width: 100%
114
+ border-collapse: collapse
115
+ font-size: 13.5
116
+ }
117
+
118
+ .tbl th {
119
+ text-align: left
120
+ padding: 9 12
121
+ background: color.card
122
+ border-bottom: 2 solid color.line
123
+ font-size: 11
124
+ text-transform: uppercase
125
+ letter-spacing: 0.09em
126
+ color: color.muted
127
+ font-family: font.mono
128
+ }
129
+
130
+ .tbl td {
131
+ padding: 10 12
132
+ border-bottom: 1 solid color.line
133
+ vertical-align: top
134
+ line-height: 1.55
135
+ color: color.ink/90
136
+ }
137
+
138
+ .tbl tr:hover td {
139
+ background: color.accent/4
140
+ }
141
+
142
+ .tbl code {
143
+ font-family: font.mono
144
+ font-size: 12.5
145
+ color: color.accent
146
+ background: color.accent/9
147
+ padding: 1 6
148
+ radius: 5
149
+ white-space: nowrap
150
+ }
151
+
152
+ .tbl-note {
153
+ font-size: 12.5
154
+ color: color.muted
155
+ margin: 8 0 0
156
+ }
157
+
158
+ // callouts
159
+ .note {
160
+ border-left: 3 solid color.accent
161
+ background: color.accent/7
162
+ radius: 0 md md 0
163
+ padding: space.3 space.4
164
+ font-size: 14
165
+ line-height: 1.65
166
+ color: color.ink/90
167
+ }
168
+
169
+ .note-warn {
170
+ border-left-color: color.err
171
+ background: color.err/7
172
+ }
173
+
174
+ .note b {
175
+ font-weight: 700
176
+ }
177
+
178
+ // docs live-demo widgets
179
+ .doc-btn {
180
+ padding: 10 18
181
+ radius: sm
182
+ background: color.card
183
+ border: 1 solid color.line
184
+ font-weight: 600
185
+ font-size: 14
186
+ cursor: pointer
187
+ transition: 150
188
+ color: color.ink
189
+ }
190
+
191
+ .doc-btn:hover {
192
+ border-color: color.accent/50
193
+ translate: 0 -1px
194
+ shadow: sm
195
+ }
196
+
197
+ .doc-card {
198
+ width: 250
199
+ padding: space.4
200
+ radius: md
201
+ background: color.card
202
+ border: 1 solid color.line
203
+ shadow: sm
204
+ transition: 200
205
+ column: gap 8
206
+ font-size: 13.5
207
+ }
208
+
209
+ .doc-card:hover {
210
+ translate: 0 -4px
211
+ shadow: lg
212
+ }
213
+
214
+ .doc-card-title {
215
+ font-weight: 700
216
+ font-size: 14.5
217
+ }
218
+
219
+ .doc-card-sub {
220
+ color: color.muted
221
+ font-size: 12.5
222
+ line-height: 1.5
223
+ }
224
+
225
+ .motion-box {
226
+ padding: space.4 space.5
227
+ radius: md
228
+ background: color.code-bg
229
+ color: color.code-ink
230
+ font-family: font.mono
231
+ font-size: 13.5
232
+ min-width: 220
233
+ text-align: center
234
+ }
235
+
236
+ .signal-box {
237
+ padding: space.4 space.5
238
+ radius: md
239
+ border: 1 solid color.line
240
+ background: color.card
241
+ font-size: 14
242
+ font-weight: 600
243
+ transition: 250
244
+ color: color.muted
245
+ }
246
+
247
+ @signal docdemo == hot {
248
+ .signal-box {
249
+ background: color.accent
250
+ border-color: color.accent
251
+ color: color.accent-ink
252
+ translate: 0 -3px
253
+ shadow: md
254
+ }
255
+ }
256
+
257
+ .tok-demo {
258
+ row: gap 10 center
259
+ }
260
+
261
+ .tok-dot {
262
+ size: 34
263
+ circle: 34
264
+ place: center
265
+ font-family: font.mono
266
+ font-size: 11
267
+ border: 1 solid color.line
268
+ background: color.card
269
+ color: color.muted
270
+ transition: 200
271
+ cursor: pointer
272
+ }
273
+
274
+ .tok-dot:hover {
275
+ translate: 0 -3px
276
+ shadow: md
277
+ border-color: color.accent/40
278
+ color: color.accent
279
+ }
280
+
281
+ .scale-bar {
282
+ row: gap 6
283
+ align-items: flex-end
284
+ }
285
+
286
+ .scale-step {
287
+ width: 34
288
+ background: color.accent/70
289
+ radius: 4 4 0 0
290
+ transition: 150
291
+ }
292
+
293
+ .scale-step:hover {
294
+ background: color.accent
295
+ }
296
+
297
+ // motion demo — replayed via DSSL.replay()
298
+ motion pop {
299
+ from {
300
+ opacity: 0
301
+ translate: 0 16
302
+ scale: 0.9
303
+ }
304
+ }
305
+
306
+ .motion-box {
307
+ animate: pop 500 snap
308
+ }
309
+
310
+ // fluid type demo
311
+ .fluid-demo {
312
+ fluid: 20, 44
313
+ font-weight: 800
314
+ letter-spacing: -0.02em
315
+ line-height: 1.15
316
+ text-align: center
317
+ }
318
+
319
+ .fine-print {
320
+ font-size: 13
321
+ }
public/styles/home.dssl ADDED
@@ -0,0 +1,422 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // =========================================================================
2
+ // home.dssl — the landing view
3
+ // =========================================================================
4
+
5
+ // ------------------------------------------------------------------- hero
6
+
7
+ .hero {
8
+ position: relative
9
+ overflow: hidden
10
+ padding: space.8 0 space.7
11
+ }
12
+
13
+ .hero-glow {
14
+ position: absolute
15
+ top: -220
16
+ right: -160
17
+ size: 560
18
+ circle: 560
19
+ background: radial-gradient(circle, color.accent/16, transparent 65%)
20
+ pointer-events: none
21
+ }
22
+
23
+ .hero-glow-2 {
24
+ position: absolute
25
+ bottom: -260
26
+ left: -200
27
+ size: 520
28
+ circle: 520
29
+ background: radial-gradient(circle, color.accent/9, transparent 60%)
30
+ pointer-events: none
31
+ }
32
+
33
+ .hero-grid {
34
+ row: gap space.6 between
35
+ align: center
36
+ position: relative
37
+ }
38
+
39
+ @media (width <= 940) {
40
+ .hero-grid {
41
+ display: grid
42
+ gap: space.6
43
+ }
44
+ }
45
+
46
+ .hero-copy {
47
+ max-width: 560
48
+ }
49
+
50
+ .hero-title {
51
+ fluid: 40, 62
52
+ font-weight: 800
53
+ line-height: 1.04
54
+ letter-spacing: -0.035em
55
+ margin: space.3 0 space.4
56
+ }
57
+
58
+ .hero-title em {
59
+ font-style: normal
60
+ color: color.accent
61
+ }
62
+
63
+ .hero-sub {
64
+ font-size: 17.5
65
+ line-height: 1.7
66
+ color: color.muted
67
+ margin: 0 0 space.5
68
+ }
69
+
70
+ .hero-ctas {
71
+ row: gap 12 center
72
+ }
73
+
74
+ .hero-meta {
75
+ row: gap 14 center
76
+ margin-top: space.5
77
+ color: color.muted
78
+ font-size: 13
79
+ font-family: font.mono
80
+ }
81
+
82
+ .hero-meta span::before {
83
+ content: ""
84
+ inline-size: 4
85
+ aspect-ratio: 1
86
+ background: color.ok
87
+ radius: full
88
+ display: inline-block
89
+ margin-right: 7
90
+ vertical-align: 1
91
+ }
92
+
93
+ // ------------------------------------------------------- hero demo stage
94
+
95
+ .demo {
96
+ width: 100%
97
+ max-width: 480
98
+ }
99
+
100
+ .demo-tabs {
101
+ row: gap 6
102
+ margin-bottom: 10
103
+ }
104
+
105
+ .demo-tab {
106
+ padding: 7 14
107
+ radius: full
108
+ font-size: 13
109
+ font-weight: 600
110
+ color: color.muted
111
+ border: 1 solid color.line
112
+ background: color.card/50
113
+ cursor: pointer
114
+ transition: 140
115
+ }
116
+
117
+ .demo-tab:hover {
118
+ color: color.ink
119
+ translate: 0 -1px
120
+ }
121
+
122
+ .demo-tab[aria-pressed="true"] {
123
+ background: color.accent
124
+ border-color: color.accent
125
+ color: color.accent-ink
126
+ }
127
+
128
+ .demo-stage {
129
+ background: color.card
130
+ border: 1 solid color.line
131
+ radius: xl
132
+ overflow: hidden
133
+ shadow: lg
134
+ }
135
+
136
+ .demo-code {
137
+ border: none
138
+ border-bottom: 1 solid color.code-line/60
139
+ radius: 0
140
+ font-size: 12.5
141
+ padding: space.3 space.4
142
+ min-height: 168
143
+ }
144
+
145
+ .demo-canvas {
146
+ padding: space.5
147
+ display: grid
148
+ place: center
149
+ gap: 14
150
+ min-height: 190
151
+ }
152
+
153
+ // shared vertical rhythm between intro text and grids
154
+ .sect-gap {
155
+ margin-top: space.4
156
+ }
157
+
158
+ // the live demo card — every rule below is real, hovering it really works
159
+ motion rise-in {
160
+ from {
161
+ opacity: 0
162
+ translate: 0 12
163
+ }
164
+ }
165
+
166
+ .demo-card {
167
+ width: 240
168
+ padding: space.4
169
+ radius: md
170
+ background: color.card
171
+ border: 1 solid color.line
172
+ shadow: md
173
+ transition: 200
174
+ column: gap 10
175
+ animate: rise-in 500 snap
176
+ }
177
+
178
+ .demo-card:hover {
179
+ translate: 0 -5px
180
+ shadow: lg
181
+ border-color: color.accent/40
182
+ }
183
+
184
+ .demo-swatch {
185
+ height: 64
186
+ radius: sm
187
+ background: linear-gradient(135deg, color.accent, color.accent/45)
188
+ transition: 250
189
+ }
190
+
191
+ .demo-card:hover .demo-swatch {
192
+ background: linear-gradient(135deg, color.accent/60, color.ink/80)
193
+ }
194
+
195
+ .demo-card h4 {
196
+ font-size: 15.5
197
+ font-weight: 700
198
+ }
199
+
200
+ .demo-card p {
201
+ font-size: 13
202
+ color: color.muted
203
+ line-height: 1.5
204
+ margin: 0
205
+ }
206
+
207
+ .demo-chip {
208
+ align-self: start
209
+ font-family: font.mono
210
+ font-size: 11
211
+ color: color.accent
212
+ background: color.accent/12
213
+ padding: 3 9
214
+ radius: full
215
+ }
216
+
217
+ // states demo: a real focusable button styled by focus-visible
218
+ .demo-btn {
219
+ padding: 12 22
220
+ radius: md
221
+ background: color.ink
222
+ color: color.paper
223
+ font-weight: 650
224
+ font-size: 14.5
225
+ cursor: pointer
226
+ transition: 160
227
+ border: none
228
+ }
229
+
230
+ .demo-btn:hover { translate: 0 -2px; shadow: md }
231
+
232
+ // motion demo
233
+ .demo-motion {
234
+ width: 200
235
+ padding: space.4
236
+ radius: md
237
+ background: color.code-bg
238
+ color: color.code-ink
239
+ font-family: font.mono
240
+ font-size: 13
241
+ text-align: center
242
+ }
243
+
244
+ // signal demo — the button toggles a JS signal; DSSL re-styles live
245
+ .demo-signal-card {
246
+ width: 240
247
+ padding: space.4
248
+ radius: md
249
+ background: color.card
250
+ border: 1 solid color.line
251
+ transition: 300
252
+ column: gap 8
253
+ font-size: 13.5
254
+ color: color.muted
255
+ }
256
+
257
+ @signal demo == on {
258
+ .demo-signal-card {
259
+ background: color.accent
260
+ border-color: color.accent
261
+ color: color.accent-ink
262
+ translate: 0 -4px
263
+ shadow: lg
264
+ }
265
+ }
266
+
267
+ // ------------------------------------------------------------ pain cards
268
+
269
+ .pain-grid {
270
+ display: grid
271
+ grid-template-columns: repeat(2, 1fr)
272
+ gap: space.4
273
+ }
274
+
275
+ @media (width <= 760) {
276
+ .pain-grid {
277
+ grid-template-columns: 1fr
278
+ }
279
+ }
280
+
281
+ .pain-card {
282
+ padding: space.5
283
+ column: gap 10
284
+ }
285
+
286
+ .pain-label {
287
+ font-family: font.mono
288
+ font-size: 12
289
+ color: color.err
290
+ font-weight: 600
291
+ }
292
+
293
+ .pain-cure {
294
+ font-family: font.mono
295
+ font-size: 12
296
+ color: color.ok
297
+ font-weight: 600
298
+ }
299
+
300
+ .pain-card h3 {
301
+ font-size: 17
302
+ font-weight: 700
303
+ margin: 0
304
+ }
305
+
306
+ .pain-card p {
307
+ margin: 0
308
+ font-size: 14
309
+ line-height: 1.65
310
+ color: color.muted
311
+ }
312
+
313
+ // ------------------------------------------------------------ comparison
314
+
315
+ .compare {
316
+ display: grid
317
+ grid-template-columns: 1fr 1fr
318
+ gap: space.4
319
+ align-items: start
320
+ }
321
+
322
+ @media (width <= 820) {
323
+ .compare {
324
+ grid-template-columns: 1fr
325
+ }
326
+ }
327
+
328
+ .compare-col {
329
+ column: gap 10
330
+ }
331
+
332
+ .compare-tag {
333
+ font-family: font.mono
334
+ font-size: 12.5
335
+ font-weight: 700
336
+ row: gap 8 center
337
+ }
338
+
339
+ .compare-tag-css { color: color.err }
340
+ .compare-tag-dssl { color: color.ok }
341
+
342
+ // ---------------------------------------------------------- feature grid
343
+
344
+ .feature-grid {
345
+ display: grid
346
+ grid-template-columns: repeat(4, 1fr)
347
+ gap: space.4
348
+ }
349
+
350
+ @media (width <= 980) {
351
+ .feature-grid {
352
+ grid-template-columns: repeat(2, 1fr)
353
+ }
354
+ }
355
+
356
+ @media (width <= 560) {
357
+ .feature-grid {
358
+ grid-template-columns: 1fr
359
+ }
360
+ }
361
+
362
+ .feature {
363
+ padding: space.5
364
+ column: gap 10
365
+ }
366
+
367
+ .feature-icon {
368
+ size: 40
369
+ circle: 40
370
+ place: center
371
+ background: color.accent/12
372
+ color: color.accent
373
+ font-size: 19
374
+ font-weight: 700
375
+ font-family: font.mono
376
+ }
377
+
378
+ .feature h3 {
379
+ font-size: 16
380
+ font-weight: 700
381
+ margin: 0
382
+ }
383
+
384
+ .feature p {
385
+ margin: 0
386
+ font-size: 13.5
387
+ line-height: 1.6
388
+ color: color.muted
389
+ }
390
+
391
+ // ------------------------------------------------------------------- cta
392
+
393
+ .cta-band {
394
+ text-align: center
395
+ padding: space.8 space.4
396
+ radius: xl
397
+ background:
398
+ radial-gradient(ellipse at 30% 0%, color.accent/14, transparent 55%),
399
+ radial-gradient(ellipse at 75% 100%, color.accent/10, transparent 50%),
400
+ color.card
401
+ border: 1 solid color.line
402
+ column: gap space.4
403
+ align-items: center
404
+ }
405
+
406
+ .cta-band h2 {
407
+ fluid: 28, 40
408
+ font-weight: 800
409
+ letter-spacing: -0.03em
410
+ margin: 0
411
+ }
412
+
413
+ .cta-band p {
414
+ color: color.muted
415
+ max-width: 520
416
+ line-height: 1.65
417
+ margin: 0
418
+ }
419
+
420
+ .cta-band .hero-ctas {
421
+ justify-content: center
422
+ }
public/styles/playground.dssl ADDED
@@ -0,0 +1,180 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // =========================================================================
2
+ // playground.dssl — the live editor view
3
+ // =========================================================================
4
+
5
+ .pg-layout {
6
+ display: grid
7
+ grid-template-columns: minmax(0, 1fr) minmax(0, 1fr)
8
+ gap: space.4
9
+ align-items: stretch
10
+ }
11
+
12
+ @media (width <= 940) {
13
+ .pg-layout {
14
+ grid-template-columns: 1fr
15
+ }
16
+ }
17
+
18
+ .pg-pane {
19
+ background: color.card
20
+ border: 1 solid color.line
21
+ radius: lg
22
+ overflow: hidden
23
+ display: flex
24
+ flex-direction: column
25
+ min-height: 560
26
+ }
27
+
28
+ .pg-head {
29
+ row: gap 8 between
30
+ align: center
31
+ padding: 10 14
32
+ border-bottom: 1 solid color.line
33
+ font-family: font.mono
34
+ font-size: 12
35
+ color: color.muted
36
+ background: color.card
37
+ }
38
+
39
+ .pg-head-tools {
40
+ row: gap 8 center
41
+ }
42
+
43
+ .pg-select {
44
+ font-family: font.mono
45
+ font-size: 12
46
+ background: color.card
47
+ color: color.ink
48
+ border: 1 solid color.line
49
+ radius: sm
50
+ padding: 5 8
51
+ cursor: pointer
52
+ }
53
+
54
+ .pg-editor {
55
+ flex: 1
56
+ width: 100%
57
+ min-height: 420
58
+ border: none
59
+ outline: none
60
+ resize: vertical
61
+ background: color.code-bg
62
+ color: color.code-ink
63
+ font-family: font.mono
64
+ font-size: 13
65
+ line-height: 1.7
66
+ padding: space.4
67
+ tab-size: 2
68
+ white-space: pre
69
+ overflow-wrap: normal
70
+ overflow-x: auto
71
+ }
72
+
73
+ .pg-canvas-wrap {
74
+ flex: 1
75
+ display: flex
76
+ flex-direction: column
77
+ min-height: 420
78
+ }
79
+
80
+ .pg-canvas {
81
+ flex: 1
82
+ padding: space.5
83
+ background:
84
+ conic-gradient(color.line/30 25%, transparent 0 50%, color.line/30 0 75%, transparent 0)
85
+ background-size: 18 18
86
+ overflow: auto
87
+ }
88
+
89
+ .pg-canvas-title {
90
+ font-family: font.mono
91
+ font-size: 11
92
+ color: color.muted
93
+ letter-spacing: 0.1em
94
+ text-transform: uppercase
95
+ margin: 0 0 space.3
96
+ }
97
+
98
+ .pg-status {
99
+ row: gap 8 center
100
+ padding: 8 14
101
+ font-family: font.mono
102
+ font-size: 12
103
+ border-top: 1 solid color.line
104
+ background: color.card
105
+ color: color.muted
106
+ }
107
+
108
+ .pg-status-ok { color: color.ok }
109
+ .pg-status-err { color: color.err }
110
+
111
+ .pg-errors {
112
+ column: gap 8
113
+ padding: 12 14
114
+ background: color.err/6
115
+ border-top: 1 solid color.err/25
116
+ max-height: 240
117
+ overflow-y: auto
118
+ scrollbar-width: thin
119
+ }
120
+
121
+ .pg-error {
122
+ font-family: font.mono
123
+ font-size: 12.5
124
+ color: color.err
125
+ line-height: 1.55
126
+ column: gap 2
127
+ }
128
+
129
+ .pg-error-hint {
130
+ color: color.muted
131
+ font-size: 12
132
+ }
133
+
134
+ .pg-error-hint em {
135
+ color: color.ok
136
+ font-style: normal
137
+ font-weight: 600
138
+ }
139
+
140
+ // playground canvas starts empty — that's the point.
141
+ // The hint below shows only while nothing has been matched yet.
142
+ .pg-hint {
143
+ place-self: center
144
+ text-align: center
145
+ color: color.muted
146
+ font-size: 14
147
+ line-height: 1.7
148
+ max-width: 380
149
+ padding: space.5
150
+ border: 1 dashed color.line
151
+ radius: md
152
+ background: color.card/70
153
+ }
154
+
155
+ .pg-hint code {
156
+ font-family: font.mono
157
+ color: color.accent
158
+ font-size: 13
159
+ }
160
+
161
+ // stat chips under the editor
162
+ .pg-stats {
163
+ row: gap 8
164
+ flex-wrap: wrap
165
+ }
166
+
167
+ .stat-chip {
168
+ font-family: font.mono
169
+ font-size: 11.5
170
+ color: color.muted
171
+ border: 1 solid color.line
172
+ background: color.card
173
+ radius: full
174
+ padding: 4 10
175
+ }
176
+
177
+ .stat-chip b {
178
+ color: color.ink
179
+ font-weight: 700
180
+ }
public/styles/site.dssl ADDED
@@ -0,0 +1,472 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // =========================================================================
2
+ // site.dssl — chrome shared by every view of the DSSL site
3
+ // =========================================================================
4
+
5
+ html {
6
+ font-family: font.sans
7
+ -webkit-font-smoothing: antialiased
8
+ scroll-behavior: smooth
9
+ }
10
+
11
+ body {
12
+ background: color.paper
13
+ color: color.ink
14
+ }
15
+
16
+ ::selection {
17
+ background: color.accent/22
18
+ }
19
+
20
+ // accessibility: one behavior, used everywhere
21
+ behavior focus-ring {
22
+ focus-visible {
23
+ outline: 2 solid color.accent
24
+ outline-offset: 2
25
+ radius: 4
26
+ }
27
+ }
28
+
29
+ a { use focus-ring }
30
+ button { use focus-ring }
31
+ input { use focus-ring }
32
+ textarea { use focus-ring }
33
+ select { use focus-ring }
34
+ [tabindex] { use focus-ring }
35
+
36
+ // ---------------------------------------------------------------- layout
37
+
38
+ .site {
39
+ min-height: 100vh
40
+ display: flex
41
+ flex-direction: column
42
+ }
43
+
44
+ .wrap {
45
+ width: 100%
46
+ max-width: 1140
47
+ margin: 0 auto
48
+ padding: 0 space.5
49
+ }
50
+
51
+ .main {
52
+ flex: 1
53
+ }
54
+
55
+ // ------------------------------------------------------------------ nav
56
+
57
+ .nav {
58
+ position: sticky
59
+ top: 0
60
+ z: 50
61
+ background: color.paper/82
62
+ backdrop-filter: blur(14) saturate(1.5)
63
+ border-bottom: 1 solid color.line
64
+ transition: 200
65
+ }
66
+
67
+ .nav-inner {
68
+ row: gap space.4 between
69
+ height: 64
70
+ align: center
71
+ }
72
+
73
+ .nav-logo {
74
+ row: gap 2 center
75
+ font-weight: 800
76
+ font-size: 21
77
+ letter-spacing: -0.02em
78
+ color: color.ink
79
+ transition: 120
80
+ }
81
+
82
+ .logo-mark {
83
+ font-family: font.mono
84
+ color: color.accent
85
+ font-weight: 700
86
+ margin-right: 6
87
+ }
88
+
89
+ .nav-links {
90
+ row: gap 4 center
91
+ }
92
+
93
+ .nav-link {
94
+ padding: 8 13
95
+ radius: sm
96
+ color: color.muted
97
+ font-size: 14.5
98
+ font-weight: 550
99
+ transition: 130
100
+ }
101
+
102
+ .nav-link:hover {
103
+ color: color.ink
104
+ background: color.line/60
105
+ }
106
+
107
+ // active view highlighting via a data attribute on <main> — no JS needed
108
+ [data-view="home"] .nl-home,
109
+ [data-view="docs"] .nl-docs,
110
+ [data-view="spec"] .nl-spec,
111
+ [data-view="playground"] .nl-playground,
112
+ [data-view="changelog"] .nl-changelog {
113
+ color: color.accent
114
+ background: color.accent/10
115
+ }
116
+
117
+ .nav-side {
118
+ row: gap 8 center
119
+ }
120
+
121
+ .theme-btn {
122
+ circle: 36
123
+ place: center
124
+ color: color.muted
125
+ transition: 150
126
+ border: 1 solid color.line
127
+ }
128
+
129
+ .theme-btn:hover {
130
+ color: color.ink
131
+ background: color.line/70
132
+ translate: 0 -1px
133
+ }
134
+
135
+ // icon switching driven by an html attribute — the engine re-styles on
136
+ // attribute mutations, so the toggle needs zero style manipulation in JS
137
+ html[data-theme-mode="dark"] .icon-sun { display: none }
138
+ html[data-theme-mode="light"] .icon-moon { display: none }
139
+ html:not([data-theme-mode="dark"]):not([data-theme-mode="light"]) .theme-btn {
140
+ border-style: dashed
141
+ }
142
+
143
+ .ver-badge {
144
+ font-family: font.mono
145
+ font-size: 11.5
146
+ color: color.muted
147
+ border: 1 solid color.line
148
+ radius: full
149
+ padding: 4 10
150
+ background: color.card/60
151
+ }
152
+
153
+ .nav-toggle { display: none }
154
+
155
+ @media (width <= 860) {
156
+ .nav-toggle {
157
+ display: grid
158
+ place: center
159
+ size: 36
160
+ radius: sm
161
+ color: color.muted
162
+ border: 1 solid color.line
163
+ transition: 130
164
+ }
165
+ .nav-toggle:hover { color: color.ink; background: color.line/60 }
166
+
167
+ .nav-links {
168
+ display: none
169
+ position: absolute
170
+ top: 64
171
+ left: 0
172
+ right: 0
173
+ column: gap 2
174
+ padding: space.3 space.5 space.4
175
+ background: color.paper
176
+ border-bottom: 1 solid color.line
177
+ box-shadow: shadow.lg
178
+ }
179
+
180
+ @signal nav == open {
181
+ .nav-links { display: flex }
182
+ }
183
+ }
184
+
185
+ // ---------------------------------------------------------------- buttons
186
+
187
+ .btn {
188
+ row: gap 8 center
189
+ padding: 11 20
190
+ radius: md
191
+ font-weight: 620
192
+ font-size: 15
193
+ transition: 160
194
+ cursor: pointer
195
+ border: 1 solid transparent
196
+ user-select: none
197
+ line-height: 1.2
198
+ }
199
+
200
+ .btn-primary {
201
+ background: color.accent
202
+ color: color.accent-ink
203
+ shadow: sm
204
+ }
205
+
206
+ .btn-primary:hover {
207
+ translate: 0 -2px
208
+ shadow: md
209
+ }
210
+
211
+ .btn-primary:active {
212
+ translate: 0 0
213
+ shadow: sm
214
+ transition: 60
215
+ }
216
+
217
+ .btn-ghost {
218
+ border-color: color.line
219
+ color: color.ink
220
+ background: color.card/40
221
+ }
222
+
223
+ .btn-ghost:hover {
224
+ background: color.line/45
225
+ translate: 0 -2px
226
+ shadow: sm
227
+ }
228
+
229
+ .btn-sm {
230
+ padding: 8 14
231
+ font-size: 13.5
232
+ radius: sm
233
+ }
234
+
235
+ // ------------------------------------------------------------------ cards
236
+
237
+ .card {
238
+ background: color.card
239
+ border: 1 solid color.line
240
+ radius: lg
241
+ transition: 180
242
+ }
243
+
244
+ .card-hover:hover {
245
+ translate: 0 -3px
246
+ shadow: lg
247
+ border-color: color.accent/35
248
+ }
249
+
250
+ .kicker {
251
+ font-family: font.mono
252
+ font-size: 12.5
253
+ font-weight: 600
254
+ color: color.accent
255
+ letter-spacing: 0.14em
256
+ text-transform: uppercase
257
+ row: gap 8 center
258
+ }
259
+
260
+ .kicker::before {
261
+ content: "//"
262
+ color: color.accent/55
263
+ }
264
+
265
+ .section {
266
+ padding: space.8 0
267
+ }
268
+
269
+ .section-title {
270
+ font-size: 30
271
+ font-weight: 750
272
+ letter-spacing: -0.025em
273
+ margin: space.2 0 space.3
274
+ }
275
+
276
+ .section-sub {
277
+ color: color.muted
278
+ font-size: 16.5
279
+ line-height: 1.65
280
+ max-width: 640
281
+ }
282
+
283
+ // -------------------------------------------------------------- code block
284
+
285
+ .code {
286
+ background: color.code-bg
287
+ color: color.code-ink
288
+ radius: md
289
+ padding: space.4
290
+ font-family: font.mono
291
+ font-size: 13
292
+ line-height: 1.75
293
+ overflow-x: auto
294
+ tab-size: 2
295
+ border: 1 solid color.code-line/60
296
+ }
297
+
298
+ .code code {
299
+ font-family: font.mono
300
+ }
301
+
302
+ .code-head {
303
+ row: gap 8 between
304
+ align: center
305
+ padding: 10 space.4
306
+ background: color.code-bg
307
+ border: 1 solid color.code-line/60
308
+ border-bottom: none
309
+ radius: md md 0 0
310
+ font-family: font.mono
311
+ font-size: 12
312
+ color: color.code-com
313
+ }
314
+
315
+ .code-head + .code {
316
+ radius: 0 0 md md
317
+ }
318
+
319
+ .code-plain {
320
+ radius: md
321
+ }
322
+
323
+ .copy-btn {
324
+ color: color.code-com
325
+ font-size: 11.5
326
+ row: gap 6 center
327
+ padding: 4 10
328
+ radius: full
329
+ border: 1 solid color.code-line
330
+ transition: 130
331
+ cursor: pointer
332
+ font-family: font.mono
333
+ }
334
+
335
+ .copy-btn:hover {
336
+ color: color.code-ink
337
+ border-color: color.code-com
338
+ }
339
+
340
+ // syntax token colors (spans produced by the site's tiny highlighter)
341
+ .tok-com { color: color.code-com; font-style: italic }
342
+ .tok-sel { color: color.code-sel }
343
+ .tok-prop { color: color.code-prop }
344
+ .tok-at { color: color.code-key; font-weight: 600 }
345
+ .tok-state { color: color.code-key }
346
+ .tok-num { color: color.code-num }
347
+ .tok-str { color: color.code-str }
348
+ .tok-val { color: color.code-ink }
349
+ .tok-punct { color: color.code-com }
350
+ .tok-use { color: color.code-num }
351
+
352
+ // ------------------------------------------------------------------ prose
353
+
354
+ .prose {
355
+ max-width: 780
356
+ }
357
+
358
+ .prose p {
359
+ margin: 0 0 space.4
360
+ line-height: 1.8
361
+ color: color.ink/88
362
+ font-size: 16
363
+ }
364
+
365
+ .prose p:last-child { margin-bottom: 0 }
366
+
367
+ .prose h2 {
368
+ font-size: 26
369
+ font-weight: 750
370
+ letter-spacing: -0.02em
371
+ margin: space.7 0 space.3
372
+ scroll-margin-top: 88
373
+ }
374
+
375
+ .prose h3 {
376
+ font-size: 19
377
+ font-weight: 700
378
+ margin: space.5 0 space.2
379
+ scroll-margin-top: 88
380
+ }
381
+
382
+ .prose ul {
383
+ margin: 0 0 space.4
384
+ column: gap 8
385
+ padding: 0
386
+ }
387
+
388
+ .prose li {
389
+ padding-left: 18
390
+ position: relative
391
+ line-height: 1.7
392
+ color: color.ink/88
393
+ font-size: 15.5
394
+ }
395
+
396
+ .prose li::before {
397
+ content: ""
398
+ position: absolute
399
+ left: 0
400
+ top: 9
401
+ size: 6
402
+ circle: 6
403
+ background: color.accent
404
+ }
405
+
406
+ .prose strong {
407
+ font-weight: 700
408
+ color: color.ink
409
+ }
410
+
411
+ .prose code {
412
+ font-family: font.mono
413
+ font-size: 0.86em
414
+ background: color.accent/10
415
+ color: color.accent
416
+ padding: 2 6
417
+ radius: 6
418
+ }
419
+
420
+ .prose .code code {
421
+ background: transparent
422
+ color: inherit
423
+ padding: 0
424
+ }
425
+
426
+ .inline-tag {
427
+ font-family: font.mono
428
+ font-size: 12
429
+ background: color.accent/10
430
+ color: color.accent
431
+ border: 1 solid color.accent/25
432
+ padding: 2 8
433
+ radius: full
434
+ }
435
+
436
+ // ----------------------------------------------------------------- footer
437
+
438
+ .footer {
439
+ margin-top: auto
440
+ border-top: 1 solid color.line
441
+ padding: space.6 0 space.5
442
+ background: color.card/40
443
+ }
444
+
445
+ .footer-inner {
446
+ row: gap space.4 between
447
+ align: center
448
+ }
449
+
450
+ .footer-note {
451
+ color: color.muted
452
+ font-size: 13.5
453
+ line-height: 1.6
454
+ }
455
+
456
+ .footer-links {
457
+ row: gap 4 center
458
+ }
459
+
460
+ // -------------------------------------------------------------- utilities
461
+
462
+ @layer utilities {
463
+ .visually-hidden {
464
+ position: absolute
465
+ width: 1
466
+ height: 1
467
+ overflow: hidden
468
+ clip-path: inset(50%)
469
+ white-space: nowrap
470
+ }
471
+ .mt-0 { margin-top: 0 }
472
+ }
public/styles/tokens.dssl ADDED
@@ -0,0 +1,104 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // =========================================================================
2
+ // tokens.dssl — DSSL's own design tokens
3
+ // The dark palette lives in a behavior and is `use`d by both the system
4
+ // preference and the manual theme signal — define once, apply twice.
5
+ // =========================================================================
6
+
7
+ tokens {
8
+ color {
9
+ paper: #faf8f4
10
+ card: #ffffff
11
+ ink: #1a1721
12
+ muted: #6d6680
13
+ line: #e9e3d9
14
+ accent: oklch(0.58 0.19 33)
15
+ accent-ink: #fff6ee
16
+ ok: #2e7d4f
17
+ err: #b3372e
18
+ code-bg: #211d2b
19
+ code-ink: #ece9f4
20
+ code-line: #37314a
21
+ code-key: #f2a86c
22
+ code-sel: #8fd0b2
23
+ code-prop: #9ec1f0
24
+ code-str: #d8b16e
25
+ code-num: #d8a0df
26
+ code-com: #857d9c
27
+ }
28
+ radius {
29
+ sm: 8
30
+ md: 14
31
+ lg: 20
32
+ xl: 28
33
+ full: 999
34
+ }
35
+ shadow {
36
+ sm: 0 1 2 color.ink/8, 0 1 3 color.ink/6
37
+ md: 0 2 4 color.ink/10, 0 4 14 color.ink/8
38
+ lg: 0 4 8 color.ink/12, 0 14 36 color.ink/12
39
+ }
40
+ }
41
+
42
+ // ---- dark palette (shared) ---------------------------------------------
43
+
44
+ behavior palette-dark {
45
+ tokens {
46
+ color {
47
+ paper: #14111c
48
+ card: #1d1927
49
+ ink: #edeaf6
50
+ muted: #a29ab8
51
+ line: #2c2739
52
+ accent: oklch(0.72 0.15 45)
53
+ accent-ink: #241304
54
+ ok: #6fc793
55
+ err: #e5766a
56
+ code-bg: #100e18
57
+ code-ink: #e4e0f0
58
+ code-line: #292438
59
+ code-key: #f2a86c
60
+ code-sel: #8fd0b2
61
+ code-prop: #9ec1f0
62
+ code-str: #d8b16e
63
+ code-num: #d8a0df
64
+ code-com: #6f6885
65
+ }
66
+ }
67
+ }
68
+
69
+ @media (prefers-color-scheme: dark) {
70
+ use palette-dark
71
+ }
72
+
73
+ @signal theme == dark {
74
+ use palette-dark
75
+ }
76
+
77
+ @signal theme == light {
78
+ use palette-light
79
+ }
80
+
81
+ behavior palette-light {
82
+ tokens {
83
+ color {
84
+ paper: #faf8f4
85
+ card: #ffffff
86
+ ink: #1a1721
87
+ muted: #6d6680
88
+ line: #e9e3d9
89
+ accent: oklch(0.58 0.19 33)
90
+ accent-ink: #fff6ee
91
+ ok: #2e7d4f
92
+ err: #b3372e
93
+ code-bg: #211d2b
94
+ code-ink: #ece9f4
95
+ code-line: #37314a
96
+ code-key: #f2a86c
97
+ code-sel: #8fd0b2
98
+ code-prop: #9ec1f0
99
+ code-str: #d8b16e
100
+ code-num: #d8a0df
101
+ code-com: #857d9c
102
+ }
103
+ }
104
+ }
scripts/dssl-smoke.mjs ADDED
@@ -0,0 +1,131 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Smoke test for the DSSL compiler (parser + resolver, no DOM)
2
+ import { parse } from '../public/dssl/parser.js';
3
+ import { resolve } from '../public/dssl/resolver.js';
4
+
5
+ const sample = `
6
+ // tokens + defaults override
7
+ tokens {
8
+ color {
9
+ ink: #16161d
10
+ paper: #faf7f2
11
+ accent: oklch(62% 0.19 35)
12
+ }
13
+ radius {
14
+ sm: 8
15
+ md: 14
16
+ }
17
+ }
18
+
19
+ behavior focus-ring {
20
+ focus-visible {
21
+ outline: 2 solid color.accent
22
+ outline-offset: 2
23
+ }
24
+ }
25
+
26
+ motion rise-in {
27
+ from {
28
+ opacity: 0
29
+ translate: 0 12
30
+ }
31
+ }
32
+
33
+ .card {
34
+ background: color.card
35
+ padding: space.4
36
+ radius: md
37
+ shadow: md
38
+ transition: 150 snap
39
+ font: 16/1.5 color.ink
40
+ use focus-ring
41
+ animate: rise-in 400 snap both
42
+
43
+ .title {
44
+ font-weight: 700
45
+ color: color.ink/80
46
+ }
47
+
48
+ hover {
49
+ translate: 0 -2px
50
+ shadow: lg
51
+
52
+ .tip { opacity: 1 }
53
+ }
54
+
55
+ ::before {
56
+ content: ""
57
+ background: color.accent
58
+ }
59
+ }
60
+
61
+ gallery {
62
+ grid: auto-fill 220
63
+ gap: space.4
64
+ }
65
+
66
+ @media (width >= 768) {
67
+ .card { padding: space.6 }
68
+ }
69
+
70
+ @signal theme == dark {
71
+ tokens { color { paper: #14121a } }
72
+ .card { background: color.card }
73
+ }
74
+
75
+ @layer utilities {
76
+ .mt-0 { margin-top: 0 }
77
+ }
78
+
79
+ pin { .hero-title { color: red } }
80
+
81
+ // deliberate errors below (should produce did-you-mean hints):
82
+ .badge {
83
+ colour: red
84
+ background: color.acnt
85
+ flex-direction: banana
86
+ }
87
+ `;
88
+
89
+ const { ast, errors: parseErrors } = parse(sample, 'smoke.dssl');
90
+ console.log('parse errors:', parseErrors.length);
91
+ for (const e of parseErrors) console.log(` PARSE line ${e.line}: ${e.message}${e.hint ? ` (${e.hint})` : ''}`);
92
+ const doc = resolve(ast, 'smoke.dssl');
93
+
94
+ console.log('--- resolve errors:', doc.errors.length);
95
+ for (const e of doc.errors) console.log(` line ${e.line}: ${e.message}${e.hint ? ` (did you mean "${e.hint}"?)` : ''}`);
96
+
97
+ console.log('--- rules:', doc.rules.length, ' shadowRules:', doc.shadowRules.length);
98
+ for (const r of doc.rules) {
99
+ const conds = [
100
+ r.media.length ? `@media[${r.media.join(' ')}]` : '',
101
+ r.signals.length ? `@signal[${r.signals.map(s => s.name + s.op + s.value).join(' ')}]` : '',
102
+ ].filter(Boolean).join(' ');
103
+ console.log(` #${r.id} L${r.layer} ord${r.order} ${conds} ${r.sel} -> ${r.decls.map(d => d.prop).join(', ')}`);
104
+ }
105
+ console.log('--- shadow rules:');
106
+ for (const s of doc.shadowRules) console.log(` ${s.sel}::${s.pseudo} -> ${s.decls.map(d => d.prop + ':' + d.value).join('; ')}`);
107
+ console.log('--- media queries:', doc.mediaQueries.join(' | '));
108
+ console.log('--- sample token values:');
109
+ for (const k of ['color.ink', 'space.4', 'radius.sm', 'font.sans']) console.log(' ', k, '=', doc.tokens[k]);
110
+
111
+ // sanity asserts
112
+ const assert = (cond, msg) => { if (!cond) { console.error('ASSERT FAIL:', msg); process.exitCode = 1; } };
113
+ assert(parseErrors.length === 0, 'no parse errors');
114
+ assert(doc.errors.length >= 3, 'expected at least 3 resolver errors (typos)');
115
+ assert(doc.errors.some(e => e.hint === 'color'), 'did-you-mean for color.acnt -> color');
116
+ assert(doc.errors.some(e => e.message.includes('banana')), 'unknown value error for flex-direction: banana');
117
+ const cardRule = doc.rules.find(r => r.sel === '.card');
118
+ assert(cardRule, '.card rule exists');
119
+ assert(cardRule.decls.some(d => d.prop === 'background' && d.value.includes('var(--dssl-color-card)')), 'background resolved to var');
120
+ assert(cardRule.decls.some(d => d.prop === 'padding' && d.value.includes('var(--dssl-space-4)')), 'space.4 resolved to var ref');
121
+ assert(cardRule.decls.some(d => d.prop === '@animate' && d.value.frames && d.value.frames.length === 1), 'animate wired with frames');
122
+ assert(doc.rules.some(r => r.sel === '.card:focus-visible'), 'behavior expanded into state selector');
123
+ assert(doc.rules.some(r => r.sel === '.card:hover .tip'), 'nested state descendant composed');
124
+ assert(doc.rules.some(r => r.sel === ':root' && r.signals.length === 1), 'signal token override rule');
125
+ assert(doc.rules.some(r => r.layer === 'utilities' && r.sel === '.mt-0'), 'layer assignment');
126
+ assert(doc.rules.some(r => r.layer === 'pin' && r.sel === '.hero-title'), 'pin layer');
127
+ assert(doc.rules.some(r => r.sel === 'gallery' && r.decls.some(d => String(d.value).includes('minmax(220px'))), 'grid sugar');
128
+ assert(doc.rules.some(r => r.sel === '.card' && r.media.length === 1), 'media rule for .card');
129
+ const shadow = doc.shadowRules.find(s => s.pseudo === 'before');
130
+ assert(shadow && shadow.decls.some(d => d.prop === 'content' && d.value === '""'), 'content autoclosed');
131
+ console.log(process.exitCode ? 'SMOKE TEST FAILED' : 'SMOKE TEST OK');
scripts/dssl-styles-check.mjs ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Compile the real site stylesheets through the DSSL compiler,
2
+ // sharing a token registry exactly like the runtime does (tokens.dssl first).
3
+ import { readFileSync, readdirSync } from 'fs';
4
+ import { parse } from '../public/dssl/parser.js';
5
+ import { resolve, DEFAULT_TOKENS } from '../public/dssl/resolver.js';
6
+
7
+ const dir = new URL('../public/styles/', import.meta.url).pathname;
8
+ const order = ['tokens.dssl', 'site.dssl', 'home.dssl', 'docs.dssl', 'playground.dssl', 'changelog.dssl'];
9
+ const files = readdirSync(dir).filter(f => f.endsWith('.dssl'));
10
+ const sorted = order.filter(f => files.includes(f)).concat(files.filter(f => !order.includes(f)));
11
+ const registry = new Map(Object.entries(DEFAULT_TOKENS));
12
+ let failed = false;
13
+
14
+ for (const f of sorted) {
15
+ const src = readFileSync(dir + f, 'utf8');
16
+ const { ast, errors: perrs } = parse(src, f);
17
+ const doc = resolve(ast, f, Object.fromEntries(registry));
18
+ for (const [k, v] of Object.entries(doc.tokens)) registry.set(k, v);
19
+ const errs = [...perrs, ...doc.errors];
20
+ console.log(`\n=== ${f}: ${doc.rules.length} rules, ${doc.shadowRules.length} shadow, ${errs.length} error(s)`);
21
+ for (const e of errs) {
22
+ failed = true;
23
+ console.log(` line ${e.line}: ${e.message}${e.hint ? ` (did you mean "${e.hint}"?)` : ''}`);
24
+ }
25
+ }
26
+
27
+ console.log(failed ? '\nSTYLESHEET CHECK FAILED' : '\nSTYLESHEET CHECK OK');
28
+ process.exitCode = failed ? 1 : 0;
src/app/api/route.ts ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ import { NextResponse } from "next/server";
2
+
3
+ export async function GET() {
4
+ return NextResponse.json({ message: "Hello, world!" });
5
+ }
src/app/globals.css ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ /* This app is styled by DSSL (public/styles/*.dssl), not by CSS.
2
+ This file is intentionally empty. */
src/app/layout.tsx ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { Metadata, Viewport } from "next";
2
+ import "./globals.css";
3
+
4
+ export const metadata: Metadata = {
5
+ title: "DSSL — Domain Specific Style Language",
6
+ description:
7
+ "DSSL is a styling language for the web: design tokens, its own cascade, loud errors and zero specificity wars. Ships as a ~20 kB engine, no build step.",
8
+ keywords: ["DSSL", "CSS alternative", "style language", "design tokens", "web"],
9
+ icons: { icon: "/favicon.svg" },
10
+ };
11
+
12
+ export const viewport: Viewport = {
13
+ width: "device-width",
14
+ initialScale: 1,
15
+ };
16
+
17
+ // The ONLY non-DSSL styling in this app: a pre-paint shim so the very first
18
+ // frame doesn't flash white before the DSSL engine boots (dark-mode respect).
19
+ const prePaint = `
20
+ html { background: #faf8f4; }
21
+ @media (prefers-color-scheme: dark) { html { background: #14111c; } }
22
+ `;
23
+
24
+ export default function RootLayout({
25
+ children,
26
+ }: Readonly<{ children: React.ReactNode }>) {
27
+ return (
28
+ <html lang="en" data-dssl-reset suppressHydrationWarning>
29
+ <head>
30
+ <style dangerouslySetInnerHTML={{ __html: prePaint }} />
31
+ </head>
32
+ <body>{children}</body>
33
+ </html>
34
+ );
35
+ }
src/app/page.tsx ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ import SiteRoot from "@/components/dssl/SiteRoot";
2
+
3
+ export default function Home() {
4
+ return <SiteRoot />;
5
+ }
src/components/dssl/CodeBlock.tsx ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+ import { highlightDssl } from "./highlight";
5
+ import { CheckIcon, CopyIcon } from "./icons";
6
+
7
+ export default function CodeBlock({
8
+ code,
9
+ file,
10
+ plain = false,
11
+ }: {
12
+ code: string;
13
+ file?: string;
14
+ plain?: boolean;
15
+ }) {
16
+ const [copied, setCopied] = useState(false);
17
+
18
+ const copy = async () => {
19
+ try {
20
+ await navigator.clipboard.writeText(code);
21
+ setCopied(true);
22
+ setTimeout(() => setCopied(false), 1400);
23
+ } catch { /* clipboard unavailable */ }
24
+ };
25
+
26
+ const body = (
27
+ <pre className={`code${plain ? " code-plain" : ""}`}>
28
+ <code dangerouslySetInnerHTML={{ __html: highlightDssl(code) }} />
29
+ </pre>
30
+ );
31
+
32
+ if (!file) return body;
33
+
34
+ return (
35
+ <div>
36
+ <div className="code-head">
37
+ <span>{file}</span>
38
+ <button type="button" className="copy-btn" onClick={copy} aria-label="Copy code">
39
+ {copied ? <CheckIcon /> : <CopyIcon />}
40
+ {copied ? "copied" : "copy"}
41
+ </button>
42
+ </div>
43
+ {body}
44
+ </div>
45
+ );
46
+ }
src/components/dssl/SiteRoot.tsx ADDED
@@ -0,0 +1,189 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { useCallback, useEffect, useState, useSyncExternalStore } from "react";
4
+ import HomeView from "./views/HomeView";
5
+ import DocsView from "./views/DocsView";
6
+ import SpecView from "./views/SpecView";
7
+ import PlaygroundView from "./views/PlaygroundView";
8
+ import ChangelogView from "./views/ChangelogView";
9
+ import { SunIcon, MoonIcon, MenuIcon } from "./icons";
10
+
11
+ export type ViewName = "home" | "docs" | "spec" | "playground" | "changelog";
12
+
13
+ const VIEWS: { name: ViewName; label: string }[] = [
14
+ { name: "home", label: "Home" },
15
+ { name: "docs", label: "Docs" },
16
+ { name: "spec", label: "Spec" },
17
+ { name: "playground", label: "Playground" },
18
+ { name: "changelog", label: "Changelog" },
19
+ ];
20
+
21
+ function viewFromHash(): ViewName {
22
+ const h = window.location.hash;
23
+ const m = h.match(/^#\/(\w+)/);
24
+ const name = m?.[1] as ViewName | undefined;
25
+ return name && VIEWS.some(v => v.name === name) ? name : "home";
26
+ }
27
+
28
+ const subscribeHash = (cb: () => void) => {
29
+ window.addEventListener("hashchange", cb);
30
+ return () => window.removeEventListener("hashchange", cb);
31
+ };
32
+
33
+ declare global {
34
+ interface Window {
35
+ DSSL?: {
36
+ load: (urls: string[], opts?: { reset?: boolean }) => Promise<unknown>;
37
+ apply: (src: string, opts?: { name?: string; scope?: string; replace?: boolean }) => DsslApplyResult;
38
+ compile?: (src: string) => DsslApplyResult;
39
+ signal: (n: string, v: string | null) => void;
40
+ getSignal?: (n: string) => string | undefined;
41
+ replay: (sel?: string) => void;
42
+ refresh: () => void;
43
+ version: string;
44
+ };
45
+ }
46
+ }
47
+
48
+ export interface DsslError {
49
+ file?: string;
50
+ line?: number;
51
+ message: string;
52
+ hint?: string;
53
+ }
54
+
55
+ export interface DsslApplyResult {
56
+ ok: boolean;
57
+ errors: DsslError[];
58
+ stats: { rules: number; shadowRules: number; motions: number; tokens: number; mediaQueries: number };
59
+ }
60
+
61
+ export default function SiteRoot() {
62
+ // the view IS the hash — derived, never stored twice
63
+ const view = useSyncExternalStore(
64
+ subscribeHash,
65
+ viewFromHash,
66
+ () => "home" as ViewName,
67
+ );
68
+ const [dsslReady, setDsslReady] = useState(false);
69
+
70
+ // ---- boot the DSSL engine (once) ----
71
+ useEffect(() => {
72
+ if (window.DSSL) {
73
+ const t = setTimeout(() => setDsslReady(true), 0);
74
+ return () => clearTimeout(t);
75
+ }
76
+ const existing = document.getElementById("dssl-engine-script");
77
+ const onReady = () => setDsslReady(true);
78
+ window.addEventListener("dssl-ready", onReady);
79
+ if (!existing) {
80
+ const s = document.createElement("script");
81
+ s.id = "dssl-engine-script";
82
+ s.type = "module";
83
+ s.src = "/dssl/boot.js";
84
+ document.head.appendChild(s);
85
+ }
86
+ return () => window.removeEventListener("dssl-ready", onReady);
87
+ }, []);
88
+
89
+ // ---- apply the stylesheets (once the engine is up) ----
90
+ useEffect(() => {
91
+ if (!dsslReady || !window.DSSL) return;
92
+ void window.DSSL.load([
93
+ "/styles/tokens.dssl",
94
+ "/styles/site.dssl",
95
+ "/styles/home.dssl",
96
+ "/styles/docs.dssl",
97
+ "/styles/playground.dssl",
98
+ "/styles/changelog.dssl",
99
+ ]);
100
+ }, [dsslReady]);
101
+
102
+ // ---- hash router: view IS the hash (bare anchors like #tokens don't re-route) ----
103
+
104
+ const go = useCallback((v: ViewName) => {
105
+ window.location.hash = v === "home" ? "" : `#/${v}`;
106
+ window.DSSL?.signal("nav", null); // close the mobile sheet
107
+ window.scrollTo({ top: 0, behavior: "instant" as ScrollBehavior });
108
+ }, []);
109
+
110
+ const cycleTheme = useCallback(() => {
111
+ if (!window.DSSL) return;
112
+ const cur = document.documentElement.getAttribute("data-theme-mode");
113
+ const next = cur === "light" ? "dark" : cur === "dark" ? null : "light";
114
+ if (next) {
115
+ document.documentElement.setAttribute("data-theme-mode", next);
116
+ window.DSSL.signal("theme", next);
117
+ } else {
118
+ document.documentElement.removeAttribute("data-theme-mode");
119
+ window.DSSL.signal("theme", null); // back to "auto" (system preference)
120
+ }
121
+ }, []);
122
+
123
+ const toggleNav = useCallback(() => {
124
+ const open = window.DSSL?.getSignal?.("nav") === "open";
125
+ window.DSSL?.signal("nav", open ? null : "open");
126
+ }, []);
127
+
128
+ return (
129
+ <div className="site">
130
+ <header className="nav">
131
+ <div className="wrap nav-inner">
132
+ <a href="#/" className="nav-logo" onClick={(e) => { e.preventDefault(); go("home"); }}>
133
+ <span className="logo-mark">{"//"}</span>
134
+ DSSL
135
+ </a>
136
+
137
+ <nav className="nav-links" aria-label="Main">
138
+ {VIEWS.map(v => (
139
+ <a
140
+ key={v.name}
141
+ href={`#/${v.name}`}
142
+ className={`nav-link nl-${v.name}`}
143
+ onClick={(e) => { e.preventDefault(); go(v.name); }}
144
+ >
145
+ {v.label}
146
+ </a>
147
+ ))}
148
+ </nav>
149
+
150
+ <div className="nav-side">
151
+ <span className="ver-badge">v0.2.0</span>
152
+ <button type="button" className="theme-btn" onClick={cycleTheme} aria-label="Toggle color theme" title="Theme: auto / light / dark">
153
+ <span className="icon-sun"><SunIcon /></span>
154
+ <span className="icon-moon"><MoonIcon /></span>
155
+ </button>
156
+ <button type="button" className="nav-toggle" onClick={toggleNav} aria-label="Toggle menu">
157
+ <MenuIcon />
158
+ </button>
159
+ </div>
160
+ </div>
161
+ </header>
162
+
163
+ <main className="main" data-view={view}>
164
+ {view === "home" && <HomeView go={go} />}
165
+ {view === "docs" && <DocsView />}
166
+ {view === "spec" && <SpecView />}
167
+ {view === "playground" && <PlaygroundView />}
168
+ {view === "changelog" && <ChangelogView />}
169
+ </main>
170
+
171
+ <footer className="footer">
172
+ <div className="wrap footer-inner">
173
+ <div className="footer-note">
174
+ <strong>DSSL</strong> — domain specific style language. Built v0.1 → v0.2 while
175
+ building this site; every feature it needed, it grew. This page runs on its own
176
+ engine, no stylesheet attached.
177
+ </div>
178
+ <nav className="footer-links" aria-label="Footer">
179
+ {VIEWS.map(v => (
180
+ <a key={v.name} href={`#/${v.name}`} className="nav-link" onClick={(e) => { e.preventDefault(); go(v.name); }}>
181
+ {v.label}
182
+ </a>
183
+ ))}
184
+ </nav>
185
+ </div>
186
+ </footer>
187
+ </div>
188
+ );
189
+ }
src/components/dssl/highlight.ts ADDED
@@ -0,0 +1,87 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // A ~40-line DSSL syntax highlighter for code samples.
2
+ // Emits <span class="tok-*"> markup; colors come from DSSL itself (site.dssl).
3
+
4
+ const escapeHtml = (s: string) =>
5
+ s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
6
+
7
+ const STATES = new Set([
8
+ "hover", "focus", "focus-visible", "active", "disabled", "checked", "empty",
9
+ "target", "invalid", "valid", "required", "open", "placeholder-shown",
10
+ ]);
11
+
12
+ export function highlightDssl(src: string): string {
13
+ const lines = src.split("\n");
14
+ const out: string[] = [];
15
+
16
+ for (const line of lines) {
17
+ let html = "";
18
+ let i = 0;
19
+ const n = line.length;
20
+
21
+ while (i < n) {
22
+ const rest = line.slice(i);
23
+
24
+ // comments
25
+ const com = rest.match(/^\/\/.*/);
26
+ if (com) { html += `<span class="tok-com">${escapeHtml(com[0])}</span>`; break; }
27
+
28
+ const ws = rest.match(/^\s+/);
29
+ if (ws) { html += ws[0]; i += ws[0].length; continue; }
30
+
31
+ // strings
32
+ const str = rest.match(/^"[^"]*"|^'[^']*'/);
33
+ if (str) { html += `<span class="tok-str">${escapeHtml(str[0])}</span>`; i += str[0].length; continue; }
34
+
35
+ // at-rules
36
+ const at = rest.match(/^@[a-z-]+/);
37
+ if (at) { html += `<span class="tok-at">${escapeHtml(at[0])}</span>`; i += at[0].length; continue; }
38
+
39
+ // numbers with optional unit
40
+ const num = rest.match(/^-?\d*\.?\d+(?:[a-z%]+)?/);
41
+ if (num) { html += `<span class="tok-num">${escapeHtml(num[0])}</span>`; i += num[0].length; continue; }
42
+
43
+ // use keyword
44
+ const use = rest.match(/^use\s+[\w-]+/);
45
+ if (use) { html += `<span class="tok-use">${escapeHtml(use[0])}</span>`; i += use[0].length; continue; }
46
+
47
+ // identifiers (may contain dots for token refs)
48
+ const id = rest.match(/^--?[A-Za-z_][\w.\\-]*/);
49
+ if (id) {
50
+ const word = id[0];
51
+ const after = line.slice(i + word.length).trimStart();
52
+ const isBlock = after.startsWith("{");
53
+ const isState = STATES.has(word);
54
+ const isHeader = isBlock && !word.includes(".") && !word.startsWith("-") && !word.startsWith("&");
55
+ if (isState) html += `<span class="tok-state">${escapeHtml(word)}</span>`;
56
+ else if (word.startsWith("--")) html += `<span class="tok-num">${escapeHtml(word)}</span>`;
57
+ else if (isHeader) html += `<span class="tok-sel">${escapeHtml(word)}</span>`;
58
+ else {
59
+ // declaration property vs bare value identifier (like `pin` or a selector fragment)
60
+ const colonNext = /^[\w.\\-]*\s*:/.test(rest) && !after.startsWith(":");
61
+ const looksProp = /^[a-z][a-z0-9-]*$/i.test(word) && /^\s*:/.test(line.slice(i + word.length));
62
+ html += `<span class="${looksProp || colonNext && looksProp ? "tok-prop" : isBlock ? "tok-sel" : "tok-val"}">${escapeHtml(word)}</span>`;
63
+ }
64
+ i += word.length;
65
+ continue;
66
+ }
67
+
68
+ // pseudo classes in selectors
69
+ const pseudo = rest.match(/^::?[a-z-]+/);
70
+ if (pseudo) { html += `<span class="tok-state">${escapeHtml(pseudo[0])}</span>`; i += pseudo[0].length; continue; }
71
+
72
+ // punctuation
73
+ const punct = rest.match(/^[{}:,()[\]&*>~+.#/-]+/);
74
+ if (punct) {
75
+ const cls = punct[0].startsWith(".") || punct[0].startsWith("#") ? "tok-sel" : "tok-punct";
76
+ html += `<span class="${cls}">${escapeHtml(punct[0])}</span>`;
77
+ i += punct[0].length;
78
+ continue;
79
+ }
80
+
81
+ html += escapeHtml(line[i]);
82
+ i++;
83
+ }
84
+ out.push(html);
85
+ }
86
+ return out.join("\n");
87
+ }
src/components/dssl/icons.tsx ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Tiny inline icon set (no icon library needed — DSSL styles the rest)
2
+ export function SunIcon() {
3
+ return (
4
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" aria-hidden="true">
5
+ <circle cx="12" cy="12" r="4" />
6
+ <path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4" />
7
+ </svg>
8
+ );
9
+ }
10
+
11
+ export function MoonIcon() {
12
+ return (
13
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
14
+ <path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8Z" />
15
+ </svg>
16
+ );
17
+ }
18
+
19
+ export function MenuIcon() {
20
+ return (
21
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" aria-hidden="true">
22
+ <path d="M4 7h16M4 12h16M4 17h16" />
23
+ </svg>
24
+ );
25
+ }
26
+
27
+ export function ArrowIcon() {
28
+ return (
29
+ <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
30
+ <path d="M5 12h14M13 6l6 6-6 6" />
31
+ </svg>
32
+ );
33
+ }
34
+
35
+ export function CopyIcon() {
36
+ return (
37
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
38
+ <rect x="9" y="9" width="12" height="12" rx="2" />
39
+ <path d="M5 15V5a2 2 0 0 1 2-2h10" />
40
+ </svg>
41
+ );
42
+ }
43
+
44
+ export function CheckIcon() {
45
+ return (
46
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
47
+ <path d="M20 6 9 17l-5-5" />
48
+ </svg>
49
+ );
50
+ }
51
+
52
+ export function PlayIcon() {
53
+ return (
54
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
55
+ <path d="M8 5.5v13l11-6.5-11-6.5Z" />
56
+ </svg>
57
+ );
58
+ }
src/components/dssl/views/ChangelogView.tsx ADDED
@@ -0,0 +1,178 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import CodeBlock from "../CodeBlock";
4
+
5
+ const ENTRIES: {
6
+ version: string;
7
+ date: string;
8
+ title: string;
9
+ why?: string;
10
+ body: string;
11
+ code?: string;
12
+ kind?: "spec" | "vote" | "add";
13
+ }[] = [
14
+ {
15
+ version: "v0.1",
16
+ date: "phase 1 · draft",
17
+ title: "“First Light” draft published",
18
+ kind: "spec",
19
+ body:
20
+ "The full language spec goes up for review: thesis (CSS talks to the engine, DSSL talks to the design), tokens, one-cascade-sentence, semantic properties, behaviors, motion, signals, loud errors — and a deliberate list of what v0.1 would NOT do.",
21
+ },
22
+ {
23
+ version: "v0.1 → v0.2",
24
+ date: "phase 1 · ratification",
25
+ title: "Seven votes, seven decisions",
26
+ kind: "vote",
27
+ body:
28
+ "Braces over indentation. Own engine over compile-to-CSS (the bold pick). Bare numbers are px. Cascade: layers + order, no specificity, no !important. Moderate vocabulary. CSS-shaped conditions (@media, nested state blocks — plus @signal as the JS bridge). Docs site shape. The spec was amended to match.",
29
+ },
30
+ {
31
+ version: "v0.2.0",
32
+ date: "build phase",
33
+ title: "Engine skeleton",
34
+ kind: "add",
35
+ body:
36
+ "parser.js → resolver.js → engine.js → index.js: the four-module runtime. Braces grammar with one-liners, flattening, behaviors, motion, scoped engines, event-delegated states, MutationObserver re-matching. ~1,000 lines, zero dependencies.",
37
+ },
38
+ {
39
+ version: "v0.2.1",
40
+ date: "build phase",
41
+ title: "Color-aware functions",
42
+ why: "tokens.dssl — the palette",
43
+ body:
44
+ "The first stylesheet broke the compiler: oklch(62% 0.19 35) had its chroma and hue gaining phantom px. Bare numbers inside color functions (oklch, rgb, color-mix, cubic-bezier, steps…) must stay bare; calc-family keeps px.",
45
+ code: `// before: oklch(62% 0.19px 35px) ← nonsense
46
+ accent: oklch(62% 0.19 35) // now: untouched`,
47
+ },
48
+ {
49
+ version: "v0.2.2",
50
+ date: "build phase",
51
+ title: "Color-space & gradient vocabulary",
52
+ why: "site.dssl — chrome",
53
+ body:
54
+ "Real stylesheets say “in oklab”, “to bottom right”, “flex-end”, “thin”, “collapse”, “vertical”. The keyword pass-list grew from ~200 to ~260 words. Loud errors need a generous dictionary.",
55
+ },
56
+ {
57
+ version: "v0.2.3",
58
+ date: "build phase",
59
+ title: "circle: sugar",
60
+ why: "theme toggle & feature icons",
61
+ body:
62
+ "size: 36 + border-radius: 50% appeared four times before anyone admitted it was one concept. circle: N sets width, height and a 50% radius.",
63
+ code: `.theme-btn {
64
+ circle: 36
65
+ place: center
66
+ }`,
67
+ },
68
+ {
69
+ version: "v0.2.4",
70
+ date: "build phase",
71
+ title: "Multi-atom radius presets",
72
+ why: "code block headers",
73
+ body:
74
+ "radius: md md 0 0 — a corner-aware radius that mixes token presets with raw values, per atom. The single-atom shortcut had to become a real parser.",
75
+ code: `.code-head + .code {
76
+ radius: 0 0 md md
77
+ }`,
78
+ },
79
+ {
80
+ version: "v0.2.5",
81
+ date: "build phase",
82
+ title: "Multi-line values & selector lists",
83
+ why: "hero gradients, nav active states",
84
+ body:
85
+ "The parser was strictly line-oriented, which died the moment a radial-gradient wanted two lines or the nav wanted a five-selector list. Logical chunks now continue while parens are open or the line ends with a top-level comma/colon.",
86
+ code: `background:
87
+ radial-gradient(ellipse at 30% 0%, color.accent/14,
88
+ transparent 55%),
89
+ color.card
90
+
91
+ [data-view="home"] .nl-home,
92
+ [data-view="docs"] .nl-docs {
93
+ color: color.accent
94
+ }`,
95
+ },
96
+ {
97
+ version: "v0.2.6",
98
+ date: "build phase",
99
+ title: "align: property",
100
+ why: "nav & footer centering",
101
+ body:
102
+ "align: center → align-items: center. The one-word grid/flex alignment the vocabulary was missing.",
103
+ },
104
+ {
105
+ version: "v0.2.7",
106
+ date: "build phase",
107
+ title: "Top-level pseudo-elements",
108
+ why: "site.dssl — ::selection",
109
+ body:
110
+ "::selection at the top level now means “every element” (::selection with a * base) instead of an error.",
111
+ },
112
+ {
113
+ version: "v0.2.8",
114
+ date: "build phase",
115
+ title: "Shared token registry",
116
+ why: "every view referencing the palette",
117
+ body:
118
+ "Each bundle resolved in isolation, so color.code-bg — defined in tokens.dssl — was invisible to site.dssl. Bundles now resolve against a registry that folds in every previous bundle's tokens, in load order.",
119
+ },
120
+ {
121
+ version: "v0.2.9",
122
+ date: "build phase",
123
+ title: "Scoped stylesheets & scoped tokens",
124
+ why: "the playground",
125
+ body:
126
+ "The playground applies user code with DSSL.apply(src, { scope: '#pg-canvas', name: 'pg', replace: true }). Scoped engines keep stylesheets sandboxed, and :root rules map onto the scope element so presets can define their own tokens without touching the page.",
127
+ code: `DSSL.apply(src, {
128
+ scope: '#pg-canvas',
129
+ name: 'pg',
130
+ replace: true,
131
+ })`,
132
+ },
133
+ {
134
+ version: "v0.2.11",
135
+ date: "browser testing",
136
+ title: "Stale-style cleanup",
137
+ why: "the theme toggle",
138
+ body:
139
+ "Cycling auto → light → dark → auto left the theme icons invisible: computeAll only enumerated elements whose rules CURRENTLY matched, so elements a rule had stopped matching were never cleaned up. The engine now keeps a registry of every element it ever styled (with mutation-driven garbage collection) and re-evaluates all of them.",
140
+ },
141
+ {
142
+ version: "v0.2.12",
143
+ date: "browser testing",
144
+ title: "Registry as a base bundle",
145
+ why: "dark mode itself",
146
+ body:
147
+ "Every stylesheet bundle re-emitted the full token map unconditionally, so site.dssl's light tokens out-cascaded tokens.dssl's @signal dark palette — dark mode silently stayed light. Bundles now emit only the tokens they actually declare, and the shared registry is installed once as a base bundle that sorts below every author rule.",
148
+ },
149
+ ];
150
+
151
+ export default function ChangelogView() {
152
+ return (
153
+ <div className="wrap">
154
+ <div className="page-head">
155
+ <div className="kicker">built with itself</div>
156
+ <h1 className="page-title">Changelog</h1>
157
+ <p className="page-sub">
158
+ The deal was: build the language, then build its website, and every time the
159
+ website needs something the language lacks — add it, honestly. This page is
160
+ that list.
161
+ </p>
162
+ </div>
163
+
164
+ <div className="tl">
165
+ {ENTRIES.map((e) => (
166
+ <div key={e.version + e.title} className={`tl-item ${e.kind === "add" ? "tl-item-add" : ""}`}>
167
+ <span className="tl-date">{e.date}</span>
168
+ <span className="tl-version">{e.version}</span>
169
+ {e.why && <span className="tl-why">needed by: {e.why}</span>}
170
+ <h3>{e.title}</h3>
171
+ <p>{e.body}</p>
172
+ {e.code && <CodeBlock plain code={e.code} />}
173
+ </div>
174
+ ))}
175
+ </div>
176
+ </div>
177
+ );
178
+ }
src/components/dssl/views/DocsView.tsx ADDED
@@ -0,0 +1,400 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import CodeBlock from "../CodeBlock";
4
+ import { PlayIcon } from "../icons";
5
+
6
+ const SIDE = [
7
+ { head: "language", id: "start", label: "Getting started" },
8
+ { head: null, id: "syntax", label: "Syntax" },
9
+ { head: null, id: "tokens", label: "Tokens" },
10
+ { head: null, id: "selectors", label: "Selectors & nesting" },
11
+ { head: null, id: "states", label: "States" },
12
+ { head: null, id: "world", label: "@media · @container · @signal" },
13
+ { head: null, id: "cascade", label: "Cascade & layers" },
14
+ { head: "properties", id: "semantic", label: "Semantic properties" },
15
+ { head: null, id: "behaviors", label: "Behaviors" },
16
+ { head: null, id: "motion", label: "Motion" },
17
+ { head: null, id: "errors", label: "Errors" },
18
+ { head: "runtime", id: "api", label: "JS API" },
19
+ ];
20
+
21
+ export default function DocsView() {
22
+ const toggleDocSignal = (hot: boolean) => {
23
+ window.DSSL?.signal("docdemo", hot ? "hot" : null);
24
+ };
25
+
26
+ return (
27
+ <div className="wrap">
28
+ <div className="page-head">
29
+ <div className="kicker">language tour</div>
30
+ <h1 className="page-title">The DSSL docs</h1>
31
+ <p className="page-sub">
32
+ Everything in v0.2, with live examples — the boxes you can hover, focus and
33
+ toggle are styled by exactly the rules shown next to them.
34
+ </p>
35
+ </div>
36
+
37
+ <div className="docs-layout">
38
+ {/* ------------------------------------------------ sidebar */}
39
+ <aside className="docs-side">
40
+ {SIDE.map((s, i) => (
41
+ <div key={s.id}>
42
+ {s.head && <div className="side-head">{s.head}</div>}
43
+ <a href={`#${s.id}`}>{s.label}</a>
44
+ </div>
45
+ ))}
46
+ </aside>
47
+
48
+ {/* ------------------------------------------------ body */}
49
+ <div className="docs-body">
50
+ <section className="doc-block" id="start">
51
+ <h2 className="section-title">Getting started</h2>
52
+ <p className="doc-lead">
53
+ DSSL ships as plain ES modules. No bundler, no build step, no dependencies —
54
+ drop two files on any HTML page (any vintage), point a link tag at your
55
+ stylesheet, and the engine does the rest client-side.
56
+ </p>
57
+ <CodeBlock file="index.html" code={`<link rel="stylesheet/dssl" href="app.dssl">
58
+ <script type="module" src="dssl/boot.js"></script>
59
+
60
+ <!-- works with React, Vue, Svelte, anything that emits DOM:
61
+ the engine re-matches automatically on mutations -->`} />
62
+ <p className="doc-lead">
63
+ Styles live in <code>.dssl</code> files. The engine fetches them, parses,
64
+ resolves tokens, and applies the winning declarations straight onto elements.
65
+ </p>
66
+ </section>
67
+
68
+ <section className="doc-block" id="syntax">
69
+ <h2 className="section-title">Syntax</h2>
70
+ <p className="doc-lead">
71
+ Braces, one declaration per line, no required semicolons. Newlines end
72
+ declarations; <code>;</code> is optional sugar for packing declarations.
73
+ Comments are <code>{"// line"}</code> or block comments. Values span lines
74
+ when parens are open or the line ends with a comma or colon.
75
+ </p>
76
+ <CodeBlock file="syntax.dssl" code={`.card {
77
+ background: color.card // no semicolon needed
78
+ padding: space.4; radius: md // optional semicolons
79
+
80
+ .title {
81
+ font: 16/1.4 color.ink // size / line-height / color
82
+ }
83
+ }
84
+
85
+ .one-liner { padding: 8; shadow: sm }
86
+
87
+ background: // values can continue
88
+ linear-gradient(135deg,
89
+ color.accent, color.accent/45)`} />
90
+ </section>
91
+
92
+ <section className="doc-block" id="tokens">
93
+ <h2 className="section-title">Tokens</h2>
94
+ <p className="doc-lead">
95
+ Design tokens are the variables. They compile to custom properties on
96
+ <code> :root</code> (inspectable in DevTools), and any bare identifier in a
97
+ value resolves against them — misspell one and you get an error, not silence.
98
+ A slash adds alpha via <code>color-mix</code>.
99
+ </p>
100
+ <CodeBlock file="tokens.dssl" code={`tokens {
101
+ color {
102
+ ink: #16161d
103
+ accent: oklch(62% 0.19 35)
104
+ }
105
+ radius { md: 14 }
106
+ shadow { md: 0 2 4 ink/10, 0 4 12 ink/8 }
107
+ }
108
+
109
+ .card {
110
+ background: color.card
111
+ color: color.ink/85 // alpha modifier
112
+ radius: md
113
+ shadow: md
114
+ }`} />
115
+ <div className="demo-box">
116
+ <div className="tok-demo">
117
+ <button type="button" className="tok-dot">ink</button>
118
+ <button type="button" className="tok-dot">muted</button>
119
+ <button type="button" className="tok-dot">accent</button>
120
+ <button type="button" className="tok-dot">ok</button>
121
+ <button type="button" className="tok-dot">err</button>
122
+ </div>
123
+ <p className="doc-lead fine-print">
124
+ These dots resolve <code>color.*</code> tokens — flip the theme in the nav
125
+ and watch them re-evaluate.
126
+ </p>
127
+ <div className="scale-bar" aria-label="Spacing scale">
128
+ {"space.1 space.2 space.3 space.4 space.5 space.6".split(" ").map(s => (
129
+ <div key={s} className="scale-step" title={s} style={{ height: (parseInt(s.slice(-1)) * 8 + 8) }} />
130
+ ))}
131
+ </div>
132
+ </div>
133
+ <p className="tbl-note">
134
+ Built-ins ship with sensible defaults: <code>space.1–.8</code>,{" "}
135
+ <code>radius.sm–full</code>, <code>shadow.sm–xl</code>, <code>font.sans/mono</code>.
136
+ Override any of them from your own <code>tokens</code> block.
137
+ </p>
138
+ </section>
139
+
140
+ <section className="doc-block" id="selectors">
141
+ <h2 className="section-title">Selectors & nesting</h2>
142
+ <p className="doc-lead">
143
+ Every CSS selector works — elements, classes, ids, attributes, combinators,
144
+ structural pseudo-classes. Nesting is native: a nested selector is a
145
+ descendant; <code>&amp;</code> substitutes the parent explicitly.
146
+ </p>
147
+ <CodeBlock file="selectors.dssl" code={`.gallery {
148
+ gap: space.4
149
+
150
+ .tile { shadow: sm } // .gallery .tile
151
+ &.featured { border-color: color.accent } // .gallery.featured
152
+ > .tile:nth-child(2n) { translate: 0 -2 }
153
+
154
+ hover { // states nest too (next section)
155
+ .tile { shadow: lg }
156
+ }
157
+ }`} />
158
+ </section>
159
+
160
+ <section className="doc-block" id="states">
161
+ <h2 className="section-title">States</h2>
162
+ <p className="doc-lead">
163
+ An element&apos;s own pseudo-states are plain nested blocks —{" "}
164
+ <code>hover</code>, <code>focus</code>, <code>focus-visible</code>,{" "}
165
+ <code>active</code>, <code>disabled</code>, <code>checked</code>… The engine
166
+ listens to the events and re-evaluates live. Rules inside a state block can
167
+ target descendants (tooltips, dropdowns) or the element itself.
168
+ </p>
169
+ <CodeBlock file="states.dssl" code={`.doc-card {
170
+ transition: 200
171
+
172
+ hover {
173
+ translate: 0 -4
174
+ shadow: lg
175
+ }
176
+
177
+ focus-visible {
178
+ outline: 2 solid color.accent
179
+ outline-offset: 2
180
+ }
181
+
182
+ active { translate: 0 -1 }
183
+ }`} />
184
+ <div className="demo-box">
185
+ <div className="doc-card">
186
+ <span className="doc-card-title">Hover me</span>
187
+ <span className="doc-card-sub">
188
+ This card lifts on hover — the engine matched <code>:hover</code> itself.
189
+ </span>
190
+ </div>
191
+ <button type="button" className="doc-btn">Tab to me — focus-visible ring</button>
192
+ </div>
193
+ </section>
194
+
195
+ <section className="doc-block" id="world">
196
+ <h2 className="section-title">@media · @container · @signal</h2>
197
+ <p className="doc-lead">
198
+ The world around the element is one shape: CSS-shaped conditions.{" "}
199
+ <code>@media</code> for viewport and preferences (bare numbers mean px),{" "}
200
+ <code>@container</code> for container queries, and <code>@signal</code> —
201
+ the JS bridge. Conditions compose by nesting.
202
+ </p>
203
+ <CodeBlock file="world.dssl" code={`.panel {
204
+ padding: space.3
205
+
206
+ @media (width >= 768) {
207
+ padding: space.6
208
+ }
209
+
210
+ @container (min-width: 480px) {
211
+ grid: 2
212
+ }
213
+
214
+ @media (prefers-color-scheme: dark) {
215
+ background: color.ink
216
+ }
217
+
218
+ @signal theme == dark { // JS: DSSL.signal('theme', 'dark')
219
+ background: color.ink
220
+ }
221
+
222
+ @media (width <= 860) {
223
+ @signal nav == open { // composed: narrow AND menu open
224
+ display: block
225
+ }
226
+ }
227
+ }`} />
228
+ <div className="demo-box">
229
+ <div className="signal-box">@signal docdemo == hot — cold</div>
230
+ <div className="tok-demo">
231
+ <button type="button" className="doc-btn" onClick={() => toggleDocSignal(true)}>heat it</button>
232
+ <button type="button" className="doc-btn" onClick={() => toggleDocSignal(false)}>cool it</button>
233
+ </div>
234
+ </div>
235
+ </section>
236
+
237
+ <section className="doc-block" id="cascade">
238
+ <h2 className="section-title">Cascade & layers</h2>
239
+ <p className="doc-lead">
240
+ The whole cascade in one sentence: <b>later wins, layers trump order.</b>{" "}
241
+ No specificity arithmetic, no <code>!important</code>. Rules live in{" "}
242
+ <code>tokens &lt; base &lt; components &lt; utilities &lt; pin</code>; yours
243
+ default to <code>components</code>. The <code>pin</code> block is the single
244
+ sanctioned escape hatch.
245
+ </p>
246
+ <CodeBlock file="cascade.dssl" code={`#hero .title { color: color.ink }
247
+
248
+ .legacy .panel .title { color: color.paper }
249
+ // ↑ WINS in DSSL (it is later) — CSS would score the #id higher
250
+
251
+ @layer utilities {
252
+ .mt-0 { margin-top: 0 }
253
+ }
254
+
255
+ .wonky-badge {
256
+ pin: color color.accent-ink // beats everything DSSL emitted
257
+ }`} />
258
+ <div className="note">
259
+ <b>Why no specificity?</b> Because it never survives contact with a real
260
+ codebase. Source order is readable, predictable, and refactorable — and when
261
+ you truly need to win, <code>pin</code> says so explicitly instead of
262
+ leaking <code>!important</code> into every stylesheet.
263
+ </div>
264
+ </section>
265
+
266
+ <section className="doc-block" id="semantic">
267
+ <h2 className="section-title">Semantic properties</h2>
268
+ <p className="doc-lead">
269
+ All standard CSS properties pass through untouched. On top of them, DSSL
270
+ speaks a small design vocabulary — properties that expand to the machinery
271
+ they mean. Bare numbers are pixels; unitless where CSS wants them.
272
+ </p>
273
+ <table className="tbl">
274
+ <thead>
275
+ <tr><th>Property</th><th>Example</th><th>Expands to</th></tr>
276
+ </thead>
277
+ <tbody>
278
+ <tr><td><code>size</code></td><td><code>size: 40</code> · <code>size: 100% 40</code></td><td>width + height (one or two values)</td></tr>
279
+ <tr><td><code>row</code> / <code>column</code></td><td><code>row: gap 8 center wrap</code></td><td>flexbox + only the words you name (between, reverse, inline…)</td></tr>
280
+ <tr><td><code>grid</code></td><td><code>grid: 3</code> · <code>grid: auto-fill 220</code></td><td>repeat(3, 1fr) · repeat(auto-fill, minmax(220px, 1fr))</td></tr>
281
+ <tr><td><code>stack</code></td><td><code>stack: center</code></td><td>overlay grid; children share cell 1/1</td></tr>
282
+ <tr><td><code>place</code> / <code>align</code></td><td><code>place: center</code></td><td>place-items/content · align-items</td></tr>
283
+ <tr><td><code>cover</code></td><td><code>cover: fixed</code></td><td>position + inset: 0</td></tr>
284
+ <tr><td><code>sticky</code></td><td><code>sticky: 0</code></td><td>position: sticky; top: 0px</td></tr>
285
+ <tr><td><code>font</code></td><td><code>font: 16/1.4 ink</code></td><td>font-size / line-height / color</td></tr>
286
+ <tr><td><code>fluid</code></td><td><code>fluid: 16, 24</code></td><td>clamp() interpolating 16→24px across 360→1200px</td></tr>
287
+ <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>
288
+ <tr><td><code>circle</code></td><td><code>circle: 36</code></td><td>width + height + border-radius: 50%</td></tr>
289
+ <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>
290
+ <tr><td><code>truncate</code></td><td><code>truncate</code></td><td>ellipsis overflow (overflow hidden, nowrap)</td></tr>
291
+ <tr><td><code>z</code></td><td><code>z: 50</code></td><td>z-index</td></tr>
292
+ <tr><td><code>transition</code></td><td><code>transition: 150 snap</code></td><td>all 150ms cubic-bezier(0.2, 0.9, 0.3, 1)</td></tr>
293
+ <tr><td><code>animate</code></td><td><code>animate: pop 400 snap</code></td><td>Web Animations API run of a motion block</td></tr>
294
+ </tbody>
295
+ </table>
296
+ <p className="tbl-note">Named easings: <code>snap</code>, <code>smooth</code>, <code>bounce</code> + the standard set.</p>
297
+ </section>
298
+
299
+ <section className="doc-block" id="behaviors">
300
+ <h2 className="section-title">Behaviors</h2>
301
+ <p className="doc-lead">
302
+ A behavior is a named chunk of DSSL — declarations, states, even nested
303
+ rules — spliced into any rule with <code>use</code>. Perfect for focus
304
+ rings, chip styles, reset-ish bundles. No parameters in v0.2; compose by
305
+ nesting behaviors.
306
+ </p>
307
+ <CodeBlock file="behaviors.dssl" code={`behavior focus-ring {
308
+ focus-visible {
309
+ outline: 2 solid color.accent
310
+ outline-offset: 2
311
+ }
312
+ }
313
+
314
+ button { use focus-ring }
315
+ .card { use focus-ring }
316
+
317
+ // the dark palette on this very site is a behavior,
318
+ // reused by @media and @signal — define once, apply twice`} />
319
+ </section>
320
+
321
+ <section className="doc-block" id="motion">
322
+ <h2 className="section-title">Motion</h2>
323
+ <p className="doc-lead">
324
+ Declare a <code>motion</code> with keyframe blocks (<code>from</code>,{" "}
325
+ <code>to</code>, <code>at 40%</code>) — the missing end interpolates from the
326
+ element&apos;s natural state. Apply it with <code>animate</code>. The engine
327
+ runs it through the Web Animations API, and every animation silently
328
+ disables under reduced-motion unless the motion is{" "}
329
+ <code>essential: true</code>.
330
+ </p>
331
+ <CodeBlock file="motion.dssl" code={`motion pop {
332
+ from {
333
+ opacity: 0
334
+ translate: 0 16
335
+ scale: 0.9
336
+ }
337
+ }
338
+
339
+ .motion-box {
340
+ animate: pop 500 snap // name · duration · easing · fill
341
+ }`} />
342
+ <div className="demo-box">
343
+ <div className="motion-box">motion pop — replay me</div>
344
+ <button type="button" className="doc-btn" onClick={() => window.DSSL?.replay(".motion-box")}>
345
+ <span className="tok-demo"><PlayIcon /> DSSL.replay(&apos;.motion-box&apos;)</span>
346
+ </button>
347
+ </div>
348
+ </section>
349
+
350
+ <section className="doc-block" id="errors">
351
+ <h2 className="section-title">Errors that talk back</h2>
352
+ <p className="doc-lead">
353
+ CSS fails silently; DSSL refuses to compile and tells you where and what it
354
+ expected, with a did-you-mean built from edit distance over every known
355
+ property, token and motion. The playground shows this live.
356
+ </p>
357
+ <CodeBlock file="typo.dssl" code={`.badge {
358
+ colour: red // ✗ unknown property "colour"
359
+ // → did you mean "color"?
360
+
361
+ background: color.acnt // ✗ unknown value "color.acnt"
362
+ // → did you mean "color.accent"?
363
+
364
+ flex-direction: banana // ✗ unknown value "banana"
365
+ }`} />
366
+ <div className="note">
367
+ <b>Escape hatches are explicit.</b> Custom properties (<code>--*</code>) pass
368
+ through unvalidated, and unknown identifiers inside <code>calc()</code> and
369
+ friends are your business — the loud checks protect the design vocabulary,
370
+ not the pressure valve.
371
+ </div>
372
+ </section>
373
+
374
+ <section className="doc-block" id="api">
375
+ <h2 className="section-title">JS API</h2>
376
+ <p className="doc-lead">
377
+ The engine exposes a small, synchronous surface. Signals are the bridge:
378
+ they re-evaluate every <code>@signal</code> rule and mirror onto{" "}
379
+ <code>data-dssl-*</code> attributes for DevTools visibility.
380
+ </p>
381
+ <table className="tbl">
382
+ <thead>
383
+ <tr><th>Call</th><th>What it does</th></tr>
384
+ </thead>
385
+ <tbody>
386
+ <tr><td><code>DSSL.load(urls, opts?)</code></td><td>fetch + apply .dssl files in order (shared token registry)</td></tr>
387
+ <tr><td><code>DSSL.apply(src, opts?)</code></td><td>apply a source string; <code>{"{ scope, name, replace }"}</code> for scoped stylesheets</td></tr>
388
+ <tr><td><code>DSSL.compile(src)</code></td><td>parse + resolve without touching the DOM — powers the playground</td></tr>
389
+ <tr><td><code>DSSL.signal(name, value)</code></td><td>set/clear a signal; @signal rules re-evaluate instantly</td></tr>
390
+ <tr><td><code>DSSL.replay(sel?)</code></td><td>re-run animations on matching elements</td></tr>
391
+ <tr><td><code>DSSL.refresh()</code></td><td>full re-compute (rarely needed — the engine watches the DOM)</td></tr>
392
+ <tr><td><code>DSSL.errors()</code></td><td>structured errors from the last apply</td></tr>
393
+ </tbody>
394
+ </table>
395
+ </section>
396
+ </div>
397
+ </div>
398
+ </div>
399
+ );
400
+ }
src/components/dssl/views/HomeView.tsx ADDED
@@ -0,0 +1,331 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+ import CodeBlock from "../CodeBlock";
5
+ import { highlightDssl } from "../highlight";
6
+ import { ArrowIcon, PlayIcon } from "../icons";
7
+ import type { ViewName } from "../SiteRoot";
8
+
9
+ const DEMOS = {
10
+ tokens: `.demo-card {
11
+ background: color.card
12
+ radius: md
13
+ shadow: md
14
+
15
+ .demo-chip {
16
+ color: color.accent
17
+ background: color.accent/12
18
+ }
19
+
20
+ .demo-swatch {
21
+ background: linear-gradient(
22
+ 135deg, color.accent, color.accent/45)
23
+ }
24
+ }`,
25
+ states: `.demo-card {
26
+ transition: 200
27
+ }
28
+
29
+ .demo-card:hover {
30
+ translate: 0 -5px
31
+ shadow: lg
32
+ border-color: color.accent/40
33
+
34
+ .demo-swatch {
35
+ background: linear-gradient(
36
+ 135deg, color.accent/60, color.ink/80)
37
+ }
38
+ }`,
39
+ motion: `motion rise-in {
40
+ from {
41
+ opacity: 0
42
+ translate: 0 12
43
+ }
44
+ }
45
+
46
+ .demo-card {
47
+ animate: rise-in 500 snap both
48
+ }`,
49
+ };
50
+
51
+ type DemoKey = keyof typeof DEMOS;
52
+
53
+ export default function HomeView({ go }: { go: (v: ViewName) => void }) {
54
+ const [demo, setDemo] = useState<DemoKey>("tokens");
55
+ const [signalOn, setSignalOn] = useState(false);
56
+
57
+ const toggleSignal = () => {
58
+ const next = !signalOn;
59
+ setSignalOn(next);
60
+ window.DSSL?.signal("demo", next ? "on" : null);
61
+ };
62
+
63
+ const replayMotion = () => window.DSSL?.replay(".demo-motion");
64
+
65
+ return (
66
+ <>
67
+ {/* ------------------------------------------------ hero */}
68
+ <section className="hero">
69
+ <div className="hero-glow" aria-hidden="true" />
70
+ <div className="hero-glow-2" aria-hidden="true" />
71
+ <div className="wrap hero-grid">
72
+ <div className="hero-copy">
73
+ <div className="kicker">domain specific style language</div>
74
+ <h1 className="hero-title">
75
+ CSS talks to the <em>engine</em>.
76
+ <br />
77
+ DSSL talks to the <em>design</em>.
78
+ </h1>
79
+ <p className="hero-sub">
80
+ A styling language for the web that pairs with any HTML and any JavaScript.
81
+ Its own cascade — no specificity math, no <code>!important</code> — design
82
+ tokens as the vocabulary, states you can read, motion in four words, and
83
+ errors that actually speak up. Zero dependencies, no build step.
84
+ </p>
85
+ <div className="hero-ctas">
86
+ <button type="button" className="btn btn-primary" onClick={() => go("docs")}>
87
+ Read the docs <ArrowIcon />
88
+ </button>
89
+ <button type="button" className="btn btn-ghost" onClick={() => go("playground")}>
90
+ <PlayIcon /> Try the playground
91
+ </button>
92
+ </div>
93
+ <div className="hero-meta" aria-label="Engine facts">
94
+ <span>~20 kB engine</span>
95
+ <span>zero dependencies</span>
96
+ <span>any HTML, any JS</span>
97
+ </div>
98
+ </div>
99
+
100
+ {/* live demo stage — the tabs swap real DSSL, the card below is styled by it */}
101
+ <div className="demo" aria-label="Live DSSL demo">
102
+ <div className="demo-tabs">
103
+ {(Object.keys(DEMOS) as DemoKey[]).map(k => (
104
+ <button
105
+ key={k}
106
+ type="button"
107
+ className="demo-tab"
108
+ aria-pressed={demo === k}
109
+ onClick={() => setDemo(k)}
110
+ >
111
+ {k}
112
+ </button>
113
+ ))}
114
+ </div>
115
+ <div className="demo-stage">
116
+ <pre className="code demo-code">
117
+ <code dangerouslySetInnerHTML={{ __html: highlightDssl(DEMOS[demo]) }} />
118
+ </pre>
119
+ <div className="demo-canvas">
120
+ <div className="demo-card">
121
+ <div className="demo-swatch" />
122
+ <h4>Styled by DSSL</h4>
123
+ <p>Hover me — states, tokens and motion are all handled by the engine.</p>
124
+ <span className="demo-chip">live</span>
125
+ </div>
126
+ {demo === "motion" && (
127
+ <button type="button" className="btn btn-ghost btn-sm" onClick={() => window.DSSL?.replay(".demo-card")}>
128
+ <PlayIcon /> replay rise-in
129
+ </button>
130
+ )}
131
+ </div>
132
+ </div>
133
+ </div>
134
+ </div>
135
+ </section>
136
+
137
+ {/* ------------------------------------------------ pain points */}
138
+ <section className="section">
139
+ <div className="wrap">
140
+ <div className="kicker">why another language</div>
141
+ <h2 className="section-title">Four fights CSS makes you pick. DSSL refuses all of them.</h2>
142
+ <div className="pain-grid sect-gap">
143
+ <div className="card card-hover pain-card">
144
+ <span className="pain-label">CSS · specificity wars</span>
145
+ <h3>Cascade by arithmetic</h3>
146
+ <p>
147
+ <code>#id</code> beats <code>.class</code> beats <code>tag</code>, then{" "}
148
+ <code>!important</code> beats everyone. In DSSL the rule is one sentence:
149
+ later wins, inside layers. That is the whole cascade.
150
+ </p>
151
+ <span className="pain-cure">DSSL · layers + source order</span>
152
+ </div>
153
+ <div className="card card-hover pain-card">
154
+ <span className="pain-label">CSS · silent failures</span>
155
+ <h3>Typo? Shipped anyway.</h3>
156
+ <p>
157
+ Misspell <code>background</code> or a variable and CSS quietly does nothing.
158
+ DSSL refuses to compile: file, line, and a did-you-mean that usually nails it.
159
+ </p>
160
+ <span className="pain-cure">DSSL · loud errors</span>
161
+ </div>
162
+ <div className="card card-hover pain-card">
163
+ <span className="pain-label">CSS · keyframe ceremony</span>
164
+ <h3>Motion needs a paragraph</h3>
165
+ <p>
166
+ <code>@keyframes</code> plus an <code>animation</code> shorthand with five
167
+ comma-separated values. DSSL: declare a motion once, then{" "}
168
+ <code>animate: rise-in 400 snap</code>.
169
+ </p>
170
+ <span className="pain-cure">DSSL · motion blocks</span>
171
+ </div>
172
+ <div className="card card-hover pain-card">
173
+ <span className="pain-label">CSS · invisible theming</span>
174
+ <h3>JS ↔ styles is a hack</h3>
175
+ <p>
176
+ Theming from JavaScript means poking data-attributes or inline styles by hand.
177
+ DSSL has a first-class bridge: <code>DSSL.signal(&apos;theme&apos;, &apos;dark&apos;)</code>{" "}
178
+ re-styles the page, inspectably.
179
+ </p>
180
+ <span className="pain-cure">DSSL · signals</span>
181
+ </div>
182
+ </div>
183
+ </div>
184
+ </section>
185
+
186
+ {/* ------------------------------------------------ side-by-side */}
187
+ <section className="section">
188
+ <div className="wrap">
189
+ <div className="kicker">same card, two languages</div>
190
+ <h2 className="section-title">The vocabulary is the difference</h2>
191
+ <div className="compare sect-gap">
192
+ <div className="compare-col">
193
+ <span className="compare-tag compare-tag-css">styles.css — 14 lines of machinery</span>
194
+ <CodeBlock plain code={`.card {
195
+ background-color: var(--card);
196
+ border-radius: 14px;
197
+ padding: 16px;
198
+ box-shadow: 0 2px 4px rgb(0 0 0 / .1),
199
+ 0 4px 14px rgb(0 0 0 / .08);
200
+ transition: all 160ms cubic-bezier(.2,.9,.3,1);
201
+ }
202
+ .card:hover {
203
+ transform: translateY(-4px);
204
+ box-shadow: 0 4px 8px rgb(0 0 0 / .12),
205
+ 0 14px 36px rgb(0 0 0 / .12);
206
+ }
207
+ .card:hover .chip { background: var(--accent-soft); }`} />
208
+ </div>
209
+ <div className="compare-col">
210
+ <span className="compare-tag compare-tag-dssl">card.dssl — 8 lines of design</span>
211
+ <CodeBlock plain code={`.card {
212
+ background: color.card
213
+ radius: md
214
+ padding: space.4
215
+ shadow: md
216
+ transition: 160 snap
217
+
218
+ hover {
219
+ translate: 0 -4
220
+ shadow: lg
221
+ .chip { background: color.accent/12 }
222
+ }
223
+ }`} />
224
+ </div>
225
+ </div>
226
+ </div>
227
+ </section>
228
+
229
+ {/* ------------------------------------------------ features */}
230
+ <section className="section">
231
+ <div className="wrap">
232
+ <div className="kicker">the engine</div>
233
+ <h2 className="section-title">No stylesheet is attached to this page</h2>
234
+ <p className="section-sub">
235
+ Every rule below is implemented by the DSSL engine — it matches selectors
236
+ against the live DOM itself, runs its own cascade, and drives motion through
237
+ the Web Animations API. Only pseudo-elements fall back to generated CSS.
238
+ </p>
239
+ <div className="feature-grid sect-gap">
240
+ <div className="card card-hover feature">
241
+ <span className="feature-icon">{"{}"}</span>
242
+ <h3>Its own cascade</h3>
243
+ <p>Later wins. Layers when you care. No specificity score, no !important — ever.</p>
244
+ </div>
245
+ <div className="card card-hover feature">
246
+ <span className="feature-icon">◆</span>
247
+ <h3>Tokens as vocabulary</h3>
248
+ <p>color.ink, space.4, radius.md — references compile to inspectable custom properties.</p>
249
+ </div>
250
+ <div className="card card-hover feature">
251
+ <span className="feature-icon">→</span>
252
+ <h3>Readable states</h3>
253
+ <p>hover, focus-visible, active as blocks. Nested rules under a state just work.</p>
254
+ </div>
255
+ <div className="card card-hover feature">
256
+ <span className="feature-icon">≋</span>
257
+ <h3>One-word conditions</h3>
258
+ <p>@media, @container and @signal in the same shape. Bare numbers mean pixels.</p>
259
+ </div>
260
+ <div className="card card-hover feature">
261
+ <span className="feature-icon">✦</span>
262
+ <h3>Motion in four words</h3>
263
+ <p>Declare a motion, then animate: rise-in 400 snap both. Reduced-motion is respected by default.</p>
264
+ </div>
265
+ <div className="card card-hover feature">
266
+ <span className="feature-icon">§</span>
267
+ <h3>Behaviors</h3>
268
+ <p>Name a pattern once — focus rings, chips — and use it anywhere with one line.</p>
269
+ </div>
270
+ <div className="card card-hover feature">
271
+ <span className="feature-icon">!</span>
272
+ <h3>Loud errors</h3>
273
+ <p>Unknown property, unknown token, bad value — each fails with file, line and a suggestion.</p>
274
+ </div>
275
+ <div className="card card-hover feature">
276
+ <span className="feature-icon">⇄</span>
277
+ <h3>JS bridge</h3>
278
+ <p>dssl.signal() re-styles live, dssl.replay() re-runs motion, dssl.compile() powers tools.</p>
279
+ </div>
280
+ </div>
281
+ </div>
282
+ </section>
283
+
284
+ {/* ------------------------------------------------ bridge demo */}
285
+ <section className="section">
286
+ <div className="wrap">
287
+ <div className="kicker">javascript, meet styles</div>
288
+ <h2 className="section-title">One call restyles the page</h2>
289
+ <div className="compare sect-gap" style={{ alignItems: "center" }}>
290
+ <div className="compare-col">
291
+ <CodeBlock file="theme-toggle.js" code={`// flip a signal — DSSL re-evaluates
292
+ // every @signal rule in the page
293
+
294
+ DSSL.signal('demo', 'on') // on
295
+ DSSL.signal('demo', null) // off`} />
296
+ </div> <div className="compare-col demo-canvas" style={{ maxWidth: 480 }}>
297
+ <button type="button" className="btn btn-primary" onClick={toggleSignal}>
298
+ {signalOn ? "signal: demo == on" : "signal: demo == off"} — click
299
+ </button>
300
+ <div className="demo-signal-card">
301
+ <strong>@signal demo == on</strong>
302
+ <span>This card is styled by a signal-conditional rule. Flip the signal and the engine recomputes it — inspect the html element to see the mirror attribute.</span>
303
+ </div>
304
+ </div>
305
+ </div>
306
+ </div>
307
+ </section>
308
+
309
+ {/* ------------------------------------------------ cta */}
310
+ <section className="section">
311
+ <div className="wrap">
312
+ <div className="cta-band">
313
+ <h2>Write styles that read like design.</h2>
314
+ <p>
315
+ The full language tour takes about ten minutes. Or paste straight into the
316
+ playground and break things — the errors will keep you company.
317
+ </p>
318
+ <div className="hero-ctas">
319
+ <button type="button" className="btn btn-primary" onClick={() => go("docs")}>
320
+ Language tour <ArrowIcon />
321
+ </button>
322
+ <button type="button" className="btn btn-ghost" onClick={() => go("spec")}>
323
+ Read the spec
324
+ </button>
325
+ </div>
326
+ </div>
327
+ </div>
328
+ </section>
329
+ </>
330
+ );
331
+ }
src/components/dssl/views/PlaygroundView.tsx ADDED
@@ -0,0 +1,387 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { useCallback, useEffect, useRef, useState } from "react";
4
+ import type { DsslError, DsslApplyResult } from "../SiteRoot";
5
+
6
+ const PRESETS: Record<string, { label: string; src: string }> = {
7
+ cards: {
8
+ label: "Cards",
9
+ src: `// style the canvas — bare numbers are px,
10
+ // bare identifiers are tokens
11
+ tokens {
12
+ color {
13
+ hotspot: oklch(0.62 0.17 55)
14
+ }
15
+ }
16
+
17
+ .pc-board {
18
+ grid: auto-fill 190
19
+ gap: space.4
20
+ width: 100%
21
+ }
22
+
23
+ .pc-card {
24
+ background: color.card
25
+ border: 1 solid color.line
26
+ radius: md
27
+ padding: space.4
28
+ shadow: sm
29
+ column: gap 8
30
+ transition: 180
31
+
32
+ hover {
33
+ translate: 0 -4
34
+ shadow: lg
35
+ border-color: color.hotspot/45
36
+
37
+ .pc-badge {
38
+ background: color.hotspot
39
+ color: color.accent-ink
40
+ }
41
+ }
42
+ }
43
+
44
+ .pc-badge {
45
+ align-self: start
46
+ font-family: font.mono
47
+ font-size: 11
48
+ background: color.accent/12
49
+ color: color.accent
50
+ padding: 3 9
51
+ radius: full
52
+ transition: 180
53
+ }
54
+
55
+ .pc-title { font-weight: 700; font-size: 15 }
56
+ .pc-text { color: color.muted; font-size: 13; line-height: 1.55 }
57
+
58
+ .pc-btn {
59
+ padding: 8 14
60
+ radius: sm
61
+ background: color.ink
62
+ color: color.paper
63
+ font-size: 13
64
+ font-weight: 600
65
+
66
+ hover { translate: 0 -2 }
67
+ active { translate: 0 0 }
68
+ }`,
69
+ },
70
+ tokens: {
71
+ label: "Tokens",
72
+ src: `// tokens compile to custom properties on the
73
+ // canvas element — try flipping these
74
+ tokens {
75
+ color {
76
+ pop: oklch(0.65 0.19 300)
77
+ calm: oklch(0.68 0.13 190)
78
+ }
79
+ radius { md: 18 }
80
+ }
81
+
82
+ .pc-board {
83
+ grid: 3
84
+ gap: space.4
85
+ }
86
+
87
+ .pc-card {
88
+ background: color.card
89
+ border-top: 4 solid color.pop
90
+ radius: md
91
+ padding: space.4
92
+ column: gap 8
93
+ shadow: sm
94
+ }
95
+
96
+ .pc-card:nth-child(2) {
97
+ border-top-color: color.calm
98
+ }
99
+
100
+ .pc-card:nth-child(3) {
101
+ border-top-color: color.pop/55
102
+ }
103
+
104
+ .pc-badge {
105
+ background: color.pop/12
106
+ color: color.pop
107
+ padding: 3 9
108
+ radius: full
109
+ font-family: font.mono
110
+ font-size: 11
111
+ align-self: start
112
+ }
113
+
114
+ .pc-title { font-weight: 700; font-size: 15 }
115
+ .pc-text { color: color.muted; font-size: 13 }`,
116
+ },
117
+ motion: {
118
+ label: "Motion",
119
+ src: `// declare once, animate anywhere.
120
+ // reduced-motion users: this all politely
121
+ // does nothing unless marked essential
122
+ motion float {
123
+ from {
124
+ opacity: 0
125
+ translate: 0 18
126
+ scale: 0.94
127
+ }
128
+ }
129
+
130
+ motion shimmer {
131
+ at 0 { translate: 0 0 }
132
+ at 50 { translate: 0 -10 }
133
+ at 100 { translate: 0 0 }
134
+ }
135
+
136
+ .pc-board {
137
+ grid: 3
138
+ gap: space.4
139
+ }
140
+
141
+ .pc-card {
142
+ background: color.card
143
+ border: 1 solid color.line
144
+ radius: md
145
+ padding: space.4
146
+ column: gap 8
147
+ animate: float 600 snap both
148
+
149
+ hover { shadow: lg; translate: 0 -4 }
150
+ }
151
+
152
+ .pc-card:nth-child(2) { animate: float 600 snap 150 both }
153
+ .pc-card:nth-child(3) { animate: float 600 snap 300 both }
154
+
155
+ .pc-badge {
156
+ background: color.accent/12
157
+ color: color.accent
158
+ padding: 3 9
159
+ radius: full
160
+ font-family: font.mono
161
+ font-size: 11
162
+ align-self: start
163
+ }
164
+
165
+ .pc-title { font-weight: 700; font-size: 15 }
166
+ .pc-text { color: color.muted; font-size: 13 }
167
+
168
+ // press "replay" above to re-run the motion`,
169
+ },
170
+ signals: {
171
+ label: "Signals",
172
+ src: `// press the button in the canvas —
173
+ // DSSL.signal('pgdemo', 'hot') re-styles live
174
+ .pc-board {
175
+ grid: 3
176
+ gap: space.4
177
+ }
178
+
179
+ .pc-card {
180
+ background: color.card
181
+ border: 1 solid color.line
182
+ radius: md
183
+ padding: space.4
184
+ column: gap 8
185
+ transition: 250
186
+ }
187
+
188
+ @signal pgdemo == hot {
189
+ .pc-card {
190
+ background: color.accent
191
+ border-color: color.accent
192
+ color: color.accent-ink
193
+ translate: 0 -3
194
+ shadow: lg
195
+
196
+ .pc-text { color: color.accent-ink/80 }
197
+ .pc-badge {
198
+ background: color.accent-ink/20
199
+ color: color.accent-ink
200
+ }
201
+ }
202
+ }
203
+
204
+ .pc-badge {
205
+ background: color.accent/12
206
+ color: color.accent
207
+ padding: 3 9
208
+ radius: full
209
+ font-family: font.mono
210
+ font-size: 11
211
+ align-self: start
212
+ }
213
+
214
+ .pc-title { font-weight: 700; font-size: 15 }
215
+ .pc-text { color: color.muted; font-size: 13 }`,
216
+ },
217
+ errors: {
218
+ label: "Errors",
219
+ src: `// this preset fails on purpose —
220
+ // read the panel below the editor
221
+ .pc-card {
222
+ backgroud: color.card // typo
223
+ radius: md
224
+ padding: space.4
225
+ colr: color.ink // typo
226
+ shadow: color.acnt // unknown token
227
+ }
228
+
229
+ .pc-title {
230
+ font-weight: 700
231
+ bananify: 12 // unknown property
232
+ }`,
233
+ },
234
+ };
235
+
236
+ const SAMPLE_CARDS = [
237
+ { badge: "one", title: "Styled live", text: "Everything on the left is applied to this canvas by the DSSL engine — scoped, so the rest of the site is untouched." },
238
+ { badge: "two", title: "Tokens & states", text: "Use color.*, space.*, radius.* — or define your own. Hover states work right here." },
239
+ { badge: "three", title: "Loud errors", text: "Misspell a token and the panel below explains what it thinks you meant." },
240
+ ];
241
+
242
+ export default function PlaygroundView() {
243
+ const editorRef = useRef<HTMLTextAreaElement>(null);
244
+ const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
245
+ const [errors, setErrors] = useState<DsslError[]>([]);
246
+ const [stats, setStats] = useState<DsslApplyResult["stats"] | null>(null);
247
+ const [engineReady, setEngineReady] = useState(false);
248
+ const [preset, setPreset] = useState("cards");
249
+ const [hot, setHot] = useState(false);
250
+
251
+ const run = useCallback((src: string) => {
252
+ if (!window.DSSL) return;
253
+ const res = window.DSSL.apply(src, { scope: "#pg-canvas", name: "pg", replace: true });
254
+ setErrors(res.errors);
255
+ setStats(res.stats);
256
+ }, []);
257
+
258
+ // wait for the engine, then run the default preset (scoped!)
259
+ useEffect(() => {
260
+ if (window.DSSL) {
261
+ const t = setTimeout(() => setEngineReady(true), 0);
262
+ return () => clearTimeout(t);
263
+ }
264
+ const onReady = () => setEngineReady(true);
265
+ window.addEventListener("dssl-ready", onReady);
266
+ return () => window.removeEventListener("dssl-ready", onReady);
267
+ }, []);
268
+
269
+ useEffect(() => {
270
+ if (!engineReady) return;
271
+ const t = setTimeout(() => run(PRESETS.cards.src), 0);
272
+ return () => clearTimeout(t);
273
+ }, [engineReady, run]);
274
+
275
+ const onEdit = () => {
276
+ if (timerRef.current) clearTimeout(timerRef.current);
277
+ timerRef.current = setTimeout(() => {
278
+ if (editorRef.current) run(editorRef.current.value);
279
+ }, 450);
280
+ };
281
+
282
+ const loadPreset = (key: string) => {
283
+ setPreset(key);
284
+ if (editorRef.current) {
285
+ editorRef.current.value = PRESETS[key].src;
286
+ run(PRESETS[key].src);
287
+ }
288
+ };
289
+
290
+ const toggleHot = () => {
291
+ const next = !hot;
292
+ setHot(next);
293
+ window.DSSL?.signal("pgdemo", next ? "hot" : null);
294
+ };
295
+
296
+ const ok = errors.length === 0;
297
+
298
+ return (
299
+ <div className="wrap">
300
+ <div className="page-head">
301
+ <div className="kicker">try it</div>
302
+ <h1 className="page-title">Playground</h1>
303
+ <p className="page-sub">
304
+ Real engine, real errors, zero setup. Your stylesheet is applied{" "}
305
+ <b>scoped</b> to the canvas — nothing leaks to the rest of the page.
306
+ </p>
307
+ </div>
308
+
309
+ <div className="pg-layout">
310
+ {/* ------------------------------------ editor */}
311
+ <div className="pg-pane">
312
+ <div className="pg-head">
313
+ <span>canvas.dssl</span>
314
+ <div className="pg-head-tools">
315
+ <select
316
+ className="pg-select"
317
+ value={preset}
318
+ aria-label="Load preset"
319
+ onChange={(e) => loadPreset(e.target.value)}
320
+ >
321
+ {Object.entries(PRESETS).map(([k, p]) => (
322
+ <option key={k} value={k}>{p.label}</option>
323
+ ))}
324
+ </select>
325
+ </div>
326
+ </div>
327
+ <textarea
328
+ ref={editorRef}
329
+ className="pg-editor"
330
+ defaultValue={PRESETS.cards.src}
331
+ onInput={onEdit}
332
+ spellCheck={false}
333
+ aria-label="DSSL source editor"
334
+ />
335
+ <div className={`pg-status ${ok ? "pg-status-ok" : "pg-status-err"}`} role="status">
336
+ {ok
337
+ ? `ok — ${stats?.rules ?? 0} rules, ${stats?.tokens ?? 0} tokens, ${stats?.motions ?? 0} motions`
338
+ : `${errors.length} error${errors.length === 1 ? "" : "s"} — fix and keep going`}
339
+ </div>
340
+ {!ok && (
341
+ <div className="pg-errors" aria-live="polite">
342
+ {errors.map((e, i) => (
343
+ <div key={i} className="pg-error">
344
+ <span>line {e.line ?? "?"} — {e.message}</span>
345
+ {e.hint && <span className="pg-error-hint">did you mean <em>“{e.hint}”</em>?</span>}
346
+ </div>
347
+ ))}
348
+ </div>
349
+ )}
350
+ </div>
351
+
352
+ {/* ------------------------------------ canvas */}
353
+ <div className="pg-pane">
354
+ <div className="pg-head">
355
+ <span>#pg-canvas — scoped</span>
356
+ <div className="pg-head-tools">
357
+ {preset === "signals" && (
358
+ <button type="button" className="doc-btn btn-sm" onClick={toggleHot}>
359
+ {hot ? "signal: hot" : "signal: cold"}
360
+ </button>
361
+ )}
362
+ {(preset === "motion" || preset === "cards") && (
363
+ <button type="button" className="doc-btn btn-sm" onClick={() => window.DSSL?.replay(".pc-card")}>
364
+ replay
365
+ </button>
366
+ )}
367
+ </div>
368
+ </div>
369
+ <div className="pg-canvas-wrap">
370
+ <div className="pg-canvas" id="pg-canvas">
371
+ <p className="pg-canvas-title">canvas — your dssl styles this</p>
372
+ <div className="pc-board">
373
+ {SAMPLE_CARDS.map(c => (
374
+ <article className="pc-card" key={c.badge}>
375
+ <span className="pc-badge">{c.badge}</span>
376
+ <h4 className="pc-title">{c.title}</h4>
377
+ <p className="pc-text">{c.text}</p>
378
+ </article>
379
+ ))}
380
+ </div>
381
+ </div>
382
+ </div>
383
+ </div>
384
+ </div>
385
+ </div>
386
+ );
387
+ }
src/components/dssl/views/SpecView.tsx ADDED
@@ -0,0 +1,204 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import CodeBlock from "../CodeBlock";
4
+
5
+ export default function SpecView() {
6
+ return (
7
+ <div className="wrap">
8
+ <div className="page-head">
9
+ <div className="kicker">normative</div>
10
+ <h1 className="page-title">DSSL v0.2 — “First Light”, ratified</h1>
11
+ <p className="page-sub">
12
+ The v0.1 draft was put to a seven-point vote. This document is the result —
13
+ every decision below is implemented by the engine serving this page.
14
+ </p>
15
+ </div>
16
+
17
+ <div className="docs-layout">
18
+ <aside className="docs-side">
19
+ {[
20
+ ["decision", "Decision log"],
21
+ ["architecture", "Architecture"],
22
+ ["s-syntax", "Syntax"],
23
+ ["s-tokens", "Tokens"],
24
+ ["s-cascade", "Cascade"],
25
+ ["s-conditions", "Conditions"],
26
+ ["s-motion", "Motion"],
27
+ ["s-signals", "Signals"],
28
+ ["s-errors", "Errors"],
29
+ ["s-limits", "Deliberate limits"],
30
+ ["s-version", "Versioning"],
31
+ ].map(([id, label]) => (
32
+ <a key={id} href={`#${id}`}>{label}</a>
33
+ ))}
34
+ </aside>
35
+
36
+ <div className="docs-body prose">
37
+ <section className="doc-block" id="decision">
38
+ <h2 className="section-title">Decision log</h2>
39
+ <p className="doc-lead">
40
+ Seven questions, seven answers — the draft spec was amended to match.
41
+ </p>
42
+ <table className="tbl">
43
+ <thead>
44
+ <tr><th>#</th><th>Question</th><th>Decision</th><th>Consequence</th></tr>
45
+ </thead>
46
+ <tbody>
47
+ <tr><td>1</td><td>Syntax style</td><td><b>Braces</b></td><td>C-shaped blocks; semicolons optional, newline-terminated declarations</td></tr>
48
+ <tr><td>2</td><td>Styling engine</td><td><b>Own engine</b></td><td>No CSS output for regular rules — selectors matched against the live DOM, declarations applied directly; pseudo-elements excepted (shadow CSS)</td></tr>
49
+ <tr><td>3</td><td>Bare numbers</td><td><b>px</b></td><td><code>padding: 16</code> is 16px; unitless preserved where CSS demands it</td></tr>
50
+ <tr><td>4</td><td>Cascade</td><td><b>Layers + order</b></td><td>No specificity, no !important; five layers; pin as the only escape hatch</td></tr>
51
+ <tr><td>5</td><td>Vocabulary</td><td><b>Moderate sugar</b></td><td>All CSS names valid + a small semantic set (row, grid, fluid, …)</td></tr>
52
+ <tr><td>6</td><td>Conditions</td><td><b>CSS shapes</b></td><td>Nested state blocks (hover, focus…) + @media / @container; @signal added as the JS bridge</td></tr>
53
+ <tr><td>7</td><td>Website shape</td><td><b>Docs site</b></td><td>Multi-view docs with live examples, playground, spec and changelog</td></tr>
54
+ </tbody>
55
+ </table>
56
+ </section>
57
+
58
+ <section className="doc-block" id="architecture">
59
+ <h2 className="section-title">1 · Architecture</h2>
60
+ <p>
61
+ DSSL is a styling <b>language plus engine</b>. Source files use the{" "}
62
+ <code>.dssl</code> extension. The engine — four dependency-free ES modules
63
+ (parser, resolver, engine, API) — fetches stylesheets, parses them, resolves
64
+ tokens, and computes styles for elements in the live DOM. It survives DOM
65
+ mutations via a MutationObserver, re-evaluates states on interaction events,
66
+ and re-runs media conditions through <code>matchMedia</code> listeners.
67
+ </p>
68
+ <p>
69
+ Regular rules produce <b>no CSS</b>: winning declarations are applied to
70
+ elements directly. Pseudo-elements, which cannot be expressed inline, are
71
+ the single exception — they compile to a shadow stylesheet that references
72
+ the same tokens. A tiny opt-in reset (<code>data-dssl-reset</code>) is
73
+ likewise generated CSS, zero-specificity via <code>:where()</code>.
74
+ </p>
75
+ <CodeBlock file="pipeline.txt" code={`.dssl source
76
+ └─ parser.js braces syntax → node tree (loud, line-numbered errors)
77
+ └─ resolver.js tokens · semantic expansion · behaviors · flatten
78
+ └─ engine.js match → cascade (layer, order) → element.style
79
+ states via events · @media via matchMedia
80
+ motion via Web Animations API
81
+ └─ shadow CSS ::before/::after/::selection + reset only`} />
82
+ </section>
83
+
84
+ <section className="doc-block" id="s-syntax">
85
+ <h2 className="section-title">2 · Syntax</h2>
86
+ <ul>
87
+ <li>Blocks open with <code>header {"{"}</code> and close with <code>{"}"}</code>; one-liners allowed.</li>
88
+ <li>Declarations are <code>prop: value</code>, newline- or <code>;</code>-terminated. Valueless semantics (<code>truncate</code>) and <code>use name</code> need no colon.</li>
89
+ <li>Comments: <code>{"//"}</code> and block comments. Strings are <code>&quot;…&quot;</code> or <code>&apos;…&apos;</code>.</li>
90
+ <li>Values continue across lines while parens are open, or the line ends with a top-level comma or colon.</li>
91
+ <li>Bare numbers are px; <code>16/1.4</code> size-over-lineheight in <code>font</code>; <code>token/40</code> is alpha.</li>
92
+ </ul>
93
+ </section>
94
+
95
+ <section className="doc-block" id="s-tokens">
96
+ <h2 className="section-title">3 · Tokens</h2>
97
+ <p>
98
+ <code>tokens {"{ }"}</code> blocks define named values in nested groups
99
+ (<code>color.ink</code>, <code>space.4</code>). They compile to{" "}
100
+ <code>--dssl-*</code> custom properties on <code>:root</code>. Reference
101
+ resolution is strict: an identifier that matches no token and no known
102
+ keyword is a compile error with a suggestion. Tokens may be defined
103
+ conditionally (inside <code>@media</code>/<code>@signal</code>) — that is
104
+ how theming works. Bundles share one token registry in load order.
105
+ </p>
106
+ </section>
107
+
108
+ <section className="doc-block" id="s-cascade">
109
+ <h2 className="section-title">4 · Cascade</h2>
110
+ <p>
111
+ For every element, the engine collects all structurally-matching rules
112
+ whose conditions pass, sorts them by <b>(layer, source order)</b>, and
113
+ applies declarations in sequence — later declarations overwrite earlier
114
+ ones on the element. Layers, lowest first:
115
+ </p>
116
+ <CodeBlock plain code={`tokens < base < components < utilities < pin`} />
117
+ <p>
118
+ Author rules default to <code>components</code>. <code>@layer</code>{" "}
119
+ re-assigns a subtree. <code>pin</code> blocks apply with{" "}
120
+ <code>!important</code>-grade priority as the single sanctioned escape
121
+ hatch. Selectors keep their native meaning — ids and pseudo-classes select
122
+ exactly as in CSS — they simply carry <b>no weight</b> in conflict
123
+ resolution.
124
+ </p>
125
+ </section>
126
+
127
+ <section className="doc-block" id="s-conditions">
128
+ <h2 className="section-title">5 · Conditions</h2>
129
+ <ul>
130
+ <li><b>Element states</b> — nested blocks named for the pseudo-class (<code>hover</code>, <code>focus-visible</code>, <code>active</code>, <code>disabled</code>, <code>checked</code>, …). The engine wires the events; rules re-evaluate live via <code>matches()</code>.</li>
131
+ <li><b>@media</b> — any media query; bare numbers become px (<code>(width &gt;= 768)</code>).</li>
132
+ <li><b>@container</b> — evaluated against the nearest ancestor with <code>container-type</code> set, tracked by the engine with a ResizeObserver.</li>
133
+ <li><b>@signal</b> — <code>name</code> (truthy), <code>name == value</code>, <code>name != value</code>; driven from JS.</li>
134
+ <li>Conditions compose by nesting; siblings AND together. Raw pseudo-classes in selector text (<code>.tab:hover</code>) also work.</li>
135
+ </ul>
136
+ </section>
137
+
138
+ <section className="doc-block" id="s-motion">
139
+ <h2 className="section-title">6 · Motion</h2>
140
+ <p>
141
+ <code>motion name {"{ }"}</code> declares keyframes (<code>from</code>,{" "}
142
+ <code>to</code>, <code>at N%</code>); a single terminal block interpolates
143
+ from the element&apos;s natural state. <code>animate: name duration easing fill</code>{" "}
144
+ runs it via the Web Animations API — the engine deduplicates re-runs so
145
+ recomputes never restart animations; <code>DSSL.replay()</code> does that
146
+ on demand. Reduced-motion disables animations unless{" "}
147
+ <code>essential: true</code>.
148
+ </p>
149
+ </section>
150
+
151
+ <section className="doc-block" id="s-signals">
152
+ <h2 className="section-title">7 · Signals</h2>
153
+ <p>
154
+ <code>DSSL.signal(name, value)</code> sets a named value in the engine and
155
+ mirrors it onto <code>&lt;html&gt;</code> as{" "}
156
+ <code>data-dssl-signal-&lt;name&gt;</code> — inspectable, framework-free
157
+ theming and UI state. Clearing with <code>null</code> returns conditions to
158
+ their system-preference defaults.
159
+ </p>
160
+ </section>
161
+
162
+ <section className="doc-block" id="s-errors">
163
+ <h2 className="section-title">8 · Errors</h2>
164
+ <p>
165
+ Unknown properties, unknown tokens, unknown motions, malformed conditions
166
+ and bad header shapes are <b>compile errors</b> carrying file, line,
167
+ message and — where the edit distance is close — a suggestion. Error
168
+ objects are structured (<code>DSSL.errors()</code>) so tools like the
169
+ playground can render them. Uncaught selector evaluation failures are
170
+ contained per-rule and reported once.
171
+ </p>
172
+ </section>
173
+
174
+ <section className="doc-block" id="s-limits">
175
+ <h2 className="section-title">9 · Deliberate limits (v0.2)</h2>
176
+ <ul>
177
+ <li>No <code>@import</code>/module graph — bundles merge in load order.</li>
178
+ <li>Behaviors take no parameters; no loops or user functions.</li>
179
+ <li>Comma selector lists do not combine with nested state blocks (bind states per-rule instead).</li>
180
+ <li>Pseudo-element rules accept declarations only.</li>
181
+ <li>No minification or build step — the engine is the pipeline.</li>
182
+ </ul>
183
+ <div className="note">
184
+ <b>The growth protocol:</b> anything the site needed that the language
185
+ lacked was added to the language, logged in the changelog with the reason.
186
+ That is how v0.1 became v0.2.
187
+ </div>
188
+ </section>
189
+
190
+ <section className="doc-block" id="s-version">
191
+ <h2 className="section-title">10 · Versioning</h2>
192
+ <p>
193
+ DSSL follows semver for the language surface: new semantic properties,
194
+ condition kinds and API calls are minor versions; breaking grammar changes
195
+ are major. The engine reports <code>DSSL.version</code> — currently{" "}
196
+ <b>0.2.0</b>, “First Light”, ratified and grown during the construction of
197
+ the site you are reading.
198
+ </p>
199
+ </section>
200
+ </div>
201
+ </div>
202
+ </div>
203
+ );
204
+ }
src/components/ui/accordion.tsx ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import * as AccordionPrimitive from "@radix-ui/react-accordion"
5
+ import { ChevronDownIcon } from "lucide-react"
6
+
7
+ import { cn } from "@/lib/utils"
8
+
9
+ function Accordion({
10
+ ...props
11
+ }: React.ComponentProps<typeof AccordionPrimitive.Root>) {
12
+ return <AccordionPrimitive.Root data-slot="accordion" {...props} />
13
+ }
14
+
15
+ function AccordionItem({
16
+ className,
17
+ ...props
18
+ }: React.ComponentProps<typeof AccordionPrimitive.Item>) {
19
+ return (
20
+ <AccordionPrimitive.Item
21
+ data-slot="accordion-item"
22
+ className={cn("border-b last:border-b-0", className)}
23
+ {...props}
24
+ />
25
+ )
26
+ }
27
+
28
+ function AccordionTrigger({
29
+ className,
30
+ children,
31
+ ...props
32
+ }: React.ComponentProps<typeof AccordionPrimitive.Trigger>) {
33
+ return (
34
+ <AccordionPrimitive.Header className="flex">
35
+ <AccordionPrimitive.Trigger
36
+ data-slot="accordion-trigger"
37
+ className={cn(
38
+ "focus-visible:border-ring focus-visible:ring-ring/50 flex flex-1 items-start justify-between gap-4 rounded-md py-4 text-left text-sm font-medium transition-all outline-none hover:underline focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180",
39
+ className
40
+ )}
41
+ {...props}
42
+ >
43
+ {children}
44
+ <ChevronDownIcon className="text-muted-foreground pointer-events-none size-4 shrink-0 translate-y-0.5 transition-transform duration-200" />
45
+ </AccordionPrimitive.Trigger>
46
+ </AccordionPrimitive.Header>
47
+ )
48
+ }
49
+
50
+ function AccordionContent({
51
+ className,
52
+ children,
53
+ ...props
54
+ }: React.ComponentProps<typeof AccordionPrimitive.Content>) {
55
+ return (
56
+ <AccordionPrimitive.Content
57
+ data-slot="accordion-content"
58
+ className="data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm"
59
+ {...props}
60
+ >
61
+ <div className={cn("pt-0 pb-4", className)}>{children}</div>
62
+ </AccordionPrimitive.Content>
63
+ )
64
+ }
65
+
66
+ export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
src/components/ui/alert-dialog.tsx ADDED
@@ -0,0 +1,157 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
5
+
6
+ import { cn } from "@/lib/utils"
7
+ import { buttonVariants } from "@/components/ui/button"
8
+
9
+ function AlertDialog({
10
+ ...props
11
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Root>) {
12
+ return <AlertDialogPrimitive.Root data-slot="alert-dialog" {...props} />
13
+ }
14
+
15
+ function AlertDialogTrigger({
16
+ ...props
17
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Trigger>) {
18
+ return (
19
+ <AlertDialogPrimitive.Trigger data-slot="alert-dialog-trigger" {...props} />
20
+ )
21
+ }
22
+
23
+ function AlertDialogPortal({
24
+ ...props
25
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Portal>) {
26
+ return (
27
+ <AlertDialogPrimitive.Portal data-slot="alert-dialog-portal" {...props} />
28
+ )
29
+ }
30
+
31
+ function AlertDialogOverlay({
32
+ className,
33
+ ...props
34
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Overlay>) {
35
+ return (
36
+ <AlertDialogPrimitive.Overlay
37
+ data-slot="alert-dialog-overlay"
38
+ className={cn(
39
+ "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
40
+ className
41
+ )}
42
+ {...props}
43
+ />
44
+ )
45
+ }
46
+
47
+ function AlertDialogContent({
48
+ className,
49
+ ...props
50
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Content>) {
51
+ return (
52
+ <AlertDialogPortal>
53
+ <AlertDialogOverlay />
54
+ <AlertDialogPrimitive.Content
55
+ data-slot="alert-dialog-content"
56
+ className={cn(
57
+ "bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
58
+ className
59
+ )}
60
+ {...props}
61
+ />
62
+ </AlertDialogPortal>
63
+ )
64
+ }
65
+
66
+ function AlertDialogHeader({
67
+ className,
68
+ ...props
69
+ }: React.ComponentProps<"div">) {
70
+ return (
71
+ <div
72
+ data-slot="alert-dialog-header"
73
+ className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
74
+ {...props}
75
+ />
76
+ )
77
+ }
78
+
79
+ function AlertDialogFooter({
80
+ className,
81
+ ...props
82
+ }: React.ComponentProps<"div">) {
83
+ return (
84
+ <div
85
+ data-slot="alert-dialog-footer"
86
+ className={cn(
87
+ "flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
88
+ className
89
+ )}
90
+ {...props}
91
+ />
92
+ )
93
+ }
94
+
95
+ function AlertDialogTitle({
96
+ className,
97
+ ...props
98
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Title>) {
99
+ return (
100
+ <AlertDialogPrimitive.Title
101
+ data-slot="alert-dialog-title"
102
+ className={cn("text-lg font-semibold", className)}
103
+ {...props}
104
+ />
105
+ )
106
+ }
107
+
108
+ function AlertDialogDescription({
109
+ className,
110
+ ...props
111
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Description>) {
112
+ return (
113
+ <AlertDialogPrimitive.Description
114
+ data-slot="alert-dialog-description"
115
+ className={cn("text-muted-foreground text-sm", className)}
116
+ {...props}
117
+ />
118
+ )
119
+ }
120
+
121
+ function AlertDialogAction({
122
+ className,
123
+ ...props
124
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Action>) {
125
+ return (
126
+ <AlertDialogPrimitive.Action
127
+ className={cn(buttonVariants(), className)}
128
+ {...props}
129
+ />
130
+ )
131
+ }
132
+
133
+ function AlertDialogCancel({
134
+ className,
135
+ ...props
136
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Cancel>) {
137
+ return (
138
+ <AlertDialogPrimitive.Cancel
139
+ className={cn(buttonVariants({ variant: "outline" }), className)}
140
+ {...props}
141
+ />
142
+ )
143
+ }
144
+
145
+ export {
146
+ AlertDialog,
147
+ AlertDialogPortal,
148
+ AlertDialogOverlay,
149
+ AlertDialogTrigger,
150
+ AlertDialogContent,
151
+ AlertDialogHeader,
152
+ AlertDialogFooter,
153
+ AlertDialogTitle,
154
+ AlertDialogDescription,
155
+ AlertDialogAction,
156
+ AlertDialogCancel,
157
+ }
src/components/ui/alert.tsx ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+ import { cva, type VariantProps } from "class-variance-authority"
3
+
4
+ import { cn } from "@/lib/utils"
5
+
6
+ const alertVariants = cva(
7
+ "relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current",
8
+ {
9
+ variants: {
10
+ variant: {
11
+ default: "bg-card text-card-foreground",
12
+ destructive:
13
+ "text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90",
14
+ },
15
+ },
16
+ defaultVariants: {
17
+ variant: "default",
18
+ },
19
+ }
20
+ )
21
+
22
+ function Alert({
23
+ className,
24
+ variant,
25
+ ...props
26
+ }: React.ComponentProps<"div"> & VariantProps<typeof alertVariants>) {
27
+ return (
28
+ <div
29
+ data-slot="alert"
30
+ role="alert"
31
+ className={cn(alertVariants({ variant }), className)}
32
+ {...props}
33
+ />
34
+ )
35
+ }
36
+
37
+ function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
38
+ return (
39
+ <div
40
+ data-slot="alert-title"
41
+ className={cn(
42
+ "col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight",
43
+ className
44
+ )}
45
+ {...props}
46
+ />
47
+ )
48
+ }
49
+
50
+ function AlertDescription({
51
+ className,
52
+ ...props
53
+ }: React.ComponentProps<"div">) {
54
+ return (
55
+ <div
56
+ data-slot="alert-description"
57
+ className={cn(
58
+ "text-muted-foreground col-start-2 grid justify-items-start gap-1 text-sm [&_p]:leading-relaxed",
59
+ className
60
+ )}
61
+ {...props}
62
+ />
63
+ )
64
+ }
65
+
66
+ export { Alert, AlertTitle, AlertDescription }
src/components/ui/aspect-ratio.tsx ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio"
4
+
5
+ function AspectRatio({
6
+ ...props
7
+ }: React.ComponentProps<typeof AspectRatioPrimitive.Root>) {
8
+ return <AspectRatioPrimitive.Root data-slot="aspect-ratio" {...props} />
9
+ }
10
+
11
+ export { AspectRatio }
src/components/ui/avatar.tsx ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import * as AvatarPrimitive from "@radix-ui/react-avatar"
5
+
6
+ import { cn } from "@/lib/utils"
7
+
8
+ function Avatar({
9
+ className,
10
+ ...props
11
+ }: React.ComponentProps<typeof AvatarPrimitive.Root>) {
12
+ return (
13
+ <AvatarPrimitive.Root
14
+ data-slot="avatar"
15
+ className={cn(
16
+ "relative flex size-8 shrink-0 overflow-hidden rounded-full",
17
+ className
18
+ )}
19
+ {...props}
20
+ />
21
+ )
22
+ }
23
+
24
+ function AvatarImage({
25
+ className,
26
+ ...props
27
+ }: React.ComponentProps<typeof AvatarPrimitive.Image>) {
28
+ return (
29
+ <AvatarPrimitive.Image
30
+ data-slot="avatar-image"
31
+ className={cn("aspect-square size-full", className)}
32
+ {...props}
33
+ />
34
+ )
35
+ }
36
+
37
+ function AvatarFallback({
38
+ className,
39
+ ...props
40
+ }: React.ComponentProps<typeof AvatarPrimitive.Fallback>) {
41
+ return (
42
+ <AvatarPrimitive.Fallback
43
+ data-slot="avatar-fallback"
44
+ className={cn(
45
+ "bg-muted flex size-full items-center justify-center rounded-full",
46
+ className
47
+ )}
48
+ {...props}
49
+ />
50
+ )
51
+ }
52
+
53
+ export { Avatar, AvatarImage, AvatarFallback }
src/components/ui/badge.tsx ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+ import { Slot } from "@radix-ui/react-slot"
3
+ import { cva, type VariantProps } from "class-variance-authority"
4
+
5
+ import { cn } from "@/lib/utils"
6
+
7
+ const badgeVariants = cva(
8
+ "inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
9
+ {
10
+ variants: {
11
+ variant: {
12
+ default:
13
+ "border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
14
+ secondary:
15
+ "border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
16
+ destructive:
17
+ "border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
18
+ outline:
19
+ "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
20
+ },
21
+ },
22
+ defaultVariants: {
23
+ variant: "default",
24
+ },
25
+ }
26
+ )
27
+
28
+ function Badge({
29
+ className,
30
+ variant,
31
+ asChild = false,
32
+ ...props
33
+ }: React.ComponentProps<"span"> &
34
+ VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
35
+ const Comp = asChild ? Slot : "span"
36
+
37
+ return (
38
+ <Comp
39
+ data-slot="badge"
40
+ className={cn(badgeVariants({ variant }), className)}
41
+ {...props}
42
+ />
43
+ )
44
+ }
45
+
46
+ export { Badge, badgeVariants }
src/components/ui/breadcrumb.tsx ADDED
@@ -0,0 +1,109 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+ import { Slot } from "@radix-ui/react-slot"
3
+ import { ChevronRight, MoreHorizontal } from "lucide-react"
4
+
5
+ import { cn } from "@/lib/utils"
6
+
7
+ function Breadcrumb({ ...props }: React.ComponentProps<"nav">) {
8
+ return <nav aria-label="breadcrumb" data-slot="breadcrumb" {...props} />
9
+ }
10
+
11
+ function BreadcrumbList({ className, ...props }: React.ComponentProps<"ol">) {
12
+ return (
13
+ <ol
14
+ data-slot="breadcrumb-list"
15
+ className={cn(
16
+ "text-muted-foreground flex flex-wrap items-center gap-1.5 text-sm break-words sm:gap-2.5",
17
+ className
18
+ )}
19
+ {...props}
20
+ />
21
+ )
22
+ }
23
+
24
+ function BreadcrumbItem({ className, ...props }: React.ComponentProps<"li">) {
25
+ return (
26
+ <li
27
+ data-slot="breadcrumb-item"
28
+ className={cn("inline-flex items-center gap-1.5", className)}
29
+ {...props}
30
+ />
31
+ )
32
+ }
33
+
34
+ function BreadcrumbLink({
35
+ asChild,
36
+ className,
37
+ ...props
38
+ }: React.ComponentProps<"a"> & {
39
+ asChild?: boolean
40
+ }) {
41
+ const Comp = asChild ? Slot : "a"
42
+
43
+ return (
44
+ <Comp
45
+ data-slot="breadcrumb-link"
46
+ className={cn("hover:text-foreground transition-colors", className)}
47
+ {...props}
48
+ />
49
+ )
50
+ }
51
+
52
+ function BreadcrumbPage({ className, ...props }: React.ComponentProps<"span">) {
53
+ return (
54
+ <span
55
+ data-slot="breadcrumb-page"
56
+ role="link"
57
+ aria-disabled="true"
58
+ aria-current="page"
59
+ className={cn("text-foreground font-normal", className)}
60
+ {...props}
61
+ />
62
+ )
63
+ }
64
+
65
+ function BreadcrumbSeparator({
66
+ children,
67
+ className,
68
+ ...props
69
+ }: React.ComponentProps<"li">) {
70
+ return (
71
+ <li
72
+ data-slot="breadcrumb-separator"
73
+ role="presentation"
74
+ aria-hidden="true"
75
+ className={cn("[&>svg]:size-3.5", className)}
76
+ {...props}
77
+ >
78
+ {children ?? <ChevronRight />}
79
+ </li>
80
+ )
81
+ }
82
+
83
+ function BreadcrumbEllipsis({
84
+ className,
85
+ ...props
86
+ }: React.ComponentProps<"span">) {
87
+ return (
88
+ <span
89
+ data-slot="breadcrumb-ellipsis"
90
+ role="presentation"
91
+ aria-hidden="true"
92
+ className={cn("flex size-9 items-center justify-center", className)}
93
+ {...props}
94
+ >
95
+ <MoreHorizontal className="size-4" />
96
+ <span className="sr-only">More</span>
97
+ </span>
98
+ )
99
+ }
100
+
101
+ export {
102
+ Breadcrumb,
103
+ BreadcrumbList,
104
+ BreadcrumbItem,
105
+ BreadcrumbLink,
106
+ BreadcrumbPage,
107
+ BreadcrumbSeparator,
108
+ BreadcrumbEllipsis,
109
+ }
src/components/ui/button.tsx ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+ import { Slot } from "@radix-ui/react-slot"
3
+ import { cva, type VariantProps } from "class-variance-authority"
4
+
5
+ import { cn } from "@/lib/utils"
6
+
7
+ const buttonVariants = cva(
8
+ "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
9
+ {
10
+ variants: {
11
+ variant: {
12
+ default:
13
+ "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
14
+ destructive:
15
+ "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
16
+ outline:
17
+ "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
18
+ secondary:
19
+ "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
20
+ ghost:
21
+ "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
22
+ link: "text-primary underline-offset-4 hover:underline",
23
+ },
24
+ size: {
25
+ default: "h-9 px-4 py-2 has-[>svg]:px-3",
26
+ sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
27
+ lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
28
+ icon: "size-9",
29
+ },
30
+ },
31
+ defaultVariants: {
32
+ variant: "default",
33
+ size: "default",
34
+ },
35
+ }
36
+ )
37
+
38
+ function Button({
39
+ className,
40
+ variant,
41
+ size,
42
+ asChild = false,
43
+ ...props
44
+ }: React.ComponentProps<"button"> &
45
+ VariantProps<typeof buttonVariants> & {
46
+ asChild?: boolean
47
+ }) {
48
+ const Comp = asChild ? Slot : "button"
49
+
50
+ return (
51
+ <Comp
52
+ data-slot="button"
53
+ className={cn(buttonVariants({ variant, size, className }))}
54
+ {...props}
55
+ />
56
+ )
57
+ }
58
+
59
+ export { Button, buttonVariants }
src/components/ui/calendar.tsx ADDED
@@ -0,0 +1,213 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import {
5
+ ChevronDownIcon,
6
+ ChevronLeftIcon,
7
+ ChevronRightIcon,
8
+ } from "lucide-react"
9
+ import { DayButton, DayPicker, getDefaultClassNames } from "react-day-picker"
10
+
11
+ import { cn } from "@/lib/utils"
12
+ import { Button, buttonVariants } from "@/components/ui/button"
13
+
14
+ function Calendar({
15
+ className,
16
+ classNames,
17
+ showOutsideDays = true,
18
+ captionLayout = "label",
19
+ buttonVariant = "ghost",
20
+ formatters,
21
+ components,
22
+ ...props
23
+ }: React.ComponentProps<typeof DayPicker> & {
24
+ buttonVariant?: React.ComponentProps<typeof Button>["variant"]
25
+ }) {
26
+ const defaultClassNames = getDefaultClassNames()
27
+
28
+ return (
29
+ <DayPicker
30
+ showOutsideDays={showOutsideDays}
31
+ className={cn(
32
+ "bg-background group/calendar p-3 [--cell-size:--spacing(8)] [[data-slot=card-content]_&]:bg-transparent [[data-slot=popover-content]_&]:bg-transparent",
33
+ String.raw`rtl:**:[.rdp-button\_next>svg]:rotate-180`,
34
+ String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`,
35
+ className
36
+ )}
37
+ captionLayout={captionLayout}
38
+ formatters={{
39
+ formatMonthDropdown: (date) =>
40
+ date.toLocaleString("default", { month: "short" }),
41
+ ...formatters,
42
+ }}
43
+ classNames={{
44
+ root: cn("w-fit", defaultClassNames.root),
45
+ months: cn(
46
+ "flex gap-4 flex-col md:flex-row relative",
47
+ defaultClassNames.months
48
+ ),
49
+ month: cn("flex flex-col w-full gap-4", defaultClassNames.month),
50
+ nav: cn(
51
+ "flex items-center gap-1 w-full absolute top-0 inset-x-0 justify-between",
52
+ defaultClassNames.nav
53
+ ),
54
+ button_previous: cn(
55
+ buttonVariants({ variant: buttonVariant }),
56
+ "size-(--cell-size) aria-disabled:opacity-50 p-0 select-none",
57
+ defaultClassNames.button_previous
58
+ ),
59
+ button_next: cn(
60
+ buttonVariants({ variant: buttonVariant }),
61
+ "size-(--cell-size) aria-disabled:opacity-50 p-0 select-none",
62
+ defaultClassNames.button_next
63
+ ),
64
+ month_caption: cn(
65
+ "flex items-center justify-center h-(--cell-size) w-full px-(--cell-size)",
66
+ defaultClassNames.month_caption
67
+ ),
68
+ dropdowns: cn(
69
+ "w-full flex items-center text-sm font-medium justify-center h-(--cell-size) gap-1.5",
70
+ defaultClassNames.dropdowns
71
+ ),
72
+ dropdown_root: cn(
73
+ "relative has-focus:border-ring border border-input shadow-xs has-focus:ring-ring/50 has-focus:ring-[3px] rounded-md",
74
+ defaultClassNames.dropdown_root
75
+ ),
76
+ dropdown: cn(
77
+ "absolute bg-popover inset-0 opacity-0",
78
+ defaultClassNames.dropdown
79
+ ),
80
+ caption_label: cn(
81
+ "select-none font-medium",
82
+ captionLayout === "label"
83
+ ? "text-sm"
84
+ : "rounded-md pl-2 pr-1 flex items-center gap-1 text-sm h-8 [&>svg]:text-muted-foreground [&>svg]:size-3.5",
85
+ defaultClassNames.caption_label
86
+ ),
87
+ table: "w-full border-collapse",
88
+ weekdays: cn("flex", defaultClassNames.weekdays),
89
+ weekday: cn(
90
+ "text-muted-foreground rounded-md flex-1 font-normal text-[0.8rem] select-none",
91
+ defaultClassNames.weekday
92
+ ),
93
+ week: cn("flex w-full mt-2", defaultClassNames.week),
94
+ week_number_header: cn(
95
+ "select-none w-(--cell-size)",
96
+ defaultClassNames.week_number_header
97
+ ),
98
+ week_number: cn(
99
+ "text-[0.8rem] select-none text-muted-foreground",
100
+ defaultClassNames.week_number
101
+ ),
102
+ day: cn(
103
+ "relative w-full h-full p-0 text-center [&:first-child[data-selected=true]_button]:rounded-l-md [&:last-child[data-selected=true]_button]:rounded-r-md group/day aspect-square select-none",
104
+ defaultClassNames.day
105
+ ),
106
+ range_start: cn(
107
+ "rounded-l-md bg-accent",
108
+ defaultClassNames.range_start
109
+ ),
110
+ range_middle: cn("rounded-none", defaultClassNames.range_middle),
111
+ range_end: cn("rounded-r-md bg-accent", defaultClassNames.range_end),
112
+ today: cn(
113
+ "bg-accent text-accent-foreground rounded-md data-[selected=true]:rounded-none",
114
+ defaultClassNames.today
115
+ ),
116
+ outside: cn(
117
+ "text-muted-foreground aria-selected:text-muted-foreground",
118
+ defaultClassNames.outside
119
+ ),
120
+ disabled: cn(
121
+ "text-muted-foreground opacity-50",
122
+ defaultClassNames.disabled
123
+ ),
124
+ hidden: cn("invisible", defaultClassNames.hidden),
125
+ ...classNames,
126
+ }}
127
+ components={{
128
+ Root: ({ className, rootRef, ...props }) => {
129
+ return (
130
+ <div
131
+ data-slot="calendar"
132
+ ref={rootRef}
133
+ className={cn(className)}
134
+ {...props}
135
+ />
136
+ )
137
+ },
138
+ Chevron: ({ className, orientation, ...props }) => {
139
+ if (orientation === "left") {
140
+ return (
141
+ <ChevronLeftIcon className={cn("size-4", className)} {...props} />
142
+ )
143
+ }
144
+
145
+ if (orientation === "right") {
146
+ return (
147
+ <ChevronRightIcon
148
+ className={cn("size-4", className)}
149
+ {...props}
150
+ />
151
+ )
152
+ }
153
+
154
+ return (
155
+ <ChevronDownIcon className={cn("size-4", className)} {...props} />
156
+ )
157
+ },
158
+ DayButton: CalendarDayButton,
159
+ WeekNumber: ({ children, ...props }) => {
160
+ return (
161
+ <td {...props}>
162
+ <div className="flex size-(--cell-size) items-center justify-center text-center">
163
+ {children}
164
+ </div>
165
+ </td>
166
+ )
167
+ },
168
+ ...components,
169
+ }}
170
+ {...props}
171
+ />
172
+ )
173
+ }
174
+
175
+ function CalendarDayButton({
176
+ className,
177
+ day,
178
+ modifiers,
179
+ ...props
180
+ }: React.ComponentProps<typeof DayButton>) {
181
+ const defaultClassNames = getDefaultClassNames()
182
+
183
+ const ref = React.useRef<HTMLButtonElement>(null)
184
+ React.useEffect(() => {
185
+ if (modifiers.focused) ref.current?.focus()
186
+ }, [modifiers.focused])
187
+
188
+ return (
189
+ <Button
190
+ ref={ref}
191
+ variant="ghost"
192
+ size="icon"
193
+ data-day={day.date.toLocaleDateString()}
194
+ data-selected-single={
195
+ modifiers.selected &&
196
+ !modifiers.range_start &&
197
+ !modifiers.range_end &&
198
+ !modifiers.range_middle
199
+ }
200
+ data-range-start={modifiers.range_start}
201
+ data-range-end={modifiers.range_end}
202
+ data-range-middle={modifiers.range_middle}
203
+ className={cn(
204
+ "data-[selected-single=true]:bg-primary data-[selected-single=true]:text-primary-foreground data-[range-middle=true]:bg-accent data-[range-middle=true]:text-accent-foreground data-[range-start=true]:bg-primary data-[range-start=true]:text-primary-foreground data-[range-end=true]:bg-primary data-[range-end=true]:text-primary-foreground group-data-[focused=true]/day:border-ring group-data-[focused=true]/day:ring-ring/50 dark:hover:text-accent-foreground flex aspect-square size-auto w-full min-w-(--cell-size) flex-col gap-1 leading-none font-normal group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10 group-data-[focused=true]/day:ring-[3px] data-[range-end=true]:rounded-md data-[range-end=true]:rounded-r-md data-[range-middle=true]:rounded-none data-[range-start=true]:rounded-md data-[range-start=true]:rounded-l-md [&>span]:text-xs [&>span]:opacity-70",
205
+ defaultClassNames.day,
206
+ className
207
+ )}
208
+ {...props}
209
+ />
210
+ )
211
+ }
212
+
213
+ export { Calendar, CalendarDayButton }
src/components/ui/card.tsx ADDED
@@ -0,0 +1,92 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+
3
+ import { cn } from "@/lib/utils"
4
+
5
+ function Card({ className, ...props }: React.ComponentProps<"div">) {
6
+ return (
7
+ <div
8
+ data-slot="card"
9
+ className={cn(
10
+ "bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
11
+ className
12
+ )}
13
+ {...props}
14
+ />
15
+ )
16
+ }
17
+
18
+ function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
19
+ return (
20
+ <div
21
+ data-slot="card-header"
22
+ className={cn(
23
+ "@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
24
+ className
25
+ )}
26
+ {...props}
27
+ />
28
+ )
29
+ }
30
+
31
+ function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
32
+ return (
33
+ <div
34
+ data-slot="card-title"
35
+ className={cn("leading-none font-semibold", className)}
36
+ {...props}
37
+ />
38
+ )
39
+ }
40
+
41
+ function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
42
+ return (
43
+ <div
44
+ data-slot="card-description"
45
+ className={cn("text-muted-foreground text-sm", className)}
46
+ {...props}
47
+ />
48
+ )
49
+ }
50
+
51
+ function CardAction({ className, ...props }: React.ComponentProps<"div">) {
52
+ return (
53
+ <div
54
+ data-slot="card-action"
55
+ className={cn(
56
+ "col-start-2 row-span-2 row-start-1 self-start justify-self-end",
57
+ className
58
+ )}
59
+ {...props}
60
+ />
61
+ )
62
+ }
63
+
64
+ function CardContent({ className, ...props }: React.ComponentProps<"div">) {
65
+ return (
66
+ <div
67
+ data-slot="card-content"
68
+ className={cn("px-6", className)}
69
+ {...props}
70
+ />
71
+ )
72
+ }
73
+
74
+ function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
75
+ return (
76
+ <div
77
+ data-slot="card-footer"
78
+ className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
79
+ {...props}
80
+ />
81
+ )
82
+ }
83
+
84
+ export {
85
+ Card,
86
+ CardHeader,
87
+ CardFooter,
88
+ CardTitle,
89
+ CardAction,
90
+ CardDescription,
91
+ CardContent,
92
+ }