@uistate/examples 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (137) hide show
  1. package/README.md +40 -0
  2. package/cssState/.gitkeep +0 -0
  3. package/eventState/001-counter/README.md +44 -0
  4. package/eventState/001-counter/index.html +33 -0
  5. package/eventState/002-counter-improved/README.md +44 -0
  6. package/eventState/002-counter-improved/index.html +47 -0
  7. package/eventState/003-input-reactive/README.md +44 -0
  8. package/eventState/003-input-reactive/index.html +33 -0
  9. package/eventState/004-computed-state/README.md +45 -0
  10. package/eventState/004-computed-state/index.html +65 -0
  11. package/eventState/005-conditional-rendering/README.md +42 -0
  12. package/eventState/005-conditional-rendering/index.html +39 -0
  13. package/eventState/006-list-rendering/README.md +49 -0
  14. package/eventState/006-list-rendering/index.html +63 -0
  15. package/eventState/007-form-validation/README.md +52 -0
  16. package/eventState/007-form-validation/index.html +102 -0
  17. package/eventState/008-undo-redo/README.md +70 -0
  18. package/eventState/008-undo-redo/index.html +108 -0
  19. package/eventState/009-localStorage-side-effects/README.md +72 -0
  20. package/eventState/009-localStorage-side-effects/index.html +57 -0
  21. package/eventState/010-decoupled-components/README.md +74 -0
  22. package/eventState/010-decoupled-components/index.html +93 -0
  23. package/eventState/011-async-patterns/README.md +98 -0
  24. package/eventState/011-async-patterns/index.html +132 -0
  25. package/eventState/028-counter-improved-eventTest/LICENSE +55 -0
  26. package/eventState/028-counter-improved-eventTest/README.md +131 -0
  27. package/eventState/028-counter-improved-eventTest/app/store.js +9 -0
  28. package/eventState/028-counter-improved-eventTest/index.html +49 -0
  29. package/eventState/028-counter-improved-eventTest/runtime/core/behaviors.runtime.js +282 -0
  30. package/eventState/028-counter-improved-eventTest/runtime/core/eventState.js +100 -0
  31. package/eventState/028-counter-improved-eventTest/runtime/core/eventStateNew.js +149 -0
  32. package/eventState/028-counter-improved-eventTest/runtime/core/helpers.js +212 -0
  33. package/eventState/028-counter-improved-eventTest/runtime/core/router.js +271 -0
  34. package/eventState/028-counter-improved-eventTest/store.d.ts +8 -0
  35. package/eventState/028-counter-improved-eventTest/style.css +170 -0
  36. package/eventState/028-counter-improved-eventTest/tests/README.md +208 -0
  37. package/eventState/028-counter-improved-eventTest/tests/counter.test.js +116 -0
  38. package/eventState/028-counter-improved-eventTest/tests/eventTest.js +176 -0
  39. package/eventState/028-counter-improved-eventTest/tests/generateTypes.js +168 -0
  40. package/eventState/028-counter-improved-eventTest/tests/run.js +20 -0
  41. package/eventState/030-todo-app-with-eventTest/LICENSE +55 -0
  42. package/eventState/030-todo-app-with-eventTest/README.md +121 -0
  43. package/eventState/030-todo-app-with-eventTest/app/router.js +25 -0
  44. package/eventState/030-todo-app-with-eventTest/app/store.js +16 -0
  45. package/eventState/030-todo-app-with-eventTest/app/views/home.js +11 -0
  46. package/eventState/030-todo-app-with-eventTest/app/views/todoDemo.js +88 -0
  47. package/eventState/030-todo-app-with-eventTest/index.html +65 -0
  48. package/eventState/030-todo-app-with-eventTest/runtime/core/behaviors.runtime.js +282 -0
  49. package/eventState/030-todo-app-with-eventTest/runtime/core/eventState.js +100 -0
  50. package/eventState/030-todo-app-with-eventTest/runtime/core/eventStateNew.js +149 -0
  51. package/eventState/030-todo-app-with-eventTest/runtime/core/helpers.js +212 -0
  52. package/eventState/030-todo-app-with-eventTest/runtime/core/router.js +271 -0
  53. package/eventState/030-todo-app-with-eventTest/store.d.ts +18 -0
  54. package/eventState/030-todo-app-with-eventTest/style.css +170 -0
  55. package/eventState/030-todo-app-with-eventTest/tests/README.md +208 -0
  56. package/eventState/030-todo-app-with-eventTest/tests/eventTest.js +176 -0
  57. package/eventState/030-todo-app-with-eventTest/tests/generateTypes.js +189 -0
  58. package/eventState/030-todo-app-with-eventTest/tests/run.js +20 -0
  59. package/eventState/030-todo-app-with-eventTest/tests/todos.test.js +167 -0
  60. package/eventState/031-todo-app-with-eventTest/LICENSE +55 -0
  61. package/eventState/031-todo-app-with-eventTest/README.md +54 -0
  62. package/eventState/031-todo-app-with-eventTest/TUTORIAL.md +390 -0
  63. package/eventState/031-todo-app-with-eventTest/WHY_EVENTSTATE.md +777 -0
  64. package/eventState/031-todo-app-with-eventTest/app/bridges.js +113 -0
  65. package/eventState/031-todo-app-with-eventTest/app/router.js +26 -0
  66. package/eventState/031-todo-app-with-eventTest/app/store.js +15 -0
  67. package/eventState/031-todo-app-with-eventTest/app/views/home.js +46 -0
  68. package/eventState/031-todo-app-with-eventTest/app/views/todoDemo.js +69 -0
  69. package/eventState/031-todo-app-with-eventTest/devtools/dock.js +41 -0
  70. package/eventState/031-todo-app-with-eventTest/devtools/stateTracker.dock.js +10 -0
  71. package/eventState/031-todo-app-with-eventTest/devtools/stateTracker.js +246 -0
  72. package/eventState/031-todo-app-with-eventTest/devtools/telemetry.js +104 -0
  73. package/eventState/031-todo-app-with-eventTest/devtools/typeGenerator.js +339 -0
  74. package/eventState/031-todo-app-with-eventTest/index.html +103 -0
  75. package/eventState/031-todo-app-with-eventTest/package-lock.json +2184 -0
  76. package/eventState/031-todo-app-with-eventTest/package.json +24 -0
  77. package/eventState/031-todo-app-with-eventTest/runtime/core/behaviors.runtime.js +282 -0
  78. package/eventState/031-todo-app-with-eventTest/runtime/core/eventState.js +100 -0
  79. package/eventState/031-todo-app-with-eventTest/runtime/core/eventStateNew.js +149 -0
  80. package/eventState/031-todo-app-with-eventTest/runtime/core/helpers.js +212 -0
  81. package/eventState/031-todo-app-with-eventTest/runtime/core/router.js +271 -0
  82. package/eventState/031-todo-app-with-eventTest/runtime/extensions/boundary.js +36 -0
  83. package/eventState/031-todo-app-with-eventTest/runtime/extensions/converge.js +63 -0
  84. package/eventState/031-todo-app-with-eventTest/runtime/extensions/eventState.plus.js +210 -0
  85. package/eventState/031-todo-app-with-eventTest/runtime/extensions/hydrate.js +157 -0
  86. package/eventState/031-todo-app-with-eventTest/runtime/extensions/queryBinding.js +69 -0
  87. package/eventState/031-todo-app-with-eventTest/runtime/forms/computed.js +78 -0
  88. package/eventState/031-todo-app-with-eventTest/runtime/forms/meta.js +51 -0
  89. package/eventState/031-todo-app-with-eventTest/runtime/forms/submitWithBoundary.js +28 -0
  90. package/eventState/031-todo-app-with-eventTest/runtime/forms/validators.js +55 -0
  91. package/eventState/031-todo-app-with-eventTest/store.d.ts +23 -0
  92. package/eventState/031-todo-app-with-eventTest/style.css +170 -0
  93. package/eventState/031-todo-app-with-eventTest/tests/README.md +208 -0
  94. package/eventState/031-todo-app-with-eventTest/tests/eventTest.js +176 -0
  95. package/eventState/031-todo-app-with-eventTest/tests/generateTypes.js +191 -0
  96. package/eventState/031-todo-app-with-eventTest/tests/run.js +20 -0
  97. package/eventState/031-todo-app-with-eventTest/tests/todos.test.js +192 -0
  98. package/eventState/032-todo-app-with-eventTest/LICENSE +55 -0
  99. package/eventState/032-todo-app-with-eventTest/README.md +54 -0
  100. package/eventState/032-todo-app-with-eventTest/TUTORIAL.md +390 -0
  101. package/eventState/032-todo-app-with-eventTest/WHY_EVENTSTATE.md +777 -0
  102. package/eventState/032-todo-app-with-eventTest/app/actions/index.js +153 -0
  103. package/eventState/032-todo-app-with-eventTest/app/bridges.js +113 -0
  104. package/eventState/032-todo-app-with-eventTest/app/router.js +26 -0
  105. package/eventState/032-todo-app-with-eventTest/app/store.js +15 -0
  106. package/eventState/032-todo-app-with-eventTest/app/views/home.js +46 -0
  107. package/eventState/032-todo-app-with-eventTest/app/views/todoDemo.js +69 -0
  108. package/eventState/032-todo-app-with-eventTest/devtools/dock.js +41 -0
  109. package/eventState/032-todo-app-with-eventTest/devtools/stateTracker.dock.js +10 -0
  110. package/eventState/032-todo-app-with-eventTest/devtools/stateTracker.js +246 -0
  111. package/eventState/032-todo-app-with-eventTest/devtools/telemetry.js +104 -0
  112. package/eventState/032-todo-app-with-eventTest/devtools/typeGenerator.js +339 -0
  113. package/eventState/032-todo-app-with-eventTest/index.html +87 -0
  114. package/eventState/032-todo-app-with-eventTest/package-lock.json +2184 -0
  115. package/eventState/032-todo-app-with-eventTest/package.json +24 -0
  116. package/eventState/032-todo-app-with-eventTest/runtime/core/behaviors.runtime.js +282 -0
  117. package/eventState/032-todo-app-with-eventTest/runtime/core/eventState.js +100 -0
  118. package/eventState/032-todo-app-with-eventTest/runtime/core/eventStateNew.js +149 -0
  119. package/eventState/032-todo-app-with-eventTest/runtime/core/helpers.js +212 -0
  120. package/eventState/032-todo-app-with-eventTest/runtime/core/router.js +271 -0
  121. package/eventState/032-todo-app-with-eventTest/runtime/extensions/boundary.js +36 -0
  122. package/eventState/032-todo-app-with-eventTest/runtime/extensions/converge.js +63 -0
  123. package/eventState/032-todo-app-with-eventTest/runtime/extensions/eventState.plus.js +210 -0
  124. package/eventState/032-todo-app-with-eventTest/runtime/extensions/hydrate.js +157 -0
  125. package/eventState/032-todo-app-with-eventTest/runtime/extensions/queryBinding.js +69 -0
  126. package/eventState/032-todo-app-with-eventTest/runtime/forms/computed.js +78 -0
  127. package/eventState/032-todo-app-with-eventTest/runtime/forms/meta.js +51 -0
  128. package/eventState/032-todo-app-with-eventTest/runtime/forms/submitWithBoundary.js +28 -0
  129. package/eventState/032-todo-app-with-eventTest/runtime/forms/validators.js +55 -0
  130. package/eventState/032-todo-app-with-eventTest/store.d.ts +23 -0
  131. package/eventState/032-todo-app-with-eventTest/style.css +170 -0
  132. package/eventState/032-todo-app-with-eventTest/tests/README.md +208 -0
  133. package/eventState/032-todo-app-with-eventTest/tests/eventTest.js +176 -0
  134. package/eventState/032-todo-app-with-eventTest/tests/generateTypes.js +191 -0
  135. package/eventState/032-todo-app-with-eventTest/tests/run.js +20 -0
  136. package/eventState/032-todo-app-with-eventTest/tests/todos.test.js +192 -0
  137. package/package.json +27 -0
