Z User commited on
Commit ·
15d6beb
1
Parent(s): a53475b
sync work in progress
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .gitignore +54 -2
- bun.lock +0 -0
- components.json +21 -0
- eslint.config.mjs +50 -0
- examples/websocket/frontend.tsx +196 -0
- examples/websocket/server.ts +138 -0
- next.config.ts +12 -0
- package.json +94 -0
- postcss.config.mjs +5 -0
- prisma/schema.prisma +32 -0
- public/dssl/boot.js +2 -0
- public/dssl/engine.js +447 -0
- public/dssl/index.js +162 -0
- public/dssl/parser.js +228 -0
- public/dssl/resolver.js +776 -0
- public/dssl/utils.js +34 -0
- public/favicon.svg +5 -0
- public/logo.svg +29 -0
- public/robots.txt +14 -0
- public/styles/changelog.dssl +97 -0
- public/styles/docs.dssl +321 -0
- public/styles/home.dssl +422 -0
- public/styles/playground.dssl +180 -0
- public/styles/site.dssl +472 -0
- public/styles/tokens.dssl +104 -0
- scripts/dssl-smoke.mjs +131 -0
- scripts/dssl-styles-check.mjs +28 -0
- src/app/api/route.ts +5 -0
- src/app/globals.css +2 -0
- src/app/layout.tsx +35 -0
- src/app/page.tsx +5 -0
- src/components/dssl/CodeBlock.tsx +46 -0
- src/components/dssl/SiteRoot.tsx +189 -0
- src/components/dssl/highlight.ts +87 -0
- src/components/dssl/icons.tsx +58 -0
- src/components/dssl/views/ChangelogView.tsx +178 -0
- src/components/dssl/views/DocsView.tsx +400 -0
- src/components/dssl/views/HomeView.tsx +331 -0
- src/components/dssl/views/PlaygroundView.tsx +387 -0
- src/components/dssl/views/SpecView.tsx +204 -0
- src/components/ui/accordion.tsx +66 -0
- src/components/ui/alert-dialog.tsx +157 -0
- src/components/ui/alert.tsx +66 -0
- src/components/ui/aspect-ratio.tsx +11 -0
- src/components/ui/avatar.tsx +53 -0
- src/components/ui/badge.tsx +46 -0
- src/components/ui/breadcrumb.tsx +109 -0
- src/components/ui/button.tsx +59 -0
- src/components/ui/calendar.tsx +213 -0
- src/components/ui/card.tsx +92 -0
.gitignore
CHANGED
|
@@ -1,2 +1,54 @@
|
|
| 1 |
-
|
| 2 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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, "&").replace(/</g, "<").replace(/>/g, ">");
|
| 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>&</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'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 < base < components < utilities < 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'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('.motion-box')</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('theme', 'dark')</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>"…"</code> or <code>'…'</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 >= 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'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><html></code> as{" "}
|
| 156 |
+
<code>data-dssl-signal-<name></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 |
+
}
|