@everystate/examples 1.0.0 → 1.0.1

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 (152) hide show
  1. package/README.md +15 -0
  2. package/everyState-core/001-counter/README.md +44 -0
  3. package/everyState-core/001-counter/index.html +79 -0
  4. package/everyState-core/002-counter-improved/README.md +44 -0
  5. package/everyState-core/002-counter-improved/index.html +83 -0
  6. package/everyState-core/003-input-reactive/README.md +44 -0
  7. package/everyState-core/003-input-reactive/index.html +68 -0
  8. package/everyState-core/004-computed-state/README.md +45 -0
  9. package/everyState-core/004-computed-state/index.html +83 -0
  10. package/everyState-core/005-conditional-rendering/README.md +42 -0
  11. package/everyState-core/005-conditional-rendering/index.html +68 -0
  12. package/everyState-core/006-list-rendering/README.md +49 -0
  13. package/everyState-core/006-list-rendering/index.html +92 -0
  14. package/everyState-core/007-form-validation/README.md +52 -0
  15. package/everyState-core/007-form-validation/index.html +108 -0
  16. package/everyState-core/008-undo-redo/README.md +70 -0
  17. package/everyState-core/008-undo-redo/index.html +133 -0
  18. package/everyState-core/009-localStorage-side-effects/README.md +72 -0
  19. package/everyState-core/009-localStorage-side-effects/index.html +80 -0
  20. package/everyState-core/010-decoupled-components/README.md +74 -0
  21. package/everyState-core/010-decoupled-components/index.html +117 -0
  22. package/everyState-core/011-async-patterns/README.md +98 -0
  23. package/everyState-core/011-async-patterns/index.html +132 -0
  24. package/everyState-css/001-stateDrivenCSS/index.html +377 -0
  25. package/everyState-css/002-cssV2FullDemo/index.html +630 -0
  26. package/everyState-view/001/counter/index.css +31 -0
  27. package/everyState-view/001/counter/index.html +50 -0
  28. package/everyState-view/002/datatable/index.css +70 -0
  29. package/everyState-view/002/datatable/index.html +118 -0
  30. package/everyState-view/003/todo/index.css +260 -0
  31. package/everyState-view/003/todo/index.html +218 -0
  32. package/everyState-view/003-input-reactive/README.md +44 -0
  33. package/everyState-view/003-input-reactive/index.html +68 -0
  34. package/everyState-view/004/quotesFetcher/index.css +124 -0
  35. package/everyState-view/004/quotesFetcher/index.html +108 -0
  36. package/everyState-view/004_01/quotesFetcher/app.js +32 -0
  37. package/everyState-view/004_01/quotesFetcher/components/appHeader/appSubtitle.js +2 -0
  38. package/everyState-view/004_01/quotesFetcher/components/appHeader/appTitle.js +2 -0
  39. package/everyState-view/004_01/quotesFetcher/components/appHeader.js +9 -0
  40. package/everyState-view/004_01/quotesFetcher/components/historyHeading.js +2 -0
  41. package/everyState-view/004_01/quotesFetcher/components/historyList/histAuthor.js +2 -0
  42. package/everyState-view/004_01/quotesFetcher/components/historyList/histQuote.js +2 -0
  43. package/everyState-view/004_01/quotesFetcher/components/historyList.js +14 -0
  44. package/everyState-view/004_01/quotesFetcher/components/quoteCard/fetchButton.js +2 -0
  45. package/everyState-view/004_01/quotesFetcher/components/quoteCard/quoteAuthor.js +2 -0
  46. package/everyState-view/004_01/quotesFetcher/components/quoteCard/quoteMessage.js +2 -0
  47. package/everyState-view/004_01/quotesFetcher/components/quoteCard/quoteText.js +2 -0
  48. package/everyState-view/004_01/quotesFetcher/components/quoteCard.js +11 -0
  49. package/everyState-view/004_01/quotesFetcher/index.css +124 -0
  50. package/everyState-view/004_01/quotesFetcher/index.html +23 -0
  51. package/everyState-view/004_01/quotesFetcher/store.js +35 -0
  52. package/everyState-view/004_02/quotesFetcher/app.js +20 -0
  53. package/everyState-view/004_02/quotesFetcher/components.js +46 -0
  54. package/everyState-view/004_02/quotesFetcher/index.css +124 -0
  55. package/everyState-view/004_02/quotesFetcher/index.html +23 -0
  56. package/everyState-view/004_02/quotesFetcher/store.js +35 -0
  57. package/everyState-view/004_03/quotesFetcher/actions.js +27 -0
  58. package/everyState-view/004_03/quotesFetcher/app.js +19 -0
  59. package/everyState-view/004_03/quotesFetcher/components.js +28 -0
  60. package/everyState-view/004_03/quotesFetcher/index.css +124 -0
  61. package/everyState-view/004_03/quotesFetcher/index.html +23 -0
  62. package/everyState-view/004_03/quotesFetcher/resolve.js +34 -0
  63. package/everyState-view/004_03/quotesFetcher/store.js +11 -0
  64. package/everyState-view/004_04/quotesFetcher/actions.js +66 -0
  65. package/everyState-view/004_04/quotesFetcher/app.js +24 -0
  66. package/everyState-view/004_04/quotesFetcher/components/archive.js +40 -0
  67. package/everyState-view/004_04/quotesFetcher/components/fetcher.js +29 -0
  68. package/everyState-view/004_04/quotesFetcher/components.js +20 -0
  69. package/everyState-view/004_04/quotesFetcher/index.css +283 -0
  70. package/everyState-view/004_04/quotesFetcher/index.html +24 -0
  71. package/everyState-view/004_04/quotesFetcher/resolve.js +34 -0
  72. package/everyState-view/004_04/quotesFetcher/store.js +21 -0
  73. package/everyState-view/004_04/statedump.json +826 -0
  74. package/everyState-view/004_05/quoteExplorer/actions.js +58 -0
  75. package/everyState-view/004_05/quoteExplorer/app.js +27 -0
  76. package/everyState-view/004_05/quoteExplorer/components.js +83 -0
  77. package/everyState-view/004_05/quoteExplorer/index.css +231 -0
  78. package/everyState-view/004_05/quoteExplorer/index.html +23 -0
  79. package/everyState-view/004_05/quoteExplorer/resolve.js +50 -0
  80. package/everyState-view/004_05/quoteExplorer/store.js +33 -0
  81. package/everyState-view/004_06/quoteExplorer/actions.js +21 -0
  82. package/everyState-view/004_06/quoteExplorer/app.js +44 -0
  83. package/everyState-view/004_06/quoteExplorer/components.js +80 -0
  84. package/everyState-view/004_06/quoteExplorer/derived.js +43 -0
  85. package/everyState-view/004_06/quoteExplorer/index.css +346 -0
  86. package/everyState-view/004_06/quoteExplorer/index.html +25 -0
  87. package/everyState-view/004_06/quoteExplorer/intents.js +44 -0
  88. package/everyState-view/004_06/quoteExplorer/policies.js +25 -0
  89. package/everyState-view/004_06/quoteExplorer/resolve.js +51 -0
  90. package/everyState-view/004_06/quoteExplorer/store.js +44 -0
  91. package/everyState-view/004_07/quoteExplorer/app.js +47 -0
  92. package/everyState-view/004_07/quoteExplorer/components.js +85 -0
  93. package/everyState-view/004_07/quoteExplorer/derived.js +43 -0
  94. package/everyState-view/004_07/quoteExplorer/index.css +346 -0
  95. package/everyState-view/004_07/quoteExplorer/index.html +25 -0
  96. package/everyState-view/004_07/quoteExplorer/intents.js +51 -0
  97. package/everyState-view/004_07/quoteExplorer/policies.js +21 -0
  98. package/everyState-view/004_07/quoteExplorer/resolve.js +39 -0
  99. package/everyState-view/004_07/quoteExplorer/store.js +44 -0
  100. package/everyState-view/004_08/quoteExplorer/app.js +78 -0
  101. package/everyState-view/004_08/quoteExplorer/components.js +85 -0
  102. package/everyState-view/004_08/quoteExplorer/derived.js +43 -0
  103. package/everyState-view/004_08/quoteExplorer/index.css +346 -0
  104. package/everyState-view/004_08/quoteExplorer/index.html +25 -0
  105. package/everyState-view/004_08/quoteExplorer/intents.js +51 -0
  106. package/everyState-view/004_08/quoteExplorer/policies.js +21 -0
  107. package/everyState-view/004_08/quoteExplorer/resolve.js +39 -0
  108. package/everyState-view/004_08/quoteExplorer/store.js +44 -0
  109. package/everyState-view/004_08_V2/app.js +78 -0
  110. package/everyState-view/004_08_V2/components/appDetail.js +8 -0
  111. package/everyState-view/004_08_V2/components/appDetailBar.js +7 -0
  112. package/everyState-view/004_08_V2/components/appDetailBarClose.js +8 -0
  113. package/everyState-view/004_08_V2/components/appDetailBarCount.js +7 -0
  114. package/everyState-view/004_08_V2/components/appDetailBarHeading.js +7 -0
  115. package/everyState-view/004_08_V2/components/appDetailQuotes.js +15 -0
  116. package/everyState-view/004_08_V2/components/appHeader.js +7 -0
  117. package/everyState-view/004_08_V2/components/appHeaderSubtitle.js +7 -0
  118. package/everyState-view/004_08_V2/components/appHeaderTitle.js +7 -0
  119. package/everyState-view/004_08_V2/components/appLog.js +7 -0
  120. package/everyState-view/004_08_V2/components/appLogHeading.js +7 -0
  121. package/everyState-view/004_08_V2/components/appLogList.js +16 -0
  122. package/everyState-view/004_08_V2/components/appSearch.js +7 -0
  123. package/everyState-view/004_08_V2/components/appSearchInput.js +9 -0
  124. package/everyState-view/004_08_V2/components/appStats.js +7 -0
  125. package/everyState-view/004_08_V2/components/appStatsContent.js +8 -0
  126. package/everyState-view/004_08_V2/components/appStatsContentFavcount.js +7 -0
  127. package/everyState-view/004_08_V2/components/appStatsContentText.js +7 -0
  128. package/everyState-view/004_08_V2/components/appStatsToggle.js +8 -0
  129. package/everyState-view/004_08_V2/components/appTags.js +7 -0
  130. package/everyState-view/004_08_V2/components/appTagsLabel.js +7 -0
  131. package/everyState-view/004_08_V2/components/appTagsRow.js +15 -0
  132. package/everyState-view/004_08_V2/components/index.js +59 -0
  133. package/everyState-view/004_08_V2/components/utils/css.js +88 -0
  134. package/everyState-view/004_08_V2/components/utils/elements.js +87 -0
  135. package/everyState-view/004_08_V2/components.js +79 -0
  136. package/everyState-view/004_08_V2/derived.js +43 -0
  137. package/everyState-view/004_08_V2/index.css +350 -0
  138. package/everyState-view/004_08_V2/index.html +25 -0
  139. package/everyState-view/004_08_V2/intents.js +51 -0
  140. package/everyState-view/004_08_V2/policies.js +21 -0
  141. package/everyState-view/004_08_V2/resolve.js +39 -0
  142. package/everyState-view/004_08_V2/store.js +44 -0
  143. package/everyState-view/006/api-datatable/index.css +388 -0
  144. package/everyState-view/006/api-datatable/index.html +355 -0
  145. package/everyState-view/007/apiUsers/index.html +307 -0
  146. package/everyState-view/007-form-validation/README.md +52 -0
  147. package/everyState-view/007-form-validation/index.html +108 -0
  148. package/everyState-view/010-decoupled-components/README.md +74 -0
  149. package/everyState-view/010-decoupled-components/index.html +117 -0
  150. package/everyState-view/index.html +36 -0
  151. package/index.js +0 -5
  152. package/package.json +2 -4
