@birdapi/velinstyle 1.2.0 → 1.2.2
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/README.de.md +150 -24
- package/README.md +155 -33
- package/cli/__fixtures__/atelier-library/showcases/01-login/app.css +1 -0
- package/cli/__fixtures__/atelier-library/showcases/01-login/app.js +2 -0
- package/cli/__fixtures__/atelier-library/showcases/01-login/index.html +5 -0
- package/cli/__fixtures__/atelier-library/showcases/04-pricing/app.css +1 -0
- package/cli/__fixtures__/atelier-library/showcases/04-pricing/app.js +2 -0
- package/cli/__fixtures__/atelier-library/showcases/04-pricing/index.html +5 -0
- package/cli/__fixtures__/atelier-library/showcases/36-calendar/app.css +1 -0
- package/cli/__fixtures__/atelier-library/showcases/36-calendar/app.js +2 -0
- package/cli/__fixtures__/atelier-library/showcases/36-calendar/index.html +5 -0
- package/cli/atelier-catalog.json +1267 -0
- package/cli/atelier-formats.js +159 -0
- package/cli/atelier.js +382 -0
- package/cli/blueprints/empty-state.html +3 -5
- package/cli/cli-manifest.json +23 -10
- package/cli/docgen/extract-attributes.js +2 -0
- package/cli/docgen/extract-cli.js +1 -1
- package/cli/index.js +181 -6
- package/cli/production/component-graph.json +166 -0
- package/cli/production/explain.js +52 -0
- package/cli/production/extract.js +238 -0
- package/cli/production/graph.js +102 -0
- package/cli/production/report.js +78 -0
- package/cli/production/run.js +312 -0
- package/cli/production/trim-css.js +177 -0
- package/cli/production/trim-fonts.js +23 -0
- package/cli/production/trim-icons.js +38 -0
- package/cli/production/trim-js.js +48 -0
- package/cli/production/trim-motion.js +22 -0
- package/cli/production/trim-themes.js +50 -0
- package/cli/production/watch.js +38 -0
- package/cli/review.js +48 -1
- package/cli/scripts/write-atelier-fixtures.mjs +33 -0
- package/cli/transparency.js +221 -0
- package/components/index.js +3 -0
- package/components/runtime/component-loaders.js +3 -0
- package/components/velin-drawer.js +43 -4
- package/components/velin-empty-state.js +83 -0
- package/components/velin-modal.js +76 -15
- package/components/velin-otp-input.js +220 -0
- package/components/velin-password-strength.js +147 -0
- package/components/velin-sheet.js +49 -4
- package/core/a11y/component-contracts.json +23 -4
- package/core/attributes/registry.js +14 -0
- package/core/meta/knowledge/components.json +75 -3
- package/core/meta/schema.js +10 -0
- package/core/transparency/attach.js +74 -0
- package/core/transparency/claims.js +97 -0
- package/core/transparency/doctor.js +142 -0
- package/core/transparency/engine.js +75 -0
- package/core/transparency/export.js +98 -0
- package/core/transparency/index.js +30 -0
- package/core/transparency/migrate.js +130 -0
- package/core/transparency/normalize.js +125 -0
- package/core/transparency/policy.js +105 -0
- package/core/transparency/providers.js +235 -0
- package/core/transparency/registry.js +104 -0
- package/core/transparency/renderer.js +111 -0
- package/core/transparency/reporter.js +113 -0
- package/core/transparency/validator.js +112 -0
- package/dist/chunks/attach-H2ZSEAE6.js +365 -0
- package/dist/chunks/attributes-2ORR27KA.js +404 -0
- package/dist/chunks/attributes-DZCXX2RZ.js +404 -0
- package/dist/chunks/chunk-CQMTCEI6.js +113 -0
- package/dist/chunks/chunk-Y6HN7HWX.js +116 -0
- package/dist/chunks/runtime-entry.js +1 -1
- package/dist/chunks/velin-drawer-A7Q7EBOS.js +162 -0
- package/dist/chunks/velin-empty-state-3NTUH2RF.js +81 -0
- package/dist/chunks/velin-modal-3MV4FYBK.js +231 -0
- package/dist/chunks/velin-otp-input-PSK42MM6.js +207 -0
- package/dist/chunks/velin-password-strength-TAXMLSED.js +136 -0
- package/dist/chunks/velin-sheet-N2VVYXFP.js +157 -0
- package/dist/llms.txt +5 -4
- package/dist/search-index.json +94 -5
- package/dist/velin-agent.json +290 -23
- package/dist/velinstyle-components.iife.js +3189 -2202
- package/dist/velinstyle-components.js +3187 -2196
- package/dist/velinstyle-components.min.js +261 -119
- package/dist/velinstyle.css +214 -6
- package/dist/velinstyle.d.ts +3 -0
- package/dist/velinstyle.min.css +1 -1
- package/package.json +143 -142
- package/packages/velinstyle-skills/catalog.json +1 -1
- package/src/components/data-table.css +19 -0
- package/src/components/empty-auth.css +33 -0
- package/src/components/table.css +16 -6
- package/src/components/transparency.css +138 -0
- package/src/velinstyle.css +2 -0
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Production Builder orchestration.
|
|
3
|
+
*/
|
|
4
|
+
import {
|
|
5
|
+
existsSync, readFileSync, writeFileSync, mkdirSync, unlinkSync, statSync, readdirSync,
|
|
6
|
+
} from 'fs';
|
|
7
|
+
import { join, resolve, dirname } from 'path';
|
|
8
|
+
import { execSync } from 'child_process';
|
|
9
|
+
import { fileURLToPath, pathToFileURL } from 'url';
|
|
10
|
+
import { extractProject, extractToPlain, DEFAULT_IGNORE } from './extract.js';
|
|
11
|
+
import { resolveClosure, listKnownThemeNames } from './graph.js';
|
|
12
|
+
import { buildProductionCss } from './trim-css.js';
|
|
13
|
+
import { buildProductionJs, detectRuntimeFeatures } from './trim-js.js';
|
|
14
|
+
import { resolveUsedThemes, writeThemes } from './trim-themes.js';
|
|
15
|
+
import { buildIconSpriteSubset } from './trim-icons.js';
|
|
16
|
+
import { shouldIncludeMotion, motionClassSet } from './trim-motion.js';
|
|
17
|
+
import { buildReport } from './report.js';
|
|
18
|
+
import { buildExplain } from './explain.js';
|
|
19
|
+
import { watchProduction } from './watch.js';
|
|
20
|
+
|
|
21
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
22
|
+
export const PKG_ROOT = join(__dirname, '../..');
|
|
23
|
+
|
|
24
|
+
function parseSafelist(raw) {
|
|
25
|
+
if (!raw) return [];
|
|
26
|
+
if (Array.isArray(raw)) return raw.map(String);
|
|
27
|
+
const s = String(raw);
|
|
28
|
+
if (existsSync(s) && statSync(s).isFile()) {
|
|
29
|
+
return readFileSync(s, 'utf-8')
|
|
30
|
+
.split(/\r?\n/)
|
|
31
|
+
.map((l) => l.trim())
|
|
32
|
+
.filter((l) => l && !l.startsWith('#'));
|
|
33
|
+
}
|
|
34
|
+
return s.split(/[,]+/).map((x) => x.trim()).filter(Boolean);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function loadUserConfig(cwd) {
|
|
38
|
+
const configPath = resolve(cwd, 'velinstyle.config.js');
|
|
39
|
+
if (!existsSync(configPath)) return {};
|
|
40
|
+
try {
|
|
41
|
+
const mod = await import(pathToFileURL(configPath).href);
|
|
42
|
+
return mod.default || {};
|
|
43
|
+
} catch {
|
|
44
|
+
return {};
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function lightningBundle(css, outFile, { minify = true } = {}) {
|
|
49
|
+
mkdirSync(dirname(outFile), { recursive: true });
|
|
50
|
+
const tmpPath = resolve(dirname(outFile), '.velin-production-tmp.css');
|
|
51
|
+
writeFileSync(tmpPath, css);
|
|
52
|
+
let ok = false;
|
|
53
|
+
try {
|
|
54
|
+
const binName = process.platform === 'win32' ? 'lightningcss.cmd' : 'lightningcss';
|
|
55
|
+
const localBin = join(PKG_ROOT, 'node_modules', '.bin', binName);
|
|
56
|
+
const cli = existsSync(localBin) ? `"${localBin}"` : 'npx lightningcss';
|
|
57
|
+
const minifyFlag = minify ? '--minify' : '';
|
|
58
|
+
execSync(`${cli} --bundle ${minifyFlag} "${tmpPath}" -o "${outFile}"`, {
|
|
59
|
+
stdio: 'pipe',
|
|
60
|
+
cwd: PKG_ROOT,
|
|
61
|
+
shell: true,
|
|
62
|
+
});
|
|
63
|
+
ok = true;
|
|
64
|
+
} catch {
|
|
65
|
+
writeFileSync(outFile, css);
|
|
66
|
+
} finally {
|
|
67
|
+
try { unlinkSync(tmpPath); } catch { /* ignore */ }
|
|
68
|
+
}
|
|
69
|
+
return ok;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function originalBytesEstimate() {
|
|
73
|
+
let n = 0;
|
|
74
|
+
const css = join(PKG_ROOT, 'dist', 'velinstyle.min.css');
|
|
75
|
+
const js = join(PKG_ROOT, 'dist', 'velinstyle-components.min.js');
|
|
76
|
+
const icons = join(PKG_ROOT, 'dist', 'velin-icons.svg');
|
|
77
|
+
for (const f of [css, js, icons]) {
|
|
78
|
+
if (existsSync(f)) n += statSync(f).size;
|
|
79
|
+
}
|
|
80
|
+
const themeDir = join(PKG_ROOT, 'dist', 'themes');
|
|
81
|
+
if (existsSync(themeDir)) {
|
|
82
|
+
for (const f of readdirSync(themeDir)) {
|
|
83
|
+
if (f.endsWith('.css')) n += statSync(join(themeDir, f)).size;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return n;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* @param {object} opts
|
|
91
|
+
* @param {string[]} [opts.args] raw CLI args after command
|
|
92
|
+
* @param {object} [opts.helpers] { getArg, hasFlag, C }
|
|
93
|
+
*/
|
|
94
|
+
export async function runProduction(opts = {}) {
|
|
95
|
+
const {
|
|
96
|
+
args = [],
|
|
97
|
+
helpers = {},
|
|
98
|
+
cwd = process.cwd(),
|
|
99
|
+
} = opts;
|
|
100
|
+
const getArg = helpers.getArg || ((flag, alias) => {
|
|
101
|
+
const i = args.indexOf(flag);
|
|
102
|
+
const a = alias ? args.indexOf(alias) : -1;
|
|
103
|
+
const idx = i !== -1 ? i : a;
|
|
104
|
+
return idx !== -1 && args[idx + 1] ? args[idx + 1] : null;
|
|
105
|
+
});
|
|
106
|
+
const hasFlag = helpers.hasFlag || ((flag, alias) => args.includes(flag) || (alias && args.includes(alias)));
|
|
107
|
+
const C = helpers.C || {
|
|
108
|
+
green: (s) => s, yellow: (s) => s, cyan: (s) => s, dim: (s) => s, bold: (s) => s, red: (s) => s,
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const config = await loadUserConfig(cwd);
|
|
112
|
+
const prodCfg = config.production || {};
|
|
113
|
+
|
|
114
|
+
const pathArg = args.find((a) => a && !a.startsWith('-')) || '.';
|
|
115
|
+
const targetPath = resolve(cwd, pathArg);
|
|
116
|
+
const outDir = resolve(cwd, getArg('--out', '-o') || prodCfg.out || './dist/velin-production');
|
|
117
|
+
const explain = hasFlag('--explain');
|
|
118
|
+
const doWatch = hasFlag('--watch');
|
|
119
|
+
const reportPath = getArg('--report') || null;
|
|
120
|
+
const safelist = [
|
|
121
|
+
...parseSafelist(prodCfg.safelist),
|
|
122
|
+
...parseSafelist(getArg('--safelist')),
|
|
123
|
+
];
|
|
124
|
+
const noJs = hasFlag('--no-js');
|
|
125
|
+
const noIcons = hasFlag('--no-icons');
|
|
126
|
+
const noThemes = hasFlag('--no-themes');
|
|
127
|
+
const noMotion = hasFlag('--no-motion');
|
|
128
|
+
const minify = !hasFlag('--no-minify');
|
|
129
|
+
|
|
130
|
+
const once = async () => {
|
|
131
|
+
mkdirSync(outDir, { recursive: true });
|
|
132
|
+
|
|
133
|
+
const extracted = extractProject(targetPath, {
|
|
134
|
+
ignore: DEFAULT_IGNORE,
|
|
135
|
+
safelist,
|
|
136
|
+
outDir,
|
|
137
|
+
cwd,
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
const closure = resolveClosure(
|
|
141
|
+
{ classes: extracted.classes, tags: extracted.tags },
|
|
142
|
+
{
|
|
143
|
+
safelistFiles: safelist.filter((s) => s.endsWith('.css') || s.includes('/')),
|
|
144
|
+
},
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
const includeMotion = shouldIncludeMotion(extracted, { force: noMotion ? false : null });
|
|
148
|
+
const motionUsed = motionClassSet(extracted);
|
|
149
|
+
|
|
150
|
+
const utilitySet = new Set(closure.utilityFiles);
|
|
151
|
+
const componentFiles = closure.cssFiles.filter((f) => !utilitySet.has(f));
|
|
152
|
+
const utilityFiles = closure.utilityFiles.filter((f) => {
|
|
153
|
+
if (!includeMotion && /animation|view-transition|scroll-animation|chart-animation/.test(f)) return false;
|
|
154
|
+
return true;
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
const cssResult = buildProductionCss({
|
|
158
|
+
pkgRoot: PKG_ROOT,
|
|
159
|
+
readFile: (p) => readFileSync(p, 'utf-8'),
|
|
160
|
+
exists: existsSync,
|
|
161
|
+
componentFiles,
|
|
162
|
+
utilityFiles,
|
|
163
|
+
usedClasses: extracted.classes,
|
|
164
|
+
includeMotionFiles: includeMotion,
|
|
165
|
+
motionUsed,
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
const cssOut = join(outDir, 'velinstyle.css');
|
|
169
|
+
const bundled = lightningBundle(cssResult.css, cssOut, { minify });
|
|
170
|
+
const cssBytes = existsSync(cssOut) ? statSync(cssOut).size : Buffer.byteLength(cssResult.css);
|
|
171
|
+
|
|
172
|
+
let jsBytes = 0;
|
|
173
|
+
let jsOut = null;
|
|
174
|
+
const features = detectRuntimeFeatures(extracted);
|
|
175
|
+
if (!noJs) {
|
|
176
|
+
jsOut = join(outDir, 'velinstyle.js');
|
|
177
|
+
const js = buildProductionJs({ tags: closure.tags, features });
|
|
178
|
+
writeFileSync(jsOut, js);
|
|
179
|
+
jsBytes = Buffer.byteLength(js);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
let themeMeta = { selected: [], skipped: [], written: [] };
|
|
183
|
+
if (!noThemes) {
|
|
184
|
+
const resolved = resolveUsedThemes(extracted, {
|
|
185
|
+
configThemes: prodCfg.themes ?? 'auto',
|
|
186
|
+
pkgRoot: PKG_ROOT,
|
|
187
|
+
defaultThemes: [],
|
|
188
|
+
});
|
|
189
|
+
const written = writeThemes({
|
|
190
|
+
pkgRoot: PKG_ROOT,
|
|
191
|
+
outDir,
|
|
192
|
+
themes: resolved.selected,
|
|
193
|
+
minifyPrefer: minify,
|
|
194
|
+
});
|
|
195
|
+
themeMeta = { ...resolved, ...written };
|
|
196
|
+
} else {
|
|
197
|
+
themeMeta.skipped = listKnownThemeNames(PKG_ROOT);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
let iconMeta = { included: [], skipped: [], bytes: 0, availableCount: 0 };
|
|
201
|
+
if (!noIcons) {
|
|
202
|
+
iconMeta = buildIconSpriteSubset({
|
|
203
|
+
pkgRoot: PKG_ROOT,
|
|
204
|
+
outDir,
|
|
205
|
+
iconNames: extracted.icons,
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const themeBytes = (themeMeta.written || []).reduce((n, w) => {
|
|
210
|
+
try { return n + statSync(w.file).size; } catch { return n; }
|
|
211
|
+
}, 0);
|
|
212
|
+
|
|
213
|
+
const productionBytes = cssBytes + jsBytes + (iconMeta.bytes || 0) + themeBytes;
|
|
214
|
+
const original = originalBytesEstimate();
|
|
215
|
+
|
|
216
|
+
const fullCss = existsSync(join(PKG_ROOT, 'dist', 'velinstyle.min.css'))
|
|
217
|
+
? statSync(join(PKG_ROOT, 'dist', 'velinstyle.min.css')).size : 0;
|
|
218
|
+
const fullJs = existsSync(join(PKG_ROOT, 'dist', 'velinstyle-components.min.js'))
|
|
219
|
+
? statSync(join(PKG_ROOT, 'dist', 'velinstyle-components.min.js')).size : 0;
|
|
220
|
+
const fullIcons = existsSync(join(PKG_ROOT, 'dist', 'velin-icons.svg'))
|
|
221
|
+
? statSync(join(PKG_ROOT, 'dist', 'velin-icons.svg')).size : 0;
|
|
222
|
+
|
|
223
|
+
let themeSaved = 0;
|
|
224
|
+
for (const name of themeMeta.skipped || []) {
|
|
225
|
+
const p = join(PKG_ROOT, 'dist', 'themes', `${name}.min.css`);
|
|
226
|
+
if (existsSync(p)) themeSaved += statSync(p).size;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const report = buildReport({
|
|
230
|
+
originalBytes: original || (fullCss + fullJs + fullIcons),
|
|
231
|
+
productionBytes,
|
|
232
|
+
breakdown: {
|
|
233
|
+
CSS: Math.max(0, fullCss - cssBytes),
|
|
234
|
+
JS: Math.max(0, fullJs - jsBytes),
|
|
235
|
+
Icons: Math.max(0, fullIcons - (iconMeta.bytes || 0)),
|
|
236
|
+
Themes: themeSaved,
|
|
237
|
+
Motion: includeMotion ? 0 : 8 * 1024,
|
|
238
|
+
Fonts: 2 * 1024,
|
|
239
|
+
},
|
|
240
|
+
themes: themeMeta.selected || [],
|
|
241
|
+
icons: iconMeta.included || [],
|
|
242
|
+
components: cssResult.included,
|
|
243
|
+
tags: closure.tags,
|
|
244
|
+
filesScanned: extracted.files.length,
|
|
245
|
+
imagesSkipped: true,
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
writeFileSync(join(outDir, 'production-report.json'), JSON.stringify(report, null, 2));
|
|
249
|
+
writeFileSync(join(outDir, 'production-report.txt'), report.text);
|
|
250
|
+
writeFileSync(join(outDir, 'used.json'), JSON.stringify({
|
|
251
|
+
...extractToPlain(extracted),
|
|
252
|
+
closure: {
|
|
253
|
+
cssFiles: closure.cssFiles,
|
|
254
|
+
tags: closure.tags,
|
|
255
|
+
matchedPrefixes: closure.matchedPrefixes,
|
|
256
|
+
},
|
|
257
|
+
}, null, 2));
|
|
258
|
+
|
|
259
|
+
if (reportPath) {
|
|
260
|
+
const absReport = resolve(cwd, reportPath);
|
|
261
|
+
mkdirSync(dirname(absReport), { recursive: true });
|
|
262
|
+
if (absReport.endsWith('.md') || absReport.endsWith('.txt')) {
|
|
263
|
+
writeFileSync(absReport, report.text);
|
|
264
|
+
} else {
|
|
265
|
+
writeFileSync(absReport, JSON.stringify(report, null, 2));
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
let explainOut = null;
|
|
270
|
+
if (explain) {
|
|
271
|
+
explainOut = buildExplain({
|
|
272
|
+
skippedCss: cssResult.skipped,
|
|
273
|
+
skippedThemes: themeMeta.skipped || [],
|
|
274
|
+
skippedIcons: iconMeta.skipped || [],
|
|
275
|
+
skippedMotion: !includeMotion,
|
|
276
|
+
graphExplain: closure.explain,
|
|
277
|
+
});
|
|
278
|
+
writeFileSync(join(outDir, 'explain.txt'), explainOut.text);
|
|
279
|
+
console.log(`\n${explainOut.text}`);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
console.log(C.green(`\nProduction build → ${outDir}`));
|
|
283
|
+
console.log(report.text);
|
|
284
|
+
console.log(C.dim(`\nCSS: ${cssOut}${bundled ? ' (lightningcss)' : ' (raw)'}`));
|
|
285
|
+
if (jsOut) console.log(C.dim(`JS: ${jsOut} (${closure.tags.length} components)`));
|
|
286
|
+
if (!noIcons) console.log(C.dim(`Icons: ${iconMeta.included.length} / ${iconMeta.availableCount || '?'}`));
|
|
287
|
+
if (!noThemes) console.log(C.dim(`Themes: ${(themeMeta.selected || []).join(', ') || '(none)'}`));
|
|
288
|
+
|
|
289
|
+
return {
|
|
290
|
+
outDir,
|
|
291
|
+
report,
|
|
292
|
+
explain: explainOut,
|
|
293
|
+
extracted,
|
|
294
|
+
closure,
|
|
295
|
+
cssOut,
|
|
296
|
+
jsOut,
|
|
297
|
+
};
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
const result = await once();
|
|
301
|
+
|
|
302
|
+
if (doWatch) {
|
|
303
|
+
console.log(C.cyan(`\nWatching ${targetPath} … (Ctrl+C to stop)`));
|
|
304
|
+
watchProduction(targetPath, async () => {
|
|
305
|
+
console.log(C.dim('\n[watch] rebuild…'));
|
|
306
|
+
await once();
|
|
307
|
+
});
|
|
308
|
+
await new Promise(() => {});
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return result;
|
|
312
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Filter CSS source to rules that reference used velin-/expo- classes.
|
|
3
|
+
*/
|
|
4
|
+
import { join } from 'path';
|
|
5
|
+
|
|
6
|
+
const CLASS_IN_SEL = /\.(?:velin|expo)-[\w:-]+/g;
|
|
7
|
+
|
|
8
|
+
const ALWAYS_KEEP_RE = [
|
|
9
|
+
/:root\b/,
|
|
10
|
+
/\[data-velin-/,
|
|
11
|
+
/prefers-reduced-motion/,
|
|
12
|
+
/prefers-contrast/,
|
|
13
|
+
/forced-colors/,
|
|
14
|
+
/@font-face/,
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
function selectorNeedsClass(selector, used) {
|
|
18
|
+
const classes = selector.match(CLASS_IN_SEL);
|
|
19
|
+
if (!classes || !classes.length) return true;
|
|
20
|
+
return classes.some((c) => used.has(c.slice(1)));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function shouldAlwaysKeep(block) {
|
|
24
|
+
return ALWAYS_KEEP_RE.some((re) => re.test(block));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function filterCssByUsedClasses(css, usedClasses, { keepAllWithoutVelinClass = true } = {}) {
|
|
28
|
+
const used = usedClasses instanceof Set ? usedClasses : new Set(usedClasses);
|
|
29
|
+
const keptKeyframes = new Set();
|
|
30
|
+
const chunks = [];
|
|
31
|
+
const parts = splitCssTopLevel(css);
|
|
32
|
+
|
|
33
|
+
for (const part of parts) {
|
|
34
|
+
const trimmed = part.trim();
|
|
35
|
+
if (!trimmed) continue;
|
|
36
|
+
|
|
37
|
+
if (trimmed.startsWith('@keyframes') || trimmed.startsWith('@-webkit-keyframes')) {
|
|
38
|
+
const name = trimmed.match(/@[-a-z]*keyframes\s+([\w-]+)/i)?.[1];
|
|
39
|
+
chunks.push({ type: 'keyframes', name, text: ensureBrace(trimmed) });
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (trimmed.startsWith('@media') || trimmed.startsWith('@supports') || trimmed.startsWith('@container')) {
|
|
44
|
+
const innerMatch = trimmed.match(/^(@[^{]+)\{([\s\S]*)\}$/);
|
|
45
|
+
if (!innerMatch) {
|
|
46
|
+
chunks.push({ type: 'raw', text: ensureBrace(trimmed) });
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
const filteredInner = filterCssByUsedClasses(innerMatch[2], used, { keepAllWithoutVelinClass });
|
|
50
|
+
if (filteredInner.trim()) {
|
|
51
|
+
const block = `${innerMatch[1].trim()} {\n${filteredInner}\n}`;
|
|
52
|
+
chunks.push({ type: 'at', text: block });
|
|
53
|
+
collectAnimationNames(filteredInner, keptKeyframes);
|
|
54
|
+
}
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (shouldAlwaysKeep(trimmed)) {
|
|
59
|
+
chunks.push({ type: 'rule', text: ensureBrace(trimmed) });
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const sel = trimmed.split('{')[0] || '';
|
|
64
|
+
CLASS_IN_SEL.lastIndex = 0;
|
|
65
|
+
const hasVelin = CLASS_IN_SEL.test(sel);
|
|
66
|
+
CLASS_IN_SEL.lastIndex = 0;
|
|
67
|
+
if (!hasVelin) {
|
|
68
|
+
if (keepAllWithoutVelinClass) chunks.push({ type: 'rule', text: ensureBrace(trimmed) });
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (selectorNeedsClass(sel, used)) {
|
|
72
|
+
const text = ensureBrace(trimmed);
|
|
73
|
+
chunks.push({ type: 'rule', text });
|
|
74
|
+
collectAnimationNames(text, keptKeyframes);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const out = [];
|
|
79
|
+
for (const c of chunks) {
|
|
80
|
+
if (c.type === 'keyframes') {
|
|
81
|
+
if (c.name && keptKeyframes.has(c.name)) out.push(c.text);
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
out.push(c.text);
|
|
85
|
+
}
|
|
86
|
+
return out.join('\n\n') + (out.length ? '\n' : '');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function ensureBrace(s) {
|
|
90
|
+
return s.endsWith('}') ? s : `${s}}`;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function collectAnimationNames(text, into) {
|
|
94
|
+
for (const m of text.matchAll(/animation(?:-name)?\s*:\s*([^;}+]+)/gi)) {
|
|
95
|
+
for (const name of m[1].split(/[\s,]+/)) {
|
|
96
|
+
if (name && name !== 'none' && !name.startsWith('var(')) into.add(name.trim());
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function splitCssTopLevel(css) {
|
|
102
|
+
const src = String(css || '').replace(/\/\*[\s\S]*?\*\//g, '');
|
|
103
|
+
const parts = [];
|
|
104
|
+
let depth = 0;
|
|
105
|
+
let start = 0;
|
|
106
|
+
for (let i = 0; i < src.length; i++) {
|
|
107
|
+
const ch = src[i];
|
|
108
|
+
if (ch === '{') depth++;
|
|
109
|
+
else if (ch === '}') {
|
|
110
|
+
depth--;
|
|
111
|
+
if (depth === 0) {
|
|
112
|
+
parts.push(src.slice(start, i + 1));
|
|
113
|
+
start = i + 1;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
if (start < src.length) {
|
|
118
|
+
const tail = src.slice(start).trim();
|
|
119
|
+
if (tail) parts.push(tail);
|
|
120
|
+
}
|
|
121
|
+
return parts;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function buildProductionCss({
|
|
125
|
+
pkgRoot,
|
|
126
|
+
readFile,
|
|
127
|
+
exists,
|
|
128
|
+
componentFiles = [],
|
|
129
|
+
utilityFiles = [],
|
|
130
|
+
usedClasses,
|
|
131
|
+
includeMotionFiles = true,
|
|
132
|
+
motionUsed = null,
|
|
133
|
+
}) {
|
|
134
|
+
const used = usedClasses instanceof Set ? usedClasses : new Set(usedClasses);
|
|
135
|
+
let css = '/* VelinStyle Production Build */\n';
|
|
136
|
+
const included = [];
|
|
137
|
+
const skipped = [];
|
|
138
|
+
const seen = new Set();
|
|
139
|
+
|
|
140
|
+
for (const rel of componentFiles) {
|
|
141
|
+
if (seen.has(rel)) continue;
|
|
142
|
+
seen.add(rel);
|
|
143
|
+
const path = join(pkgRoot, 'src', rel);
|
|
144
|
+
if (!exists(path)) {
|
|
145
|
+
skipped.push({ file: rel, reason: 'missing' });
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
css += readFile(path) + '\n';
|
|
149
|
+
included.push(rel);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
for (const rel of utilityFiles) {
|
|
153
|
+
if (seen.has(rel)) continue;
|
|
154
|
+
seen.add(rel);
|
|
155
|
+
const path = join(pkgRoot, 'src', rel);
|
|
156
|
+
if (!exists(path)) {
|
|
157
|
+
skipped.push({ file: rel, reason: 'missing' });
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
const isMotion = /animation|motion|view-transition|scroll-animation|chart-animation/.test(rel);
|
|
161
|
+
if (isMotion && !includeMotionFiles) {
|
|
162
|
+
skipped.push({ file: rel, reason: 'motion disabled' });
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
const raw = readFile(path);
|
|
166
|
+
const filterSet = isMotion && motionUsed ? new Set([...used, ...motionUsed]) : used;
|
|
167
|
+
const filtered = filterCssByUsedClasses(raw, filterSet);
|
|
168
|
+
if (!filtered.trim()) {
|
|
169
|
+
skipped.push({ file: rel, reason: 'no matching utilities' });
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
css += filtered + '\n';
|
|
173
|
+
included.push(rel);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return { css, included, skipped };
|
|
177
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Font utility trim — keep only used velin-font-* / weight utilities in text.css slice.
|
|
3
|
+
* (Full tokens/typography.css stays in alwaysCss; this filters utility text layer extras.)
|
|
4
|
+
*/
|
|
5
|
+
import { filterCssByUsedClasses } from './trim-css.js';
|
|
6
|
+
|
|
7
|
+
export function filterFontUtilities(cssText, usedClasses) {
|
|
8
|
+
const used = usedClasses instanceof Set ? usedClasses : new Set(usedClasses);
|
|
9
|
+
// Always keep base text utilities that are structural if any font class used
|
|
10
|
+
const keep = new Set(used);
|
|
11
|
+
if ([...used].some((c) => c.startsWith('velin-font-') || c.startsWith('velin-text-'))) {
|
|
12
|
+
keep.add('velin-font-sans');
|
|
13
|
+
keep.add('velin-font-normal');
|
|
14
|
+
}
|
|
15
|
+
return filterCssByUsedClasses(cssText, keep, { keepAllWithoutVelinClass: false });
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function summarizeFontUsage(extracted) {
|
|
19
|
+
const classes = [...(extracted.fontWeights || extracted.classes || [])]
|
|
20
|
+
.filter((c) => String(c).startsWith('velin-font-') || String(c).startsWith('velin-text-'))
|
|
21
|
+
.sort();
|
|
22
|
+
return { classes, count: classes.length };
|
|
23
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Icon sprite trim — subset of icons/svg → production sprite.
|
|
3
|
+
*/
|
|
4
|
+
import { existsSync, readdirSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
|
|
5
|
+
import { join, basename } from 'path';
|
|
6
|
+
|
|
7
|
+
export function buildIconSpriteSubset({ pkgRoot, outDir, iconNames }) {
|
|
8
|
+
const svgDir = join(pkgRoot, 'icons', 'svg');
|
|
9
|
+
const names = new Set([...(iconNames || [])].map((n) => String(n).toLowerCase()));
|
|
10
|
+
const available = existsSync(svgDir)
|
|
11
|
+
? readdirSync(svgDir).filter((f) => f.endsWith('.svg')).map((f) => basename(f, '.svg'))
|
|
12
|
+
: [];
|
|
13
|
+
|
|
14
|
+
const selected = available.filter((id) => names.has(id.toLowerCase()));
|
|
15
|
+
// If icons were requested but none match, still emit empty sprite shell when WC icon used
|
|
16
|
+
const includeAllIfEmptyScan = names.size === 0;
|
|
17
|
+
const ids = includeAllIfEmptyScan ? [] : selected;
|
|
18
|
+
const skipped = available.filter((id) => !ids.includes(id));
|
|
19
|
+
|
|
20
|
+
const symbols = ids.map((id) => {
|
|
21
|
+
let content = readFileSync(join(svgDir, `${id}.svg`), 'utf-8');
|
|
22
|
+
content = content.replace(/<svg[^>]*>/, '').replace(/<\/svg>/, '').trim();
|
|
23
|
+
return ` <symbol id="${id}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">\n ${content}\n </symbol>`;
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const sprite = `<svg xmlns="http://www.w3.org/2000/svg" style="display:none">\n${symbols.join('\n')}\n</svg>\n`;
|
|
27
|
+
mkdirSync(outDir, { recursive: true });
|
|
28
|
+
const outFile = join(outDir, 'velin-icons.svg');
|
|
29
|
+
writeFileSync(outFile, sprite, 'utf-8');
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
outFile,
|
|
33
|
+
included: ids,
|
|
34
|
+
skipped,
|
|
35
|
+
availableCount: available.length,
|
|
36
|
+
bytes: Buffer.byteLength(sprite, 'utf-8'),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Emit a slim runtime stub that registers only detected Web Components.
|
|
3
|
+
*/
|
|
4
|
+
export function buildProductionJs({ tags = [], features = {} } = {}) {
|
|
5
|
+
const unique = [...new Set(tags.map((t) => String(t).toLowerCase()))].sort();
|
|
6
|
+
const list = JSON.stringify(unique, null, 2);
|
|
7
|
+
const wantAttributes = Boolean(features.attributes);
|
|
8
|
+
const wantHighlight = Boolean(features.highlight);
|
|
9
|
+
const wantMotion = Boolean(features.motion);
|
|
10
|
+
|
|
11
|
+
return `/* VelinStyle Production Runtime — generated */
|
|
12
|
+
/* Registers only Web Components detected in your project. */
|
|
13
|
+
|
|
14
|
+
const TAGS = ${list};
|
|
15
|
+
|
|
16
|
+
export async function bootProduction(options = {}) {
|
|
17
|
+
const { register, bootFromDOM } = await import('@birdapi/velinstyle/runtime');
|
|
18
|
+
if (options.root || ${wantAttributes} || ${wantHighlight} || ${wantMotion}) {
|
|
19
|
+
return bootFromDOM(options.root || (typeof document !== 'undefined' ? document : undefined), {
|
|
20
|
+
tags: TAGS,
|
|
21
|
+
attributes: options.attributes ?? ${wantAttributes},
|
|
22
|
+
highlight: options.highlight ?? ${wantHighlight},
|
|
23
|
+
haptic: options.haptic ?? false,
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
return register(TAGS);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export { TAGS };
|
|
30
|
+
|
|
31
|
+
if (typeof document !== 'undefined' && !globalThis.__VELIN_PRODUCTION_NO_AUTOBOOT__) {
|
|
32
|
+
bootProduction().catch((err) => {
|
|
33
|
+
console.warn('[velinstyle production] boot failed:', err);
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function detectRuntimeFeatures(extracted) {
|
|
40
|
+
const attrs = extracted.attrs || new Set();
|
|
41
|
+
const classes = extracted.classes || new Set();
|
|
42
|
+
const motion = extracted.motion || new Set();
|
|
43
|
+
return {
|
|
44
|
+
attributes: [...attrs].some((a) => a.startsWith('data-velin-') && a !== 'data-velin-theme' && a !== 'data-velin-component'),
|
|
45
|
+
highlight: [...classes].some((c) => c.includes('highlight') || c.includes('code')),
|
|
46
|
+
motion: motion.size > 0 || [...classes].some((c) => c.includes('animate')),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Motion trim helpers — decide whether motion CSS files participate + explain removals.
|
|
3
|
+
*/
|
|
4
|
+
export function shouldIncludeMotion(extracted, { force = null } = {}) {
|
|
5
|
+
if (force === true) return true;
|
|
6
|
+
if (force === false) return false;
|
|
7
|
+
const motion = extracted.motion || new Set();
|
|
8
|
+
const classes = extracted.classes || new Set();
|
|
9
|
+
if (motion.size > 0) return true;
|
|
10
|
+
for (const c of classes) {
|
|
11
|
+
if (/animate|motion|transition|scroll-fade|scroll-reveal/i.test(c)) return true;
|
|
12
|
+
}
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function motionClassSet(extracted) {
|
|
17
|
+
const set = new Set(extracted.motion || []);
|
|
18
|
+
for (const c of extracted.classes || []) {
|
|
19
|
+
if (/animate|motion|scroll-/i.test(c)) set.add(c);
|
|
20
|
+
}
|
|
21
|
+
return set;
|
|
22
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Theme trim — copy only used theme CSS into production output.
|
|
3
|
+
*/
|
|
4
|
+
import { existsSync, readFileSync, copyFileSync, mkdirSync, writeFileSync } from 'fs';
|
|
5
|
+
import { join } from 'path';
|
|
6
|
+
import { listKnownThemeNames } from './graph.js';
|
|
7
|
+
|
|
8
|
+
export function resolveUsedThemes(extracted, { configThemes = 'auto', pkgRoot, defaultThemes = ['dark'] } = {}) {
|
|
9
|
+
const known = new Set(listKnownThemeNames(pkgRoot));
|
|
10
|
+
const used = new Set();
|
|
11
|
+
|
|
12
|
+
if (Array.isArray(configThemes)) {
|
|
13
|
+
for (const t of configThemes) used.add(String(t).toLowerCase());
|
|
14
|
+
} else {
|
|
15
|
+
for (const t of extracted.themes || []) used.add(String(t).toLowerCase());
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Always allow dark / light token paths if referenced via contrast toggles
|
|
19
|
+
for (const d of defaultThemes) {
|
|
20
|
+
if (known.has(d)) used.add(d);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// If nothing detected and auto — keep no optional themes (tokens cover default)
|
|
24
|
+
const selected = [...used].filter((t) => known.has(t)).sort();
|
|
25
|
+
const skipped = [...known].filter((t) => !used.has(t)).sort();
|
|
26
|
+
return { selected, skipped, known: [...known].sort() };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function writeThemes({ pkgRoot, outDir, themes, minifyPrefer = true }) {
|
|
30
|
+
const themeOut = join(outDir, 'themes');
|
|
31
|
+
mkdirSync(themeOut, { recursive: true });
|
|
32
|
+
const written = [];
|
|
33
|
+
const missing = [];
|
|
34
|
+
|
|
35
|
+
for (const name of themes) {
|
|
36
|
+
const distMin = join(pkgRoot, 'dist', 'themes', `${name}.min.css`);
|
|
37
|
+
const src = join(pkgRoot, 'src', 'themes', `${name}.css`);
|
|
38
|
+
const dest = join(themeOut, minifyPrefer && existsSync(distMin) ? `${name}.min.css` : `${name}.css`);
|
|
39
|
+
if (minifyPrefer && existsSync(distMin)) {
|
|
40
|
+
copyFileSync(distMin, dest);
|
|
41
|
+
written.push({ name, file: dest, source: 'dist' });
|
|
42
|
+
} else if (existsSync(src)) {
|
|
43
|
+
writeFileSync(dest, readFileSync(src, 'utf-8'));
|
|
44
|
+
written.push({ name, file: dest, source: 'src' });
|
|
45
|
+
} else {
|
|
46
|
+
missing.push(name);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return { written, missing, themeOut };
|
|
50
|
+
}
|