@birdapi/velinstyle 1.2.1 → 1.3.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.
Files changed (156) hide show
  1. package/README.de.md +148 -11
  2. package/README.md +174 -16
  3. package/cli/__fixtures__/atelier-library/showcases/01-login/app.css +1 -0
  4. package/cli/__fixtures__/atelier-library/showcases/01-login/app.js +2 -0
  5. package/cli/__fixtures__/atelier-library/showcases/01-login/index.html +5 -0
  6. package/cli/__fixtures__/atelier-library/showcases/04-pricing/app.css +1 -0
  7. package/cli/__fixtures__/atelier-library/showcases/04-pricing/app.js +2 -0
  8. package/cli/__fixtures__/atelier-library/showcases/04-pricing/index.html +5 -0
  9. package/cli/__fixtures__/atelier-library/showcases/36-calendar/app.css +1 -0
  10. package/cli/__fixtures__/atelier-library/showcases/36-calendar/app.js +2 -0
  11. package/cli/__fixtures__/atelier-library/showcases/36-calendar/index.html +5 -0
  12. package/cli/atelier-catalog.json +1267 -0
  13. package/cli/atelier-formats.js +159 -0
  14. package/cli/atelier.js +382 -0
  15. package/cli/blueprints/empty-state.html +3 -5
  16. package/cli/cli-manifest.json +441 -161
  17. package/cli/docgen/extract-cli.js +2 -1
  18. package/cli/docs-generate.js +9 -0
  19. package/cli/experience.js +173 -0
  20. package/cli/index.js +1384 -361
  21. package/cli/motion.js +738 -0
  22. package/cli/production/component-graph.json +166 -0
  23. package/cli/production/explain.js +52 -0
  24. package/cli/production/extract.js +278 -0
  25. package/cli/production/graph.js +102 -0
  26. package/cli/production/report.js +168 -0
  27. package/cli/production/run.js +355 -0
  28. package/cli/production/trim-css.js +177 -0
  29. package/cli/production/trim-fonts.js +23 -0
  30. package/cli/production/trim-icons.js +38 -0
  31. package/cli/production/trim-js.js +48 -0
  32. package/cli/production/trim-motion.js +22 -0
  33. package/cli/production/trim-themes.js +50 -0
  34. package/cli/production/watch.js +38 -0
  35. package/cli/review.js +48 -1
  36. package/cli/scripts/write-atelier-fixtures.mjs +33 -0
  37. package/cli/security.js +190 -0
  38. package/cli/skills.js +53 -10
  39. package/cli/transparency.js +140 -41
  40. package/cli/workflow.js +32 -17
  41. package/components/index.js +3 -0
  42. package/components/runtime/component-loaders.js +3 -0
  43. package/components/velin-drawer.js +43 -4
  44. package/components/velin-empty-state.js +83 -0
  45. package/components/velin-modal.js +76 -15
  46. package/components/velin-otp-input.js +220 -0
  47. package/components/velin-password-strength.js +147 -0
  48. package/components/velin-sheet.js +49 -4
  49. package/components/velin-theme-toggle.js +15 -1
  50. package/core/a11y/component-contracts.json +391 -255
  51. package/core/attributes/registry.js +1 -1
  52. package/core/meta/knowledge/components.json +75 -3
  53. package/core/motion/analyze.js +38 -0
  54. package/core/motion/blueprint.js +51 -0
  55. package/core/motion/composer.js +114 -0
  56. package/core/motion/data/registry.json +352 -0
  57. package/core/motion/effects.js +17 -1
  58. package/core/motion/index.js +112 -9
  59. package/core/motion/optimize.js +44 -0
  60. package/core/motion/policy.js +77 -0
  61. package/core/motion/registry.js +131 -0
  62. package/core/motion/review.js +59 -0
  63. package/core/motion/scan.js +150 -0
  64. package/core/motion/timeline.js +68 -0
  65. package/core/motion/transitions.js +49 -0
  66. package/core/motion/triggers.js +168 -0
  67. package/core/security/checks/fs.js +92 -0
  68. package/core/security/detect.js +35 -0
  69. package/core/security/engine.js +116 -0
  70. package/core/security/index.js +18 -0
  71. package/core/security/registry.js +85 -0
  72. package/core/security/report/report.js +113 -0
  73. package/core/security/rules/ci/index.js +146 -0
  74. package/core/security/rules/consumer/index.js +174 -0
  75. package/core/security/rules/deps/index.js +182 -0
  76. package/core/security/rules/index.js +24 -0
  77. package/core/security/rules/publish/index.js +230 -0
  78. package/core/security/rules/repo/index.js +143 -0
  79. package/core/security/rules/secrets/index.js +133 -0
  80. package/core/security/score/dependency-health.js +53 -0
  81. package/core/security/score/release-health.js +66 -0
  82. package/core/security/score/score.js +102 -0
  83. package/core/security/timeline/timeline.js +76 -0
  84. package/dist/chunks/attributes-2ORR27KA.js +404 -0
  85. package/dist/chunks/attributes-HK7VIOLA.js +1080 -0
  86. package/dist/chunks/attributes-VRHHVNDO.js +1080 -0
  87. package/dist/chunks/chunk-MYKUI364.js +116 -0
  88. package/dist/chunks/chunk-NEVBPD5T.js +116 -0
  89. package/dist/chunks/chunk-Y6HN7HWX.js +116 -0
  90. package/dist/chunks/runtime-entry.js +1 -1
  91. package/dist/chunks/velin-drawer-A7Q7EBOS.js +162 -0
  92. package/dist/chunks/velin-empty-state-3NTUH2RF.js +81 -0
  93. package/dist/chunks/velin-modal-3MV4FYBK.js +231 -0
  94. package/dist/chunks/velin-otp-input-PSK42MM6.js +207 -0
  95. package/dist/chunks/velin-password-strength-TAXMLSED.js +136 -0
  96. package/dist/chunks/velin-sheet-N2VVYXFP.js +157 -0
  97. package/dist/chunks/velin-theme-toggle-J2NHC7IM.js +304 -0
  98. package/dist/llms.txt +4 -4
  99. package/dist/search-index.json +232 -5
  100. package/dist/velin-agent.json +433 -46
  101. package/dist/velinstyle-components.iife.js +3695 -2272
  102. package/dist/velinstyle-components.js +3698 -2271
  103. package/dist/velinstyle-components.min.js +262 -120
  104. package/dist/velinstyle.css +96 -13
  105. package/dist/velinstyle.d.ts +3 -0
  106. package/dist/velinstyle.min.css +1 -1
  107. package/package.json +19 -4
  108. package/packages/velinstyle-cli-core/package.json +11 -0
  109. package/packages/velinstyle-cli-core/src/config.js +84 -0
  110. package/packages/velinstyle-cli-core/src/contract.js +56 -0
  111. package/packages/velinstyle-cli-core/src/event-bus.js +50 -0
  112. package/packages/velinstyle-cli-core/src/index.js +18 -0
  113. package/packages/velinstyle-cli-core/src/lifecycle.js +33 -0
  114. package/packages/velinstyle-cli-core/src/runner.js +129 -0
  115. package/packages/velinstyle-cli-registry/data/commands.json +1216 -0
  116. package/packages/velinstyle-cli-registry/data/layouts.json +33 -0
  117. package/packages/velinstyle-cli-registry/data/widgets.json +35 -0
  118. package/packages/velinstyle-cli-registry/package.json +13 -0
  119. package/packages/velinstyle-cli-registry/src/index.js +88 -0
  120. package/packages/velinstyle-cli-renderer/package.json +8 -0
  121. package/packages/velinstyle-cli-renderer/src/index.js +286 -0
  122. package/packages/velinstyle-cli-theme/package.json +8 -0
  123. package/packages/velinstyle-cli-theme/src/index.js +84 -0
  124. package/packages/velinstyle-cli-ui/package.json +8 -0
  125. package/packages/velinstyle-cli-ui/src/index.js +34 -0
  126. package/packages/velinstyle-motion/README.md +81 -0
  127. package/packages/velinstyle-motion/package.json +37 -0
  128. package/packages/velinstyle-motion/src/ai/mini-prompt.js +119 -0
  129. package/packages/velinstyle-motion/src/ai/provider.js +59 -0
  130. package/packages/velinstyle-motion/src/ai/providers/pixverse-setup.js +402 -0
  131. package/packages/velinstyle-motion/src/ai/providers/pixverse.js +245 -0
  132. package/packages/velinstyle-motion/src/config.js +257 -0
  133. package/packages/velinstyle-motion/src/export/html-clip.js +141 -0
  134. package/packages/velinstyle-motion/src/index.js +304 -0
  135. package/packages/velinstyle-motion/src/jobs/cache.js +30 -0
  136. package/packages/velinstyle-motion/src/jobs/queue.js +69 -0
  137. package/packages/velinstyle-motion/src/jobs/usage.js +53 -0
  138. package/packages/velinstyle-motion/src/local/effects.js +139 -0
  139. package/packages/velinstyle-motion/src/local/engine.js +96 -0
  140. package/packages/velinstyle-motion/src/providers-catalog.js +44 -0
  141. package/packages/velinstyle-motion/src/registry/presets.js +25 -0
  142. package/packages/velinstyle-motion/src/registry/presets.json +85 -0
  143. package/packages/velinstyle-motion/src/ux/cost-gate.js +37 -0
  144. package/packages/velinstyle-motion/src/ux/pixverse-gate.js +153 -0
  145. package/packages/velinstyle-motion/src/ux/wizard.js +171 -0
  146. package/packages/velinstyle-motion/src/validate-image.js +91 -0
  147. package/packages/velinstyle-skills/catalog.json +1 -1
  148. package/packages/velinstyle-skills/registry.json +1 -142
  149. package/packages/velinstyle-skills/skills/velin-motion-reduced-safe/SKILL.md +1 -1
  150. package/packages/velinstyle-skills/skills/velin-motion-reveal-feedback/SKILL.md +1 -1
  151. package/src/components/data-table.css +19 -0
  152. package/src/components/empty-auth.css +33 -0
  153. package/src/components/table.css +16 -6
  154. package/src/tokens/motion.css +7 -0
  155. package/src/utilities/scroll-animation.css +66 -0
  156. package/src/velinstyle.css +1 -0
