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.
Files changed (33) hide show
  1. package/dist/compiler/accessibility-engine.d.ts +57 -0
  2. package/dist/compiler/constraint-solver.d.ts +85 -0
  3. package/dist/compiler/css-if-transpiler.d.ts +33 -0
  4. package/dist/compiler/design-orchestrator.d.ts +119 -0
  5. package/dist/compiler/intent-api.d.ts +73 -0
  6. package/dist/compiler/intent-engine.d.ts +19 -1
  7. package/dist/compiler/layout-intelligence.d.ts +71 -0
  8. package/dist/compiler/pass-manager.d.ts +157 -0
  9. package/dist/compiler/pattern-learner.d.ts +112 -0
  10. package/dist/compiler/responsive-inference.d.ts +63 -0
  11. package/dist/compiler/scroll-timeline.d.ts +91 -0
  12. package/dist/compiler/semantic-tokens.d.ts +57 -0
  13. package/dist/compiler/source-optimizer.d.ts +109 -0
  14. package/dist/compiler/style-ir.d.ts +183 -0
  15. package/dist/index.d.ts +23 -0
  16. package/dist/index.js +4126 -2
  17. package/package.json +1 -1
  18. package/src/compiler/accessibility-engine.ts +502 -0
  19. package/src/compiler/constraint-solver.ts +407 -0
  20. package/src/compiler/css-if-transpiler.ts +117 -0
  21. package/src/compiler/design-orchestrator.ts +322 -0
  22. package/src/compiler/intent-api.ts +505 -0
  23. package/src/compiler/intent-engine.ts +291 -1
  24. package/src/compiler/layout-intelligence.ts +697 -0
  25. package/src/compiler/pass-manager.ts +657 -0
  26. package/src/compiler/pattern-learner.ts +398 -0
  27. package/src/compiler/responsive-inference.ts +415 -0
  28. package/src/compiler/scroll-timeline.ts +284 -0
  29. package/src/compiler/semantic-tokens.ts +468 -0
  30. package/src/compiler/source-optimizer.ts +541 -0
  31. package/src/compiler/style-ir.ts +495 -0
  32. package/src/index.ts +209 -0
  33. 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;