@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,111 @@
|
|
|
1
|
+
/** @type {Map<string, (el: HTMLElement, record: object) => void>} */
|
|
2
|
+
const renderers = new Map();
|
|
3
|
+
|
|
4
|
+
export function registerTransparencyRenderer(name, fn) {
|
|
5
|
+
renderers.set(name, fn);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function listTransparencyRenderers() {
|
|
9
|
+
return [...renderers.keys()];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Render a disclosure mark onto an element.
|
|
14
|
+
* @param {HTMLElement} el
|
|
15
|
+
* @param {import('./registry.js').DisclosureRecord} record
|
|
16
|
+
*/
|
|
17
|
+
export function renderDisclosure(el, record) {
|
|
18
|
+
if (typeof document === 'undefined' || !el) return null;
|
|
19
|
+
const name = record.renderer || 'badge';
|
|
20
|
+
const custom = renderers.get(name);
|
|
21
|
+
if (custom) {
|
|
22
|
+
custom(el, record);
|
|
23
|
+
return el.querySelector('.velin-transparency');
|
|
24
|
+
}
|
|
25
|
+
return defaultRender(el, record);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function defaultRender(el, record) {
|
|
29
|
+
const cs = getComputedStyle(el);
|
|
30
|
+
if (cs.position === 'static') el.style.position = 'relative';
|
|
31
|
+
|
|
32
|
+
let mark = el.querySelector(':scope > .velin-transparency');
|
|
33
|
+
if (!mark) {
|
|
34
|
+
mark = document.createElement('div');
|
|
35
|
+
el.prepend(mark);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const renderer = record.renderer || 'badge';
|
|
39
|
+
const tone = record.tone || 'neutral';
|
|
40
|
+
const position = record.position || 'top-right';
|
|
41
|
+
mark.className = [
|
|
42
|
+
'velin-transparency',
|
|
43
|
+
`velin-transparency--${renderer}`,
|
|
44
|
+
`velin-transparency--tone-${tone}`,
|
|
45
|
+
`velin-transparency--pos-${position}`,
|
|
46
|
+
].join(' ');
|
|
47
|
+
mark.setAttribute('data-velin-transparency-id', record.id);
|
|
48
|
+
mark.setAttribute('role', 'note');
|
|
49
|
+
|
|
50
|
+
const claimsText = (record.claims || []).join(', ');
|
|
51
|
+
const prov = record.provenance || {};
|
|
52
|
+
const sr = document.createElement('span');
|
|
53
|
+
sr.className = 'velin-sr-only';
|
|
54
|
+
const lang = (el.closest('[lang]')?.getAttribute('lang') || document.documentElement.lang || 'en').startsWith('de')
|
|
55
|
+
? 'de'
|
|
56
|
+
: 'en';
|
|
57
|
+
sr.textContent = lang === 'de'
|
|
58
|
+
? `Transparenzhinweis: ${record.label}. ${claimsText}. ${provenanceSr(prov, 'de')}`
|
|
59
|
+
: `Transparency notice: ${record.label}. ${claimsText}. ${provenanceSr(prov, 'en')}`;
|
|
60
|
+
|
|
61
|
+
const visible = document.createElement('span');
|
|
62
|
+
visible.className = 'velin-transparency__label';
|
|
63
|
+
visible.textContent = record.label || 'Transparency';
|
|
64
|
+
|
|
65
|
+
const details = document.createElement('span');
|
|
66
|
+
details.className = 'velin-transparency__details';
|
|
67
|
+
details.hidden = renderer === 'badge' || renderer === 'icon';
|
|
68
|
+
details.textContent = formatDetails(record, lang);
|
|
69
|
+
|
|
70
|
+
mark.replaceChildren(sr, visible, details);
|
|
71
|
+
|
|
72
|
+
if (record.description) mark.title = record.description;
|
|
73
|
+
else if (details.textContent) mark.title = details.textContent;
|
|
74
|
+
|
|
75
|
+
el.setAttribute('data-velin-transparency', record.id);
|
|
76
|
+
return mark;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function provenanceSr(p, lang) {
|
|
80
|
+
const parts = [];
|
|
81
|
+
if (p.createdBy) parts.push(lang === 'de' ? `Erstellt von ${p.createdBy}` : `Created by ${p.createdBy}`);
|
|
82
|
+
if (p.createdAt) parts.push(lang === 'de' ? `am ${p.createdAt}` : `on ${p.createdAt}`);
|
|
83
|
+
if (p.approvedBy) parts.push(lang === 'de' ? `Freigegeben von ${p.approvedBy}` : `Approved by ${p.approvedBy}`);
|
|
84
|
+
if (p.license) parts.push(p.license);
|
|
85
|
+
if (p.version) parts.push(`v${p.version}`);
|
|
86
|
+
return parts.join('. ');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function formatDetails(record, lang) {
|
|
90
|
+
const p = record.provenance || {};
|
|
91
|
+
const bits = [];
|
|
92
|
+
if (p.approvedBy) bits.push(lang === 'de' ? `Freigabe: ${p.approvedBy}` : `Approved: ${p.approvedBy}`);
|
|
93
|
+
if (p.license) bits.push(p.license);
|
|
94
|
+
if (p.version) bits.push(`v${p.version}`);
|
|
95
|
+
if (p.updated || record.updated) bits.push(record.updated || p.publishedAt || '');
|
|
96
|
+
return bits.filter(Boolean).join(' · ');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Register alias names to default path
|
|
100
|
+
for (const name of ['overlay', 'badge', 'inline', 'tooltip', 'footer', 'ribbon', 'panel', 'icon', 'corner-badge', 'stamp', 'sidebar', 'floating-card', 'banner']) {
|
|
101
|
+
registerTransparencyRenderer(name, (el, record) => {
|
|
102
|
+
defaultRender(el, { ...record, renderer: normalizeRendererName(name) });
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function normalizeRendererName(name) {
|
|
107
|
+
if (name === 'corner-badge' || name === 'stamp') return 'badge';
|
|
108
|
+
if (name === 'floating-card' || name === 'sidebar') return 'panel';
|
|
109
|
+
if (name === 'banner') return 'footer';
|
|
110
|
+
return name;
|
|
111
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build report artifacts from a doctor result.
|
|
3
|
+
* @param {Awaited<ReturnType<import('./doctor.js').transparencyDoctor>>} report
|
|
4
|
+
* @param {{ title?: string }} [opts]
|
|
5
|
+
*/
|
|
6
|
+
export function buildTransparencyReports(report, opts = {}) {
|
|
7
|
+
const title = opts.title || 'Velin Transparency Report';
|
|
8
|
+
const json = {
|
|
9
|
+
schema: 'velinstyle.transparency.report',
|
|
10
|
+
version: 1,
|
|
11
|
+
generatedAt: new Date().toISOString(),
|
|
12
|
+
title,
|
|
13
|
+
...report,
|
|
14
|
+
};
|
|
15
|
+
const sarif = toSarif(report, title);
|
|
16
|
+
const html = toHtml(report, title);
|
|
17
|
+
return { json, sarif, html };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function toSarif(report, title) {
|
|
21
|
+
const results = (report.findings || []).map((f) => ({
|
|
22
|
+
ruleId: f.code,
|
|
23
|
+
level: f.severity === 'error' ? 'error' : f.severity === 'warning' ? 'warning' : 'note',
|
|
24
|
+
message: { text: f.message },
|
|
25
|
+
locations: f.id || f.target
|
|
26
|
+
? [{
|
|
27
|
+
physicalLocation: {
|
|
28
|
+
artifactLocation: { uri: report.file || 'document.html' },
|
|
29
|
+
region: { snippet: { text: f.id || f.target?.src || '' } },
|
|
30
|
+
},
|
|
31
|
+
}]
|
|
32
|
+
: [],
|
|
33
|
+
}));
|
|
34
|
+
return {
|
|
35
|
+
$schema: 'https://json.schemastore.org/sarif-2.1.0.json',
|
|
36
|
+
version: '2.1.0',
|
|
37
|
+
runs: [{
|
|
38
|
+
tool: {
|
|
39
|
+
driver: {
|
|
40
|
+
name: 'velinstyle-transparency',
|
|
41
|
+
informationUri: 'https://velinstyle.info',
|
|
42
|
+
rules: [],
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
results,
|
|
46
|
+
properties: { title, scores: report.scores },
|
|
47
|
+
}],
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function toHtml(report, title) {
|
|
52
|
+
const scores = report.scores || {};
|
|
53
|
+
const rows = (report.findings || []).map((f) => `
|
|
54
|
+
<tr>
|
|
55
|
+
<td>${esc(f.severity)}</td>
|
|
56
|
+
<td><code>${esc(f.code)}</code></td>
|
|
57
|
+
<td>${esc(f.message)}</td>
|
|
58
|
+
<td>${esc(f.id || '')}</td>
|
|
59
|
+
</tr>`).join('');
|
|
60
|
+
const items = (report.registry?.items || []).map((r) => `
|
|
61
|
+
<tr>
|
|
62
|
+
<td><code>${esc(r.id)}</code></td>
|
|
63
|
+
<td>${esc(r.type)}</td>
|
|
64
|
+
<td>${esc(r.label)}</td>
|
|
65
|
+
<td>${esc((r.claims || []).join(', '))}</td>
|
|
66
|
+
<td>${esc(JSON.stringify(r.provenance || {}))}</td>
|
|
67
|
+
</tr>`).join('');
|
|
68
|
+
return `<!DOCTYPE html>
|
|
69
|
+
<html lang="en">
|
|
70
|
+
<head>
|
|
71
|
+
<meta charset="utf-8">
|
|
72
|
+
<title>${esc(title)}</title>
|
|
73
|
+
<style>
|
|
74
|
+
body{font-family:system-ui,sans-serif;margin:2rem;background:#0c0b0a;color:#f5f2eb}
|
|
75
|
+
table{border-collapse:collapse;width:100%;margin-block:1rem}
|
|
76
|
+
th,td{border:1px solid #333;padding:.5rem;text-align:left;vertical-align:top}
|
|
77
|
+
th{background:#1a1917}
|
|
78
|
+
.scores{display:flex;flex-wrap:wrap;gap:.75rem}
|
|
79
|
+
.score{padding:.75rem 1rem;background:#1a1917;border-radius:8px;min-width:7rem}
|
|
80
|
+
.ok{color:#7ddea0}.bad{color:#f5a5a5}
|
|
81
|
+
</style>
|
|
82
|
+
</head>
|
|
83
|
+
<body>
|
|
84
|
+
<h1>${esc(title)}</h1>
|
|
85
|
+
<p class="${report.ok ? 'ok' : 'bad'}">${report.ok ? 'PASS' : 'FAIL'} — ${report.summary?.errors || 0} errors, ${report.summary?.warnings || 0} warnings</p>
|
|
86
|
+
<div class="scores">
|
|
87
|
+
${scoreCard('Transparency', scores.transparency)}
|
|
88
|
+
${scoreCard('AI', scores.ai)}
|
|
89
|
+
${scoreCard('Trust', scores.trust)}
|
|
90
|
+
${scoreCard('Metadata', scores.metadata)}
|
|
91
|
+
${scoreCard('Compliance', scores.compliance)}
|
|
92
|
+
${scoreCard('Provenance', scores.provenance)}
|
|
93
|
+
</div>
|
|
94
|
+
<h2>Findings</h2>
|
|
95
|
+
<table><thead><tr><th>Severity</th><th>Code</th><th>Message</th><th>Id</th></tr></thead><tbody>${rows || '<tr><td colspan="4">None</td></tr>'}</tbody></table>
|
|
96
|
+
<h2>Registry</h2>
|
|
97
|
+
<table><thead><tr><th>Id</th><th>Type</th><th>Label</th><th>Claims</th><th>Provenance</th></tr></thead><tbody>${items || '<tr><td colspan="5">Empty</td></tr>'}</tbody></table>
|
|
98
|
+
</body>
|
|
99
|
+
</html>`;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function scoreCard(name, value) {
|
|
103
|
+
const v = value == null ? '—' : `${value}%`;
|
|
104
|
+
return `<div class="score"><strong>${esc(name)}</strong><div>${v}</div></div>`;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function esc(s) {
|
|
108
|
+
return String(s ?? '')
|
|
109
|
+
.replace(/&/g, '&')
|
|
110
|
+
.replace(/</g, '<')
|
|
111
|
+
.replace(/>/g, '>')
|
|
112
|
+
.replace(/"/g, '"');
|
|
113
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { CLAIM_CATALOG } from './claims.js';
|
|
2
|
+
import { requiredProvenanceFields } from './policy.js';
|
|
3
|
+
|
|
4
|
+
const VALID_STATUS = new Set([
|
|
5
|
+
'generated', 'assisted', 'ai-assisted', 'human-reviewed', 'human-edited', 'verified', 'edited', 'draft',
|
|
6
|
+
]);
|
|
7
|
+
const VALID_REVIEW = new Set(['human', 'human-reviewed', 'verified', 'none']);
|
|
8
|
+
const VALID_TYPES = new Set([
|
|
9
|
+
'ai', 'trust', 'compliance', 'metadata', 'review', 'verification', 'license',
|
|
10
|
+
'accessibility', 'security', 'custom', 'image', 'video', 'audio', 'text', 'pdf', 'document',
|
|
11
|
+
]);
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @param {import('./registry.js').DisclosureRecord} record
|
|
15
|
+
* @param {object} policy
|
|
16
|
+
*/
|
|
17
|
+
export function validateRecord(record, policy) {
|
|
18
|
+
const findings = [];
|
|
19
|
+
if (!record?.id) {
|
|
20
|
+
findings.push(finding('error', 'invalid.id', 'Disclosure missing id'));
|
|
21
|
+
}
|
|
22
|
+
if (record.type && !VALID_TYPES.has(String(record.type).toLowerCase())) {
|
|
23
|
+
findings.push(finding('warning', 'invalid.type', `Unknown type "${record.type}"`, record.id));
|
|
24
|
+
}
|
|
25
|
+
if (record.status && !VALID_STATUS.has(String(record.status).toLowerCase())) {
|
|
26
|
+
findings.push(finding('error', 'invalid.status', `Invalid status "${record.status}"`, record.id));
|
|
27
|
+
}
|
|
28
|
+
if (record.review && !VALID_REVIEW.has(String(record.review).toLowerCase())) {
|
|
29
|
+
findings.push(finding('error', 'invalid.review', `Invalid review "${record.review}"`, record.id));
|
|
30
|
+
}
|
|
31
|
+
if (!record.label) {
|
|
32
|
+
findings.push(finding('warning', 'missing.label', 'Missing label', record.id));
|
|
33
|
+
}
|
|
34
|
+
if (policy.rules?.requireReview && !record.review && !record.claims?.includes('review.human')) {
|
|
35
|
+
findings.push(finding('error', 'policy.requireReview', 'Policy requires review', record.id));
|
|
36
|
+
}
|
|
37
|
+
if (policy.rules?.allowGenerated === false && record.claims?.includes('ai.generated')) {
|
|
38
|
+
findings.push(finding('error', 'policy.allowGenerated', 'Generated content not allowed by policy', record.id));
|
|
39
|
+
}
|
|
40
|
+
if (policy.rules?.minimumStatus === 'human-reviewed') {
|
|
41
|
+
const ok = record.claims?.includes('review.human') || record.claims?.includes('review.verified')
|
|
42
|
+
|| record.review === 'human' || record.review === 'human-reviewed' || record.review === 'verified';
|
|
43
|
+
if (!ok && (record.claims?.includes('ai.generated') || record.claims?.includes('ai.assisted'))) {
|
|
44
|
+
findings.push(finding('error', 'policy.minimumStatus', 'minimumStatus human-reviewed not met', record.id));
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
for (const claim of record.claims || []) {
|
|
48
|
+
if (!CLAIM_CATALOG[claim] && !String(claim).startsWith('custom.')) {
|
|
49
|
+
findings.push(finding('warning', 'unknown.claim', `Unknown claim "${claim}"`, record.id));
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
const mediaKind = inferMediaKind(record);
|
|
53
|
+
const required = requiredProvenanceFields(mediaKind, policy);
|
|
54
|
+
for (const field of required) {
|
|
55
|
+
if (!record.provenance?.[field]) {
|
|
56
|
+
findings.push(finding('error', `missing-provenance.${field}`, `Missing provenance.${field}`, record.id));
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
for (const field of policy.provenance?.recommended || []) {
|
|
60
|
+
if (!record.provenance?.[field]) {
|
|
61
|
+
findings.push(finding('info', `recommended-provenance.${field}`, `Recommended provenance.${field}`, record.id));
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (record.updated || record.provenance?.reviewedAt) {
|
|
65
|
+
const staleDays = policy.rules?.staleDays ?? 365;
|
|
66
|
+
const dateStr = record.provenance?.reviewedAt || record.updated;
|
|
67
|
+
const age = ageDays(dateStr);
|
|
68
|
+
if (age != null && age > staleDays) {
|
|
69
|
+
findings.push(finding('warning', 'stale-reviewedAt', `Review/update is ${age} days old (limit ${staleDays})`, record.id));
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return findings;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* @param {import('./registry.js').DisclosureRecord[]} records
|
|
77
|
+
* @param {object} policy
|
|
78
|
+
*/
|
|
79
|
+
export function validateRecords(records, policy) {
|
|
80
|
+
const findings = [];
|
|
81
|
+
const seen = new Map();
|
|
82
|
+
for (const r of records) {
|
|
83
|
+
if (seen.has(r.id)) {
|
|
84
|
+
findings.push(finding('error', 'duplicate.id', `Duplicate disclosure id "${r.id}"`, r.id));
|
|
85
|
+
}
|
|
86
|
+
seen.set(r.id, true);
|
|
87
|
+
findings.push(...validateRecord(r, policy));
|
|
88
|
+
}
|
|
89
|
+
return findings;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function inferMediaKind(record) {
|
|
93
|
+
const tag = String(record.target?.tag || '').toUpperCase();
|
|
94
|
+
const type = String(record.type || '').toLowerCase();
|
|
95
|
+
const src = String(record.target?.src || '');
|
|
96
|
+
if (tag === 'IMG' || type === 'image' || /\.(png|jpe?g|webp|gif|svg)(\?|$)/i.test(src)) return 'images';
|
|
97
|
+
if (tag === 'VIDEO' || type === 'video' || /\.(mp4|webm|mov)(\?|$)/i.test(src)) return 'videos';
|
|
98
|
+
if (tag === 'AUDIO' || type === 'audio' || /\.(mp3|wav|ogg)(\?|$)/i.test(src)) return 'audio';
|
|
99
|
+
if (type === 'pdf' || /\.pdf(\?|$)/i.test(src)) return 'pdf';
|
|
100
|
+
return 'text';
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function ageDays(dateStr) {
|
|
104
|
+
if (!dateStr) return null;
|
|
105
|
+
const t = Date.parse(dateStr);
|
|
106
|
+
if (Number.isNaN(t)) return null;
|
|
107
|
+
return Math.floor((Date.now() - t) / 86400000);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function finding(severity, code, message, id) {
|
|
111
|
+
return { severity, code, message, id: id || null };
|
|
112
|
+
}
|