@everystate/examples 1.0.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -0
- package/everyState-core/001-counter/README.md +44 -0
- package/everyState-core/001-counter/index.html +79 -0
- package/everyState-core/002-counter-improved/README.md +44 -0
- package/everyState-core/002-counter-improved/index.html +83 -0
- package/everyState-core/003-input-reactive/README.md +44 -0
- package/everyState-core/003-input-reactive/index.html +68 -0
- package/everyState-core/004-computed-state/README.md +45 -0
- package/everyState-core/004-computed-state/index.html +83 -0
- package/everyState-core/005-conditional-rendering/README.md +42 -0
- package/everyState-core/005-conditional-rendering/index.html +68 -0
- package/everyState-core/006-list-rendering/README.md +49 -0
- package/everyState-core/006-list-rendering/index.html +92 -0
- package/everyState-core/007-form-validation/README.md +52 -0
- package/everyState-core/007-form-validation/index.html +108 -0
- package/everyState-core/008-undo-redo/README.md +70 -0
- package/everyState-core/008-undo-redo/index.html +133 -0
- package/everyState-core/009-localStorage-side-effects/README.md +72 -0
- package/everyState-core/009-localStorage-side-effects/index.html +80 -0
- package/everyState-core/010-decoupled-components/README.md +74 -0
- package/everyState-core/010-decoupled-components/index.html +117 -0
- package/everyState-core/011-async-patterns/README.md +98 -0
- package/everyState-core/011-async-patterns/index.html +132 -0
- package/everyState-css/001-stateDrivenCSS/index.html +377 -0
- package/everyState-css/002-cssV2FullDemo/index.html +630 -0
- package/everyState-view/001/counter/index.css +31 -0
- package/everyState-view/001/counter/index.html +50 -0
- package/everyState-view/002/datatable/index.css +70 -0
- package/everyState-view/002/datatable/index.html +118 -0
- package/everyState-view/003/todo/index.css +260 -0
- package/everyState-view/003/todo/index.html +218 -0
- package/everyState-view/003-input-reactive/README.md +44 -0
- package/everyState-view/003-input-reactive/index.html +68 -0
- package/everyState-view/004/quotesFetcher/index.css +124 -0
- package/everyState-view/004/quotesFetcher/index.html +108 -0
- package/everyState-view/004_01/quotesFetcher/app.js +32 -0
- package/everyState-view/004_01/quotesFetcher/components/appHeader/appSubtitle.js +2 -0
- package/everyState-view/004_01/quotesFetcher/components/appHeader/appTitle.js +2 -0
- package/everyState-view/004_01/quotesFetcher/components/appHeader.js +9 -0
- package/everyState-view/004_01/quotesFetcher/components/historyHeading.js +2 -0
- package/everyState-view/004_01/quotesFetcher/components/historyList/histAuthor.js +2 -0
- package/everyState-view/004_01/quotesFetcher/components/historyList/histQuote.js +2 -0
- package/everyState-view/004_01/quotesFetcher/components/historyList.js +14 -0
- package/everyState-view/004_01/quotesFetcher/components/quoteCard/fetchButton.js +2 -0
- package/everyState-view/004_01/quotesFetcher/components/quoteCard/quoteAuthor.js +2 -0
- package/everyState-view/004_01/quotesFetcher/components/quoteCard/quoteMessage.js +2 -0
- package/everyState-view/004_01/quotesFetcher/components/quoteCard/quoteText.js +2 -0
- package/everyState-view/004_01/quotesFetcher/components/quoteCard.js +11 -0
- package/everyState-view/004_01/quotesFetcher/index.css +124 -0
- package/everyState-view/004_01/quotesFetcher/index.html +23 -0
- package/everyState-view/004_01/quotesFetcher/store.js +35 -0
- package/everyState-view/004_02/quotesFetcher/app.js +20 -0
- package/everyState-view/004_02/quotesFetcher/components.js +46 -0
- package/everyState-view/004_02/quotesFetcher/index.css +124 -0
- package/everyState-view/004_02/quotesFetcher/index.html +23 -0
- package/everyState-view/004_02/quotesFetcher/store.js +35 -0
- package/everyState-view/004_03/quotesFetcher/actions.js +27 -0
- package/everyState-view/004_03/quotesFetcher/app.js +19 -0
- package/everyState-view/004_03/quotesFetcher/components.js +28 -0
- package/everyState-view/004_03/quotesFetcher/index.css +124 -0
- package/everyState-view/004_03/quotesFetcher/index.html +23 -0
- package/everyState-view/004_03/quotesFetcher/resolve.js +34 -0
- package/everyState-view/004_03/quotesFetcher/store.js +11 -0
- package/everyState-view/004_04/quotesFetcher/actions.js +66 -0
- package/everyState-view/004_04/quotesFetcher/app.js +24 -0
- package/everyState-view/004_04/quotesFetcher/components/archive.js +40 -0
- package/everyState-view/004_04/quotesFetcher/components/fetcher.js +29 -0
- package/everyState-view/004_04/quotesFetcher/components.js +20 -0
- package/everyState-view/004_04/quotesFetcher/index.css +283 -0
- package/everyState-view/004_04/quotesFetcher/index.html +24 -0
- package/everyState-view/004_04/quotesFetcher/resolve.js +34 -0
- package/everyState-view/004_04/quotesFetcher/store.js +21 -0
- package/everyState-view/004_04/statedump.json +826 -0
- package/everyState-view/004_05/quoteExplorer/actions.js +58 -0
- package/everyState-view/004_05/quoteExplorer/app.js +27 -0
- package/everyState-view/004_05/quoteExplorer/components.js +83 -0
- package/everyState-view/004_05/quoteExplorer/index.css +231 -0
- package/everyState-view/004_05/quoteExplorer/index.html +23 -0
- package/everyState-view/004_05/quoteExplorer/resolve.js +50 -0
- package/everyState-view/004_05/quoteExplorer/store.js +33 -0
- package/everyState-view/004_06/quoteExplorer/actions.js +21 -0
- package/everyState-view/004_06/quoteExplorer/app.js +44 -0
- package/everyState-view/004_06/quoteExplorer/components.js +80 -0
- package/everyState-view/004_06/quoteExplorer/derived.js +43 -0
- package/everyState-view/004_06/quoteExplorer/index.css +346 -0
- package/everyState-view/004_06/quoteExplorer/index.html +25 -0
- package/everyState-view/004_06/quoteExplorer/intents.js +44 -0
- package/everyState-view/004_06/quoteExplorer/policies.js +25 -0
- package/everyState-view/004_06/quoteExplorer/resolve.js +51 -0
- package/everyState-view/004_06/quoteExplorer/store.js +44 -0
- package/everyState-view/004_07/quoteExplorer/app.js +47 -0
- package/everyState-view/004_07/quoteExplorer/components.js +85 -0
- package/everyState-view/004_07/quoteExplorer/derived.js +43 -0
- package/everyState-view/004_07/quoteExplorer/index.css +346 -0
- package/everyState-view/004_07/quoteExplorer/index.html +25 -0
- package/everyState-view/004_07/quoteExplorer/intents.js +51 -0
- package/everyState-view/004_07/quoteExplorer/policies.js +21 -0
- package/everyState-view/004_07/quoteExplorer/resolve.js +39 -0
- package/everyState-view/004_07/quoteExplorer/store.js +44 -0
- package/everyState-view/004_08/quoteExplorer/app.js +78 -0
- package/everyState-view/004_08/quoteExplorer/components.js +85 -0
- package/everyState-view/004_08/quoteExplorer/derived.js +43 -0
- package/everyState-view/004_08/quoteExplorer/index.css +346 -0
- package/everyState-view/004_08/quoteExplorer/index.html +25 -0
- package/everyState-view/004_08/quoteExplorer/intents.js +51 -0
- package/everyState-view/004_08/quoteExplorer/policies.js +21 -0
- package/everyState-view/004_08/quoteExplorer/resolve.js +39 -0
- package/everyState-view/004_08/quoteExplorer/store.js +44 -0
- package/everyState-view/004_08_V2/app.js +78 -0
- package/everyState-view/004_08_V2/components/appDetail.js +8 -0
- package/everyState-view/004_08_V2/components/appDetailBar.js +7 -0
- package/everyState-view/004_08_V2/components/appDetailBarClose.js +8 -0
- package/everyState-view/004_08_V2/components/appDetailBarCount.js +7 -0
- package/everyState-view/004_08_V2/components/appDetailBarHeading.js +7 -0
- package/everyState-view/004_08_V2/components/appDetailQuotes.js +15 -0
- package/everyState-view/004_08_V2/components/appHeader.js +7 -0
- package/everyState-view/004_08_V2/components/appHeaderSubtitle.js +7 -0
- package/everyState-view/004_08_V2/components/appHeaderTitle.js +7 -0
- package/everyState-view/004_08_V2/components/appLog.js +7 -0
- package/everyState-view/004_08_V2/components/appLogHeading.js +7 -0
- package/everyState-view/004_08_V2/components/appLogList.js +16 -0
- package/everyState-view/004_08_V2/components/appSearch.js +7 -0
- package/everyState-view/004_08_V2/components/appSearchInput.js +9 -0
- package/everyState-view/004_08_V2/components/appStats.js +7 -0
- package/everyState-view/004_08_V2/components/appStatsContent.js +8 -0
- package/everyState-view/004_08_V2/components/appStatsContentFavcount.js +7 -0
- package/everyState-view/004_08_V2/components/appStatsContentText.js +7 -0
- package/everyState-view/004_08_V2/components/appStatsToggle.js +8 -0
- package/everyState-view/004_08_V2/components/appTags.js +7 -0
- package/everyState-view/004_08_V2/components/appTagsLabel.js +7 -0
- package/everyState-view/004_08_V2/components/appTagsRow.js +15 -0
- package/everyState-view/004_08_V2/components/index.js +59 -0
- package/everyState-view/004_08_V2/components/utils/css.js +88 -0
- package/everyState-view/004_08_V2/components/utils/elements.js +87 -0
- package/everyState-view/004_08_V2/components.js +79 -0
- package/everyState-view/004_08_V2/derived.js +43 -0
- package/everyState-view/004_08_V2/index.css +350 -0
- package/everyState-view/004_08_V2/index.html +25 -0
- package/everyState-view/004_08_V2/intents.js +51 -0
- package/everyState-view/004_08_V2/policies.js +21 -0
- package/everyState-view/004_08_V2/resolve.js +39 -0
- package/everyState-view/004_08_V2/store.js +44 -0
- package/everyState-view/006/api-datatable/index.css +388 -0
- package/everyState-view/006/api-datatable/index.html +355 -0
- package/everyState-view/007/apiUsers/index.html +307 -0
- package/everyState-view/007-form-validation/README.md +52 -0
- package/everyState-view/007-form-validation/index.html +108 -0
- package/everyState-view/010-decoupled-components/README.md +74 -0
- package/everyState-view/010-decoupled-components/index.html +117 -0
- package/everyState-view/index.html +36 -0
- package/index.js +0 -5
- package/package.json +2 -4
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// ─── App: boot sequence ─────────────────────────────────────────
|
|
2
|
+
// 004_08: Fully transparent auto-intent.
|
|
3
|
+
//
|
|
4
|
+
// No Proxy. No actions.js. No runtime metaprogramming.
|
|
5
|
+
// intentHandlers() scans the component registry at boot time and
|
|
6
|
+
// builds a plain handlers map: { 'intent.X': fn } for every
|
|
7
|
+
// onClick/onDblClick/onEnter/onBlur target starting with 'intent.'.
|
|
8
|
+
//
|
|
9
|
+
// The result is a regular object. console.log(handlers) shows every key.
|
|
10
|
+
// grep 'intent.' finds the spec, the handler, and the subscriber.
|
|
11
|
+
|
|
12
|
+
import { flatten } from '@everystate/view/resolve';
|
|
13
|
+
import { mount } from '@everystate/view/project';
|
|
14
|
+
import { createPerfMonitor, mountOverlay } from '@everystate/perf';
|
|
15
|
+
import { store } from './store.js';
|
|
16
|
+
import { c } from './components.js';
|
|
17
|
+
import { resolveTree, buildRefs } from './resolve.js';
|
|
18
|
+
import { mountIntents } from './intents.js';
|
|
19
|
+
import { mountDerived } from './derived.js';
|
|
20
|
+
import { mountPolicies } from './policies.js';
|
|
21
|
+
|
|
22
|
+
// ─── intentHandlers: scan registry, build plain handlers map ────
|
|
23
|
+
// Walks all component specs + nested templates. For every event
|
|
24
|
+
// target starting with 'intent.', registers a handler that calls
|
|
25
|
+
// store.set(intentPath, resolvedArg). Returns a plain object.
|
|
26
|
+
|
|
27
|
+
function intentHandlers(store, components) {
|
|
28
|
+
const handlers = {};
|
|
29
|
+
const seen = new Set();
|
|
30
|
+
const EVENT_KEYS = ['onClick', 'onDblClick', 'onEnter', 'onBlur'];
|
|
31
|
+
|
|
32
|
+
function scan(spec) {
|
|
33
|
+
if (!spec || typeof spec !== 'object') return;
|
|
34
|
+
for (const key of EVENT_KEYS) {
|
|
35
|
+
const target = spec[key];
|
|
36
|
+
if (typeof target === 'string' && target.startsWith('intent.')) {
|
|
37
|
+
const name = target.replace(/\(.*\)$/, '');
|
|
38
|
+
if (!seen.has(name)) {
|
|
39
|
+
seen.add(name);
|
|
40
|
+
handlers[name] = (arg, event) => {
|
|
41
|
+
store.set(name, arg instanceof Event ? Date.now() : (arg ?? Date.now()));
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
// Recurse into templates and template children
|
|
47
|
+
if (spec.template) scan(spec.template);
|
|
48
|
+
if (Array.isArray(spec.template?.children)) {
|
|
49
|
+
spec.template.children.forEach(scan);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
Object.values(components).forEach(scan);
|
|
54
|
+
return handlers;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ─── Boot ───────────────────────────────────────────────────────
|
|
58
|
+
|
|
59
|
+
// 1. Resolve component tree + flatten into store
|
|
60
|
+
const spec = resolveTree(c, 'app');
|
|
61
|
+
const { nodes } = flatten(spec, store, 'view');
|
|
62
|
+
const refs = buildRefs(nodes);
|
|
63
|
+
|
|
64
|
+
// 2. Mount reactive subscribers (order: derived, policies, intents)
|
|
65
|
+
mountDerived(store);
|
|
66
|
+
mountPolicies(store);
|
|
67
|
+
mountIntents(store, refs);
|
|
68
|
+
|
|
69
|
+
// 3. Build handlers from registry (plain object, fully inspectable)
|
|
70
|
+
const handlers = intentHandlers(store, c);
|
|
71
|
+
console.log('[004_08 intent handlers]', Object.keys(handlers));
|
|
72
|
+
|
|
73
|
+
// 4. Mount DOM
|
|
74
|
+
mount(store, 'view', document.getElementById('app'), handlers);
|
|
75
|
+
|
|
76
|
+
// 5. Perf overlay
|
|
77
|
+
const perf = createPerfMonitor(store);
|
|
78
|
+
mountOverlay(perf, document.body);
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// ─── Component registry ─────────────────────────────────────────
|
|
2
|
+
// Flat dot-path keyed specs. Every component is a resource.
|
|
3
|
+
// children/template are dot-path strings or inline objects.
|
|
4
|
+
//
|
|
5
|
+
// KEY CHANGE from 004_06:
|
|
6
|
+
// onClick targets use 'intent.*' directly instead of 'actions.*'.
|
|
7
|
+
// The view engine's auto-intent handler writes the resolved arg
|
|
8
|
+
// to the store at the intent path. No actions.js file needed.
|
|
9
|
+
//
|
|
10
|
+
// {path} interpolation reads from the store at mount time.
|
|
11
|
+
// ref: stable handle for intent handlers to toggle view node classes.
|
|
12
|
+
// bind: two-way binding to a store path (no action needed).
|
|
13
|
+
|
|
14
|
+
export const c = {
|
|
15
|
+
// ── Root ──
|
|
16
|
+
'app': {
|
|
17
|
+
tag: 'div', class: 'app',
|
|
18
|
+
children: ['app.header', 'app.search', 'app.tags', 'app.detail', 'app.stats', 'app.log']
|
|
19
|
+
},
|
|
20
|
+
|
|
21
|
+
// ── Header ──
|
|
22
|
+
'app.header': { tag: 'header', class: 'app-header', children: ['app.header.title', 'app.header.subtitle'] },
|
|
23
|
+
'app.header.title': { tag: 'h1', text: '💬 Quote Explorer Pro' },
|
|
24
|
+
'app.header.subtitle': { tag: 'p', class: 'subtitle', text: 'Intent-driven · Derived state · Policy subscriptions' },
|
|
25
|
+
|
|
26
|
+
// ── Search (two-way bind - no action needed) ──
|
|
27
|
+
'app.search': { tag: 'div', class: 'search-bar', children: ['app.search.input'] },
|
|
28
|
+
'app.search.input': { tag: 'input', class: 'search-input', placeholder: 'Search quotes…', bind: 'state.searchTerm' },
|
|
29
|
+
|
|
30
|
+
// ── Author Tags ──
|
|
31
|
+
'app.tags': { tag: 'div', class: 'author-tags', children: ['app.tags.label', 'app.tags.list'] },
|
|
32
|
+
'app.tags.label': { tag: 'span', class: 'tags-label', text: 'Pick an author:' },
|
|
33
|
+
'app.tags.list': {
|
|
34
|
+
tag: 'div', class: 'tags-row',
|
|
35
|
+
forEach: 'state.authors', as: 'author',
|
|
36
|
+
template: { tag: 'button', class: 'tag', text: 'author', onClick: 'intent.selectAuthor(author)' }
|
|
37
|
+
},
|
|
38
|
+
|
|
39
|
+
// ── Detail Panel (conditional - hidden by default) ──
|
|
40
|
+
'app.detail': {
|
|
41
|
+
tag: 'div', class: 'detail-panel hidden', ref: 'detail',
|
|
42
|
+
children: ['app.detail.bar', 'app.detail.quotes']
|
|
43
|
+
},
|
|
44
|
+
'app.detail.bar': { tag: 'div', class: 'detail-bar', children: ['app.detail.bar.heading', 'app.detail.bar.count', 'app.detail.bar.close'] },
|
|
45
|
+
'app.detail.bar.heading': { tag: 'h2', class: 'detail-heading', text: '{state.selectedAuthor}' },
|
|
46
|
+
'app.detail.bar.count': { tag: 'span', class: 'badge', text: '{derived.quoteCount} quotes' },
|
|
47
|
+
'app.detail.bar.close': { tag: 'button', class: 'btn btn-close', text: '✕', onClick: 'intent.closeDetail' },
|
|
48
|
+
'app.detail.quotes': {
|
|
49
|
+
tag: 'ul', class: 'detail-quotes',
|
|
50
|
+
forEach: 'derived.filteredQuotes', as: 'item',
|
|
51
|
+
template: {
|
|
52
|
+
tag: 'li', class: 'quote-item',
|
|
53
|
+
children: [
|
|
54
|
+
{ tag: 'span', class: 'quote-text', text: 'item.text' },
|
|
55
|
+
{ tag: 'button', class: 'fav-btn', classIf: { 'is-fav': 'item.fav' }, text: '♥', onClick: 'intent.toggleFav(item.id)' }
|
|
56
|
+
]
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
|
|
60
|
+
// ── Stats Section (conditional toggle) ──
|
|
61
|
+
'app.stats': { tag: 'div', class: 'stats-section', children: ['app.stats.toggle', 'app.stats.content'] },
|
|
62
|
+
'app.stats.toggle': { tag: 'button', class: 'btn btn-secondary', text: '{ui.toggleLabel}', onClick: 'intent.toggleStats' },
|
|
63
|
+
'app.stats.content': {
|
|
64
|
+
tag: 'div', class: 'stats-content hidden', ref: 'statsContent',
|
|
65
|
+
children: ['app.stats.content.text', 'app.stats.content.favcount']
|
|
66
|
+
},
|
|
67
|
+
'app.stats.content.text': { tag: 'p', class: 'stats-text', text: '{derived.stats}' },
|
|
68
|
+
'app.stats.content.favcount': { tag: 'p', class: 'stats-fav', text: '❤ {derived.favCount} favorites' },
|
|
69
|
+
|
|
70
|
+
// ── Activity Log (policy-driven via intent.* wildcard) ──
|
|
71
|
+
'app.log': { tag: 'div', class: 'log-section', children: ['app.log.heading', 'app.log.list'] },
|
|
72
|
+
'app.log.heading': { tag: 'h3', class: 'log-heading', text: '📋 Activity Log' },
|
|
73
|
+
'app.log.list': {
|
|
74
|
+
tag: 'ul', class: 'log-list',
|
|
75
|
+
forEach: 'derived.activityLog', as: 'entry',
|
|
76
|
+
template: {
|
|
77
|
+
tag: 'li', class: 'log-entry',
|
|
78
|
+
children: [
|
|
79
|
+
{ tag: 'span', class: 'log-time', text: 'entry.time' },
|
|
80
|
+
{ tag: 'span', class: 'log-intent', text: 'entry.intent' },
|
|
81
|
+
{ tag: 'span', class: 'log-value', text: 'entry.value' },
|
|
82
|
+
]
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// ─── Derived state ──────────────────────────────────────────────
|
|
2
|
+
// Subscribe to state.* and auto-compute derived values.
|
|
3
|
+
// Pure recomputation: reads state.*, writes derived.*.
|
|
4
|
+
// No intent knowledge, no view knowledge.
|
|
5
|
+
//
|
|
6
|
+
// KEY CHANGE from 004_06: uses setMany() for atomic batch writes.
|
|
7
|
+
|
|
8
|
+
export function mountDerived(store) {
|
|
9
|
+
function recompute() {
|
|
10
|
+
const quotes = store.get('state.quotes') || [];
|
|
11
|
+
const author = store.get('state.selectedAuthor') || '';
|
|
12
|
+
const search = (store.get('state.searchTerm') || '').toLowerCase().trim();
|
|
13
|
+
|
|
14
|
+
// Filter: only when an author is selected
|
|
15
|
+
let filtered = [];
|
|
16
|
+
if (author) {
|
|
17
|
+
filtered = quotes.filter(q => q.author === author);
|
|
18
|
+
if (search) {
|
|
19
|
+
filtered = filtered.filter(q =>
|
|
20
|
+
q.text.toLowerCase().includes(search)
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const totalAuthors = (store.get('state.authors') || []).length;
|
|
26
|
+
const favTotal = quotes.filter(q => q.fav).length;
|
|
27
|
+
|
|
28
|
+
store.setMany({
|
|
29
|
+
'derived.filteredQuotes': filtered,
|
|
30
|
+
'derived.quoteCount': filtered.length,
|
|
31
|
+
'derived.favCount': favTotal,
|
|
32
|
+
'derived.stats': `${quotes.length} total quotes · ${totalAuthors} authors · ${favTotal} favorites`,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Recompute whenever any state.* path changes
|
|
37
|
+
const unsub = store.subscribe('state.*', recompute);
|
|
38
|
+
|
|
39
|
+
// Initial computation
|
|
40
|
+
recompute();
|
|
41
|
+
|
|
42
|
+
return unsub;
|
|
43
|
+
}
|
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
/* ─── 004_06 Quote Explorer Pro ─────────────────────────────── */
|
|
2
|
+
|
|
3
|
+
:root {
|
|
4
|
+
--bg: #0f172a;
|
|
5
|
+
--surface: #1e293b;
|
|
6
|
+
--surface-alt: #334155;
|
|
7
|
+
--border: #475569;
|
|
8
|
+
--text: #e2e8f0;
|
|
9
|
+
--text-muted: #94a3b8;
|
|
10
|
+
--accent: #38bdf8;
|
|
11
|
+
--accent-hover: #7dd3fc;
|
|
12
|
+
--danger: #ef4444;
|
|
13
|
+
--fav: #f43f5e;
|
|
14
|
+
--fav-muted: #64748b;
|
|
15
|
+
--success: #22c55e;
|
|
16
|
+
--radius: 8px;
|
|
17
|
+
--shadow: 0 4px 16px rgba(0,0,0,.3);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
21
|
+
|
|
22
|
+
body {
|
|
23
|
+
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
24
|
+
background: var(--bg);
|
|
25
|
+
color: var(--text);
|
|
26
|
+
line-height: 1.6;
|
|
27
|
+
min-height: 100vh;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/* ── App layout ── */
|
|
31
|
+
|
|
32
|
+
.app {
|
|
33
|
+
max-width: 720px;
|
|
34
|
+
margin: 0 auto;
|
|
35
|
+
padding: 2rem 1.5rem 4rem;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/* ── Header ── */
|
|
39
|
+
|
|
40
|
+
.app-header {
|
|
41
|
+
text-align: center;
|
|
42
|
+
margin-bottom: 2rem;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.app-header h1 {
|
|
46
|
+
font-size: 2rem;
|
|
47
|
+
font-weight: 700;
|
|
48
|
+
background: linear-gradient(135deg, var(--accent), #a78bfa);
|
|
49
|
+
-webkit-background-clip: text;
|
|
50
|
+
-webkit-text-fill-color: transparent;
|
|
51
|
+
background-clip: text;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.subtitle {
|
|
55
|
+
color: var(--text-muted);
|
|
56
|
+
font-size: 0.85rem;
|
|
57
|
+
margin-top: 0.25rem;
|
|
58
|
+
letter-spacing: 0.04em;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/* ── Search bar ── */
|
|
62
|
+
|
|
63
|
+
.search-bar {
|
|
64
|
+
margin-bottom: 1.5rem;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.search-input {
|
|
68
|
+
width: 100%;
|
|
69
|
+
padding: 0.7rem 1rem;
|
|
70
|
+
background: var(--surface);
|
|
71
|
+
color: var(--text);
|
|
72
|
+
border: 1px solid var(--border);
|
|
73
|
+
border-radius: var(--radius);
|
|
74
|
+
font-size: 0.95rem;
|
|
75
|
+
outline: none;
|
|
76
|
+
transition: border-color 0.2s;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.search-input::placeholder {
|
|
80
|
+
color: var(--text-muted);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.search-input:focus {
|
|
84
|
+
border-color: var(--accent);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/* ── Author tags ── */
|
|
88
|
+
|
|
89
|
+
.author-tags {
|
|
90
|
+
margin-bottom: 1.5rem;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.tags-label {
|
|
94
|
+
display: block;
|
|
95
|
+
font-size: 0.8rem;
|
|
96
|
+
color: var(--text-muted);
|
|
97
|
+
margin-bottom: 0.5rem;
|
|
98
|
+
text-transform: uppercase;
|
|
99
|
+
letter-spacing: 0.06em;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.tags-row {
|
|
103
|
+
display: flex;
|
|
104
|
+
flex-wrap: wrap;
|
|
105
|
+
gap: 0.5rem;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.tag {
|
|
109
|
+
padding: 0.4rem 0.9rem;
|
|
110
|
+
background: var(--surface);
|
|
111
|
+
color: var(--accent);
|
|
112
|
+
border: 1px solid var(--border);
|
|
113
|
+
border-radius: 999px;
|
|
114
|
+
font-size: 0.85rem;
|
|
115
|
+
cursor: pointer;
|
|
116
|
+
transition: all 0.15s;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
.tag:hover {
|
|
120
|
+
background: var(--accent);
|
|
121
|
+
color: var(--bg);
|
|
122
|
+
border-color: var(--accent);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/* ── Detail panel ── */
|
|
126
|
+
|
|
127
|
+
.detail-panel {
|
|
128
|
+
background: var(--surface);
|
|
129
|
+
border: 1px solid var(--border);
|
|
130
|
+
border-radius: var(--radius);
|
|
131
|
+
padding: 1.25rem;
|
|
132
|
+
margin-bottom: 1.5rem;
|
|
133
|
+
animation: slideIn 0.25s ease-out;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
@keyframes slideIn {
|
|
137
|
+
from { opacity: 0; transform: translateY(-8px); }
|
|
138
|
+
to { opacity: 1; transform: translateY(0); }
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
.detail-bar {
|
|
142
|
+
display: flex;
|
|
143
|
+
align-items: center;
|
|
144
|
+
gap: 0.75rem;
|
|
145
|
+
margin-bottom: 1rem;
|
|
146
|
+
padding-bottom: 0.75rem;
|
|
147
|
+
border-bottom: 1px solid var(--border);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
.detail-heading {
|
|
151
|
+
font-size: 1.2rem;
|
|
152
|
+
font-weight: 600;
|
|
153
|
+
flex: 1;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
.badge {
|
|
157
|
+
font-size: 0.75rem;
|
|
158
|
+
padding: 0.2rem 0.6rem;
|
|
159
|
+
background: var(--accent);
|
|
160
|
+
color: var(--bg);
|
|
161
|
+
border-radius: 999px;
|
|
162
|
+
font-weight: 600;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
.detail-quotes {
|
|
166
|
+
list-style: none;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
.quote-item {
|
|
170
|
+
display: flex;
|
|
171
|
+
align-items: flex-start;
|
|
172
|
+
gap: 0.75rem;
|
|
173
|
+
padding: 0.6rem 0;
|
|
174
|
+
border-bottom: 1px solid rgba(255,255,255,0.05);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
.quote-item:last-child {
|
|
178
|
+
border-bottom: none;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
.quote-text {
|
|
182
|
+
flex: 1;
|
|
183
|
+
font-size: 0.92rem;
|
|
184
|
+
color: var(--text);
|
|
185
|
+
line-height: 1.5;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
.fav-btn {
|
|
189
|
+
background: none;
|
|
190
|
+
border: none;
|
|
191
|
+
font-size: 1.1rem;
|
|
192
|
+
cursor: pointer;
|
|
193
|
+
color: var(--fav-muted);
|
|
194
|
+
transition: color 0.15s, transform 0.15s;
|
|
195
|
+
padding: 0.2rem;
|
|
196
|
+
line-height: 1;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
.fav-btn:hover {
|
|
200
|
+
color: var(--fav);
|
|
201
|
+
transform: scale(1.2);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
.fav-btn.is-fav {
|
|
205
|
+
color: var(--fav);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/* ── Buttons ── */
|
|
209
|
+
|
|
210
|
+
.btn {
|
|
211
|
+
padding: 0.45rem 0.9rem;
|
|
212
|
+
border: 1px solid var(--border);
|
|
213
|
+
border-radius: var(--radius);
|
|
214
|
+
background: var(--surface);
|
|
215
|
+
color: var(--text);
|
|
216
|
+
font-size: 0.85rem;
|
|
217
|
+
cursor: pointer;
|
|
218
|
+
transition: all 0.15s;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
.btn:hover {
|
|
222
|
+
background: var(--surface-alt);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
.btn-close {
|
|
226
|
+
font-size: 1rem;
|
|
227
|
+
padding: 0.2rem 0.5rem;
|
|
228
|
+
color: var(--text-muted);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
.btn-close:hover {
|
|
232
|
+
color: var(--danger);
|
|
233
|
+
border-color: var(--danger);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
.btn-secondary {
|
|
237
|
+
background: var(--surface);
|
|
238
|
+
color: var(--accent);
|
|
239
|
+
border-color: var(--accent);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
.btn-secondary:hover {
|
|
243
|
+
background: var(--accent);
|
|
244
|
+
color: var(--bg);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/* ── Stats section ── */
|
|
248
|
+
|
|
249
|
+
.stats-section {
|
|
250
|
+
margin-bottom: 1.5rem;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
.stats-content {
|
|
254
|
+
margin-top: 0.75rem;
|
|
255
|
+
padding: 1rem;
|
|
256
|
+
background: var(--surface);
|
|
257
|
+
border: 1px solid var(--border);
|
|
258
|
+
border-radius: var(--radius);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
.stats-text {
|
|
262
|
+
font-size: 0.9rem;
|
|
263
|
+
color: var(--text-muted);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
.stats-fav {
|
|
267
|
+
font-size: 0.9rem;
|
|
268
|
+
color: var(--fav);
|
|
269
|
+
margin-top: 0.3rem;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/* ── Activity log ── */
|
|
273
|
+
|
|
274
|
+
.log-section {
|
|
275
|
+
margin-top: 1rem;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
.log-heading {
|
|
279
|
+
font-size: 0.95rem;
|
|
280
|
+
color: var(--text-muted);
|
|
281
|
+
margin-bottom: 0.5rem;
|
|
282
|
+
font-weight: 600;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
.log-list {
|
|
286
|
+
list-style: none;
|
|
287
|
+
max-height: 260px;
|
|
288
|
+
overflow-y: auto;
|
|
289
|
+
background: var(--surface);
|
|
290
|
+
border: 1px solid var(--border);
|
|
291
|
+
border-radius: var(--radius);
|
|
292
|
+
padding: 0.5rem;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
.log-entry {
|
|
296
|
+
display: flex;
|
|
297
|
+
gap: 0.6rem;
|
|
298
|
+
padding: 0.35rem 0.5rem;
|
|
299
|
+
font-size: 0.78rem;
|
|
300
|
+
font-family: 'JetBrains Mono', 'Fira Code', monospace;
|
|
301
|
+
border-bottom: 1px solid rgba(255,255,255,0.04);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
.log-entry:last-child {
|
|
305
|
+
border-bottom: none;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
.log-time {
|
|
309
|
+
color: var(--text-muted);
|
|
310
|
+
min-width: 70px;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
.log-intent {
|
|
314
|
+
color: var(--accent);
|
|
315
|
+
font-weight: 600;
|
|
316
|
+
min-width: 110px;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
.log-value {
|
|
320
|
+
color: var(--text);
|
|
321
|
+
opacity: 0.7;
|
|
322
|
+
overflow: hidden;
|
|
323
|
+
text-overflow: ellipsis;
|
|
324
|
+
white-space: nowrap;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/* ── Utility ── */
|
|
328
|
+
|
|
329
|
+
.hidden {
|
|
330
|
+
display: none !important;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/* ── Scrollbar ── */
|
|
334
|
+
|
|
335
|
+
.log-list::-webkit-scrollbar {
|
|
336
|
+
width: 5px;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
.log-list::-webkit-scrollbar-track {
|
|
340
|
+
background: var(--surface);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
.log-list::-webkit-scrollbar-thumb {
|
|
344
|
+
background: var(--border);
|
|
345
|
+
border-radius: 3px;
|
|
346
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>004_08 - Quote Explorer Pro (transparent auto-intent)</title>
|
|
7
|
+
<link rel="stylesheet" href="index.css" />
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<div id="app"></div>
|
|
11
|
+
|
|
12
|
+
<script type="importmap">
|
|
13
|
+
{
|
|
14
|
+
"imports": {
|
|
15
|
+
"@everystate/core": "/everystate-core/index.js",
|
|
16
|
+
"@everystate/core/queryClient": "/everystate-core/queryClient.js",
|
|
17
|
+
"@everystate/view/resolve": "/everystate-view/resolve.js",
|
|
18
|
+
"@everystate/view/project": "/everystate-view/project.js",
|
|
19
|
+
"@everystate/perf": "/everystate-perf/index.js"
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
</script>
|
|
23
|
+
<script type="module" src="app.js"></script>
|
|
24
|
+
</body>
|
|
25
|
+
</html>
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// ─── Intent handlers ────────────────────────────────────────────
|
|
2
|
+
// Subscribe to intent.* paths and handle business logic.
|
|
3
|
+
// UI events emit intents directly (no actions.js intermediary).
|
|
4
|
+
// Refs are injected so handlers can toggle view node classes.
|
|
5
|
+
//
|
|
6
|
+
// KEY CHANGES from 004_06:
|
|
7
|
+
// - setMany() for atomic batching (fewer subscriber fires)
|
|
8
|
+
// - Cleaner unsubs with const off = [...] pattern
|
|
9
|
+
|
|
10
|
+
export function mountIntents(store, refs) {
|
|
11
|
+
const off = [
|
|
12
|
+
|
|
13
|
+
// ── Select author -> show detail panel ──
|
|
14
|
+
store.subscribe('intent.selectAuthor', (author) => {
|
|
15
|
+
store.setMany({
|
|
16
|
+
'state.selectedAuthor': author,
|
|
17
|
+
[`view.nodes.${refs.detail}.class`]: 'detail-panel',
|
|
18
|
+
});
|
|
19
|
+
}),
|
|
20
|
+
|
|
21
|
+
// ── Close detail -> hide panel, clear search ──
|
|
22
|
+
store.subscribe('intent.closeDetail', () => {
|
|
23
|
+
store.setMany({
|
|
24
|
+
'state.selectedAuthor': '',
|
|
25
|
+
'state.searchTerm': '',
|
|
26
|
+
[`view.nodes.${refs.detail}.class`]: 'detail-panel hidden',
|
|
27
|
+
});
|
|
28
|
+
}),
|
|
29
|
+
|
|
30
|
+
// ── Toggle favorite on a quote ──
|
|
31
|
+
store.subscribe('intent.toggleFav', (id) => {
|
|
32
|
+
const quotes = store.get('state.quotes') || [];
|
|
33
|
+
store.set('state.quotes', quotes.map(q =>
|
|
34
|
+
q.id === id ? { ...q, fav: !q.fav } : q
|
|
35
|
+
));
|
|
36
|
+
}),
|
|
37
|
+
|
|
38
|
+
// ── Toggle stats visibility ──
|
|
39
|
+
store.subscribe('intent.toggleStats', () => {
|
|
40
|
+
const visible = store.get('ui.statsVisible');
|
|
41
|
+
store.setMany({
|
|
42
|
+
'ui.statsVisible': !visible,
|
|
43
|
+
[`view.nodes.${refs.statsContent}.class`]: visible ? 'stats-content hidden' : 'stats-content',
|
|
44
|
+
'ui.toggleLabel': visible ? '📊 Show Stats' : '📊 Hide Stats',
|
|
45
|
+
});
|
|
46
|
+
}),
|
|
47
|
+
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
return () => off.forEach(u => u());
|
|
51
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// ─── Policy subscriptions ───────────────────────────────────────
|
|
2
|
+
// Cross-cutting concerns via wildcard subscribers.
|
|
3
|
+
// intent.* captures every intent and logs it to the activity feed.
|
|
4
|
+
// This is the "policy layer" pattern from the book (Ch 12, 17).
|
|
5
|
+
|
|
6
|
+
export function mountPolicies(store) {
|
|
7
|
+
// Activity log: capture all intents via wildcard
|
|
8
|
+
return store.subscribe('intent.*', ({ path, value }) => {
|
|
9
|
+
const log = store.get('derived.activityLog') || [];
|
|
10
|
+
const intent = path.replace('intent.', '');
|
|
11
|
+
const display = typeof value === 'object'
|
|
12
|
+
? JSON.stringify(value)
|
|
13
|
+
: String(value ?? '');
|
|
14
|
+
const entry = {
|
|
15
|
+
time: new Date().toLocaleTimeString(),
|
|
16
|
+
intent,
|
|
17
|
+
value: display.length > 30 ? display.slice(0, 30) + '…' : display,
|
|
18
|
+
};
|
|
19
|
+
store.set('derived.activityLog', [entry, ...log].slice(0, 15));
|
|
20
|
+
});
|
|
21
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// ─── Resolve utilities ──────────────────────────────────────────
|
|
2
|
+
// Reduced from 004_06: resolveActions is gone (no actions.js).
|
|
3
|
+
// Only resolveTree + buildRefs remain.
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* resolveTree(registry, key)
|
|
7
|
+
* Recursively builds a nested spec from the flat dot-path registry.
|
|
8
|
+
* String references in children/template are replaced with resolved specs.
|
|
9
|
+
*/
|
|
10
|
+
export function resolveTree(registry, key) {
|
|
11
|
+
const spec = registry[key];
|
|
12
|
+
if (!spec) return null;
|
|
13
|
+
const resolved = { ...spec };
|
|
14
|
+
|
|
15
|
+
if (Array.isArray(resolved.children)) {
|
|
16
|
+
resolved.children = resolved.children.map(childKey =>
|
|
17
|
+
typeof childKey === 'string' ? resolveTree(registry, childKey) : childKey
|
|
18
|
+
).filter(Boolean);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (typeof resolved.template === 'string') {
|
|
22
|
+
resolved.template = registry[resolved.template] || resolved.template;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return resolved;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* buildRefs(nodes)
|
|
30
|
+
* Scans normalized view nodes for ref properties.
|
|
31
|
+
* Returns { refName -> nodeId } so intent handlers can target view nodes.
|
|
32
|
+
*/
|
|
33
|
+
export function buildRefs(nodes) {
|
|
34
|
+
const refs = {};
|
|
35
|
+
for (const [id, node] of Object.entries(nodes)) {
|
|
36
|
+
if (node.ref) refs[node.ref] = id;
|
|
37
|
+
}
|
|
38
|
+
return refs;
|
|
39
|
+
}
|