@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,166 @@
1
+ {
2
+ "version": 1,
3
+ "alwaysCss": [
4
+ "tokens/fonts.css",
5
+ "tokens/color.css",
6
+ "tokens/spacing.css",
7
+ "tokens/typography.css",
8
+ "tokens/radius.css",
9
+ "tokens/shadow.css",
10
+ "tokens/motion.css",
11
+ "tokens/z-index.css",
12
+ "tokens/aspect-ratio.css",
13
+ "base/root.css",
14
+ "base/reset.css",
15
+ "base/focus.css",
16
+ "base/content.css",
17
+ "a11y/sr-only.css",
18
+ "a11y/skip-link.css",
19
+ "a11y/reduced-motion.css",
20
+ "a11y/forced-colors.css",
21
+ "a11y/focus-appearance.css",
22
+ "a11y/target-size.css",
23
+ "layout/breakpoints.css",
24
+ "layout/container.css"
25
+ ],
26
+ "classPrefixes": {
27
+ "velin-btn": { "css": ["components/button.css"] },
28
+ "velin-card": { "css": ["components/card.css"] },
29
+ "velin-input": { "css": ["components/input.css", "components/form-validation.css"] },
30
+ "velin-textarea": { "css": ["components/input.css", "components/form-validation.css"] },
31
+ "velin-select": { "css": ["components/input.css", "components/form-validation.css"] },
32
+ "velin-label": { "css": ["components/input.css"] },
33
+ "velin-field": { "css": ["components/input.css", "components/form-validation.css"] },
34
+ "velin-nav": { "css": ["components/nav.css"] },
35
+ "velin-navbar": { "css": ["components/nav.css"] },
36
+ "velin-alert": { "css": ["components/alert.css"] },
37
+ "velin-badge": { "css": ["components/badge.css"] },
38
+ "velin-table": { "css": ["components/table.css"] },
39
+ "velin-data-table": { "css": ["components/data-table.css", "components/table.css"] },
40
+ "velin-tooltip": { "css": ["components/tooltip.css"] },
41
+ "velin-modal": { "css": ["components/modal.css", "components/button.css"] },
42
+ "velin-dialog": { "css": ["components/modal.css", "components/button.css"] },
43
+ "velin-breadcrumb": { "css": ["components/breadcrumb.css"] },
44
+ "velin-pagination": { "css": ["components/pagination.css"] },
45
+ "velin-progress": { "css": ["components/progress.css"] },
46
+ "velin-spinner": { "css": ["components/spinner.css"] },
47
+ "velin-list": { "css": ["components/list-group.css"] },
48
+ "velin-avatar": { "css": ["components/avatar.css"] },
49
+ "velin-switch": { "css": ["components/switch.css"] },
50
+ "velin-divider": { "css": ["components/divider.css"] },
51
+ "velin-chip": { "css": ["components/chip.css"] },
52
+ "velin-timeline": { "css": ["components/timeline.css"] },
53
+ "velin-stepper": { "css": ["components/stepper.css"] },
54
+ "velin-stat": { "css": ["components/stat.css"] },
55
+ "velin-drawer": { "css": ["components/drawer.css", "components/button.css"] },
56
+ "velin-sheet": { "css": ["components/drawer.css", "components/button.css"] },
57
+ "velin-input-group": { "css": ["components/input-group.css", "components/input.css"] },
58
+ "velin-collapse": { "css": ["components/collapse.css"] },
59
+ "velin-accordion": { "css": ["components/collapse.css"] },
60
+ "velin-calendar": { "css": ["components/calendar-dropzone.css"] },
61
+ "velin-dropzone": { "css": ["components/calendar-dropzone.css"] },
62
+ "velin-grid": { "css": ["layout/grid.css"] },
63
+ "velin-flex": { "css": ["layout/flex.css"] },
64
+ "velin-container": { "css": ["layout/container.css"] },
65
+ "velin-stack": { "css": ["layout/patterns.css"] },
66
+ "velin-cluster": { "css": ["layout/patterns.css"] },
67
+ "velin-sidebar": { "css": ["layout/patterns.css", "layout/app-shell.css"] },
68
+ "velin-app": { "css": ["layout/app-shell.css"] },
69
+ "velin-shell": { "css": ["layout/app-shell.css"] },
70
+ "velin-skeleton": { "css": ["a11y/skeleton.css"] },
71
+ "velin-sr-only": { "css": ["a11y/sr-only.css"] },
72
+ "velin-skip": { "css": ["a11y/skip-link.css"] },
73
+ "velin-transparency": { "css": ["components/transparency.css"] },
74
+ "velin-disclosure": { "css": ["components/transparency.css"] },
75
+ "velin-otp-input": { "css": ["components/empty-auth.css", "components/input.css"] },
76
+ "velin-password-strength": { "css": ["components/empty-auth.css", "components/progress.css"] },
77
+ "velin-empty-state": { "css": ["components/empty-auth.css", "components/button.css"] }
78
+ },
79
+ "wc": {
80
+ "velin-modal": { "css": ["components/modal.css", "components/button.css"], "peers": ["velin-icon"] },
81
+ "velin-dialog": { "css": ["components/modal.css", "components/button.css"], "peers": ["velin-icon"] },
82
+ "velin-drawer": { "css": ["components/drawer.css", "components/button.css"], "peers": [] },
83
+ "velin-sheet": { "css": ["components/drawer.css", "components/button.css"], "peers": [] },
84
+ "velin-toast": { "css": ["components/alert.css"], "peers": ["velin-icon"] },
85
+ "velin-tooltip": { "css": ["components/tooltip.css"], "peers": [] },
86
+ "velin-tooltip-wc": { "css": ["components/tooltip.css"], "peers": [] },
87
+ "velin-tabs": { "css": ["components/nav.css"], "peers": [] },
88
+ "velin-icon": { "css": [], "peers": [] },
89
+ "velin-accordion": { "css": ["components/collapse.css"], "peers": [] },
90
+ "velin-collapse": { "css": ["components/collapse.css"], "peers": [] },
91
+ "velin-dropdown": { "css": ["components/nav.css", "components/button.css"], "peers": [] },
92
+ "velin-popover": { "css": ["components/tooltip.css"], "peers": [] },
93
+ "velin-carousel": { "css": [], "peers": ["velin-icon"] },
94
+ "velin-lightbox": { "css": ["components/modal.css"], "peers": ["velin-icon"] },
95
+ "velin-data-table": { "css": ["components/data-table.css", "components/table.css"], "peers": [] },
96
+ "velin-stepper": { "css": ["components/stepper.css"], "peers": [] },
97
+ "velin-stepper-wc": { "css": ["components/stepper.css"], "peers": [] },
98
+ "velin-theme-toggle": { "css": ["components/button.css"], "peers": ["velin-icon"] },
99
+ "velin-search": { "css": ["components/input.css", "components/input-group.css"], "peers": ["velin-icon"] },
100
+ "velin-combobox": { "css": ["components/input.css"], "peers": [] },
101
+ "velin-command": { "css": ["components/modal.css", "components/input.css"], "peers": [] },
102
+ "velin-bottom-nav": { "css": ["components/nav.css"], "peers": ["velin-icon"] },
103
+ "velin-menubar": { "css": ["components/nav.css"], "peers": [] },
104
+ "velin-calendar": { "css": ["components/calendar-dropzone.css"], "peers": [] },
105
+ "velin-file-dropzone": { "css": ["components/calendar-dropzone.css"], "peers": [] },
106
+ "velin-progress-ring": { "css": ["components/progress.css"], "peers": [] },
107
+ "velin-rating": { "css": ["components/chip.css"], "peers": ["velin-icon"] },
108
+ "velin-scroll-top": { "css": ["components/button.css"], "peers": ["velin-icon"] },
109
+ "velin-scrollspy": { "css": [], "peers": [] },
110
+ "velin-sparkline": { "css": [], "peers": [] },
111
+ "velin-countdown": { "css": ["components/stat.css"], "peers": [] },
112
+ "velin-counter": { "css": ["components/stat.css"], "peers": [] },
113
+ "velin-live-dot": { "css": ["components/badge.css"], "peers": [] },
114
+ "velin-announcer": { "css": ["a11y/sr-only.css"], "peers": [] },
115
+ "velin-copy": { "css": ["components/button.css"], "peers": ["velin-icon"] },
116
+ "velin-email": { "css": ["components/input.css"], "peers": [] },
117
+ "velin-secure-field": { "css": ["components/input.css", "a11y/security.css"], "peers": [] },
118
+ "velin-form-summary": { "css": ["components/form-validation.css", "components/alert.css"], "peers": [] },
119
+ "velin-persist": { "css": [], "peers": [] },
120
+ "velin-code-block": { "css": [], "peers": [] },
121
+ "velin-segmented-control": { "css": ["components/chip.css", "components/button.css"], "peers": [] },
122
+ "velin-otp-input": { "css": ["components/empty-auth.css", "components/input.css"], "peers": [] },
123
+ "velin-password-strength": { "css": ["components/empty-auth.css", "components/progress.css"], "peers": [] },
124
+ "velin-empty-state": { "css": ["components/empty-auth.css", "components/button.css"], "peers": [] }
125
+ },
126
+ "utilityFiles": [
127
+ "utilities/color.css",
128
+ "utilities/spacing.css",
129
+ "utilities/display.css",
130
+ "utilities/text.css",
131
+ "utilities/sizing.css",
132
+ "utilities/border.css",
133
+ "utilities/position.css",
134
+ "utilities/animation.css",
135
+ "utilities/gradient.css",
136
+ "utilities/print.css",
137
+ "utilities/responsive.css",
138
+ "utilities/divide.css",
139
+ "utilities/scroll.css",
140
+ "utilities/color-mix.css",
141
+ "utilities/scroll-animation.css",
142
+ "utilities/view-transition.css",
143
+ "utilities/scope.css",
144
+ "utilities/anchor.css",
145
+ "utilities/filter.css",
146
+ "utilities/filter-effects.css",
147
+ "utilities/chart-animation.css",
148
+ "utilities/container-style.css",
149
+ "utilities/state.css",
150
+ "utilities/safe-area.css"
151
+ ],
152
+ "motionFiles": [
153
+ "utilities/animation.css",
154
+ "utilities/scroll-animation.css",
155
+ "utilities/chart-animation.css",
156
+ "utilities/view-transition.css",
157
+ "tokens/motion.css",
158
+ "a11y/reduced-motion.css"
159
+ ],
160
+ "fontClassPrefixes": [
161
+ "velin-font-",
162
+ "velin-text-",
163
+ "velin-leading-",
164
+ "velin-tracking-"
165
+ ]
166
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * --explain: human-readable removals with reasons.
3
+ */
4
+ export function buildExplain({
5
+ skippedCss = [],
6
+ skippedThemes = [],
7
+ skippedIcons = [],
8
+ skippedMotion = false,
9
+ graphExplain = [],
10
+ } = {}) {
11
+ const removed = [];
12
+
13
+ for (const item of skippedCss) {
14
+ removed.push({
15
+ id: item.file || item,
16
+ reason: item.reason || 'Nicht benutzt',
17
+ });
18
+ }
19
+ for (const theme of skippedThemes) {
20
+ removed.push({ id: `${theme}.theme.css`, reason: 'Nicht benutzt' });
21
+ }
22
+ for (const icon of skippedIcons.slice(0, 40)) {
23
+ removed.push({ id: `icon:${icon}`, reason: 'Nicht benutzt' });
24
+ }
25
+ if (skippedIcons.length > 40) {
26
+ removed.push({ id: `icon:…(+${skippedIcons.length - 40})`, reason: 'Nicht benutzt' });
27
+ }
28
+ if (skippedMotion) {
29
+ removed.push({ id: 'motion utilities', reason: 'Keine Animate-/Motion-Klassen gefunden' });
30
+ }
31
+
32
+ const kept = [];
33
+ const seen = new Set();
34
+ for (const e of graphExplain) {
35
+ if (!e.keep || seen.has(e.keep)) continue;
36
+ seen.add(e.keep);
37
+ kept.push({ id: e.keep, reason: e.reason });
38
+ }
39
+
40
+ const lines = ['Entfernt'];
41
+ if (!removed.length) lines.push('(nichts entfernt — oder Scan fand fast alles)');
42
+ for (const r of removed) {
43
+ lines.push(`${r.id.padEnd(28)} ${r.reason}`);
44
+ }
45
+ lines.push('');
46
+ lines.push('Behalten (Auszug)');
47
+ for (const k of kept.slice(0, 30)) {
48
+ lines.push(`${k.id.padEnd(28)} ${k.reason}`);
49
+ }
50
+
51
+ return { removed, kept, text: lines.join('\n') };
52
+ }
@@ -0,0 +1,278 @@
1
+ /**
2
+ * Content extract for Production Builder — used classes, WC tags, themes, icons, motion, fonts.
3
+ */
4
+ import { existsSync, readdirSync, readFileSync, statSync } from 'fs';
5
+ import { join, extname, resolve, relative } from 'path';
6
+
7
+ export const DEFAULT_IGNORE = [
8
+ 'node_modules', 'dist', '.git', '.next', '.nuxt', 'vendor', 'build', 'coverage',
9
+ ];
10
+
11
+ export const CONTENT_EXTS = new Set([
12
+ '.html', '.htm',
13
+ '.blade.php',
14
+ '.twig',
15
+ '.js', '.jsx', '.mjs', '.cjs', '.ts', '.tsx',
16
+ '.vue',
17
+ '.astro',
18
+ '.md', '.mdx',
19
+ '.css',
20
+ ]);
21
+
22
+ /** Resolve multi-dot extensions (.blade.php, .mdx) before extname. */
23
+ export function contentExtOf(filePath) {
24
+ const name = String(filePath || '').toLowerCase().replace(/\\/g, '/');
25
+ const base = name.includes('/') ? name.slice(name.lastIndexOf('/') + 1) : name;
26
+ if (base.endsWith('.blade.php')) return '.blade.php';
27
+ if (base.endsWith('.mdx')) return '.mdx';
28
+ return extname(base).toLowerCase();
29
+ }
30
+
31
+ export function isContentFile(filePath) {
32
+ return CONTENT_EXTS.has(contentExtOf(filePath));
33
+ }
34
+
35
+ /** Strip template expressions so {{ class }} / {% %} are not mistaken for tokens. */
36
+ export function stripTemplateNoise(content, ext) {
37
+ let text = String(content || '');
38
+ if (ext === '.astro') {
39
+ text = text.replace(/^---[\s\S]*?---\s*/, ' ');
40
+ }
41
+ if (ext === '.blade.php' || ext === '.twig' || ext === '.vue' || ext === '.astro' || ext === '.mdx') {
42
+ text = text
43
+ .replace(/\{\{[\s\S]*?\}\}/g, ' ')
44
+ .replace(/\{!![\s\S]*?!!\}/g, ' ')
45
+ .replace(/\{%[\s\S]*?%\}/g, ' ')
46
+ .replace(/@\{\{[\s\S]*?\}\}/g, ' ');
47
+ }
48
+ return text;
49
+ }
50
+
51
+ const CLASS_ATTR_RE = /class(?:Name)?\s*=\s*["'`]([^"'`]+)["'`]/gi;
52
+ const CLASS_TOKEN_RE = /\b((?:velin|expo)-[\w:-]+)\b/g;
53
+ const TAG_RE = /<(velin-[a-z0-9-]+)\b/gi;
54
+ const DATA_COMPONENT_RE = /data-velin-component\s*=\s*["'](velin-[a-z0-9-]+)["']/gi;
55
+ const THEME_RE = /data-velin-theme\s*=\s*["']([^"']+)["']/gi;
56
+ const THEMES_LIST_RE = /themes\s*[:=]\s*\[([^\]]+)\]/gi;
57
+ const ICON_NAME_RE = /(?:name|icon)\s*=\s*["']([a-z0-9][a-z0-9_-]*)["']/gi;
58
+ const ICON_HASH_RE = /velin-icons\.svg#([a-z0-9][a-z0-9_-]*)/gi;
59
+ const MOTION_ATTR_RE = /data-velin-(?:animate|motion|scroll)\s*=\s*["']([^"']+)["']/gi;
60
+ const MOTION_BARE_RE = /\svelin-(?:motion|reveal|fade|slide|scale|parallax|hover|stagger|scroll|trigger|timeline|motion-sequence)(?:\s|=|>)/gi;
61
+ const FONT_FACE_RE = /@font-face\s*\{([\s\S]*?)\}/gi;
62
+ const FONT_WEIGHT_RE = /font-weight\s*:\s*([0-9]+|normal|bold)/gi;
63
+
64
+ function shouldIgnore(name, ignore) {
65
+ return ignore.includes(name);
66
+ }
67
+
68
+ export function walkContentFiles(dir, { ignore = DEFAULT_IGNORE, out = [], root = dir } = {}) {
69
+ if (!existsSync(dir)) return out;
70
+ let entries;
71
+ try {
72
+ entries = readdirSync(dir, { withFileTypes: true });
73
+ } catch {
74
+ return out;
75
+ }
76
+ for (const entry of entries) {
77
+ if (shouldIgnore(entry.name, ignore)) continue;
78
+ const full = join(dir, entry.name);
79
+ if (entry.isDirectory()) {
80
+ walkContentFiles(full, { ignore, out, root });
81
+ continue;
82
+ }
83
+ if (!entry.isFile()) continue;
84
+ const ext = contentExtOf(entry.name);
85
+ if (!CONTENT_EXTS.has(ext)) continue;
86
+ out.push(full);
87
+ }
88
+ return out;
89
+ }
90
+
91
+ export function collectContentFiles(targetPath, { ignore = DEFAULT_IGNORE, extraIgnore = [] } = {}) {
92
+ const abs = resolve(targetPath);
93
+ const ignoreList = [...new Set([...ignore, ...extraIgnore])];
94
+ if (!existsSync(abs)) return [];
95
+ const st = statSync(abs);
96
+ if (st.isFile()) {
97
+ return isContentFile(abs) ? [abs] : [];
98
+ }
99
+ return walkContentFiles(abs, { ignore: ignoreList });
100
+ }
101
+
102
+ function addClassTokens(text, classes) {
103
+ for (const m of String(text || '').matchAll(CLASS_TOKEN_RE)) {
104
+ classes.add(m[1]);
105
+ // Responsive / state variants: velin-md:flex → also keep flex base if present as velin-flex
106
+ const bare = m[1].replace(/^(?:velin|expo)-(?:sm|md|lg|xl|2xl):/, (pref) => {
107
+ const ns = pref.startsWith('expo') ? 'expo-' : 'velin-';
108
+ return ns;
109
+ });
110
+ if (bare !== m[1] && bare.startsWith('velin-')) classes.add(bare);
111
+ }
112
+ }
113
+
114
+ /**
115
+ * @param {string} content
116
+ * @param {{ isCss?: boolean }} [opts]
117
+ */
118
+ export function extractFromText(content, { isCss = false } = {}) {
119
+ const classes = new Set();
120
+ const tags = new Set();
121
+ const themes = new Set();
122
+ const icons = new Set();
123
+ const motion = new Set();
124
+ const fontWeights = new Set();
125
+ const attrs = new Set();
126
+
127
+ const text = String(content || '');
128
+
129
+ if (!isCss) {
130
+ for (const m of text.matchAll(CLASS_ATTR_RE)) {
131
+ addClassTokens(m[1], classes);
132
+ }
133
+ }
134
+ addClassTokens(text, classes);
135
+
136
+ for (const m of text.matchAll(TAG_RE)) tags.add(m[1].toLowerCase());
137
+ for (const m of text.matchAll(DATA_COMPONENT_RE)) tags.add(m[1].toLowerCase());
138
+
139
+ for (const m of text.matchAll(THEME_RE)) {
140
+ for (const t of m[1].split(/[\s,|]+/)) {
141
+ if (t) themes.add(t.trim().toLowerCase());
142
+ }
143
+ }
144
+ for (const m of text.matchAll(THEMES_LIST_RE)) {
145
+ for (const part of m[1].split(/[,'"`\s]+/)) {
146
+ if (part && /^[a-z][a-z0-9_-]*$/i.test(part)) themes.add(part.toLowerCase());
147
+ }
148
+ }
149
+
150
+ for (const m of text.matchAll(ICON_NAME_RE)) icons.add(m[1].toLowerCase());
151
+ for (const m of text.matchAll(ICON_HASH_RE)) icons.add(m[1].toLowerCase());
152
+
153
+ for (const m of text.matchAll(MOTION_ATTR_RE)) {
154
+ for (const part of m[1].split(/[\s,]+/)) {
155
+ if (part) motion.add(part.trim());
156
+ }
157
+ }
158
+ for (const m of text.matchAll(MOTION_BARE_RE)) {
159
+ motion.add(m[0].trim().replace(/=.*/, '').trim() || 'velin-motion');
160
+ }
161
+ for (const cls of classes) {
162
+ if (cls.includes('animate') || cls.includes('motion') || cls.startsWith('velin-scroll-')) {
163
+ motion.add(cls);
164
+ }
165
+ }
166
+
167
+ for (const cls of classes) {
168
+ if (cls.startsWith('velin-font-') || cls.startsWith('velin-text-')) {
169
+ fontWeights.add(cls);
170
+ }
171
+ }
172
+
173
+ for (const m of text.matchAll(FONT_FACE_RE)) {
174
+ for (const w of m[1].matchAll(FONT_WEIGHT_RE)) fontWeights.add(String(w[1]));
175
+ }
176
+
177
+ for (const m of text.matchAll(/data-velin-([\w-]+)/gi)) {
178
+ attrs.add(`data-velin-${m[1].toLowerCase()}`);
179
+ }
180
+
181
+ return { classes, tags, themes, icons, motion, fontWeights, attrs };
182
+ }
183
+
184
+ /**
185
+ * Scan a project path and merge all extracts.
186
+ * @returns {{
187
+ * files: string[],
188
+ * classes: Set<string>,
189
+ * tags: Set<string>,
190
+ * themes: Set<string>,
191
+ * icons: Set<string>,
192
+ * motion: Set<string>,
193
+ * fontWeights: Set<string>,
194
+ * attrs: Set<string>,
195
+ * }}
196
+ */
197
+ export function extractProject(targetPath, options = {}) {
198
+ const {
199
+ ignore = DEFAULT_IGNORE,
200
+ contentGlobs, // reserved
201
+ safelist = [],
202
+ outDir,
203
+ cwd = process.cwd(),
204
+ } = options;
205
+
206
+ const extraIgnore = [];
207
+ if (outDir) {
208
+ try {
209
+ const rel = relative(resolve(cwd), resolve(outDir)).split(/[/\\]/)[0];
210
+ if (rel && rel !== '..' && rel !== '.') extraIgnore.push(rel);
211
+ } catch { /* ignore */ }
212
+ }
213
+
214
+ const files = collectContentFiles(targetPath, { ignore, extraIgnore });
215
+ const merged = {
216
+ files,
217
+ classes: new Set(),
218
+ tags: new Set(),
219
+ themes: new Set(),
220
+ icons: new Set(),
221
+ motion: new Set(),
222
+ fontWeights: new Set(),
223
+ attrs: new Set(),
224
+ };
225
+
226
+ for (const file of files) {
227
+ let body = '';
228
+ try {
229
+ body = readFileSync(file, 'utf-8');
230
+ } catch {
231
+ continue;
232
+ }
233
+ const ext = contentExtOf(file);
234
+ const isCss = ext === '.css';
235
+ const part = extractFromText(isCss ? body : stripTemplateNoise(body, ext), { isCss });
236
+ for (const c of part.classes) merged.classes.add(c);
237
+ for (const t of part.tags) merged.tags.add(t);
238
+ for (const t of part.themes) merged.themes.add(t);
239
+ for (const i of part.icons) merged.icons.add(i);
240
+ for (const m of part.motion) merged.motion.add(m);
241
+ for (const f of part.fontWeights) merged.fontWeights.add(f);
242
+ for (const a of part.attrs) merged.attrs.add(a);
243
+ }
244
+
245
+ for (const item of safelist) {
246
+ const s = String(item).trim();
247
+ if (!s) continue;
248
+ if (s.startsWith('velin-') && !s.includes('.')) {
249
+ if (s.includes('-') && /^velin-[a-z]+-[a-z]/.test(s) && !s.includes('--') && s.split('-').length >= 3 && !s.startsWith('velin-btn') && !s.startsWith('velin-card')) {
250
+ // could be tag or class — add both conservatively
251
+ merged.tags.add(s);
252
+ merged.classes.add(s);
253
+ } else {
254
+ merged.classes.add(s);
255
+ }
256
+ } else if (s.endsWith('.css') || s.includes('/')) {
257
+ // handled by trim safelist files
258
+ } else if (/^[a-z][a-z0-9_-]*$/i.test(s)) {
259
+ merged.themes.add(s.toLowerCase());
260
+ merged.icons.add(s.toLowerCase());
261
+ }
262
+ }
263
+
264
+ return merged;
265
+ }
266
+
267
+ export function extractToPlain(extracted) {
268
+ return {
269
+ files: extracted.files,
270
+ classes: [...extracted.classes].sort(),
271
+ tags: [...extracted.tags].sort(),
272
+ themes: [...extracted.themes].sort(),
273
+ icons: [...extracted.icons].sort(),
274
+ motion: [...extracted.motion].sort(),
275
+ fontWeights: [...extracted.fontWeights].sort(),
276
+ attrs: [...extracted.attrs].sort(),
277
+ };
278
+ }
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Component dependency graph — closure over used classes/tags → CSS files + WC tags.
3
+ */
4
+ import { readFileSync, existsSync, readdirSync } from 'fs';
5
+ import { dirname, join } from 'path';
6
+ import { fileURLToPath } from 'url';
7
+
8
+ const __dirname = dirname(fileURLToPath(import.meta.url));
9
+ const GRAPH_PATH = join(__dirname, 'component-graph.json');
10
+
11
+ let _graphCache = null;
12
+
13
+ export function loadGraph(path = GRAPH_PATH) {
14
+ if (_graphCache && path === GRAPH_PATH) return _graphCache;
15
+ const graph = JSON.parse(readFileSync(path, 'utf-8'));
16
+ if (path === GRAPH_PATH) _graphCache = graph;
17
+ return graph;
18
+ }
19
+
20
+ function longestPrefixMatch(className, prefixes) {
21
+ let best = null;
22
+ let bestLen = -1;
23
+ for (const prefix of Object.keys(prefixes)) {
24
+ if (className === prefix || className.startsWith(`${prefix}-`) || className.startsWith(`${prefix}--`)) {
25
+ if (prefix.length > bestLen) {
26
+ best = prefix;
27
+ bestLen = prefix.length;
28
+ }
29
+ }
30
+ }
31
+ return best;
32
+ }
33
+
34
+ /**
35
+ * @param {{ classes: Iterable<string>, tags: Iterable<string> }} used
36
+ * @param {{ graph?: object, safelistFiles?: string[] }} [opts]
37
+ */
38
+ export function resolveClosure(used, opts = {}) {
39
+ const graph = opts.graph || loadGraph();
40
+ const cssFiles = new Set(graph.alwaysCss || []);
41
+ const tags = new Set();
42
+ const matchedPrefixes = new Set();
43
+ const explain = [];
44
+
45
+ for (const f of opts.safelistFiles || []) {
46
+ const norm = String(f).replace(/^src\//, '');
47
+ cssFiles.add(norm);
48
+ explain.push({ keep: norm, reason: 'safelist' });
49
+ }
50
+
51
+ for (const cls of used.classes || []) {
52
+ const prefix = longestPrefixMatch(cls, graph.classPrefixes || {});
53
+ if (!prefix) continue;
54
+ matchedPrefixes.add(prefix);
55
+ const entry = graph.classPrefixes[prefix];
56
+ for (const file of entry.css || []) {
57
+ cssFiles.add(file);
58
+ explain.push({ keep: file, reason: `class ${cls} → ${prefix}` });
59
+ }
60
+ }
61
+
62
+ const queue = [...(used.tags || [])];
63
+ const seen = new Set();
64
+
65
+ while (queue.length) {
66
+ const tag = queue.shift();
67
+ if (!tag || seen.has(tag)) continue;
68
+ seen.add(tag);
69
+ tags.add(tag);
70
+ const entry = graph.wc?.[tag];
71
+ if (!entry) {
72
+ explain.push({ keep: tag, reason: 'unknown WC — keep tag, no CSS edges' });
73
+ continue;
74
+ }
75
+ for (const file of entry.css || []) {
76
+ cssFiles.add(file);
77
+ explain.push({ keep: file, reason: `wc ${tag}` });
78
+ }
79
+ for (const peer of entry.peers || []) {
80
+ if (!seen.has(peer)) queue.push(peer);
81
+ }
82
+ }
83
+
84
+ return {
85
+ cssFiles: [...cssFiles].sort(),
86
+ tags: [...tags].sort(),
87
+ matchedPrefixes: [...matchedPrefixes].sort(),
88
+ utilityFiles: [...(graph.utilityFiles || [])],
89
+ motionFiles: [...(graph.motionFiles || [])],
90
+ alwaysCss: [...(graph.alwaysCss || [])],
91
+ explain,
92
+ };
93
+ }
94
+
95
+ export function listKnownThemeNames(pkgRoot) {
96
+ const dir = join(pkgRoot, 'src', 'themes');
97
+ if (!existsSync(dir)) return [];
98
+ return readdirSync(dir)
99
+ .filter((f) => f.endsWith('.css'))
100
+ .map((f) => f.replace(/\.css$/, ''))
101
+ .sort();
102
+ }