chaincss 2.1.39 → 2.2.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/dist/compiler/css-if-transpiler.d.ts +33 -0
- package/dist/compiler/design-orchestrator.d.ts +119 -0
- package/dist/compiler/intent-engine.d.ts +19 -1
- package/dist/compiler/scroll-timeline.d.ts +91 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +651 -2
- package/package.json +1 -1
- package/src/compiler/css-if-transpiler.ts +117 -0
- package/src/compiler/design-orchestrator.ts +322 -0
- package/src/compiler/intent-engine.ts +291 -1
- package/src/compiler/scroll-timeline.ts +284 -0
- package/src/index.ts +34 -0
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
// src/compiler/intent-engine.ts
|
|
2
|
+
|
|
2
3
|
import type { CorrectionResult, HealMode, HealResult, IntentContext } from '../core/types.js';
|
|
4
|
+
import { detectIfPatterns, emitCSSIf } from './css-if-transpiler.js';
|
|
3
5
|
export type { CorrectionResult, HealMode, HealResult, IntentContext };
|
|
4
6
|
|
|
5
7
|
interface ValueCorrection { wrong: string; correct: string; confidence: number; }
|
|
@@ -13,6 +15,262 @@ const SEMANTIC_INTENTS: Array<{pattern: RegExp; handler: Function; description:
|
|
|
13
15
|
{ pattern: /^(rounded|round)$/i, handler: (v: string, ctx: any) => ({ original: v, property: ctx.property||'border-radius', corrected: '9999px', defaults: { borderRadius: '9999px' }, confidence: 0.8, intent: 'rounded-pill', explanation: '"rounded" -> border-radius: 9999px (pill)' }), description: 'rounded -> pill' },
|
|
14
16
|
];
|
|
15
17
|
|
|
18
|
+
|
|
19
|
+
// ============================================================================
|
|
20
|
+
// Layout Macros — High-level semantic intents
|
|
21
|
+
// These compile complex multi-property layouts from simple intent names
|
|
22
|
+
// ============================================================================
|
|
23
|
+
|
|
24
|
+
interface LayoutMacro {
|
|
25
|
+
name: string;
|
|
26
|
+
description: string;
|
|
27
|
+
properties: Record<string, string | number>;
|
|
28
|
+
defaults?: Record<string, string | number>;
|
|
29
|
+
mediaQueries?: Record<string, Record<string, any>>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const LAYOUT_MACROS: Record<string, LayoutMacro> = {
|
|
33
|
+
stickyHeader: {
|
|
34
|
+
name: 'stickyHeader',
|
|
35
|
+
description: 'Sticky header with scroll shadow and entrance animation',
|
|
36
|
+
properties: {
|
|
37
|
+
position: 'sticky',
|
|
38
|
+
top: '0',
|
|
39
|
+
zIndex: '50',
|
|
40
|
+
backgroundColor: 'var(--header-bg, white)',
|
|
41
|
+
backdropFilter: 'blur(8px)',
|
|
42
|
+
borderBottom: '1px solid transparent',
|
|
43
|
+
},
|
|
44
|
+
defaults: {
|
|
45
|
+
'--header-bg': 'white',
|
|
46
|
+
'--header-shadow': '0 4px 12px rgba(0,0,0,0.1)',
|
|
47
|
+
},
|
|
48
|
+
mediaQueries: {
|
|
49
|
+
'(max-width: 768px)': {
|
|
50
|
+
padding: '12px 16px',
|
|
51
|
+
},
|
|
52
|
+
'(min-width: 769px)': {
|
|
53
|
+
padding: '16px 32px',
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
|
|
58
|
+
card: {
|
|
59
|
+
name: 'card',
|
|
60
|
+
description: 'Standard card container with hover lift effect',
|
|
61
|
+
properties: {
|
|
62
|
+
display: 'flex',
|
|
63
|
+
flexDirection: 'column',
|
|
64
|
+
borderRadius: '12px',
|
|
65
|
+
backgroundColor: 'var(--card-bg, white)',
|
|
66
|
+
boxShadow: '0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.08)',
|
|
67
|
+
transition: 'box-shadow 0.2s ease, transform 0.2s ease',
|
|
68
|
+
overflow: 'hidden',
|
|
69
|
+
},
|
|
70
|
+
defaults: {
|
|
71
|
+
'--card-bg': 'white',
|
|
72
|
+
'--card-hover-shadow': '0 10px 30px rgba(0,0,0,0.15)',
|
|
73
|
+
},
|
|
74
|
+
mediaQueries: {
|
|
75
|
+
'(hover: hover)': {
|
|
76
|
+
'&:hover': {
|
|
77
|
+
boxShadow: 'var(--card-hover-shadow)',
|
|
78
|
+
transform: 'translateY(-2px)',
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
hero: {
|
|
85
|
+
name: 'hero',
|
|
86
|
+
description: 'Full-width hero section with centered content',
|
|
87
|
+
properties: {
|
|
88
|
+
display: 'flex',
|
|
89
|
+
flexDirection: 'column',
|
|
90
|
+
justifyContent: 'center',
|
|
91
|
+
alignItems: 'center',
|
|
92
|
+
width: '100%',
|
|
93
|
+
minHeight: '60vh',
|
|
94
|
+
padding: '48px 24px',
|
|
95
|
+
textAlign: 'center',
|
|
96
|
+
},
|
|
97
|
+
defaults: {},
|
|
98
|
+
mediaQueries: {
|
|
99
|
+
'(max-width: 768px)': {
|
|
100
|
+
minHeight: '40vh',
|
|
101
|
+
padding: '32px 16px',
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
|
|
106
|
+
container: {
|
|
107
|
+
name: 'container',
|
|
108
|
+
description: 'Responsive centered container with max-width',
|
|
109
|
+
properties: {
|
|
110
|
+
width: '100%',
|
|
111
|
+
maxWidth: '1200px',
|
|
112
|
+
marginLeft: 'auto',
|
|
113
|
+
marginRight: 'auto',
|
|
114
|
+
paddingLeft: '16px',
|
|
115
|
+
paddingRight: '16px',
|
|
116
|
+
},
|
|
117
|
+
defaults: {},
|
|
118
|
+
mediaQueries: {
|
|
119
|
+
'(min-width: 768px)': {
|
|
120
|
+
paddingLeft: '24px',
|
|
121
|
+
paddingRight: '24px',
|
|
122
|
+
},
|
|
123
|
+
'(min-width: 1024px)': {
|
|
124
|
+
paddingLeft: '32px',
|
|
125
|
+
paddingRight: '32px',
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
|
|
130
|
+
center: {
|
|
131
|
+
name: 'center',
|
|
132
|
+
description: 'Absolute centering using flexbox',
|
|
133
|
+
properties: {
|
|
134
|
+
display: 'flex',
|
|
135
|
+
justifyContent: 'center',
|
|
136
|
+
alignItems: 'center',
|
|
137
|
+
},
|
|
138
|
+
defaults: {},
|
|
139
|
+
},
|
|
140
|
+
|
|
141
|
+
gridList: {
|
|
142
|
+
name: 'gridList',
|
|
143
|
+
description: 'Responsive grid list with auto-fit columns',
|
|
144
|
+
properties: {
|
|
145
|
+
display: 'grid',
|
|
146
|
+
gridTemplateColumns: 'repeat(auto-fit, minmax(280px, 1fr))',
|
|
147
|
+
gap: '24px',
|
|
148
|
+
},
|
|
149
|
+
defaults: {},
|
|
150
|
+
mediaQueries: {
|
|
151
|
+
'(max-width: 640px)': {
|
|
152
|
+
gridTemplateColumns: '1fr',
|
|
153
|
+
gap: '16px',
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
|
|
158
|
+
sidebar: {
|
|
159
|
+
name: 'sidebar',
|
|
160
|
+
description: 'Two-column layout: sidebar + main content',
|
|
161
|
+
properties: {
|
|
162
|
+
display: 'grid',
|
|
163
|
+
gridTemplateColumns: '280px 1fr',
|
|
164
|
+
gap: '32px',
|
|
165
|
+
minHeight: '100vh',
|
|
166
|
+
},
|
|
167
|
+
defaults: {},
|
|
168
|
+
mediaQueries: {
|
|
169
|
+
'(max-width: 1024px)': {
|
|
170
|
+
gridTemplateColumns: '1fr',
|
|
171
|
+
gap: '24px',
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
|
|
176
|
+
pill: {
|
|
177
|
+
name: 'pill',
|
|
178
|
+
description: 'Pill-shaped element (fully rounded)',
|
|
179
|
+
properties: {
|
|
180
|
+
borderRadius: '9999px',
|
|
181
|
+
padding: '8px 20px',
|
|
182
|
+
display: 'inline-flex',
|
|
183
|
+
alignItems: 'center',
|
|
184
|
+
justifyContent: 'center',
|
|
185
|
+
},
|
|
186
|
+
defaults: {},
|
|
187
|
+
},
|
|
188
|
+
|
|
189
|
+
glass: {
|
|
190
|
+
name: 'glass',
|
|
191
|
+
description: 'Frosted glass morphism effect',
|
|
192
|
+
properties: {
|
|
193
|
+
backgroundColor: 'rgba(255, 255, 255, 0.1)',
|
|
194
|
+
backdropFilter: 'blur(16px)',
|
|
195
|
+
border: '1px solid rgba(255, 255, 255, 0.2)',
|
|
196
|
+
borderRadius: '16px',
|
|
197
|
+
},
|
|
198
|
+
defaults: {},
|
|
199
|
+
},
|
|
200
|
+
|
|
201
|
+
truncate: {
|
|
202
|
+
name: 'truncate',
|
|
203
|
+
description: 'Single-line text truncation with ellipsis',
|
|
204
|
+
properties: {
|
|
205
|
+
overflow: 'hidden',
|
|
206
|
+
textOverflow: 'ellipsis',
|
|
207
|
+
whiteSpace: 'nowrap',
|
|
208
|
+
},
|
|
209
|
+
defaults: {},
|
|
210
|
+
},
|
|
211
|
+
|
|
212
|
+
srOnly: {
|
|
213
|
+
name: 'srOnly',
|
|
214
|
+
description: 'Screen-reader only (visually hidden but accessible)',
|
|
215
|
+
properties: {
|
|
216
|
+
position: 'absolute',
|
|
217
|
+
width: '1px',
|
|
218
|
+
height: '1px',
|
|
219
|
+
padding: '0',
|
|
220
|
+
margin: '-1px',
|
|
221
|
+
overflow: 'hidden',
|
|
222
|
+
clip: 'rect(0, 0, 0, 0)',
|
|
223
|
+
whiteSpace: 'nowrap',
|
|
224
|
+
borderWidth: '0',
|
|
225
|
+
},
|
|
226
|
+
defaults: {},
|
|
227
|
+
},
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
// ============================================================================
|
|
231
|
+
// Layout Macro Resolver
|
|
232
|
+
// ============================================================================
|
|
233
|
+
|
|
234
|
+
function resolveLayoutMacro(name: string): LayoutMacro | null {
|
|
235
|
+
return LAYOUT_MACROS[name] || null;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function expandLayoutMacro(name: string): Record<string, any> | null {
|
|
239
|
+
const macro = resolveLayoutMacro(name);
|
|
240
|
+
if (!macro) return null;
|
|
241
|
+
|
|
242
|
+
// Start with properties
|
|
243
|
+
const result: Record<string, any> = { ...macro.properties };
|
|
244
|
+
|
|
245
|
+
// Merge defaults (CSS custom properties) into the result
|
|
246
|
+
if (macro.defaults) {
|
|
247
|
+
Object.assign(result, macro.defaults);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Apply media queries as nested atRules
|
|
251
|
+
if (macro.mediaQueries) {
|
|
252
|
+
result.atRules = result.atRules || [];
|
|
253
|
+
for (const [query, props] of Object.entries(macro.mediaQueries)) {
|
|
254
|
+
result.atRules.push({
|
|
255
|
+
type: 'media',
|
|
256
|
+
query,
|
|
257
|
+
styles: props,
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return result;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function getAvailableMacros(): string[] {
|
|
266
|
+
return Object.keys(LAYOUT_MACROS);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function getMacroDescription(name: string): string | null {
|
|
270
|
+
const macro = resolveLayoutMacro(name);
|
|
271
|
+
return macro?.description || null;
|
|
272
|
+
}
|
|
273
|
+
|
|
16
274
|
const VALUE_CORRECTIONS: Record<string, ValueCorrection[]> = {
|
|
17
275
|
'display': [{wrong:'flexbox',correct:'flex',confidence:0.95},{wrong:'inline-flexbox',correct:'inline-flex',confidence:0.95}],
|
|
18
276
|
'position': [{wrong:'abs',correct:'absolute',confidence:0.9},{wrong:'rel',correct:'relative',confidence:0.9}],
|
|
@@ -101,12 +359,44 @@ export const intent = {
|
|
|
101
359
|
},
|
|
102
360
|
getCorrections(property: string): ValueCorrection[] { return VALUE_CORRECTIONS[property] || []; },
|
|
103
361
|
explain(correction: CorrectionResult): string { return correction.explanation; },
|
|
104
|
-
|
|
362
|
+
cssIf: { detect: detectIfPatterns, emit: emitCSSIf },
|
|
105
363
|
getIntents() { return SEMANTIC_INTENTS.map(r => ({ pattern: r.pattern.toString(), description: r.description })); },
|
|
364
|
+
getKnownProperties(): string[] { return [...KNOWN_PROPERTIES]; },
|
|
365
|
+
|
|
366
|
+
// Layout Macros
|
|
367
|
+
macro(name: string): Record<string, any> | null { return expandLayoutMacro(name); },
|
|
368
|
+
getMacros(): string[] { return getAvailableMacros(); },
|
|
369
|
+
getMacroDescription(name: string): string | null { return getMacroDescription(name); },
|
|
370
|
+
hasMacro(name: string): boolean { return name in LAYOUT_MACROS; },
|
|
371
|
+
/**
|
|
372
|
+
* Apply a layout macro to an existing styles object.
|
|
373
|
+
* Merges macro properties with user overrides.
|
|
374
|
+
*/
|
|
375
|
+
applyMacro(name: string, overrides?: Record<string, any>): Record<string, any> | null {
|
|
376
|
+
const macro = expandLayoutMacro(name);
|
|
377
|
+
if (!macro) return null;
|
|
378
|
+
if (!overrides) return macro;
|
|
379
|
+
// Deep merge: user overrides win
|
|
380
|
+
const merged = { ...macro };
|
|
381
|
+
for (const [key, value] of Object.entries(overrides)) {
|
|
382
|
+
if (key === 'atRules' && Array.isArray(value) && Array.isArray(merged.atRules)) {
|
|
383
|
+
merged.atRules = [...merged.atRules, ...value];
|
|
384
|
+
} else if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
|
385
|
+
merged[key] = { ...(merged[key] || {}), ...value };
|
|
386
|
+
} else {
|
|
387
|
+
merged[key] = value;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
return merged;
|
|
391
|
+
},
|
|
106
392
|
};
|
|
107
393
|
|
|
108
394
|
export const correct = intent.correct.bind(intent);
|
|
109
395
|
export const heal = intent.heal.bind(intent);
|
|
110
396
|
export const validate = intent.validate.bind(intent);
|
|
111
397
|
export const getIntent = intent.getIntent.bind(intent);
|
|
398
|
+
export const macro = intent.macro.bind(intent);
|
|
399
|
+
export const applyMacro = intent.applyMacro.bind(intent);
|
|
400
|
+
export const getMacros = intent.getMacros.bind(intent);
|
|
401
|
+
export const hasMacro = intent.hasMacro.bind(intent);
|
|
112
402
|
export default intent;
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
// src/compiler/scroll-timeline.ts
|
|
2
|
+
/**
|
|
3
|
+
* Scroll-Driven Animations Engine
|
|
4
|
+
*
|
|
5
|
+
* Compiles timeline-based animation descriptions into:
|
|
6
|
+
* 1. Native CSS scroll-timeline / view-timeline (Chromium 115+)
|
|
7
|
+
* 2. @supports fallback with JavaScript polyfill hint
|
|
8
|
+
*
|
|
9
|
+
* API inspiration: GSAP's ScrollTrigger, but compiles to 0kb JS.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
// ============================================================================
|
|
13
|
+
// Types
|
|
14
|
+
// ============================================================================
|
|
15
|
+
|
|
16
|
+
export interface ScrollTimelineConfig {
|
|
17
|
+
/** Name of the timeline */
|
|
18
|
+
name: string;
|
|
19
|
+
/** What drives the timeline */
|
|
20
|
+
source: 'scroll' | 'view';
|
|
21
|
+
/** The scrollable element (default: nearest scrollable ancestor) */
|
|
22
|
+
scroller?: 'nearest' | 'root' | 'self' | string;
|
|
23
|
+
/** Scroll axis */
|
|
24
|
+
axis?: 'block' | 'inline' | 'x' | 'y';
|
|
25
|
+
/** For view timelines: when does the element enter/exit */
|
|
26
|
+
inset?: string | { start: string; end: string };
|
|
27
|
+
/** Timeline range (for view timelines) */
|
|
28
|
+
range?: 'cover' | 'contain' | 'entry' | 'exit' | 'entry-crossing' | 'exit-crossing' | string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface KeyframeStep {
|
|
32
|
+
/** Percentage or keyword (e.g., '0%', 'from', 'to') */
|
|
33
|
+
offset: string;
|
|
34
|
+
/** CSS properties at this keyframe */
|
|
35
|
+
properties: Record<string, string | number>;
|
|
36
|
+
/** Easing for this segment */
|
|
37
|
+
easing?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface ScrollAnimation {
|
|
41
|
+
/** Selector to animate */
|
|
42
|
+
selector: string;
|
|
43
|
+
/** Timeline configuration */
|
|
44
|
+
timeline: ScrollTimelineConfig;
|
|
45
|
+
/** Keyframes */
|
|
46
|
+
keyframes: KeyframeStep[];
|
|
47
|
+
/** Animation duration (maps to timeline range) */
|
|
48
|
+
duration?: string;
|
|
49
|
+
/** Fill mode */
|
|
50
|
+
fill?: 'none' | 'forwards' | 'backwards' | 'both';
|
|
51
|
+
/** Iteration count */
|
|
52
|
+
iterations?: number | 'infinite';
|
|
53
|
+
/** Delay before starting */
|
|
54
|
+
delay?: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface ScrollTimelineResult {
|
|
58
|
+
/** Native CSS for scroll-driven animation */
|
|
59
|
+
css: string;
|
|
60
|
+
/** The @keyframes name generated */
|
|
61
|
+
animationName: string;
|
|
62
|
+
/** The timeline name generated */
|
|
63
|
+
timelineName: string;
|
|
64
|
+
/** Fallback CSS for browsers without scroll-timeline */
|
|
65
|
+
fallback: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ============================================================================
|
|
69
|
+
// Presets — common scroll animations
|
|
70
|
+
// ============================================================================
|
|
71
|
+
|
|
72
|
+
export const SCROLL_PRESETS: Record<string, ScrollAnimation> = {
|
|
73
|
+
fadeIn: {
|
|
74
|
+
selector: '',
|
|
75
|
+
timeline: { name: 'fade-in', source: 'view', range: 'entry' },
|
|
76
|
+
keyframes: [
|
|
77
|
+
{ offset: '0%', properties: { opacity: '0', transform: 'translateY(20px)' } },
|
|
78
|
+
{ offset: '100%', properties: { opacity: '1', transform: 'translateY(0)' } },
|
|
79
|
+
],
|
|
80
|
+
},
|
|
81
|
+
fadeOut: {
|
|
82
|
+
selector: '',
|
|
83
|
+
timeline: { name: 'fade-out', source: 'view', range: 'exit' },
|
|
84
|
+
keyframes: [
|
|
85
|
+
{ offset: '0%', properties: { opacity: '1' } },
|
|
86
|
+
{ offset: '100%', properties: { opacity: '0' } },
|
|
87
|
+
],
|
|
88
|
+
},
|
|
89
|
+
scaleIn: {
|
|
90
|
+
selector: '',
|
|
91
|
+
timeline: { name: 'scale-in', source: 'view', range: 'entry' },
|
|
92
|
+
keyframes: [
|
|
93
|
+
{ offset: '0%', properties: { opacity: '0', transform: 'scale(0.8)' } },
|
|
94
|
+
{ offset: '100%', properties: { opacity: '1', transform: 'scale(1)' } },
|
|
95
|
+
],
|
|
96
|
+
},
|
|
97
|
+
slideLeft: {
|
|
98
|
+
selector: '',
|
|
99
|
+
timeline: { name: 'slide-left', source: 'view', range: 'entry' },
|
|
100
|
+
keyframes: [
|
|
101
|
+
{ offset: '0%', properties: { opacity: '0', transform: 'translateX(-40px)' } },
|
|
102
|
+
{ offset: '100%', properties: { opacity: '1', transform: 'translateX(0)' } },
|
|
103
|
+
],
|
|
104
|
+
},
|
|
105
|
+
slideRight: {
|
|
106
|
+
selector: '',
|
|
107
|
+
timeline: { name: 'slide-right', source: 'view', range: 'entry' },
|
|
108
|
+
keyframes: [
|
|
109
|
+
{ offset: '0%', properties: { opacity: '0', transform: 'translateX(40px)' } },
|
|
110
|
+
{ offset: '100%', properties: { opacity: '1', transform: 'translateX(0)' } },
|
|
111
|
+
],
|
|
112
|
+
},
|
|
113
|
+
parallax: {
|
|
114
|
+
selector: '',
|
|
115
|
+
timeline: { name: 'parallax', source: 'scroll', scroller: 'root' },
|
|
116
|
+
keyframes: [
|
|
117
|
+
{ offset: '0%', properties: { transform: 'translateY(0)' } },
|
|
118
|
+
{ offset: '100%', properties: { transform: 'translateY(-20%)' } },
|
|
119
|
+
],
|
|
120
|
+
},
|
|
121
|
+
stickyReveal: {
|
|
122
|
+
selector: '',
|
|
123
|
+
timeline: { name: 'sticky-reveal', source: 'view', range: 'contain' },
|
|
124
|
+
keyframes: [
|
|
125
|
+
{ offset: '0%', properties: { opacity: '0', clipPath: 'inset(0 0 100% 0)' } },
|
|
126
|
+
{ offset: '50%', properties: { opacity: '1', clipPath: 'inset(0 0 0% 0)' } },
|
|
127
|
+
{ offset: '100%', properties: { opacity: '1', clipPath: 'inset(0 0 0% 0)' } },
|
|
128
|
+
],
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
// ============================================================================
|
|
133
|
+
// Core Compiler
|
|
134
|
+
// ============================================================================
|
|
135
|
+
|
|
136
|
+
let animCounter = 0;
|
|
137
|
+
|
|
138
|
+
function generateName(prefix: string): string {
|
|
139
|
+
return prefix + '-' + (animCounter++).toString(36);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Compile a scroll animation into CSS.
|
|
144
|
+
*
|
|
145
|
+
* Output includes:
|
|
146
|
+
* - @keyframes definition
|
|
147
|
+
* - animation-timeline property
|
|
148
|
+
* - animation-range (for view timelines)
|
|
149
|
+
* - @supports fallback for older browsers
|
|
150
|
+
*/
|
|
151
|
+
export function compileScrollAnimation(animation: ScrollAnimation): ScrollTimelineResult {
|
|
152
|
+
const animName = animation.timeline.name || generateName('scroll-anim');
|
|
153
|
+
const timelineName = '--' + animName + '-tl';
|
|
154
|
+
|
|
155
|
+
let css = '';
|
|
156
|
+
|
|
157
|
+
// 1. Define the scroll timeline
|
|
158
|
+
css += '/* Scroll Timeline: ' + animName + ' */\n';
|
|
159
|
+
|
|
160
|
+
if (animation.timeline.source === 'view') {
|
|
161
|
+
// view-timeline
|
|
162
|
+
const range = animation.timeline.range || 'entry';
|
|
163
|
+
css += animation.selector + ' {\n';
|
|
164
|
+
css += ' view-timeline-name: ' + timelineName + ';\n';
|
|
165
|
+
css += ' view-timeline-axis: ' + (animation.timeline.axis || 'block') + ';\n';
|
|
166
|
+
if (animation.timeline.inset) {
|
|
167
|
+
const inset = typeof animation.timeline.inset === 'string'
|
|
168
|
+
? animation.timeline.inset
|
|
169
|
+
: animation.timeline.inset.start + ' ' + animation.timeline.inset.end;
|
|
170
|
+
css += ' view-timeline-inset: ' + inset + ';\n';
|
|
171
|
+
}
|
|
172
|
+
css += '}\n\n';
|
|
173
|
+
} else {
|
|
174
|
+
// scroll-timeline
|
|
175
|
+
const scroller = animation.timeline.scroller === 'root' ? 'root'
|
|
176
|
+
: animation.timeline.scroller === 'self' ? 'self'
|
|
177
|
+
: animation.timeline.scroller || 'nearest';
|
|
178
|
+
css += animation.selector + ' {\n';
|
|
179
|
+
if (scroller === 'root' || scroller === 'self' || scroller === 'nearest') {
|
|
180
|
+
css += ' scroll-timeline-name: ' + timelineName + ';\n';
|
|
181
|
+
css += ' scroll-timeline-axis: ' + (animation.timeline.axis || 'block') + ';\n';
|
|
182
|
+
}
|
|
183
|
+
css += '}\n\n';
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// 2. Define @keyframes
|
|
187
|
+
css += '@keyframes ' + animName + ' {\n';
|
|
188
|
+
for (const step of animation.keyframes) {
|
|
189
|
+
css += ' ' + step.offset + ' {\n';
|
|
190
|
+
for (const [prop, value] of Object.entries(step.properties)) {
|
|
191
|
+
const kebabProp = prop.replace(/([A-Z])/g, '-$1').toLowerCase();
|
|
192
|
+
css += ' ' + kebabProp + ': ' + value + ';\n';
|
|
193
|
+
}
|
|
194
|
+
css += ' }\n';
|
|
195
|
+
}
|
|
196
|
+
css += '}\n\n';
|
|
197
|
+
|
|
198
|
+
// 3. Apply animation to target
|
|
199
|
+
const targetSelector = animation.selector + ' > *' || animation.selector;
|
|
200
|
+
css += '/* Apply animation to children */\n';
|
|
201
|
+
css += targetSelector + ' {\n';
|
|
202
|
+
css += ' animation: ' + animName + ' linear both;\n';
|
|
203
|
+
css += ' animation-timeline: ' + timelineName + ';\n';
|
|
204
|
+
if (animation.timeline.source === 'view') {
|
|
205
|
+
const range = animation.timeline.range || 'entry';
|
|
206
|
+
css += ' animation-range: ' + range + ';\n';
|
|
207
|
+
}
|
|
208
|
+
if (animation.delay) css += ' animation-delay: ' + animation.delay + ';\n';
|
|
209
|
+
css += '}\n\n';
|
|
210
|
+
|
|
211
|
+
// 4. @supports fallback
|
|
212
|
+
css += '/* Fallback for browsers without scroll-timeline */\n';
|
|
213
|
+
css += '@supports not (animation-timeline: scroll()) {\n';
|
|
214
|
+
css += ' ' + targetSelector + ' {\n';
|
|
215
|
+
css += ' /* Use JS polyfill: https://github.com/flackr/scroll-timeline */\n';
|
|
216
|
+
css += ' animation: ' + animName + ' 1s ease both;\n';
|
|
217
|
+
css += ' }\n';
|
|
218
|
+
css += '}\n';
|
|
219
|
+
|
|
220
|
+
return {
|
|
221
|
+
css,
|
|
222
|
+
animationName: animName,
|
|
223
|
+
timelineName,
|
|
224
|
+
fallback: '',
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Compile multiple scroll animations for a page.
|
|
230
|
+
*/
|
|
231
|
+
export function compileScrollAnimations(animations: ScrollAnimation[]): string {
|
|
232
|
+
let css = '/* ============================================================\n';
|
|
233
|
+
css += ' ChainCSS Scroll-Driven Animations\n';
|
|
234
|
+
css += ' Generated: ' + new Date().toISOString() + '\n';
|
|
235
|
+
css += ' ============================================================ */\n\n';
|
|
236
|
+
|
|
237
|
+
for (const animation of animations) {
|
|
238
|
+
const result = compileScrollAnimation(animation);
|
|
239
|
+
css += result.css;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return css;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Create a scroll animation from a preset.
|
|
247
|
+
*/
|
|
248
|
+
export function createScrollAnimation(
|
|
249
|
+
preset: keyof typeof SCROLL_PRESETS,
|
|
250
|
+
selector: string,
|
|
251
|
+
overrides?: Partial<ScrollAnimation>
|
|
252
|
+
): ScrollAnimation {
|
|
253
|
+
const base = SCROLL_PRESETS[preset];
|
|
254
|
+
if (!base) throw new Error('Unknown scroll preset: ' + preset);
|
|
255
|
+
|
|
256
|
+
return {
|
|
257
|
+
...base,
|
|
258
|
+
selector,
|
|
259
|
+
timeline: { ...base.timeline, ...overrides?.timeline },
|
|
260
|
+
keyframes: overrides?.keyframes || base.keyframes,
|
|
261
|
+
...overrides,
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Get available scroll animation presets.
|
|
267
|
+
*/
|
|
268
|
+
export function getScrollPresets(): string[] {
|
|
269
|
+
return Object.keys(SCROLL_PRESETS);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// ============================================================================
|
|
273
|
+
// Exports
|
|
274
|
+
// ============================================================================
|
|
275
|
+
|
|
276
|
+
export const scrollTimeline = {
|
|
277
|
+
compile: compileScrollAnimation,
|
|
278
|
+
compileAll: compileScrollAnimations,
|
|
279
|
+
create: createScrollAnimation,
|
|
280
|
+
presets: SCROLL_PRESETS,
|
|
281
|
+
getPresets: getScrollPresets,
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
export default scrollTimeline;
|
package/src/index.ts
CHANGED
|
@@ -162,6 +162,40 @@ export type { BreakpointsMap, ResponsiveStyle } from './compiler/breakpoints.js'
|
|
|
162
162
|
// ============================================================================
|
|
163
163
|
export const VERSION = '3.0.0';
|
|
164
164
|
|
|
165
|
+
// ============================================================================
|
|
166
|
+
// Default Export - Keep original chain for backward compatibility
|
|
167
|
+
// ============================================================================
|
|
168
|
+
// ============================================================================
|
|
169
|
+
// 🆕 Design System Orchestrator (v2.3)
|
|
170
|
+
// ============================================================================
|
|
171
|
+
export {
|
|
172
|
+
orchestrator,
|
|
173
|
+
contrastRatio,
|
|
174
|
+
checkContrast,
|
|
175
|
+
auditContrast,
|
|
176
|
+
createContextualToken,
|
|
177
|
+
resolveContextual,
|
|
178
|
+
generateContextualCSS,
|
|
179
|
+
validateTokenRelationships,
|
|
180
|
+
} from './compiler/design-orchestrator.js';
|
|
181
|
+
export type { ContrastResult, ContrastReport, ContextualToken, TokenContext } from './compiler/design-orchestrator.js';
|
|
182
|
+
|
|
183
|
+
// ============================================================================
|
|
184
|
+
// Default Export - Keep original chain for backward compatibility
|
|
185
|
+
// ============================================================================
|
|
186
|
+
// ============================================================================
|
|
187
|
+
// 🆕 Scroll Timeline Engine (v2.3)
|
|
188
|
+
// ============================================================================
|
|
189
|
+
export {
|
|
190
|
+
scrollTimeline,
|
|
191
|
+
compileScrollAnimation,
|
|
192
|
+
compileScrollAnimations,
|
|
193
|
+
createScrollAnimation,
|
|
194
|
+
getScrollPresets,
|
|
195
|
+
SCROLL_PRESETS,
|
|
196
|
+
} from './compiler/scroll-timeline.js';
|
|
197
|
+
export type { ScrollTimelineConfig, ScrollAnimation, ScrollTimelineResult, KeyframeStep } from './compiler/scroll-timeline.js';
|
|
198
|
+
|
|
165
199
|
// ============================================================================
|
|
166
200
|
// Default Export - Keep original chain for backward compatibility
|
|
167
201
|
// ============================================================================
|