@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.
- package/README.md +15 -0
- package/everyState-core/001-counter/README.md +44 -0
- package/everyState-core/001-counter/index.html +79 -0
- package/everyState-core/002-counter-improved/README.md +44 -0
- package/everyState-core/002-counter-improved/index.html +83 -0
- package/everyState-core/003-input-reactive/README.md +44 -0
- package/everyState-core/003-input-reactive/index.html +68 -0
- package/everyState-core/004-computed-state/README.md +45 -0
- package/everyState-core/004-computed-state/index.html +83 -0
- package/everyState-core/005-conditional-rendering/README.md +42 -0
- package/everyState-core/005-conditional-rendering/index.html +68 -0
- package/everyState-core/006-list-rendering/README.md +49 -0
- package/everyState-core/006-list-rendering/index.html +92 -0
- package/everyState-core/007-form-validation/README.md +52 -0
- package/everyState-core/007-form-validation/index.html +108 -0
- package/everyState-core/008-undo-redo/README.md +70 -0
- package/everyState-core/008-undo-redo/index.html +133 -0
- package/everyState-core/009-localStorage-side-effects/README.md +72 -0
- package/everyState-core/009-localStorage-side-effects/index.html +80 -0
- package/everyState-core/010-decoupled-components/README.md +74 -0
- package/everyState-core/010-decoupled-components/index.html +117 -0
- package/everyState-core/011-async-patterns/README.md +98 -0
- package/everyState-core/011-async-patterns/index.html +132 -0
- package/everyState-css/001-stateDrivenCSS/index.html +377 -0
- package/everyState-css/002-cssV2FullDemo/index.html +630 -0
- package/everyState-view/001/counter/index.css +31 -0
- package/everyState-view/001/counter/index.html +50 -0
- package/everyState-view/002/datatable/index.css +70 -0
- package/everyState-view/002/datatable/index.html +118 -0
- package/everyState-view/003/todo/index.css +260 -0
- package/everyState-view/003/todo/index.html +218 -0
- package/everyState-view/003-input-reactive/README.md +44 -0
- package/everyState-view/003-input-reactive/index.html +68 -0
- package/everyState-view/004/quotesFetcher/index.css +124 -0
- package/everyState-view/004/quotesFetcher/index.html +108 -0
- package/everyState-view/004_01/quotesFetcher/app.js +32 -0
- package/everyState-view/004_01/quotesFetcher/components/appHeader/appSubtitle.js +2 -0
- package/everyState-view/004_01/quotesFetcher/components/appHeader/appTitle.js +2 -0
- package/everyState-view/004_01/quotesFetcher/components/appHeader.js +9 -0
- package/everyState-view/004_01/quotesFetcher/components/historyHeading.js +2 -0
- package/everyState-view/004_01/quotesFetcher/components/historyList/histAuthor.js +2 -0
- package/everyState-view/004_01/quotesFetcher/components/historyList/histQuote.js +2 -0
- package/everyState-view/004_01/quotesFetcher/components/historyList.js +14 -0
- package/everyState-view/004_01/quotesFetcher/components/quoteCard/fetchButton.js +2 -0
- package/everyState-view/004_01/quotesFetcher/components/quoteCard/quoteAuthor.js +2 -0
- package/everyState-view/004_01/quotesFetcher/components/quoteCard/quoteMessage.js +2 -0
- package/everyState-view/004_01/quotesFetcher/components/quoteCard/quoteText.js +2 -0
- package/everyState-view/004_01/quotesFetcher/components/quoteCard.js +11 -0
- package/everyState-view/004_01/quotesFetcher/index.css +124 -0
- package/everyState-view/004_01/quotesFetcher/index.html +23 -0
- package/everyState-view/004_01/quotesFetcher/store.js +35 -0
- package/everyState-view/004_02/quotesFetcher/app.js +20 -0
- package/everyState-view/004_02/quotesFetcher/components.js +46 -0
- package/everyState-view/004_02/quotesFetcher/index.css +124 -0
- package/everyState-view/004_02/quotesFetcher/index.html +23 -0
- package/everyState-view/004_02/quotesFetcher/store.js +35 -0
- package/everyState-view/004_03/quotesFetcher/actions.js +27 -0
- package/everyState-view/004_03/quotesFetcher/app.js +19 -0
- package/everyState-view/004_03/quotesFetcher/components.js +28 -0
- package/everyState-view/004_03/quotesFetcher/index.css +124 -0
- package/everyState-view/004_03/quotesFetcher/index.html +23 -0
- package/everyState-view/004_03/quotesFetcher/resolve.js +34 -0
- package/everyState-view/004_03/quotesFetcher/store.js +11 -0
- package/everyState-view/004_04/quotesFetcher/actions.js +66 -0
- package/everyState-view/004_04/quotesFetcher/app.js +24 -0
- package/everyState-view/004_04/quotesFetcher/components/archive.js +40 -0
- package/everyState-view/004_04/quotesFetcher/components/fetcher.js +29 -0
- package/everyState-view/004_04/quotesFetcher/components.js +20 -0
- package/everyState-view/004_04/quotesFetcher/index.css +283 -0
- package/everyState-view/004_04/quotesFetcher/index.html +24 -0
- package/everyState-view/004_04/quotesFetcher/resolve.js +34 -0
- package/everyState-view/004_04/quotesFetcher/store.js +21 -0
- package/everyState-view/004_04/statedump.json +826 -0
- package/everyState-view/004_05/quoteExplorer/actions.js +58 -0
- package/everyState-view/004_05/quoteExplorer/app.js +27 -0
- package/everyState-view/004_05/quoteExplorer/components.js +83 -0
- package/everyState-view/004_05/quoteExplorer/index.css +231 -0
- package/everyState-view/004_05/quoteExplorer/index.html +23 -0
- package/everyState-view/004_05/quoteExplorer/resolve.js +50 -0
- package/everyState-view/004_05/quoteExplorer/store.js +33 -0
- package/everyState-view/004_06/quoteExplorer/actions.js +21 -0
- package/everyState-view/004_06/quoteExplorer/app.js +44 -0
- package/everyState-view/004_06/quoteExplorer/components.js +80 -0
- package/everyState-view/004_06/quoteExplorer/derived.js +43 -0
- package/everyState-view/004_06/quoteExplorer/index.css +346 -0
- package/everyState-view/004_06/quoteExplorer/index.html +25 -0
- package/everyState-view/004_06/quoteExplorer/intents.js +44 -0
- package/everyState-view/004_06/quoteExplorer/policies.js +25 -0
- package/everyState-view/004_06/quoteExplorer/resolve.js +51 -0
- package/everyState-view/004_06/quoteExplorer/store.js +44 -0
- package/everyState-view/004_07/quoteExplorer/app.js +47 -0
- package/everyState-view/004_07/quoteExplorer/components.js +85 -0
- package/everyState-view/004_07/quoteExplorer/derived.js +43 -0
- package/everyState-view/004_07/quoteExplorer/index.css +346 -0
- package/everyState-view/004_07/quoteExplorer/index.html +25 -0
- package/everyState-view/004_07/quoteExplorer/intents.js +51 -0
- package/everyState-view/004_07/quoteExplorer/policies.js +21 -0
- package/everyState-view/004_07/quoteExplorer/resolve.js +39 -0
- package/everyState-view/004_07/quoteExplorer/store.js +44 -0
- package/everyState-view/004_08/quoteExplorer/app.js +78 -0
- package/everyState-view/004_08/quoteExplorer/components.js +85 -0
- package/everyState-view/004_08/quoteExplorer/derived.js +43 -0
- package/everyState-view/004_08/quoteExplorer/index.css +346 -0
- package/everyState-view/004_08/quoteExplorer/index.html +25 -0
- package/everyState-view/004_08/quoteExplorer/intents.js +51 -0
- package/everyState-view/004_08/quoteExplorer/policies.js +21 -0
- package/everyState-view/004_08/quoteExplorer/resolve.js +39 -0
- package/everyState-view/004_08/quoteExplorer/store.js +44 -0
- package/everyState-view/004_08_V2/app.js +78 -0
- package/everyState-view/004_08_V2/components/appDetail.js +8 -0
- package/everyState-view/004_08_V2/components/appDetailBar.js +7 -0
- package/everyState-view/004_08_V2/components/appDetailBarClose.js +8 -0
- package/everyState-view/004_08_V2/components/appDetailBarCount.js +7 -0
- package/everyState-view/004_08_V2/components/appDetailBarHeading.js +7 -0
- package/everyState-view/004_08_V2/components/appDetailQuotes.js +15 -0
- package/everyState-view/004_08_V2/components/appHeader.js +7 -0
- package/everyState-view/004_08_V2/components/appHeaderSubtitle.js +7 -0
- package/everyState-view/004_08_V2/components/appHeaderTitle.js +7 -0
- package/everyState-view/004_08_V2/components/appLog.js +7 -0
- package/everyState-view/004_08_V2/components/appLogHeading.js +7 -0
- package/everyState-view/004_08_V2/components/appLogList.js +16 -0
- package/everyState-view/004_08_V2/components/appSearch.js +7 -0
- package/everyState-view/004_08_V2/components/appSearchInput.js +9 -0
- package/everyState-view/004_08_V2/components/appStats.js +7 -0
- package/everyState-view/004_08_V2/components/appStatsContent.js +8 -0
- package/everyState-view/004_08_V2/components/appStatsContentFavcount.js +7 -0
- package/everyState-view/004_08_V2/components/appStatsContentText.js +7 -0
- package/everyState-view/004_08_V2/components/appStatsToggle.js +8 -0
- package/everyState-view/004_08_V2/components/appTags.js +7 -0
- package/everyState-view/004_08_V2/components/appTagsLabel.js +7 -0
- package/everyState-view/004_08_V2/components/appTagsRow.js +15 -0
- package/everyState-view/004_08_V2/components/index.js +59 -0
- package/everyState-view/004_08_V2/components/utils/css.js +88 -0
- package/everyState-view/004_08_V2/components/utils/elements.js +87 -0
- package/everyState-view/004_08_V2/components.js +79 -0
- package/everyState-view/004_08_V2/derived.js +43 -0
- package/everyState-view/004_08_V2/index.css +350 -0
- package/everyState-view/004_08_V2/index.html +25 -0
- package/everyState-view/004_08_V2/intents.js +51 -0
- package/everyState-view/004_08_V2/policies.js +21 -0
- package/everyState-view/004_08_V2/resolve.js +39 -0
- package/everyState-view/004_08_V2/store.js +44 -0
- package/everyState-view/006/api-datatable/index.css +388 -0
- package/everyState-view/006/api-datatable/index.html +355 -0
- package/everyState-view/007/apiUsers/index.html +307 -0
- package/everyState-view/007-form-validation/README.md +52 -0
- package/everyState-view/007-form-validation/index.html +108 -0
- package/everyState-view/010-decoupled-components/README.md +74 -0
- package/everyState-view/010-decoupled-components/index.html +117 -0
- package/everyState-view/index.html +36 -0
- package/index.js +0 -5
- 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>
|