@@ -0,0 +1,271 @@
1
+ // runtime/core/router.js — Generic SPA router factory for eventState stores
2
+ // Usage:
3
+ // const router = createRouter({
4
+ // routes: [{ path: '/', view: 'home', component: HomeView }],
5
+ // store,
6
+ // rootSelector: '[data-route-root]',
7
+ // debug: true
8
+ // });
9
+ // router.start();
10
+
11
+ export function createRouter(config) {
12
+ const {
13
+ routes = [],
14
+ store,
15
+ rootSelector = '[data-route-root]',
16
+ fallback = null,
17
+ debug = false,
18
+ linkSelector = 'a[data-link]',
19
+ navSelector = 'nav a[data-link]',
20
+ } = config;
21
+
22
+ // Detect base path from <base href> if present
23
+ const BASE_PATH = (() => {
24
+ const b = document.querySelector('base[href]');
25
+ if (!b) return '';
26
+ try {
27
+ const u = new URL(b.getAttribute('href'), location.href);
28
+ let p = u.pathname;
29
+ if (p.length > 1 && p.endsWith('/')) p = p.slice(0, -1);
30
+ return p;
31
+ } catch { return ''; }
32
+ })();
33
+
34
+ function stripBase(pathname) {
35
+ if (BASE_PATH && pathname.startsWith(BASE_PATH)) {
36
+ const rest = pathname.slice(BASE_PATH.length) || '/';
37
+ return rest.startsWith('/') ? rest : ('/' + rest);
38
+ }
39
+ return pathname;
40
+ }
41
+
42
+ function withBase(pathname) {
43
+ if (!BASE_PATH) return pathname;
44
+ if (pathname === '/') return BASE_PATH || '/';
45
+ return (BASE_PATH + (pathname.startsWith('/') ? '' : '/') + pathname);
46
+ }
47
+
48
+ function normalizePath(p) {
49
+ if (!p) return '/';
50
+ try {
51
+ if (p[0] !== '/') p = '/' + p;
52
+ if (p === '/index.html') return '/';
53
+ if (p.length > 1 && p.endsWith('/')) p = p.slice(0, -1);
54
+ return p;
55
+ } catch { return '/'; }
56
+ }
57
+
58
+ function resolve(pathname) {
59
+ const p = normalizePath(pathname);
60
+ const r = routes.find(r => r.path === p);
61
+ if (r) return { ...r, params: {} };
62
+ if (fallback) return { ...fallback, params: {} };
63
+ return null;
64
+ }
65
+
66
+ function getRoot() {
67
+ const el = document.querySelector(rootSelector);
68
+ if (!el) throw new Error('Route root not found: ' + rootSelector);
69
+ return el;
70
+ }
71
+
72
+ function setActiveNav(pathname) {
73
+ document.querySelectorAll(navSelector).forEach(a => {
74
+ const url = new URL(a.getAttribute('href'), location.href);
75
+ const linkPath = normalizePath(stripBase(url.pathname));
76
+ const here = normalizePath(pathname);
77
+ const isExact = linkPath === here;
78
+ const isParent = !isExact && linkPath !== '/' && here.startsWith(linkPath);
79
+ const active = isExact || isParent;
80
+ a.classList.toggle('active', active);
81
+ if (isExact) a.setAttribute('aria-current', 'page');
82
+ else a.removeAttribute('aria-current');
83
+ });
84
+ }
85
+
86
+ // State
87
+ let current = { viewKey: null, unboot: null, path: null, search: '' };
88
+ let navController = null;
89
+ const scrollPositions = new Map();
90
+ history.scrollRestoration = 'manual';
91
+
92
+ // Core navigate function
93
+ async function navigate(pathname, { replace = false, search = '', restoreScroll = false } = {}) {
94
+ const root = getRoot();
95
+ const appPath = normalizePath(stripBase(pathname));
96
+ const resolved = resolve(appPath);
97
+
98
+ if (!resolved) {
99
+ if (debug) console.warn('[router] No route found for:', appPath);
100
+ return;
101
+ }
102
+
103
+ const viewKey = resolved.view;
104
+ const component = resolved.component;
105
+ const html = document.documentElement;
106
+ const prevViewKey = current.viewKey;
107
+
108
+ const searchStr = search && search.startsWith('?') ? search : (search ? ('?' + search) : '');
109
+
110
+ // Always log navigation for telemetry
111
+ console.log('[nav] navigate', { from: current.path, to: appPath, view: viewKey });
112
+
113
+ if (debug) {
114
+ console.debug('[router] navigate', { pathname, appPath, searchStr, view: viewKey, params: resolved.params });
115
+ }
116
+
117
+ // Same-route no-op guard
118
+ if (current.path === appPath && current.search === searchStr) {
119
+ return;
120
+ }
121
+
122
+ // Abort in-flight boot
123
+ if (navController) navController.abort();
124
+ navController = new AbortController();
125
+ const { signal } = navController;
126
+
127
+ // Transition start
128
+ if (store) {
129
+ try { store.set('ui.route.transitioning', true); } catch {}
130
+ }
131
+ html.setAttribute('data-transitioning', 'on');
132
+
133
+ // Save scroll position
134
+ if (current.path) {
135
+ scrollPositions.set(current.path, { x: scrollX, y: scrollY });
136
+ }
137
+
138
+ // Unboot previous view
139
+ if (typeof current.unboot === 'function') {
140
+ try { await current.unboot(); } catch (_e) {}
141
+ }
142
+
143
+ // Clear DOM
144
+ root.replaceChildren();
145
+
146
+ // Boot new view
147
+ const unboot = await (component.boot?.({ store, el: root, signal }) || Promise.resolve(() => {}));
148
+ current = { viewKey, unboot, path: appPath, search: searchStr };
149
+
150
+ // Update nav active state
151
+ setActiveNav(appPath);
152
+
153
+ // Parse query params
154
+ const urlForQuery = new URL(location.origin + withBase(appPath) + searchStr);
155
+ const q = {};
156
+ urlForQuery.searchParams.forEach((v, k) => { q[k] = v; });
157
+
158
+ // Update store
159
+ if (store) {
160
+ try {
161
+ if (store.setMany) {
162
+ store.setMany({
163
+ 'ui.route.view': viewKey,
164
+ 'ui.route.path': appPath,
165
+ 'ui.route.params': resolved.params || {},
166
+ 'ui.route.query': q,
167
+ });
168
+ } else {
169
+ store.set('ui.route.view', viewKey);
170
+ store.set('ui.route.path', appPath);
171
+ store.set('ui.route.params', resolved.params || {});
172
+ store.set('ui.route.query', q);
173
+ }
174
+ } catch {}
175
+ }
176
+
177
+ // Update browser history
178
+ const useReplace = replace || (prevViewKey === viewKey);
179
+ if (useReplace) history.replaceState({}, '', withBase(appPath) + searchStr);
180
+ else history.pushState({}, '', withBase(appPath) + searchStr);
181
+
182
+ // Set view attribute
183
+ html.setAttribute('data-view', viewKey);
184
+
185
+ if (debug) {
186
+ console.debug('[router] view', { viewKey, path: appPath, query: q });
187
+ }
188
+
189
+ // Transition end
190
+ if (store) {
191
+ try { store.set('ui.route.transitioning', false); } catch {}
192
+ }
193
+ html.setAttribute('data-transitioning', 'off');
194
+
195
+ // Focus management
196
+ if (!root.hasAttribute('tabindex')) root.setAttribute('tabindex', '-1');
197
+ try { root.focus({ preventScroll: true }); } catch {}
198
+
199
+ // Scroll restoration
200
+ if (restoreScroll) {
201
+ const pos = scrollPositions.get(appPath);
202
+ if (pos) scrollTo(pos.x, pos.y);
203
+ } else {
204
+ scrollTo(0, 0);
205
+ }
206
+ }
207
+
208
+ function navigateQuery(patch = {}, { replace = true } = {}) {
209
+ const params = new URLSearchParams(current.search || '');
210
+ for (const [k, v] of Object.entries(patch)) {
211
+ if (v === null || v === undefined || v === '') params.delete(k);
212
+ else params.set(k, String(v));
213
+ }
214
+ const searchStr = params.toString();
215
+ const prefixed = searchStr ? ('?' + searchStr) : '';
216
+ const path = current.path || normalizePath(stripBase(location.pathname));
217
+ return navigate(path, { search: prefixed, replace });
218
+ }
219
+
220
+ function navigatePath(path, { replace = true } = {}) {
221
+ const appPath = normalizePath(stripBase(path));
222
+ const searchStr = current.search || '';
223
+ return navigate(appPath, { search: searchStr, replace });
224
+ }
225
+
226
+ function onClick(e) {
227
+ const a = e.target.closest(linkSelector);
228
+ if (!a) return;
229
+ if (e.defaultPrevented) return;
230
+ if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey || e.button !== 0) return;
231
+ const url = new URL(a.getAttribute('href'), location.href);
232
+ if (url.origin !== location.origin) return;
233
+ e.preventDefault();
234
+
235
+ // Log navigation click
236
+ console.log('[nav] click', { href: a.getAttribute('href'), text: a.textContent.trim() });
237
+
238
+ navigate(url.pathname, { search: url.search }).catch(() => {});
239
+ }
240
+
241
+ function onPop() {
242
+ navigate(location.pathname, { replace: true, search: location.search, restoreScroll: true }).catch(() => {});
243
+ }
244
+
245
+ // Public API
246
+ return {
247
+ navigate,
248
+ navigateQuery,
249
+ navigatePath,
250
+
251
+ start() {
252
+ window.addEventListener('click', onClick);
253
+ window.addEventListener('popstate', onPop);
254
+ navigate(location.pathname, { replace: true, search: location.search, restoreScroll: true });
255
+ return this;
256
+ },
257
+
258
+ stop() {
259
+ window.removeEventListener('click', onClick);
260
+ window.removeEventListener('popstate', onPop);
261
+ if (typeof current.unboot === 'function') {
262
+ try { current.unboot(); } catch {}
263
+ }
264
+ return this;
265
+ },
266
+
267
+ getCurrent() {
268
+ return { ...current };
269
+ }
270
+ };
271
+ }
@@ -0,0 +1,8 @@
1
+ // Auto-generated from test assertions
2
+ // DO NOT EDIT - regenerate by running: node tests/generateTypes.js
3
+
4
+ export interface StoreState {
5
+ count: number;
6
+ }
7
+
8
+ export default StoreState;
@@ -0,0 +1,170 @@
1
+ body {
2
+ font: 14px/1.4 system-ui, sans-serif;
3
+ margin: 0;
4
+ }
5
+
6
+ /* Hide focus outline on route container (set programmatically for a11y) */
7
+ [data-route-root]:focus {
8
+ outline: none;
9
+ }
10
+
11
+ nav {
12
+ padding: 10px 14px;
13
+ border-bottom: 1px solid #eee;
14
+ position: sticky;
15
+ top: 0;
16
+ background: #fff;
17
+ z-index: 10;
18
+ }
19
+
20
+ nav a {
21
+ margin-right: 12px;
22
+ text-decoration: none;
23
+ color: #0366d6;
24
+ }
25
+
26
+ nav a.active {
27
+ font-weight: 600;
28
+ }
29
+
30
+ main {
31
+ padding: 16px;
32
+ }
33
+
34
+ .loading-badge {
35
+ margin-left: 8px;
36
+ font-size: 12px;
37
+ color: #666;
38
+ display: none;
39
+ }
40
+
41
+ html[data-transitioning="on"] .loading-badge {
42
+ display: inline;
43
+ }
44
+
45
+ /* 404 view demo: tint the top bar only on notfound */
46
+ html[data-view="notfound"] nav {
47
+ background: #fff7f7;
48
+ border-bottom-color: #f5c2c2;
49
+ }
50
+
51
+ html[data-view="notfound"] nav a {
52
+ color: #b91c1c;
53
+ }
54
+
55
+ html[data-view="notfound"] .loading-badge {
56
+ color: #b91c1c;
57
+ }
58
+
59
+ /* Tabs styling (used by views/demo.js) */
60
+ .tabs {
61
+ margin-top: 12px;
62
+ }
63
+
64
+ .tab-bar {
65
+ display: flex;
66
+ gap: 8px;
67
+ border-bottom: 1px solid #eee;
68
+ padding-bottom: 6px;
69
+ }
70
+
71
+ .tab {
72
+ appearance: none;
73
+ border: 1px solid #ddd;
74
+ background: #fafafa;
75
+ border-radius: 6px;
76
+ padding: 6px 10px;
77
+ cursor: pointer;
78
+ }
79
+
80
+ .tab:hover {
81
+ background: #f3f4f6;
82
+ }
83
+
84
+ .tab.active {
85
+ background: #e5efff;
86
+ border-color: #bcd1ff;
87
+ }
88
+
89
+ .tab-panels {
90
+ padding-top: 10px;
91
+ }
92
+
93
+ .tab-panel {
94
+ display: none;
95
+ }
96
+
97
+ .tab-panel[data-active] {
98
+ display: block;
99
+ }
100
+
101
+ :root {
102
+ --bg: #ffffff;
103
+ --fg: #111111;
104
+ --muted: #555;
105
+ --card: #f6f6f6;
106
+ --border: #e5e5e5;
107
+ --btn-bg: #f8f8f8;
108
+ --btn-fg: #111;
109
+ --btn-border: #ddd;
110
+ --input-bg: #fff;
111
+ --input-fg: #222;
112
+ --input-border: #d6d6d6;
113
+ }
114
+
115
+ body[data-theme='dark'] {
116
+ --bg: #0f1115;
117
+ --fg: #cfd3dc;
118
+ --muted: #9aa3b2;
119
+ --card: #151923;
120
+ --border: #242a36;
121
+ --btn-bg: #1a2030;
122
+ --btn-fg: #cfd3dc;
123
+ --btn-border: #2b3242;
124
+ --input-bg: #1b2130;
125
+ --input-fg: #cfd3dc;
126
+ --input-border: #2b3242;
127
+ }
128
+
129
+ body {
130
+ background: var(--bg);
131
+ color: var(--fg);
132
+ }
133
+
134
+ a {
135
+ color: inherit;
136
+ }
137
+
138
+ nav {
139
+ background: var(--card);
140
+ border-bottom: 1px solid var(--border);
141
+ }
142
+
143
+ .btn {
144
+ appearance: none;
145
+ border: 1px solid var(--btn-border);
146
+ background: var(--btn-bg);
147
+ color: var(--btn-fg);
148
+ border-radius: 6px;
149
+ padding: 6px 10px;
150
+ cursor: pointer;
151
+ }
152
+
153
+ input,
154
+ select,
155
+ textarea {
156
+ background: var(--input-bg);
157
+ color: var(--input-fg);
158
+ border: 1px solid var(--input-border);
159
+ border-radius: 6px;
160
+ padding: 6px 8px;
161
+ }
162
+
163
+ input::placeholder,
164
+ textarea::placeholder {
165
+ color: color-mix(in srgb, var(--input-fg) 55%, transparent);
166
+ }
167
+
168
+ .btn:hover {
169
+ filter: brightness(1.05);
170
+ }
@@ -0,0 +1,208 @@
1
+ # EventTest - Event-Sequence Testing
2
+
3
+ Event-driven TDD for EventState applications.
4
+
5
+ ## Features
6
+
7
+ - ✅ **Event-sequence testing** - Test state changes through event flows
8
+ - ✅ **Type extraction** - Generate `.d.ts` files from test assertions
9
+ - ✅ **No DOM required** - Tests run in Node.js
10
+ - ✅ **Fluent API** - Chainable assertions
11
+ - ✅ **Type-safe** - Tests define types, not manual definitions
12
+
13
+ ## Usage
14
+
15
+ ### Writing Tests
16
+
17
+ ```javascript
18
+ import { createEventTest, test } from './eventTest.js';
19
+
20
+ test('add todo', () => {
21
+ const t = createEventTest({
22
+ domain: { todos: { items: [] } }
23
+ });
24
+
25
+ // Trigger intent
26
+ t.trigger('intent.todo.add', { text: 'Buy milk' });
27
+
28
+ // Assert types (for .d.ts generation)
29
+ t.assertArrayOf('domain.todos.items', {
30
+ id: 'number',
31
+ text: 'string',
32
+ done: 'boolean'
33
+ });
34
+
35
+ // Assert values
36
+ t.assertArrayLength('domain.todos.items', 1);
37
+ });
38
+ ```
39
+
40
+ ### Running Tests
41
+
42
+ ```bash
43
+ # Run all tests
44
+ node tests/run.js
45
+
46
+ # Or run specific test file
47
+ node tests/todos.test.js
48
+ ```
49
+
50
+ ### Generating Types
51
+
52
+ ```bash
53
+ # Generate store.d.ts from test assertions
54
+ node tests/generateTypes.js
55
+ ```
56
+
57
+ ## API Reference
58
+
59
+ ### `createEventTest(initialState)`
60
+
61
+ Creates a test instance with isolated store.
62
+
63
+ **Returns:** Test API object
64
+
65
+ ### Test API
66
+
67
+ #### `.trigger(path, value)`
68
+
69
+ Set a value in the store (trigger state change).
70
+
71
+ ```javascript
72
+ t.trigger('intent.todo.add', { text: 'Buy milk' });
73
+ ```
74
+
75
+ #### `.assertPath(path, expected)`
76
+
77
+ Assert exact value at path.
78
+
79
+ ```javascript
80
+ t.assertPath('ui.theme', 'dark');
81
+ ```
82
+
83
+ #### `.assertType(path, type)`
84
+
85
+ Assert primitive type. Stores type info for `.d.ts` generation.
86
+
87
+ ```javascript
88
+ t.assertType('ui.theme', 'string');
89
+ ```
90
+
91
+ #### `.assertArrayOf(path, elementShape)`
92
+
93
+ Assert array with element shape. Stores type info for `.d.ts` generation.
94
+
95
+ ```javascript
96
+ t.assertArrayOf('domain.todos.items', {
97
+ id: 'number',
98
+ text: 'string',
99
+ done: 'boolean'
100
+ });
101
+ ```
102
+
103
+ #### `.assertShape(path, objectShape)`
104
+
105
+ Assert object shape. Stores type info for `.d.ts` generation.
106
+
107
+ ```javascript
108
+ t.assertShape('ui.route', {
109
+ path: 'string',
110
+ view: 'string'
111
+ });
112
+ ```
113
+
114
+ #### `.assertArrayLength(path, length)`
115
+
116
+ Assert array length.
117
+
118
+ ```javascript
119
+ t.assertArrayLength('domain.todos.items', 3);
120
+ ```
121
+
122
+ #### `.assertEventFired(path, times)`
123
+
124
+ Assert event fired N times.
125
+
126
+ ```javascript
127
+ t.assertEventFired('domain.todos.items', 1);
128
+ ```
129
+
130
+ #### `.getEventLog()`
131
+
132
+ Get all captured events.
133
+
134
+ ```javascript
135
+ const log = t.getEventLog();
136
+ // [{ timestamp: 123, path: 'domain.todos.items', value: [...] }]
137
+ ```
138
+
139
+ #### `.getTypeAssertions()`
140
+
141
+ Get all type assertions (for type generation).
142
+
143
+ ```javascript
144
+ const assertions = t.getTypeAssertions();
145
+ // [{ path: 'domain.todos.items', type: 'array', elementShape: {...} }]
146
+ ```
147
+
148
+ ## Workflow
149
+
150
+ ### 1. Write Tests (TDD)
151
+
152
+ ```javascript
153
+ // tests/todos.test.js
154
+ test('add todo', () => {
155
+ const t = createEventTest({ domain: { todos: { items: [] } } });
156
+ t.trigger('intent.todo.add', { text: 'Buy milk' });
157
+ t.assertArrayOf('domain.todos.items', {
158
+ id: 'number',
159
+ text: 'string',
160
+ done: 'boolean'
161
+ });
162
+ });
163
+ ```
164
+
165
+ ### 2. Implement Bridges
166
+
167
+ ```javascript
168
+ // app/bridges.js
169
+ store.subscribe('intent.todo.add', ({ text }) => {
170
+ const items = store.get('domain.todos.items') || [];
171
+ const id = items.length + 1;
172
+ store.set('domain.todos.items', [...items, { id, text, done: false }]);
173
+ });
174
+ ```
175
+
176
+ ### 3. Generate Types
177
+
178
+ ```bash
179
+ node tests/generateTypes.js
180
+ # Creates store.d.ts with perfect types!
181
+ ```
182
+
183
+ ### 4. Build UI
184
+
185
+ ```javascript
186
+ // Now you have autocomplete!
187
+ const items = store.get('domain.todos.items');
188
+ // TypeScript knows: Array<{ id: number, text: string, done: boolean }>
189
+ ```
190
+
191
+ ## Benefits
192
+
193
+ - **Tests define behavior** - TDD workflow
194
+ - **Types derived from tests** - No manual type definitions
195
+ - **Perfect coverage** - Every test adds type information
196
+ - **No DOM needed** - Fast feedback loop
197
+ - **Deterministic** - Same tests = same types
198
+ - **Watcher-friendly** - Auto-regenerate on test changes
199
+
200
+ ## Philosophy
201
+
202
+ EventTest follows EventState's core principles:
203
+
204
+ 1. **State-first** - Tests work with state paths
205
+ 2. **Dot-paths everywhere** - No special syntax
206
+ 3. **Types are optional** - Tests work without TypeScript
207
+ 4. **Types are derived** - Not hand-written
208
+ 5. **JS-first** - Clean, readable code