@echodial/deck 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +1327 -0
- package/bin/deck.mjs +219 -0
- package/build.mjs +338 -0
- package/dist/api-buckets.json +10486 -0
- package/dist/api.json +57896 -0
- package/dist/brand/deck-apple-touch-icon.png +0 -0
- package/dist/brand/deck-logo-dark.svg +1 -0
- package/dist/brand/deck-logo-light.svg +1 -0
- package/dist/brand/deck-logo-stacked.svg +1 -0
- package/dist/brand/deck-logo.svg +1 -0
- package/dist/brand/deck-mark.svg +7 -0
- package/dist/brand/deck-og.png +0 -0
- package/dist/collisions.json +223 -0
- package/dist/deck-adapters.js +482 -0
- package/dist/deck-adapters.min.js +23 -0
- package/dist/deck-extras.js +823 -0
- package/dist/deck-extras.min.js +5 -0
- package/dist/deck-icons.svg +184 -0
- package/dist/deck.bundle.js +2638 -0
- package/dist/deck.bundle.min.js +86 -0
- package/dist/deck.css +7904 -0
- package/dist/deck.esm.js +2644 -0
- package/dist/deck.js +1331 -0
- package/dist/deck.min.css +32 -0
- package/dist/deck.min.js +60 -0
- package/dist/layers/anchor.css +192 -0
- package/dist/layers/buttons.css +210 -0
- package/dist/layers/charts.css +337 -0
- package/dist/layers/combobox.css +248 -0
- package/dist/layers/components.css +868 -0
- package/dist/layers/container.css +199 -0
- package/dist/layers/datagrid.css +335 -0
- package/dist/layers/datepicker.css +305 -0
- package/dist/layers/forms.css +401 -0
- package/dist/layers/gradients.css +302 -0
- package/dist/layers/inputs.css +522 -0
- package/dist/layers/layers.css +87 -0
- package/dist/layers/layout.css +235 -0
- package/dist/layers/logical.css +226 -0
- package/dist/layers/media.css +400 -0
- package/dist/layers/mobile.css +245 -0
- package/dist/layers/motion.css +379 -0
- package/dist/layers/nav.css +771 -0
- package/dist/layers/perf.css +234 -0
- package/dist/layers/print.css +198 -0
- package/dist/layers/reset.css +103 -0
- package/dist/layers/space3d.css +280 -0
- package/dist/layers/toasts.css +201 -0
- package/dist/layers/tokens.css +229 -0
- package/dist/layers/type.css +114 -0
- package/dist/layers/utilities.css +241 -0
- package/dist/sizes.json +52 -0
- package/dist/usage.json +6340 -0
- package/package.json +94 -0
- package/src/00-layers.css +87 -0
- package/src/01-tokens.css +229 -0
- package/src/02-reset.css +103 -0
- package/src/03-type.css +114 -0
- package/src/04-layout.css +235 -0
- package/src/05-buttons.css +210 -0
- package/src/06-forms.css +401 -0
- package/src/07-components.css +868 -0
- package/src/08-mobile.css +245 -0
- package/src/09-utilities.css +241 -0
- package/src/10-datepicker.css +305 -0
- package/src/11-combobox.css +248 -0
- package/src/12-datagrid.css +335 -0
- package/src/13-toasts.css +201 -0
- package/src/14-charts.css +337 -0
- package/src/16-motion.css +379 -0
- package/src/18-container.css +199 -0
- package/src/19-logical.css +226 -0
- package/src/20-gradients.css +302 -0
- package/src/21-space3d.css +280 -0
- package/src/22-nav.css +771 -0
- package/src/23-inputs.css +522 -0
- package/src/24-media.css +400 -0
- package/src/25-anchor.css +192 -0
- package/src/26-perf.css +234 -0
- package/src/99-print.css +198 -0
- package/src/brand/deck-apple-touch-icon.png +0 -0
- package/src/brand/deck-logo-dark.svg +1 -0
- package/src/brand/deck-logo-light.svg +1 -0
- package/src/brand/deck-logo-stacked.svg +1 -0
- package/src/brand/deck-logo.svg +1 -0
- package/src/brand/deck-mark.svg +7 -0
- package/src/brand/deck-og.png +0 -0
- package/src/brand/sources.json +7 -0
- package/src/deck-icons.svg +184 -0
- package/src/js/deck-adapters.js +482 -0
- package/src/js/deck-extras.js +823 -0
- package/src/js/deck.js +1331 -0
package/README.md
ADDED
|
@@ -0,0 +1,1327 @@
|
|
|
1
|
+
# Deck
|
|
2
|
+
|
|
3
|
+
**Deck is a CSS framework that ships as one 26.7 KB Brotli (32.7 KB gzip) stylesheet: buttons, forms,
|
|
4
|
+
tables, a data grid, charts, overlays, an icon sprite, and a full color system. You add it
|
|
5
|
+
with one `<link>` tag. There is no build step, no config file, and zero dependencies.**
|
|
6
|
+
|
|
7
|
+
```html
|
|
8
|
+
<link rel="stylesheet" href="/assets/deck/deck.css">
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
That is the whole install. Nothing to compile, nothing to purge, nothing to configure.
|
|
12
|
+
|
|
13
|
+
- **No build step.** The file you download is the file the browser reads.
|
|
14
|
+
- **Retheme from one number.** Set `--hue-brand` to 0–360 and every brand color, focus
|
|
15
|
+
ring, badge, chart series, and shadow is recomputed — at runtime, no rebuild.
|
|
16
|
+
- **Your CSS wins.** Deck ships in cascade layers, so an ordinary unlayered rule
|
|
17
|
+
overrides it without a single `!important`.
|
|
18
|
+
- **RTL built in.** Written in logical properties end to end; `dir="rtl"` flips the
|
|
19
|
+
whole page with no second stylesheet.
|
|
20
|
+
- **Zero runtime dependencies.** The JavaScript is optional and dependency-free.
|
|
21
|
+
|
|
22
|
+
Version 0.1.0 · MIT · Chrome 117+, Edge 117+, Safari 17.4+, Firefox 128+
|
|
23
|
+
|
|
24
|
+
## What Deck weighs
|
|
25
|
+
|
|
26
|
+
A page that loads the stylesheet, the icon sprite, and the optional JavaScript transfers **62.7 KB** Brotli, or **76.3 KB** gzip. Every browser Deck supports sends `br` in
|
|
27
|
+
`Accept-Encoding`, and Cloudflare, Vercel, Netlify, and nginx with `ngx_brotli` negotiate
|
|
28
|
+
it for text by default, so Brotli is what most users actually receive.
|
|
29
|
+
|
|
30
|
+
| File | Brotli | gzip |
|
|
31
|
+
| --- | --- | --- |
|
|
32
|
+
| `deck.min.css` | **26.7 KB** | 32.7 KB |
|
|
33
|
+
| `deck-icons.svg` | **27.1 KB** | 33.6 KB |
|
|
34
|
+
| `deck.min.js` | **8.9 KB** | 10.0 KB |
|
|
35
|
+
| **All three** | **62.7 KB** | **76.3 KB** |
|
|
36
|
+
|
|
37
|
+
The sprite is the largest single file, slightly bigger than the stylesheet — worth stating
|
|
38
|
+
plainly rather than leaving you to find it in devtools. Swapping `deck.min.js` for the full
|
|
39
|
+
`deck.bundle.min.js` (adds the date picker, combobox, data grid, toasts, QR encoder)
|
|
40
|
+
makes the JavaScript 17.3 KB and the total 71.1 KB Brotli (85.8 KB gzip).
|
|
41
|
+
|
|
42
|
+
### The sprite is a manifest, not a fixed cost
|
|
43
|
+
|
|
44
|
+
The 75 icons are a default so the demo works out of the box, not a floor. `tools/icons/icons.txt`
|
|
45
|
+
lists what to extract — delete the lines you do not need and rebuild:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
# keep only the icons you use
|
|
49
|
+
$ cat > tools/icons/icons.txt <<'EOF'
|
|
50
|
+
check = check
|
|
51
|
+
search = search
|
|
52
|
+
settings = settings
|
|
53
|
+
@hand deck-mark
|
|
54
|
+
@hand deck-wordmark
|
|
55
|
+
EOF
|
|
56
|
+
|
|
57
|
+
$ npm run icons
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
**A twelve-icon sprite measures 5.7 KB Brotli (6.9 KB gzip)** — generated and measured, not
|
|
61
|
+
estimated. Against 27.1 KB for the full set, trimming the manifest is the difference between the
|
|
62
|
+
sprite dominating page weight and disappearing into it.
|
|
63
|
+
|
|
64
|
+
This matters because an external sprite is all-or-nothing per request: the browser fetches
|
|
65
|
+
the whole file to resolve a single `<use>`, so an unused icon is not free the way an unused
|
|
66
|
+
CSS class is. That is why the manifest exists. Keep the two `@hand` lines — they carry the
|
|
67
|
+
brand marks through from the previous sprite, and dropping them drops the marks.
|
|
68
|
+
|
|
69
|
+
### Everything in the package
|
|
70
|
+
|
|
71
|
+
| File | Brotli | gzip | What it is |
|
|
72
|
+
| --- | --- | --- | --- |
|
|
73
|
+
| `deck.min.css` | 26.7 KB | 32.7 KB | The whole framework |
|
|
74
|
+
| `deck.min.js` | 8.9 KB | 10.0 KB | Optional behaviour, no dependencies |
|
|
75
|
+
| `deck-extras.min.js` | 5.7 KB | 6.5 KB | Date picker, combobox, data grid, toasts, QR encoder |
|
|
76
|
+
| `deck-adapters.min.js` | 4.0 KB | 4.5 KB | Optional library integrations, inert unless one is loaded |
|
|
77
|
+
| `deck.bundle.min.js` | 17.3 KB | 19.5 KB | All three scripts in one file |
|
|
78
|
+
| `deck-icons.svg` | 27.1 KB | 33.6 KB | 152 symbols: 75 icons at two weights, plus two brand marks |
|
|
79
|
+
| `src/` | — | — | The 26 source stylesheets, concatenated to build `deck.css` |
|
|
80
|
+
| `dist/layers/` | — | — | One file per layer, if you only want part of Deck |
|
|
81
|
+
| `php/` | — | — | Optional PHP helper for Composer users |
|
|
82
|
+
| `bin/deck.mjs` | — | — | The `npx @echodial/deck` CLI |
|
|
83
|
+
|
|
84
|
+
Every size above is what `npm run build` prints, in decimal KB, and is written into this
|
|
85
|
+
file by the build rather than typed.
|
|
86
|
+
|
|
87
|
+
The component demo is `public_html/index.php` — every component on one page. Run it with
|
|
88
|
+
`npm run demo && npm start`.
|
|
89
|
+
|
|
90
|
+
## Repository layout
|
|
91
|
+
|
|
92
|
+
```
|
|
93
|
+
deck/
|
|
94
|
+
├─ src/ everything hand-written
|
|
95
|
+
│ ├─ 00-layers.css … 26 stylesheets, concatenated in filename order
|
|
96
|
+
│ ├─ deck-icons.svg the sprite — GENERATED, see `npm run icons`
|
|
97
|
+
│ ├─ brand/ the logo: one master, the rest derived from it
|
|
98
|
+
│ └─ js/ deck.js, deck-extras.js, deck-adapters.js
|
|
99
|
+
├─ dist/ entirely generated — safe to delete, `npm run build` rebuilds it
|
|
100
|
+
│ ├─ deck.css / .min.css
|
|
101
|
+
│ ├─ deck.js / -extras / -adapters, plus .min.js of each
|
|
102
|
+
│ ├─ deck.bundle.js / .min.js, deck.esm.js
|
|
103
|
+
│ ├─ deck-icons.svg
|
|
104
|
+
│ ├─ brand/ copy of src/brand/
|
|
105
|
+
│ └─ layers/ one file per layer, for partial adoption
|
|
106
|
+
├─ php/ Deck.php and Installer.php (PSR-4: EchoDial\Deck\)
|
|
107
|
+
├─ bin/deck.mjs the `npx @echodial/deck` CLI
|
|
108
|
+
├─ public_html/ the Helm docroot — the demo site, not part of the package
|
|
109
|
+
│ ├─ index.php component demo
|
|
110
|
+
│ ├─ php-helper.php the PHP helper, demonstrated
|
|
111
|
+
│ └─ assets/ published copies, both gitignored
|
|
112
|
+
│ ├─ deck/ dist/
|
|
113
|
+
│ └─ images/ dist/brand/
|
|
114
|
+
├─ build.mjs
|
|
115
|
+
├─ tools/
|
|
116
|
+
│ ├─ make-brand.mjs regenerates the logo family from the master
|
|
117
|
+
│ └─ icons/ the icon toolchain: `npm run icons`
|
|
118
|
+
│ ├─ icons.txt the list of icons to extract
|
|
119
|
+
│ ├─ build-icons.mjs writes src/deck-icons.svg from the font
|
|
120
|
+
│ └─ Material_Symbols_Rounded/ build time source, never shipped
|
|
121
|
+
├─ package.json npm; `files` ships src, dist, bin, build.mjs
|
|
122
|
+
├─ .gitattributes `export-ignore` keeps tools/ out of source archives
|
|
123
|
+
├─ composer.json Packagist; PSR-4 points at php/
|
|
124
|
+
└─ LICENSE
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Two rules keep this straight:
|
|
128
|
+
|
|
129
|
+
**`src/` is written, `dist/` is generated.** Never edit anything in `dist/` — the next
|
|
130
|
+
build overwrites it. `npm run clean && npm run build` should always reproduce it exactly.
|
|
131
|
+
|
|
132
|
+
**`dist/` is committed anyway.** Composer has no build step; Packagist just ships the
|
|
133
|
+
repository, so the built files have to be in it. That is the one place where the usual
|
|
134
|
+
"never commit build output" rule does not apply.
|
|
135
|
+
|
|
136
|
+
`public_html/` is the demo site for local development under Helm. It is not part of
|
|
137
|
+
either package — npm ships `files`, Composer ships `php/` and `dist/`. Its asset folder
|
|
138
|
+
is a published copy, so it is gitignored; run `npm run demo` after a clone to fill it.
|
|
139
|
+
|
|
140
|
+
### Commands
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
npm run build # src/ -> dist/
|
|
144
|
+
npm run demo # build, then publish dist/ into public_html/assets/
|
|
145
|
+
npm run brand # regenerate the logo family after changing the mark
|
|
146
|
+
npm run icons # regenerate src/deck-icons.svg from tools/icons/icons.txt
|
|
147
|
+
npm run clean # delete dist/
|
|
148
|
+
npm start # php -S localhost:4321 -t public_html
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
## Install
|
|
152
|
+
|
|
153
|
+
Deck ships three ways. Pick whichever matches how the project already works.
|
|
154
|
+
|
|
155
|
+
### 1. Just the files
|
|
156
|
+
|
|
157
|
+
Download `deck.css` and `deck-icons.svg`, drop them next to your other assets, and add
|
|
158
|
+
one line. No package manager, no build step, no Node on the server.
|
|
159
|
+
|
|
160
|
+
```html
|
|
161
|
+
<link rel="stylesheet" href="/assets/deck.css">
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Or let the CLI put them there for you — this does not install anything permanently:
|
|
165
|
+
|
|
166
|
+
```bash
|
|
167
|
+
npx @echodial/deck init public/assets/deck
|
|
168
|
+
npx @echodial/deck starter public/index.html # a working page to start from
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### 2. npm
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
npm install @echodial/deck
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
```js
|
|
178
|
+
import '@echodial/deck/css';
|
|
179
|
+
import Deck from '@echodial/deck';
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
Subpath exports, so you can take only what you need:
|
|
183
|
+
|
|
184
|
+
| Import | What it is |
|
|
185
|
+
|---|---|
|
|
186
|
+
| `@echodial/deck/css` | the whole stylesheet |
|
|
187
|
+
| `@echodial/deck/css/min` | minified |
|
|
188
|
+
| `@echodial/deck/icons` | the sprite |
|
|
189
|
+
| `@echodial/deck/js` | core behaviour |
|
|
190
|
+
| `@echodial/deck/extras` | datepicker, combobox, grid, toasts, QR |
|
|
191
|
+
| `@echodial/deck/adapters` | optional library integrations |
|
|
192
|
+
| `@echodial/deck/bundle` | all three in one file |
|
|
193
|
+
| `@echodial/deck/layers/tokens.css` | one layer at a time |
|
|
194
|
+
| `@echodial/deck/src/*` | the unconcatenated sources |
|
|
195
|
+
|
|
196
|
+
The `layers/` exports matter if you only want part of Deck. `layers/tokens.css` plus
|
|
197
|
+
`layers/reset.css` gives you the design system with none of the components, which is a
|
|
198
|
+
reasonable way to adopt it into an existing app one screen at a time.
|
|
199
|
+
|
|
200
|
+
### 3. CDN
|
|
201
|
+
|
|
202
|
+
Publishing to npm makes the CDNs work with no extra step:
|
|
203
|
+
|
|
204
|
+
```html
|
|
205
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@echodial/deck@0.1/dist/deck.min.css">
|
|
206
|
+
<script src="https://cdn.jsdelivr.net/npm/@echodial/deck@0.1/dist/deck.bundle.min.js" defer></script>
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
### 4. Composer
|
|
210
|
+
|
|
211
|
+
```bash
|
|
212
|
+
composer require echodial/deck
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
Assets cannot be served out of `vendor/`, so the package publishes them into your public
|
|
216
|
+
directory on install. Configure the destination in your own `composer.json`:
|
|
217
|
+
|
|
218
|
+
```json
|
|
219
|
+
{
|
|
220
|
+
"extra": {
|
|
221
|
+
"deck": {
|
|
222
|
+
"publish-to": "public/assets/deck",
|
|
223
|
+
"auto-publish": true
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
Or run it whenever you like:
|
|
230
|
+
|
|
231
|
+
```bash
|
|
232
|
+
composer deck-publish
|
|
233
|
+
composer deck-publish -- public/static/deck
|
|
234
|
+
composer deck-publish -- --link # symlink during development
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
Publishing skips files that have not changed, so a redeploy does not churn mtimes and
|
|
238
|
+
invalidate every cache-busting URL for nothing.
|
|
239
|
+
|
|
240
|
+
#### The PHP helper
|
|
241
|
+
|
|
242
|
+
Optional, framework-agnostic, and about two hundred lines. No container, no service
|
|
243
|
+
provider, no facade — it works in Keel, Laravel, Symfony, WordPress, or a single
|
|
244
|
+
`index.php`.
|
|
245
|
+
|
|
246
|
+
```php
|
|
247
|
+
use EchoDial\Deck\Deck;
|
|
248
|
+
|
|
249
|
+
Deck::configure([
|
|
250
|
+
'base' => '/assets/deck',
|
|
251
|
+
'adapters' => true,
|
|
252
|
+
'bundle' => true,
|
|
253
|
+
]);
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
```php
|
|
257
|
+
<html <?= Deck::htmlAttributes(lang: 'en') ?>>
|
|
258
|
+
<head>
|
|
259
|
+
<?= Deck::head() ?>
|
|
260
|
+
</head>
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
`Deck::head()` emits the viewport meta tag Deck's mobile-first layout assumes, the
|
|
264
|
+
stylesheet, the scripts in the right order, and the icon sprite path — with `?v=` cache
|
|
265
|
+
busting from the file mtime, so a deploy invalidates the browser cache and nothing else
|
|
266
|
+
does.
|
|
267
|
+
|
|
268
|
+
```php
|
|
269
|
+
<?= Deck::icon('check-circle', 'icon icon-lg') ?>
|
|
270
|
+
<?= Deck::css() ?>
|
|
271
|
+
<?= Deck::js() ?>
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
Per-tenant theming, which is the thing Tailwind needs a rebuild for:
|
|
275
|
+
|
|
276
|
+
```php
|
|
277
|
+
<html <?= Deck::theme(hue: $tenant->brand_hue, mode: $user->theme) ?>>
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
One inline style. No second stylesheet, no rebuild, no per-customer asset pipeline.
|
|
281
|
+
|
|
282
|
+
### Building from source
|
|
283
|
+
|
|
284
|
+
```bash
|
|
285
|
+
node build.mjs
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
The build script requires nothing. If esbuild happens to be installed it is used for
|
|
289
|
+
minification because it is better at it; otherwise a conservative built-in minifier runs
|
|
290
|
+
that walks the file character by character so strings, `url()` values, and data URIs are
|
|
291
|
+
never touched. The build never depends on a toolchain being present, which is the same
|
|
292
|
+
promise the framework makes.
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
## Theming
|
|
296
|
+
|
|
297
|
+
Every color in the framework derives from six hue numbers. Change one line and the
|
|
298
|
+
buttons, links, focus rings, badges, tab bar, and shadows all follow:
|
|
299
|
+
|
|
300
|
+
```css
|
|
301
|
+
:root {
|
|
302
|
+
--hue-brand: 265; /* violet instead of harbor teal */
|
|
303
|
+
--chroma-brand: .14; /* more saturated */
|
|
304
|
+
}
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
Per-tenant theming in a multi-tenant Keel app becomes a single inline style on
|
|
308
|
+
`<html>` — no rebuild, no separate stylesheet per customer.
|
|
309
|
+
|
|
310
|
+
Dark mode is automatic from the OS. To force it, set `data-theme="dark"` or
|
|
311
|
+
`data-theme="light"` on `<html>`.
|
|
312
|
+
|
|
313
|
+
## Layers
|
|
314
|
+
|
|
315
|
+
Deck declares its cascade layers up front:
|
|
316
|
+
|
|
317
|
+
```
|
|
318
|
+
deck.reset, deck.tokens, deck.type, deck.layout, deck.components, deck.mobile, deck.utilities
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
Any CSS you write outside a layer beats all of them, so you override Deck by writing
|
|
322
|
+
a normal rule. No `!important`, no specificity arms race.
|
|
323
|
+
|
|
324
|
+
## Icons
|
|
325
|
+
|
|
326
|
+
```html
|
|
327
|
+
<svg class="icon"><use href="/assets/deck-icons.svg#check"></use></svg>
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
Icons inherit `color` and scale with `font-size`, so they sit on the text baseline.
|
|
331
|
+
Sizes: `.icon-sm` `.icon` `.icon-lg` `.icon-xl`.
|
|
332
|
+
|
|
333
|
+
Set: check, check-double, x, plus, minus, chevron-down/up/left/right, arrow-right,
|
|
334
|
+
arrow-left, arrow-up-right, search, menu, more-horizontal, more-vertical, filter,
|
|
335
|
+
sort, refresh, home, grid, list, chart, trend-up, trend-down, user, users, settings,
|
|
336
|
+
log-out, bell, mail, phone, message, calendar, clock, file, folder, clipboard,
|
|
337
|
+
download, upload, trash, edit, copy, link, external, tag, image, camera, eye,
|
|
338
|
+
eye-off, lock, unlock, shield, star, heart, bookmark, info, alert-circle,
|
|
339
|
+
alert-triangle, check-circle, x-circle, help, credit-card, dollar, receipt, car,
|
|
340
|
+
truck, wrench, gauge, sun, moon, map-pin, send, sparkle.
|
|
341
|
+
|
|
342
|
+
### Two cuts, not one stroke width
|
|
343
|
+
|
|
344
|
+
Every icon ships twice:
|
|
345
|
+
|
|
346
|
+
| Symbol | Weight | Use with |
|
|
347
|
+
| --- | --- | --- |
|
|
348
|
+
| `#check` | wght 400 | `.icon`, `.icon-lg`, `.icon-xl` |
|
|
349
|
+
| `#check-sm` | wght 500 | `.icon-sm` |
|
|
350
|
+
|
|
351
|
+
```html
|
|
352
|
+
<svg class="icon icon-sm"><use href="/assets/deck-icons.svg#check-sm"></use></svg>
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
The symbols are filled outlines, so `stroke-width` does nothing to them and `.icon-sm`
|
|
356
|
+
cannot thicken its way to legibility at 16px. The heavier cut is a real second drawing
|
|
357
|
+
instead. CSS cannot rewrite a `<use href>`, so the pairing has to live in the markup —
|
|
358
|
+
`.icon-sm` goes with the `-sm` symbol. Everything else takes the plain name.
|
|
359
|
+
|
|
360
|
+
Both cuts carry `fill="currentColor" stroke="none"`, which beats the `.icon` stroke
|
|
361
|
+
defaults by inheritance, so icons follow `color` and retune with `--hue-brand` exactly
|
|
362
|
+
as they always did.
|
|
363
|
+
|
|
364
|
+
### Regenerating the sprite
|
|
365
|
+
|
|
366
|
+
`src/deck-icons.svg` is **generated**. Editing it by hand loses the edit on the next run.
|
|
367
|
+
|
|
368
|
+
```
|
|
369
|
+
npm run icons
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
That reads `tools/icons/icons.txt` — one `sprite-id = material-glyph-name` per line —
|
|
373
|
+
out of the Material Symbols Rounded variable font and writes both cuts of every listed
|
|
374
|
+
icon. To add, drop or swap an icon, edit that list and rerun. Any of the 4,025 glyphs in
|
|
375
|
+
the font is available; only the names on the list end up in the sprite, which is how a
|
|
376
|
+
4,025 icon library ships as a 75 icon file — or a twelve icon one at 5.7 KB Brotli, if
|
|
377
|
+
that is all your project uses.
|
|
378
|
+
|
|
379
|
+
The font is a **build time source only**. It never reaches a browser: a webfont would be
|
|
380
|
+
a 15 MB download or a subsetting step, and Deck's whole premise is not having a build
|
|
381
|
+
step. It lives in `tools/`, which is excluded from the npm package (`files` in
|
|
382
|
+
`package.json`) and from GitHub source archives (`export-ignore` in `.gitattributes`).
|
|
383
|
+
Nobody installing Deck needs it — the generated sprite is committed.
|
|
384
|
+
|
|
385
|
+
For the same reason `npm run icons` is deliberately **not** part of `npm run build`. It
|
|
386
|
+
is a maintainer task, run on purpose:
|
|
387
|
+
|
|
388
|
+
```
|
|
389
|
+
npm run icons # regenerate src/deck-icons.svg from the font
|
|
390
|
+
npm run build # everything else, needs no font
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
If the font is missing, the script says so and stops. Download Material Symbols Rounded
|
|
394
|
+
from [fonts.google.com/icons](https://fonts.google.com/icons) and unzip it into
|
|
395
|
+
`tools/icons/`.
|
|
396
|
+
|
|
397
|
+
`#deck-mark` and `#deck-wordmark` are not in the font. They are Deck's own drawings,
|
|
398
|
+
marked `@hand` in the list, read out of the existing sprite and carried through verbatim;
|
|
399
|
+
the script fails rather than regenerate a sprite without them.
|
|
400
|
+
|
|
401
|
+
### Attribution
|
|
402
|
+
|
|
403
|
+
The icon outlines are derived from **Material Symbols** by Google, used under the
|
|
404
|
+
[Apache License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0). The full
|
|
405
|
+
licence text is kept at `tools/icons/LICENSE.txt` and the notice is reproduced in the
|
|
406
|
+
header comment of `src/deck-icons.svg`, which is the file that actually ships — keep it
|
|
407
|
+
there. The two brand marks are not derived from the font and are not covered by it.
|
|
408
|
+
|
|
409
|
+
Deck's own code and stylesheets remain MIT, per `LICENSE`.
|
|
410
|
+
|
|
411
|
+
## Logo
|
|
412
|
+
|
|
413
|
+
<picture>
|
|
414
|
+
<source media="(prefers-color-scheme: dark)" srcset="src/brand/deck-logo-dark.svg">
|
|
415
|
+
<img alt="Deck" src="src/brand/deck-logo-light.svg" width="300">
|
|
416
|
+
</picture>
|
|
417
|
+
|
|
418
|
+
The mark is three planks in perspective — a deck of cards, a deck of layers, the thing
|
|
419
|
+
the framework is named after. It is drawn on the same 24×24 grid as the icons and it
|
|
420
|
+
lives in the same sprite.
|
|
421
|
+
|
|
422
|
+
### Which copy to use
|
|
423
|
+
|
|
424
|
+
This is the one rule that matters, and it is easy to get wrong:
|
|
425
|
+
|
|
426
|
+
| You want | Use | Because |
|
|
427
|
+
| --- | --- | --- |
|
|
428
|
+
| A logo that follows `--hue-brand` | `<use href="deck-icons.svg#deck-mark">` | A `<use>` against a sprite in the same document inherits `color` |
|
|
429
|
+
| A logo in an `<img>`, a `<link>`, or `og:image` | a file from `src/brand/` | An externally referenced SVG has no colour context, so `currentColor` resolves to black |
|
|
430
|
+
|
|
431
|
+
```html
|
|
432
|
+
<!-- retunes with the palette -->
|
|
433
|
+
<svg class="icon icon-lg icon-fill" style="color: var(--brand)">
|
|
434
|
+
<use href="/assets/deck-icons.svg#deck-mark"></use>
|
|
435
|
+
</svg>
|
|
436
|
+
|
|
437
|
+
<!-- static, for a favicon or a social card -->
|
|
438
|
+
<link rel="icon" href="/assets/images/deck-mark.svg" type="image/svg+xml">
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
Note `.icon-fill`. The brand marks are filled, not stroked, so the `.icon` stroke
|
|
442
|
+
defaults draw them as hollow outlines without it. `#deck-wordmark` is the lettering on
|
|
443
|
+
its own, at `0 0 91.81 24`, for when you are setting the lockup yourself.
|
|
444
|
+
|
|
445
|
+
### The favicon is a different drawing
|
|
446
|
+
|
|
447
|
+
`deck-mark.svg` is the mark alone, never the lockup — a 124×24 lockup is a smear at
|
|
448
|
+
16px. It carries its own `prefers-color-scheme` block, so it follows the browser chrome
|
|
449
|
+
instead of picking a side and disappearing in the other one.
|
|
450
|
+
|
|
451
|
+
### Family
|
|
452
|
+
|
|
453
|
+
Two files are drawn by hand; `tools/make-brand.mjs` derives the rest from them.
|
|
454
|
+
|
|
455
|
+
| File | Drawn or derived | What it is |
|
|
456
|
+
| --- | --- | --- |
|
|
457
|
+
| `deck-logo.svg` | drawn | Horizontal lockup, `currentColor`, `0 0 123.81 24` |
|
|
458
|
+
| `deck-logo-stacked.svg` | drawn | Mark over wordmark, `currentColor`, `0 0 91.81 54` |
|
|
459
|
+
| `deck-logo-light.svg` | from the lockup | Explicit dark fill, for light backgrounds |
|
|
460
|
+
| `deck-logo-dark.svg` | from the lockup | Explicit light fill, for dark backgrounds |
|
|
461
|
+
| `deck-mark.svg` | from `#deck-mark` | Mark only, theme-aware, for `rel="icon"` |
|
|
462
|
+
| `deck-og.png` | from the lockup | 1200×630 social card, white on the brand teal |
|
|
463
|
+
| `deck-apple-touch-icon.png` | from `#deck-mark` | 180×180, for `rel="apple-touch-icon"` |
|
|
464
|
+
|
|
465
|
+
`npm run demo` publishes all of them to `public_html/assets/images/`, which is
|
|
466
|
+
gitignored for the same reason `assets/deck/` is: it is a copy, not a source.
|
|
467
|
+
|
|
468
|
+
### Regenerating
|
|
469
|
+
|
|
470
|
+
```bash
|
|
471
|
+
npm run brand # after changing the mark
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
The two PNGs cannot be produced in-process — rasterising needs a renderer — so the
|
|
475
|
+
generator shells out to headless Chrome or Edge via its `--screenshot` flag, and the
|
|
476
|
+
results are committed. Deck itself stays dependency-free; the browser is only needed
|
|
477
|
+
when the drawing changes, not to build or use the framework.
|
|
478
|
+
|
|
479
|
+
Because those rasters are committed, they can go stale. `tools/make-brand.mjs` records
|
|
480
|
+
a hash of every master it read in `src/brand/sources.json`, and `build.mjs` recomputes
|
|
481
|
+
them and **fails the build** if one has moved:
|
|
482
|
+
|
|
483
|
+
```
|
|
484
|
+
Brand assets are stale. These masters have changed since
|
|
485
|
+
tools/make-brand.mjs last ran:
|
|
486
|
+
|
|
487
|
+
src/brand/deck-logo.svg recorded b44a710d0269aada, now a81ac26bc87ec010
|
|
488
|
+
|
|
489
|
+
Run: node tools/make-brand.mjs
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
Hashes rather than timestamps, because a fresh clone gives every file the same mtime.
|
|
493
|
+
|
|
494
|
+
## Emoji
|
|
495
|
+
|
|
496
|
+
`.emoji` pins the emoji font stack and the baseline so they render consistently on
|
|
497
|
+
Windows, iOS, and Android. Also `.emoji-lg`, `.emoji-xl`, `.emoji-hero`,
|
|
498
|
+
`.emoji-tile`, `.emoji-grid`, `.reaction`.
|
|
499
|
+
|
|
500
|
+
## Layout primitives
|
|
501
|
+
|
|
502
|
+
`.container` `.stack` `.cluster` `.bar` `.grid` `.split` `.center` `.section`
|
|
503
|
+
`.scroller` `.sticky-top` `.app-shell` `.cq`
|
|
504
|
+
|
|
505
|
+
`.grid` auto-fits by content width, so most layouts need no breakpoints at all.
|
|
506
|
+
|
|
507
|
+
## Components
|
|
508
|
+
|
|
509
|
+
Buttons, forms (input, textarea, select, check, radio, switch, range, file, input
|
|
510
|
+
group, search, fieldset), card, panel, badge, chip, alert, avatar, table (restacks
|
|
511
|
+
below 640px), list rows, tabs, segmented control, accordion, breadcrumb, pagination,
|
|
512
|
+
progress, ring, spinner, skeleton, tooltip, menu, modal, bottom sheet, toast, empty
|
|
513
|
+
state, stat, timeline, navbar, sidebar, tab bar, FAB.
|
|
514
|
+
|
|
515
|
+
## Mobile specifics
|
|
516
|
+
|
|
517
|
+
- Every interactive control clears a 44px touch target
|
|
518
|
+
- Inputs render at 16px on coarse pointers, so iOS never zooms on focus
|
|
519
|
+
- `env(safe-area-inset-*)` handled on the tab bar, FAB, sticky form bar, and sheets
|
|
520
|
+
- `100dvh` instead of `100vh`, so the URL bar doesn't cut off the last row
|
|
521
|
+
- Bottom sheet on a phone becomes a centered dialog at 640px and up
|
|
522
|
+
|
|
523
|
+
## Browser support
|
|
524
|
+
|
|
525
|
+
Chrome/Edge 117+, Safari 17.4+, Firefox 128+. Deck uses `oklch()`, `light-dark()`,
|
|
526
|
+
`@layer`, `:has()`, `@starting-style`, popover, and `field-sizing`. Older browsers
|
|
527
|
+
still get a usable page — they lose the entry animations and auto-growing textareas,
|
|
528
|
+
not the layout.
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
## Date picker
|
|
532
|
+
|
|
533
|
+
```html
|
|
534
|
+
<div class="datefield" data-deck-datepicker data-mode="range" data-months="2" data-presets>
|
|
535
|
+
<input class="input" name="period">
|
|
536
|
+
</div>
|
|
537
|
+
```
|
|
538
|
+
|
|
539
|
+
Attributes: `data-mode="single|range"`, `data-format="mdy|dmy|iso"`, `data-min`,
|
|
540
|
+
`data-max` (ISO dates), `data-months`, `data-week-start`, `data-presets`.
|
|
541
|
+
|
|
542
|
+
Fires `deck:change` on the input with `{ start, end }` as ISO strings. Under 480px
|
|
543
|
+
the panel becomes a bottom sheet.
|
|
544
|
+
|
|
545
|
+
## Combobox
|
|
546
|
+
|
|
547
|
+
```html
|
|
548
|
+
<div class="combo" data-deck-combo data-multi data-create data-placeholder="Add people">
|
|
549
|
+
<select name="assignees[]" multiple hidden>
|
|
550
|
+
<option value="rissa" selected>Rissa Molina</option>
|
|
551
|
+
<option value="ken" data-sub="Fixed ops" data-group="Managers">Ken Spence</option>
|
|
552
|
+
</select>
|
|
553
|
+
</div>
|
|
554
|
+
```
|
|
555
|
+
|
|
556
|
+
The real `<select>` stays in the DOM and stays in sync, so a normal PHP form post
|
|
557
|
+
works with nothing extra on the server. `data-sub` adds a second line, `data-group`
|
|
558
|
+
groups options, `data-create` allows adding new values, `data-multi` gives tokens.
|
|
559
|
+
|
|
560
|
+
For a remote source, set `data-url="/api/repos?q="` — deck.js appends the query,
|
|
561
|
+
debounces (`data-debounce`, default 220ms), and expects JSON rows of
|
|
562
|
+
`{ value, label, sub, group, disabled }`. Use `data-min-chars` to hold off until
|
|
563
|
+
the user has typed enough.
|
|
564
|
+
|
|
565
|
+
Events: `deck:change` with `{ values }`, `deck:create` with `{ value }`.
|
|
566
|
+
|
|
567
|
+
## Data grid
|
|
568
|
+
|
|
569
|
+
```html
|
|
570
|
+
<div class="dg-wrap" data-deck-grid style="--dg-height:360px">
|
|
571
|
+
<table class="dg dg-zebra">
|
|
572
|
+
<thead><tr>
|
|
573
|
+
<th class="dg-check dg-pin-start">…</th>
|
|
574
|
+
<th class="dg-pin-start-2" data-sort="text" data-resize>Order</th>
|
|
575
|
+
<th class="dg-num" data-sort="num">Total</th>
|
|
576
|
+
<th class="dg-actions dg-pin-end"></th>
|
|
577
|
+
</tr></thead>
|
|
578
|
+
```
|
|
579
|
+
|
|
580
|
+
- `dg-pin-start` / `dg-pin-start-2` / `dg-pin-end` freeze columns. The drop shadow
|
|
581
|
+
only appears once the grid is actually scrolled sideways.
|
|
582
|
+
- `data-sort="text|num|date"` makes a header sortable. Put `data-value` on a cell
|
|
583
|
+
when the display text isn't sortable (formatted currency, relative dates).
|
|
584
|
+
- `data-resize` adds a drag grip to a column.
|
|
585
|
+
- `dg-compact` / `dg-comfy` change row density; `dg-zebra` adds striping.
|
|
586
|
+
- `tfoot` sticks to the bottom for totals.
|
|
587
|
+
- `dg-cards` plus `data-label` on each `td` restacks the grid into cards below 44rem.
|
|
588
|
+
|
|
589
|
+
Events: `deck:sort`, `deck:select`.
|
|
590
|
+
|
|
591
|
+
## Toasts
|
|
592
|
+
|
|
593
|
+
```js
|
|
594
|
+
Deck.toast('Deploy succeeded');
|
|
595
|
+
|
|
596
|
+
Deck.toast({
|
|
597
|
+
kind: 'warn', // good | warn | bad | info | loading | ''
|
|
598
|
+
title: 'Project archived',
|
|
599
|
+
text: 'You can undo this.',
|
|
600
|
+
duration: 8000, // 0 keeps it until dismissed
|
|
601
|
+
actions: [{ label: 'Undo', onClick: () => restore() }]
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
const t = Deck.toast({ kind: 'loading', title: 'Submitting…', duration: 0 });
|
|
605
|
+
t.update({ kind: 'good', title: 'Submitted', duration: 4000 });
|
|
606
|
+
t.dismiss();
|
|
607
|
+
|
|
608
|
+
Deck.toasts.clear();
|
|
609
|
+
```
|
|
610
|
+
|
|
611
|
+
Toasts stack rather than stringing down the screen. Hovering the stack fans it out
|
|
612
|
+
and pauses every timer. Drag or swipe one sideways to dismiss. Position the region
|
|
613
|
+
with `.toast-region-start`, `.toast-region-center`, or `.toast-region-top`.
|
|
614
|
+
|
|
615
|
+
## Charts
|
|
616
|
+
|
|
617
|
+
Bars and donuts are CSS driven by `--value` (0–100). Lines are inline SVG you style
|
|
618
|
+
with classes. Series colors `s1`–`s6` are derived from `--hue-brand`, so charts
|
|
619
|
+
retheme with everything else.
|
|
620
|
+
|
|
621
|
+
```html
|
|
622
|
+
<div class="chart-columns">
|
|
623
|
+
<div class="chart-col s1" style="--value:73" data-label="Jun" data-value="146"></div>
|
|
624
|
+
</div>
|
|
625
|
+
|
|
626
|
+
<div class="chart-bar">
|
|
627
|
+
<span class="chart-bar-label">Cooler line</span>
|
|
628
|
+
<span class="chart-bar-track"><span class="chart-bar-fill s1" style="--value:92"></span></span>
|
|
629
|
+
<span class="chart-bar-value">92</span>
|
|
630
|
+
</div>
|
|
631
|
+
|
|
632
|
+
<div class="donut" style="--stops: var(--c1) 0 62%, var(--c3) 62% 84%, var(--c4) 84% 100%"></div>
|
|
633
|
+
|
|
634
|
+
<svg class="chart-svg" viewBox="0 0 300 120" preserveAspectRatio="none">
|
|
635
|
+
<path class="chart-area s1" d="…"/>
|
|
636
|
+
<path class="chart-line s1" d="…"/>
|
|
637
|
+
</svg>
|
|
638
|
+
```
|
|
639
|
+
|
|
640
|
+
Also: `.chart-col-stack`, `.chart-group`, `.chart-meter`, `.chart-heat`,
|
|
641
|
+
`.sparkline`, `.chart-legend`, `.chart-x`, `.chart-y`, `.chart-gridline`.
|
|
642
|
+
|
|
643
|
+
## Print
|
|
644
|
+
|
|
645
|
+
Printing is handled in the `deck.print` layer. Nav, tab bar, buttons, toasts,
|
|
646
|
+
pickers, and menus drop out. The grid unfreezes and prints every column with the
|
|
647
|
+
header repeated on each page. Mobile card fallbacks revert to real tables. Dark mode
|
|
648
|
+
is forced back to light. External link targets are printed in parentheses.
|
|
649
|
+
|
|
650
|
+
Helpers: `.page-break`, `.page-break-after`, `.keep-together`, `.no-print`,
|
|
651
|
+
`.print-only`, `.print-keep` (for a button you do want on paper), `.no-print-url`
|
|
652
|
+
(suppress the printed href), `.print-header`, `.print-footer`.
|
|
653
|
+
|
|
654
|
+
Change the paper size in one place:
|
|
655
|
+
|
|
656
|
+
```css
|
|
657
|
+
@page { size: A4; margin: 18mm 15mm; }
|
|
658
|
+
```
|
|
659
|
+
|
|
660
|
+
## JS API
|
|
661
|
+
|
|
662
|
+
```js
|
|
663
|
+
Deck.init(container) // wire up anything with data-deck-* inside container
|
|
664
|
+
Deck.toast(opts) // returns { update, dismiss }
|
|
665
|
+
Deck.toasts.clear()
|
|
666
|
+
Deck.theme('dark') // 'light' | 'dark', persisted to localStorage
|
|
667
|
+
Deck.theme() // read current
|
|
668
|
+
Deck.hue(265) // retint the whole app at runtime
|
|
669
|
+
Deck.iconSprite // path to deck-icons.svg
|
|
670
|
+
```
|
|
671
|
+
|
|
672
|
+
|
|
673
|
+
## Motion
|
|
674
|
+
|
|
675
|
+
Nothing animates unless you ask for it by class. Everything is wrapped in
|
|
676
|
+
`prefers-reduced-motion: no-preference`, with one deliberate exception: spinners,
|
|
677
|
+
skeletons, and progress bars keep moving under reduced motion, just slower. A frozen
|
|
678
|
+
spinner reads as broken, and progress feedback is information rather than decoration.
|
|
679
|
+
|
|
680
|
+
### Transition utilities
|
|
681
|
+
|
|
682
|
+
`.transition` `.transition-colors` `.transition-move` `.transition-size`
|
|
683
|
+
`.transition-opacity`, sized with `.dur-1` through `.dur-5`, timed with `.ease-out`
|
|
684
|
+
`.ease-in` `.ease-spring` `.ease-bounce` `.ease-overshoot` `.ease-linear`, offset with
|
|
685
|
+
`.delay-1` `.delay-2` `.delay-3`. `.no-motion` opts a single element out.
|
|
686
|
+
|
|
687
|
+
The bounce and overshoot easings are `linear()` springs, so you get a real spring
|
|
688
|
+
curve with no physics library.
|
|
689
|
+
|
|
690
|
+
### Entrances
|
|
691
|
+
|
|
692
|
+
`.enter` `.enter-rise` `.enter-drop` `.enter-start` `.enter-end` `.enter-pop`
|
|
693
|
+
`.enter-blur`. Put `.stagger` on the parent and children sequence in; the first twelve
|
|
694
|
+
are pure CSS and deck.js sets the index past that. `--stagger-step` controls the gap,
|
|
695
|
+
`--travel` controls how far things move.
|
|
696
|
+
|
|
697
|
+
### Scroll reveals
|
|
698
|
+
|
|
699
|
+
```html
|
|
700
|
+
<div class="card reveal">…</div>
|
|
701
|
+
<div class="scroll-progress"></div>
|
|
702
|
+
```
|
|
703
|
+
|
|
704
|
+
`.reveal` `.reveal-fade` `.reveal-pop` use `animation-timeline: view()`, so the
|
|
705
|
+
animation is tied to scroll position with no IntersectionObserver at all. deck.js adds
|
|
706
|
+
an observer fallback for browsers that don't support it yet. `.scroll-progress` is a
|
|
707
|
+
reading-progress bar driven by `scroll(root block)`, and `.shrink-on-scroll` condenses
|
|
708
|
+
a sticky header past 120px.
|
|
709
|
+
|
|
710
|
+
### Attention
|
|
711
|
+
|
|
712
|
+
`.shake` `.flash` `.flash-good` `.pulse` `.ping` `.nudge`. A `.field.is-invalid`
|
|
713
|
+
shakes once on its own and won't repeat, and deck.js clears the state as soon as the
|
|
714
|
+
input becomes valid.
|
|
715
|
+
|
|
716
|
+
### Micro-interactions
|
|
717
|
+
|
|
718
|
+
`.lift` `.press` `.sweep` (underline draws in), `.icon-follow` (arrow steps forward
|
|
719
|
+
when its button is hovered), and `.ripple` — add the class and deck.js handles the ink
|
|
720
|
+
from the pointer position.
|
|
721
|
+
|
|
722
|
+
### Expand and collapse
|
|
723
|
+
|
|
724
|
+
```js
|
|
725
|
+
Deck.toggle(panel);
|
|
726
|
+
```
|
|
727
|
+
|
|
728
|
+
A real `height: auto` transition using `interpolate-size`. No measuring in JavaScript,
|
|
729
|
+
no `max-height` guess that clips long content.
|
|
730
|
+
|
|
731
|
+
### View transitions
|
|
732
|
+
|
|
733
|
+
This is the one that matters most for Keel. Add this to your app CSS:
|
|
734
|
+
|
|
735
|
+
```css
|
|
736
|
+
@view-transition { navigation: auto; }
|
|
737
|
+
```
|
|
738
|
+
|
|
739
|
+
Full page loads in a plain PHP multi-page app now cross-fade like a single page app —
|
|
740
|
+
no router, no JavaScript, no client-side rendering. Deck styles what the browser
|
|
741
|
+
generates: content moves, and the header and tab bar hold still.
|
|
742
|
+
|
|
743
|
+
Give the same `view-transition-name` to matching elements on both pages and the
|
|
744
|
+
browser tweens between them — a row in a list morphing into a detail page header:
|
|
745
|
+
|
|
746
|
+
```html
|
|
747
|
+
<!-- list page --> <tr style="view-transition-name: order-1042">
|
|
748
|
+
<!-- detail page --> <h1 style="view-transition-name: order-1042">
|
|
749
|
+
```
|
|
750
|
+
|
|
751
|
+
Helpers: `.vt-header` `.vt-main` `.vt-tabbar`, and `.vt-hold` with `--vt` for a
|
|
752
|
+
dynamic name. Back navigations slide the other way; deck.js sets the direction on
|
|
753
|
+
popstate.
|
|
754
|
+
|
|
755
|
+
For same-page DOM changes, wrap the update:
|
|
756
|
+
|
|
757
|
+
```js
|
|
758
|
+
Deck.transition(() => row.remove());
|
|
759
|
+
Deck.transition(() => list.prepend(newRow), { direction: 'back' });
|
|
760
|
+
```
|
|
761
|
+
|
|
762
|
+
It falls back to running the change immediately where unsupported or where the person
|
|
763
|
+
asked for reduced motion.
|
|
764
|
+
|
|
765
|
+
### Ticker
|
|
766
|
+
|
|
767
|
+
`.marquee` with two identical `.marquee-track` children scrolls a status strip —
|
|
768
|
+
recalls, backordered parts, campaign notices. It pauses on hover and the duration is
|
|
769
|
+
`--marquee-dur`.
|
|
770
|
+
|
|
771
|
+
### JS additions
|
|
772
|
+
|
|
773
|
+
```js
|
|
774
|
+
Deck.play(node, 'shake') // one-shot class, cleans up after itself, returns a promise
|
|
775
|
+
Deck.toggle(node, force) // height:auto expand/collapse
|
|
776
|
+
Deck.transition(fn, opts) // view-transition wrapper with fallback
|
|
777
|
+
Deck.reduced() // true when the person asked for reduced motion
|
|
778
|
+
```
|
|
779
|
+
|
|
780
|
+
Mark a number with `data-deck-tick` and it animates up green or down red whenever its
|
|
781
|
+
text changes.
|
|
782
|
+
|
|
783
|
+
|
|
784
|
+
## Cascade layers
|
|
785
|
+
|
|
786
|
+
The whole cascade contract lives in `00-layers.css`, declared before any rule exists.
|
|
787
|
+
Order is decided there — not by file order, not by specificity, never by `!important`.
|
|
788
|
+
|
|
789
|
+
```css
|
|
790
|
+
@layer
|
|
791
|
+
deck.reset, deck.tokens, deck.type, deck.layout,
|
|
792
|
+
deck.components, deck.mobile, deck.motion, deck.effects,
|
|
793
|
+
deck.utilities, deck.rtl, deck.print,
|
|
794
|
+
|
|
795
|
+
app.base, app.components, app.pages, app.overrides;
|
|
796
|
+
```
|
|
797
|
+
|
|
798
|
+
Four `app.*` layers are reserved and left empty for you. A rule in `app.pages` beats
|
|
799
|
+
every Deck rule with a single class selector — no `.page .card .btn` chains, no
|
|
800
|
+
escalation. Anything you write **outside** a layer beats all layers, so a one-off rule
|
|
801
|
+
in a Keel view template always wins.
|
|
802
|
+
|
|
803
|
+
Wrap vendor CSS so it stops fighting you:
|
|
804
|
+
|
|
805
|
+
```css
|
|
806
|
+
@import url("vendor/thing.css") layer(vendor);
|
|
807
|
+
```
|
|
808
|
+
|
|
809
|
+
`00-layers.css` also registers the typed custom properties (`@property`) that make
|
|
810
|
+
angles, colors, and lengths interpolable — that's what lets a gradient angle or a tilt
|
|
811
|
+
animate at all. Registered: `--g-angle`, `--g-from`, `--g-to`, `--g-stop`, `--sheen`,
|
|
812
|
+
`--tilt-x`, `--tilt-y`, `--depth`.
|
|
813
|
+
|
|
814
|
+
## Container queries
|
|
815
|
+
|
|
816
|
+
Deck already used `container-type` for the `.cq` helper; this is the full set.
|
|
817
|
+
|
|
818
|
+
```html
|
|
819
|
+
<div class="cq">
|
|
820
|
+
<article class="card card-flex">…</article>
|
|
821
|
+
</div>
|
|
822
|
+
```
|
|
823
|
+
|
|
824
|
+
The same markup goes horizontal in a wide column and stays stacked in a narrow rail,
|
|
825
|
+
without either one knowing where it was placed.
|
|
826
|
+
|
|
827
|
+
- **Declaring:** `.cq`, `.cq-size`, and named containers `.cq-panel` `.cq-pane`
|
|
828
|
+
`.cq-row` `.cq-shell`.
|
|
829
|
+
- **Container units:** `.text-cq` `.display-cq` `.pad-cq` `.gap-cq` scale with `cqi`,
|
|
830
|
+
so a heading in a sidebar stays small on a 32-inch monitor.
|
|
831
|
+
- **Adaptive components:** `.card-flex` `.stat-cq` `.metarow` `.field-row-cq`
|
|
832
|
+
`.actions-cq` `.dg-cq`.
|
|
833
|
+
- **Breakpoint utilities:** `.cq-sm\:row` `.cq-md\:hidden` `.cq-lg\:grid-2` and so on.
|
|
834
|
+
|
|
835
|
+
`.field-row-cq` and `.dg-cq` are strictly better than their media-query versions: a
|
|
836
|
+
two-up field row or a data grid inside a modal or sheet is narrow no matter how wide
|
|
837
|
+
the screen is.
|
|
838
|
+
|
|
839
|
+
**Style queries.** Set `--tone` on a container and children adapt with no extra classes:
|
|
840
|
+
|
|
841
|
+
```html
|
|
842
|
+
<div class="cq-tone" style="--tone: critical">
|
|
843
|
+
<div class="card tone-surface"><span class="tone-text">Out of coverage</span></div>
|
|
844
|
+
</div>
|
|
845
|
+
```
|
|
846
|
+
|
|
847
|
+
Tones: `clear`, `caution`, `critical`. Where style queries aren't supported the
|
|
848
|
+
fallback is simply no change.
|
|
849
|
+
|
|
850
|
+
## Logical properties and RTL
|
|
851
|
+
|
|
852
|
+
Deck is written in logical properties end to end — `inline-size`, `block-size`,
|
|
853
|
+
`inset-inline-start`, `padding-block`, `border-start-start-radius`. A full right-to-left
|
|
854
|
+
flip needs nothing but `dir="rtl"` on `<html>`:
|
|
855
|
+
|
|
856
|
+
```js
|
|
857
|
+
Deck.dir('rtl');
|
|
858
|
+
```
|
|
859
|
+
|
|
860
|
+
What logical properties can't fix by themselves is content, so `19-logical.css` handles
|
|
861
|
+
the rest in the `deck.rtl` layer: pointing icons mirror (chevrons, arrows, send, log-out)
|
|
862
|
+
while checkmarks, wrenches, and clocks don't; the select arrow, search icon, switch knob,
|
|
863
|
+
grid pin shadows, chart fills, marquee, and entrance animations all flip; and the
|
|
864
|
+
breadcrumb separator swaps.
|
|
865
|
+
|
|
866
|
+
- **Explicit direction:** `.dir-ltr` `.dir-rtl` `.bidi-isolate` `.bidi-plaintext`.
|
|
867
|
+
`.code-ltr`, `.mono`, `code`, and `.nums` are isolated by default — an identifier like
|
|
868
|
+
a commit hash or an order number reads left to right in every language and must not
|
|
869
|
+
scramble the text around it. Reach for `.code-ltr` when you need to force the direction
|
|
870
|
+
on something that is not already monospace.
|
|
871
|
+
- **Mirroring control:** `.mirror-rtl` to mirror, `.no-flip` to never mirror.
|
|
872
|
+
- **Logical utilities:** `.mis-*` `.mie-*` `.pis-*` `.pie-*` `.bis` `.bie`
|
|
873
|
+
`.inset-is-0` `.r-start` `.r-end`. For the block axis reach for `.mt-*` and `.mb-*`,
|
|
874
|
+
which are named after the physical edge but declare `margin-block-start` and
|
|
875
|
+
`margin-block-end`, and for full sizes `.w-full` and `.h-full`, which declare
|
|
876
|
+
`inline-size` and `block-size`. Deck used to ship logically-named spellings of those
|
|
877
|
+
as well; two names for one declaration is worse than one name that needs a sentence.
|
|
878
|
+
- **Writing modes:** `.writing-vertical` `.writing-upright` `.writing-sideways`, and
|
|
879
|
+
`.th-vertical` for a rotated column header that still measures correctly.
|
|
880
|
+
|
|
881
|
+
## Gradients
|
|
882
|
+
|
|
883
|
+
Every gradient derives from `--hue-brand` and interpolates in oklab, which avoids the
|
|
884
|
+
grey dead zone sRGB produces when blending two saturated colors. The hue slider retunes
|
|
885
|
+
all of them.
|
|
886
|
+
|
|
887
|
+
- **Surfaces:** `.g-surface` `.g-sunken` `.g-brand` `.g-brand-soft` `.g-dark`
|
|
888
|
+
- **Mesh:** `.g-mesh` `.g-mesh-subtle` `.g-mesh-drift` — three soft radial blooms, no
|
|
889
|
+
image, no SVG filter, and no `blur()` over a large area, which is expensive.
|
|
890
|
+
- **Text:** `.g-text` `.g-text-shine` — clipped to the glyphs with a real color
|
|
891
|
+
underneath so the text survives if the clip fails.
|
|
892
|
+
- **Borders:** `.g-border` `.g-border-soft` `.g-border-spin` — two clip boxes rather
|
|
893
|
+
than `border-image`, so it works with any `border-radius`. The spin animates because
|
|
894
|
+
`--g-angle` is registered.
|
|
895
|
+
- **Scrims:** `.g-scrim` `.g-scrim-top` — an eased floor under a caption, instead of a
|
|
896
|
+
flat overlay that dulls the whole image.
|
|
897
|
+
- **Fade masks:** `.g-fade-inline` `.g-fade-end` `.g-fade-block` `.g-fade-more` — for
|
|
898
|
+
content that runs off an edge or is collapsed.
|
|
899
|
+
- **Sheen:** `.g-sheen` — a highlight sweeping on hover, driven by the registered
|
|
900
|
+
`--sheen` percentage so it eases instead of jumping.
|
|
901
|
+
- **Patterns:** `.g-grid-lines` `.g-dots` `.g-stripes` `.g-hatch` — gradients standing
|
|
902
|
+
in for images, so they cost nothing to download and retint automatically.
|
|
903
|
+
- **Status and accents:** `.g-good` `.g-warn` `.g-bad` `.g-conic` `.g-ring`
|
|
904
|
+
`.g-ring-spin` `.g-shimmer` `.g-bar-fill`
|
|
905
|
+
|
|
906
|
+
`.chart-area-g` expects an SVG gradient def with `id="deck-area-gradient"` in the
|
|
907
|
+
document. Engines without `oklch()` fall back to the flat brand color rather than a
|
|
908
|
+
broken gradient.
|
|
909
|
+
|
|
910
|
+
## 3D transforms
|
|
911
|
+
|
|
912
|
+
Depth when it carries meaning: a card with two sides, a stack-depth you're working down
|
|
913
|
+
through, a control that physically depresses.
|
|
914
|
+
|
|
915
|
+
Everything uses `rotate`, `translate`, and `scale` as **individual properties** rather
|
|
916
|
+
than the `transform` shorthand, so two effects on one element compose instead of
|
|
917
|
+
overwriting each other.
|
|
918
|
+
|
|
919
|
+
- **Scene:** `.scene` `.scene-near` `.scene-far` set the vanishing point; `.space`
|
|
920
|
+
applies `preserve-3d`.
|
|
921
|
+
- **Flip:** `.flip` / `.flip-x` with `.flip-front` and `.flip-back` stacked in one grid
|
|
922
|
+
cell, so the card is exactly as tall as its taller side. `data-deck-flip` on a button
|
|
923
|
+
wires it up and marks the hidden face `inert` so it's off the keyboard path.
|
|
924
|
+
- **Tilt:** `.tilt` with `data-tilt="10"`. deck.js writes a single rotation about a
|
|
925
|
+
computed axis. `.tilt-lift` floats content above the face on Z.
|
|
926
|
+
- **Depth stack:** `.pile` for a stack-depth of records, `.is-fanned` to spread it,
|
|
927
|
+
`Deck.advance(stack)` to dismiss the top card.
|
|
928
|
+
- **Coverflow:** `.coverflow` — scroll snap does the mechanics, 3D only does the read.
|
|
929
|
+
- **Cube:** `.cube` with six faces and `data-face="front|back|start|end|top|bottom"`,
|
|
930
|
+
or `.cube-spin`.
|
|
931
|
+
- **Depressible:** `.btn-3d` — the face moves down into its own shadow.
|
|
932
|
+
- **Parallax:** `.parallax` with `.parallax-back` `.parallax-mid` `.parallax-front` —
|
|
933
|
+
true Z-depth parallax on the compositor, no scroll handler and no jank.
|
|
934
|
+
- **Page turn:** `.turn-out` / `.turn-in`, pairs with `Deck.transition()`.
|
|
935
|
+
|
|
936
|
+
Under reduced motion, flips still flip (the state change is the information) but the
|
|
937
|
+
tilt, cube, and coverflow rotations are dropped.
|
|
938
|
+
|
|
939
|
+
### JS additions
|
|
940
|
+
|
|
941
|
+
```js
|
|
942
|
+
Deck.dir('rtl') // read or set direction, persisted
|
|
943
|
+
Deck.flip(card, true) // flip a card
|
|
944
|
+
Deck.advance(stack) // dismiss the top card of a depth stack
|
|
945
|
+
Deck.face(cube, 'top') // rotate a cube to a face
|
|
946
|
+
```
|
|
947
|
+
|
|
948
|
+
|
|
949
|
+
## Extended components
|
|
950
|
+
|
|
951
|
+
`deck-extras.js` is optional and loads after `deck.js`. Everything below has CSS that
|
|
952
|
+
works without it; the script adds behaviour.
|
|
953
|
+
|
|
954
|
+
### Carousel
|
|
955
|
+
|
|
956
|
+
```html
|
|
957
|
+
<div class="carousel carousel-peek" data-deck-carousel data-autoplay="6000">
|
|
958
|
+
<button class="carousel-arrow carousel-prev">…</button>
|
|
959
|
+
<div class="carousel-track">
|
|
960
|
+
<div class="carousel-slide">…</div>
|
|
961
|
+
</div>
|
|
962
|
+
<button class="carousel-arrow carousel-next">…</button>
|
|
963
|
+
<div class="carousel-dots"></div>
|
|
964
|
+
</div>
|
|
965
|
+
```
|
|
966
|
+
|
|
967
|
+
Scroll snap does the work, so it swipes correctly with JavaScript off — arrows and dots
|
|
968
|
+
are enhancement. Variants: `.carousel-peek` shows a sliver of the next slide,
|
|
969
|
+
`.carousel-multi` shows three. Autoplay pauses on hover, on focus, and when the tab is
|
|
970
|
+
hidden, and never starts under reduced motion. Fires `deck:slide`.
|
|
971
|
+
|
|
972
|
+
### Drawer, mega menu, speed dial, banner
|
|
973
|
+
|
|
974
|
+
- `.drawer` / `.drawer-end` — a side panel on `<dialog>`, so focus trapping and escape
|
|
975
|
+
are the browser's. `data-deck-drawer="#id"` on a trigger; `data-drawer-close` on any
|
|
976
|
+
button inside. Clicking the backdrop closes it.
|
|
977
|
+
- `.mega` — a wide popover panel with `.mega-grid` `.mega-col` `.mega-item`
|
|
978
|
+
`.mega-feature` `.mega-footer`. `data-deck-mega="#id"` adds hover intent on pointer
|
|
979
|
+
devices and click everywhere else.
|
|
980
|
+
- `.speed-dial` — a FAB that fans out into labelled actions, with a staggered entrance
|
|
981
|
+
and the plus rotating into a close. Sits above the tab bar and the safe area.
|
|
982
|
+
- `.banner` / `.banner-bottom` — a sticky announcement strip. Add `data-dismiss-key="x"`
|
|
983
|
+
and the dismissal persists in localStorage.
|
|
984
|
+
|
|
985
|
+
### Back to top
|
|
986
|
+
|
|
987
|
+
```html
|
|
988
|
+
<span id="top" tabindex="-1"></span>
|
|
989
|
+
…
|
|
990
|
+
<a class="back-to-top" href="#top" aria-label="Back to top">
|
|
991
|
+
<svg class="icon"><use href="/assets/deck-icons.svg#chevron-up"></use></svg>
|
|
992
|
+
</a>
|
|
993
|
+
```
|
|
994
|
+
|
|
995
|
+
A **link**, not a button. `#top` is a real target at the head of the document, so the
|
|
996
|
+
browser moves focus there along with the scroll. A button calling `scrollTo()` scrolls
|
|
997
|
+
the page and leaves a keyboard user parked at the bottom of it — they press Tab and land
|
|
998
|
+
back in the footer. Give the target `tabindex="-1"` so it can receive that focus.
|
|
999
|
+
|
|
1000
|
+
Show and hide is a scroll-driven animation on `scroll(root block)`, ranged `400px 520px`
|
|
1001
|
+
— the same mechanism as `.scroll-progress`. There is no scroll listener anywhere.
|
|
1002
|
+
`visibility` is part of the keyframe on purpose: it takes the link out of the tab order
|
|
1003
|
+
and out of the accessibility tree while it is off screen, which is what `aria-hidden` is
|
|
1004
|
+
reaching for and which CSS can do on its own.
|
|
1005
|
+
|
|
1006
|
+
Where `animation-timeline` is missing, `deck.js` marks the link with `data-deck-btt` and
|
|
1007
|
+
toggles `.is-visible` from an **IntersectionObserver** on a 400px sentinel at the top of
|
|
1008
|
+
the document — still not a scroll handler. Unmarked, with no JavaScript at all, the link
|
|
1009
|
+
simply stays visible and still works.
|
|
1010
|
+
|
|
1011
|
+
It parks at the bottom inline-end corner and stacks over whatever else is there:
|
|
1012
|
+
`body:has(.fab, .speed-dial)` lifts it a FAB's height, `body:has(.tabbar)` lifts it a tab
|
|
1013
|
+
bar's height, and both together lift it over both. Every branch clears
|
|
1014
|
+
`env(safe-area-inset-bottom)`. Written in logical properties, so `dir="rtl"` moves it to
|
|
1015
|
+
the other corner with no extra rule. It never prints.
|
|
1016
|
+
|
|
1017
|
+
Under `prefers-reduced-motion: reduce` the reveal animation does not apply at all, so the
|
|
1018
|
+
link is simply always there with no entrance, and `02-reset` has already put `html` back
|
|
1019
|
+
to `scroll-behavior: auto` — the jump is instant.
|
|
1020
|
+
|
|
1021
|
+
### Stepper
|
|
1022
|
+
|
|
1023
|
+
`.stepper` with `.step`, `.step-marker`, `.step-label`, `.step-note`. States are
|
|
1024
|
+
`.is-done` and `.is-current`. Numbers come from a CSS counter, so inserting a step
|
|
1025
|
+
renumbers everything. `.stepper-vertical` always stacks; `.stepper-auto` stacks below
|
|
1026
|
+
40rem, which is what five steps need on a phone.
|
|
1027
|
+
|
|
1028
|
+
The stepper is for a process you're moving through. The timeline in `07-components` is
|
|
1029
|
+
for recording what already happened — they aren't the same component.
|
|
1030
|
+
|
|
1031
|
+
### Inputs
|
|
1032
|
+
|
|
1033
|
+
- **Floating label** — `.float` with `placeholder=" "` on the input. Pure CSS via
|
|
1034
|
+
`:placeholder-shown`, so the label can never get out of sync with the value.
|
|
1035
|
+
`.float-outline` notches the label into the border.
|
|
1036
|
+
- **Number** — `.number` with real buttons instead of the native spinner, press-and-hold
|
|
1037
|
+
to repeat, min/max disabling, and an optional `.number-unit`.
|
|
1038
|
+
- **Phone** — `.phone` with a country select welded to the field. `data-mask="(###) ###-####"`
|
|
1039
|
+
formats as you type; `data-code` and `data-flag` drive the prefix. The number stays LTR
|
|
1040
|
+
and bidi-isolated even in an RTL document. Fires `deck:change` with `{ code, number, e164 }`.
|
|
1041
|
+
- **Rating** — `.rating` over real radio inputs, so it posts a value and works with the
|
|
1042
|
+
keyboard. `.rating-static` with `--value` shows a partial fill for an average. Use the
|
|
1043
|
+
solid `#star-fill` symbol, not `#star`: both components tell a selected star from an
|
|
1044
|
+
empty one by colour alone, so an outlined glyph leaves the two states identical.
|
|
1045
|
+
- **Range selector** — `.range-pair` with two native range inputs stacked. Real inputs
|
|
1046
|
+
mean real keyboard support and a real form post; `data-gap` keeps the handles apart.
|
|
1047
|
+
Fires `deck:change` with `{ min, max }`.
|
|
1048
|
+
- **Copy** — `.copy` with `.copy-btn data-deck-copy`, or `.copy-inline` for an icon
|
|
1049
|
+
beside an identifier in a table. Falls back to `execCommand` on http origins where the
|
|
1050
|
+
clipboard API is unavailable.
|
|
1051
|
+
|
|
1052
|
+
### WYSIWYG editor
|
|
1053
|
+
|
|
1054
|
+
`.editor` with `.editor-toolbar`, `.editor-content`, `.editor-footer`. Buttons carry
|
|
1055
|
+
`data-cmd`; `data-target="#hidden-input"` keeps a hidden field in sync for a normal form
|
|
1056
|
+
post. Paste arrives as plain text, so a paste out of Word doesn't drag its styling in.
|
|
1057
|
+
`data-limit` drives the character counter. Fires `deck:change` with `{ html, text }`.
|
|
1058
|
+
|
|
1059
|
+
### Video, gallery, lazy loading
|
|
1060
|
+
|
|
1061
|
+
- `.video` — a responsive frame for `<video>` or an embed, with `.video-poster` and
|
|
1062
|
+
`.video-play` for click-to-load. Ratios: `.video-square` `.video-portrait` `.video-wide`.
|
|
1063
|
+
- `.masonry` — CSS columns by default, upgrading to real `grid-template-rows: masonry`
|
|
1064
|
+
where supported, which preserves row order. Use it when the images have different
|
|
1065
|
+
shapes and you would rather not crop them; use `.gallery` below when they should all
|
|
1066
|
+
be the same size.
|
|
1067
|
+
- `.lazy` — a frame that holds its aspect ratio so nothing shifts, shimmers while
|
|
1068
|
+
waiting, and fades the image in on decode. Put the URL in `data-src` and
|
|
1069
|
+
`deck-extras.js` loads it 200px before it enters view.
|
|
1070
|
+
|
|
1071
|
+
### Chat
|
|
1072
|
+
|
|
1073
|
+
`.chat` with `.msg` / `.msg-out`, `.bubble`, `.bubble-meta`, `.bubble-name`,
|
|
1074
|
+
`.bubble-attachment`, `.bubble-system`, `.bubble-typing`, and `.chat-composer`.
|
|
1075
|
+
Consecutive messages from one side group automatically — only the last bubble in a run
|
|
1076
|
+
keeps its tail, and repeated avatars hide themselves.
|
|
1077
|
+
|
|
1078
|
+
### QR code
|
|
1079
|
+
|
|
1080
|
+
```html
|
|
1081
|
+
<div class="qr" data-deck-qr="https://example.com/orders/1042" data-ecl="M"></div>
|
|
1082
|
+
```
|
|
1083
|
+
|
|
1084
|
+
```js
|
|
1085
|
+
const svg = Deck.qr.svg('1FTFW1E85MFA12345', 'H');
|
|
1086
|
+
```
|
|
1087
|
+
|
|
1088
|
+
A complete encoder, written for this framework: byte mode, versions 1 through 10, error
|
|
1089
|
+
correction L / M / Q / H, Reed-Solomon over GF(256), all eight masks scored by the four
|
|
1090
|
+
standard penalty rules, and BCH format and version information. No library, no network
|
|
1091
|
+
call, no canvas — it emits SVG with horizontal runs merged into rects, so the markup
|
|
1092
|
+
stays small even at version 10.
|
|
1093
|
+
|
|
1094
|
+
It's verified by round-trip: encode, then read the matrix back out through the format
|
|
1095
|
+
information, the mask, the zig-zag, and the block de-interleave, and confirm the
|
|
1096
|
+
original string comes back. Six cases across all four correction levels and versions 1
|
|
1097
|
+
through 10 pass.
|
|
1098
|
+
|
|
1099
|
+
Sizes: `.qr-sm` `.qr` `.qr-lg`, or set `--qr-size`. `--qr-fg` and `--qr-bg` control the
|
|
1100
|
+
colors — keep the contrast high or scanners will struggle. `.qr-logo` punches a mark
|
|
1101
|
+
out of the middle, which is only safe at correction level Q or H.
|
|
1102
|
+
|
|
1103
|
+
Content longer than a version 10 code can hold throws with a readable message rather
|
|
1104
|
+
than rendering something unscannable. If you hit it, link to the content instead of
|
|
1105
|
+
embedding it.
|
|
1106
|
+
|
|
1107
|
+
### Indicators
|
|
1108
|
+
|
|
1109
|
+
`.indicator` with `-good` `-warn` `-bad` `-brand` `-lg` `-ring`, `.status-line` for a
|
|
1110
|
+
dot plus a label, and `.with-indicator` + `.indicator-badge` for a count on an icon
|
|
1111
|
+
(`.indicator-badge-dot` for a bare dot).
|
|
1112
|
+
|
|
1113
|
+
### Jumbotron and footer
|
|
1114
|
+
|
|
1115
|
+
`.jumbotron` / `.jumbotron-center` / `.jumbotron-media` for a hero, and `.footer` with
|
|
1116
|
+
`.footer-grid` `.footer-brand` `.footer-col` `.footer-heading` `.footer-bottom`
|
|
1117
|
+
`.footer-social`.
|
|
1118
|
+
|
|
1119
|
+
### Sidebar
|
|
1120
|
+
|
|
1121
|
+
```html
|
|
1122
|
+
<aside class="cq-shell">
|
|
1123
|
+
<nav class="panel sidebar" aria-label="Workspace">
|
|
1124
|
+
<span class="sidebar-group">Issues</span>
|
|
1125
|
+
<a class="sidebar-link" aria-current="page" href="/issues">
|
|
1126
|
+
<svg class="icon">…</svg><span>Open</span><span class="badge push">42</span>
|
|
1127
|
+
</a>
|
|
1128
|
+
</nav>
|
|
1129
|
+
</aside>
|
|
1130
|
+
```
|
|
1131
|
+
|
|
1132
|
+
A bare nav list: `.sidebar-group` for a heading, `.sidebar-link` for a row, `.push` to
|
|
1133
|
+
shove a count to the far end, `aria-current` for the active one. It brings no width and
|
|
1134
|
+
no chrome of its own, so put it in whatever rail your shell already has — a `.split`
|
|
1135
|
+
rail, a `.drawer`, a `.panel`.
|
|
1136
|
+
|
|
1137
|
+
Put `.cq-shell` on that rail and the links collapse to icons below 15rem. That is a
|
|
1138
|
+
container query keyed to the rail, not a media query keyed to the window, so a sidebar
|
|
1139
|
+
in a narrow column collapses on a 32 inch monitor and the same markup in a wide column
|
|
1140
|
+
does not. The rule lives in `18-container.css` as the worked example of a named
|
|
1141
|
+
container.
|
|
1142
|
+
|
|
1143
|
+
### Tooltips
|
|
1144
|
+
|
|
1145
|
+
There are two, and the difference matters.
|
|
1146
|
+
|
|
1147
|
+
```html
|
|
1148
|
+
<button class="btn btn-icon tooltip" data-tip="Re-run failed jobs" aria-label="Re-run failed jobs">…</button>
|
|
1149
|
+
|
|
1150
|
+
<button class="btn" popovertarget="tipBuild">Why did this build fail?</button>
|
|
1151
|
+
<div class="tip" id="tipBuild" popover>The integration suite timed out.<span class="tip-arrow"></span></div>
|
|
1152
|
+
```
|
|
1153
|
+
|
|
1154
|
+
- `.tooltip` is a `::after` on the trigger reading `data-tip`. No extra markup, no
|
|
1155
|
+
JavaScript, nothing to keep in sync. It is pinned above the trigger and **cannot
|
|
1156
|
+
flip**, so near the top of a scrollport it runs off the edge, and it hides itself
|
|
1157
|
+
under `(pointer: coarse)` because a hover tip never worked on a phone anyway.
|
|
1158
|
+
- `.tip` is a real popover placed with CSS anchor positioning (`25-anchor.css`), so it
|
|
1159
|
+
**can** flip — `position-try-fallbacks: flip-block, flip-inline` — and it carries a
|
|
1160
|
+
`.tip-arrow` that stays pointed at its anchor. `deck.js` pairs the trigger and the
|
|
1161
|
+
panel automatically from `popovertarget`.
|
|
1162
|
+
|
|
1163
|
+
Reach for `.tooltip` for a short label on an icon button in the middle of a page. Reach
|
|
1164
|
+
for `.tip` when the text is longer, has to survive an edge, or should open on click.
|
|
1165
|
+
|
|
1166
|
+
### Gallery
|
|
1167
|
+
|
|
1168
|
+
```html
|
|
1169
|
+
<div class="gallery">
|
|
1170
|
+
<a class="span-2" href="…"><img src="…" alt="Dashboard screenshot"></a>
|
|
1171
|
+
<a href="…"><img src="…" alt="Logo on a light background"></a>
|
|
1172
|
+
<a class="span-wide" href="…"><img src="…" alt="Social card, 1200 by 630"></a>
|
|
1173
|
+
</div>
|
|
1174
|
+
```
|
|
1175
|
+
|
|
1176
|
+
Equal square tiles on `auto-fill`, so nine photos and three photos both come out tidy
|
|
1177
|
+
without a breakpoint. `.span-2` promotes a tile to 2×2 and `.span-wide` to
|
|
1178
|
+
2×1, which is how you lead with the shot that matters. Images cover their cell and
|
|
1179
|
+
scale slightly on hover when the tile is a link. `.masonry` is the other choice — use it
|
|
1180
|
+
when the images have different shapes and you would rather not crop them.
|
|
1181
|
+
|
|
1182
|
+
### JS additions
|
|
1183
|
+
|
|
1184
|
+
```js
|
|
1185
|
+
Deck.qr.svg(text, 'M') // SVG string
|
|
1186
|
+
Deck.qr.build(text, 'M') // { modules, size, version } for your own renderer
|
|
1187
|
+
Deck.copy(text) // clipboard write with a fallback, returns a promise
|
|
1188
|
+
```
|
|
1189
|
+
|
|
1190
|
+
|
|
1191
|
+
## Libraries
|
|
1192
|
+
|
|
1193
|
+
Deck's core is zero-dependency and that is deliberate — it is the one thing Tailwind
|
|
1194
|
+
cannot claim, and it is why Deck drops into a Keel view with a single `<link>` tag. So
|
|
1195
|
+
a dependency has to earn its place by doing a job Deck genuinely does worse.
|
|
1196
|
+
|
|
1197
|
+
`deck-adapters.js` is the mechanism. Each adapter activates **only** if the library is
|
|
1198
|
+
already on the page. Load none of them and nothing changes. Load one and Deck hands
|
|
1199
|
+
that job over while keeping its own markup, classes, and styling.
|
|
1200
|
+
|
|
1201
|
+
```html
|
|
1202
|
+
<script src="/assets/deck.js" defer></script>
|
|
1203
|
+
<script src="/assets/deck-extras.js" defer></script>
|
|
1204
|
+
<script src="/assets/deck-adapters.js" defer></script>
|
|
1205
|
+
|
|
1206
|
+
<!-- add only what you want -->
|
|
1207
|
+
<script src="https://cdn.jsdelivr.net/npm/@floating-ui/dom" defer></script>
|
|
1208
|
+
<script src="https://cdn.jsdelivr.net/npm/sortablejs" defer></script>
|
|
1209
|
+
```
|
|
1210
|
+
|
|
1211
|
+
| Job | Deck alone | With a library | Verdict |
|
|
1212
|
+
|---|---|---|---|
|
|
1213
|
+
| Placement | CSS anchor positioning | Floating UI (~9 KB) | Library only where anchor positioning is missing |
|
|
1214
|
+
| Rich text | `execCommand`, deprecated | Tiptap or Quill | Use the library |
|
|
1215
|
+
| Charts | CSS charts that retheme | Chart.js, themed by Deck | CSS for tiles, Chart.js for real axes |
|
|
1216
|
+
| Drag and drop | Native HTML DnD, poor on touch | SortableJS | Use the library |
|
|
1217
|
+
| Icons | 74-icon sprite | Lucide (1500 icons) | Sprite covers Deck; Lucide for the rest |
|
|
1218
|
+
| Long lists | `content-visibility` | A virtualizer (~5 KB) | Keep the browser |
|
|
1219
|
+
| Dates and locales | `Intl` | date-fns and friends | Keep `Intl` |
|
|
1220
|
+
|
|
1221
|
+
`Deck.adapters.report()` names what is actually doing each job on the current page.
|
|
1222
|
+
Worth running when a component behaves differently between two environments.
|
|
1223
|
+
|
|
1224
|
+
### Placement
|
|
1225
|
+
|
|
1226
|
+
Every floating thing in Deck was positioned by hand with `getBoundingClientRect`, which
|
|
1227
|
+
does not flip at the bottom of the window, does not shift back inside at an edge, and
|
|
1228
|
+
does not follow its anchor inside a scrolling container. `25-anchor.css` fixes that with
|
|
1229
|
+
CSS anchor positioning — natively, on the compositor, with no listeners.
|
|
1230
|
+
|
|
1231
|
+
`deck.js` pairs every `popovertarget` with its panel and generates a unique
|
|
1232
|
+
`anchor-name`, so you write no extra markup. Where the browser lacks it, the Floating UI
|
|
1233
|
+
adapter takes over with `flip`, `shift`, `size`, and `arrow`; where neither is present,
|
|
1234
|
+
Deck's own placement runs as before.
|
|
1235
|
+
|
|
1236
|
+
New anchored components: `.tip` (a tooltip that can flip and carry an arrow, unlike the
|
|
1237
|
+
`::after` one) and `.pop` (a popover card with a title, body, and actions).
|
|
1238
|
+
|
|
1239
|
+
The real win is subtler: an anchored panel goes in the top layer, which is the fix for
|
|
1240
|
+
the bug that bites every combobox nested inside a modal or an `overflow: hidden` card.
|
|
1241
|
+
|
|
1242
|
+
### Rich text
|
|
1243
|
+
|
|
1244
|
+
`document.execCommand` is deprecated and inconsistent, and rewriting onto Selection and
|
|
1245
|
+
Range means building a document model — which is what Tiptap and Quill already are. The
|
|
1246
|
+
adapter hands `.editor-content` over to whichever is present and keeps Deck's toolbar
|
|
1247
|
+
chrome, so the markup and CSS are unchanged. Toolbar `data-cmd` values are mapped to
|
|
1248
|
+
each library's command set, and active state still lights the buttons.
|
|
1249
|
+
|
|
1250
|
+
Deck's built-in editor stays as the fallback so a form still works with no library.
|
|
1251
|
+
|
|
1252
|
+
### Charts
|
|
1253
|
+
|
|
1254
|
+
Deck's CSS charts retheme with the hue slider and cost nothing, which is right for
|
|
1255
|
+
dashboard tiles. What they cannot do is a time axis, a crosshair, a zoom, or twenty
|
|
1256
|
+
thousand points. The adapter sets Chart.js defaults from Deck's tokens — fonts, grid
|
|
1257
|
+
color, tooltip surface, point styles — and re-reads them when the theme or hue changes,
|
|
1258
|
+
so a Chart.js canvas follows the slider like everything else.
|
|
1259
|
+
|
|
1260
|
+
```js
|
|
1261
|
+
Deck.chart(canvas, config); // same as new Chart(), with the series palette applied
|
|
1262
|
+
```
|
|
1263
|
+
|
|
1264
|
+
### Drag and drop
|
|
1265
|
+
|
|
1266
|
+
Deck's CSS already uses SortableJS's default class names (`sortable-ghost`,
|
|
1267
|
+
`sortable-chosen`, `sortable-drag`), so no configuration is needed. Add
|
|
1268
|
+
`data-deck-sortable="groupname"` and optionally `data-handle=".drag-handle"`. Without
|
|
1269
|
+
the library it falls back to native HTML drag and drop, which works on a desktop and is
|
|
1270
|
+
poor on touch — that's the honest reason to load SortableJS.
|
|
1271
|
+
|
|
1272
|
+
Comes with `.kanban`, `.kanban-col`, `.kanban-head`, `.kanban-body`, `.kanban-card`, and
|
|
1273
|
+
`.kanban-empty`. Fires `deck:reorder` with the new order as an array of `data-id` values,
|
|
1274
|
+
which is what you POST back to Keel.
|
|
1275
|
+
|
|
1276
|
+
### Long lists
|
|
1277
|
+
|
|
1278
|
+
The usual answer to a ten thousand row grid is a virtualization library: measure the
|
|
1279
|
+
viewport, render a window, position a spacer, reconcile every scroll frame. It works and
|
|
1280
|
+
it breaks find-in-page, printing, accessibility tree order, and selection across the
|
|
1281
|
+
boundary.
|
|
1282
|
+
|
|
1283
|
+
`content-visibility: auto` does the same job in the engine. Off-screen subtrees are
|
|
1284
|
+
skipped during layout, style, paint, and hit testing but stay in the DOM, so Ctrl+F still
|
|
1285
|
+
finds them and the print stylesheet still prints them. One line of CSS, no JavaScript.
|
|
1286
|
+
|
|
1287
|
+
```html
|
|
1288
|
+
<table class="dg dg-virtual">
|
|
1289
|
+
```
|
|
1290
|
+
|
|
1291
|
+
Also `.list-virtual`, `.virtual` (with `--item-size`), and `.defer` for whole sections
|
|
1292
|
+
below the fold. The print stylesheet forces all of them back to `visible`, or half a
|
|
1293
|
+
report comes out blank.
|
|
1294
|
+
|
|
1295
|
+
`.contain` and `.contain-paint` are the companion: on a dashboard with twenty cards,
|
|
1296
|
+
containment is the difference between one layout pass and twenty.
|
|
1297
|
+
|
|
1298
|
+
### Dates and locales
|
|
1299
|
+
|
|
1300
|
+
Month names, weekday names, and the first day of the week now come from `Intl` rather
|
|
1301
|
+
than a hardcoded English array. It is built into every browser and correct in every
|
|
1302
|
+
locale, so a date library adds nothing.
|
|
1303
|
+
|
|
1304
|
+
```html
|
|
1305
|
+
<div class="datefield" data-deck-datepicker data-locale="de-DE" data-format="dmy">
|
|
1306
|
+
```
|
|
1307
|
+
|
|
1308
|
+
`data-locale` overrides the document language. Week start comes from
|
|
1309
|
+
`Intl.Locale.getWeekInfo()` — Sunday in the US, Monday across most of Europe — and can
|
|
1310
|
+
still be forced with `data-week-start`. Day cell labels use `dateStyle: 'full'`, so a
|
|
1311
|
+
screen reader reads a properly localized date.
|
|
1312
|
+
|
|
1313
|
+
```js
|
|
1314
|
+
Deck.locale('es-MX') // { months, monthsShort, days, weekStart, long, full }
|
|
1315
|
+
```
|
|
1316
|
+
|
|
1317
|
+
### Icons
|
|
1318
|
+
|
|
1319
|
+
The 74-icon sprite covers what the framework itself needs plus the automotive set it was
|
|
1320
|
+
built for. Past that, the cheapest move is to add the name to `tools/icons/icons.txt` and
|
|
1321
|
+
run `npm run icons` — any of the 4,025 Material Symbols glyphs is one line away.
|
|
1322
|
+
|
|
1323
|
+
If you would rather not regenerate, write `<span data-icon="briefcase" class="icon">` and
|
|
1324
|
+
the Lucide adapter swaps in the path data, keeping Deck's `.icon` sizing rules. No
|
|
1325
|
+
adapter, no swap, and the sprite still works. Be aware that Lucide draws real strokes
|
|
1326
|
+
while the sprite is filled outlines, so the two do not match at close range; use one or
|
|
1327
|
+
the other in a given screen.
|