@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.
Files changed (89) hide show
  1. package/README.de.md +150 -24
  2. package/README.md +155 -33
  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 +23 -10
  17. package/cli/docgen/extract-attributes.js +2 -0
  18. package/cli/docgen/extract-cli.js +1 -1
  19. package/cli/index.js +181 -6
  20. package/cli/production/component-graph.json +166 -0
  21. package/cli/production/explain.js +52 -0
  22. package/cli/production/extract.js +238 -0
  23. package/cli/production/graph.js +102 -0
  24. package/cli/production/report.js +78 -0
  25. package/cli/production/run.js +312 -0
  26. package/cli/production/trim-css.js +177 -0
  27. package/cli/production/trim-fonts.js +23 -0
  28. package/cli/production/trim-icons.js +38 -0
  29. package/cli/production/trim-js.js +48 -0
  30. package/cli/production/trim-motion.js +22 -0
  31. package/cli/production/trim-themes.js +50 -0
  32. package/cli/production/watch.js +38 -0
  33. package/cli/review.js +48 -1
  34. package/cli/scripts/write-atelier-fixtures.mjs +33 -0
  35. package/cli/transparency.js +221 -0
  36. package/components/index.js +3 -0
  37. package/components/runtime/component-loaders.js +3 -0
  38. package/components/velin-drawer.js +43 -4
  39. package/components/velin-empty-state.js +83 -0
  40. package/components/velin-modal.js +76 -15
  41. package/components/velin-otp-input.js +220 -0
  42. package/components/velin-password-strength.js +147 -0
  43. package/components/velin-sheet.js +49 -4
  44. package/core/a11y/component-contracts.json +23 -4
  45. package/core/attributes/registry.js +14 -0
  46. package/core/meta/knowledge/components.json +75 -3
  47. package/core/meta/schema.js +10 -0
  48. package/core/transparency/attach.js +74 -0
  49. package/core/transparency/claims.js +97 -0
  50. package/core/transparency/doctor.js +142 -0
  51. package/core/transparency/engine.js +75 -0
  52. package/core/transparency/export.js +98 -0
  53. package/core/transparency/index.js +30 -0
  54. package/core/transparency/migrate.js +130 -0
  55. package/core/transparency/normalize.js +125 -0
  56. package/core/transparency/policy.js +105 -0
  57. package/core/transparency/providers.js +235 -0
  58. package/core/transparency/registry.js +104 -0
  59. package/core/transparency/renderer.js +111 -0
  60. package/core/transparency/reporter.js +113 -0
  61. package/core/transparency/validator.js +112 -0
  62. package/dist/chunks/attach-H2ZSEAE6.js +365 -0
  63. package/dist/chunks/attributes-2ORR27KA.js +404 -0
  64. package/dist/chunks/attributes-DZCXX2RZ.js +404 -0
  65. package/dist/chunks/chunk-CQMTCEI6.js +113 -0
  66. package/dist/chunks/chunk-Y6HN7HWX.js +116 -0
  67. package/dist/chunks/runtime-entry.js +1 -1
  68. package/dist/chunks/velin-drawer-A7Q7EBOS.js +162 -0
  69. package/dist/chunks/velin-empty-state-3NTUH2RF.js +81 -0
  70. package/dist/chunks/velin-modal-3MV4FYBK.js +231 -0
  71. package/dist/chunks/velin-otp-input-PSK42MM6.js +207 -0
  72. package/dist/chunks/velin-password-strength-TAXMLSED.js +136 -0
  73. package/dist/chunks/velin-sheet-N2VVYXFP.js +157 -0
  74. package/dist/llms.txt +5 -4
  75. package/dist/search-index.json +94 -5
  76. package/dist/velin-agent.json +290 -23
  77. package/dist/velinstyle-components.iife.js +3189 -2202
  78. package/dist/velinstyle-components.js +3187 -2196
  79. package/dist/velinstyle-components.min.js +261 -119
  80. package/dist/velinstyle.css +214 -6
  81. package/dist/velinstyle.d.ts +3 -0
  82. package/dist/velinstyle.min.css +1 -1
  83. package/package.json +143 -142
  84. package/packages/velinstyle-skills/catalog.json +1 -1
  85. package/src/components/data-table.css +19 -0
  86. package/src/components/empty-auth.css +33 -0
  87. package/src/components/table.css +16 -6
  88. package/src/components/transparency.css +138 -0
  89. package/src/velinstyle.css +2 -0
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Watch content files and re-run production pipeline (debounced, Windows-safe).
3
+ */
4
+ import { watch as fsWatch } from 'fs';
5
+ import { resolve } from 'path';
6
+
7
+ export function watchProduction(rootPath, onChange, { debounceMs = 300, ignoreNames = ['node_modules', 'dist', '.git'] } = {}) {
8
+ const abs = resolve(rootPath);
9
+ let timer = null;
10
+ let closed = false;
11
+
12
+ const kick = (event, filename) => {
13
+ if (closed) return;
14
+ if (filename && ignoreNames.some((n) => String(filename).includes(n))) return;
15
+ clearTimeout(timer);
16
+ timer = setTimeout(() => {
17
+ Promise.resolve()
18
+ .then(() => onChange({ event, filename }))
19
+ .catch((err) => console.error('[velinstyle production --watch]', err));
20
+ }, debounceMs);
21
+ };
22
+
23
+ let watcher;
24
+ try {
25
+ watcher = fsWatch(abs, { recursive: true }, kick);
26
+ } catch {
27
+ // recursive watch unsupported — watch root only
28
+ watcher = fsWatch(abs, kick);
29
+ }
30
+
31
+ return {
32
+ close() {
33
+ closed = true;
34
+ clearTimeout(timer);
35
+ try { watcher.close(); } catch { /* ignore */ }
36
+ },
37
+ };
38
+ }
package/cli/review.js CHANGED
@@ -225,6 +225,43 @@ export function reviewHtml(html, ctx = {}) {
225
225
  const errors = issues.filter((i) => i.severity === 'error').length;
226
226
  const warnings = issues.filter((i) => i.severity === 'warning').length;
227
227
 
228
+ // Optimization heuristics (soft) — full vendor without production output hints
229
+ const usesFullCss = /velinstyle\.min\.css|velinstyle\.css/i.test(text) && !/velin-production/i.test(text);
230
+ const usesFullIife = /velinstyle-components\.min\.js/i.test(text);
231
+ const unusedThemeLinks = [...text.matchAll(/themes\/([a-z0-9-]+)\.min\.css/gi)]
232
+ .map((m) => m[1].toLowerCase());
233
+ const themeAttrs = [...text.matchAll(/data-velin-theme\s*=\s*["']([^"']+)["']/gi)]
234
+ .flatMap((m) => m[1].toLowerCase().split(/[\s,|]+/));
235
+ const unusedThemes = unusedThemeLinks.filter((t) => themeAttrs.length && !themeAttrs.includes(t));
236
+
237
+ if (usesFullCss && usesFullIife) {
238
+ issues.push({
239
+ code: 'optimization.full-bundle',
240
+ severity: 'warning',
241
+ message: 'Full CSS + components IIFE without production output',
242
+ fix: 'Run `velinstyle build --production` and link dist/velin-production assets for publish.',
243
+ });
244
+ } else if (usesFullCss) {
245
+ issues.push({
246
+ code: 'optimization.full-css',
247
+ severity: 'warning',
248
+ message: 'Full VelinStyle CSS bundle linked',
249
+ fix: 'Prefer production CSS from `velinstyle production` for go-live.',
250
+ });
251
+ }
252
+ if (unusedThemes.length) {
253
+ issues.push({
254
+ code: 'optimization.unused-themes',
255
+ severity: 'warning',
256
+ message: `${unusedThemes.length} theme stylesheet(s) not referenced by data-velin-theme`,
257
+ fix: `Remove unused theme links (${unusedThemes.slice(0, 5).join(', ')}) or run production theme trim.`,
258
+ });
259
+ }
260
+
261
+ const optIssues = issues.filter((i) => i.code.startsWith('optimization'));
262
+ const unusedComponentHint = [...text.matchAll(/<(velin-[a-z0-9-]+)\b/gi)].length;
263
+ const classHits = [...text.matchAll(/\bvelin-[\w:-]+\b/g)].length;
264
+
228
265
  const score = (base, penalty) => Math.max(0, Math.round((base - penalty) * 10) / 10);
229
266
 
230
267
  let scores = {
@@ -234,6 +271,7 @@ export function reviewHtml(html, ctx = {}) {
234
271
  performance: score(10, issues.filter((i) => i.code.startsWith('perf')).length * 2),
235
272
  conversion: score(10, issues.filter((i) => i.code.startsWith('conversion')).length * 2),
236
273
  visual: score(9, issues.filter((i) => i.code.startsWith('design')).length * 1.2),
274
+ optimization: score(10, optIssues.length * 2 + (unusedThemes.length ? Math.min(3, unusedThemes.length * 0.5) : 0)),
237
275
  };
238
276
 
239
277
  if (thin) {
@@ -250,10 +288,13 @@ export function reviewHtml(html, ctx = {}) {
250
288
  if (profile === 'app') scores.seo = Math.min(scores.seo, 7);
251
289
  }
252
290
 
291
+ const finalErrors = issues.filter((i) => i.severity === 'error').length;
292
+ const finalWarnings = issues.filter((i) => i.severity === 'warning').length;
293
+
253
294
  const avg = Object.values(scores).reduce((a, b) => a + b, 0) / Object.values(scores).length;
254
295
  const promptScore = Math.max(0, Math.min(10, avg - (ctx.plan?.warnings?.length ? 1 : 0)));
255
296
 
256
- const gate = errors > 0 ? 'fail' : warnings > 0 ? 'warn' : 'pass';
297
+ const gate = finalErrors > 0 ? 'fail' : finalWarnings > 0 ? 'warn' : 'pass';
257
298
 
258
299
  return {
259
300
  version: 1,
@@ -262,6 +303,12 @@ export function reviewHtml(html, ctx = {}) {
262
303
  scores,
263
304
  issues,
264
305
  gate,
306
+ optimization: {
307
+ fullBundle: Boolean(usesFullCss && usesFullIife),
308
+ unusedThemes: unusedThemes.length,
309
+ componentTags: unusedComponentHint,
310
+ classTokens: classHits,
311
+ },
265
312
  };
266
313
  }
267
314
 
@@ -0,0 +1,33 @@
1
+ import { mkdirSync, writeFileSync } from 'fs';
2
+ import { join, dirname } from 'path';
3
+ import { fileURLToPath } from 'url';
4
+
5
+ const root = join(dirname(fileURLToPath(import.meta.url)), '..', '__fixtures__', 'atelier-library', 'showcases');
6
+ const items = [
7
+ { id: '01-login', title: 'Fixture Login' },
8
+ { id: '04-pricing', title: 'Fixture Pricing' },
9
+ { id: '36-calendar', title: 'Fixture Calendar' },
10
+ ];
11
+
12
+ for (const it of items) {
13
+ const d = join(root, it.id);
14
+ mkdirSync(d, { recursive: true });
15
+ writeFileSync(
16
+ join(d, 'index.html'),
17
+ `<!DOCTYPE html>
18
+ <html lang="en"><head><meta charset="UTF-8"><title>${it.title}</title>
19
+ <link rel="stylesheet" href="app.css"></head>
20
+ <body><div id="root" data-atelier-id="${it.id}"></div>
21
+ <script type="module" src="app.js"></script></body></html>
22
+ `,
23
+ );
24
+ writeFileSync(
25
+ join(d, 'app.js'),
26
+ `const root = document.getElementById('root');
27
+ root.innerHTML = '<section class="velin-section" data-fixture="${it.id}"><h1>${it.title}</h1></section>';
28
+ `,
29
+ );
30
+ writeFileSync(join(d, 'app.css'), `[data-fixture="${it.id}"] { padding: 1rem; }\n`);
31
+ }
32
+
33
+ console.log('fixtures at', root);
@@ -0,0 +1,221 @@
1
+ /**
2
+ * CLI: velinstyle transparency doctor|validate|report|export|migrate|scan
3
+ */
4
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync, statSync } from 'fs';
5
+ import { join, resolve, extname, basename } from 'path';
6
+ import {
7
+ createTransparencyEngine,
8
+ normalizePolicy,
9
+ } from '../core/transparency/index.js';
10
+
11
+ function loadPolicy(policyPath) {
12
+ if (!policyPath) return normalizePolicy({});
13
+ const abs = resolve(policyPath);
14
+ if (!existsSync(abs)) throw new Error(`Policy not found: ${abs}`);
15
+ return normalizePolicy(JSON.parse(readFileSync(abs, 'utf-8')));
16
+ }
17
+
18
+ function collectHtmlFiles(target) {
19
+ const abs = resolve(target || '.');
20
+ if (!existsSync(abs)) throw new Error(`Path not found: ${abs}`);
21
+ const st = statSync(abs);
22
+ if (st.isFile()) return [abs];
23
+ const out = [];
24
+ const walk = (dir) => {
25
+ for (const name of readdirSync(dir)) {
26
+ if (name === 'node_modules' || name === '.git' || name === 'dist') continue;
27
+ const p = join(dir, name);
28
+ const s = statSync(p);
29
+ if (s.isDirectory()) walk(p);
30
+ else if (/\.html?$/i.test(name)) out.push(p);
31
+ }
32
+ };
33
+ walk(abs);
34
+ return out;
35
+ }
36
+
37
+ function printScores(scores, C) {
38
+ if (!scores) return;
39
+ console.log(C.bold('\nScores'));
40
+ for (const [k, v] of Object.entries(scores)) {
41
+ console.log(` ${k.padEnd(14)} ${v}%`);
42
+ }
43
+ }
44
+
45
+ /**
46
+ * @param {string[]} args process.argv slice after "transparency"
47
+ * @param {{ C: object, getArg: Function, hasFlag: Function }} util
48
+ */
49
+ export async function transparencyCmd(args, util) {
50
+ const { C, getArg, hasFlag } = util;
51
+ const sub = args[0] || 'doctor';
52
+ const rest = args.slice(1);
53
+ const target = rest.find((a) => !a.startsWith('-')) || '.';
54
+ const policyPath = getArg('--policy');
55
+ const policy = loadPolicy(policyPath);
56
+ const asJson = hasFlag('--json');
57
+ const engine = createTransparencyEngine({ policy });
58
+
59
+ if (sub === 'scan') {
60
+ return runDoctor(target, engine, { asJson, C });
61
+ }
62
+ if (sub === 'doctor') {
63
+ return runDoctor(target, engine, { asJson, C });
64
+ }
65
+ if (sub === 'validate') {
66
+ return runValidate(target, engine, { asJson, C });
67
+ }
68
+ if (sub === 'report') {
69
+ return runReport(target, engine, { C, getArg });
70
+ }
71
+ if (sub === 'export') {
72
+ return runExport(target, engine, { C, getArg });
73
+ }
74
+ if (sub === 'migrate') {
75
+ return runMigrate(target, engine, { C, hasFlag, getArg });
76
+ }
77
+ if (sub === 'suggest') {
78
+ return runMigrate(target, engine, { C, hasFlag, getArg, forceDry: true });
79
+ }
80
+ if (sub === 'apply') {
81
+ return runMigrate(target, engine, { C, hasFlag, getArg, forceApply: true });
82
+ }
83
+
84
+ console.log(`Unknown transparency subcommand: ${sub}
85
+ Usage:
86
+ velinstyle transparency doctor|validate|report|export|migrate|scan [path]
87
+ `);
88
+ process.exitCode = 1;
89
+ }
90
+
91
+ async function runDoctor(target, engine, { asJson, C }) {
92
+ const files = collectHtmlFiles(target);
93
+ if (!files.length) {
94
+ console.log(C.yellow('No HTML files found.'));
95
+ process.exitCode = 1;
96
+ return;
97
+ }
98
+ const reports = [];
99
+ let failed = 0;
100
+ for (const file of files) {
101
+ const html = readFileSync(file, 'utf-8');
102
+ const report = await engine.doctor(html, { file });
103
+ reports.push(report);
104
+ if (!report.ok) failed += 1;
105
+ if (!asJson) {
106
+ console.log(C.bold(`\n── ${basename(file)} ──`));
107
+ console.log(report.ok ? C.green('PASS') : C.red('FAIL'));
108
+ console.log(`disclosures=${report.summary.disclosures} errors=${report.summary.errors} warnings=${report.summary.warnings}`);
109
+ printScores(report.scores, C);
110
+ for (const f of report.findings.slice(0, 40)) {
111
+ const color = f.severity === 'error' ? C.red : f.severity === 'warning' ? C.yellow : C.dim;
112
+ console.log(color(` [${f.severity}] ${f.code}: ${f.message}`));
113
+ }
114
+ if (report.findings.length > 40) console.log(C.dim(` … ${report.findings.length - 40} more`));
115
+ }
116
+ }
117
+ if (asJson) {
118
+ console.log(JSON.stringify(files.length === 1 ? reports[0] : { reports }, null, 2));
119
+ } else {
120
+ console.log(failed ? C.red(`\ntransparency doctor: ${failed} file(s) failed`) : C.green('\ntransparency doctor: ok'));
121
+ }
122
+ if (failed) process.exitCode = 1;
123
+ return reports;
124
+ }
125
+
126
+ async function runValidate(target, engine, { asJson, C }) {
127
+ const files = collectHtmlFiles(target);
128
+ const all = [];
129
+ let failed = 0;
130
+ for (const file of files) {
131
+ const html = readFileSync(file, 'utf-8');
132
+ const result = await engine.validate(html, { file });
133
+ all.push({ file, ...result });
134
+ if (!result.ok) failed += 1;
135
+ if (!asJson) {
136
+ console.log(`${basename(file)}: ${result.ok ? C.green('valid') : C.red('invalid')} (${result.findings.length} findings)`);
137
+ }
138
+ }
139
+ if (asJson) console.log(JSON.stringify(all, null, 2));
140
+ if (failed) process.exitCode = 1;
141
+ }
142
+
143
+ async function runReport(target, engine, { C, getArg }) {
144
+ const outDir = resolve(getArg('--out') || 'transparency-report');
145
+ mkdirSync(outDir, { recursive: true });
146
+ const files = collectHtmlFiles(target);
147
+ const combined = { schema: 'velinstyle.transparency.report.bundle', version: 1, files: [] };
148
+ for (const file of files) {
149
+ const html = readFileSync(file, 'utf-8');
150
+ const artifacts = await engine.report(html, { file, title: `Transparency — ${basename(file)}` });
151
+ const base = basename(file, extname(file));
152
+ writeFileSync(join(outDir, `${base}.report.json`), JSON.stringify(artifacts.json, null, 2));
153
+ writeFileSync(join(outDir, `${base}.report.sarif`), JSON.stringify(artifacts.sarif, null, 2));
154
+ writeFileSync(join(outDir, `${base}.report.html`), artifacts.html);
155
+ combined.files.push({ file, scores: artifacts.json.scores, ok: artifacts.json.ok });
156
+ }
157
+ writeFileSync(join(outDir, 'index.json'), JSON.stringify(combined, null, 2));
158
+ console.log(C.green(`Wrote transparency reports to ${outDir}`));
159
+ }
160
+
161
+ async function runExport(target, engine, { C, getArg }) {
162
+ const format = getArg('--format') || 'json';
163
+ const out = getArg('--output') || getArg('-o');
164
+ const files = collectHtmlFiles(target);
165
+ const fresh = createTransparencyEngine({ policy: engine.policy });
166
+ for (const file of files) {
167
+ const html = readFileSync(file, 'utf-8');
168
+ const { records } = await createTransparencyEngine({ policy: engine.policy }).ingest(html, { file });
169
+ for (const r of records) fresh.registry.register(r);
170
+ }
171
+ const body = fresh.export(format);
172
+ if (out) {
173
+ writeFileSync(resolve(out), body);
174
+ console.log(C.green(`Exported ${format} → ${out}`));
175
+ } else {
176
+ console.log(body);
177
+ }
178
+ }
179
+
180
+ async function runMigrate(target, engine, { C, hasFlag, getArg, forceDry, forceApply }) {
181
+ const apply = forceApply || hasFlag('--apply') || hasFlag('--write');
182
+ const dryRun = forceDry || (!apply);
183
+ const files = collectHtmlFiles(target);
184
+ let total = 0;
185
+ for (const file of files) {
186
+ const html = readFileSync(file, 'utf-8');
187
+ const result = await engine.migrate(html, { file, apply: apply && !dryRun, dryRun });
188
+ total += result.suggestions.length;
189
+ console.log(C.bold(`\n── ${basename(file)} ──`));
190
+ console.log(`${result.suggestions.length} suggestion(s)${apply && !dryRun ? `, applied ${result.applied}` : ' (dry-run)'}`);
191
+ for (const s of result.suggestions.slice(0, 30)) {
192
+ console.log(C.dim(` • ${s.id || s.field || s.kind}: ${s.reason}`));
193
+ }
194
+ if (apply && !dryRun && result.applied) {
195
+ writeFileSync(file, result.html);
196
+ console.log(C.green(` wrote ${file}`));
197
+ }
198
+ }
199
+ if (!total) console.log(C.green('\nNo migration suggestions.'));
200
+ }
201
+
202
+ /**
203
+ * Lightweight doctor for velinstyle check integration.
204
+ */
205
+ export async function transparencyCheckStep(path, { policyPath, quiet, C } = {}) {
206
+ const policy = loadPolicy(policyPath);
207
+ const engine = createTransparencyEngine({ policy });
208
+ const files = collectHtmlFiles(path);
209
+ if (!files.length) return { ok: true, skipped: true, scores: null };
210
+ let failed = 0;
211
+ let lastScores = null;
212
+ for (const file of files.slice(0, 20)) {
213
+ const report = await engine.doctor(readFileSync(file, 'utf-8'), { file });
214
+ lastScores = report.scores;
215
+ if (!report.ok) failed += 1;
216
+ if (!quiet) {
217
+ console.log(` ${basename(file)}: transparency ${report.scores?.transparency ?? '—'}% (${report.summary.errors} errors)`);
218
+ }
219
+ }
220
+ return { ok: failed === 0, failed, scores: lastScores };
221
+ }
@@ -38,6 +38,9 @@ export { default as VelinFormSummary } from './velin-form-summary.js';
38
38
  export { default as VelinCalendar } from './velin-calendar.js';
39
39
  export { default as VelinFileDropzone } from './velin-file-dropzone.js';
40
40
  export { default as VelinSearch } from './velin-search.js';
41
+ export { default as VelinOtpInput } from './velin-otp-input.js';
42
+ export { default as VelinPasswordStrength, scorePassword } from './velin-password-strength.js';
43
+ export { default as VelinEmptyState } from './velin-empty-state.js';
41
44
  export { bindDeclarativeSearch } from './velin-search.js';
42
45
  export { initReveal, initMotion, velinMotion } from './velin-reveal.js';
43
46
  export { initA11y, announce, getAnnouncer } from './a11y-entry.js';
@@ -19,6 +19,7 @@ export const COMPONENT_LOADERS = {
19
19
  'velin-drawer': () => import('../velin-drawer.js'),
20
20
  'velin-dropdown': () => import('../velin-dropdown.js'),
21
21
  'velin-email': () => import('../velin-email.js'),
22
+ 'velin-empty-state': () => import('../velin-empty-state.js'),
22
23
  'velin-file-dropzone': () => import('../velin-file-dropzone.js'),
23
24
  'velin-form-summary': () => import('../velin-form-summary.js'),
24
25
  'velin-icon': () => import('../velin-icon.js'),
@@ -26,6 +27,8 @@ export const COMPONENT_LOADERS = {
26
27
  'velin-live-dot': () => import('../velin-live-dot.js'),
27
28
  'velin-menubar': () => import('../velin-menubar.js'),
28
29
  'velin-modal': () => import('../velin-modal.js'),
30
+ 'velin-otp-input': () => import('../velin-otp-input.js'),
31
+ 'velin-password-strength': () => import('../velin-password-strength.js'),
29
32
  'velin-persist': () => import('../velin-persist.js'),
30
33
  'velin-popover': () => import('../velin-popover.js'),
31
34
  'velin-progress-ring': () => import('../velin-progress-ring.js'),
@@ -49,25 +49,28 @@ const styles = `
49
49
  `;
50
50
 
51
51
  class VelinDrawer extends HTMLElement {
52
- static get observedAttributes() { return ['open']; }
52
+ static get observedAttributes() { return ['open', 'title']; }
53
53
 
54
54
  constructor() {
55
55
  super();
56
56
  this.attachShadow({ mode: 'open', delegatesFocus: true });
57
57
  this._prev = null;
58
58
  this._onKey = this._onKey.bind(this);
59
+ this._onTitleSlot = this._onTitleSlot.bind(this);
59
60
  }
60
61
 
61
62
  connectedCallback() {
62
- const title = this.getAttribute('title') || '';
63
- const safeTitle = escapeHTML(title);
63
+ if (this.shadowRoot.querySelector('.drawer')) return;
64
64
  const titleId = 'velin-drawer-title';
65
65
  this.shadowRoot.innerHTML = `
66
66
  <style>${styles}</style>
67
67
  <div class="overlay" part="overlay"></div>
68
68
  <div class="drawer" role="dialog" aria-modal="true" aria-labelledby="${titleId}" part="drawer">
69
69
  <div class="header" part="header">
70
- <h2 class="title" id="${titleId}">${safeTitle}</h2>
70
+ <h2 class="title" id="${titleId}" part="title">
71
+ <slot name="title"></slot>
72
+ <span class="title-fallback"></span>
73
+ </h2>
71
74
  <button class="close-btn" aria-label="Close" part="close">&#215;</button>
72
75
  </div>
73
76
  <div class="body" part="body"><slot></slot></div>
@@ -75,15 +78,51 @@ class VelinDrawer extends HTMLElement {
75
78
  `;
76
79
  this.shadowRoot.querySelector('.close-btn').addEventListener('click', () => this.close());
77
80
  this.shadowRoot.querySelector('.overlay').addEventListener('click', () => this.close());
81
+ this.shadowRoot.querySelector('slot[name="title"]').addEventListener('slotchange', this._onTitleSlot);
82
+ this._syncTitle();
78
83
  }
79
84
 
80
85
  attributeChangedCallback(name) {
81
86
  if (name === 'open') this.hasAttribute('open') ? this._open() : this._close();
87
+ if (name === 'title') this._syncTitle();
82
88
  }
83
89
 
84
90
  open() { this.setAttribute('open', ''); }
85
91
  close() { this.removeAttribute('open'); this.dispatchEvent(new CustomEvent('velin-close', { bubbles: true })); }
86
92
 
93
+ _syncTitle() {
94
+ const fallback = this.shadowRoot?.querySelector('.title-fallback');
95
+ if (!fallback) return;
96
+ fallback.textContent = this.getAttribute('title') || '';
97
+ }
98
+
99
+ _hasTitleSlot() {
100
+ const slot = this.shadowRoot?.querySelector('slot[name="title"]');
101
+ if (!slot) return false;
102
+ return slot.assignedNodes({ flatten: true }).some((n) => {
103
+ if (n.nodeType === Node.TEXT_NODE) return Boolean(n.textContent.trim());
104
+ return n.nodeType === Node.ELEMENT_NODE;
105
+ });
106
+ }
107
+
108
+ _onTitleSlot() {
109
+ const dialog = this.shadowRoot?.querySelector('[role="dialog"]');
110
+ const fallback = this.shadowRoot?.querySelector('.title-fallback');
111
+ if (!dialog || !fallback) return;
112
+ if (this._hasTitleSlot()) {
113
+ fallback.hidden = true;
114
+ dialog.removeAttribute('aria-labelledby');
115
+ const slot = this.shadowRoot.querySelector('slot[name="title"]');
116
+ const label = slot.assignedNodes({ flatten: true }).map((n) => n.textContent || '').join(' ').trim();
117
+ if (label) dialog.setAttribute('aria-label', label);
118
+ } else {
119
+ fallback.hidden = false;
120
+ dialog.removeAttribute('aria-label');
121
+ dialog.setAttribute('aria-labelledby', 'velin-drawer-title');
122
+ this._syncTitle();
123
+ }
124
+ }
125
+
87
126
  _open() {
88
127
  this._prev = saveFocus();
89
128
  setBackgroundInert(this);
@@ -0,0 +1,83 @@
1
+ const styles = `
2
+ :host {
3
+ display: block;
4
+ text-align: center;
5
+ padding: var(--velin-space-8, 2rem) var(--velin-space-4, 1rem);
6
+ }
7
+ .illustration {
8
+ display: flex;
9
+ justify-content: center;
10
+ margin-block-end: var(--velin-space-4, 1rem);
11
+ color: var(--velin-color-text-muted, #666);
12
+ }
13
+ .title {
14
+ margin: 0 0 var(--velin-space-2, 0.5rem);
15
+ font-size: var(--velin-text-xl, 1.25rem);
16
+ font-weight: var(--velin-weight-bold, 700);
17
+ color: var(--velin-color-text, #111);
18
+ }
19
+ .description {
20
+ margin: 0 0 var(--velin-space-4, 1rem);
21
+ color: var(--velin-color-text-muted, #666);
22
+ max-inline-size: 36rem;
23
+ margin-inline: auto;
24
+ }
25
+ .actions {
26
+ display: flex;
27
+ flex-wrap: wrap;
28
+ gap: var(--velin-space-3, 0.75rem);
29
+ justify-content: center;
30
+ }
31
+ `;
32
+
33
+ class VelinEmptyState extends HTMLElement {
34
+ static get observedAttributes() {
35
+ return ['heading', 'description'];
36
+ }
37
+
38
+ constructor() {
39
+ super();
40
+ this.attachShadow({ mode: 'open' });
41
+ }
42
+
43
+ connectedCallback() {
44
+ if (this.shadowRoot.querySelector('.root')) {
45
+ this._syncText();
46
+ return;
47
+ }
48
+ const titleId = 'velin-empty-title';
49
+ this.shadowRoot.innerHTML = `
50
+ <style>${styles}</style>
51
+ <section class="root" part="root" role="status" aria-labelledby="${titleId}">
52
+ <div class="illustration" part="illustration"><slot name="illustration"></slot></div>
53
+ <h2 class="title" id="${titleId}" part="title">
54
+ <slot name="title"><span class="heading-fallback"></span></slot>
55
+ </h2>
56
+ <div class="description" part="description">
57
+ <slot name="description"><span class="description-fallback"></span></slot>
58
+ </div>
59
+ <div class="actions" part="actions"><slot name="actions"></slot></div>
60
+ <slot></slot>
61
+ </section>
62
+ `;
63
+ this._syncText();
64
+ }
65
+
66
+ attributeChangedCallback() {
67
+ this._syncText();
68
+ }
69
+
70
+ _syncText() {
71
+ const heading = this.shadowRoot?.querySelector('.heading-fallback');
72
+ const description = this.shadowRoot?.querySelector('.description-fallback');
73
+ if (heading) heading.textContent = this.getAttribute('heading') || 'Nothing here yet';
74
+ if (description) {
75
+ const text = this.getAttribute('description') || '';
76
+ description.textContent = text;
77
+ description.hidden = !text;
78
+ }
79
+ }
80
+ }
81
+
82
+ customElements.define('velin-empty-state', VelinEmptyState);
83
+ export default VelinEmptyState;