@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.
- package/README.md +40 -0
- package/cssState/.gitkeep +0 -0
- package/eventState/001-counter/README.md +44 -0
- package/eventState/001-counter/index.html +33 -0
- package/eventState/002-counter-improved/README.md +44 -0
- package/eventState/002-counter-improved/index.html +47 -0
- package/eventState/003-input-reactive/README.md +44 -0
- package/eventState/003-input-reactive/index.html +33 -0
- package/eventState/004-computed-state/README.md +45 -0
- package/eventState/004-computed-state/index.html +65 -0
- package/eventState/005-conditional-rendering/README.md +42 -0
- package/eventState/005-conditional-rendering/index.html +39 -0
- package/eventState/006-list-rendering/README.md +49 -0
- package/eventState/006-list-rendering/index.html +63 -0
- package/eventState/007-form-validation/README.md +52 -0
- package/eventState/007-form-validation/index.html +102 -0
- package/eventState/008-undo-redo/README.md +70 -0
- package/eventState/008-undo-redo/index.html +108 -0
- package/eventState/009-localStorage-side-effects/README.md +72 -0
- package/eventState/009-localStorage-side-effects/index.html +57 -0
- package/eventState/010-decoupled-components/README.md +74 -0
- package/eventState/010-decoupled-components/index.html +93 -0
- package/eventState/011-async-patterns/README.md +98 -0
- package/eventState/011-async-patterns/index.html +132 -0
- package/eventState/028-counter-improved-eventTest/LICENSE +55 -0
- package/eventState/028-counter-improved-eventTest/README.md +131 -0
- package/eventState/028-counter-improved-eventTest/app/store.js +9 -0
- package/eventState/028-counter-improved-eventTest/index.html +49 -0
- package/eventState/028-counter-improved-eventTest/runtime/core/behaviors.runtime.js +282 -0
- package/eventState/028-counter-improved-eventTest/runtime/core/eventState.js +100 -0
- package/eventState/028-counter-improved-eventTest/runtime/core/eventStateNew.js +149 -0
- package/eventState/028-counter-improved-eventTest/runtime/core/helpers.js +212 -0
- package/eventState/028-counter-improved-eventTest/runtime/core/router.js +271 -0
- package/eventState/028-counter-improved-eventTest/store.d.ts +8 -0
- package/eventState/028-counter-improved-eventTest/style.css +170 -0
- package/eventState/028-counter-improved-eventTest/tests/README.md +208 -0
- package/eventState/028-counter-improved-eventTest/tests/counter.test.js +116 -0
- package/eventState/028-counter-improved-eventTest/tests/eventTest.js +176 -0
- package/eventState/028-counter-improved-eventTest/tests/generateTypes.js +168 -0
- package/eventState/028-counter-improved-eventTest/tests/run.js +20 -0
- package/eventState/030-todo-app-with-eventTest/LICENSE +55 -0
- package/eventState/030-todo-app-with-eventTest/README.md +121 -0
- package/eventState/030-todo-app-with-eventTest/app/router.js +25 -0
- package/eventState/030-todo-app-with-eventTest/app/store.js +16 -0
- package/eventState/030-todo-app-with-eventTest/app/views/home.js +11 -0
- package/eventState/030-todo-app-with-eventTest/app/views/todoDemo.js +88 -0
- package/eventState/030-todo-app-with-eventTest/index.html +65 -0
- package/eventState/030-todo-app-with-eventTest/runtime/core/behaviors.runtime.js +282 -0
- package/eventState/030-todo-app-with-eventTest/runtime/core/eventState.js +100 -0
- package/eventState/030-todo-app-with-eventTest/runtime/core/eventStateNew.js +149 -0
- package/eventState/030-todo-app-with-eventTest/runtime/core/helpers.js +212 -0
- package/eventState/030-todo-app-with-eventTest/runtime/core/router.js +271 -0
- package/eventState/030-todo-app-with-eventTest/store.d.ts +18 -0
- package/eventState/030-todo-app-with-eventTest/style.css +170 -0
- package/eventState/030-todo-app-with-eventTest/tests/README.md +208 -0
- package/eventState/030-todo-app-with-eventTest/tests/eventTest.js +176 -0
- package/eventState/030-todo-app-with-eventTest/tests/generateTypes.js +189 -0
- package/eventState/030-todo-app-with-eventTest/tests/run.js +20 -0
- package/eventState/030-todo-app-with-eventTest/tests/todos.test.js +167 -0
- package/eventState/031-todo-app-with-eventTest/LICENSE +55 -0
- package/eventState/031-todo-app-with-eventTest/README.md +54 -0
- package/eventState/031-todo-app-with-eventTest/TUTORIAL.md +390 -0
- package/eventState/031-todo-app-with-eventTest/WHY_EVENTSTATE.md +777 -0
- package/eventState/031-todo-app-with-eventTest/app/bridges.js +113 -0
- package/eventState/031-todo-app-with-eventTest/app/router.js +26 -0
- package/eventState/031-todo-app-with-eventTest/app/store.js +15 -0
- package/eventState/031-todo-app-with-eventTest/app/views/home.js +46 -0
- package/eventState/031-todo-app-with-eventTest/app/views/todoDemo.js +69 -0
- package/eventState/031-todo-app-with-eventTest/devtools/dock.js +41 -0
- package/eventState/031-todo-app-with-eventTest/devtools/stateTracker.dock.js +10 -0
- package/eventState/031-todo-app-with-eventTest/devtools/stateTracker.js +246 -0
- package/eventState/031-todo-app-with-eventTest/devtools/telemetry.js +104 -0
- package/eventState/031-todo-app-with-eventTest/devtools/typeGenerator.js +339 -0
- package/eventState/031-todo-app-with-eventTest/index.html +103 -0
- package/eventState/031-todo-app-with-eventTest/package-lock.json +2184 -0
- package/eventState/031-todo-app-with-eventTest/package.json +24 -0
- package/eventState/031-todo-app-with-eventTest/runtime/core/behaviors.runtime.js +282 -0
- package/eventState/031-todo-app-with-eventTest/runtime/core/eventState.js +100 -0
- package/eventState/031-todo-app-with-eventTest/runtime/core/eventStateNew.js +149 -0
- package/eventState/031-todo-app-with-eventTest/runtime/core/helpers.js +212 -0
- package/eventState/031-todo-app-with-eventTest/runtime/core/router.js +271 -0
- package/eventState/031-todo-app-with-eventTest/runtime/extensions/boundary.js +36 -0
- package/eventState/031-todo-app-with-eventTest/runtime/extensions/converge.js +63 -0
- package/eventState/031-todo-app-with-eventTest/runtime/extensions/eventState.plus.js +210 -0
- package/eventState/031-todo-app-with-eventTest/runtime/extensions/hydrate.js +157 -0
- package/eventState/031-todo-app-with-eventTest/runtime/extensions/queryBinding.js +69 -0
- package/eventState/031-todo-app-with-eventTest/runtime/forms/computed.js +78 -0
- package/eventState/031-todo-app-with-eventTest/runtime/forms/meta.js +51 -0
- package/eventState/031-todo-app-with-eventTest/runtime/forms/submitWithBoundary.js +28 -0
- package/eventState/031-todo-app-with-eventTest/runtime/forms/validators.js +55 -0
- package/eventState/031-todo-app-with-eventTest/store.d.ts +23 -0
- package/eventState/031-todo-app-with-eventTest/style.css +170 -0
- package/eventState/031-todo-app-with-eventTest/tests/README.md +208 -0
- package/eventState/031-todo-app-with-eventTest/tests/eventTest.js +176 -0
- package/eventState/031-todo-app-with-eventTest/tests/generateTypes.js +191 -0
- package/eventState/031-todo-app-with-eventTest/tests/run.js +20 -0
- package/eventState/031-todo-app-with-eventTest/tests/todos.test.js +192 -0
- package/eventState/032-todo-app-with-eventTest/LICENSE +55 -0
- package/eventState/032-todo-app-with-eventTest/README.md +54 -0
- package/eventState/032-todo-app-with-eventTest/TUTORIAL.md +390 -0
- package/eventState/032-todo-app-with-eventTest/WHY_EVENTSTATE.md +777 -0
- package/eventState/032-todo-app-with-eventTest/app/actions/index.js +153 -0
- package/eventState/032-todo-app-with-eventTest/app/bridges.js +113 -0
- package/eventState/032-todo-app-with-eventTest/app/router.js +26 -0
- package/eventState/032-todo-app-with-eventTest/app/store.js +15 -0
- package/eventState/032-todo-app-with-eventTest/app/views/home.js +46 -0
- package/eventState/032-todo-app-with-eventTest/app/views/todoDemo.js +69 -0
- package/eventState/032-todo-app-with-eventTest/devtools/dock.js +41 -0
- package/eventState/032-todo-app-with-eventTest/devtools/stateTracker.dock.js +10 -0
- package/eventState/032-todo-app-with-eventTest/devtools/stateTracker.js +246 -0
- package/eventState/032-todo-app-with-eventTest/devtools/telemetry.js +104 -0
- package/eventState/032-todo-app-with-eventTest/devtools/typeGenerator.js +339 -0
- package/eventState/032-todo-app-with-eventTest/index.html +87 -0
- package/eventState/032-todo-app-with-eventTest/package-lock.json +2184 -0
- package/eventState/032-todo-app-with-eventTest/package.json +24 -0
- package/eventState/032-todo-app-with-eventTest/runtime/core/behaviors.runtime.js +282 -0
- package/eventState/032-todo-app-with-eventTest/runtime/core/eventState.js +100 -0
- package/eventState/032-todo-app-with-eventTest/runtime/core/eventStateNew.js +149 -0
- package/eventState/032-todo-app-with-eventTest/runtime/core/helpers.js +212 -0
- package/eventState/032-todo-app-with-eventTest/runtime/core/router.js +271 -0
- package/eventState/032-todo-app-with-eventTest/runtime/extensions/boundary.js +36 -0
- package/eventState/032-todo-app-with-eventTest/runtime/extensions/converge.js +63 -0
- package/eventState/032-todo-app-with-eventTest/runtime/extensions/eventState.plus.js +210 -0
- package/eventState/032-todo-app-with-eventTest/runtime/extensions/hydrate.js +157 -0
- package/eventState/032-todo-app-with-eventTest/runtime/extensions/queryBinding.js +69 -0
- package/eventState/032-todo-app-with-eventTest/runtime/forms/computed.js +78 -0
- package/eventState/032-todo-app-with-eventTest/runtime/forms/meta.js +51 -0
- package/eventState/032-todo-app-with-eventTest/runtime/forms/submitWithBoundary.js +28 -0
- package/eventState/032-todo-app-with-eventTest/runtime/forms/validators.js +55 -0
- package/eventState/032-todo-app-with-eventTest/store.d.ts +23 -0
- package/eventState/032-todo-app-with-eventTest/style.css +170 -0
- package/eventState/032-todo-app-with-eventTest/tests/README.md +208 -0
- package/eventState/032-todo-app-with-eventTest/tests/eventTest.js +176 -0
- package/eventState/032-todo-app-with-eventTest/tests/generateTypes.js +191 -0
- package/eventState/032-todo-app-with-eventTest/tests/run.js +20 -0
- package/eventState/032-todo-app-with-eventTest/tests/todos.test.js +192 -0
- 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 };
|