@birdapi/velinstyle 1.2.0 → 1.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.de.md +150 -24
- package/README.md +155 -33
- package/cli/__fixtures__/atelier-library/showcases/01-login/app.css +1 -0
- package/cli/__fixtures__/atelier-library/showcases/01-login/app.js +2 -0
- package/cli/__fixtures__/atelier-library/showcases/01-login/index.html +5 -0
- package/cli/__fixtures__/atelier-library/showcases/04-pricing/app.css +1 -0
- package/cli/__fixtures__/atelier-library/showcases/04-pricing/app.js +2 -0
- package/cli/__fixtures__/atelier-library/showcases/04-pricing/index.html +5 -0
- package/cli/__fixtures__/atelier-library/showcases/36-calendar/app.css +1 -0
- package/cli/__fixtures__/atelier-library/showcases/36-calendar/app.js +2 -0
- package/cli/__fixtures__/atelier-library/showcases/36-calendar/index.html +5 -0
- package/cli/atelier-catalog.json +1267 -0
- package/cli/atelier-formats.js +159 -0
- package/cli/atelier.js +382 -0
- package/cli/blueprints/empty-state.html +3 -5
- package/cli/cli-manifest.json +23 -10
- package/cli/docgen/extract-attributes.js +2 -0
- package/cli/docgen/extract-cli.js +1 -1
- package/cli/index.js +181 -6
- package/cli/production/component-graph.json +166 -0
- package/cli/production/explain.js +52 -0
- package/cli/production/extract.js +238 -0
- package/cli/production/graph.js +102 -0
- package/cli/production/report.js +78 -0
- package/cli/production/run.js +312 -0
- package/cli/production/trim-css.js +177 -0
- package/cli/production/trim-fonts.js +23 -0
- package/cli/production/trim-icons.js +38 -0
- package/cli/production/trim-js.js +48 -0
- package/cli/production/trim-motion.js +22 -0
- package/cli/production/trim-themes.js +50 -0
- package/cli/production/watch.js +38 -0
- package/cli/review.js +48 -1
- package/cli/scripts/write-atelier-fixtures.mjs +33 -0
- package/cli/transparency.js +221 -0
- package/components/index.js +3 -0
- package/components/runtime/component-loaders.js +3 -0
- package/components/velin-drawer.js +43 -4
- package/components/velin-empty-state.js +83 -0
- package/components/velin-modal.js +76 -15
- package/components/velin-otp-input.js +220 -0
- package/components/velin-password-strength.js +147 -0
- package/components/velin-sheet.js +49 -4
- package/core/a11y/component-contracts.json +23 -4
- package/core/attributes/registry.js +14 -0
- package/core/meta/knowledge/components.json +75 -3
- package/core/meta/schema.js +10 -0
- package/core/transparency/attach.js +74 -0
- package/core/transparency/claims.js +97 -0
- package/core/transparency/doctor.js +142 -0
- package/core/transparency/engine.js +75 -0
- package/core/transparency/export.js +98 -0
- package/core/transparency/index.js +30 -0
- package/core/transparency/migrate.js +130 -0
- package/core/transparency/normalize.js +125 -0
- package/core/transparency/policy.js +105 -0
- package/core/transparency/providers.js +235 -0
- package/core/transparency/registry.js +104 -0
- package/core/transparency/renderer.js +111 -0
- package/core/transparency/reporter.js +113 -0
- package/core/transparency/validator.js +112 -0
- package/dist/chunks/attach-H2ZSEAE6.js +365 -0
- package/dist/chunks/attributes-2ORR27KA.js +404 -0
- package/dist/chunks/attributes-DZCXX2RZ.js +404 -0
- package/dist/chunks/chunk-CQMTCEI6.js +113 -0
- package/dist/chunks/chunk-Y6HN7HWX.js +116 -0
- package/dist/chunks/runtime-entry.js +1 -1
- package/dist/chunks/velin-drawer-A7Q7EBOS.js +162 -0
- package/dist/chunks/velin-empty-state-3NTUH2RF.js +81 -0
- package/dist/chunks/velin-modal-3MV4FYBK.js +231 -0
- package/dist/chunks/velin-otp-input-PSK42MM6.js +207 -0
- package/dist/chunks/velin-password-strength-TAXMLSED.js +136 -0
- package/dist/chunks/velin-sheet-N2VVYXFP.js +157 -0
- package/dist/llms.txt +5 -4
- package/dist/search-index.json +94 -5
- package/dist/velin-agent.json +290 -23
- package/dist/velinstyle-components.iife.js +3189 -2202
- package/dist/velinstyle-components.js +3187 -2196
- package/dist/velinstyle-components.min.js +261 -119
- package/dist/velinstyle.css +214 -6
- package/dist/velinstyle.d.ts +3 -0
- package/dist/velinstyle.min.css +1 -1
- package/package.json +143 -142
- package/packages/velinstyle-skills/catalog.json +1 -1
- package/src/components/data-table.css +19 -0
- package/src/components/empty-auth.css +33 -0
- package/src/components/table.css +16 -6
- package/src/components/transparency.css +138 -0
- package/src/velinstyle.css +2 -0
|
@@ -0,0 +1,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 =
|
|
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
|
+
}
|
package/components/index.js
CHANGED
|
@@ -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
|
-
|
|
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}"
|
|
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">×</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;
|