chaincss 2.2.0 → 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.
@@ -0,0 +1,505 @@
1
+ // src/compiler/intent-api.ts
2
+ /**
3
+ * Intent-Based API
4
+ *
5
+ * The highest-level API — developers declare WHAT they want,
6
+ * the compiler resolves HOW using all available modules.
7
+ *
8
+ * @example
9
+ * chain.intent('card') // → full card component
10
+ * chain.intent('center-content') // → flex/grid centering
11
+ * chain.intent('button-primary') // → accessible blue button
12
+ */
13
+
14
+ import type { StyleIR, IRRule, IRPass } from './style-ir.js';
15
+ import { createDeclaration, createRule } from './style-ir.js';
16
+ import { resolveSemantic } from './semantic-tokens.js';
17
+
18
+ // ============================================================================
19
+ // Intent Catalog
20
+ // ============================================================================
21
+
22
+ interface IntentDefinition {
23
+ /** Human-readable name */
24
+ name: string;
25
+ /** Category for organization */
26
+ category: 'layout' | 'component' | 'semantic' | 'interaction';
27
+ /** Description */
28
+ description: string;
29
+ /** Semantic tokens to apply */
30
+ semantics?: Array<{ category: string; intent: string }>;
31
+ /** Direct properties (for simple intents) */
32
+ properties?: Record<string, string | number>;
33
+ /** Pseudo-classes */
34
+ states?: Record<string, Record<string, string | number>>;
35
+ /** Responsive overrides */
36
+ responsive?: Record<string, Record<string, string | number>>;
37
+ /** Accessibility requirements */
38
+ a11y?: string[];
39
+ }
40
+
41
+ const INTENT_CATALOG: Record<string, IntentDefinition> = {
42
+ // ==========================================================================
43
+ // LAYOUT INTENTS
44
+ // ==========================================================================
45
+ 'center-content': {
46
+ name: 'center-content',
47
+ category: 'layout',
48
+ description: 'Center content both horizontally and vertically',
49
+ semantics: [
50
+ { category: 'surface', intent: 'container' },
51
+ ],
52
+ properties: {
53
+ display: 'flex',
54
+ justifyContent: 'center',
55
+ alignItems: 'center',
56
+ },
57
+ },
58
+ 'stack': {
59
+ name: 'stack',
60
+ category: 'layout',
61
+ description: 'Vertical stack with consistent spacing',
62
+ properties: {
63
+ display: 'flex',
64
+ flexDirection: 'column',
65
+ },
66
+ semantics: [
67
+ { category: 'spacing', intent: 'comfortable' },
68
+ ],
69
+ },
70
+ 'sidebar-layout': {
71
+ name: 'sidebar-layout',
72
+ category: 'layout',
73
+ description: 'Two-column layout with mobile collapse',
74
+ properties: {
75
+ display: 'grid',
76
+ gridTemplateColumns: '280px 1fr',
77
+ minHeight: '100vh',
78
+ },
79
+ semantics: [
80
+ { category: 'spacing', intent: 'comfortable' },
81
+ ],
82
+ responsive: {
83
+ 'mobile': { gridTemplateColumns: '1fr' },
84
+ },
85
+ },
86
+ 'grid-list': {
87
+ name: 'grid-list',
88
+ category: 'layout',
89
+ description: 'Responsive auto-fit grid',
90
+ properties: {
91
+ display: 'grid',
92
+ gridTemplateColumns: 'repeat(auto-fit, minmax(280px, 1fr))',
93
+ },
94
+ semantics: [
95
+ { category: 'spacing', intent: 'comfortable' },
96
+ ],
97
+ },
98
+
99
+ // ==========================================================================
100
+ // COMPONENT INTENTS
101
+ // ==========================================================================
102
+ 'card': {
103
+ name: 'card',
104
+ category: 'component',
105
+ description: 'Content card with shadow, radius, and hover lift',
106
+ semantics: [
107
+ { category: 'surface', intent: 'container' },
108
+ { category: 'elevation', intent: 'raised' },
109
+ { category: 'spacing', intent: 'comfortable' },
110
+ ],
111
+ properties: {
112
+ display: 'flex',
113
+ flexDirection: 'column',
114
+ overflow: 'hidden',
115
+ transition: 'box-shadow 0.2s ease, transform 0.2s ease',
116
+ },
117
+ states: {
118
+ hover: {
119
+ boxShadow: '0 10px 30px rgba(0,0,0,0.15)',
120
+ transform: 'translateY(-2px)',
121
+ },
122
+ },
123
+ responsive: {
124
+ 'mobile': { padding: '16px' },
125
+ },
126
+ a11y: ['contrast', 'focus-visible'],
127
+ },
128
+ 'button-primary': {
129
+ name: 'button-primary',
130
+ category: 'component',
131
+ description: 'Primary call-to-action button',
132
+ semantics: [
133
+ { category: 'surface', intent: 'interactive' },
134
+ { category: 'spacing', intent: 'compact' },
135
+ { category: 'state', intent: 'hover' },
136
+ { category: 'state', intent: 'focus' },
137
+ { category: 'state', intent: 'active' },
138
+ { category: 'state', intent: 'disabled' },
139
+ ],
140
+ properties: {
141
+ display: 'inline-flex',
142
+ alignItems: 'center',
143
+ justifyContent: 'center',
144
+ fontWeight: '600',
145
+ border: 'none',
146
+ userSelect: 'none',
147
+ },
148
+ a11y: ['contrast', 'touch-target', 'focus-visible'],
149
+ },
150
+ 'button-secondary': {
151
+ name: 'button-secondary',
152
+ category: 'component',
153
+ description: 'Secondary outlined button',
154
+ semantics: [
155
+ { category: 'spacing', intent: 'compact' },
156
+ { category: 'state', intent: 'focus' },
157
+ { category: 'state', intent: 'disabled' },
158
+ ],
159
+ properties: {
160
+ display: 'inline-flex',
161
+ alignItems: 'center',
162
+ justifyContent: 'center',
163
+ fontWeight: '500',
164
+ backgroundColor: 'transparent',
165
+ border: '1px solid $colors.neutral.300',
166
+ color: '$colors.neutral.700',
167
+ userSelect: 'none',
168
+ },
169
+ states: {
170
+ hover: { backgroundColor: '$colors.neutral.50' },
171
+ },
172
+ a11y: ['contrast', 'touch-target', 'focus-visible'],
173
+ },
174
+ 'input-field': {
175
+ name: 'input-field',
176
+ category: 'component',
177
+ description: 'Text input with focus and error states',
178
+ semantics: [
179
+ { category: 'surface', intent: 'input' },
180
+ { category: 'spacing', intent: 'compact' },
181
+ { category: 'state', intent: 'focus' },
182
+ { category: 'state', intent: 'disabled' },
183
+ ],
184
+ properties: {
185
+ width: '100%',
186
+ fontSize: '16px',
187
+ lineHeight: '1.5',
188
+ transition: 'border-color 0.2s ease, box-shadow 0.2s ease',
189
+ },
190
+ a11y: ['contrast'],
191
+ },
192
+ 'modal': {
193
+ name: 'modal',
194
+ category: 'component',
195
+ description: 'Modal dialog with overlay backdrop',
196
+ semantics: [
197
+ { category: 'surface', intent: 'overlay' },
198
+ { category: 'elevation', intent: 'modal' },
199
+ { category: 'spacing', intent: 'spacious' },
200
+ ],
201
+ properties: {
202
+ display: 'flex',
203
+ flexDirection: 'column',
204
+ maxWidth: '560px',
205
+ margin: 'auto',
206
+ },
207
+ a11y: ['contrast', 'focus-visible'],
208
+ },
209
+ 'tooltip': {
210
+ name: 'tooltip',
211
+ category: 'component',
212
+ description: 'Hover tooltip',
213
+ semantics: [
214
+ { category: 'surface', intent: 'tooltip' },
215
+ ],
216
+ properties: {
217
+ position: 'absolute',
218
+ zIndex: '50',
219
+ pointerEvents: 'none',
220
+ },
221
+ a11y: ['contrast'],
222
+ },
223
+
224
+ // ==========================================================================
225
+ // SEMANTIC INTENTS
226
+ // ==========================================================================
227
+ 'hero-section': {
228
+ name: 'hero-section',
229
+ category: 'semantic',
230
+ description: 'Full-width hero banner',
231
+ semantics: [
232
+ { category: 'spacing', intent: 'generous' },
233
+ ],
234
+ properties: {
235
+ display: 'flex',
236
+ flexDirection: 'column',
237
+ justifyContent: 'center',
238
+ alignItems: 'center',
239
+ width: '100%',
240
+ minHeight: '60vh',
241
+ textAlign: 'center',
242
+ },
243
+ responsive: {
244
+ 'mobile': { minHeight: '40vh', padding: '32px 16px' },
245
+ },
246
+ },
247
+ 'sticky-header': {
248
+ name: 'sticky-header',
249
+ category: 'semantic',
250
+ description: 'Sticky header with backdrop blur',
251
+ semantics: [
252
+ { category: 'elevation', intent: 'sticky' },
253
+ { category: 'spacing', intent: 'compact' },
254
+ ],
255
+ properties: {
256
+ backgroundColor: 'rgba(255,255,255,0.9)',
257
+ backdropFilter: 'blur(8px)',
258
+ borderBottom: '1px solid rgba(0,0,0,0.05)',
259
+ },
260
+ },
261
+ 'call-to-action': {
262
+ name: 'call-to-action',
263
+ category: 'semantic',
264
+ description: 'Attention-grabbing CTA section',
265
+ semantics: [
266
+ { category: 'surface', intent: 'interactive' },
267
+ { category: 'spacing', intent: 'spacious' },
268
+ ],
269
+ properties: {
270
+ textAlign: 'center',
271
+ },
272
+ },
273
+ 'muted-text': {
274
+ name: 'muted-text',
275
+ category: 'semantic',
276
+ description: 'Secondary, less prominent text',
277
+ semantics: [
278
+ { category: 'text', intent: 'muted' },
279
+ ],
280
+ },
281
+ 'visually-hidden': {
282
+ name: 'visually-hidden',
283
+ category: 'semantic',
284
+ description: 'Visible only to screen readers',
285
+ properties: {
286
+ position: 'absolute',
287
+ width: '1px',
288
+ height: '1px',
289
+ padding: '0',
290
+ margin: '-1px',
291
+ overflow: 'hidden',
292
+ clip: 'rect(0, 0, 0, 0)',
293
+ whiteSpace: 'nowrap',
294
+ borderWidth: '0',
295
+ },
296
+ },
297
+
298
+ // ==========================================================================
299
+ // INTERACTION INTENTS
300
+ // ==========================================================================
301
+ 'hover-lift': {
302
+ name: 'hover-lift',
303
+ category: 'interaction',
304
+ description: 'Subtle lift on hover',
305
+ states: {
306
+ hover: {
307
+ transform: 'translateY(-2px)',
308
+ boxShadow: '0 8px 25px rgba(0,0,0,0.12)',
309
+ transition: 'all 0.2s ease',
310
+ },
311
+ },
312
+ a11y: ['focus-visible'],
313
+ },
314
+ 'focus-ring': {
315
+ name: 'focus-ring',
316
+ category: 'interaction',
317
+ description: 'Accessible focus indicator',
318
+ states: {
319
+ 'focus-visible': {
320
+ outline: '2px solid $colors.primary.500',
321
+ outlineOffset: '2px',
322
+ },
323
+ },
324
+ },
325
+ };
326
+
327
+ // ============================================================================
328
+ // Resolver
329
+ // ============================================================================
330
+
331
+ /**
332
+ * Resolve an intent into all its constituent parts.
333
+ * Calls semantic tokens, properties, states, responsive overrides.
334
+ */
335
+ export function resolveIntent(
336
+ intentName: string,
337
+ options?: {
338
+ theme?: 'light' | 'dark' | 'high-contrast';
339
+ viewport?: string;
340
+ }
341
+ ): {
342
+ properties: Record<string, string | number>;
343
+ states: Record<string, Record<string, string | number>>;
344
+ responsive: Record<string, Record<string, string | number>>;
345
+ a11y: string[];
346
+ description: string;
347
+ } | null {
348
+ const intent = INTENT_CATALOG[intentName];
349
+ if (!intent) return null;
350
+
351
+ const properties: Record<string, string | number> = {};
352
+ const states: Record<string, Record<string, string | number>> = {};
353
+ const responsive: Record<string, Record<string, string | number>> = {};
354
+
355
+ // 1. Resolve semantic tokens
356
+ if (intent.semantics) {
357
+ for (const sem of intent.semantics) {
358
+ const resolved = resolveSemantic(sem.category as any, sem.intent, {
359
+ mode: options?.theme || 'light',
360
+ });
361
+ if (resolved) {
362
+ for (const [prop, value] of Object.entries(resolved.properties)) {
363
+ if (resolved.pseudoClass) {
364
+ if (!states[resolved.pseudoClass]) states[resolved.pseudoClass] = {};
365
+ states[resolved.pseudoClass][prop] = value;
366
+ } else {
367
+ properties[prop] = value;
368
+ }
369
+ }
370
+ }
371
+ }
372
+ }
373
+
374
+ // 2. Apply direct properties
375
+ if (intent.properties) {
376
+ Object.assign(properties, intent.properties);
377
+ }
378
+
379
+ // 3. Apply states
380
+ if (intent.states) {
381
+ for (const [state, props] of Object.entries(intent.states)) {
382
+ if (!states[state]) states[state] = {};
383
+ Object.assign(states[state], props);
384
+ }
385
+ }
386
+
387
+ // 4. Apply responsive overrides
388
+ if (intent.responsive) {
389
+ Object.assign(responsive, intent.responsive);
390
+ }
391
+
392
+ return {
393
+ properties,
394
+ states,
395
+ responsive,
396
+ a11y: intent.a11y || [],
397
+ description: intent.description,
398
+ };
399
+ }
400
+
401
+ /**
402
+ * Get all available intents.
403
+ */
404
+ export function getAvailableIntents(): string[] {
405
+ return Object.keys(INTENT_CATALOG);
406
+ }
407
+
408
+ /**
409
+ * Get intents by category.
410
+ */
411
+ export function getIntentsByCategory(category: IntentDefinition['category']): string[] {
412
+ return Object.entries(INTENT_CATALOG)
413
+ .filter(([, def]) => def.category === category)
414
+ .map(([name]) => name);
415
+ }
416
+
417
+ /**
418
+ * Get a description of an intent.
419
+ */
420
+ export function getIntentDescription(intentName: string): string | null {
421
+ return INTENT_CATALOG[intentName]?.description || null;
422
+ }
423
+
424
+ // ============================================================================
425
+ // IR Pass
426
+ // ============================================================================
427
+
428
+ /**
429
+ * Intent API IR pass.
430
+ * Resolves _intent metadata on rules into full component definitions.
431
+ */
432
+ export const intentAPIPass: IRPass = (ir: StyleIR): StyleIR => {
433
+ for (const rule of ir.rules) {
434
+ const intentName: string = rule.meta._intent;
435
+ if (!intentName) continue;
436
+
437
+ const resolved = resolveIntent(intentName);
438
+ if (!resolved) continue;
439
+
440
+ // Apply properties
441
+ for (const [prop, value] of Object.entries(resolved.properties)) {
442
+ rule.declarations.push({
443
+ id: 'intent-prop-' + Date.now() + '-' + prop,
444
+ property: prop,
445
+ value,
446
+ history: [{
447
+ pass: 'intent-api',
448
+ action: 'resolved-intent',
449
+ timestamp: Date.now(),
450
+ reason: 'intent("' + intentName + '") → ' + prop + ': ' + value,
451
+ }],
452
+ meta: { intent: intentName },
453
+ });
454
+ }
455
+
456
+ // Apply states as pseudo-classes
457
+ for (const [stateName, stateProps] of Object.entries(resolved.states)) {
458
+ rule.pseudoClasses.push({
459
+ id: 'intent-state-' + Date.now() + '-' + stateName,
460
+ name: stateName,
461
+ declarations: Object.entries(stateProps).map(([prop, value]) => ({
462
+ id: 'intent-decl-' + prop,
463
+ property: prop,
464
+ value,
465
+ history: [{
466
+ pass: 'intent-api',
467
+ action: 'resolved-state',
468
+ timestamp: Date.now(),
469
+ reason: 'intent("' + intentName + '") state:' + stateName,
470
+ }],
471
+ meta: {},
472
+ })),
473
+ source: rule.source,
474
+ history: [],
475
+ });
476
+ }
477
+
478
+ // Store responsive info for responsive-inference pass
479
+ if (Object.keys(resolved.responsive).length > 0) {
480
+ rule.meta._responsiveIntents = resolved.responsive;
481
+ }
482
+
483
+ // Store a11y requirements for accessibility pass
484
+ if (resolved.a11y.length > 0) {
485
+ rule.meta._a11yRequirements = resolved.a11y;
486
+ }
487
+ }
488
+
489
+ return ir;
490
+ };
491
+
492
+ // ============================================================================
493
+ // Quick API
494
+ // ============================================================================
495
+
496
+ export const intentAPI = {
497
+ resolve: resolveIntent,
498
+ list: getAvailableIntents,
499
+ byCategory: getIntentsByCategory,
500
+ description: getIntentDescription,
501
+ catalog: INTENT_CATALOG,
502
+ pass: intentAPIPass,
503
+ };
504
+
505
+ export default intentAPI;