@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,246 @@
1
+ // stateTracker.js — OCP-friendly utility widget
2
+ // Purpose: When imported, renders a floating button you can click to cycle between
3
+ // the four corners of the viewport. Double-click toggles a full-height sidebar
4
+ // that opens on the left or right depending on the button's current horizontal corner.
5
+ //
6
+ // Design:
7
+ // - No external deps. Pure JS + a single <style> tag for cosmetics.
8
+ // - Open/Closed: exposes installStateTracker(opts) returning an uninstall function.
9
+ // Auto-installs on import with defaults, but can be disabled via global flag.
10
+ // - Minimal footprint and no global CSS leakage (scoped class names).
11
+
12
+ (function(){
13
+ const AUTO_INSTALL = true; // set to false if you prefer manual install only
14
+
15
+ const STYLE_CSS = `
16
+ .stt-pill { pointer-events: auto; position: fixed; display: inline-flex; gap: 6px; padding: 4px; border-radius: 999px;
17
+ background: rgba(17,17,17,.9); backdrop-filter: saturate(120%) blur(4px); box-shadow: 0 2px 10px rgba(0,0,0,.25);
18
+ border: 1px solid rgba(255,255,255,.12); z-index: 2147483600;
19
+ }
20
+ .stt-btn { width: 36px; height: 36px; border-radius: 18px; display: grid; place-items: center;
21
+ font: 600 12px/1 system-ui, -apple-system, Segoe UI, Roboto, sans-serif; color: #fff; background: #111;
22
+ border: 1px solid rgba(255,255,255,.15); cursor: pointer; user-select: none;
23
+ }
24
+ .stt-btn:hover { background: #1a1a1a; }
25
+ .stt-btn:active { transform: translateY(1px); }
26
+
27
+ .stt-corner-top-left { top: 12px; left: 12px; }
28
+ .stt-corner-top-right { top: 12px; right: 12px; }
29
+ .stt-corner-bottom-right { bottom: 12px; right: 12px; }
30
+ .stt-corner-bottom-left { bottom: 12px; left: 12px; }
31
+
32
+ .stt-sidebar { pointer-events: auto; position: fixed; top: 0; height: 100vh; width: min(80vw, 320px);
33
+ background: var(--stt-sidebar-bg, #181818); color: var(--stt-sidebar-fg, #eee);
34
+ border-inline: 1px solid rgba(255,255,255,.12); box-shadow: 0 0 24px rgba(0,0,0,.25);
35
+ transform: translateX(var(--stt-x, 0)); transition: transform 180ms ease, opacity 180ms ease;
36
+ opacity: var(--stt-opacity, 0); will-change: transform, opacity; z-index: 2147483600;
37
+ }
38
+ .stt-sidebar.right { right: 0; --stt-x: 100%; }
39
+ .stt-sidebar.left { left: 0; --stt-x: -100%; }
40
+ .stt-open .stt-sidebar { --stt-x: 0; --stt-opacity: 1; }
41
+
42
+ .stt-sidebar header { padding: 8px 10px; font-weight: 700; border-bottom: 1px solid rgba(255,255,255,.08);
43
+ display: flex; align-items: center; justify-content: space-between; gap: 8px; }
44
+ .stt-header-title { font-size: 12px; font-weight: 700; letter-spacing: .3px; opacity: .9; }
45
+ .stt-header-actions { display: inline-flex; gap: 6px; }
46
+ .stt-icon-btn { width: 26px; height: 26px; border-radius: 6px; display: grid; place-items: center;
47
+ font: 600 12px/1 system-ui, -apple-system, Segoe UI, Roboto, sans-serif; color: #eee; background: #222;
48
+ border: 1px solid rgba(255,255,255,.12); cursor: pointer; user-select: none; }
49
+ .stt-icon-btn:hover { background: #2a2a2a; }
50
+ .stt-sidebar .stt-content { padding: 12px 14px; font-size: 12px; line-height: 1.35;
51
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
52
+ white-space: pre; overflow: auto; max-height: calc(100vh - 48px);
53
+ }
54
+ /* Inline tools inside sidebar */
55
+ .stt-row { display: flex; gap: 6px; padding: 6px 0; flex-wrap: wrap; }
56
+ .stt-btn-sm { height: 28px; padding: 4px 10px; border-radius: 6px;
57
+ font: 600 12px/1 system-ui, -apple-system, Segoe UI, Roboto, sans-serif; color: #eee; background: #222;
58
+ border: 1px solid rgba(255,255,255,.12); cursor: pointer; user-select: none; }
59
+ .stt-btn-sm:hover { background: #2a2a2a; }
60
+ `;
61
+
62
+ const CORNERS = ['top-left','top-right','bottom-right','bottom-left'];
63
+
64
+ function createEl(tag, cls){ const el = document.createElement(tag); if (cls) el.className = cls; return el; }
65
+
66
+ function installStateTracker({
67
+ corner = 'bottom-right',
68
+ appendTo = document.body,
69
+ title = 'ST',
70
+ store = undefined,
71
+ pathPrefix = 'ui.stateTracker',
72
+ } = {}){
73
+ if (!appendTo) appendTo = document.body;
74
+ // Allow global opt-in binding without importing store here (keeps OCP)
75
+ if (!store && typeof window !== 'undefined' && window.stateTrackerStore){
76
+ store = window.stateTrackerStore;
77
+ }
78
+
79
+ // Inject style once
80
+ let styleEl = document.getElementById('stt-style');
81
+ if (!styleEl){
82
+ styleEl = document.createElement('style');
83
+ styleEl.id = 'stt-style';
84
+ styleEl.textContent = STYLE_CSS;
85
+ document.head.appendChild(styleEl);
86
+ }
87
+
88
+ const pill = createEl('div', 'stt-pill');
89
+ const btnMove = createEl('button', 'stt-btn stt-btn-move');
90
+ btnMove.type = 'button'; btnMove.title = 'Move'; btnMove.textContent = '◷';
91
+ const btnToggle = createEl('button', 'stt-btn stt-btn-toggle');
92
+ btnToggle.type = 'button'; btnToggle.title = 'Toggle'; btnToggle.textContent = title;
93
+ pill.appendChild(btnMove);
94
+ pill.appendChild(btnToggle);
95
+
96
+ const sidebar = createEl('aside', 'stt-sidebar');
97
+ const header = createEl('header');
98
+ const hTitle = createEl('div', 'stt-header-title'); hTitle.textContent = 'State Tracker';
99
+ const hActions = createEl('div', 'stt-header-actions');
100
+ const btnCopy = createEl('button', 'stt-icon-btn'); btnCopy.type = 'button'; btnCopy.title = 'Copy state'; btnCopy.textContent = '⎘';
101
+ const btnClose = createEl('button', 'stt-icon-btn'); btnClose.type = 'button'; btnClose.title = 'Close'; btnClose.textContent = '×';
102
+ hActions.appendChild(btnCopy); hActions.appendChild(btnClose);
103
+ header.appendChild(hTitle); header.appendChild(hActions);
104
+ const content = createEl('div', 'stt-content');
105
+ // Helper to create a button with safe handler
106
+ function mkBtn(label, title, handler){
107
+ const b = document.createElement('button');
108
+ b.className = 'stt-btn-sm'; b.type = 'button'; b.textContent = label; if (title) b.title = title;
109
+ b.addEventListener('click', (e) => { try { handler && handler(e); } catch(err){ console.warn('[stateTracker]', err); } });
110
+ return b;
111
+ }
112
+
113
+ // State JSON directly visible (no accordion)
114
+ const pre = document.createElement('pre');
115
+ pre.className = 'stt-pre';
116
+ pre.textContent = 'Loading state…';
117
+ content.appendChild(pre);
118
+ sidebar.appendChild(header); sidebar.appendChild(content);
119
+
120
+ document.body.appendChild(pill);
121
+ document.body.appendChild(sidebar);
122
+
123
+ function applyCorner(){
124
+ pill.classList.remove(
125
+ 'stt-corner-top-left','stt-corner-top-right','stt-corner-bottom-right','stt-corner-bottom-left'
126
+ );
127
+ const cls = `stt-corner-${corner}`;
128
+ pill.classList.add(cls);
129
+ const isRight = corner.includes('right');
130
+ sidebar.classList.toggle('right', isRight);
131
+ sidebar.classList.toggle('left', !isRight);
132
+ }
133
+
134
+ let open = false;
135
+ function setOpen(v){
136
+ open = !!v;
137
+ document.documentElement.classList.toggle('stt-open', open);
138
+ if (store && typeof store.set === 'function'){
139
+ try { store.set(`${pathPrefix}.open`, open); } catch {}
140
+ }
141
+ }
142
+
143
+ applyCorner();
144
+
145
+ // Render full state tree (if store provided), throttled to animation frame
146
+ let scheduled = false;
147
+ function renderState(){
148
+ if (!store) return;
149
+ try {
150
+ const obj = store.get();
151
+ pre.textContent = JSON.stringify(obj, null, 2);
152
+ } catch (e) {
153
+ pre.textContent = '[stateTracker] Unable to render state: ' + (e && e.message ? e.message : e);
154
+ }
155
+ }
156
+ function scheduleRender(){
157
+ if (scheduled) return; scheduled = true;
158
+ requestAnimationFrame(() => { scheduled = false; renderState(); });
159
+ }
160
+ if (store){ renderState(); }
161
+
162
+ // Left button: move between corners
163
+ btnMove.addEventListener('click', (e) => {
164
+ console.log('[stateTracker] move click', { corner });
165
+ const idx = CORNERS.indexOf(corner);
166
+ corner = CORNERS[(idx + 1) % CORNERS.length];
167
+ applyCorner();
168
+ if (store && typeof store.set === 'function'){
169
+ try { store.set(`${pathPrefix}.corner`, corner); } catch {}
170
+ }
171
+ });
172
+
173
+ // Right button: toggle sidebar
174
+ btnToggle.addEventListener('click', (e) => {
175
+ console.log('[stateTracker] toggle click', { openBefore: open });
176
+ setOpen(!open);
177
+ console.log('[stateTracker] toggle result', { openAfter: open });
178
+ if (open && store) scheduleRender();
179
+ });
180
+
181
+ // Close via header close button or ESC
182
+ btnClose.addEventListener('click', () => setOpen(false));
183
+ window.addEventListener('keydown', (e) => { if (e.key === 'Escape') setOpen(false); });
184
+
185
+ // Copy state structure
186
+ async function copyState(){
187
+ try {
188
+ // ensure latest state text
189
+ renderState();
190
+ const text = pre.textContent || '';
191
+ if (navigator.clipboard && navigator.clipboard.writeText){
192
+ await navigator.clipboard.writeText(text);
193
+ } else {
194
+ const ta = document.createElement('textarea');
195
+ ta.value = text; document.body.appendChild(ta); ta.select();
196
+ document.execCommand('copy'); ta.remove();
197
+ }
198
+ const old = btnCopy.textContent; btnCopy.textContent = '✓';
199
+ setTimeout(() => { btnCopy.textContent = old; }, 900);
200
+ } catch(err){
201
+ console.warn('[stateTracker] copy failed', err);
202
+ }
203
+ }
204
+ btnCopy.addEventListener('click', copyState);
205
+
206
+ // Subscribe to all store changes to refresh view (if store provided)
207
+ let unsubscribe = null;
208
+ if (store && typeof store.subscribe === 'function'){
209
+ try {
210
+ unsubscribe = store.subscribe('*', () => { if (open) scheduleRender(); });
211
+ } catch {}
212
+ }
213
+
214
+ // Clean up function
215
+ const uninstall = () => {
216
+ try { pill.remove(); } catch {}
217
+ try { sidebar.remove(); } catch {}
218
+ window.removeEventListener('keydown', () => {});
219
+ if (unsubscribe){ try { unsubscribe(); } catch {} }
220
+ };
221
+
222
+ return {
223
+ setCorner(next){ if (CORNERS.includes(next)){ corner = next; applyCorner(); } },
224
+ getCorner(){ return corner; },
225
+ isOpen(){ return open; },
226
+ open(){ setOpen(true); },
227
+ close(){ setOpen(false); },
228
+ toggle(){ setOpen(!open); },
229
+ uninstall,
230
+ elements: { pill, btnMove, btnToggle, sidebar },
231
+ };
232
+ }
233
+
234
+ // Auto-install on import (but still export installer)
235
+ const api = { installStateTracker };
236
+ if (AUTO_INSTALL) {
237
+ // Guard against SSR/non-DOM contexts
238
+ if (typeof document !== 'undefined' && document.body){
239
+ api.instance = installStateTracker();
240
+ }
241
+ }
242
+
243
+ // UMD-lite export
244
+ if (typeof module !== 'undefined' && module.exports){ module.exports = api; }
245
+ else if (typeof window !== 'undefined'){ window.stateTracker = api; }
246
+ })();
@@ -0,0 +1,104 @@
1
+ // telemetry.dev.js — dev-only console buffer + copy-to-clipboard button
2
+ // Safe to include in production: it will bail out immediately when not in DEV.
3
+
4
+ import store from '../app/store.js';
5
+
6
+ // if (import.meta && import.meta.env && import.meta.env.DEV) {
7
+ const MAX = 500;
8
+ const buf = [];
9
+ const orig = {
10
+ log: console.log.bind(console),
11
+ warn: console.warn.bind(console),
12
+ error: console.error.bind(console),
13
+ info: console.info.bind(console),
14
+ debug: console.debug.bind(console),
15
+ };
16
+
17
+ function push(level, args){
18
+ buf.push({ t: Date.now(), level, args: Array.from(args) });
19
+ if (buf.length > MAX) buf.shift();
20
+ }
21
+
22
+ console.log = (...a) => { push('log', a); orig.log(...a); };
23
+ console.warn = (...a) => { push('warn', a); orig.warn(...a); };
24
+ console.error = (...a) => { push('error', a); orig.error(...a); };
25
+ console.info = (...a) => { push('info', a); orig.info(...a); };
26
+ console.debug = (...a) => { push('debug', a); orig.debug(...a); };
27
+
28
+ // Expose a simple API
29
+ window.__telemetry = {
30
+ get: () => buf.slice(),
31
+ clear: () => { buf.length = 0; },
32
+ copy: async () => {
33
+ try {
34
+ const text = JSON.stringify(buf, null, 2);
35
+ await navigator.clipboard.writeText(text);
36
+ orig.info('[telemetry] Copied console buffer to clipboard (', buf.length, 'entries )');
37
+ } catch (e) {
38
+ orig.warn('[telemetry] Clipboard copy failed', e);
39
+ }
40
+ }
41
+ };
42
+
43
+ // Register with shared dev dock if available
44
+ if (window.__devdock && typeof window.__devdock.register === 'function'){
45
+ window.__devdock.register({ id: 'copy-console', label: 'Console', title: 'Copy console buffer', onClick: () => window.__telemetry.copy() });
46
+ } else {
47
+ // Fallback: simple floating button
48
+ const dockId = 'dev-tools-dock';
49
+ let dock = document.getElementById(dockId);
50
+ if (!dock){
51
+ dock = document.createElement('div');
52
+ dock.id = dockId;
53
+ Object.assign(dock.style, {
54
+ position: 'fixed', left: '10px', bottom: '10px', zIndex: 9999,
55
+ display: 'flex', gap: '6px', alignItems: 'center',
56
+ });
57
+ document.body.appendChild(dock);
58
+ }
59
+ const btn = document.createElement('button');
60
+ btn.type = 'button';
61
+ btn.textContent = 'Copy Console';
62
+ Object.assign(btn.style, {
63
+ appearance: 'none', border: '1px solid #ddd', background: '#fff',
64
+ borderRadius: '6px', padding: '6px 10px', cursor: 'pointer',
65
+ boxShadow: '0 1px 2px rgba(0,0,0,0.06)'
66
+ });
67
+ btn.addEventListener('click', () => window.__telemetry.copy());
68
+ dock.appendChild(btn);
69
+ }
70
+
71
+ // ============================================
72
+ // SITEWIDE TELEMETRY: Log all state changes
73
+ // ============================================
74
+ if (typeof window !== 'undefined') {
75
+ if (!store) {
76
+ orig.error('[telemetry] Store is undefined!');
77
+ } else if (!store.subscribe) {
78
+ orig.error('[telemetry] Store has no subscribe method!', store);
79
+ } else {
80
+ // Log all state changes (except noisy ones)
81
+ store.subscribe('*', (detail) => {
82
+ const { path, value } = detail;
83
+
84
+ // Skip transitioning state (too noisy)
85
+ if (path === 'ui.route.transitioning') return;
86
+
87
+ // Skip intent paths here (logged separately below)
88
+ if (path.startsWith('intent.')) return;
89
+
90
+ // Format like existing telemetry
91
+ console.log(`[state] ${path}`, value);
92
+ });
93
+
94
+ // Log all intents separately (more prominent)
95
+ store.subscribe('intent.*', (detail) => {
96
+ const { path, value } = detail;
97
+ const intentName = path.replace('intent.', '');
98
+ console.log(`[intent] ${intentName}`, value);
99
+ });
100
+
101
+ orig.info('[telemetry] Sitewide state tracking enabled');
102
+ }
103
+ }
104
+ // }
@@ -0,0 +1,339 @@
1
+ // typeGenerator.js — Generate TypeScript definitions from telemetry
2
+ // Analyzes runtime telemetry to extract state shape and generate .d.ts files
3
+
4
+ import store from '../app/store.js';
5
+
6
+ class TypeGenerator {
7
+ constructor() {
8
+ this.pathTypes = new Map(); // path → type info
9
+ this.pathSamples = new Map(); // path → sample values
10
+ }
11
+
12
+ /**
13
+ * Observe a state change and infer its type
14
+ */
15
+ observe(path, value) {
16
+ const type = this.inferType(value);
17
+
18
+ // Store sample for complex types
19
+ if (typeof value === 'object' && value !== null) {
20
+ if (!this.pathSamples.has(path)) {
21
+ this.pathSamples.set(path, []);
22
+ }
23
+ const samples = this.pathSamples.get(path);
24
+ if (samples.length < 10) {
25
+ samples.push(value);
26
+ }
27
+ }
28
+
29
+ // Merge with existing type info
30
+ if (this.pathTypes.has(path)) {
31
+ const existing = this.pathTypes.get(path);
32
+ this.pathTypes.set(path, this.mergeTypes(existing, type));
33
+ } else {
34
+ this.pathTypes.set(path, type);
35
+ }
36
+ }
37
+
38
+ /**
39
+ * Infer TypeScript type from a value
40
+ */
41
+ inferType(value) {
42
+ if (value === null) return 'null';
43
+ if (value === undefined) return 'undefined';
44
+
45
+ if (Array.isArray(value)) {
46
+ if (value.length === 0) return 'Array<unknown>';
47
+
48
+ // Sample first few elements and merge their types
49
+ const samples = value.slice(0, 10);
50
+ let mergedElementType = null;
51
+
52
+ for (const sample of samples) {
53
+ const elementType = this.inferType(sample);
54
+ if (mergedElementType === null) {
55
+ mergedElementType = elementType;
56
+ } else {
57
+ mergedElementType = this.mergeTypes(mergedElementType, elementType);
58
+ }
59
+ }
60
+
61
+ return `Array<${this.typeToString(mergedElementType)}>`;
62
+ }
63
+
64
+ if (typeof value === 'object') {
65
+ // Infer object shape
66
+ const shape = {};
67
+ for (const [key, val] of Object.entries(value)) {
68
+ shape[key] = this.inferType(val);
69
+ }
70
+ return shape;
71
+ }
72
+
73
+ // Primitives
74
+ if (typeof value === 'string') return 'string';
75
+ if (typeof value === 'number') return 'number';
76
+ if (typeof value === 'boolean') return 'boolean';
77
+
78
+ return 'unknown';
79
+ }
80
+
81
+ /**
82
+ * Convert type representation to string
83
+ */
84
+ typeToString(type) {
85
+ if (typeof type === 'string') return type;
86
+
87
+ if (typeof type === 'object' && !Array.isArray(type)) {
88
+ const props = Object.entries(type)
89
+ .map(([k, v]) => `${k}: ${this.typeToString(v)}`)
90
+ .join('; ');
91
+ return `{ ${props} }`;
92
+ }
93
+
94
+ return 'unknown';
95
+ }
96
+
97
+ /**
98
+ * Merge two type definitions (for union types)
99
+ */
100
+ mergeTypes(type1, type2) {
101
+ // Normalize for comparison
102
+ const str1 = this.normalizeType(type1);
103
+ const str2 = this.normalizeType(type2);
104
+
105
+ if (str1 === str2) return type1;
106
+
107
+ // Handle string types
108
+ if (typeof type1 === 'string' && typeof type2 === 'string') {
109
+ if (type1 === type2) return type1;
110
+ // Avoid duplicate unions
111
+ if (type1.includes(type2)) return type1;
112
+ if (type2.includes(type1)) return type2;
113
+ return `${type1} | ${type2}`;
114
+ }
115
+
116
+ // Handle object merging (merge properties, not create union)
117
+ if (typeof type1 === 'object' && typeof type2 === 'object' &&
118
+ !Array.isArray(type1) && !Array.isArray(type2)) {
119
+ const merged = { ...type1 };
120
+ for (const [key, val] of Object.entries(type2)) {
121
+ if (key in merged) {
122
+ merged[key] = this.mergeTypes(merged[key], val);
123
+ } else {
124
+ merged[key] = val;
125
+ }
126
+ }
127
+ return merged;
128
+ }
129
+
130
+ // Mixed types: create union
131
+ if (typeof type1 === 'string' && typeof type2 === 'object') {
132
+ return `${type1} | ${this.typeToString(type2)}`;
133
+ }
134
+ if (typeof type1 === 'object' && typeof type2 === 'string') {
135
+ return `${this.typeToString(type1)} | ${type2}`;
136
+ }
137
+
138
+ return type1; // Default: keep first type
139
+ }
140
+
141
+ /**
142
+ * Normalize type for comparison
143
+ */
144
+ normalizeType(type) {
145
+ if (typeof type === 'string') return type;
146
+ return JSON.stringify(type, Object.keys(type).sort());
147
+ }
148
+
149
+ /**
150
+ * Build hierarchical tree from flat paths
151
+ */
152
+ buildTree() {
153
+ const tree = {};
154
+
155
+ for (const [path, type] of this.pathTypes) {
156
+ const parts = path.split('.');
157
+ let current = tree;
158
+
159
+ for (let i = 0; i < parts.length; i++) {
160
+ const part = parts[i];
161
+
162
+ if (i === parts.length - 1) {
163
+ // Leaf node
164
+ current[part] = { __type: type };
165
+ } else {
166
+ // Branch node
167
+ if (!current[part]) {
168
+ current[part] = {};
169
+ }
170
+ current = current[part];
171
+ }
172
+ }
173
+ }
174
+
175
+ return tree;
176
+ }
177
+
178
+ /**
179
+ * Generate TypeScript interface from type info
180
+ */
181
+ generateInterface(obj, indent = 0) {
182
+ const lines = [];
183
+ const spaces = ' '.repeat(indent);
184
+
185
+ for (const [key, value] of Object.entries(obj)) {
186
+ if (value.__type !== undefined) {
187
+ // Leaf node with type
188
+ const type = this.formatType(value.__type);
189
+ lines.push(`${spaces}${key}: ${type};`);
190
+ } else {
191
+ // Nested object
192
+ lines.push(`${spaces}${key}: {`);
193
+ lines.push(this.generateInterface(value, indent + 1));
194
+ lines.push(`${spaces}};`);
195
+ }
196
+ }
197
+
198
+ return lines.join('\n');
199
+ }
200
+
201
+ /**
202
+ * Format type for TypeScript output
203
+ */
204
+ formatType(type) {
205
+ if (typeof type === 'string') {
206
+ return type;
207
+ }
208
+
209
+ if (typeof type === 'object' && !Array.isArray(type)) {
210
+ // Inline object type
211
+ const props = Object.entries(type)
212
+ .map(([k, v]) => `${k}: ${this.formatType(v)}`)
213
+ .join('; ');
214
+ return `{ ${props} }`;
215
+ }
216
+
217
+ return 'unknown';
218
+ }
219
+
220
+ /**
221
+ * Generate complete .d.ts file
222
+ */
223
+ generateDTS() {
224
+ const lines = [
225
+ '// Auto-generated from runtime telemetry',
226
+ '// DO NOT EDIT - regenerate by using the app and clicking "Generate Types"',
227
+ '',
228
+ 'export interface StoreState {',
229
+ ];
230
+
231
+ const tree = this.buildTree();
232
+ lines.push(this.generateInterface(tree, 1));
233
+ lines.push('}');
234
+ lines.push('');
235
+ lines.push('export default StoreState;');
236
+ lines.push('');
237
+
238
+ return lines.join('\n');
239
+ }
240
+
241
+ /**
242
+ * Parse telemetry buffer and extract types
243
+ */
244
+ parseBuffer(buffer) {
245
+ for (const entry of buffer) {
246
+ if (entry.level !== 'log') continue;
247
+
248
+ const args = entry.args;
249
+ if (!args || args.length < 2) continue;
250
+
251
+ const tag = args[0];
252
+
253
+ // Parse [state] entries
254
+ if (typeof tag === 'string' && tag.startsWith('[state]')) {
255
+ const path = tag.replace('[state] ', '');
256
+ const value = args[1];
257
+ this.observe(path, value);
258
+ }
259
+
260
+ // Parse [intent] entries
261
+ if (typeof tag === 'string' && tag.startsWith('[intent]')) {
262
+ const intentName = tag.replace('[intent] ', '');
263
+ const path = `intent.${intentName}`;
264
+ const value = args[1];
265
+ this.observe(path, value);
266
+ }
267
+ }
268
+ }
269
+
270
+ /**
271
+ * Parse test assertions and extract types
272
+ */
273
+ parseTestAssertions(assertions) {
274
+ for (const assertion of assertions) {
275
+ const { path, type, elementShape, shape } = assertion;
276
+
277
+ if (type === 'array' && elementShape) {
278
+ // Array type with element shape
279
+ this.pathTypes.set(path, `Array<${this.typeToString(elementShape)}>`);
280
+ } else if (type === 'object' && shape) {
281
+ // Object type with shape
282
+ this.pathTypes.set(path, shape);
283
+ } else if (type) {
284
+ // Primitive type
285
+ this.pathTypes.set(path, type);
286
+ }
287
+ }
288
+ }
289
+
290
+ /**
291
+ * Save .d.ts file (browser download)
292
+ */
293
+ save(filename = 'store.d.ts') {
294
+ const dts = this.generateDTS();
295
+ const blob = new Blob([dts], { type: 'text/plain' });
296
+ const url = URL.createObjectURL(blob);
297
+ const a = document.createElement('a');
298
+ a.href = url;
299
+ a.download = filename;
300
+ a.click();
301
+ URL.revokeObjectURL(url);
302
+ }
303
+ }
304
+
305
+ // Create and expose type generator
306
+ const typeGenerator = new TypeGenerator();
307
+ window.__typeGenerator = typeGenerator;
308
+
309
+ // Subscribe to all state changes for live type inference
310
+ if (store && store.subscribe) {
311
+ store.subscribe('*', (detail) => {
312
+ const { path, value } = detail;
313
+ typeGenerator.observe(path, value);
314
+ });
315
+
316
+ console.info('[typeGenerator] Live type inference enabled');
317
+ }
318
+
319
+ // Register with dev dock
320
+ if (window.__devdock && typeof window.__devdock.register === 'function') {
321
+ window.__devdock.register({
322
+ id: 'generate-types',
323
+ label: 'Types',
324
+ title: 'Generate TypeScript definitions',
325
+ onClick: () => {
326
+ // Also parse telemetry buffer for historical data
327
+ if (window.__telemetry) {
328
+ const buffer = window.__telemetry.get();
329
+ typeGenerator.parseBuffer(buffer);
330
+ }
331
+
332
+ typeGenerator.save('store.d.ts');
333
+ console.info('[typeGenerator] Generated store.d.ts');
334
+ }
335
+ });
336
+ }
337
+
338
+ export default typeGenerator;
339
+ export { TypeGenerator };