@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,113 @@
|
|
|
1
|
+
// bridges.app.js
|
|
2
|
+
// App-level intent → domain bridges and derived wildcard bridges
|
|
3
|
+
// Mirrors patterns proven in examples 300d (intent bridge) and 300e (derived wildcard).
|
|
4
|
+
|
|
5
|
+
import store from './store.js';
|
|
6
|
+
|
|
7
|
+
// ------------------------------
|
|
8
|
+
// Selection: intent → domain
|
|
9
|
+
// ------------------------------
|
|
10
|
+
store.subscribe('intent.selection.toggle', (payload) => {
|
|
11
|
+
const cur = store.get('ui.selection.ids') || [];
|
|
12
|
+
const set = new Set(Array.isArray(cur) ? cur : []);
|
|
13
|
+
let id, checked;
|
|
14
|
+
if (payload && typeof payload === 'object') { id = payload.id; checked = payload.checked; }
|
|
15
|
+
else { id = payload; checked = undefined; }
|
|
16
|
+
const key = String(id);
|
|
17
|
+
if (checked === undefined) {
|
|
18
|
+
// toggle membership
|
|
19
|
+
if (set.has(key)) set.delete(key); else set.add(key);
|
|
20
|
+
} else {
|
|
21
|
+
if (checked) set.add(key); else set.delete(key);
|
|
22
|
+
}
|
|
23
|
+
const next = Array.from(set);
|
|
24
|
+
store.set('ui.selection.ids', next);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
// Derived: selection count
|
|
28
|
+
store.subscribe('ui.selection.ids', () => {
|
|
29
|
+
const ids = store.get('ui.selection.ids') || [];
|
|
30
|
+
const next = Array.isArray(ids) ? ids.length : 0;
|
|
31
|
+
if (store.get('ui.selection.count') !== next) store.set('ui.selection.count', next);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// ------------------------------
|
|
35
|
+
// Quotes: async fetch with last-request-wins
|
|
36
|
+
// ------------------------------
|
|
37
|
+
let quotesReqId = 0;
|
|
38
|
+
store.subscribe('intent.quotes.load', ({ url, ttlMs }) => {
|
|
39
|
+
quotesReqId += 1;
|
|
40
|
+
const myId = quotesReqId;
|
|
41
|
+
store.set('ui.quotes.loading', true);
|
|
42
|
+
store.set('ui.quotes.error', null);
|
|
43
|
+
|
|
44
|
+
const doFetch = (globalThis.fetch ? globalThis.fetch(String(url)) : Promise.reject(new Error('fetch unavailable')));
|
|
45
|
+
doFetch
|
|
46
|
+
.then(async (res) => {
|
|
47
|
+
if (myId !== quotesReqId) return; // stale
|
|
48
|
+
if (!res.ok) throw new Error('HTTP ' + (res.status || 'error'));
|
|
49
|
+
const data = await res.json();
|
|
50
|
+
store.set('ui.quotes.data', data);
|
|
51
|
+
})
|
|
52
|
+
.catch((err) => {
|
|
53
|
+
if (myId !== quotesReqId) return; // stale
|
|
54
|
+
store.set('ui.quotes.error', String(err && err.message || err));
|
|
55
|
+
})
|
|
56
|
+
.finally(() => {
|
|
57
|
+
if (myId !== quotesReqId) return; // stale
|
|
58
|
+
store.set('ui.quotes.loading', false);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// Derived: quotes empty
|
|
63
|
+
const updateQuotesEmpty = () => {
|
|
64
|
+
const d = store.get('ui.quotes?.data') ?? store.get('ui.quotes.data');
|
|
65
|
+
const empty = Array.isArray(d) ? d.length === 0 : (d == null);
|
|
66
|
+
if (store.get('ui.quotes.empty') !== empty) store.set('ui.quotes.empty', empty);
|
|
67
|
+
};
|
|
68
|
+
store.subscribe('ui.quotes.data', updateQuotesEmpty);
|
|
69
|
+
store.subscribe('ui.quotes.error', updateQuotesEmpty);
|
|
70
|
+
|
|
71
|
+
// ------------------------------
|
|
72
|
+
// Items: derived helpers for repeaters
|
|
73
|
+
// ------------------------------
|
|
74
|
+
const updateItemsMeta = () => {
|
|
75
|
+
const items = store.get('ui.items');
|
|
76
|
+
const count = Array.isArray(items) ? items.length : 0;
|
|
77
|
+
if (store.get('ui.itemsCount') !== count) store.set('ui.itemsCount', count);
|
|
78
|
+
const empty = count === 0;
|
|
79
|
+
if (store.get('ui.empty') !== empty) store.set('ui.empty', empty);
|
|
80
|
+
};
|
|
81
|
+
store.subscribe('ui.items', updateItemsMeta);
|
|
82
|
+
try { updateItemsMeta(); } catch {}
|
|
83
|
+
|
|
84
|
+
// ------------------------------
|
|
85
|
+
// Todos: imperative renderer-friendly bridges
|
|
86
|
+
// ------------------------------
|
|
87
|
+
// Add todo
|
|
88
|
+
store.subscribe('intent.todo.add', ({ text }) => {
|
|
89
|
+
const items = store.get('domain.todos.items') || [];
|
|
90
|
+
const nextId = items.reduce((m, t) => Math.max(m, Number(t?.id || 0)), 0) + 1;
|
|
91
|
+
const todo = { id: nextId, text: String(text || '').trim(), done: false };
|
|
92
|
+
if (!todo.text) return;
|
|
93
|
+
store.set('domain.todos.items', [...items, todo]);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// Toggle todo
|
|
97
|
+
store.subscribe('intent.todo.toggle', ({ id }) => {
|
|
98
|
+
const items = store.get('domain.todos.items') || [];
|
|
99
|
+
const out = items.map(t => (String(t?.id) === String(id)) ? { ...t, done: !t.done } : t);
|
|
100
|
+
store.set('domain.todos.items', out);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// Clear completed
|
|
104
|
+
store.subscribe('intent.todo.clearCompleted', () => {
|
|
105
|
+
const items = store.get('domain.todos.items') || [];
|
|
106
|
+
store.set('domain.todos.items', items.filter(t => !t.done));
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// UI filter
|
|
110
|
+
store.subscribe('intent.ui.filter', ({ filter }) => {
|
|
111
|
+
const f = (filter === 'active' || filter === 'completed') ? filter : 'all';
|
|
112
|
+
store.set('ui.todos.filter', f);
|
|
113
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// app/router.js — App-specific router configuration
|
|
2
|
+
import { createRouter } from '../runtime/core/router.js';
|
|
3
|
+
import { upgradeEventState } from '../runtime/extensions/eventState.plus.js';
|
|
4
|
+
import store from './store.js';
|
|
5
|
+
import * as Home from './views/home.js';
|
|
6
|
+
import * as TodoDemo from './views/todoDemo.js';
|
|
7
|
+
|
|
8
|
+
// Upgrade store for setMany support
|
|
9
|
+
const storePlus = upgradeEventState(store);
|
|
10
|
+
|
|
11
|
+
// Create and start router
|
|
12
|
+
const router = createRouter({
|
|
13
|
+
routes: [
|
|
14
|
+
{ path: '/', view: 'home', component: Home },
|
|
15
|
+
{ path: '/todo-demo', view: 'todo-demo', component: TodoDemo },
|
|
16
|
+
],
|
|
17
|
+
store: storePlus,
|
|
18
|
+
rootSelector: '[data-route-root]',
|
|
19
|
+
fallback: { path: '/', view: 'home', component: Home },
|
|
20
|
+
debug: import.meta?.env?.DEV || false,
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
router.start();
|
|
24
|
+
|
|
25
|
+
// Export router API
|
|
26
|
+
export const { navigate, navigateQuery, navigatePath } = router;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// store.js — singleton eventState store for the SPA
|
|
2
|
+
import { createEventState } from '../runtime/core/eventStateNew.js';
|
|
3
|
+
|
|
4
|
+
const initial = {
|
|
5
|
+
ui: {
|
|
6
|
+
route: { path: '/', view: 'home', params: {}, query: {}, transitioning: false },
|
|
7
|
+
todos: { filter: 'all' },
|
|
8
|
+
},
|
|
9
|
+
domain: {
|
|
10
|
+
todos: { items: [] },
|
|
11
|
+
},
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const store = createEventState(initial);
|
|
15
|
+
export default store;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// views/home.js — Home route
|
|
2
|
+
export async function boot({ store, el, signal }){
|
|
3
|
+
// Artificial 1-second delay to show the loader
|
|
4
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
5
|
+
|
|
6
|
+
const container = document.createElement('main');
|
|
7
|
+
container.style.cssText = 'max-width: 600px; margin: 2rem auto; padding: 0 1rem;';
|
|
8
|
+
container.innerHTML = `
|
|
9
|
+
<h1>Home works!</h1>
|
|
10
|
+
<p>This is the home page of the todo app example.</p>
|
|
11
|
+
<p><a href="/todo-demo" data-link>Go to Todo App Demo →</a></p>
|
|
12
|
+
|
|
13
|
+
<hr style="margin: 2rem 0;">
|
|
14
|
+
|
|
15
|
+
<h2>Telemetry Test</h2>
|
|
16
|
+
<p>Counter: <strong id="counter-display">0</strong></p>
|
|
17
|
+
<button id="increment-btn" class="btn">Increment Counter</button>
|
|
18
|
+
`;
|
|
19
|
+
el.appendChild(container);
|
|
20
|
+
|
|
21
|
+
// Initialize counter in store
|
|
22
|
+
if (store.get('ui.test.counter') === undefined) {
|
|
23
|
+
store.set('ui.test.counter', 0);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Counter display subscription
|
|
27
|
+
const counterDisplay = container.querySelector('#counter-display');
|
|
28
|
+
const updateDisplay = () => {
|
|
29
|
+
const count = store.get('ui.test.counter') || 0;
|
|
30
|
+
counterDisplay.textContent = count;
|
|
31
|
+
};
|
|
32
|
+
const unsub = store.subscribe('ui.test.counter', updateDisplay);
|
|
33
|
+
updateDisplay();
|
|
34
|
+
|
|
35
|
+
// Increment button
|
|
36
|
+
const incrementBtn = container.querySelector('#increment-btn');
|
|
37
|
+
incrementBtn.addEventListener('click', () => {
|
|
38
|
+
const current = store.get('ui.test.counter') || 0;
|
|
39
|
+
console.log('[TEST] Incrementing counter from', current, 'to', current + 1);
|
|
40
|
+
store.set('ui.test.counter', current + 1);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// Cleanup on navigation
|
|
44
|
+
if (signal) signal.addEventListener('abort', () => { unsub(); });
|
|
45
|
+
return () => { unsub(); };
|
|
46
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// views/todoDemo.js — Minimal Todo app demo (imperative renderer, no bootstrap)
|
|
2
|
+
export async function boot({ store, el, signal }){
|
|
3
|
+
const main = document.createElement('main');
|
|
4
|
+
main.innerHTML = `
|
|
5
|
+
<h1>Todo app demo</h1>
|
|
6
|
+
<section class="beh-section">
|
|
7
|
+
<div style="display:flex; gap:8px; align-items:center; flex-wrap:wrap">
|
|
8
|
+
<input id="newTodo" placeholder="New todo" />
|
|
9
|
+
<button id="addTodo" class="btn">Add</button>
|
|
10
|
+
<div style="display:flex; gap:6px; align-items:center">
|
|
11
|
+
<button id="fAll" class="btn">All</button>
|
|
12
|
+
<button id="fActive" class="btn">Active</button>
|
|
13
|
+
<button id="fCompleted" class="btn">Completed</button>
|
|
14
|
+
<button id="clearCompleted" class="btn" style="background:#b91c1c; color:white">Clear Completed</button>
|
|
15
|
+
</div>
|
|
16
|
+
<span id="filterBadge" style="padding:2px 6px; border:1px solid #888; border-radius:8px; font-size:.85em; color:#555">filter: all</span>
|
|
17
|
+
</div>
|
|
18
|
+
<ul id="todos" style="margin-top:10px; padding-left: 18px;"></ul>
|
|
19
|
+
</section>
|
|
20
|
+
`;
|
|
21
|
+
el.appendChild(main);
|
|
22
|
+
|
|
23
|
+
const input = main.querySelector('#newTodo');
|
|
24
|
+
const ul = main.querySelector('#todos');
|
|
25
|
+
const btnAdd = main.querySelector('#addTodo');
|
|
26
|
+
const btnAll = main.querySelector('#fAll');
|
|
27
|
+
const btnAct = main.querySelector('#fActive');
|
|
28
|
+
const btnDone = main.querySelector('#fCompleted');
|
|
29
|
+
const btnClear = main.querySelector('#clearCompleted');
|
|
30
|
+
|
|
31
|
+
// Intents from UI
|
|
32
|
+
btnAdd?.addEventListener('click', () => {
|
|
33
|
+
const text = (input?.value || '').trim();
|
|
34
|
+
if (!text) return; store.set('intent.todo.add', { text }); input.value = '';
|
|
35
|
+
});
|
|
36
|
+
input?.addEventListener('keydown', (e) => { if (e.key === 'Enter') btnAdd?.click(); });
|
|
37
|
+
btnAll?.addEventListener('click', () => store.set('intent.ui.filter', { filter: 'all' }));
|
|
38
|
+
btnAct?.addEventListener('click', () => store.set('intent.ui.filter', { filter: 'active' }));
|
|
39
|
+
btnDone?.addEventListener('click', () => store.set('intent.ui.filter', { filter: 'completed' }));
|
|
40
|
+
btnClear?.addEventListener('click', () => store.set('intent.todo.clearCompleted'));
|
|
41
|
+
|
|
42
|
+
// Render
|
|
43
|
+
const itemsPath = 'domain.todos.items';
|
|
44
|
+
const filterPath = 'ui.todos.filter';
|
|
45
|
+
function render(){
|
|
46
|
+
const items = store.get(itemsPath) || [];
|
|
47
|
+
const filter = store.get(filterPath) || 'all';
|
|
48
|
+
const badge = main.querySelector('#filterBadge');
|
|
49
|
+
if (badge) badge.textContent = `filter: ${filter}`;
|
|
50
|
+
ul.replaceChildren();
|
|
51
|
+
let rows = items;
|
|
52
|
+
if (filter === 'active') rows = items.filter(t => !t.done);
|
|
53
|
+
else if (filter === 'completed') rows = items.filter(t => !!t.done);
|
|
54
|
+
rows.forEach((t) => {
|
|
55
|
+
const li = document.createElement('li');
|
|
56
|
+
li.style.display = 'flex'; li.style.gap = '8px'; li.style.alignItems = 'center';
|
|
57
|
+
const cb = document.createElement('input'); cb.type = 'checkbox'; cb.checked = !!t.done;
|
|
58
|
+
cb.addEventListener('change', () => store.set('intent.todo.toggle', { id: t.id }));
|
|
59
|
+
const span = document.createElement('span'); span.textContent = t.text; if (t.done) span.style.textDecoration = 'line-through';
|
|
60
|
+
li.appendChild(cb); li.appendChild(span); ul.appendChild(li);
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
const off1 = store.subscribe(itemsPath, render);
|
|
64
|
+
const off2 = store.subscribe(filterPath, render);
|
|
65
|
+
render();
|
|
66
|
+
|
|
67
|
+
if (signal) signal.addEventListener('abort', () => { try { off1 && off1(); off2 && off2(); } catch {} });
|
|
68
|
+
return () => { try { off1 && off1(); off2 && off2(); } catch {} };
|
|
69
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// dev-dock.dev.js — shared dev dock for buttons (DEV only)
|
|
2
|
+
// if (import.meta && import.meta.env && import.meta.env.DEV) {
|
|
3
|
+
const DOCK_ID = 'dev-dock-root';
|
|
4
|
+
const STYLE_ID = 'dev-dock-style';
|
|
5
|
+
const CSS = `
|
|
6
|
+
#${DOCK_ID} { position: fixed; left: 10px; bottom: 10px; z-index: 2147483200; pointer-events: auto;
|
|
7
|
+
display: inline-flex; flex-direction: row; gap: 6px; align-items: center; background: rgba(17,17,17,.85);
|
|
8
|
+
border: 1px solid rgba(255,255,255,.12); border-radius: 10px; padding: 6px; box-shadow: 0 2px 10px rgba(0,0,0,.25); }
|
|
9
|
+
#${DOCK_ID} .dock-btn { appearance: none; border: 1px solid rgba(255,255,255,.15); background: #222; color: #eee;
|
|
10
|
+
font: 600 12px/1 system-ui, -apple-system, Segoe UI, Roboto, sans-serif; border-radius: 8px; padding: 6px 10px; cursor: pointer; }
|
|
11
|
+
#${DOCK_ID} .dock-btn:hover { background: #2a2a2a; }
|
|
12
|
+
#${DOCK_ID} > button { cursor: pointer; }
|
|
13
|
+
#${DOCK_ID} .dock-sep { width: 1px; height: 18px; background: rgba(255,255,255,.12); margin: 0 4px; }
|
|
14
|
+
`;
|
|
15
|
+
|
|
16
|
+
// Style
|
|
17
|
+
let style = document.getElementById(STYLE_ID);
|
|
18
|
+
if (!style){ style = document.createElement('style'); style.id = STYLE_ID; style.textContent = CSS; document.head.appendChild(style); }
|
|
19
|
+
|
|
20
|
+
// Root
|
|
21
|
+
let root = document.getElementById(DOCK_ID);
|
|
22
|
+
if (!root){ root = document.createElement('div'); root.id = DOCK_ID; document.body.appendChild(root); }
|
|
23
|
+
|
|
24
|
+
const registry = new Map();
|
|
25
|
+
function register({ id, label, title = '', onClick }){
|
|
26
|
+
// De-dup by id (helps during HMR)
|
|
27
|
+
try { const exist = root.querySelector(`[data-dock-id="${id}"]`); if (exist) exist.remove(); } catch {}
|
|
28
|
+
const btn = document.createElement('button');
|
|
29
|
+
btn.type = 'button'; btn.className = 'dock-btn'; btn.textContent = label; if (title) btn.title = title;
|
|
30
|
+
btn.setAttribute('data-dock-id', id);
|
|
31
|
+
btn.addEventListener('click', (e) => { try { onClick?.(e); } catch (err) { console.warn('[dev-dock] button error', err); } });
|
|
32
|
+
root.appendChild(btn);
|
|
33
|
+
registry.set(id, btn);
|
|
34
|
+
return () => { try { btn.remove(); } catch{}; registry.delete(id); };
|
|
35
|
+
}
|
|
36
|
+
function separator(){ const sep = document.createElement('div'); sep.className = 'dock-sep'; root.appendChild(sep); return () => sep.remove(); }
|
|
37
|
+
|
|
38
|
+
window.__devdock = { register, separator, root };
|
|
39
|
+
// Notify listeners that the dock is ready
|
|
40
|
+
try { window.dispatchEvent(new CustomEvent('devdock:ready')); } catch {}
|
|
41
|
+
// }
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// stateTracker.dock.dev.js — register a State toggle in dev dock and hide pill (DEV only)
|
|
2
|
+
// if (import.meta && import.meta.env && import.meta.env.DEV) {
|
|
3
|
+
const st = window.stateTracker?.instance;
|
|
4
|
+
const dock = window.__devdock;
|
|
5
|
+
if (dock && st) {
|
|
6
|
+
// Hide the pill; dev dock becomes the control surface
|
|
7
|
+
try { st.elements?.pill?.style && (st.elements.pill.style.display = 'none'); } catch {}
|
|
8
|
+
dock.register({ id: 'state-tracker', label: 'State', title: 'Toggle State Tracker', onClick: () => st.toggle() });
|
|
9
|
+
}
|
|
10
|
+
// }
|
|
@@ -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
|
+
// }
|