@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/bin/deck.mjs
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* deck — copy Deck's assets into a project.
|
|
4
|
+
*
|
|
5
|
+
* npx @echodial/deck init
|
|
6
|
+
* npx @echodial/deck init public/assets
|
|
7
|
+
* npx @echodial/deck init --min --bundle
|
|
8
|
+
* npx @echodial/deck starter public/index.html
|
|
9
|
+
*
|
|
10
|
+
* For people who are not using npm as a build tool and just want the files
|
|
11
|
+
* sitting in a folder, which is most of the people Deck is for.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { copyFile, mkdir, readFile, writeFile, access } from 'node:fs/promises';
|
|
15
|
+
import { constants } from 'node:fs';
|
|
16
|
+
import path from 'node:path';
|
|
17
|
+
import { fileURLToPath } from 'node:url';
|
|
18
|
+
|
|
19
|
+
const root = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
|
|
20
|
+
const DIST = path.join(root, 'dist');
|
|
21
|
+
const pkg = JSON.parse(await readFile(path.join(root, 'package.json'), 'utf8'));
|
|
22
|
+
|
|
23
|
+
// Piping into head/less closes stdout early; that is not a crash
|
|
24
|
+
process.stdout.on('error', err => { if (err.code === 'EPIPE') process.exit(0); });
|
|
25
|
+
|
|
26
|
+
const args = process.argv.slice(2);
|
|
27
|
+
const cmd = args[0];
|
|
28
|
+
const flags = new Set(args.filter(a => a.startsWith('--')));
|
|
29
|
+
const positional = args.slice(1).filter(a => !a.startsWith('--'));
|
|
30
|
+
|
|
31
|
+
const c = {
|
|
32
|
+
dim: s => `\x1b[2m${s}\x1b[0m`,
|
|
33
|
+
bold: s => `\x1b[1m${s}\x1b[0m`,
|
|
34
|
+
green: s => `\x1b[32m${s}\x1b[0m`,
|
|
35
|
+
red: s => `\x1b[31m${s}\x1b[0m`
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const exists = async p => { try { await access(p, constants.F_OK); return true; } catch { return false; } };
|
|
39
|
+
|
|
40
|
+
function usage() {
|
|
41
|
+
console.log(`
|
|
42
|
+
${c.bold(`Deck v${pkg.version}`)} ${c.dim('— a CSS framework with no build step')}
|
|
43
|
+
|
|
44
|
+
${c.bold('npx @echodial/deck init')} ${c.dim('[dir]')} copy the assets (default: assets/deck)
|
|
45
|
+
${c.bold('npx @echodial/deck starter')} ${c.dim('[file]')} write a starter HTML page
|
|
46
|
+
${c.bold('npx @echodial/deck list')} show what is in the package
|
|
47
|
+
|
|
48
|
+
${c.dim('--min')} copy the minified stylesheet only
|
|
49
|
+
${c.dim('--bundle')} copy one combined script instead of three
|
|
50
|
+
${c.dim('--css-only')} skip the scripts entirely
|
|
51
|
+
${c.dim('--force')} overwrite files that already exist
|
|
52
|
+
`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const SETS = {
|
|
56
|
+
css: ['deck.css', 'deck-icons.svg'],
|
|
57
|
+
cssMin: ['deck.min.css', 'deck-icons.svg'],
|
|
58
|
+
js: ['deck.js', 'deck-extras.js', 'deck-adapters.js'],
|
|
59
|
+
jsBundle: ['deck.bundle.min.js']
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
async function init() {
|
|
63
|
+
const target = path.resolve(process.cwd(), positional[0] || 'assets/deck');
|
|
64
|
+
const files = [
|
|
65
|
+
...(flags.has('--min') ? SETS.cssMin : SETS.css),
|
|
66
|
+
...(flags.has('--css-only') ? [] : flags.has('--bundle') ? SETS.jsBundle : SETS.js)
|
|
67
|
+
];
|
|
68
|
+
|
|
69
|
+
await mkdir(target, { recursive: true });
|
|
70
|
+
|
|
71
|
+
let written = 0, skipped = 0, missing = 0;
|
|
72
|
+
for (const file of files) {
|
|
73
|
+
const from = path.join(DIST, file);
|
|
74
|
+
const to = path.join(target, file);
|
|
75
|
+
if (!(await exists(from))) { missing++; continue; }
|
|
76
|
+
if (await exists(to) && !flags.has('--force')) { skipped++; continue; }
|
|
77
|
+
await copyFile(from, to);
|
|
78
|
+
written++;
|
|
79
|
+
console.log(` ${c.green('+')} ${path.relative(process.cwd(), to)}`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (skipped) console.log(c.dim(` ${skipped} already there (use --force to overwrite)`));
|
|
83
|
+
if (missing) console.log(c.dim(` ${missing} not built (run \`npm run build\` in the package)`));
|
|
84
|
+
|
|
85
|
+
const rel = path.relative(process.cwd(), target).replace(/\\/g, '/');
|
|
86
|
+
// The folder on disk is not the folder in the URL. Strip the usual document
|
|
87
|
+
// roots so the snippet we print is actually pasteable.
|
|
88
|
+
const web = rel.replace(/^(public_html|public|web|htdocs|httpdocs|www|wwwroot|dist|static)\//, '');
|
|
89
|
+
const cssFile = flags.has('--min') ? 'deck.min.css' : 'deck.css';
|
|
90
|
+
|
|
91
|
+
console.log(`
|
|
92
|
+
${c.bold('Add to your layout:')}
|
|
93
|
+
|
|
94
|
+
<link rel="stylesheet" href="/${web}/${cssFile}">${flags.has('--css-only') ? '' : `
|
|
95
|
+
<script src="/${web}/${flags.has('--bundle') ? 'deck.bundle.min.js' : 'deck.js'}" defer></script>`}
|
|
96
|
+
|
|
97
|
+
${c.dim('deck-icons.svg sits beside deck.js and is found automatically.')}
|
|
98
|
+
${c.dim('To load the sprite from somewhere else:')}
|
|
99
|
+
|
|
100
|
+
<script src="/${web}/deck.js" data-deck-icons="/your/path/deck-icons.svg" defer></script>
|
|
101
|
+
`);
|
|
102
|
+
|
|
103
|
+
return written;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async function starter() {
|
|
107
|
+
const file = path.resolve(process.cwd(), positional[0] || 'index.html');
|
|
108
|
+
if (await exists(file) && !flags.has('--force')) {
|
|
109
|
+
console.log(c.red(` ${path.relative(process.cwd(), file)} already exists. Use --force to overwrite.`));
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const html = `<!doctype html>
|
|
114
|
+
<html lang="en">
|
|
115
|
+
<head>
|
|
116
|
+
<meta charset="utf-8">
|
|
117
|
+
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
|
118
|
+
<title>Built with Deck</title>
|
|
119
|
+
<link rel="stylesheet" href="/assets/deck/deck.css">
|
|
120
|
+
<script src="/assets/deck/deck.js" defer></script>
|
|
121
|
+
<script src="/assets/deck/deck-extras.js" defer></script>
|
|
122
|
+
<style>
|
|
123
|
+
/* Your CSS goes in a layer and beats every Deck rule with no !important. */
|
|
124
|
+
@layer app.pages {
|
|
125
|
+
.hero { padding-block: var(--space-section); }
|
|
126
|
+
}
|
|
127
|
+
</style>
|
|
128
|
+
</head>
|
|
129
|
+
<body>
|
|
130
|
+
|
|
131
|
+
<a class="skip-link" href="#main">Skip to content</a>
|
|
132
|
+
|
|
133
|
+
<header class="sticky-top">
|
|
134
|
+
<div class="container">
|
|
135
|
+
<nav class="navbar">
|
|
136
|
+
<a class="navbar-brand" href="/">Your app</a>
|
|
137
|
+
<div class="navbar-links">
|
|
138
|
+
<a class="nav-link" aria-current="page" href="/">Home</a>
|
|
139
|
+
<a class="nav-link" href="/about">About</a>
|
|
140
|
+
</div>
|
|
141
|
+
<button class="btn btn-icon btn-ghost push" data-deck-theme aria-label="Switch theme">
|
|
142
|
+
<svg class="icon"><use href="/assets/deck/deck-icons.svg#moon"></use></svg>
|
|
143
|
+
</button>
|
|
144
|
+
</nav>
|
|
145
|
+
</div>
|
|
146
|
+
</header>
|
|
147
|
+
|
|
148
|
+
<main id="main" class="container section stack-8">
|
|
149
|
+
|
|
150
|
+
<section class="hero stack-5">
|
|
151
|
+
<h1 class="display">Start here.</h1>
|
|
152
|
+
<p class="lede">This page is using Deck. There is no build step, no config file,
|
|
153
|
+
and no dependencies. Change one number below and the whole page follows.</p>
|
|
154
|
+
<div class="cluster">
|
|
155
|
+
<button class="btn btn-primary btn-lg">Primary action</button>
|
|
156
|
+
<button class="btn btn-lg">Secondary</button>
|
|
157
|
+
</div>
|
|
158
|
+
</section>
|
|
159
|
+
|
|
160
|
+
<section class="stack-4">
|
|
161
|
+
<label class="label" for="hue">Brand hue</label>
|
|
162
|
+
<input id="hue" class="range" type="range" min="0" max="360" value="196"
|
|
163
|
+
oninput="Deck.hue(this.value)">
|
|
164
|
+
<div class="grid">
|
|
165
|
+
<div class="card"><div class="card-body">
|
|
166
|
+
<h3 class="card-title">Cards</h3>
|
|
167
|
+
<p class="text-sm text-muted">Every surface, badge, and focus ring follows the hue.</p>
|
|
168
|
+
</div></div>
|
|
169
|
+
<div class="card"><div class="card-body">
|
|
170
|
+
<h3 class="card-title">Forms</h3>
|
|
171
|
+
<div class="field">
|
|
172
|
+
<label class="label" for="email">Email</label>
|
|
173
|
+
<input class="input" id="email" type="email" placeholder="you@example.com">
|
|
174
|
+
</div>
|
|
175
|
+
</div></div>
|
|
176
|
+
<div class="card"><div class="card-body">
|
|
177
|
+
<h3 class="card-title">Feedback</h3>
|
|
178
|
+
<button class="btn btn-sm btn-soft"
|
|
179
|
+
onclick="Deck.toast({kind:'good', title:'It works'})">Fire a toast</button>
|
|
180
|
+
</div></div>
|
|
181
|
+
</div>
|
|
182
|
+
</section>
|
|
183
|
+
|
|
184
|
+
</main>
|
|
185
|
+
|
|
186
|
+
<footer class="footer">
|
|
187
|
+
<div class="container footer-bottom">
|
|
188
|
+
<span>Built with Deck</span>
|
|
189
|
+
</div>
|
|
190
|
+
</footer>
|
|
191
|
+
|
|
192
|
+
</body>
|
|
193
|
+
</html>
|
|
194
|
+
`;
|
|
195
|
+
|
|
196
|
+
await mkdir(path.dirname(file), { recursive: true });
|
|
197
|
+
await writeFile(file, html);
|
|
198
|
+
console.log(` ${c.green('+')} ${path.relative(process.cwd(), file)}`);
|
|
199
|
+
console.log(c.dim(`\n Run \`npx @echodial/deck init public/assets/deck\` if you have not copied the assets yet.\n`));
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async function list() {
|
|
203
|
+
const { readdir, stat } = await import('node:fs/promises');
|
|
204
|
+
const files = (await readdir(DIST)).filter(f => !f.startsWith('.')).sort();
|
|
205
|
+
console.log(`\n${c.bold(`Deck v${pkg.version}`)} ${c.dim(DIST)}\n`);
|
|
206
|
+
for (const f of files) {
|
|
207
|
+
const s = await stat(path.join(DIST, f));
|
|
208
|
+
if (s.isDirectory()) { console.log(` ${f}/`); continue; }
|
|
209
|
+
console.log(` ${f.padEnd(26)} ${c.dim((s.size / 1000).toFixed(1) + ' KB')}`);
|
|
210
|
+
}
|
|
211
|
+
console.log();
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
switch (cmd) {
|
|
215
|
+
case 'init': await init(); break;
|
|
216
|
+
case 'starter': await starter(); break;
|
|
217
|
+
case 'list': await list(); break;
|
|
218
|
+
default: usage();
|
|
219
|
+
}
|
package/build.mjs
ADDED
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Deck build
|
|
4
|
+
*
|
|
5
|
+
* node build.mjs
|
|
6
|
+
*
|
|
7
|
+
* Everything under src/ is a source file. Everything under dist/ is generated
|
|
8
|
+
* and safe to delete: this script rebuilds all of it. src/*.css concatenates
|
|
9
|
+
* into dist/deck.css, src/js/* is copied and minified, and src/deck-icons.svg
|
|
10
|
+
* is copied across. Requires nothing. If esbuild happens to be installed it is
|
|
11
|
+
* used for minification because it is better at it; otherwise a conservative
|
|
12
|
+
* built-in minifier runs, so the build never depends on a toolchain being
|
|
13
|
+
* present. That is the same promise the framework itself makes.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { readdir, readFile, writeFile, mkdir, copyFile } from 'node:fs/promises';
|
|
17
|
+
import { createHash } from 'node:crypto';
|
|
18
|
+
import { gzipSync, brotliCompressSync, constants as Z } from 'node:zlib';
|
|
19
|
+
import path from 'node:path';
|
|
20
|
+
import { fileURLToPath } from 'node:url';
|
|
21
|
+
|
|
22
|
+
const root = path.dirname(fileURLToPath(import.meta.url));
|
|
23
|
+
const SRC = path.join(root, 'src');
|
|
24
|
+
const JS = path.join(SRC, 'js');
|
|
25
|
+
const BRAND = path.join(SRC, 'brand');
|
|
26
|
+
const DIST = path.join(root, 'dist');
|
|
27
|
+
|
|
28
|
+
const pkg = JSON.parse(await readFile(path.join(root, 'package.json'), 'utf8'));
|
|
29
|
+
|
|
30
|
+
const BANNER = `/*! ==========================================================================
|
|
31
|
+
* Deck v${pkg.version} — the CSS framework for Keel
|
|
32
|
+
* Mobile first. One file. No build step. No config. No dependencies.
|
|
33
|
+
*
|
|
34
|
+
* <link rel="stylesheet" href="/assets/deck.css">
|
|
35
|
+
*
|
|
36
|
+
* Retheme the entire app from one line:
|
|
37
|
+
* :root { --hue-brand: 265; }
|
|
38
|
+
*
|
|
39
|
+
* Layer order (so your app CSS always wins without !important):
|
|
40
|
+
* deck.reset, deck.tokens, deck.type, deck.layout, deck.components,
|
|
41
|
+
* deck.mobile, deck.motion, deck.effects, deck.utilities, deck.rtl,
|
|
42
|
+
* deck.print, then app.base, app.components, app.pages, app.overrides
|
|
43
|
+
*
|
|
44
|
+
* ${pkg.license} licence. ${pkg.homepage}
|
|
45
|
+
* ======================================================================== */
|
|
46
|
+
|
|
47
|
+
`;
|
|
48
|
+
|
|
49
|
+
/* -------------------------------------------------------------------------
|
|
50
|
+
Built-in CSS minifier.
|
|
51
|
+
Deliberately conservative: it walks the file character by character so that
|
|
52
|
+
strings, url() values, and data URIs are never touched, because a minifier
|
|
53
|
+
that mangles a data URI produces a stylesheet that looks fine and renders
|
|
54
|
+
wrong. It only removes comments, collapses runs of whitespace, and drops
|
|
55
|
+
whitespace that is provably optional.
|
|
56
|
+
------------------------------------------------------------------------- */
|
|
57
|
+
function minifyCSS(css) {
|
|
58
|
+
let out = '';
|
|
59
|
+
let i = 0;
|
|
60
|
+
const n = css.length;
|
|
61
|
+
const NEEDS_SPACE = /[a-zA-Z0-9_%)\]'"-]/;
|
|
62
|
+
|
|
63
|
+
while (i < n) {
|
|
64
|
+
const c = css[i];
|
|
65
|
+
|
|
66
|
+
// Preserve /*! banner comments, drop the rest
|
|
67
|
+
if (c === '/' && css[i + 1] === '*') {
|
|
68
|
+
const bang = css[i + 2] === '!';
|
|
69
|
+
const end = css.indexOf('*/', i + 2);
|
|
70
|
+
const stop = end === -1 ? n : end + 2;
|
|
71
|
+
if (bang) out += css.slice(i, stop);
|
|
72
|
+
i = stop;
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Strings pass through untouched
|
|
77
|
+
if (c === '"' || c === "'") {
|
|
78
|
+
const quote = c;
|
|
79
|
+
let j = i + 1;
|
|
80
|
+
while (j < n && !(css[j] === quote && css[j - 1] !== '\\')) j++;
|
|
81
|
+
out += css.slice(i, j + 1);
|
|
82
|
+
i = j + 1;
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// url(...) passes through untouched, data URIs included
|
|
87
|
+
if ((c === 'u' || c === 'U') && /^url\(/i.test(css.slice(i, i + 4))) {
|
|
88
|
+
let j = i + 4, depth = 1;
|
|
89
|
+
while (j < n && depth > 0) {
|
|
90
|
+
if (css[j] === '(') depth++;
|
|
91
|
+
else if (css[j] === ')') depth--;
|
|
92
|
+
else if (css[j] === '"' || css[j] === "'") {
|
|
93
|
+
const q = css[j++];
|
|
94
|
+
while (j < n && !(css[j] === q && css[j - 1] !== '\\')) j++;
|
|
95
|
+
}
|
|
96
|
+
j++;
|
|
97
|
+
}
|
|
98
|
+
out += css.slice(i, j).replace(/\s+/g, '');
|
|
99
|
+
i = j;
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Collapse whitespace, then decide whether any is still needed
|
|
104
|
+
if (/\s/.test(c)) {
|
|
105
|
+
let j = i;
|
|
106
|
+
while (j < n && /\s/.test(css[j])) j++;
|
|
107
|
+
const prev = out[out.length - 1] || '';
|
|
108
|
+
const next = css[j] || '';
|
|
109
|
+
const structural = '{}:;,>~+()[]';
|
|
110
|
+
const keep =
|
|
111
|
+
NEEDS_SPACE.test(prev) && NEEDS_SPACE.test(next) ||
|
|
112
|
+
// a descendant combinator, or the space before a ( in a media query
|
|
113
|
+
(prev === ')' && NEEDS_SPACE.test(next)) ||
|
|
114
|
+
// never glue "and(" in @media, or a calc operand
|
|
115
|
+
(/[+\-*/]/.test(next) && prev !== '(' && !structural.includes(prev));
|
|
116
|
+
if (keep) out += ' ';
|
|
117
|
+
i = j;
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Drop the semicolon before a closing brace
|
|
122
|
+
if (c === ';') {
|
|
123
|
+
let j = i + 1;
|
|
124
|
+
while (j < n && /\s/.test(css[j])) j++;
|
|
125
|
+
if (css[j] === '}') { i = j; continue; }
|
|
126
|
+
out += ';';
|
|
127
|
+
i++;
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
out += c;
|
|
132
|
+
i++;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return out.replace(/\s*([{};,])\s*/g, (m, ch) => ch === ',' ? ',' : ch).trim();
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/* ------------------------------------------------------------------------- */
|
|
139
|
+
|
|
140
|
+
async function tryEsbuild() {
|
|
141
|
+
try { return (await import('esbuild')).default ?? (await import('esbuild')); }
|
|
142
|
+
catch { return null; }
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/* Every published size comes from here. Brotli is what browsers actually
|
|
146
|
+
negotiate for text on Cloudflare, Vercel, Netlify, and nginx with
|
|
147
|
+
ngx_brotli; gzip is the fallback, so both are measured and both are
|
|
148
|
+
reported. Quality 11 with a size hint is what a CDN uses for a static
|
|
149
|
+
asset it caches. */
|
|
150
|
+
const brotliSize = buf => brotliCompressSync(buf, {
|
|
151
|
+
params: {
|
|
152
|
+
[Z.BROTLI_PARAM_QUALITY]: 11,
|
|
153
|
+
[Z.BROTLI_PARAM_SIZE_HINT]: buf.length,
|
|
154
|
+
},
|
|
155
|
+
}).length;
|
|
156
|
+
|
|
157
|
+
/* filename in dist/ -> { raw, gzip, brotli }. Written to dist/sizes.json at
|
|
158
|
+
the end of the build; the docs and the demo page read it from there rather
|
|
159
|
+
than carrying hand-typed numbers. */
|
|
160
|
+
const SIZES = {};
|
|
161
|
+
|
|
162
|
+
function measure(distName, buf) {
|
|
163
|
+
const b = Buffer.isBuffer(buf) ? buf : Buffer.from(buf);
|
|
164
|
+
SIZES[distName] = { raw: b.length, gzip: gzipSync(b).length, brotli: brotliSize(b) };
|
|
165
|
+
return SIZES[distName];
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const kb = b => (b / 1000).toFixed(1).padStart(6) + ' KB';
|
|
169
|
+
|
|
170
|
+
function report(label, raw, min, distName) {
|
|
171
|
+
const s = measure(distName, min);
|
|
172
|
+
console.log(
|
|
173
|
+
` ${label.padEnd(22)} ${kb(raw.length)} raw ${kb(s.raw)} min ` +
|
|
174
|
+
`${kb(s.gzip)} gzip ${kb(s.brotli)} brotli`
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async function build() {
|
|
179
|
+
await mkdir(DIST, { recursive: true });
|
|
180
|
+
const esbuild = await tryEsbuild();
|
|
181
|
+
console.log(`\nDeck v${pkg.version} ${esbuild ? '(esbuild)' : '(built-in minifier)'}\n`);
|
|
182
|
+
|
|
183
|
+
// ---- CSS ----------------------------------------------------------------
|
|
184
|
+
const files = (await readdir(SRC)).filter(f => f.endsWith('.css')).sort();
|
|
185
|
+
const parts = await Promise.all(
|
|
186
|
+
files.map(f => readFile(path.join(SRC, f), 'utf8'))
|
|
187
|
+
);
|
|
188
|
+
const css = BANNER + parts.join('\n');
|
|
189
|
+
await writeFile(path.join(DIST, 'deck.css'), css);
|
|
190
|
+
|
|
191
|
+
// The layer statement has to stay first, so minify the body and re-attach
|
|
192
|
+
const minCss = esbuild
|
|
193
|
+
? BANNER.trim() + '\n' + (await esbuild.transform(css, {
|
|
194
|
+
loader: 'css', minify: true, target: ['chrome117', 'safari17.4', 'firefox128']
|
|
195
|
+
})).code
|
|
196
|
+
: minifyCSS(css);
|
|
197
|
+
await writeFile(path.join(DIST, 'deck.min.css'), minCss);
|
|
198
|
+
report('deck.css', css, minCss, 'deck.min.css');
|
|
199
|
+
|
|
200
|
+
// Per-layer files, for anyone who wants only part of Deck
|
|
201
|
+
await mkdir(path.join(DIST, 'layers'), { recursive: true });
|
|
202
|
+
for (const [i, f] of files.entries()) {
|
|
203
|
+
await writeFile(path.join(DIST, 'layers', f.replace(/^\d+-/, '')), parts[i]);
|
|
204
|
+
}
|
|
205
|
+
console.log(` ${'layers/'.padEnd(22)} ${files.length} files`);
|
|
206
|
+
|
|
207
|
+
// ---- Static assets ------------------------------------------------------
|
|
208
|
+
// The sprite is a source file, not a generated one, so it is copied rather
|
|
209
|
+
// than transformed. dist/ stays entirely disposable.
|
|
210
|
+
const sprite = await readFile(path.join(SRC, 'deck-icons.svg'));
|
|
211
|
+
await writeFile(path.join(DIST, 'deck-icons.svg'), sprite);
|
|
212
|
+
const spriteSize = measure('deck-icons.svg', sprite);
|
|
213
|
+
console.log(
|
|
214
|
+
` ${'deck-icons.svg'.padEnd(22)} ${kb(spriteSize.raw)} raw ${' '.repeat(9)} ` +
|
|
215
|
+
`${kb(spriteSize.gzip)} gzip ${kb(spriteSize.brotli)} brotli`
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
// ---- Brand --------------------------------------------------------------
|
|
219
|
+
// src/brand/ owns the logo. public_html/assets/images/ is a published copy of
|
|
220
|
+
// it and nothing else, so there is one drawing on disk and no second sprite.
|
|
221
|
+
//
|
|
222
|
+
// Most of src/brand/ is itself derived from the master lockup by
|
|
223
|
+
// tools/make-brand.mjs, which records the hash of every master it read. The
|
|
224
|
+
// rasters cannot be regenerated in-process without a renderer, so rather
|
|
225
|
+
// than silently shipping stale artwork the build stops when a master moves.
|
|
226
|
+
await mkdir(path.join(DIST, 'brand'), { recursive: true });
|
|
227
|
+
const brandFiles = (await readdir(BRAND)).filter(f => f !== 'sources.json').sort();
|
|
228
|
+
for (const f of brandFiles) {
|
|
229
|
+
await copyFile(path.join(BRAND, f), path.join(DIST, 'brand', f));
|
|
230
|
+
}
|
|
231
|
+
console.log(` ${'brand/'.padEnd(22)} ${brandFiles.length} files`);
|
|
232
|
+
|
|
233
|
+
const sha = t => createHash('sha256').update(t, 'utf8').digest('hex').slice(0, 16);
|
|
234
|
+
const manifest = JSON.parse(await readFile(path.join(BRAND, 'sources.json'), 'utf8'));
|
|
235
|
+
const stale = [];
|
|
236
|
+
for (const [rel, recorded] of Object.entries(manifest.masters)) {
|
|
237
|
+
const actual = sha(await readFile(path.join(root, rel), 'utf8'));
|
|
238
|
+
if (actual !== recorded) stale.push(`${rel} recorded ${recorded}, now ${actual}`);
|
|
239
|
+
}
|
|
240
|
+
if (stale.length) {
|
|
241
|
+
console.error('\n Brand assets are stale. These masters have changed since');
|
|
242
|
+
console.error(' tools/make-brand.mjs last ran:\n');
|
|
243
|
+
for (const line of stale) console.error(` ${line}`);
|
|
244
|
+
console.error('\n Run: node tools/make-brand.mjs\n');
|
|
245
|
+
process.exit(1);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// ---- JS -----------------------------------------------------------------
|
|
249
|
+
const scripts = ['deck.js', 'deck-extras.js', 'deck-adapters.js'];
|
|
250
|
+
const sources = await Promise.all(
|
|
251
|
+
scripts.map(f => readFile(path.join(JS, f), 'utf8'))
|
|
252
|
+
);
|
|
253
|
+
|
|
254
|
+
for (const [i, name] of scripts.entries()) {
|
|
255
|
+
// The unminified copy is what most people load, so ship it verbatim
|
|
256
|
+
await writeFile(path.join(DIST, name), sources[i]);
|
|
257
|
+
if (!esbuild) continue;
|
|
258
|
+
const { code } = await esbuild.transform(sources[i], {
|
|
259
|
+
loader: 'js', minify: true, target: 'es2022'
|
|
260
|
+
});
|
|
261
|
+
await writeFile(path.join(DIST, name.replace('.js', '.min.js')), code);
|
|
262
|
+
report(name, sources[i], code, name.replace('.js', '.min.js'));
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Everything in one request
|
|
266
|
+
const bundle = sources.join('\n');
|
|
267
|
+
await writeFile(path.join(DIST, 'deck.bundle.js'), bundle);
|
|
268
|
+
if (esbuild) {
|
|
269
|
+
const { code } = await esbuild.transform(bundle, { loader: 'js', minify: true, target: 'es2022' });
|
|
270
|
+
await writeFile(path.join(DIST, 'deck.bundle.min.js'), code);
|
|
271
|
+
report('deck.bundle.js', bundle, code, 'deck.bundle.min.js');
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// ESM entry, for anyone importing Deck from a bundler
|
|
275
|
+
await writeFile(path.join(DIST, 'deck.esm.js'),
|
|
276
|
+
`/* Deck v${pkg.version} — ES module entry.\n` +
|
|
277
|
+
` The scripts attach Deck to globalThis; this re-exports it so that\n` +
|
|
278
|
+
` \`import Deck from '@echodial/deck'\` behaves the way you expect. */\n` +
|
|
279
|
+
bundle + '\nexport default globalThis.Deck;\nexport { globalThis as __deckGlobal };\n');
|
|
280
|
+
|
|
281
|
+
// ---- Sizes ---------------------------------------------------------------
|
|
282
|
+
// One place for every published figure. dist/sizes.json is the single source
|
|
283
|
+
// of truth: the demo page reads it at render time, and tools/sync-sizes.mjs
|
|
284
|
+
// writes it into README.md and llms.txt. Nothing is typed by hand.
|
|
285
|
+
const sum = (...names) => ({
|
|
286
|
+
gzip: names.reduce((n, f) => n + SIZES[f].gzip, 0),
|
|
287
|
+
brotli: names.reduce((n, f) => n + SIZES[f].brotli, 0),
|
|
288
|
+
});
|
|
289
|
+
const previous = await readFile(path.join(DIST, 'sizes.json'), 'utf8')
|
|
290
|
+
.then(JSON.parse).catch(() => ({}));
|
|
291
|
+
const sizes = {
|
|
292
|
+
version: pkg.version,
|
|
293
|
+
note: 'Generated by build.mjs. Do not edit. Brotli is quality 11 with a size hint.',
|
|
294
|
+
files: SIZES,
|
|
295
|
+
totals: {
|
|
296
|
+
// A page that loads the stylesheet, the sprite, and the optional core JS
|
|
297
|
+
core: sum('deck.min.css', 'deck-icons.svg', 'deck.min.js'),
|
|
298
|
+
// The same page with the full bundle instead of deck.min.js
|
|
299
|
+
bundle: sum('deck.min.css', 'deck-icons.svg', 'deck.bundle.min.js'),
|
|
300
|
+
},
|
|
301
|
+
// Owned by tools/icons/build-icons.mjs, which needs the font. Preserved
|
|
302
|
+
// across builds so a plain `npm run build` never drops it.
|
|
303
|
+
sample: previous.sample ?? null,
|
|
304
|
+
};
|
|
305
|
+
await writeFile(path.join(DIST, 'sizes.json'), JSON.stringify(sizes, null, 2) + '\n');
|
|
306
|
+
|
|
307
|
+
// ---- Documentation --------------------------------------------------------
|
|
308
|
+
// The class inventory is generated from src/ and every docs page is checked
|
|
309
|
+
// against it, so a class that is added, renamed, or removed cannot silently
|
|
310
|
+
// leave the documentation describing something that no longer exists.
|
|
311
|
+
try {
|
|
312
|
+
const { default: docsApi } = await import('./tools/docs/extract.mjs');
|
|
313
|
+
console.log(
|
|
314
|
+
` ${'api.json'.padEnd(22)} ${String(docsApi.counts.classes).padStart(6)} classes, ` +
|
|
315
|
+
`${docsApi.counts.documented} documented, ${docsApi.counts.undocumented} outstanding`
|
|
316
|
+
);
|
|
317
|
+
await import('./tools/docs/usage.mjs');
|
|
318
|
+
await import('./tools/docs/classify.mjs');
|
|
319
|
+
const { verifyDocs } = await import('./tools/docs/verify.mjs');
|
|
320
|
+
verifyDocs();
|
|
321
|
+
} catch (err) {
|
|
322
|
+
if (err.code !== 'ERR_MODULE_NOT_FOUND') throw err;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
try {
|
|
326
|
+
const { syncDocs } = await import('./tools/sync-sizes.mjs');
|
|
327
|
+
const touched = await syncDocs(sizes, root);
|
|
328
|
+
console.log(`\n ${'sizes.json'.padEnd(22)} ${Object.keys(SIZES).length} files measured`);
|
|
329
|
+
for (const [f, n] of touched) console.log(` ${f.padEnd(22)} ${String(n).padStart(6)} figures synced`);
|
|
330
|
+
} catch (err) {
|
|
331
|
+
// tools/ is not shipped to npm, so an install can still run this build.
|
|
332
|
+
if (err.code !== 'ERR_MODULE_NOT_FOUND') throw err;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
console.log('\nDone.\n');
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
build().catch(err => { console.error(err); process.exit(1); });
|