chaincss 2.1.38 → 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.
@@ -0,0 +1,402 @@
1
+ // src/compiler/intent-engine.ts
2
+
3
+ import type { CorrectionResult, HealMode, HealResult, IntentContext } from '../core/types.js';
4
+ import { detectIfPatterns, emitCSSIf } from './css-if-transpiler.js';
5
+ export type { CorrectionResult, HealMode, HealResult, IntentContext };
6
+
7
+ interface ValueCorrection { wrong: string; correct: string; confidence: number; }
8
+
9
+ const SEMANTIC_INTENTS: Array<{pattern: RegExp; handler: Function; description: string}> = [
10
+ { pattern: /^flexbox$/i, handler: (v: string, ctx: any) => ({ original: v, property: ctx.property||'display', corrected: 'flex', defaults: { display: 'flex', justifyContent: 'center', alignItems: 'center' }, confidence: 0.95, intent: 'flexbox-centering', explanation: '"flexbox" mapped to display: flex with centering defaults.' }), description: 'flexbox -> flex + centering' },
11
+ { pattern: /^(absolutely|abs)$/i, handler: (v: string, ctx: any) => ({ original: v, property: ctx.property||'position', corrected: 'absolute', defaults: { position: 'absolute' }, confidence: 0.9, intent: 'absolute-position', explanation: '"abs/absolutely" -> position: absolute' }), description: 'abs -> absolute' },
12
+ { pattern: /^(rel|relatively)$/i, handler: (v: string, ctx: any) => ({ original: v, property: ctx.property||'position', corrected: 'relative', defaults: { position: 'relative' }, confidence: 0.9, intent: 'relative-position', explanation: '"rel/relatively" -> position: relative' }), description: 'rel -> relative' },
13
+ { pattern: /^(hidden|invisible)$/i, handler: (v: string, ctx: any) => ({ original: v, property: ctx.property||'visibility', corrected: v.toLowerCase()==='invisible'?'hidden':v.toLowerCase(), defaults: { visibility: 'hidden' }, confidence: 0.9, intent: 'visibility-toggle', explanation: '"' + v + '" -> visibility: hidden' }), description: 'invisible -> hidden' },
14
+ { pattern: /^(full|fullscreen|full-screen)$/i, handler: (v: string, ctx: any) => ({ original: v, property: ctx.property||'size', corrected: '100%', defaults: { width: '100%', height: '100%' }, confidence: 0.85, intent: 'full-size', explanation: '"full/fullscreen" -> width/height: 100%' }), description: 'full -> 100%' },
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' },
16
+ ];
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
+
274
+ const VALUE_CORRECTIONS: Record<string, ValueCorrection[]> = {
275
+ 'display': [{wrong:'flexbox',correct:'flex',confidence:0.95},{wrong:'inline-flexbox',correct:'inline-flex',confidence:0.95}],
276
+ 'position': [{wrong:'abs',correct:'absolute',confidence:0.9},{wrong:'rel',correct:'relative',confidence:0.9}],
277
+ 'text-align': [{wrong:'centered',correct:'center',confidence:0.85},{wrong:'justified',correct:'justify',confidence:0.85}],
278
+ 'overflow': [{wrong:'scrollable',correct:'auto',confidence:0.8}],
279
+ 'cursor': [{wrong:'hand',correct:'pointer',confidence:0.9}],
280
+ 'user-select': [{wrong:'unselectable',correct:'none',confidence:0.85}],
281
+ };
282
+
283
+ const KNOWN_PROPERTIES = ['display','position','color','background','width','height','font-size','text-align','cursor','opacity','z-index','overflow','visibility','flex','flex-direction','justify-content','align-items','gap','grid','transition','transform','animation','box-shadow','pointer-events','user-select','line-height'];
284
+
285
+ function levenshtein(a: string, b: string): number {
286
+ const m: number[][] = [];
287
+ for (let i = 0; i <= b.length; i++) m[i] = [i];
288
+ for (let j = 0; j <= a.length; j++) m[0][j] = j;
289
+ for (let i = 1; i <= b.length; i++)
290
+ for (let j = 1; j <= a.length; j++)
291
+ m[i][j] = Math.min(m[i-1][j]+1, m[i][j-1]+1, m[i-1][j-1]+(a[j-1]===b[i-1]?0:1));
292
+ return m[b.length][a.length];
293
+ }
294
+
295
+ function findClosestProperty(prop: string): string | null {
296
+ const lp = prop.toLowerCase();
297
+ let best: string | null = null, bestDist = Infinity;
298
+ for (const k of KNOWN_PROPERTIES) { const d = levenshtein(lp, k); if (d < bestDist && d <= 3) { bestDist = d; best = k; } }
299
+ return best;
300
+ }
301
+
302
+ function detectIntent(value: string, ctx: IntentContext = {}): CorrectionResult | null {
303
+ const lv = value.toLowerCase();
304
+ for (const rule of SEMANTIC_INTENTS) {
305
+ if (rule.pattern.test(lv)) {
306
+ const r = rule.handler(value, ctx);
307
+ if (r) return r;
308
+ }
309
+ }
310
+ return null;
311
+ }
312
+
313
+ export const intent = {
314
+ correct(property: string, value: string, context?: IntentContext): CorrectionResult | null {
315
+ const ctx = { property, value, ...context };
316
+ const si = detectIntent(value, ctx);
317
+ if (si) return si;
318
+ if (VALUE_CORRECTIONS[property]) {
319
+ const c = VALUE_CORRECTIONS[property].find(c => c.wrong === value.toLowerCase());
320
+ if (c) return { original: value, property, corrected: c.correct, defaults: { [property]: c.correct }, confidence: c.confidence, intent: 'value-correction', explanation: `"${value}" is not valid for ${property}. Did you mean "${c.correct}"?` };
321
+ }
322
+ const pc = findClosestProperty(property);
323
+ if (pc && pc !== property.toLowerCase()) {
324
+ const d = levenshtein(property.toLowerCase(), pc);
325
+ return { original: property, property, corrected: pc, defaults: {}, confidence: Math.max(0, 1 - d / Math.max(property.length, pc.length)), intent: 'property-correction', explanation: `Unknown property "${property}". Did you mean "${pc}"?` };
326
+ }
327
+ return null;
328
+ },
329
+ heal(styles: Record<string, any>, mode: HealMode = 'smart', context?: IntentContext): HealResult {
330
+ const corrections: CorrectionResult[] = [], warnings: string[] = [], fixed: Record<string, any> = {};
331
+ for (const [prop, value] of Object.entries(styles)) {
332
+ if (prop.startsWith('_') || prop === 'selectors') { fixed[prop] = value; continue; }
333
+ if (typeof value === 'object' && value !== null && prop === 'hover') {
334
+ const hr = this.heal(value as Record<string, any>, mode, { ...context, property: prop });
335
+ fixed[prop] = hr.fixed; corrections.push(...hr.corrections); warnings.push(...hr.warnings); continue;
336
+ }
337
+ if (typeof value !== 'string' && typeof value !== 'number') { fixed[prop] = value; continue; }
338
+ const sv = String(value), corr = this.correct(prop, sv, { ...context, property: prop, value: sv });
339
+ if (corr) {
340
+ corrections.push(corr);
341
+ if (mode === 'strict') { warnings.push('[strict] ' + corr.explanation); fixed[prop] = sv; }
342
+ else if (mode === 'dev') { fixed[prop] = corr.corrected; Object.assign(fixed, corr.defaults); }
343
+ else { warnings.push('[auto-fix] ' + corr.explanation); fixed[prop] = corr.corrected; Object.assign(fixed, corr.defaults); }
344
+ } else { fixed[prop] = value; }
345
+ }
346
+ return { fixed, corrections, warnings, mode };
347
+ },
348
+ getIntent(value: string, ctx?: IntentContext): string | null { const r = detectIntent(value, ctx); return r?.intent || null; },
349
+ validate(property: string, value: string): { valid: boolean; suggestion?: string } {
350
+ if (VALUE_CORRECTIONS[property]) {
351
+ const c = VALUE_CORRECTIONS[property].find(c => c.wrong === value.toLowerCase());
352
+ if (c) return c.confidence < 1 ? { valid: false, suggestion: c.correct } : { valid: true };
353
+ }
354
+ if (!KNOWN_PROPERTIES.includes(property.toLowerCase())) {
355
+ const s = findClosestProperty(property);
356
+ return s ? { valid: false, suggestion: s } : { valid: false };
357
+ }
358
+ return { valid: true };
359
+ },
360
+ getCorrections(property: string): ValueCorrection[] { return VALUE_CORRECTIONS[property] || []; },
361
+ explain(correction: CorrectionResult): string { return correction.explanation; },
362
+ cssIf: { detect: detectIfPatterns, emit: emitCSSIf },
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
+ },
392
+ };
393
+
394
+ export const correct = intent.correct.bind(intent);
395
+ export const heal = intent.heal.bind(intent);
396
+ export const validate = intent.validate.bind(intent);
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);
402
+ export default intent;