chaincss 2.1.39 → 2.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/dist/compiler/accessibility-engine.d.ts +57 -0
- package/dist/compiler/constraint-solver.d.ts +85 -0
- package/dist/compiler/css-if-transpiler.d.ts +33 -0
- package/dist/compiler/design-orchestrator.d.ts +119 -0
- package/dist/compiler/intent-api.d.ts +73 -0
- package/dist/compiler/intent-engine.d.ts +19 -1
- package/dist/compiler/layout-intelligence.d.ts +71 -0
- package/dist/compiler/pass-manager.d.ts +157 -0
- package/dist/compiler/pattern-learner.d.ts +112 -0
- package/dist/compiler/responsive-inference.d.ts +63 -0
- package/dist/compiler/scroll-timeline.d.ts +91 -0
- package/dist/compiler/semantic-tokens.d.ts +57 -0
- package/dist/compiler/source-optimizer.d.ts +109 -0
- package/dist/compiler/style-ir.d.ts +183 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.js +4126 -2
- package/package.json +1 -1
- package/src/compiler/accessibility-engine.ts +502 -0
- package/src/compiler/constraint-solver.ts +407 -0
- package/src/compiler/css-if-transpiler.ts +117 -0
- package/src/compiler/design-orchestrator.ts +322 -0
- package/src/compiler/intent-api.ts +505 -0
- package/src/compiler/intent-engine.ts +291 -1
- package/src/compiler/layout-intelligence.ts +697 -0
- package/src/compiler/pass-manager.ts +657 -0
- package/src/compiler/pattern-learner.ts +398 -0
- package/src/compiler/responsive-inference.ts +415 -0
- package/src/compiler/scroll-timeline.ts +284 -0
- package/src/compiler/semantic-tokens.ts +468 -0
- package/src/compiler/source-optimizer.ts +541 -0
- package/src/compiler/style-ir.ts +495 -0
- package/src/index.ts +209 -0
- package/ROADMAP.md +0 -31
|
@@ -0,0 +1,697 @@
|
|
|
1
|
+
// src/compiler/layout-intelligence.ts
|
|
2
|
+
/**
|
|
3
|
+
* Layout Intelligence Engine
|
|
4
|
+
*
|
|
5
|
+
* Bidirectional pattern recognition:
|
|
6
|
+
* 1. EXPAND: Human shorthand → full CSS (via existing macros)
|
|
7
|
+
* 2. RECOGNIZE: Full CSS → detected pattern (NEW — this module)
|
|
8
|
+
* 3. COMPRESS: Duplicate patterns → shared intent suggestion
|
|
9
|
+
* 4. SUGGEST: Diagnostic when verbose CSS could be a macro
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { StyleIR, IRRule, IRDeclaration, IRPass } from './style-ir.js';
|
|
13
|
+
|
|
14
|
+
// ============================================================================
|
|
15
|
+
// Types
|
|
16
|
+
// ============================================================================
|
|
17
|
+
|
|
18
|
+
export interface LayoutPattern {
|
|
19
|
+
/** Unique name of the pattern */
|
|
20
|
+
name: string;
|
|
21
|
+
/** Human-readable description */
|
|
22
|
+
description: string;
|
|
23
|
+
/** The macro call that generates this pattern */
|
|
24
|
+
macro: string;
|
|
25
|
+
/** Example usage */
|
|
26
|
+
example: string;
|
|
27
|
+
/** Properties that must match exactly */
|
|
28
|
+
required: Record<string, string | number>;
|
|
29
|
+
/** Properties that should be present but can have any value */
|
|
30
|
+
optional?: string[];
|
|
31
|
+
/** Minimum number of required matches to trigger recognition */
|
|
32
|
+
minMatches?: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface PatternMatch {
|
|
36
|
+
pattern: LayoutPattern;
|
|
37
|
+
ruleId: string;
|
|
38
|
+
selector: string;
|
|
39
|
+
matchedProperties: string[];
|
|
40
|
+
confidence: number; // 0-1
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface PatternReport {
|
|
44
|
+
matches: PatternMatch[];
|
|
45
|
+
duplicates: Array<{ pattern: string; selectors: string[]; count: number }>;
|
|
46
|
+
suggestions: Array<{ selector: string; suggestion: string; savings: number }>;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ============================================================================
|
|
50
|
+
// Pattern Database
|
|
51
|
+
// ============================================================================
|
|
52
|
+
|
|
53
|
+
const LAYOUT_PATTERNS: LayoutPattern[] = [
|
|
54
|
+
// --- Flexbox Patterns ---
|
|
55
|
+
{
|
|
56
|
+
name: 'stack-center',
|
|
57
|
+
description: 'Vertical stack with centered items',
|
|
58
|
+
macro: "stack('vertical center')",
|
|
59
|
+
example: "chain.stack('vertical center')",
|
|
60
|
+
required: {
|
|
61
|
+
display: 'flex',
|
|
62
|
+
flexDirection: 'column',
|
|
63
|
+
justifyContent: 'center',
|
|
64
|
+
alignItems: 'center',
|
|
65
|
+
},
|
|
66
|
+
optional: ['gap', 'padding'],
|
|
67
|
+
minMatches: 4,
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
name: 'stack-horizontal',
|
|
71
|
+
description: 'Horizontal stack with centered items',
|
|
72
|
+
macro: "stack('horizontal center')",
|
|
73
|
+
example: "chain.stack('horizontal center')",
|
|
74
|
+
required: {
|
|
75
|
+
display: 'flex',
|
|
76
|
+
flexDirection: 'row',
|
|
77
|
+
justifyContent: 'center',
|
|
78
|
+
alignItems: 'center',
|
|
79
|
+
},
|
|
80
|
+
optional: ['gap'],
|
|
81
|
+
minMatches: 4,
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
name: 'flex-center',
|
|
85
|
+
description: 'Flexbox absolute centering',
|
|
86
|
+
macro: 'center()',
|
|
87
|
+
example: 'chain.center()',
|
|
88
|
+
required: {
|
|
89
|
+
display: 'flex',
|
|
90
|
+
justifyContent: 'center',
|
|
91
|
+
alignItems: 'center',
|
|
92
|
+
},
|
|
93
|
+
minMatches: 3,
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
name: 'flex-between',
|
|
97
|
+
description: 'Flexbox space-between alignment',
|
|
98
|
+
macro: "stack('between')",
|
|
99
|
+
example: "chain.stack('between')",
|
|
100
|
+
required: {
|
|
101
|
+
display: 'flex',
|
|
102
|
+
justifyContent: 'space-between',
|
|
103
|
+
alignItems: 'center',
|
|
104
|
+
},
|
|
105
|
+
minMatches: 3,
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
name: 'flex-row-wrap',
|
|
109
|
+
description: 'Flex row with wrapping',
|
|
110
|
+
macro: "chain.flex().flexDir('row').flexWrap('wrap')",
|
|
111
|
+
example: "chain.flex().flexDir('row').flexWrap('wrap')",
|
|
112
|
+
required: {
|
|
113
|
+
display: 'flex',
|
|
114
|
+
flexDirection: 'row',
|
|
115
|
+
flexWrap: 'wrap',
|
|
116
|
+
},
|
|
117
|
+
minMatches: 3,
|
|
118
|
+
},
|
|
119
|
+
|
|
120
|
+
// --- Grid Patterns ---
|
|
121
|
+
{
|
|
122
|
+
name: 'grid-center',
|
|
123
|
+
description: 'Grid with centered items',
|
|
124
|
+
macro: 'gridCenter()',
|
|
125
|
+
example: 'chain.gridCenter()',
|
|
126
|
+
required: {
|
|
127
|
+
display: 'grid',
|
|
128
|
+
placeItems: 'center',
|
|
129
|
+
},
|
|
130
|
+
minMatches: 2,
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
name: 'grid-auto-fit',
|
|
134
|
+
description: 'Responsive auto-fit grid',
|
|
135
|
+
macro: "gridList()",
|
|
136
|
+
example: 'chain.gridList()',
|
|
137
|
+
required: {
|
|
138
|
+
display: 'grid',
|
|
139
|
+
},
|
|
140
|
+
optional: ['gridTemplateColumns', 'gap'],
|
|
141
|
+
minMatches: 1, // Lower because gridTemplateColumns varies
|
|
142
|
+
},
|
|
143
|
+
|
|
144
|
+
// --- Positioning Patterns ---
|
|
145
|
+
{
|
|
146
|
+
name: 'absolute-center',
|
|
147
|
+
description: 'Absolute positioning centering',
|
|
148
|
+
macro: "absolute({ top: '50%', left: '50%' })",
|
|
149
|
+
example: "chain.absolute({ top: '50%', left: '50%' }).transform('translate(-50%, -50%)')",
|
|
150
|
+
required: {
|
|
151
|
+
position: 'absolute',
|
|
152
|
+
top: '50%',
|
|
153
|
+
left: '50%',
|
|
154
|
+
},
|
|
155
|
+
optional: ['transform'],
|
|
156
|
+
minMatches: 3,
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
name: 'sticky-top',
|
|
160
|
+
description: 'Sticky element at top',
|
|
161
|
+
macro: 'stickyHeader()',
|
|
162
|
+
example: 'chain.stickyHeader()', // from intent macros
|
|
163
|
+
required: {
|
|
164
|
+
position: 'sticky',
|
|
165
|
+
top: '0',
|
|
166
|
+
},
|
|
167
|
+
optional: ['zIndex', 'backgroundColor', 'backdropFilter'],
|
|
168
|
+
minMatches: 2,
|
|
169
|
+
},
|
|
170
|
+
|
|
171
|
+
// --- Sizing Patterns ---
|
|
172
|
+
{
|
|
173
|
+
name: 'full-size',
|
|
174
|
+
description: 'Full width and height',
|
|
175
|
+
macro: "chain.size('100%')",
|
|
176
|
+
example: "chain.size('100%')",
|
|
177
|
+
required: {
|
|
178
|
+
width: '100%',
|
|
179
|
+
height: '100%',
|
|
180
|
+
},
|
|
181
|
+
minMatches: 2,
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
name: 'pill-shape',
|
|
185
|
+
description: 'Fully rounded pill element',
|
|
186
|
+
macro: 'pill()',
|
|
187
|
+
example: 'chain.pill()',
|
|
188
|
+
required: {
|
|
189
|
+
borderRadius: '9999px',
|
|
190
|
+
},
|
|
191
|
+
optional: ['padding', 'display'],
|
|
192
|
+
minMatches: 1,
|
|
193
|
+
},
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
// --- Intent Macros (from intent-engine.ts) ---
|
|
197
|
+
{
|
|
198
|
+
name: 'sticky-header',
|
|
199
|
+
description: 'Sticky header with backdrop blur',
|
|
200
|
+
macro: 'stickyHeader()',
|
|
201
|
+
example: 'chain.stickyHeader()',
|
|
202
|
+
required: {
|
|
203
|
+
position: 'sticky',
|
|
204
|
+
top: '0',
|
|
205
|
+
},
|
|
206
|
+
optional: ['zIndex', 'backgroundColor', 'backdropFilter', 'borderBottom', 'padding'],
|
|
207
|
+
minMatches: 2,
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
name: 'card-layout',
|
|
211
|
+
description: 'Card container with shadow and hover lift',
|
|
212
|
+
macro: 'card()',
|
|
213
|
+
example: 'chain.card()',
|
|
214
|
+
required: {
|
|
215
|
+
borderRadius: '12px',
|
|
216
|
+
overflow: 'hidden',
|
|
217
|
+
},
|
|
218
|
+
optional: ['display', 'flexDirection', 'backgroundColor', 'boxShadow', 'transition'],
|
|
219
|
+
minMatches: 2,
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
name: 'hero-section',
|
|
223
|
+
description: 'Full-width centered hero',
|
|
224
|
+
macro: 'hero()',
|
|
225
|
+
example: 'chain.hero()',
|
|
226
|
+
required: {
|
|
227
|
+
display: 'flex',
|
|
228
|
+
flexDirection: 'column',
|
|
229
|
+
justifyContent: 'center',
|
|
230
|
+
alignItems: 'center',
|
|
231
|
+
width: '100%',
|
|
232
|
+
},
|
|
233
|
+
optional: ['minHeight', 'padding', 'textAlign'],
|
|
234
|
+
minMatches: 4,
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
name: 'container-layout',
|
|
238
|
+
description: 'Centered max-width container',
|
|
239
|
+
macro: 'container()',
|
|
240
|
+
example: 'chain.container()',
|
|
241
|
+
required: {
|
|
242
|
+
marginLeft: 'auto',
|
|
243
|
+
marginRight: 'auto',
|
|
244
|
+
},
|
|
245
|
+
optional: ['width', 'maxWidth', 'paddingLeft', 'paddingRight'],
|
|
246
|
+
minMatches: 2,
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
name: 'sidebar-layout',
|
|
250
|
+
description: 'Sidebar + main content grid',
|
|
251
|
+
macro: 'sidebar()',
|
|
252
|
+
example: 'chain.sidebar()',
|
|
253
|
+
required: {
|
|
254
|
+
display: 'grid',
|
|
255
|
+
},
|
|
256
|
+
optional: ['gridTemplateColumns', 'gap', 'minHeight'],
|
|
257
|
+
minMatches: 1,
|
|
258
|
+
},
|
|
259
|
+
{
|
|
260
|
+
name: 'grid-list',
|
|
261
|
+
description: 'Responsive auto-fit grid list',
|
|
262
|
+
macro: 'gridList()',
|
|
263
|
+
example: 'chain.gridList()',
|
|
264
|
+
required: {
|
|
265
|
+
display: 'grid',
|
|
266
|
+
},
|
|
267
|
+
optional: ['gridTemplateColumns', 'gap'],
|
|
268
|
+
minMatches: 1,
|
|
269
|
+
},
|
|
270
|
+
{
|
|
271
|
+
name: 'truncate-text',
|
|
272
|
+
description: 'Single-line text truncation with ellipsis',
|
|
273
|
+
macro: 'truncate()',
|
|
274
|
+
example: 'chain.truncate()',
|
|
275
|
+
required: {
|
|
276
|
+
overflow: 'hidden',
|
|
277
|
+
textOverflow: 'ellipsis',
|
|
278
|
+
whiteSpace: 'nowrap',
|
|
279
|
+
},
|
|
280
|
+
minMatches: 3,
|
|
281
|
+
},
|
|
282
|
+
{
|
|
283
|
+
name: 'sr-only',
|
|
284
|
+
description: 'Screen-reader only element',
|
|
285
|
+
macro: 'srOnly()',
|
|
286
|
+
example: 'chain.srOnly()',
|
|
287
|
+
required: {
|
|
288
|
+
position: 'absolute',
|
|
289
|
+
width: '1px',
|
|
290
|
+
height: '1px',
|
|
291
|
+
},
|
|
292
|
+
optional: ['padding', 'margin', 'overflow', 'clip'],
|
|
293
|
+
minMatches: 2,
|
|
294
|
+
},
|
|
295
|
+
|
|
296
|
+
// --- Chain.ts Special Methods ---
|
|
297
|
+
{
|
|
298
|
+
name: 'inline-flex',
|
|
299
|
+
description: 'Inline flex container',
|
|
300
|
+
macro: 'inlineFlex()',
|
|
301
|
+
example: 'chain.inlineFlex()',
|
|
302
|
+
required: {
|
|
303
|
+
display: 'inline-flex',
|
|
304
|
+
},
|
|
305
|
+
minMatches: 1,
|
|
306
|
+
},
|
|
307
|
+
{
|
|
308
|
+
name: 'inline-grid',
|
|
309
|
+
description: 'Inline grid container',
|
|
310
|
+
macro: 'inlineGrid()',
|
|
311
|
+
example: 'chain.inlineGrid()',
|
|
312
|
+
required: {
|
|
313
|
+
display: 'inline-grid',
|
|
314
|
+
},
|
|
315
|
+
minMatches: 1,
|
|
316
|
+
},
|
|
317
|
+
{
|
|
318
|
+
name: 'flex-center-direction',
|
|
319
|
+
description: 'Flex centering with direction',
|
|
320
|
+
macro: "flexCenter('row')",
|
|
321
|
+
example: "chain.flexCenter('col')",
|
|
322
|
+
required: {
|
|
323
|
+
display: 'flex',
|
|
324
|
+
justifyContent: 'center',
|
|
325
|
+
alignItems: 'center',
|
|
326
|
+
},
|
|
327
|
+
optional: ['flexDirection'],
|
|
328
|
+
minMatches: 3,
|
|
329
|
+
},
|
|
330
|
+
{
|
|
331
|
+
name: 'fixed-position',
|
|
332
|
+
description: 'Fixed positioning',
|
|
333
|
+
macro: 'fixed()',
|
|
334
|
+
example: 'chain.fixed({ top: 0 })',
|
|
335
|
+
required: {
|
|
336
|
+
position: 'fixed',
|
|
337
|
+
},
|
|
338
|
+
optional: ['top', 'right', 'bottom', 'left', 'zIndex'],
|
|
339
|
+
minMatches: 1,
|
|
340
|
+
},
|
|
341
|
+
{
|
|
342
|
+
name: 'relative-position',
|
|
343
|
+
description: 'Relative positioning',
|
|
344
|
+
macro: 'relative()',
|
|
345
|
+
example: 'chain.relative()',
|
|
346
|
+
required: {
|
|
347
|
+
position: 'relative',
|
|
348
|
+
},
|
|
349
|
+
minMatches: 1,
|
|
350
|
+
},
|
|
351
|
+
{
|
|
352
|
+
name: 'hidden-element',
|
|
353
|
+
description: 'Hidden element',
|
|
354
|
+
macro: 'hide()',
|
|
355
|
+
example: 'chain.hide()',
|
|
356
|
+
required: {
|
|
357
|
+
display: 'none',
|
|
358
|
+
},
|
|
359
|
+
minMatches: 1,
|
|
360
|
+
},
|
|
361
|
+
{
|
|
362
|
+
name: 'unselectable',
|
|
363
|
+
description: 'Unselectable text',
|
|
364
|
+
macro: 'unselectable()',
|
|
365
|
+
example: 'chain.unselectable()',
|
|
366
|
+
required: {
|
|
367
|
+
userSelect: 'none',
|
|
368
|
+
},
|
|
369
|
+
minMatches: 1,
|
|
370
|
+
},
|
|
371
|
+
{
|
|
372
|
+
name: 'scrollable',
|
|
373
|
+
description: 'Scrollable container',
|
|
374
|
+
macro: 'scrollable()',
|
|
375
|
+
example: 'chain.scrollable()',
|
|
376
|
+
required: {
|
|
377
|
+
overflow: 'auto',
|
|
378
|
+
},
|
|
379
|
+
minMatches: 1,
|
|
380
|
+
},
|
|
381
|
+
{
|
|
382
|
+
name: 'square-shape',
|
|
383
|
+
description: 'Square element with equal sides',
|
|
384
|
+
macro: 'square()',
|
|
385
|
+
example: 'chain.square(100)',
|
|
386
|
+
required: {
|
|
387
|
+
width: '100px',
|
|
388
|
+
height: '100px',
|
|
389
|
+
},
|
|
390
|
+
optional: ['borderRadius'],
|
|
391
|
+
minMatches: 2,
|
|
392
|
+
},
|
|
393
|
+
{
|
|
394
|
+
name: 'circle-shape',
|
|
395
|
+
description: 'Circle element',
|
|
396
|
+
macro: 'circle()',
|
|
397
|
+
example: 'chain.circle(50)',
|
|
398
|
+
required: {
|
|
399
|
+
borderRadius: '50%',
|
|
400
|
+
},
|
|
401
|
+
optional: ['width', 'height'],
|
|
402
|
+
minMatches: 1,
|
|
403
|
+
},
|
|
404
|
+
{
|
|
405
|
+
name: 'bento-grid',
|
|
406
|
+
description: 'Bento box grid layout',
|
|
407
|
+
macro: 'bento()',
|
|
408
|
+
example: 'chain.bento(3)',
|
|
409
|
+
required: {
|
|
410
|
+
display: 'grid',
|
|
411
|
+
},
|
|
412
|
+
optional: ['gridTemplateColumns', 'gap', 'gridAutoRows'],
|
|
413
|
+
minMatches: 1,
|
|
414
|
+
},
|
|
415
|
+
{
|
|
416
|
+
name: 'focus-ring',
|
|
417
|
+
description: 'Focus ring outline',
|
|
418
|
+
macro: 'focusRing()',
|
|
419
|
+
example: 'chain.focusRing()',
|
|
420
|
+
required: {
|
|
421
|
+
outline: '2px solid #3b82f6',
|
|
422
|
+
},
|
|
423
|
+
optional: ['outlineOffset'],
|
|
424
|
+
minMatches: 1,
|
|
425
|
+
},
|
|
426
|
+
{
|
|
427
|
+
name: 'shimmer-effect',
|
|
428
|
+
description: 'Shimmer loading animation',
|
|
429
|
+
macro: 'shimmer()',
|
|
430
|
+
example: 'chain.shimmer()',
|
|
431
|
+
required: {
|
|
432
|
+
background: 'linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%)',
|
|
433
|
+
},
|
|
434
|
+
optional: ['backgroundSize', 'animation'],
|
|
435
|
+
minMatches: 1,
|
|
436
|
+
},
|
|
437
|
+
{
|
|
438
|
+
name: 'skeleton-loader',
|
|
439
|
+
description: 'Skeleton loading state',
|
|
440
|
+
macro: 'skeleton()',
|
|
441
|
+
example: 'chain.skeleton(true)',
|
|
442
|
+
required: {
|
|
443
|
+
animation: 'pulse 1.5s ease-in-out infinite',
|
|
444
|
+
},
|
|
445
|
+
optional: ['backgroundColor', 'borderRadius'],
|
|
446
|
+
minMatches: 1,
|
|
447
|
+
},
|
|
448
|
+
{
|
|
449
|
+
name: 'safe-area-bottom',
|
|
450
|
+
description: 'Safe area padding for notched devices',
|
|
451
|
+
macro: "safeArea('bottom')",
|
|
452
|
+
example: "chain.safeArea('bottom')",
|
|
453
|
+
required: {
|
|
454
|
+
paddingBottom: 'env(safe-area-inset-bottom)',
|
|
455
|
+
},
|
|
456
|
+
minMatches: 1,
|
|
457
|
+
},
|
|
458
|
+
|
|
459
|
+
// --- Glass Morphism ---
|
|
460
|
+
{
|
|
461
|
+
name: 'glass-effect',
|
|
462
|
+
description: 'Frosted glass effect',
|
|
463
|
+
macro: 'glass()',
|
|
464
|
+
example: 'chain.glass()',
|
|
465
|
+
required: {
|
|
466
|
+
backdropFilter: 'blur(16px)',
|
|
467
|
+
},
|
|
468
|
+
optional: ['backgroundColor', 'border', 'borderRadius'],
|
|
469
|
+
minMatches: 1,
|
|
470
|
+
},
|
|
471
|
+
];
|
|
472
|
+
|
|
473
|
+
// ============================================================================
|
|
474
|
+
// Pattern Matcher
|
|
475
|
+
// ============================================================================
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* Check if a rule's declarations match a layout pattern.
|
|
479
|
+
*/
|
|
480
|
+
function matchPattern(rule: IRRule, pattern: LayoutPattern): PatternMatch | null {
|
|
481
|
+
const declarations = rule.declarations;
|
|
482
|
+
const propMap = new Map(declarations.map(d => [d.property, String(d.value)]));
|
|
483
|
+
|
|
484
|
+
const matchedProperties: string[] = [];
|
|
485
|
+
let totalRequired = Object.keys(pattern.required).length;
|
|
486
|
+
let matched = 0;
|
|
487
|
+
|
|
488
|
+
// Check required properties
|
|
489
|
+
for (const [prop, expectedValue] of Object.entries(pattern.required)) {
|
|
490
|
+
const actualValue = propMap.get(prop);
|
|
491
|
+
if (actualValue === String(expectedValue)) {
|
|
492
|
+
matched++;
|
|
493
|
+
matchedProperties.push(prop);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// Check optional properties
|
|
498
|
+
if (pattern.optional) {
|
|
499
|
+
for (const prop of pattern.optional) {
|
|
500
|
+
if (propMap.has(prop)) {
|
|
501
|
+
matchedProperties.push(prop);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// Calculate confidence
|
|
507
|
+
const minMatches = pattern.minMatches || Object.keys(pattern.required).length;
|
|
508
|
+
const confidence = matched >= minMatches
|
|
509
|
+
? Math.min(1, matched / totalRequired)
|
|
510
|
+
: 0;
|
|
511
|
+
|
|
512
|
+
if (confidence >= 0.75 && matched >= minMatches) {
|
|
513
|
+
return {
|
|
514
|
+
pattern,
|
|
515
|
+
ruleId: rule.id,
|
|
516
|
+
selector: rule.selector,
|
|
517
|
+
matchedProperties,
|
|
518
|
+
confidence,
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
return null;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
* Find duplicate patterns across rules.
|
|
527
|
+
*/
|
|
528
|
+
function findDuplicates(matches: PatternMatch[]): PatternReport['duplicates'] {
|
|
529
|
+
const patternGroups = new Map<string, PatternMatch[]>();
|
|
530
|
+
|
|
531
|
+
for (const match of matches) {
|
|
532
|
+
const key = match.pattern.name;
|
|
533
|
+
const group = patternGroups.get(key) || [];
|
|
534
|
+
group.push(match);
|
|
535
|
+
patternGroups.set(key, group);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
const duplicates: PatternReport['duplicates'] = [];
|
|
539
|
+
for (const [patternName, group] of patternGroups) {
|
|
540
|
+
if (group.length >= 2) {
|
|
541
|
+
duplicates.push({
|
|
542
|
+
pattern: patternName,
|
|
543
|
+
selectors: group.map(m => m.selector),
|
|
544
|
+
count: group.length,
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
return duplicates;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
/**
|
|
553
|
+
* Generate suggestions for rules that could use macros.
|
|
554
|
+
*/
|
|
555
|
+
function generateSuggestions(matches: PatternMatch[]): PatternReport['suggestions'] {
|
|
556
|
+
const suggestions: PatternReport['suggestions'] = [];
|
|
557
|
+
|
|
558
|
+
for (const match of matches) {
|
|
559
|
+
if (match.confidence >= 0.85) {
|
|
560
|
+
const propsCount = match.matchedProperties.length;
|
|
561
|
+
suggestions.push({
|
|
562
|
+
selector: match.selector,
|
|
563
|
+
suggestion: match.pattern.macro,
|
|
564
|
+
savings: propsCount - 1, // Saving N-1 declarations
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
return suggestions;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// ============================================================================
|
|
573
|
+
// IR Pass
|
|
574
|
+
// ============================================================================
|
|
575
|
+
|
|
576
|
+
/**
|
|
577
|
+
* Layout Intelligence IR pass.
|
|
578
|
+
* Scans all rules for known layout patterns, generates diagnostics and suggestions.
|
|
579
|
+
*/
|
|
580
|
+
export const layoutIntelligencePass: IRPass = (ir: StyleIR): StyleIR => {
|
|
581
|
+
const allMatches: PatternMatch[] = [];
|
|
582
|
+
|
|
583
|
+
for (const rule of ir.rules) {
|
|
584
|
+
for (const pattern of LAYOUT_PATTERNS) {
|
|
585
|
+
const match = matchPattern(rule, pattern);
|
|
586
|
+
if (match) {
|
|
587
|
+
allMatches.push(match);
|
|
588
|
+
rule.meta.layoutPattern = pattern.name;
|
|
589
|
+
rule.meta.layoutConfidence = match.confidence;
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// Find duplicates
|
|
595
|
+
const duplicates = findDuplicates(allMatches);
|
|
596
|
+
for (const dup of duplicates) {
|
|
597
|
+
ir.diagnostics.push({
|
|
598
|
+
id: 'layout-dup-' + Date.now() + '-' + dup.pattern,
|
|
599
|
+
nodeId: ir.rules[0]?.id || ir.id,
|
|
600
|
+
severity: 'info',
|
|
601
|
+
message: 'Layout pattern "' + dup.pattern + '" found ' + dup.count + ' times: ' + dup.selectors.join(', '),
|
|
602
|
+
suggestion: 'Consider extracting: ' + (LAYOUT_PATTERNS.find(p => p.name === dup.pattern)?.macro || ''),
|
|
603
|
+
pass: 'layout-intelligence',
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
// Generate suggestions
|
|
608
|
+
const suggestions = generateSuggestions(allMatches);
|
|
609
|
+
for (const sug of suggestions) {
|
|
610
|
+
ir.diagnostics.push({
|
|
611
|
+
id: 'layout-sug-' + Date.now() + '-' + sug.selector.replace(/[.#]/g, ''),
|
|
612
|
+
nodeId: ir.rules.find(r => r.selector === sug.selector)?.id || ir.id,
|
|
613
|
+
severity: 'hint',
|
|
614
|
+
message: '"' + sug.selector + '" could use ' + sug.suggestion + ' (save ' + sug.savings + ' declarations)',
|
|
615
|
+
suggestion: sug.suggestion,
|
|
616
|
+
pass: 'layout-intelligence',
|
|
617
|
+
});
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
// Store all matches in IR meta for later use
|
|
621
|
+
ir.meta = ir.meta || {};
|
|
622
|
+
(ir.meta as any).layoutMatches = allMatches;
|
|
623
|
+
(ir.meta as any).layoutDuplicates = duplicates;
|
|
624
|
+
|
|
625
|
+
return ir;
|
|
626
|
+
};
|
|
627
|
+
|
|
628
|
+
// ============================================================================
|
|
629
|
+
// Standalone API
|
|
630
|
+
// ============================================================================
|
|
631
|
+
|
|
632
|
+
/**
|
|
633
|
+
* Analyze a set of declarations and return matching patterns.
|
|
634
|
+
*/
|
|
635
|
+
export function recognizeLayout(declarations: Record<string, string | number>): PatternMatch[] {
|
|
636
|
+
const rule: IRRule = {
|
|
637
|
+
id: 'temp-rule',
|
|
638
|
+
selector: '.temp',
|
|
639
|
+
declarations: Object.entries(declarations).map(([prop, value]) => ({
|
|
640
|
+
id: 'temp-decl-' + prop,
|
|
641
|
+
property: prop,
|
|
642
|
+
value,
|
|
643
|
+
history: [],
|
|
644
|
+
meta: {},
|
|
645
|
+
})),
|
|
646
|
+
pseudoClasses: [],
|
|
647
|
+
atRules: [],
|
|
648
|
+
nestedRules: [],
|
|
649
|
+
conditions: [],
|
|
650
|
+
isDead: false,
|
|
651
|
+
specificity: 0,
|
|
652
|
+
hash: '',
|
|
653
|
+
source: {},
|
|
654
|
+
history: [],
|
|
655
|
+
meta: {},
|
|
656
|
+
};
|
|
657
|
+
|
|
658
|
+
const matches: PatternMatch[] = [];
|
|
659
|
+
for (const pattern of LAYOUT_PATTERNS) {
|
|
660
|
+
const match = matchPattern(rule, pattern);
|
|
661
|
+
if (match) matches.push(match);
|
|
662
|
+
}
|
|
663
|
+
return matches;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
/**
|
|
667
|
+
* Get the best macro for a set of declarations.
|
|
668
|
+
*/
|
|
669
|
+
export function suggestMacro(declarations: Record<string, string | number>): string | null {
|
|
670
|
+
const matches = recognizeLayout(declarations);
|
|
671
|
+
if (matches.length === 0) return null;
|
|
672
|
+
|
|
673
|
+
// Return highest confidence match
|
|
674
|
+
const best = matches.sort((a, b) => b.confidence - a.confidence)[0];
|
|
675
|
+
return best.confidence >= 0.85 ? best.pattern.macro : null;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
/**
|
|
679
|
+
* Get all known layout patterns.
|
|
680
|
+
*/
|
|
681
|
+
export function getLayoutPatterns(): LayoutPattern[] {
|
|
682
|
+
return [...LAYOUT_PATTERNS];
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
// ============================================================================
|
|
686
|
+
// Quick API
|
|
687
|
+
// ============================================================================
|
|
688
|
+
|
|
689
|
+
export const layoutIntelligence = {
|
|
690
|
+
recognize: recognizeLayout,
|
|
691
|
+
suggestMacro,
|
|
692
|
+
getPatterns: getLayoutPatterns,
|
|
693
|
+
pass: layoutIntelligencePass,
|
|
694
|
+
patterns: LAYOUT_PATTERNS,
|
|
695
|
+
};
|
|
696
|
+
|
|
697
|
+
export default layoutIntelligence;
|