qss 0.1.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,1054 @@
1
+ /**
2
+ * QSS Engine - Quantized Spatial Styling Transpiler
3
+ * A portable, lightweight engine for agent-aware spatial styling.
4
+ * Maps intent (x, y, w, h) to deterministic CSS using Quants (qnts).
5
+ */
6
+
7
+ const QSSEngine = (function() {
8
+ // QSS-GENERATED-BEGIN (exe/qss-sync rewrites everything below, up to the closing marker)
9
+ // GENERATED FROM grammar.yml - DO NOT EDIT
10
+ const BRAND_COLORS = {
11
+ 'black': '#000000',
12
+ 'white': '#ffffff',
13
+ 'transparent': 'transparent',
14
+ 'none': 'none',
15
+ 'slate-50': '#f8fafc',
16
+ 'slate-100': '#f1f5f9',
17
+ 'slate-200': '#e2e8f0',
18
+ 'slate-300': '#cbd5e1',
19
+ 'slate-400': '#94a3b8',
20
+ 'slate-500': '#64748b',
21
+ 'slate-600': '#475569',
22
+ 'slate-700': '#334155',
23
+ 'slate-800': '#1e293b',
24
+ 'slate-900': '#0f172a',
25
+ 'slate-950': '#020617',
26
+ 'rose-500': '#f43f5e'
27
+ };
28
+
29
+ const DEPTHS = {
30
+ 'canvas': '0',
31
+ 'content': '100',
32
+ 'ui': '500',
33
+ 'overlay': '900',
34
+ 'modal': '1000'
35
+ };
36
+
37
+ let objectRegistry = {};
38
+
39
+ const FONTS = {
40
+ 'sans': "Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif",
41
+ 'mono': "'JetBrains Mono', 'Fira Code', ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace"
42
+ };
43
+
44
+ const DSL_WHITELIST = new Set([
45
+ 'absolute',
46
+ 'anchored',
47
+ 'around',
48
+ 'bg',
49
+ 'bg-alpha',
50
+ 'blend',
51
+ 'blur',
52
+ 'border',
53
+ 'border-alpha',
54
+ 'border-b',
55
+ 'border-l',
56
+ 'border-r',
57
+ 'border-t',
58
+ 'bright',
59
+ 'busy',
60
+ 'centered',
61
+ 'clickable',
62
+ 'contain',
63
+ 'contrast',
64
+ 'cover',
65
+ 'cursor-col-resize',
66
+ 'cursor-row-resize',
67
+ 'detached',
68
+ 'dev-border',
69
+ 'evenly',
70
+ 'flex',
71
+ 'flex-1',
72
+ 'flex-auto',
73
+ 'flex-col',
74
+ 'flex-none',
75
+ 'flex-nowrap',
76
+ 'flex-row',
77
+ 'flex-wrap',
78
+ 'floating',
79
+ 'font-black',
80
+ 'font-bold',
81
+ 'font-medium',
82
+ 'font-mono',
83
+ 'font-sans',
84
+ 'foreign',
85
+ 'ghost',
86
+ 'grayscale',
87
+ 'grid',
88
+ 'group',
89
+ 'h-end',
90
+ 'h-f',
91
+ 'h-full',
92
+ 'h-mid',
93
+ 'h-start',
94
+ 'horizontal',
95
+ 'hue',
96
+ 'icon-size',
97
+ 'invert',
98
+ 'invisible',
99
+ 'italic',
100
+ 'items-center',
101
+ 'justify-between',
102
+ 'justify-center',
103
+ 'leading',
104
+ 'lowercase',
105
+ 'nowrap',
106
+ 'opacity',
107
+ 'outline-none',
108
+ 'pos',
109
+ 'pos-b',
110
+ 'pos-r',
111
+ 'pos-x',
112
+ 'pos-y',
113
+ 'qss',
114
+ 'qss-content-driven',
115
+ 'qss-dampen-scaling',
116
+ 'qss-ergonomic-override',
117
+ 'qss-legibility-override',
118
+ 'qss-payload-override',
119
+ 'relative',
120
+ 'resize',
121
+ 'ring',
122
+ 'rounded',
123
+ 'rounded-full',
124
+ 'saturate',
125
+ 'scroll',
126
+ 'scroll-smooth',
127
+ 'sepia',
128
+ 'shadow',
129
+ 'smooth',
130
+ 'spaced',
131
+ 'stuck',
132
+ 'table-fixed',
133
+ 'text-alpha',
134
+ 'text-size',
135
+ 'tracking',
136
+ 'tracking-tighter',
137
+ 'tracking-wide',
138
+ 'tracking-wider',
139
+ 'tracking-widest',
140
+ 'transition-all',
141
+ 'transition-colors',
142
+ 'transition-none',
143
+ 'truncate',
144
+ 'underline',
145
+ 'unselectable',
146
+ 'uppercase',
147
+ 'v-end',
148
+ 'v-mid',
149
+ 'v-start',
150
+ 'vertical',
151
+ 'visible',
152
+ 'w-f',
153
+ 'w-full'
154
+ ]);
155
+ // QSS-GENERATED-END
156
+
157
+
158
+
159
+
160
+
161
+
162
+
163
+
164
+
165
+
166
+
167
+
168
+
169
+
170
+
171
+
172
+
173
+
174
+
175
+
176
+
177
+
178
+
179
+
180
+
181
+
182
+
183
+
184
+
185
+
186
+
187
+
188
+
189
+
190
+
191
+
192
+
193
+
194
+
195
+
196
+
197
+
198
+
199
+
200
+
201
+
202
+
203
+
204
+
205
+
206
+
207
+
208
+
209
+
210
+
211
+
212
+
213
+
214
+
215
+
216
+
217
+
218
+
219
+
220
+
221
+
222
+
223
+
224
+
225
+
226
+
227
+
228
+
229
+
230
+
231
+
232
+
233
+
234
+
235
+
236
+
237
+
238
+
239
+
240
+
241
+
242
+
243
+
244
+
245
+
246
+
247
+
248
+
249
+
250
+
251
+
252
+
253
+
254
+
255
+
256
+
257
+
258
+
259
+
260
+
261
+
262
+
263
+
264
+
265
+
266
+
267
+
268
+
269
+
270
+
271
+
272
+
273
+
274
+
275
+
276
+
277
+
278
+
279
+
280
+
281
+
282
+ // Constants
283
+ let qnt = 1;
284
+
285
+
286
+
287
+
288
+ let anchorSelector = '#qss-canvas';
289
+ let pulseInterval = null;
290
+ let observer = null;
291
+ let styleTag = null;
292
+ const knownAdjectives = new Set();
293
+ let pulseRequested = false;
294
+
295
+ /**
296
+ * Performs a global scan for QSS adjectives in the DOM.
297
+ */
298
+ function scanAndTranspile() {
299
+ const elements = document.querySelectorAll('.qss, [class*="["]');
300
+ elements.forEach(el => {
301
+ if (el.closest('.foreign')) return;
302
+ el.classList.forEach(cls => {
303
+ // Extract prefix if present (e.g., hover:opacity[1000])
304
+ const prefixMatch = cls.match(/^([a-z-]+):(.+)$/);
305
+ const baseClass = prefixMatch ? prefixMatch[2] : cls;
306
+
307
+ const isQSS = (baseClass.includes('[') && baseClass.includes(']')) ||
308
+ DSL_WHITELIST.has(baseClass) ||
309
+ /^(text|bg|border|font)-/.test(baseClass) ||
310
+ /^[A-Z]/.test(baseClass); // Matches Composite Objects
311
+
312
+ if (isQSS && !knownAdjectives.has(cls)) {
313
+ processNewClass(cls);
314
+ }
315
+ });
316
+ });
317
+ }
318
+
319
+ /**
320
+ * The Layout Heartbeat.
321
+ * Recalculates --qnt and exports Noun coordinates.
322
+ */
323
+ function updatePulse() {
324
+ if (pulseRequested) return;
325
+ pulseRequested = true;
326
+
327
+ requestAnimationFrame(() => {
328
+ pulseRequested = false;
329
+
330
+ // 1. Sync with JIT first - inject styles into DOM
331
+ scanAndTranspile();
332
+
333
+ // 2. Schedule measurement pass AFTER styles have been applied by the browser
334
+ requestAnimationFrame(() => {
335
+ measureAndExport();
336
+ });
337
+ });
338
+ }
339
+
340
+ /**
341
+ * Measures Nouns and exports CSS variables.
342
+ */
343
+ function measureAndExport() {
344
+ const anchor = document.querySelector(anchorSelector);
345
+ if (!anchor) return;
346
+
347
+ const anchorRect = anchor.getBoundingClientRect();
348
+
349
+ // The unit (--qnt) is computed by CSS alone (0.1cqw on the canvas's children, see qss_core.scss); read it
350
+ // back for the noun export instead of setting it. Ultra-wide dampening is pending a CSS-only decision.
351
+ const probe = anchor.firstElementChild || anchor;
352
+ const unit = parseFloat(getComputedStyle(probe).getPropertyValue('--qnt')) || anchorRect.width / 1000;
353
+
354
+ // Export Boundaries
355
+ const nouns = anchor.querySelectorAll('[id]');
356
+ nouns.forEach(noun => {
357
+ const rect = noun.getBoundingClientRect();
358
+ const l = (rect.left - anchorRect.left) / unit;
359
+ const t = (rect.top - anchorRect.top) / unit;
360
+ const w = rect.width / unit;
361
+ const h = rect.height / unit;
362
+ const r = (anchorRect.right - rect.right) / unit;
363
+ const b = (anchorRect.bottom - rect.bottom) / unit;
364
+
365
+ const prefix = `--noun-${noun.id}`;
366
+ document.documentElement.style.setProperty(`${prefix}-x1`, `${l}`);
367
+ document.documentElement.style.setProperty(`${prefix}-x2`, `${l + w}`);
368
+ document.documentElement.style.setProperty(`${prefix}-y1`, `${t}`);
369
+ document.documentElement.style.setProperty(`${prefix}-y2`, `${t + h}`);
370
+ document.documentElement.style.setProperty(`${prefix}-r1`, `${r}`);
371
+ document.documentElement.style.setProperty(`${prefix}-r2`, `${r + w}`);
372
+ document.documentElement.style.setProperty(`${prefix}-b1`, `${b}`);
373
+ document.documentElement.style.setProperty(`${prefix}-b2`, `${b + h}`);
374
+ document.documentElement.style.setProperty(`${prefix}-w`, `${w}`);
375
+ document.documentElement.style.setProperty(`${prefix}-h`, `${h}`);
376
+ });
377
+ }
378
+
379
+ /**
380
+ * Escapes special characters for CSS selector compatibility.
381
+ * @param {string} cls
382
+ * @returns {string} escapedCls
383
+ */
384
+ function escapeSelector(cls) {
385
+ return cls.replace(/\[/g, '\\[').replace(/\]/g, '\\]')
386
+ .replace(/\|/g, '\\|')
387
+ .replace(/\./g, '\\.')
388
+ .replace(/\^/g, '\\^')
389
+ .replace(/:/g, '\\:')
390
+ .replace(/,/g, '\\,')
391
+ .replace(/\(/g, '\\(')
392
+ .replace(/\)/g, '\\)')
393
+ .replace(/#/g, '\\#')
394
+ .replace(/%/g, '\\%');
395
+ }
396
+
397
+ /**
398
+ * Initializes the Living Observer.
399
+ */
400
+ function startObserver() {
401
+ if (observer) return;
402
+
403
+ styleTag = document.createElement('style');
404
+ styleTag.id = 'qss-jit-styles';
405
+ document.head.appendChild(styleTag);
406
+
407
+ observer = new MutationObserver((mutations) => {
408
+ const isPurelyForeign = mutations.every(mutation => {
409
+ const el = mutation.target.nodeType === 1 ? mutation.target : mutation.target.parentElement;
410
+ return el && el.closest('.foreign');
411
+ });
412
+ if (isPurelyForeign) return;
413
+ updatePulse();
414
+ });
415
+
416
+ observer.observe(document.body, {
417
+ attributes: true,
418
+ childList: true,
419
+ subtree: true,
420
+ attributeFilter: ['class']
421
+ });
422
+
423
+ updatePulse();
424
+ }
425
+
426
+ /**
427
+ * Transpiles a single QSS class and injects it into the JIT style tag.
428
+ */
429
+ function processNewClass(cls) {
430
+ // 0. Extract Prefix
431
+ let baseClass = cls;
432
+ let prefix = null;
433
+
434
+ const prefixMatch = baseClass.match(/^([a-z-]+):(.+)$/);
435
+ if (prefixMatch) {
436
+ prefix = prefixMatch[1];
437
+ baseClass = prefixMatch[2];
438
+ }
439
+
440
+ let property, value;
441
+
442
+ // 1. Check for Composite Noun Type (PascalCase, e.g., Button[200|50])
443
+ const objectMatch = baseClass.match(/^(?:qss-)?([A-Z][a-zA-Z0-9]+)(?:\[(.+)\])?$/);
444
+ if (objectMatch && objectRegistry[objectMatch[1]]) {
445
+ expandObject(cls, objectMatch[1], objectMatch[2], prefix);
446
+ return;
447
+ }
448
+
449
+ // hover/active/focus/focus-within and any group-* state are now handled at
450
+ // compile time (qss_core.scss's qss-state-boost/qss-group-boost mixins --
451
+ // architecture-decisions-2026-09-16.md section 8/9). Skip them here instead
452
+ // of injecting a redundant rule -- or, for focus-within/group-*, which the
453
+ // allowed-suffix list below never actually included, an incorrect always-on
454
+ // one (real pre-existing bug, fixed as a side effect of this removal).
455
+ // "visited" is deliberately left on the runtime path below -- A3 does not
456
+ // cover it, and no real usage exists today, but removing working support
457
+ // for a state nothing else replaces would be a real regression, not cleanup.
458
+ if (prefix && (['hover', 'active', 'focus', 'focus-within'].includes(prefix) || prefix.startsWith('group-'))) {
459
+ return;
460
+ }
461
+
462
+ // 2. Check for Atomic Adjective (lowercase with brackets, e.g., pos-x[10])
463
+ const atomicMatch = baseClass.match(/^(?:qss-)?([a-z-]+)\[(.+)\]$/);
464
+
465
+ if (atomicMatch) {
466
+ property = atomicMatch[1];
467
+ value = atomicMatch[2].replace(/_/g, ' '); // Replace underscores with spaces for CSS values
468
+ } else {
469
+ // 3. Check for Standard Adjective (e.g., flex, vertical, centered, text-indigo-500)
470
+ const standardMatch = baseClass.match(/^(?:qss-)?([a-z-]+)$/);
471
+ if (!standardMatch) return;
472
+
473
+ const raw = standardMatch[1];
474
+ if (raw.startsWith('text-')) {
475
+ property = 'text';
476
+ value = raw.replace('text-', '');
477
+ } else if (raw.startsWith('bg-')) {
478
+ property = 'bg';
479
+ value = raw.replace('bg-', '');
480
+ } else if (raw.startsWith('border-')) {
481
+ property = 'border-color';
482
+ value = raw.replace('border-', '');
483
+ } else if (raw.startsWith('font-')) {
484
+ property = 'font';
485
+ value = raw.replace('font-', '');
486
+ } else {
487
+ property = raw;
488
+ value = '';
489
+ }
490
+ }
491
+
492
+ let cssRule = mapProperty(property, value);
493
+
494
+ if (cssRule) {
495
+ const escapedCls = escapeSelector(cls);
496
+ let selector = `.${escapedCls}`;
497
+
498
+ if (prefix === 'visited') {
499
+ selector = `${selector}:${prefix}`;
500
+ }
501
+
502
+ const fullRule = `${selector}, ${anchorSelector}${anchorSelector} ${selector} { ${cssRule} }\n`;
503
+ styleTag.appendChild(document.createTextNode(fullRule));
504
+ knownAdjectives.add(cls);
505
+ }
506
+ }
507
+
508
+ /**
509
+ * Expands a Composite Adjective using its Blueprint and optional Parameters.
510
+ */
511
+ function expandObject(cls, typeName, paramString, prefix) {
512
+ const blueprint = objectRegistry[typeName];
513
+ if (!blueprint) return;
514
+
515
+ const params = paramString ? paramString.split('|') : [];
516
+ const paramMap = blueprint.params || [];
517
+
518
+ // Build base declarations from adjectives
519
+ let cssRules = (blueprint.adjectives || []).map(adj => {
520
+ const match = adj.match(/^([a-z-]+)(?:\[([^\]]+)\])?$/);
521
+ if (!match) return null;
522
+ return mapProperty(match[1], match[2] || '');
523
+ }).filter(r => r);
524
+
525
+ // Apply Intrinsic Dimensions (Defaults)
526
+ const dimensions = { ...blueprint.intrinsic };
527
+
528
+ // Apply Parametric Overrides with Clamping
529
+ params.forEach((val, idx) => {
530
+ const key = paramMap[idx];
531
+ if (key && val) {
532
+ let finalVal = val;
533
+
534
+ // Clamping Logic: Ensure parameters stay within min/max bounds
535
+ if (!isNaN(parseFloat(val))) {
536
+ const numVal = parseFloat(val);
537
+ if (blueprint[`min-${key}`] !== undefined) {
538
+ finalVal = Math.max(parseFloat(blueprint[`min-${key}`]), numVal);
539
+ }
540
+ if (blueprint[`max-${key}`] !== undefined) {
541
+ finalVal = Math.min(parseFloat(blueprint[`max-${key}`]), finalVal);
542
+ }
543
+ }
544
+
545
+ dimensions[key] = finalVal;
546
+ }
547
+ });
548
+
549
+ // Convert Dimensions to CSS (Support min/max properties)
550
+ ['w', 'h'].forEach(dim => {
551
+ if (dimensions[dim]) cssRules.push(mapProperty(dim, dimensions[dim]));
552
+ if (blueprint[`min-${dim}`]) cssRules.push(mapProperty(`min-${dim}`, blueprint[`min-${dim}`]));
553
+ if (blueprint[`max-${dim}`]) cssRules.push(mapProperty(`max-${dim}`, blueprint[`max-${dim}`]));
554
+ });
555
+
556
+ const escapedCls = escapeSelector(cls);
557
+ let selector = `.${escapedCls}`;
558
+
559
+ if (prefix && ['hover', 'active', 'focus', 'visited'].includes(prefix)) {
560
+ selector = `${selector}:${prefix}`;
561
+ }
562
+
563
+ const fullRule = `${selector}, ${anchorSelector}${anchorSelector} ${selector} {\n ${cssRules.join(';\n ')};\n}\n`;
564
+
565
+ styleTag.appendChild(document.createTextNode(fullRule));
566
+ knownAdjectives.add(cls);
567
+ }
568
+
569
+ /**
570
+ * Initializes the Unit Pulse.
571
+ */
572
+ function startPulse() {
573
+ if (pulseInterval) return;
574
+ window.addEventListener('resize', updatePulse);
575
+ updatePulse();
576
+ pulseInterval = true;
577
+ }
578
+
579
+ /**
580
+ * Configures the engine anchor
581
+ */
582
+ function setAnchor(selector) {
583
+ anchorSelector = selector;
584
+ }
585
+
586
+ /**
587
+ * Registers a reusable spatial object definition
588
+ */
589
+ function registerObject(name, definition) {
590
+ objectRegistry[name] = definition;
591
+ }
592
+
593
+ /**
594
+ * Calculates a grid of coordinates.
595
+ */
596
+ function calculateGrid(count, cols, itemW, itemH, gapX = 0, gapY = 0) {
597
+ const items = [];
598
+ for (let i = 0; i < count; i++) {
599
+ const row = Math.floor(i / cols);
600
+ const col = i % cols;
601
+ items.push({
602
+ index: i,
603
+ x: col * (itemW + gapX),
604
+ y: row * (itemH + gapY),
605
+ w: itemW,
606
+ h: itemH
607
+ });
608
+ }
609
+ return items;
610
+ }
611
+
612
+ /**
613
+ * Transpiles a QSS string to valid CSS.
614
+ * @param {string} qssText
615
+ * @returns {string} cssOutput
616
+ */
617
+ function transpile(qssText) {
618
+ if (!qssText) return '';
619
+
620
+ // Global Reset / Stage Styles (Applied automatically)
621
+ const header = `/* Generated by QSS Engine v${this.version} */\n\n`;
622
+
623
+ // Basic parser logic: find selector { block }
624
+ // Supports prefixes like hover:, active:, sm:, etc.
625
+ const rules = [];
626
+ const regex = /(([a-z-]+:)?([#\.][\w-]+))\s*\{([^}]+)\}/g;
627
+ let match;
628
+
629
+ while ((match = regex.exec(qssText)) !== null) {
630
+ const fullSelector = match[1];
631
+ const prefix = match[2] ? match[2].slice(0, -1) : null;
632
+ const baseSelector = match[3];
633
+ const declarations = match[4].split(';').map(d => d.trim()).filter(d => d);
634
+
635
+ const cssProps = declarations.map(decl => {
636
+ const [key, val] = decl.split(':').map(s => s.trim());
637
+ return mapProperty(key, val);
638
+ }).filter(p => p);
639
+
640
+ // Handle Pseudo-states and Breakpoints
641
+ let outputSelector = baseSelector.startsWith('#') ? `${baseSelector}.qss` : baseSelector;
642
+
643
+ // Apply anchor specificity
644
+ if (anchorSelector) {
645
+ outputSelector = `${anchorSelector} ${outputSelector}`;
646
+ }
647
+
648
+ if (prefix) {
649
+ if (['hover', 'active', 'focus', 'visited'].includes(prefix)) {
650
+ outputSelector = `${outputSelector}:${prefix}`;
651
+ } else {
652
+ // It's a breakpoint - we'll need to wrap this in a media query eventually
653
+ // For now, let's just generate the class name
654
+ outputSelector = `.${prefix}\\:${outputSelector.slice(1)}`;
655
+ }
656
+ }
657
+
658
+ rules.push(`${outputSelector} {\n ${cssProps.join(';\n ')};\n}`);
659
+ }
660
+
661
+ return header + rules.join('\n\n');
662
+ }
663
+
664
+ /**
665
+ * Maps QSS shorthand to CSS logic.
666
+ * Supports '#' references for Reference Anchoring.
667
+ */
668
+ function mapProperty(key, val) {
669
+ // --- Relational Midpoint Resolution ---
670
+ // Converts pos-x[^mid:explorer:inspector] -> left: calc((var(--noun-explorer-x2) + var(--noun-inspector-x1)) / 2 * var(--qnt))
671
+ const midMatch = val && val.match(/^\^mid\[([^|]+)\|([^\]]+)\]$/);
672
+ if (midMatch) {
673
+ const n1 = midMatch[1];
674
+ const n2 = midMatch[2];
675
+ const v1 = `--noun-${n1}`;
676
+ const v2 = `--noun-${n2}`;
677
+
678
+ switch(key) {
679
+ case 'x': case 'pos-x':
680
+ return `left: calc((var(${v1}-x2) + var(${v2}-x1)) / 2 * var(--qnt))`;
681
+ case 'r': case 'pos-r':
682
+ // From right, it's 1000 - Midpoint
683
+ return `right: calc((1000 - (var(${v1}-x2) + var(${v2}-x1)) / 2) * var(--qnt))`;
684
+ default:
685
+ return '';
686
+ }
687
+ }
688
+
689
+ // --- Reference Anchor Resolution ---
690
+ // Converts pos-y[^header] -> top: calc(var(--noun-header-y2) * var(--qnt))
691
+ if (val && val.startsWith('^')) {
692
+ const nounId = val.slice(1);
693
+ const varPrefix = `--noun-${nounId}`;
694
+
695
+ switch(key) {
696
+ case 'x': case 'pos-x':
697
+ return `left: calc(var(${varPrefix}-x2) * var(--qnt))`;
698
+ case 'y': case 'pos-y':
699
+ return `top: calc(var(${varPrefix}-y2) * var(--qnt))`;
700
+ case 'r': case 'pos-r':
701
+ return `right: calc(var(${varPrefix}-r2) * var(--qnt))`;
702
+ case 'b': case 'pos-b':
703
+ return `bottom: calc(var(${varPrefix}-b2) * var(--qnt))`;
704
+ case 'w':
705
+ return `width: calc(var(${varPrefix}-w) * var(--qnt))`;
706
+ case 'h':
707
+ return `height: calc(var(${varPrefix}-h) * var(--qnt))`;
708
+ default:
709
+ return `${key}: var(${varPrefix}-${key})`;
710
+ }
711
+ }
712
+
713
+ switch(key) {
714
+ // Positioning DSL
715
+ case 'detached':
716
+ case 'absolute':
717
+ return 'position: absolute';
718
+ case 'anchored':
719
+ case 'relative':
720
+ return 'position: relative';
721
+ case 'floating':
722
+ case 'fixed':
723
+ return 'position: fixed';
724
+ case 'stuck':
725
+ case 'sticky':
726
+ return 'position: sticky';
727
+
728
+ // Spatial Coordinates
729
+ case 'x':
730
+ case 'pos-x':
731
+ if (val === 'center') return 'left: 50%; transform: translateX(-50%)';
732
+ if (val.startsWith('right-')) return `left: auto; right: calc(${val.split('-')[1]} * var(--qnt))`;
733
+ return `left: calc(${val} * var(--qnt))`;
734
+
735
+ case 'y':
736
+ case 'pos-y':
737
+ if (val === 'center') return 'top: 50%; transform: translateY(-50%)';
738
+ if (val.startsWith('bottom-')) return `top: auto; bottom: calc(${val.split('-')[1]} * var(--qnt))`;
739
+ return `top: calc(${val} * var(--qnt))`;
740
+
741
+ case 'pos-r':
742
+ case 'r':
743
+ return `right: calc(${val} * var(--qnt))`;
744
+ case 'pos-b':
745
+ case 'b':
746
+ return `bottom: calc(${val} * var(--qnt))`;
747
+
748
+ // Dimensions
749
+ case 'w':
750
+ return `width: calc(${val} * var(--qnt))`;
751
+ case 'h':
752
+ return `height: calc(${val} * var(--qnt))`;
753
+ case 'w-pct':
754
+ return `width: ${parseFloat(val) / 10}%`;
755
+ case 'h-pct':
756
+ return `height: ${parseFloat(val) / 10}%`;
757
+ case 'w-f':
758
+ case 'w-full':
759
+ return 'width: 100%';
760
+ case 'h-f':
761
+ case 'h-full':
762
+ return 'height: 100%';
763
+
764
+ // Logical Positioning
765
+ case 'pos':
766
+ if (val === 'center') return 'left: 50%; top: 50%; transform: translate(-50%, -50%)';
767
+ return '';
768
+
769
+ // Category I: Layout & Spacing
770
+ case 'w-full':
771
+ case 'w-f':
772
+ return 'width: 100%';
773
+ case 'h-full':
774
+ case 'h-f':
775
+ return 'height: 100%';
776
+ case 'w':
777
+ if (val === '1000' || val === 'inherit' || val === 'full') return 'width: 100%';
778
+ return `width: calc(${val} * var(--qnt))`;
779
+ case 'h':
780
+ if (val === '1000' || val === 'inherit' || val === 'full') return 'height: 100%';
781
+ return `height: calc(${val} * var(--qnt))`;
782
+ case 'min-w':
783
+ return `min-width: calc(${val} * var(--qnt))`;
784
+ case 'max-w':
785
+ return `max-width: calc(${val} * var(--qnt))`;
786
+ case 'min-h':
787
+ return `min-height: calc(${val} * var(--qnt))`;
788
+ case 'max-h':
789
+ return `max-height: calc(${val} * var(--qnt))`;
790
+ case 'margin':
791
+ case 'm':
792
+ return `margin: calc(${val} * var(--qnt))`;
793
+ case 'mt':
794
+ return `margin-top: calc(${val} * var(--qnt))`;
795
+ case 'mb':
796
+ return `margin-bottom: calc(${val} * var(--qnt))`;
797
+ case 'ml':
798
+ return `margin-left: calc(${val} * var(--qnt))`;
799
+ case 'mr':
800
+ return `margin-right: calc(${val} * var(--qnt))`;
801
+ case 'padding':
802
+ case 'p':
803
+ return `padding: calc(${val} * var(--qnt))`;
804
+ case 'pt':
805
+ return `padding-top: calc(${val} * var(--qnt))`;
806
+ case 'pb':
807
+ return `padding-bottom: calc(${val} * var(--qnt))`;
808
+ case 'pl':
809
+ return `padding-left: calc(${val} * var(--qnt))`;
810
+ case 'pr':
811
+ return `padding-right: calc(${val} * var(--qnt))`;
812
+ case 'px':
813
+ return `padding-left: calc(${val} * var(--qnt)); padding-right: calc(${val} * var(--qnt))`;
814
+ case 'py':
815
+ return `padding-top: calc(${val} * var(--qnt)); padding-bottom: calc(${val} * var(--qnt))`;
816
+ case 'gap':
817
+ return `gap: calc(${val} * var(--qnt))`;
818
+
819
+ // Category B: Backgrounds & Visuals
820
+ case 'bg':
821
+ // Note: Actual palette mapping is baked in by sync script
822
+ return `background-color: ${val}`;
823
+ case 'blur':
824
+ return `backdrop-filter: blur(calc(${val} * var(--qnt)))`;
825
+ case 'shadow':
826
+ const shadows = {
827
+ 'sm': '0 1px 2px 0 rgba(0, 0, 0, 0.05)',
828
+ 'md': '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)',
829
+ 'lg': '0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05)',
830
+ 'xl': '0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04)',
831
+ 'inner': 'inset 0 2px 4px 0 rgba(0, 0, 0, 0.06)',
832
+ 'none': 'none'
833
+ };
834
+ return `box-shadow: ${shadows[val] || val}`;
835
+ case 'opacity':
836
+ const op = parseFloat(val);
837
+ return `opacity: ${op > 1 ? op / 1000 : op}`;
838
+ case 'z':
839
+ case 'z-index':
840
+ case 'depth':
841
+ return `z-index: ${val}`;
842
+
843
+ // Category E: Effects & Filters
844
+ case 'bright':
845
+ return `filter: brightness(${val}%)`;
846
+ case 'contrast':
847
+ return `filter: contrast(${val}%)`;
848
+ case 'grayscale':
849
+ return `filter: grayscale(${val}%)`;
850
+ case 'hue':
851
+ return `filter: hue-rotate(${val}deg)`;
852
+ case 'invert':
853
+ return `filter: invert(${val}%)`;
854
+ case 'saturate':
855
+ return `filter: saturate(${val}%)`;
856
+ case 'sepia':
857
+ return `filter: sepia(${val}%)`;
858
+ case 'blend':
859
+ return `mix-blend-mode: ${val}`;
860
+
861
+ // Category C: Borders & Shapes
862
+ case 'border':
863
+ return `border: calc(${val || 1} * var(--qnt)) solid currentColor`;
864
+ case 'border-t':
865
+ return `border-top: calc(${val || 1} * var(--qnt)) solid currentColor`;
866
+ case 'border-b':
867
+ return `border-bottom: calc(${val || 1} * var(--qnt)) solid currentColor`;
868
+ case 'border-l':
869
+ return `border-left: calc(${val || 1} * var(--qnt)) solid currentColor`;
870
+ case 'border-r':
871
+ return `border-right: calc(${val || 1} * var(--qnt)) solid currentColor`;
872
+ case 'border-color':
873
+ return `border-color: ${val}`;
874
+ case 'rounded':
875
+ if (val === 'full') return 'border-radius: 9999px';
876
+ return `border-radius: calc(${val} * var(--qnt))`;
877
+ case 'rounded-full':
878
+ return 'border-radius: 9999px';
879
+ case 'dev-border':
880
+ return `border: 1px solid #f43f5e; box-sizing: border-box`;
881
+
882
+ // Category F: Fonts & Typography
883
+ case 'text-size':
884
+ return `font-size: calc(${val} * var(--qnt))`;
885
+ case 'font':
886
+ // Note: Actual font mapping is baked in by sync script
887
+ if (val === 'black') return 'font-weight: 900';
888
+ if (val === 'bold') return 'font-weight: 700';
889
+ return `font-weight: ${val}`;
890
+ case 'weight':
891
+ return `font-weight: ${val}`;
892
+ case 'italic':
893
+ return 'font-style: italic';
894
+ case 'text':
895
+ if (['left', 'right', 'center', 'justify'].includes(val)) return `text-align: ${val}`;
896
+ return `color: ${val}`;
897
+ case 'text-color':
898
+ return `color: ${val}`;
899
+ case 'uppercase':
900
+ return 'text-transform: uppercase';
901
+ case 'lowercase':
902
+ return 'text-transform: lowercase';
903
+ case 'underline':
904
+ return 'text-decoration: underline';
905
+ case 'leading':
906
+ return `line-height: ${val}`;
907
+ case 'tracking':
908
+ return `letter-spacing: ${val}em`;
909
+ case 'nowrap':
910
+ return 'white-space: nowrap';
911
+
912
+ // Category D: Display & Flex Logic
913
+ case 'horizontal':
914
+ return 'display: flex; flex-direction: row';
915
+ case 'vertical':
916
+ return 'display: flex; flex-direction: column';
917
+ case 'centered':
918
+ return 'display: flex; align-items: center; justify-content: center';
919
+ case 'v-start':
920
+ return 'align-items: flex-start';
921
+ case 'v-mid':
922
+ case 'items-center':
923
+ return 'align-items: center';
924
+ case 'v-end':
925
+ return 'align-items: flex-end';
926
+ case 'h-start':
927
+ return 'justify-content: flex-start';
928
+ case 'h-mid':
929
+ case 'justify-center':
930
+ return 'justify-content: center';
931
+ case 'h-end':
932
+ return 'justify-content: flex-end';
933
+ case 'spaced':
934
+ case 'justify-between':
935
+ return 'justify-content: space-between';
936
+ case 'around':
937
+ return 'justify-content: space-around';
938
+ case 'evenly':
939
+ return 'justify-content: space-evenly';
940
+ case 'flex-wrap':
941
+ return 'flex-wrap: wrap';
942
+ case 'flex-nowrap':
943
+ return 'flex-wrap: nowrap';
944
+ case 'flex-1':
945
+ return 'flex: 1 1 0%';
946
+ case 'flex-auto':
947
+ return 'flex: 1 1 auto';
948
+ case 'flex-none':
949
+ return 'flex: none';
950
+ case 'order':
951
+ return `order: ${val}`;
952
+ case 'flex':
953
+ if (val === 'col' || val === 'column') return 'display: flex; flex-direction: column';
954
+ return val === 'true' || val === '' || val === 'flex' ? 'display: flex' : `display: ${val}`;
955
+ case 'flex-col':
956
+ return 'display: flex; flex-direction: column';
957
+ case 'hidden':
958
+ return 'display: none';
959
+
960
+ // Category J: Miscellaneous
961
+ case 'visible':
962
+ return 'visibility: visible';
963
+ case 'invisible':
964
+ return 'visibility: hidden';
965
+
966
+ case 'aspect':
967
+ const ratios = {
968
+ 'square': '1 / 1',
969
+ 'video': '16 / 9',
970
+ 'portrait': '3 / 4',
971
+ 'landscape': '4 / 3'
972
+ };
973
+ return `aspect-ratio: ${ratios[val] || val.replace('|', ' / ')}`;
974
+
975
+ case 'contain':
976
+ return 'object-fit: contain';
977
+ case 'cover':
978
+ return 'object-fit: cover';
979
+
980
+ case 'busy':
981
+ return 'cursor: wait';
982
+
983
+ case 'mask':
984
+ return `clip-path: ${val}`;
985
+
986
+ // Category G: Grid & Tables
987
+ case 'grid':
988
+ return 'display: grid';
989
+ case 'cols':
990
+ return `grid-template-columns: ${val}`;
991
+ case 'rows':
992
+ return `grid-template-rows: ${val}`;
993
+ case 'table-fixed':
994
+ return 'table-layout: fixed';
995
+
996
+ // Category H: Interactivity
997
+ case 'ghost':
998
+ return 'pointer-events: none';
999
+ case 'unselectable':
1000
+ return 'user-select: none';
1001
+ case 'ring':
1002
+ return `outline: calc(${val || 2} * var(--qnt)) solid #6366f1`;
1003
+ case 'resize':
1004
+ return `resize: ${val === 'x' ? 'horizontal' : val === 'y' ? 'vertical' : 'both'}; overflow: auto`;
1005
+ case 'collapsed':
1006
+ return 'width: 0; min-width: 0; overflow: hidden';
1007
+ case 'scroll-smooth':
1008
+ return 'scroll-behavior: smooth';
1009
+ case 'scroll':
1010
+ if (val === 'x') return 'overflow-x: auto; overflow-y: hidden';
1011
+ if (val === 'y') return 'overflow-y: auto; overflow-x: hidden';
1012
+ return `overflow: ${val || 'auto'}`;
1013
+ case 'clickable':
1014
+ case 'cursor-pointer':
1015
+ return 'cursor: pointer';
1016
+
1017
+ // Category A: Animations & Transitions
1018
+ case 'smooth':
1019
+ case 'transition-all':
1020
+ return 'transition: all 0.2s ease-in-out';
1021
+ case 'transition-colors':
1022
+ return 'transition: background-color 0.2s, border-color 0.2s';
1023
+ case 'transition-none':
1024
+ return 'transition: none';
1025
+
1026
+ default:
1027
+ // Fallback for raw CSS properties
1028
+ return `${key}: ${val}`;
1029
+ }
1030
+ }
1031
+
1032
+ function toggleDropdown(id) {
1033
+ const dropdown = document.getElementById(id);
1034
+ if (dropdown) {
1035
+ dropdown.classList.toggle('hidden');
1036
+ }
1037
+ }
1038
+
1039
+ return {
1040
+ transpile,
1041
+ setAnchor,
1042
+ registerObject,
1043
+ calculateGrid,
1044
+ startPulse,
1045
+ startObserver,
1046
+ toggleDropdown,
1047
+ version: '0.5.1-hardened'
1048
+ };
1049
+ })();
1050
+
1051
+ // Export for Node/CommonJS if needed, else it stays in global scope
1052
+ if (typeof module !== 'undefined' && module.exports) {
1053
+ module.exports = QSSEngine;
1054
+ }