@@ -0,0 +1,630 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>002 Full Demo - @everystate/css</title>
7
+ <style>
8
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
9
+ html, body { height: 100%; }
10
+ body { font-family: system-ui, -apple-system, sans-serif; transition: background 0.3s, color 0.3s; }
11
+ code { font-size: 0.85em; background: rgba(128,128,128,0.15); padding: 0.1em 0.35em; border-radius: 3px; }
12
+ pre code { background: none; padding: 0; }
13
+ </style>
14
+ <script type="importmap">
15
+ {
16
+ "imports": {
17
+ "@everystate/core": "../../../everystate-core/index.js",
18
+ "@everystate/perf": "../../../everystate-perf/index.js"
19
+ }
20
+ }
21
+ </script>
22
+ </head>
23
+ <body>
24
+
25
+ <div class="page">
26
+ <header class="header">
27
+ <h1 class="h1">@everystate/css</h1>
28
+ <p>Style Engine + Design Tokens + Typed CSS + Relational Constraints</p>
29
+ </header>
30
+
31
+ <!-- Section 1: Design System Tokens -->
32
+ <section class="section">
33
+ <h2 class="h2">1. Design System - Reactive Tokens</h2>
34
+ <p class="caption">Change a token, every bound style updates instantly.</p>
35
+ <div class="btn-row">
36
+ <button class="btn primary" onclick="swapTheme('default')">Default</button>
37
+ <button class="btn primary" onclick="swapTheme('ocean')">Ocean</button>
38
+ <button class="btn primary" onclick="swapTheme('forest')">Forest</button>
39
+ <button class="btn primary" onclick="swapTheme('sunset')">Sunset</button>
40
+ </div>
41
+ <div class="btn-row">
42
+ <label>Primary: <input type="color" id="pickPrimary" value="#3b82f6" oninput="ds.setToken('color.primary', this.value)"></label>
43
+ <label>Surface: <input type="color" id="pickSurface" value="#ffffff" oninput="ds.setToken('color.surface', this.value)"></label>
44
+ <label>Danger: <input type="color" id="pickDanger" value="#ef4444" oninput="ds.setToken('color.danger', this.value)"></label>
45
+ </div>
46
+ </section>
47
+
48
+ <!-- Section 2: Cards showing style engine + contrast -->
49
+ <section class="section">
50
+ <h2 class="h2">2. Style Engine + WCAG Contrast</h2>
51
+ <p class="caption">Card text color auto-adjusts for readability when background changes.</p>
52
+ <div class="card-grid">
53
+ <div class="card">
54
+ <h3 class="h3">Primary Card</h3>
55
+ <p class="small">Background bound to <code>color.primary</code>. Text auto-picks light/dark for 4.5:1 contrast.</p>
56
+ </div>
57
+ <div class="card-surface">
58
+ <h3 class="h3">Surface Card</h3>
59
+ <p class="small">Background bound to <code>color.surface</code>. Hover states from style engine paths.</p>
60
+ </div>
61
+ <div class="card-danger">
62
+ <h3 class="h3">Danger Card</h3>
63
+ <p class="small">Background bound to <code>color.danger</code>. Contrast enforced at AA level.</p>
64
+ </div>
65
+ </div>
66
+ </section>
67
+
68
+ <!-- Section 3: Typed CSS -->
69
+ <section class="section">
70
+ <h2 class="h2">3. Typed CSS - Runtime Validation</h2>
71
+ <p class="caption">Try setting invalid values. Check the console for warnings.</p>
72
+ <div class="btn-row">
73
+ <button class="btn primary" onclick="testValid()">Set Valid Padding (1rem)</button>
74
+ <button class="btn danger" onclick="testInvalidPadding()">Set Invalid Padding (5rem)</button>
75
+ <button class="btn danger" onclick="testInvalidColor()">Set Invalid Color ('banana')</button>
76
+ <button class="btn danger" onclick="testInvalidEnum()">Set Invalid Display ('table')</button>
77
+ </div>
78
+ <pre class="log" id="typedLog"></pre>
79
+ </section>
80
+
81
+ <!-- Section 4: Relational CSS -->
82
+ <section class="section">
83
+ <h2 class="h2">4. Relational CSS - Modular Type Scale</h2>
84
+ <p class="caption">All font sizes scale proportionally from one base value.</p>
85
+ <div class="btn-row">
86
+ <button class="btn primary" onclick="setBaseFont('0.875rem')">Small (0.875rem)</button>
87
+ <button class="btn primary" onclick="setBaseFont('1rem')">Medium (1rem)</button>
88
+ <button class="btn primary" onclick="setBaseFont('1.25rem')">Large (1.25rem)</button>
89
+ <button class="btn primary" onclick="setBaseFont('1.5rem')">XL (1.5rem)</button>
90
+ </div>
91
+ <div class="type-demo">
92
+ <p class="h1">Heading 1 (2×)</p>
93
+ <p class="h2">Heading 2 (1.5×)</p>
94
+ <p class="h3">Heading 3 (1.25×)</p>
95
+ <p class="body">Body text (1×)</p>
96
+ <p class="small">Small text (0.875×)</p>
97
+ </div>
98
+ </section>
99
+
100
+ <!-- Section 5: Inspector -->
101
+ <section class="section">
102
+ <h2 class="h2">5. State Inspector</h2>
103
+ <p class="caption">Tokens, bindings, relations, and violations - all inspectable.</p>
104
+ <div class="btn-row">
105
+ <button class="btn primary" onclick="showInspector('tokens')">Tokens</button>
106
+ <button class="btn primary" onclick="showInspector('bindings')">Bindings</button>
107
+ <button class="btn primary" onclick="showInspector('relations')">Relations</button>
108
+ <button class="btn primary" onclick="showInspector('violations')">Violations</button>
109
+ <button class="btn primary" onclick="showInspector('schema')">Schema</button>
110
+ </div>
111
+ <pre class="log" id="inspector"></pre>
112
+ </section>
113
+
114
+ <!-- Section 6: Composable Aliases -->
115
+ <section class="section">
116
+ <h2 class="h2">6. Composable Aliases</h2>
117
+ <p class="caption">The binding system is powerful but verbose. Aliases compose dot-paths into succinct shorthand.</p>
118
+ <div class="btn-row">
119
+ <button class="btn primary" onclick="showAliasComparison()">Show Before/After</button>
120
+ <button class="btn primary" onclick="runAliasDemo()">Run Live Demo</button>
121
+ </div>
122
+ <pre class="log" id="aliasLog"></pre>
123
+ </section>
124
+
125
+ <!-- Section 7: Event Sequence Tests -->
126
+ <section class="section">
127
+ <h2 class="h2">7. Event Sequence Tests</h2>
128
+ <div class="btn-row">
129
+ <button class="btn primary" onclick="runTests()">Run Tests</button>
130
+ </div>
131
+ <pre class="log" id="testLog"></pre>
132
+ </section>
133
+ </div>
134
+
135
+ <script type="module">
136
+ import { createEveryState } from '@everystate/core';
137
+ import { createPerfMonitor, mountOverlay } from '@everystate/perf';
138
+
139
+ // ============================================================
140
+ // Inline Style Engine
141
+ // ============================================================
142
+ const CSS_PROPERTIES = new Set(['accentColor','alignContent','alignItems','alignSelf','background','backgroundColor','backgroundImage','backgroundPosition','backgroundRepeat','backgroundSize','border','borderBottom','borderBottomColor','borderBottomLeftRadius','borderBottomRightRadius','borderBottomStyle','borderBottomWidth','borderCollapse','borderColor','borderLeft','borderLeftColor','borderLeftStyle','borderLeftWidth','borderRadius','borderRight','borderRightColor','borderRightStyle','borderRightWidth','borderSpacing','borderStyle','borderTop','borderTopColor','borderTopLeftRadius','borderTopRightRadius','borderTopStyle','borderTopWidth','borderWidth','bottom','boxShadow','boxSizing','caretColor','clear','clipPath','color','columnCount','columnGap','columnRule','columns','content','cursor','direction','display','fill','filter','flex','flexBasis','flexDirection','flexFlow','flexGrow','flexShrink','flexWrap','float','font','fontFamily','fontFeatureSettings','fontSize','fontStyle','fontVariant','fontWeight','gap','gridAutoColumns','gridAutoFlow','gridAutoRows','gridColumn','gridColumnEnd','gridColumnGap','gridColumnStart','gridGap','gridRow','gridRowEnd','gridRowGap','gridRowStart','gridTemplateAreas','gridTemplateColumns','gridTemplateRows','height','isolation','justifyContent','justifyItems','justifySelf','left','letterSpacing','lineHeight','listStyle','listStylePosition','listStyleType','margin','marginBottom','marginLeft','marginRight','marginTop','maxHeight','maxWidth','minHeight','minWidth','mixBlendMode','objectFit','objectPosition','opacity','order','outline','outlineColor','outlineOffset','outlineStyle','outlineWidth','overflow','overflowWrap','overflowX','overflowY','padding','paddingBottom','paddingLeft','paddingRight','paddingTop','perspective','placeContent','placeItems','placeSelf','pointerEvents','position','resize','right','rotate','rowGap','scale','scrollBehavior','scrollMargin','scrollPadding','stroke','strokeDasharray','strokeDashoffset','strokeLinecap','strokeLinejoin','strokeOpacity','strokeWidth','tableLayout','textAlign','textDecoration','textDecorationColor','textDecorationLine','textDecorationStyle','textIndent','textOverflow','textShadow','textTransform','top','transform','transformOrigin','transition','transitionDelay','transitionDuration','transitionProperty','transitionTimingFunction','userSelect','verticalAlign','visibility','whiteSpace','width','willChange','wordBreak','wordSpacing','writingMode','zIndex']);
143
+ const PSEUDO_CLASSES = new Map([['hover',':hover'],['focus',':focus'],['active',':active'],['disabled',':disabled'],['visited',':visited'],['checked',':checked'],['empty',':empty'],['invalid',':invalid'],['valid',':valid'],['firstChild',':first-child'],['lastChild',':last-child'],['focusWithin',':focus-within'],['focusVisible',':focus-visible']]);
144
+ const PSEUDO_ELEMENTS = new Map([['before','::before'],['after','::after'],['placeholder','::placeholder'],['selection','::selection']]);
145
+ function camelToKebab(s) { return s.replace(/([A-Z])/g, '-$1').toLowerCase(); }
146
+
147
+ function createStyleEngine(store, { namespace = 'css', id = 'everystate-css' } = {}) {
148
+ const el = document.createElement('style'); el.id = id; document.head.appendChild(el);
149
+ const sheet = el.sheet; const ruleMap = new Map();
150
+ function getOrCreate(sel) { if (!ruleMap.has(sel)) { const i = sheet.insertRule(`${sel} {}`, sheet.cssRules.length); ruleMap.set(sel, sheet.cssRules[i]); } return ruleMap.get(sel); }
151
+ function apply(sel, prop, val) { getOrCreate(sel).style.setProperty(camelToKebab(prop), val); }
152
+ function parse(fullPath) {
153
+ const path = fullPath.startsWith(namespace+'.') ? fullPath.slice(namespace.length+1) : fullPath;
154
+ const segs = path.split('.'); const prop = segs[segs.length-1];
155
+ if (!CSS_PROPERTIES.has(prop)) return null;
156
+ const parts = []; let pseudo = '';
157
+ for (let i = 0; i < segs.length-1; i++) {
158
+ const s = segs[i];
159
+ if (PSEUDO_CLASSES.has(s)) pseudo += PSEUDO_CLASSES.get(s);
160
+ else if (PSEUDO_ELEMENTS.has(s)) pseudo += PSEUDO_ELEMENTS.get(s);
161
+ else { if (pseudo) { parts[parts.length-1] += pseudo; pseudo = ''; } parts.push('.'+s); }
162
+ }
163
+ return { selector: (parts.length ? parts.join(' ') : ':root') + pseudo, prop };
164
+ }
165
+ function process(p, v) {
166
+ if (typeof v === 'object' && v !== null) { walk(p, v); return; }
167
+ const r = parse(p); if (r) apply(r.selector, r.prop, String(v));
168
+ }
169
+ function walk(prefix, obj) { for (const [k,v] of Object.entries(obj)) { const p = `${prefix}.${k}`; if (typeof v === 'object' && v !== null) walk(p,v); else process(p,v); } }
170
+ store.subscribe(`${namespace}.*`, ({ path, value }) => process(path, value));
171
+ return { apply, parse };
172
+ }
173
+
174
+ // ============================================================
175
+ // Inline Design System
176
+ // ============================================================
177
+ function createDesignSystem(store, { tokens = {}, namespace = 'tokens' } = {}) {
178
+ const bindings = new Map(); const unsubs = new Map();
179
+ function setDeep(st, pre, obj) { if (typeof obj === 'object' && obj !== null && !Array.isArray(obj)) { for (const [k,v] of Object.entries(obj)) setDeep(st, `${pre}.${k}`, v); } else st.set(pre, obj); }
180
+ setDeep(store, namespace, tokens);
181
+ function full(tp) { return tp.startsWith(namespace+'.') ? tp : `${namespace}.${tp}`; }
182
+ function ensure(tp) {
183
+ if (unsubs.has(tp)) return;
184
+ unsubs.set(tp, store.subscribe(full(tp), (val) => { const t = bindings.get(tp); if (t) for (const sp of t) store.set(sp, val); }));
185
+ }
186
+ return {
187
+ bind(sp, tp) {
188
+ if (!bindings.has(tp)) bindings.set(tp, new Set());
189
+ bindings.get(tp).add(sp); ensure(tp);
190
+ const v = store.get(full(tp)); if (v !== undefined) store.set(sp, v);
191
+ return () => { bindings.get(tp)?.delete(sp); };
192
+ },
193
+ bindAll(map) { const u = []; for (const [s,t] of Object.entries(map)) u.push(this.bind(s,t)); return () => u.forEach(f=>f()); },
194
+ setToken(tp, v) { store.set(full(tp), v); },
195
+ setTokens(tree) { setDeep(store, namespace, tree); },
196
+ getToken(tp) { return store.get(full(tp)); },
197
+ getAllTokens() { return store.get(namespace); },
198
+ getBindings() { const r = {}; for (const [t,s] of bindings) r[t] = [...s]; return r; },
199
+ destroy() { for (const u of unsubs.values()) u(); unsubs.clear(); bindings.clear(); },
200
+ };
201
+ }
202
+
203
+ // ============================================================
204
+ // Inline Typed CSS
205
+ // ============================================================
206
+ const COLOR_RE = /^(#([0-9a-f]{3,8})|rgb(a)?\(|hsl(a)?\(|transparent|currentColor|inherit|initial|unset|var\()/i;
207
+ const COLOR_NAMES = new Set(['black','white','red','green','blue','yellow','orange','purple','pink','gray','grey','cyan','magenta','brown','olive','navy','teal','maroon','aqua','lime','silver','fuchsia']);
208
+ const LENGTH_RE = /^(-?\d+(\.\d+)?)(px|rem|em|%|vh|vw|vmin|vmax|ch|ex|cm|mm|in|pt|pc)$/;
209
+ const LEN_PX = { px:1, rem:16, em:16, pt:1.333, cm:37.795, mm:3.7795, 'in':96 };
210
+ function lenToPx(v) { const m = String(v).match(LENGTH_RE); if (!m) return null; return parseFloat(m[1]) * (LEN_PX[m[3]]||0); }
211
+
212
+ function createTypedCSS(store, schema = {}, { namespace = 'css', mode = 'warn', onViolation = null } = {}) {
213
+ store.set('schema', JSON.parse(JSON.stringify(schema)));
214
+ const violations = [];
215
+ function report(c, p, msg) {
216
+ const e = { component:c, property:p, message:msg, timestamp:Date.now() }; violations.push(e);
217
+ if (violations.length > 200) violations.shift();
218
+ const fmt = `[typed-css] ${c}.${p}: ${msg}`;
219
+ if (onViolation) onViolation(e); else if (mode==='warn') console.warn(fmt); else if (mode==='error') console.error(fmt);
220
+ }
221
+ const V = {
222
+ color(v) { const s = v.trim().toLowerCase(); return (COLOR_RE.test(s)||COLOR_NAMES.has(s)) ? null : `'${v}' is not a valid color.`; },
223
+ length(v, c) {
224
+ const parts = v.trim().split(/\s+/);
225
+ if (!parts.every(p => LENGTH_RE.test(p)||p==='0'||p==='auto'||p==='inherit'||/^var\(/.test(p))) return `'${v}' is not a valid length.`;
226
+ if (c.min||c.max) { const px = lenToPx(v); if (px!==null) { if (c.min) { const mp = lenToPx(c.min); if (mp!==null&&px<mp) return `'${v}' is below minimum '${c.min}'.`; } if (c.max) { const mp = lenToPx(c.max); if (mp!==null&&px>mp) return `'${v}' exceeds maximum '${c.max}'.`; } } }
227
+ return null;
228
+ },
229
+ enum(v, c) { return c.values?.includes(v) ? null : `'${v}' not allowed. Expected: ${c.values?.join(', ')}.`; },
230
+ shadow(v, c) { if (c.maxLayers && v.split(',').length > c.maxLayers) return `Shadow has ${v.split(',').length} layers, max ${c.maxLayers}.`; return null; },
231
+ string() { return null; },
232
+ };
233
+ const unsub = store.subscribe(`${namespace}.*`, ({ path, value }) => {
234
+ if (typeof value === 'object') return;
235
+ const rel = path.startsWith(namespace+'.') ? path.slice(namespace.length+1) : path;
236
+ const segs = rel.split('.'); if (segs.length < 2) return;
237
+ const comp = segs[0]; const cs = schema[comp]; if (!cs) return;
238
+ const prop = segs[segs.length-1]; const con = cs[prop];
239
+ if (!con) { report(comp, prop, `Property not in schema. Allowed: ${Object.keys(cs).join(', ')}.`); return; }
240
+ const vf = V[con.type]; if (!vf) return;
241
+ const err = vf(String(value), con); if (err) report(comp, prop, err);
242
+ });
243
+ return {
244
+ validate(c, p, v) { const cs = schema[c]; if (!cs) return {valid:true,error:null}; const con = cs[p]; if (!con) return {valid:false,error:`'${p}' not in schema.`}; const vf = V[con.type]; if (!vf) return {valid:true,error:null}; const e = vf(String(v),con); return {valid:!e,error:e}; },
245
+ defineComponent(c, s) { schema[c] = s; store.set(`schema.${c}`, JSON.parse(JSON.stringify(s))); },
246
+ getSchema(c) { return c ? store.get(`schema.${c}`) : store.get('schema'); },
247
+ getViolations(n=50) { return violations.slice(-n); },
248
+ clearViolations() { violations.length = 0; },
249
+ destroy() { unsub(); },
250
+ };
251
+ }
252
+
253
+ // ============================================================
254
+ // Inline Relational CSS
255
+ // ============================================================
256
+ function hexToRgb(h) { h=h.replace('#',''); let r,g,b; if (h.length===3){r=parseInt(h[0]+h[0],16);g=parseInt(h[1]+h[1],16);b=parseInt(h[2]+h[2],16);} else if(h.length>=6){r=parseInt(h.substring(0,2),16);g=parseInt(h.substring(2,4),16);b=parseInt(h.substring(4,6),16);} else return null; return [r,g,b]; }
257
+ function parseColor(v) { if (!v||typeof v!=='string') return null; v=v.trim(); if (v.startsWith('#')) return hexToRgb(v); const m=v.match(/rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/); if (m) return [+m[1],+m[2],+m[3]]; const n={white:[255,255,255],black:[0,0,0],red:[255,0,0],green:[0,128,0],blue:[0,0,255]}; return n[v.toLowerCase()]||null; }
258
+ function relLum([r,g,b]) { const f=c=>{c/=255;return c<=0.03928?c/12.92:Math.pow((c+0.055)/1.055,2.4);}; return 0.2126*f(r)+0.7152*f(g)+0.0722*f(b); }
259
+ function contrast(c1,c2) { const l1=relLum(c1),l2=relLum(c2); return (Math.max(l1,l2)+0.05)/(Math.min(l1,l2)+0.05); }
260
+ function parseLen(v) { if (typeof v!=='string') return null; const m=v.trim().match(LENGTH_RE); return m?{value:parseFloat(m[1]),unit:m[3]}:null; }
261
+ function fmtLen(n,u) { return `${Math.round(n*10000)/10000}${u}`; }
262
+
263
+ function createRelationalCSS(store) {
264
+ const unsubs = []; const relations = [];
265
+ return {
266
+ derive(target, { ref, multiply=1, add=0 } = {}) {
267
+ function compute() { const s=store.get(ref); if (s==null) return; const p=parseLen(String(s)); if (p) store.set(target, fmtLen(p.value*multiply+add, p.unit)); else if (!isNaN(parseFloat(s))) store.set(target, String(parseFloat(s)*multiply+add)); }
268
+ compute(); unsubs.push(store.subscribe(ref, compute)); relations.push({type:'derive',target,source:ref,multiply,add});
269
+ },
270
+ scale(base, targets) {
271
+ const entries = Object.entries(targets);
272
+ function compute() { const s=store.get(base); if (s==null) return; const p=parseLen(String(s)); if (!p) return; for (const [t,f] of entries) store.set(t, fmtLen(p.value*f, p.unit)); }
273
+ compute(); unsubs.push(store.subscribe(base, compute)); relations.push({type:'scale',base,targets:{...targets}});
274
+ },
275
+ contrast(target, { against, light='#ffffff', dark='#1e293b', minRatio=4.5 } = {}) {
276
+ function compute() { const bg=store.get(against); if (!bg) return; const bgR=parseColor(String(bg)); if (!bgR) return; const lR=parseColor(light),dR=parseColor(dark); if (!lR||!dR) return; const lr=contrast(lR,bgR),dr=contrast(dR,bgR); if (lr>=minRatio&&dr>=minRatio) store.set(target,lr>=dr?light:dark); else if (lr>=minRatio) store.set(target,light); else if (dr>=minRatio) store.set(target,dark); else { store.set(target,lr>=dr?light:dark); console.warn(`[relational-css] contrast(${target}): best ratio ${Math.max(lr,dr).toFixed(2)}:1 < ${minRatio}:1`); } }
277
+ compute(); unsubs.push(store.subscribe(against, compute)); relations.push({type:'contrast',target,against,light,dark,minRatio});
278
+ },
279
+ clamp(target, { ref, min, max } = {}) {
280
+ const minP=parseLen(min),maxP=parseLen(max);
281
+ function compute() { const s=store.get(ref); if (s==null) return; const p=parseLen(String(s)); if (!p){store.set(target,s);return;} let v=p.value; if(minP&&p.unit===minP.unit) v=Math.max(v,minP.value); if(maxP&&p.unit===maxP.unit) v=Math.min(v,maxP.value); store.set(target,fmtLen(v,p.unit)); }
282
+ compute(); unsubs.push(store.subscribe(ref, compute)); relations.push({type:'clamp',target,ref,min,max});
283
+ },
284
+ getRelations() { return [...relations]; },
285
+ destroy() { unsubs.forEach(u=>u()); unsubs.length=0; relations.length=0; },
286
+ };
287
+ }
288
+
289
+ // ============================================================
290
+ // BOOT: store -> perf -> engine -> subscriptions (correct order)
291
+ // ============================================================
292
+ const store = createEveryState();
293
+
294
+ // Perf monitor wraps store BEFORE any subscriptions
295
+ const perf = createPerfMonitor(store);
296
+ mountOverlay(perf, document.body);
297
+
298
+ // Fullscreen body styles
299
+ function syncBody() {
300
+ const bg = store.get('css.page.background');
301
+ const fg = store.get('css.page.color');
302
+ if (bg) document.body.style.background = bg;
303
+ if (fg) document.body.style.color = fg;
304
+ }
305
+ store.subscribe('css.*', ({ path }) => {
306
+ if (path === 'css.page.background' || path === 'css.page.color' || path === 'css.page') syncBody();
307
+ });
308
+ const engine = createStyleEngine(store);
309
+
310
+ // ---- Design System ----
311
+ const ds = createDesignSystem(store, {
312
+ tokens: {
313
+ color: { primary: '#3b82f6', danger: '#ef4444', surface: '#ffffff', text: '#1e293b', muted: '#64748b' },
314
+ spacing: { xs: '0.25rem', sm: '0.5rem', md: '1rem', lg: '1.5rem', xl: '2rem' },
315
+ radius: { sm: '0.25rem', md: '0.5rem', lg: '0.75rem' },
316
+ font: { base: '1rem' },
317
+ shadow: { sm: '0 1px 2px rgba(0,0,0,0.05)', md: '0 4px 6px rgba(0,0,0,0.1)', lg: '0 10px 15px rgba(0,0,0,0.15)' },
318
+ },
319
+ });
320
+
321
+ // ---- Relational CSS ----
322
+ const rel = createRelationalCSS(store);
323
+
324
+ // Modular type scale from tokens.font.base
325
+ rel.scale('tokens.font.base', {
326
+ 'css.h1.fontSize': 2.0,
327
+ 'css.h2.fontSize': 1.5,
328
+ 'css.h3.fontSize': 1.25,
329
+ 'css.body.fontSize': 1,
330
+ 'css.small.fontSize': 0.875,
331
+ });
332
+
333
+ // Header padding is 2x section padding
334
+ rel.derive('css.header.paddingBottom', { ref: 'css.section.padding', multiply: 1.5 });
335
+
336
+ // WCAG contrast enforcement on cards
337
+ rel.contrast('css.card.color', { against: 'css.card.background', light: '#ffffff', dark: '#1e293b', minRatio: 4.5 });
338
+ rel.contrast('css.card-surface.color', { against: 'css.card-surface.background', light: '#ffffff', dark: '#1e293b', minRatio: 4.5 });
339
+ rel.contrast('css.card-danger.color', { against: 'css.card-danger.background', light: '#ffffff', dark: '#1e293b', minRatio: 4.5 });
340
+
341
+ // ---- Typed CSS ----
342
+ const typed = createTypedCSS(store, {
343
+ btn: {
344
+ background: { type: 'color' },
345
+ color: { type: 'color' },
346
+ padding: { type: 'length', min: '0.25rem', max: '3rem' },
347
+ borderRadius: { type: 'length' },
348
+ fontSize: { type: 'length', min: '0.75rem', max: '2rem' },
349
+ display: { type: 'enum', values: ['flex', 'inline-flex', 'block', 'inline-block', 'none'] },
350
+ },
351
+ card: {
352
+ background: { type: 'color' },
353
+ color: { type: 'color' },
354
+ padding: { type: 'length' },
355
+ borderRadius: { type: 'length' },
356
+ boxShadow: { type: 'shadow', maxLayers: 3 },
357
+ },
358
+ }, {
359
+ onViolation(entry) {
360
+ const log = document.getElementById('typedLog');
361
+ const line = `⚠️ ${entry.component}.${entry.property}: ${entry.message}`;
362
+ log.textContent = line + '\n' + log.textContent;
363
+ }
364
+ });
365
+
366
+ // ---- Base styles via engine ----
367
+ store.set('css.page', { maxWidth: '850px', margin: '0 auto', padding: '2rem' });
368
+ store.set('css.header', { padding: '1.5rem 0', marginBottom: '1rem' });
369
+ store.set('css.section', { marginBottom: '2rem', padding: '1rem' });
370
+ store.set('css.caption', { marginBottom: '0.75rem', opacity: '0.7' });
371
+ store.set('css.h1', { fontWeight: '700', marginBottom: '0.5rem' });
372
+ store.set('css.h2', { fontWeight: '600', marginBottom: '0.5rem' });
373
+ store.set('css.h3', { fontWeight: '600', marginBottom: '0.25rem' });
374
+ store.set('css.body', { lineHeight: '1.6' });
375
+ store.set('css.small', { lineHeight: '1.5' });
376
+
377
+ // Button styles
378
+ store.set('css.btn', { padding: '0.5rem 1rem', borderRadius: '0.5rem', border: '1px solid rgba(128,128,128,0.2)', cursor: 'pointer', transition: 'background 0.2s, transform 0.1s', display: 'inline-block', fontWeight: '500' });
379
+ store.set('css.btn.active.transform', 'scale(0.97)');
380
+ store.set('css.btn-row', { display: 'flex', gap: '0.5rem', flexWrap: 'wrap', marginBottom: '0.75rem', alignItems: 'center' });
381
+ store.set('css.btn-row.label', { display: 'flex', gap: '0.35rem', alignItems: 'center', fontSize: '0.9rem' });
382
+
383
+ // Card grid
384
+ store.set('css.card-grid', { display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))', gap: '1rem' });
385
+
386
+ // Card base styles
387
+ const cardBase = { padding: '1.25rem', borderRadius: '0.75rem', transition: 'box-shadow 0.2s, transform 0.2s', cursor: 'pointer' };
388
+ store.set('css.card', { ...cardBase, boxShadow: '0 2px 8px rgba(0,0,0,0.1)' });
389
+ store.set('css.card.hover.transform', 'translateY(-2px)');
390
+ store.set('css.card.hover.boxShadow', '0 8px 20px rgba(0,0,0,0.15)');
391
+ store.set('css.card-surface', { ...cardBase, boxShadow: '0 2px 8px rgba(0,0,0,0.08)' });
392
+ store.set('css.card-surface.hover.transform', 'translateY(-2px)');
393
+ store.set('css.card-danger', { ...cardBase, boxShadow: '0 2px 8px rgba(0,0,0,0.1)' });
394
+ store.set('css.card-danger.hover.transform', 'translateY(-2px)');
395
+
396
+ // Log/pre styles
397
+ store.set('css.log', { padding: '0.75rem', borderRadius: '0.5rem', fontSize: '0.8rem', overflow: 'auto', maxHeight: '200px', whiteSpace: 'pre-wrap', fontFamily: 'monospace', border: '1px solid rgba(128,128,128,0.2)' });
398
+
399
+ // Type demo
400
+ store.set('css.type-demo', { padding: '1rem', borderRadius: '0.5rem', border: '1px solid rgba(128,128,128,0.2)' });
401
+
402
+ // ---- Bind tokens to styles ----
403
+ // Page
404
+ ds.bind('css.page.background', 'color.surface');
405
+ ds.bind('css.page.color', 'color.text');
406
+ // Primary button
407
+ ds.bind('css.btn.primary.background', 'color.primary');
408
+ ds.bind('css.btn.primary.color', 'color.surface');
409
+ // Danger button
410
+ ds.bind('css.btn.danger.background', 'color.danger');
411
+ ds.bind('css.btn.danger.color', 'color.surface');
412
+ // Cards bound to tokens
413
+ ds.bind('css.card.background', 'color.primary');
414
+ ds.bind('css.card-surface.background', 'color.surface');
415
+ ds.bind('css.card-danger.background', 'color.danger');
416
+ // Log
417
+ ds.bind('css.log.background', 'color.surface');
418
+ ds.bind('css.log.color', 'color.text');
419
+ syncBody();
420
+
421
+ // ---- Theme presets ----
422
+ const themePresets = {
423
+ default: { color: { primary: '#3b82f6', danger: '#ef4444', surface: '#ffffff', text: '#1e293b', muted: '#64748b' } },
424
+ ocean: { color: { primary: '#0891b2', danger: '#f97316', surface: '#0c4a6e', text: '#e0f2fe', muted: '#7dd3fc' } },
425
+ forest: { color: { primary: '#16a34a', danger: '#dc2626', surface: '#14532d', text: '#dcfce7', muted: '#86efac' } },
426
+ sunset: { color: { primary: '#e11d48', danger: '#9333ea', surface: '#1c1917', text: '#fef2f2', muted: '#fda4af' } },
427
+ };
428
+
429
+ // ---- Expose to buttons ----
430
+ window.ds = ds;
431
+ window.typed = typed;
432
+ window.rel = rel;
433
+ window.store = store;
434
+
435
+ window.swapTheme = function(name) {
436
+ ds.setTokens(themePresets[name]);
437
+ syncBody();
438
+ // Update color pickers
439
+ const t = themePresets[name].color;
440
+ document.getElementById('pickPrimary').value = t.primary;
441
+ document.getElementById('pickSurface').value = t.surface;
442
+ document.getElementById('pickDanger').value = t.danger;
443
+ };
444
+
445
+ window.setBaseFont = function(size) {
446
+ ds.setToken('font.base', size);
447
+ };
448
+
449
+ window.testValid = function() { store.set('css.btn.padding', '1rem'); };
450
+ window.testInvalidPadding = function() { store.set('css.btn.padding', '5rem'); };
451
+ window.testInvalidColor = function() { store.set('css.btn.background', 'banana'); };
452
+ window.testInvalidEnum = function() { store.set('css.btn.display', 'table'); };
453
+
454
+ window.showInspector = function(what) {
455
+ const el = document.getElementById('inspector');
456
+ switch (what) {
457
+ case 'tokens': el.textContent = JSON.stringify(ds.getAllTokens(), null, 2); break;
458
+ case 'bindings': el.textContent = JSON.stringify(ds.getBindings(), null, 2); break;
459
+ case 'relations': el.textContent = JSON.stringify(rel.getRelations(), null, 2); break;
460
+ case 'violations': el.textContent = JSON.stringify(typed.getViolations(), null, 2); break;
461
+ case 'schema': el.textContent = JSON.stringify(typed.getSchema(), null, 2); break;
462
+ }
463
+ };
464
+
465
+ // ============================================================
466
+ // Composable Aliases
467
+ // ============================================================
468
+
469
+ // Property aliases: short names for common CSS properties
470
+ const P = { bg: 'background', fg: 'color', pad: 'padding', rad: 'borderRadius', fs: 'fontSize', fw: 'fontWeight', sh: 'boxShadow', dis: 'display', op: 'opacity', cur: 'cursor', tr: 'transition' };
471
+
472
+ // bindMany: one token -> many CSS paths
473
+ function bindMany(token, cssPaths) {
474
+ return cssPaths.map(p => ds.bind(p, token));
475
+ }
476
+
477
+ // component: define all token bindings for a component in one call
478
+ function component(prefix, map) {
479
+ const unsubs = [];
480
+ for (const [alias, token] of Object.entries(map)) {
481
+ const prop = P[alias] || alias;
482
+ unsubs.push(ds.bind(`css.${prefix}.${prop}`, token));
483
+ }
484
+ return () => unsubs.forEach(u => u());
485
+ }
486
+
487
+ window.showAliasComparison = function() {
488
+ const el = document.getElementById('aliasLog');
489
+ el.textContent = `VERBOSE (14 lines):
490
+ ds.bind('css.page.background', 'color.surface');
491
+ ds.bind('css.page.color', 'color.text');
492
+ ds.bind('css.btn.primary.background', 'color.primary');
493
+ ds.bind('css.btn.primary.color', 'color.surface');
494
+ ds.bind('css.btn.danger.background', 'color.danger');
495
+ ds.bind('css.btn.danger.color', 'color.surface');
496
+ ds.bind('css.card.background', 'color.primary');
497
+ ds.bind('css.card-surface.background', 'color.surface');
498
+ ds.bind('css.card-danger.background', 'color.danger');
499
+ ds.bind('css.log.background', 'color.surface');
500
+ ds.bind('css.log.color', 'color.text');
501
+
502
+ COMPOSABLE (6 lines):
503
+ const P = { bg: 'background', fg: 'color' };
504
+
505
+ bindMany('color.surface', [
506
+ 'css.page.background', 'css.btn.primary.color',
507
+ 'css.btn.danger.color', 'css.card-surface.background',
508
+ 'css.log.background'
509
+ ]);
510
+ bindMany('color.text', ['css.page.color', 'css.log.color']);
511
+ bindMany('color.primary', ['css.btn.primary.background', 'css.card.background']);
512
+ bindMany('color.danger', ['css.btn.danger.background', 'css.card-danger.background']);
513
+
514
+ COMPONENT SHORTHAND (4 lines):
515
+ component('btn.primary', { bg: 'color.primary', fg: 'color.surface' });
516
+ component('btn.danger', { bg: 'color.danger', fg: 'color.surface' });
517
+ component('page', { bg: 'color.surface', fg: 'color.text' });
518
+ component('log', { bg: 'color.surface', fg: 'color.text' });`;
519
+ };
520
+
521
+ window.runAliasDemo = function() {
522
+ const el = document.getElementById('aliasLog');
523
+ const results = [];
524
+
525
+ // Demo: use component() to rebind with aliases
526
+ component('btn.primary', { bg: 'color.primary', fg: 'color.surface' });
527
+ results.push('component("btn.primary", { bg: "color.primary", fg: "color.surface" })');
528
+ results.push(' -> css.btn.primary.background = ' + store.get('css.btn.primary.background'));
529
+ results.push(' -> css.btn.primary.color = ' + store.get('css.btn.primary.color'));
530
+
531
+ // Demo: bindMany
532
+ bindMany('color.surface', ['css.page.background', 'css.log.background']);
533
+ results.push('\nbindMany("color.surface", ["css.page.background", "css.log.background"])');
534
+ results.push(' -> css.page.background = ' + store.get('css.page.background'));
535
+ results.push(' -> css.log.background = ' + store.get('css.log.background'));
536
+
537
+ // Demo: change a token and show propagation
538
+ const before = ds.getToken('color.primary');
539
+ ds.setToken('color.primary', '#8b5cf6');
540
+ results.push('\nds.setToken("color.primary", "#8b5cf6")');
541
+ results.push(' -> css.btn.primary.background = ' + store.get('css.btn.primary.background'));
542
+ results.push(' -> css.card.background = ' + store.get('css.card.background'));
543
+ ds.setToken('color.primary', before);
544
+ results.push(' (restored to ' + before + ')');
545
+
546
+ el.textContent = results.join('\n');
547
+ };
548
+
549
+ // ============================================================
550
+ // Event Sequence Tests
551
+ // ============================================================
552
+ window.runTests = function() {
553
+ const log = document.getElementById('testLog');
554
+ const results = [];
555
+ let pass = 0, fail = 0;
556
+
557
+ function assert(label, condition) {
558
+ if (condition) { pass++; results.push(' OK ' + label); }
559
+ else { fail++; results.push(' FAIL ' + label); }
560
+ }
561
+
562
+ // Test 1: design system token propagation
563
+ {
564
+ const before = store.get('tokens.color.primary');
565
+ ds.setToken('color.primary', '#ff0000');
566
+ assert('setToken updates store', store.get('tokens.color.primary') === '#ff0000');
567
+ ds.setToken('color.primary', before);
568
+ }
569
+
570
+ // Test 2: token binding fires style update
571
+ {
572
+ const origBg = store.get('css.card.background');
573
+ ds.setToken('color.primary', '#00ff00');
574
+ const newBg = store.get('css.card.background');
575
+ assert('token bind propagates to css path', newBg === '#00ff00');
576
+ ds.setToken('color.primary', origBg || '#3b82f6');
577
+ }
578
+
579
+ // Test 3: relational scale
580
+ {
581
+ ds.setToken('font.base', '1rem');
582
+ const h1Size = store.get('css.h1.fontSize');
583
+ assert('relational scale: h1 = 2x base', h1Size === '2rem');
584
+ }
585
+
586
+ // Test 4: typed CSS catches violation
587
+ {
588
+ const before = typed.getViolations().length;
589
+ store.set('css.btn.background', 'banana');
590
+ const after = typed.getViolations().length;
591
+ assert('typed CSS records violation', after > before);
592
+ }
593
+
594
+ // Test 5: contrast auto-pick
595
+ {
596
+ ds.setToken('color.primary', '#000000');
597
+ const textColor = store.get('css.card.color');
598
+ assert('contrast picks light text on dark bg', textColor === '#ffffff');
599
+ ds.setToken('color.primary', '#ffffff');
600
+ const textColor2 = store.get('css.card.color');
601
+ assert('contrast picks dark text on light bg', textColor2 === '#1e293b');
602
+ swapTheme('default');
603
+ }
604
+
605
+ // Test 6: wildcard fires on set
606
+ {
607
+ let fired = false;
608
+ const unsub = store.subscribe('css.*', () => { fired = true; });
609
+ store.set('css.page.padding', '3rem');
610
+ assert('css.* wildcard fires on set', fired);
611
+ unsub();
612
+ store.set('css.page', { maxWidth: '850px', margin: '0 auto', padding: '2rem' });
613
+ }
614
+
615
+ // Test 7: unsubscribe stops notifications
616
+ {
617
+ let count = 0;
618
+ const unsub = store.subscribe('tokens.color.primary', () => count++);
619
+ ds.setToken('color.primary', '#aaa');
620
+ unsub();
621
+ ds.setToken('color.primary', '#bbb');
622
+ assert('unsubscribe stops notifications', count === 1);
623
+ swapTheme('default');
624
+ }
625
+
626
+ log.textContent = results.join('\n') + `\n\n${pass} passed, ${fail} failed`;
627
+ };
628
+ </script>
629
+ </body>
630
+ </html>