@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.
- package/README.de.md +148 -11
- package/README.md +174 -16
- 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 +441 -161
- package/cli/docgen/extract-cli.js +2 -1
- package/cli/docs-generate.js +9 -0
- package/cli/experience.js +173 -0
- package/cli/index.js +1384 -361
- package/cli/motion.js +738 -0
- package/cli/production/component-graph.json +166 -0
- package/cli/production/explain.js +52 -0
- package/cli/production/extract.js +278 -0
- package/cli/production/graph.js +102 -0
- package/cli/production/report.js +168 -0
- package/cli/production/run.js +355 -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/security.js +190 -0
- package/cli/skills.js +53 -10
- package/cli/transparency.js +140 -41
- package/cli/workflow.js +32 -17
- 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/components/velin-theme-toggle.js +15 -1
- package/core/a11y/component-contracts.json +391 -255
- package/core/attributes/registry.js +1 -1
- package/core/meta/knowledge/components.json +75 -3
- package/core/motion/analyze.js +38 -0
- package/core/motion/blueprint.js +51 -0
- package/core/motion/composer.js +114 -0
- package/core/motion/data/registry.json +352 -0
- package/core/motion/effects.js +17 -1
- package/core/motion/index.js +112 -9
- package/core/motion/optimize.js +44 -0
- package/core/motion/policy.js +77 -0
- package/core/motion/registry.js +131 -0
- package/core/motion/review.js +59 -0
- package/core/motion/scan.js +150 -0
- package/core/motion/timeline.js +68 -0
- package/core/motion/transitions.js +49 -0
- package/core/motion/triggers.js +168 -0
- package/core/security/checks/fs.js +92 -0
- package/core/security/detect.js +35 -0
- package/core/security/engine.js +116 -0
- package/core/security/index.js +18 -0
- package/core/security/registry.js +85 -0
- package/core/security/report/report.js +113 -0
- package/core/security/rules/ci/index.js +146 -0
- package/core/security/rules/consumer/index.js +174 -0
- package/core/security/rules/deps/index.js +182 -0
- package/core/security/rules/index.js +24 -0
- package/core/security/rules/publish/index.js +230 -0
- package/core/security/rules/repo/index.js +143 -0
- package/core/security/rules/secrets/index.js +133 -0
- package/core/security/score/dependency-health.js +53 -0
- package/core/security/score/release-health.js +66 -0
- package/core/security/score/score.js +102 -0
- package/core/security/timeline/timeline.js +76 -0
- package/dist/chunks/attributes-2ORR27KA.js +404 -0
- package/dist/chunks/attributes-HK7VIOLA.js +1080 -0
- package/dist/chunks/attributes-VRHHVNDO.js +1080 -0
- package/dist/chunks/chunk-MYKUI364.js +116 -0
- package/dist/chunks/chunk-NEVBPD5T.js +116 -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/chunks/velin-theme-toggle-J2NHC7IM.js +304 -0
- package/dist/llms.txt +4 -4
- package/dist/search-index.json +232 -5
- package/dist/velin-agent.json +433 -46
- package/dist/velinstyle-components.iife.js +3695 -2272
- package/dist/velinstyle-components.js +3698 -2271
- package/dist/velinstyle-components.min.js +262 -120
- package/dist/velinstyle.css +96 -13
- package/dist/velinstyle.d.ts +3 -0
- package/dist/velinstyle.min.css +1 -1
- package/package.json +19 -4
- package/packages/velinstyle-cli-core/package.json +11 -0
- package/packages/velinstyle-cli-core/src/config.js +84 -0
- package/packages/velinstyle-cli-core/src/contract.js +56 -0
- package/packages/velinstyle-cli-core/src/event-bus.js +50 -0
- package/packages/velinstyle-cli-core/src/index.js +18 -0
- package/packages/velinstyle-cli-core/src/lifecycle.js +33 -0
- package/packages/velinstyle-cli-core/src/runner.js +129 -0
- package/packages/velinstyle-cli-registry/data/commands.json +1216 -0
- package/packages/velinstyle-cli-registry/data/layouts.json +33 -0
- package/packages/velinstyle-cli-registry/data/widgets.json +35 -0
- package/packages/velinstyle-cli-registry/package.json +13 -0
- package/packages/velinstyle-cli-registry/src/index.js +88 -0
- package/packages/velinstyle-cli-renderer/package.json +8 -0
- package/packages/velinstyle-cli-renderer/src/index.js +286 -0
- package/packages/velinstyle-cli-theme/package.json +8 -0
- package/packages/velinstyle-cli-theme/src/index.js +84 -0
- package/packages/velinstyle-cli-ui/package.json +8 -0
- package/packages/velinstyle-cli-ui/src/index.js +34 -0
- package/packages/velinstyle-motion/README.md +81 -0
- package/packages/velinstyle-motion/package.json +37 -0
- package/packages/velinstyle-motion/src/ai/mini-prompt.js +119 -0
- package/packages/velinstyle-motion/src/ai/provider.js +59 -0
- package/packages/velinstyle-motion/src/ai/providers/pixverse-setup.js +402 -0
- package/packages/velinstyle-motion/src/ai/providers/pixverse.js +245 -0
- package/packages/velinstyle-motion/src/config.js +257 -0
- package/packages/velinstyle-motion/src/export/html-clip.js +141 -0
- package/packages/velinstyle-motion/src/index.js +304 -0
- package/packages/velinstyle-motion/src/jobs/cache.js +30 -0
- package/packages/velinstyle-motion/src/jobs/queue.js +69 -0
- package/packages/velinstyle-motion/src/jobs/usage.js +53 -0
- package/packages/velinstyle-motion/src/local/effects.js +139 -0
- package/packages/velinstyle-motion/src/local/engine.js +96 -0
- package/packages/velinstyle-motion/src/providers-catalog.js +44 -0
- package/packages/velinstyle-motion/src/registry/presets.js +25 -0
- package/packages/velinstyle-motion/src/registry/presets.json +85 -0
- package/packages/velinstyle-motion/src/ux/cost-gate.js +37 -0
- package/packages/velinstyle-motion/src/ux/pixverse-gate.js +153 -0
- package/packages/velinstyle-motion/src/ux/wizard.js +171 -0
- package/packages/velinstyle-motion/src/validate-image.js +91 -0
- package/packages/velinstyle-skills/catalog.json +1 -1
- package/packages/velinstyle-skills/registry.json +1 -142
- package/packages/velinstyle-skills/skills/velin-motion-reduced-safe/SKILL.md +1 -1
- package/packages/velinstyle-skills/skills/velin-motion-reveal-feedback/SKILL.md +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/tokens/motion.css +7 -0
- package/src/utilities/scroll-animation.css +66 -0
- package/src/velinstyle.css +1 -0
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Motion Policies — Safe / Reduced / Marketing / …
|
|
3
|
+
*/
|
|
4
|
+
import { getPolicy, listPolicies } from './registry.js';
|
|
5
|
+
import { prefersReducedMotion } from './effects.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @param {string} [id='safe']
|
|
9
|
+
*/
|
|
10
|
+
export function resolvePolicy(id = 'safe') {
|
|
11
|
+
return getPolicy(id) || getPolicy('safe');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function listMotionPolicies() {
|
|
15
|
+
return listPolicies();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Whether motion should be skipped under policy + user preference.
|
|
20
|
+
* @param {object|null} policy
|
|
21
|
+
*/
|
|
22
|
+
export function shouldDisableMotion(policy) {
|
|
23
|
+
if (!policy) return prefersReducedMotion();
|
|
24
|
+
if (policy.disabled) return true;
|
|
25
|
+
if (policy.forceReduced && prefersReducedMotion()) return true;
|
|
26
|
+
if (prefersReducedMotion() && policy.id !== 'marketing') return true;
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Cap a duration against policy.
|
|
32
|
+
* @param {number} ms
|
|
33
|
+
* @param {object|null} policy
|
|
34
|
+
*/
|
|
35
|
+
export function capDuration(ms, policy) {
|
|
36
|
+
const max = policy?.maxDurationMs ?? 800;
|
|
37
|
+
if (policy?.disabled) return 0;
|
|
38
|
+
return Math.min(Number(ms) || 0, max);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Validate policy id exists.
|
|
43
|
+
*/
|
|
44
|
+
export function validatePolicy(id) {
|
|
45
|
+
const p = getPolicy(id);
|
|
46
|
+
if (!p) return { ok: false, error: `Unknown motion policy "${id}"` };
|
|
47
|
+
return { ok: true, policy: p };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Apply CSS variables for duration/ease from attributes + policy.
|
|
52
|
+
* @param {HTMLElement} el
|
|
53
|
+
* @param {object|null} policy
|
|
54
|
+
*/
|
|
55
|
+
export function applyMotionVars(el, policy) {
|
|
56
|
+
const duration = el.getAttribute('velin-duration');
|
|
57
|
+
const delay = el.getAttribute('velin-delay');
|
|
58
|
+
const ease = el.getAttribute('velin-ease');
|
|
59
|
+
if (duration) {
|
|
60
|
+
const capped = capDuration(Number(duration), policy);
|
|
61
|
+
el.style.setProperty('--velin-motion-duration', `${capped}ms`);
|
|
62
|
+
el.style.animationDuration = `${capped}ms`;
|
|
63
|
+
}
|
|
64
|
+
if (delay) el.style.animationDelay = `${Number(delay)}ms`;
|
|
65
|
+
if (ease) {
|
|
66
|
+
const map = {
|
|
67
|
+
spring: 'var(--velin-ease-spring)',
|
|
68
|
+
bounce: 'var(--velin-ease-bounce)',
|
|
69
|
+
elastic: 'var(--velin-ease-elastic)',
|
|
70
|
+
default: 'var(--velin-ease-default)',
|
|
71
|
+
};
|
|
72
|
+
el.style.animationTimingFunction = map[ease] || ease;
|
|
73
|
+
}
|
|
74
|
+
if (policy?.disabled || shouldDisableMotion(policy)) {
|
|
75
|
+
el.dataset.velinMotion = 'off';
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Velin Motion Registry — all building blocks (Studio-ready source of truth).
|
|
3
|
+
* Browser-safe: no fs — data imported as JSON module.
|
|
4
|
+
*/
|
|
5
|
+
import data from './data/registry.json' with { type: 'json' };
|
|
6
|
+
|
|
7
|
+
/** @type {object | null} */
|
|
8
|
+
let cached = data;
|
|
9
|
+
|
|
10
|
+
const NAMESPACES = [
|
|
11
|
+
'presets',
|
|
12
|
+
'timelines',
|
|
13
|
+
'transitions',
|
|
14
|
+
'physics',
|
|
15
|
+
'composers',
|
|
16
|
+
'triggers',
|
|
17
|
+
'policies',
|
|
18
|
+
'blueprints',
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @returns {object}
|
|
23
|
+
*/
|
|
24
|
+
export function loadMotionRegistry(override = null) {
|
|
25
|
+
if (override) {
|
|
26
|
+
cached = override;
|
|
27
|
+
return cached;
|
|
28
|
+
}
|
|
29
|
+
return cached || data;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function clearMotionRegistryCache() {
|
|
33
|
+
cached = data;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function listNamespaces() {
|
|
37
|
+
return [...NAMESPACES];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* @param {string} namespace
|
|
42
|
+
* @param {{ status?: string, category?: string, tag?: string }} [filter]
|
|
43
|
+
*/
|
|
44
|
+
export function listMotion(namespace, filter = {}) {
|
|
45
|
+
const reg = loadMotionRegistry();
|
|
46
|
+
const key = namespace.endsWith('s') ? namespace : `${namespace}s`;
|
|
47
|
+
const items = reg[key] || reg[namespace] || [];
|
|
48
|
+
return items.filter((item) => {
|
|
49
|
+
if (filter.status && item.status !== filter.status) return false;
|
|
50
|
+
if (filter.category && item.category !== filter.category) return false;
|
|
51
|
+
if (filter.tag && !(item.tags || []).includes(filter.tag)) return false;
|
|
52
|
+
return true;
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function getMotion(namespace, id) {
|
|
57
|
+
return listMotion(namespace).find((item) => item.id === id) || null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function getPreset(id) {
|
|
61
|
+
return getMotion('presets', id);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function getPolicy(id) {
|
|
65
|
+
return getMotion('policies', id);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function getTrigger(id) {
|
|
69
|
+
return getMotion('triggers', id);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function getTimeline(id) {
|
|
73
|
+
return getMotion('timelines', id);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function getBlueprint(id) {
|
|
77
|
+
return getMotion('blueprints', id);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function listPresets(filter) {
|
|
81
|
+
return listMotion('presets', filter);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function listPolicies(filter) {
|
|
85
|
+
return listMotion('policies', filter);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function listTriggers(filter) {
|
|
89
|
+
return listMotion('triggers', filter);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function listBlueprints(filter) {
|
|
93
|
+
return listMotion('blueprints', filter);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function listTimelines(filter) {
|
|
97
|
+
return listMotion('timelines', filter);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function catalogSummary() {
|
|
101
|
+
const reg = loadMotionRegistry();
|
|
102
|
+
const counts = {};
|
|
103
|
+
for (const ns of NAMESPACES) {
|
|
104
|
+
counts[ns] = (reg[ns] || []).length;
|
|
105
|
+
}
|
|
106
|
+
return {
|
|
107
|
+
schema: reg.schema,
|
|
108
|
+
version: reg.version,
|
|
109
|
+
counts,
|
|
110
|
+
namespaces: NAMESPACES,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function exportMotionRegistry() {
|
|
115
|
+
return structuredClone(loadMotionRegistry());
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function resolvePresetId(id) {
|
|
119
|
+
if (!id) return null;
|
|
120
|
+
const direct = getPreset(id);
|
|
121
|
+
if (direct) return direct;
|
|
122
|
+
const aliases = {
|
|
123
|
+
scale: 'zoom',
|
|
124
|
+
'hover-lift': 'lift',
|
|
125
|
+
reveal: 'fade',
|
|
126
|
+
};
|
|
127
|
+
const mapped = aliases[id];
|
|
128
|
+
return mapped ? getPreset(mapped) : null;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export { NAMESPACES };
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Review motion markup — counts, delays, reduced-motion, layout-shift, FPS hints.
|
|
3
|
+
*/
|
|
4
|
+
import { analyzeMotionHtml } from './analyze.js';
|
|
5
|
+
import { doctorMotionHtml } from './scan.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @param {string} html
|
|
9
|
+
*/
|
|
10
|
+
export function reviewMotionHtml(html) {
|
|
11
|
+
const analysis = analyzeMotionHtml(html);
|
|
12
|
+
const doctor = doctorMotionHtml(html);
|
|
13
|
+
const findings = [...doctor.findings];
|
|
14
|
+
|
|
15
|
+
if (analysis.metrics.animations > 40) {
|
|
16
|
+
findings.push({
|
|
17
|
+
severity: 'warning',
|
|
18
|
+
code: 'motion.too-many',
|
|
19
|
+
message: `${analysis.metrics.animations} motion elements — consider reducing`,
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
if (analysis.metrics.averageMs > 600) {
|
|
23
|
+
findings.push({
|
|
24
|
+
severity: 'warning',
|
|
25
|
+
code: 'motion.avg-slow',
|
|
26
|
+
message: `Average duration ${analysis.metrics.averageMs}ms is high`,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
if (analysis.metrics.layoutShiftRisk === 'HIGH') {
|
|
30
|
+
findings.push({
|
|
31
|
+
severity: 'error',
|
|
32
|
+
code: 'motion.cls-risk',
|
|
33
|
+
message: 'Layout shift risk HIGH (parallax/infinite combination)',
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
if (analysis.metrics.infinite > 0) {
|
|
37
|
+
findings.push({
|
|
38
|
+
severity: 'warning',
|
|
39
|
+
code: 'motion.fps-risk',
|
|
40
|
+
message: `${analysis.metrics.infinite} infinite animation(s) — FPS risk on low-end devices`,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const errors = findings.filter((f) => f.severity === 'error').length;
|
|
45
|
+
const warnings = findings.filter((f) => f.severity === 'warning').length;
|
|
46
|
+
const score = Math.max(0, 100 - errors * 25 - warnings * 8);
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
ok: errors === 0,
|
|
50
|
+
gate: errors ? 'fail' : warnings ? 'warn' : 'pass',
|
|
51
|
+
scores: {
|
|
52
|
+
motion: score,
|
|
53
|
+
accessibility: analysis.metrics.reducedMotion === 'PASS' ? 100 : 40,
|
|
54
|
+
performance: analysis.metrics.layoutShiftRisk === 'LOW' ? 90 : analysis.metrics.layoutShiftRisk === 'MEDIUM' ? 60 : 30,
|
|
55
|
+
},
|
|
56
|
+
metrics: analysis.metrics,
|
|
57
|
+
findings,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Collect motion-related nodes from HTML markup (string or DOM-less regex).
|
|
3
|
+
*/
|
|
4
|
+
const MOTION_ATTR_RE =
|
|
5
|
+
/\s(?:velin-motion|velin-reveal|velin-fade|velin-slide|velin-scale|velin-parallax|velin-hover|velin-stagger|velin-scroll|velin-trigger|velin-motion-sequence|data-velin-motion|data-velin-reveal-auto)\s*(?:=\s*["']([^"']*)["'])?/gi;
|
|
6
|
+
|
|
7
|
+
const CLASS_MOTION_RE =
|
|
8
|
+
/\b(?:velin-animate(?:-[\w-]+)?|velin-parallax|velin-skeleton|velin-in-view)\b/g;
|
|
9
|
+
|
|
10
|
+
const DURATION_RE = /velin-duration\s*=\s*["'](\d+)["']/gi;
|
|
11
|
+
const INFINITE_RE = /velin-animate[^\s"']*--infinite|animation:\s*[^;]*infinite/gi;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @param {string} html
|
|
15
|
+
*/
|
|
16
|
+
export function collectMotionElements(html) {
|
|
17
|
+
const text = String(html || '');
|
|
18
|
+
const elements = [];
|
|
19
|
+
const seen = new Set();
|
|
20
|
+
|
|
21
|
+
for (const m of text.matchAll(/<([a-z0-9-]+)([^>]*)>/gi)) {
|
|
22
|
+
const tag = m[1];
|
|
23
|
+
const attrs = m[2] || '';
|
|
24
|
+
if (!/velin-(?:motion|reveal|fade|slide|scale|parallax|hover|stagger|scroll|trigger)/i.test(attrs)
|
|
25
|
+
&& !/velin-animate|velin-parallax|velin-skeleton/i.test(attrs)) {
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
const preset = attrs.match(/velin-motion\s*=\s*["']([^"']+)["']/i)?.[1]
|
|
29
|
+
|| (attrs.includes('velin-fade') ? 'fade' : null)
|
|
30
|
+
|| (attrs.includes('velin-reveal') ? 'fade' : null)
|
|
31
|
+
|| (attrs.includes('velin-slide') ? 'slide' : null)
|
|
32
|
+
|| (attrs.includes('velin-scale') ? 'zoom' : null)
|
|
33
|
+
|| (attrs.includes('velin-hover') ? 'lift' : null)
|
|
34
|
+
|| (attrs.includes('velin-parallax') ? 'parallax' : null)
|
|
35
|
+
|| null;
|
|
36
|
+
const trigger = attrs.match(/velin-trigger\s*=\s*["']([^"']+)["']/i)?.[1] || 'visible';
|
|
37
|
+
const duration = Number(attrs.match(/velin-duration\s*=\s*["'](\d+)["']/i)?.[1] || 0);
|
|
38
|
+
const key = `${tag}:${preset}:${trigger}:${attrs.slice(0, 40)}`;
|
|
39
|
+
if (seen.has(key)) continue;
|
|
40
|
+
seen.add(key);
|
|
41
|
+
elements.push({
|
|
42
|
+
tag,
|
|
43
|
+
preset,
|
|
44
|
+
trigger,
|
|
45
|
+
duration,
|
|
46
|
+
hasReducedHint: /prefers-reduced-motion|data-velin-motion\s*=\s*["']off["']/i.test(attrs),
|
|
47
|
+
infinite: /--infinite|infinite/i.test(attrs),
|
|
48
|
+
attrs: attrs.trim().slice(0, 120),
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const classHits = [...text.matchAll(CLASS_MOTION_RE)].length;
|
|
53
|
+
return { elements, classHits, rawLength: text.length };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* @param {string} html
|
|
58
|
+
* @param {{ policyId?: string }} [opts]
|
|
59
|
+
*/
|
|
60
|
+
export function scanMotionHtml(html, opts = {}) {
|
|
61
|
+
const { elements, classHits } = collectMotionElements(html);
|
|
62
|
+
const presets = new Map();
|
|
63
|
+
const triggers = new Map();
|
|
64
|
+
let infinite = 0;
|
|
65
|
+
let longCount = 0;
|
|
66
|
+
const findings = [];
|
|
67
|
+
|
|
68
|
+
for (const el of elements) {
|
|
69
|
+
if (el.preset) presets.set(el.preset, (presets.get(el.preset) || 0) + 1);
|
|
70
|
+
if (el.trigger) triggers.set(el.trigger, (triggers.get(el.trigger) || 0) + 1);
|
|
71
|
+
if (el.infinite) infinite += 1;
|
|
72
|
+
if (el.duration > 800) {
|
|
73
|
+
longCount += 1;
|
|
74
|
+
findings.push({
|
|
75
|
+
severity: 'warning',
|
|
76
|
+
code: 'motion.long-duration',
|
|
77
|
+
message: `Duration ${el.duration}ms exceeds 800ms soft cap`,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (infinite) {
|
|
83
|
+
findings.push({
|
|
84
|
+
severity: 'warning',
|
|
85
|
+
code: 'motion.infinite',
|
|
86
|
+
message: `${infinite} infinite animation hint(s)`,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const dupPresets = [...presets.entries()].filter(([, n]) => n > 8);
|
|
91
|
+
for (const [id, n] of dupPresets) {
|
|
92
|
+
findings.push({
|
|
93
|
+
severity: 'info',
|
|
94
|
+
code: 'motion.duplicate-preset',
|
|
95
|
+
message: `Preset "${id}" used ${n} times`,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
ok: !findings.some((f) => f.severity === 'error'),
|
|
101
|
+
summary: {
|
|
102
|
+
elements: elements.length,
|
|
103
|
+
classHits,
|
|
104
|
+
presets: presets.size,
|
|
105
|
+
triggers: triggers.size,
|
|
106
|
+
infinite,
|
|
107
|
+
longCount,
|
|
108
|
+
policy: opts.policyId || 'safe',
|
|
109
|
+
},
|
|
110
|
+
presets: Object.fromEntries(presets),
|
|
111
|
+
triggers: Object.fromEntries(triggers),
|
|
112
|
+
findings,
|
|
113
|
+
elements: elements.slice(0, 100),
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* @param {string} html
|
|
119
|
+
*/
|
|
120
|
+
export function doctorMotionHtml(html) {
|
|
121
|
+
const scan = scanMotionHtml(html);
|
|
122
|
+
const findings = [...scan.findings];
|
|
123
|
+
|
|
124
|
+
if (!/prefers-reduced-motion|data-velin-motion|reduced-motion/i.test(html) && scan.summary.elements > 0) {
|
|
125
|
+
findings.push({
|
|
126
|
+
severity: 'warning',
|
|
127
|
+
code: 'motion.missing-reduced',
|
|
128
|
+
message: 'No reduced-motion / data-velin-motion=off hints found near motion usage',
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const long = scan.elements.filter((e) => e.duration > 1200);
|
|
133
|
+
for (const el of long) {
|
|
134
|
+
findings.push({
|
|
135
|
+
severity: 'error',
|
|
136
|
+
code: 'motion.excessive-duration',
|
|
137
|
+
message: `Duration ${el.duration}ms is excessive`,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const errors = findings.filter((f) => f.severity === 'error').length;
|
|
142
|
+
const warnings = findings.filter((f) => f.severity === 'warning').length;
|
|
143
|
+
|
|
144
|
+
return {
|
|
145
|
+
ok: errors === 0,
|
|
146
|
+
summary: { ...scan.summary, errors, warnings },
|
|
147
|
+
findings,
|
|
148
|
+
scan,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Timeline Engine — Scene → Timeline → Steps (Registry-backed).
|
|
3
|
+
*/
|
|
4
|
+
import { getTimeline } from './registry.js';
|
|
5
|
+
import { markVisible, prefersReducedMotion } from './effects.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Run a registered timeline against a root element.
|
|
9
|
+
* Steps resolve `[data-velin-step="id"]` or `#id` or `[velin-step="id"]`.
|
|
10
|
+
* @param {string} timelineId
|
|
11
|
+
* @param {ParentNode} root
|
|
12
|
+
* @param {{ stepMs?: number, policyDisabled?: boolean }} [opts]
|
|
13
|
+
* @returns {{ ok: boolean, error?: string, ran: string[] }}
|
|
14
|
+
*/
|
|
15
|
+
export function runTimeline(timelineId, root, opts = {}) {
|
|
16
|
+
const tl = getTimeline(timelineId);
|
|
17
|
+
if (!tl) return { ok: false, error: `Unknown timeline "${timelineId}"`, ran: [] };
|
|
18
|
+
const stepMs = opts.stepMs ?? 120;
|
|
19
|
+
const ran = [];
|
|
20
|
+
|
|
21
|
+
if (opts.policyDisabled || prefersReducedMotion()) {
|
|
22
|
+
for (const step of tl.steps || []) {
|
|
23
|
+
const el = resolveStep(root, step);
|
|
24
|
+
if (el) markVisible(el);
|
|
25
|
+
ran.push(step);
|
|
26
|
+
}
|
|
27
|
+
return { ok: true, ran };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
let delay = 0;
|
|
31
|
+
for (const step of tl.steps || []) {
|
|
32
|
+
const el = resolveStep(root, step);
|
|
33
|
+
if (!el) continue;
|
|
34
|
+
const d = delay;
|
|
35
|
+
delay += stepMs;
|
|
36
|
+
if (typeof window !== 'undefined' && window.setTimeout) {
|
|
37
|
+
window.setTimeout(() => markVisible(el), d);
|
|
38
|
+
} else {
|
|
39
|
+
markVisible(el);
|
|
40
|
+
}
|
|
41
|
+
ran.push(step);
|
|
42
|
+
}
|
|
43
|
+
return { ok: true, ran, timeline: tl };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function resolveStep(root, step) {
|
|
47
|
+
if (!root?.querySelector) return null;
|
|
48
|
+
return (
|
|
49
|
+
root.querySelector(`[data-velin-step="${step}"]`)
|
|
50
|
+
|| root.querySelector(`[velin-step="${step}"]`)
|
|
51
|
+
|| root.querySelector(`#${CSS.escape?.(step) || step}`)
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Enhance `[velin-timeline="id"]` roots.
|
|
57
|
+
*/
|
|
58
|
+
export function enhanceTimelines(root) {
|
|
59
|
+
if (!root?.querySelectorAll) return () => {};
|
|
60
|
+
const nodes = root.querySelectorAll('[velin-timeline]');
|
|
61
|
+
const teardowns = [];
|
|
62
|
+
for (const el of nodes) {
|
|
63
|
+
const id = el.getAttribute('velin-timeline');
|
|
64
|
+
const result = runTimeline(id, el);
|
|
65
|
+
teardowns.push(() => result);
|
|
66
|
+
}
|
|
67
|
+
return () => {};
|
|
68
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transition helpers (View Transition + overlay hooks).
|
|
3
|
+
*/
|
|
4
|
+
import { getMotion } from './registry.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Run a document view transition if supported.
|
|
8
|
+
* @param {() => void|Promise<void>} updateCallback
|
|
9
|
+
* @param {{ name?: string }} [opts]
|
|
10
|
+
*/
|
|
11
|
+
export async function runViewTransition(updateCallback, opts = {}) {
|
|
12
|
+
const doc = typeof document !== 'undefined' ? document : null;
|
|
13
|
+
if (!doc) {
|
|
14
|
+
await updateCallback();
|
|
15
|
+
return { ok: true, mode: 'direct' };
|
|
16
|
+
}
|
|
17
|
+
if (typeof doc.startViewTransition === 'function') {
|
|
18
|
+
const vt = doc.startViewTransition(async () => {
|
|
19
|
+
if (opts.name && doc.documentElement) {
|
|
20
|
+
doc.documentElement.style.viewTransitionName = opts.name;
|
|
21
|
+
}
|
|
22
|
+
await updateCallback();
|
|
23
|
+
});
|
|
24
|
+
await vt.finished.catch(() => {});
|
|
25
|
+
return { ok: true, mode: 'view-transition', id: opts.name || null };
|
|
26
|
+
}
|
|
27
|
+
await updateCallback();
|
|
28
|
+
return { ok: true, mode: 'fallback' };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function getTransitionPreset(id) {
|
|
32
|
+
return getMotion('transitions', id);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Apply transition class hints for modal/drawer hosts.
|
|
37
|
+
* @param {HTMLElement} el
|
|
38
|
+
* @param {'modal-enter'|'drawer-enter'|'cross-fade'} id
|
|
39
|
+
*/
|
|
40
|
+
export function applyTransitionHint(el, id) {
|
|
41
|
+
if (!el) return;
|
|
42
|
+
const map = {
|
|
43
|
+
'modal-enter': 'velin-animate-fade-in',
|
|
44
|
+
'drawer-enter': 'velin-animate-on-scroll--slide-right',
|
|
45
|
+
'cross-fade': 'velin-animate-fade-in',
|
|
46
|
+
};
|
|
47
|
+
const cls = map[id];
|
|
48
|
+
if (cls) el.classList.add(cls);
|
|
49
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Motion Trigger Engine — when motion starts (First-Class, Studio-ready).
|
|
3
|
+
*/
|
|
4
|
+
import { observeInView } from './scheduler.js';
|
|
5
|
+
import { markVisible, prefersReducedMotion } from './effects.js';
|
|
6
|
+
import { getTrigger } from './registry.js';
|
|
7
|
+
|
|
8
|
+
export const TRIGGER_V1 = ['load', 'visible', 'hover', 'focus', 'click', 'scroll'];
|
|
9
|
+
export const TRIGGER_V2 = ['hidden', 'idle', 'media', 'container', 'theme', 'custom'];
|
|
10
|
+
export const ALL_TRIGGERS = [...TRIGGER_V1, ...TRIGGER_V2];
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Read trigger id from element (velin-trigger or default).
|
|
14
|
+
* @param {HTMLElement} el
|
|
15
|
+
* @param {string} [fallback='visible']
|
|
16
|
+
*/
|
|
17
|
+
export function readTrigger(el, fallback = 'visible') {
|
|
18
|
+
const raw = (el.getAttribute('velin-trigger') || fallback || 'visible').trim().toLowerCase();
|
|
19
|
+
return raw || fallback;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Bind a trigger to run `onFire` once (or per event).
|
|
24
|
+
* @param {HTMLElement} el
|
|
25
|
+
* @param {string} triggerId
|
|
26
|
+
* @param {() => void} onFire
|
|
27
|
+
* @param {{ once?: boolean, reduced?: boolean }} [opts]
|
|
28
|
+
* @returns {() => void} teardown
|
|
29
|
+
*/
|
|
30
|
+
export function bindTrigger(el, triggerId, onFire, opts = {}) {
|
|
31
|
+
const once = opts.once !== false;
|
|
32
|
+
const reduced = opts.reduced ?? prefersReducedMotion();
|
|
33
|
+
const id = (triggerId || 'visible').toLowerCase();
|
|
34
|
+
const meta = getTrigger(id);
|
|
35
|
+
|
|
36
|
+
if (reduced || el.dataset.velinMotion === 'off') {
|
|
37
|
+
onFire();
|
|
38
|
+
return () => {};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (meta?.status === 'planned' && !TRIGGER_V1.includes(id)) {
|
|
42
|
+
// Planned V2 triggers are not first-class yet: fall back to visible for runtime safety
|
|
43
|
+
return bindTrigger(el, 'visible', onFire, opts);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
let fired = false;
|
|
47
|
+
const fire = () => {
|
|
48
|
+
if (once && fired) return;
|
|
49
|
+
fired = true;
|
|
50
|
+
onFire();
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
if (id === 'load') {
|
|
54
|
+
fire();
|
|
55
|
+
return () => {};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (id === 'visible' || id === 'scroll') {
|
|
59
|
+
return observeInView(el, () => fire());
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (id === 'hover') {
|
|
63
|
+
const enter = () => fire();
|
|
64
|
+
el.addEventListener('pointerenter', enter);
|
|
65
|
+
return () => el.removeEventListener('pointerenter', enter);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (id === 'focus') {
|
|
69
|
+
const onFocus = () => fire();
|
|
70
|
+
el.addEventListener('focusin', onFocus);
|
|
71
|
+
return () => el.removeEventListener('focusin', onFocus);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (id === 'click') {
|
|
75
|
+
const onClick = () => fire();
|
|
76
|
+
el.addEventListener('click', onClick);
|
|
77
|
+
return () => el.removeEventListener('click', onClick);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (id === 'hidden') {
|
|
81
|
+
const stop = observeInView(el, () => {
|
|
82
|
+
/* enter */
|
|
83
|
+
});
|
|
84
|
+
// Fallback: use IntersectionObserver with leave
|
|
85
|
+
if (typeof IntersectionObserver === 'undefined') return () => {};
|
|
86
|
+
const io = new IntersectionObserver((entries) => {
|
|
87
|
+
for (const entry of entries) {
|
|
88
|
+
if (!entry.isIntersecting) fire();
|
|
89
|
+
}
|
|
90
|
+
}, { threshold: 0 });
|
|
91
|
+
io.observe(el);
|
|
92
|
+
return () => {
|
|
93
|
+
stop();
|
|
94
|
+
io.disconnect();
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (id === 'idle') {
|
|
99
|
+
const ms = Number(el.getAttribute('velin-trigger-delay') || 3000);
|
|
100
|
+
const t = setTimeout(fire, ms);
|
|
101
|
+
return () => clearTimeout(t);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (id === 'media') {
|
|
105
|
+
const q = el.getAttribute('velin-trigger-media') || '(min-width: 768px)';
|
|
106
|
+
if (typeof window === 'undefined' || !window.matchMedia) {
|
|
107
|
+
fire();
|
|
108
|
+
return () => {};
|
|
109
|
+
}
|
|
110
|
+
const mql = window.matchMedia(q);
|
|
111
|
+
const handler = () => {
|
|
112
|
+
if (mql.matches) fire();
|
|
113
|
+
};
|
|
114
|
+
handler();
|
|
115
|
+
mql.addEventListener?.('change', handler);
|
|
116
|
+
return () => mql.removeEventListener?.('change', handler);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (id === 'theme') {
|
|
120
|
+
const root = el.ownerDocument?.documentElement;
|
|
121
|
+
if (!root || typeof MutationObserver === 'undefined') {
|
|
122
|
+
fire();
|
|
123
|
+
return () => {};
|
|
124
|
+
}
|
|
125
|
+
const mo = new MutationObserver(() => fire());
|
|
126
|
+
mo.observe(root, { attributes: true, attributeFilter: ['data-velin-theme', 'class'] });
|
|
127
|
+
return () => mo.disconnect();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (id === 'custom') {
|
|
131
|
+
const evt = el.getAttribute('velin-trigger-event') || 'velin:motion';
|
|
132
|
+
const handler = () => fire();
|
|
133
|
+
el.addEventListener(evt, handler);
|
|
134
|
+
return () => el.removeEventListener(evt, handler);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (id === 'container') {
|
|
138
|
+
// Approximate with visible until container queries runtime ships
|
|
139
|
+
return observeInView(el, () => fire());
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Unknown → visible
|
|
143
|
+
return observeInView(el, () => fire());
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Apply motion classes then bind trigger to markVisible.
|
|
148
|
+
* @param {HTMLElement} el
|
|
149
|
+
* @param {() => void} applyPreset
|
|
150
|
+
*/
|
|
151
|
+
export function armMotionElement(el, applyPreset) {
|
|
152
|
+
applyPreset();
|
|
153
|
+
const trigger = readTrigger(el, el.hasAttribute('velin-hover') ? 'hover' : 'visible');
|
|
154
|
+
if (prefersReducedMotion() || el.dataset.velinMotion === 'off') {
|
|
155
|
+
markVisible(el);
|
|
156
|
+
return () => {};
|
|
157
|
+
}
|
|
158
|
+
if (trigger === 'hover' || trigger === 'focus') {
|
|
159
|
+
// CSS hover classes already apply; still mark visible for scroll classes
|
|
160
|
+
markVisible(el);
|
|
161
|
+
return () => {};
|
|
162
|
+
}
|
|
163
|
+
if (trigger === 'load') {
|
|
164
|
+
markVisible(el);
|
|
165
|
+
return () => {};
|
|
166
|
+
}
|
|
167
|
+
return bindTrigger(el, trigger, () => markVisible(el));
|
|
168
|
+
}
|