@@ -0,0 +1,168 @@
1
+ /**
2
+ * Production report — text + JSON with before/after breakdown.
3
+ */
4
+ export function buildReport(data) {
5
+ const {
6
+ originalBytes = 0,
7
+ productionBytes = 0,
8
+ breakdown = {},
9
+ categories = null,
10
+ themes = [],
11
+ icons = [],
12
+ components = [],
13
+ tags = [],
14
+ themeCountBefore = null,
15
+ themeCountAfter = null,
16
+ motionBefore = null,
17
+ motionAfter = null,
18
+ componentsBefore = null,
19
+ componentsAfter = null,
20
+ imagesSkipped = true,
21
+ filesScanned = 0,
22
+ } = data;
23
+
24
+ const saved = Math.max(0, originalBytes - productionBytes);
25
+ const pct = originalBytes > 0 ? Math.round((saved / originalBytes) * 100) : 0;
26
+
27
+ const rows = categories || buildCategoryRows({
28
+ breakdown,
29
+ themes,
30
+ icons,
31
+ components,
32
+ tags,
33
+ themeCountBefore,
34
+ themeCountAfter,
35
+ motionBefore,
36
+ motionAfter,
37
+ componentsBefore,
38
+ componentsAfter,
39
+ fullCss: data.fullCss,
40
+ fullJs: data.fullJs,
41
+ fullIcons: data.fullIcons,
42
+ cssBytes: data.cssBytes,
43
+ jsBytes: data.jsBytes,
44
+ iconBytes: data.iconBytes,
45
+ });
46
+
47
+ const lines = [
48
+ 'Production Report',
49
+ '',
50
+ ];
51
+
52
+ for (const row of rows) {
53
+ const before = row.beforeCount != null ? String(row.beforeCount) : formatKb(row.before);
54
+ const after = row.afterCount != null ? String(row.afterCount) : formatKb(row.after);
55
+ lines.push(`${String(row.label).padEnd(14)}${before.padStart(8)} → ${after}`);
56
+ }
57
+
58
+ lines.push('');
59
+ lines.push(`${'Savings'.padEnd(14)}${pct} %`);
60
+
61
+ if (imagesSkipped) {
62
+ lines.push('');
63
+ lines.push('Images: skipped (coming soon)');
64
+ }
65
+
66
+ lines.push('');
67
+ lines.push(`Scanned files: ${filesScanned}`);
68
+ if (themes.length) lines.push(`Themes kept: ${themes.join(', ')}`);
69
+ if (icons.length) lines.push(`Icons (${icons.length}): ${icons.slice(0, 12).join(', ')}${icons.length > 12 ? '…' : ''}`);
70
+ if (tags.length) lines.push(`Web Components: ${tags.join(', ')}`);
71
+ if (components.length) lines.push(`CSS modules: ${components.length}`);
72
+
73
+ return {
74
+ version: 2,
75
+ originalBytes,
76
+ productionBytes,
77
+ savedBytes: saved,
78
+ savedPercent: pct,
79
+ breakdown,
80
+ categories: rows,
81
+ themes,
82
+ icons,
83
+ tags,
84
+ components,
85
+ filesScanned,
86
+ images: imagesSkipped ? { status: 'skipped', note: 'coming soon' } : null,
87
+ text: lines.join('\n'),
88
+ };
89
+ }
90
+
91
+ function buildCategoryRows(opts) {
92
+ const {
93
+ fullCss = 0,
94
+ fullJs = 0,
95
+ fullIcons = 0,
96
+ cssBytes = 0,
97
+ jsBytes = 0,
98
+ iconBytes = 0,
99
+ themeCountBefore,
100
+ themeCountAfter,
101
+ motionBefore,
102
+ motionAfter,
103
+ componentsBefore,
104
+ componentsAfter,
105
+ breakdown = {},
106
+ themes = [],
107
+ components = [],
108
+ tags = [],
109
+ } = opts;
110
+
111
+ return [
112
+ { label: 'CSS', before: fullCss || (cssBytes + (breakdown.CSS || 0)), after: cssBytes },
113
+ { label: 'Runtime', before: fullJs || (jsBytes + (breakdown.JS || 0)), after: jsBytes },
114
+ { label: 'Icons', before: fullIcons || (iconBytes + (breakdown.Icons || 0)), after: iconBytes },
115
+ {
116
+ label: 'Themes',
117
+ beforeCount: themeCountBefore != null ? themeCountBefore : (themes.length + (breakdown.Themes ? 1 : 0)),
118
+ afterCount: themeCountAfter != null ? themeCountAfter : themes.length,
119
+ },
120
+ {
121
+ label: 'Motion',
122
+ beforeCount: motionBefore != null ? motionBefore : 18,
123
+ afterCount: motionAfter != null ? motionAfter : (breakdown.Motion ? 4 : 18),
124
+ },
125
+ {
126
+ label: 'Components',
127
+ beforeCount: componentsBefore != null ? componentsBefore : Math.max(components.length, tags.length, 43),
128
+ afterCount: componentsAfter != null ? componentsAfter : Math.max(tags.length, components.length ? Math.min(components.length, tags.length || components.length) : tags.length),
129
+ },
130
+ ];
131
+ }
132
+
133
+ export function formatKb(bytes) {
134
+ if (bytes == null || Number.isNaN(bytes)) return '—';
135
+ return `${(bytes / 1024).toFixed(bytes >= 10240 ? 0 : 1)} KB`;
136
+ }
137
+
138
+ export function measureBytes(parts) {
139
+ let total = 0;
140
+ for (const p of parts) {
141
+ if (typeof p === 'number') total += p;
142
+ else if (typeof p === 'string') total += Buffer.byteLength(p, 'utf-8');
143
+ else if (p && typeof p.bytes === 'number') total += p.bytes;
144
+ }
145
+ return total;
146
+ }
147
+
148
+ /** Map report → Experience contract widgets */
149
+ export function reportToExperienceWidgets(report) {
150
+ return [
151
+ {
152
+ id: 'hero',
153
+ props: {
154
+ title: 'Production Builder',
155
+ subtitle: 'Build only what your project actually uses',
156
+ eyebrow: 'Velin Experience',
157
+ },
158
+ },
159
+ {
160
+ id: 'bundle',
161
+ props: {
162
+ title: 'Production Report',
163
+ rows: report.categories || [],
164
+ savingsPct: report.savedPercent,
165
+ },
166
+ },
167
+ ];
168
+ }
@@ -0,0 +1,355 @@
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
+ fullCss,
233
+ fullJs,
234
+ fullIcons,
235
+ cssBytes,
236
+ jsBytes,
237
+ iconBytes: iconMeta.bytes || 0,
238
+ breakdown: {
239
+ CSS: Math.max(0, fullCss - cssBytes),
240
+ JS: Math.max(0, fullJs - jsBytes),
241
+ Icons: Math.max(0, fullIcons - (iconMeta.bytes || 0)),
242
+ Themes: themeSaved,
243
+ Motion: includeMotion ? 0 : 8 * 1024,
244
+ Fonts: 2 * 1024,
245
+ },
246
+ themes: themeMeta.selected || [],
247
+ themeCountBefore: listKnownThemeNames(PKG_ROOT).length,
248
+ themeCountAfter: (themeMeta.selected || []).length,
249
+ motionBefore: 18,
250
+ motionAfter: includeMotion ? 18 : 4,
251
+ componentsBefore: 43,
252
+ componentsAfter: Math.max(closure.tags.length, 1),
253
+ icons: iconMeta.included || [],
254
+ components: cssResult.included,
255
+ tags: closure.tags,
256
+ filesScanned: extracted.files.length,
257
+ imagesSkipped: true,
258
+ });
259
+
260
+ writeFileSync(join(outDir, 'production-report.json'), JSON.stringify(report, null, 2));
261
+ writeFileSync(join(outDir, 'production-report.txt'), report.text);
262
+ writeFileSync(join(outDir, 'used.json'), JSON.stringify({
263
+ ...extractToPlain(extracted),
264
+ closure: {
265
+ cssFiles: closure.cssFiles,
266
+ tags: closure.tags,
267
+ matchedPrefixes: closure.matchedPrefixes,
268
+ },
269
+ }, null, 2));
270
+
271
+ if (reportPath) {
272
+ const absReport = resolve(cwd, reportPath);
273
+ mkdirSync(dirname(absReport), { recursive: true });
274
+ if (absReport.endsWith('.md') || absReport.endsWith('.txt')) {
275
+ writeFileSync(absReport, report.text);
276
+ } else {
277
+ writeFileSync(absReport, JSON.stringify(report, null, 2));
278
+ }
279
+ }
280
+
281
+ let explainOut = null;
282
+ if (explain) {
283
+ explainOut = buildExplain({
284
+ skippedCss: cssResult.skipped,
285
+ skippedThemes: themeMeta.skipped || [],
286
+ skippedIcons: iconMeta.skipped || [],
287
+ skippedMotion: !includeMotion,
288
+ graphExplain: closure.explain,
289
+ });
290
+ writeFileSync(join(outDir, 'explain.txt'), explainOut.text);
291
+ console.log(`\n${explainOut.text}`);
292
+ }
293
+
294
+ console.log(C.green(`\nProduction build → ${outDir}`));
295
+ try {
296
+ const { createExperienceSession, parseExperienceFlags, writeRecording, loadExperienceConfig } = await import('../experience.js');
297
+ const { reportToExperienceWidgets } = await import('./report.js');
298
+ const overrides = parseExperienceFlags(args || process.argv.slice(2), helpers || {
299
+ getArg: () => null,
300
+ hasFlag: () => false,
301
+ });
302
+ const config = loadExperienceConfig(cwd, overrides);
303
+ const session = createExperienceSession(config);
304
+ const contract = {
305
+ project: { outDir, filesScanned: extracted.files.length },
306
+ lifecycle: { stage: 'summary', stages: [] },
307
+ result: { status: 'PASS', message: `Saved ${report.savedPercent}%` },
308
+ stats: { savedPercent: report.savedPercent, productionBytes, originalBytes: report.originalBytes },
309
+ widgets: reportToExperienceWidgets(report),
310
+ suggestions: explain
311
+ ? [{ text: 'See explain.txt for removed CSS/themes/icons' }]
312
+ : [{ text: 'Add --explain for removed-asset details' }],
313
+ diagnostics: [],
314
+ notifications: [
315
+ { tone: 'ok', message: `Production savings ${report.savedPercent}%` },
316
+ ],
317
+ meta: { commandId: 'production', layout: 'build', durationMs: 0 },
318
+ };
319
+ console.log(session.render(contract));
320
+ if (config.record || overrides.record) {
321
+ const paths = writeRecording({ contract, events: [], config });
322
+ console.log(`\nRecorded → ${paths.json}`);
323
+ }
324
+ } catch {
325
+ console.log(report.text);
326
+ }
327
+ console.log(C.dim(`\nCSS: ${cssOut}${bundled ? ' (lightningcss)' : ' (raw)'}`));
328
+ if (jsOut) console.log(C.dim(`JS: ${jsOut} (${closure.tags.length} components)`));
329
+ if (!noIcons) console.log(C.dim(`Icons: ${iconMeta.included.length} / ${iconMeta.availableCount || '?'}`));
330
+ if (!noThemes) console.log(C.dim(`Themes: ${(themeMeta.selected || []).join(', ') || '(none)'}`));
331
+
332
+ return {
333
+ outDir,
334
+ report,
335
+ explain: explainOut,
336
+ extracted,
337
+ closure,
338
+ cssOut,
339
+ jsOut,
340
+ };
341
+ };
342
+
343
+ const result = await once();
344
+
345
+ if (doWatch) {
346
+ console.log(C.cyan(`\nWatching ${targetPath} … (Ctrl+C to stop)`));
347
+ watchProduction(targetPath, async () => {
348
+ console.log(C.dim('\n[watch] rebuild…'));
349
+ await once();
350
+ });
351
+ await new Promise(() => {});
352
+ }
353
+
354
+ return result;
355
+ }
@@ -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
+ }