/** * el-ui v0.1.0 — Activation-based frontend framework. * State is an Engram graph. Reactivity is spreading activation. * * Bundled single-file runtime. Import this from your compiled .el components: * import { Component, Graph, Renderer, Router, mount } from './el-ui.js'; * * Source: runtime/src/ * Build: cat runtime/src/graph.js runtime/src/activation.js * runtime/src/renderer.js runtime/src/router.js runtime/src/index.js > dist/el-ui.js */ // ── graph.js ────────────────────────────────────────────────────────────────── export class Graph { constructor() { this.nodes = new Map(); this.edges = new Map(); this.subscribers = new Map(); } seed({ type, name, content, importance = 0.5 }) { const id = this._uuid(); this.nodes.set(id, { id, type, name, content, importance, edges: [] }); return id; } get(id) { return this.nodes.get(id); } update(id, newContent) { const node = this.nodes.get(id); if (!node) return; node.content = newContent; node.importance = Math.min(1.0, node.importance + 0.1); const activated = this.activate(id); for (const nodeId of activated) { const subs = this.subscribers.get(nodeId); if (subs && subs.length > 0) { const n = this.nodes.get(nodeId); if (n) subs.forEach(cb => cb(n)); } } // Always notify direct subscribers const directSubs = this.subscribers.get(id); if (directSubs && directSubs.length > 0) { directSubs.forEach(cb => cb(node)); } } activate(seedId, maxDepth = 3, pruneThreshold = 0.01) { const result = new Set([seedId]); const queue = [{ id: seedId, strength: 1.0, depth: 0 }]; const bestStrength = new Map([[seedId, 1.0]]); while (queue.length > 0) { let bestIdx = 0; for (let i = 1; i < queue.length; i++) { if (queue[i].strength > queue[bestIdx].strength) bestIdx = i; } const { id, strength, depth } = queue.splice(bestIdx, 1)[0]; if (depth >= maxDepth) continue; const node = this.nodes.get(id); if (!node) continue; for (const edgeId of node.edges) { const edge = this.edges.get(edgeId); if (!edge) continue; const target = this.nodes.get(edge.to); if (!target) continue; const targetStrength = strength * edge.weight * Math.max(0, target.importance); if (targetStrength <= pruneThreshold) continue; const prevBest = bestStrength.get(edge.to) ?? 0; if (targetStrength > prevBest) { bestStrength.set(edge.to, targetStrength); result.add(edge.to); queue.push({ id: edge.to, strength: targetStrength, depth: depth + 1 }); } } } return result; } search(query, nodeType = null) { const results = []; const q = query.toLowerCase(); for (const [, node] of this.nodes) { if (nodeType && node.type !== nodeType) continue; const content = String(node.content).toLowerCase(); const nameMatch = node.name.toLowerCase().includes(q); const contentMatch = content.includes(q); if (nameMatch || contentMatch) { const score = (nameMatch ? 0.6 : 0) + (contentMatch ? 0.4 : 0); results.push({ ...node, score: score * node.importance }); } } return results.sort((a, b) => b.score - a.score); } subscribe(nodeId, callback) { if (!this.subscribers.has(nodeId)) this.subscribers.set(nodeId, []); this.subscribers.get(nodeId).push(callback); return () => { const subs = this.subscribers.get(nodeId); if (!subs) return; const idx = subs.indexOf(callback); if (idx >= 0) subs.splice(idx, 1); }; } connect(fromId, toId, { weight = 1.0, relation = 'related' } = {}) { const edgeId = this._uuid(); this.edges.set(edgeId, { id: edgeId, from: fromId, to: toId, weight, relation }); const node = this.nodes.get(fromId); if (node) node.edges.push(edgeId); return edgeId; } dump() { return [...this.nodes.values()]; } _uuid() { if (typeof crypto !== 'undefined' && crypto.randomUUID) return crypto.randomUUID(); return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => { const r = Math.random() * 16 | 0; return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16); }); } } // ── activation.js ───────────────────────────────────────────────────────────── export const PRUNE_THRESHOLD = 0.01; export function spreadActivation(graph, seeds, { maxDepth = 3, limit = 20, pruneThreshold = PRUNE_THRESHOLD, } = {}) { const bestStrength = new Map(); const queue = []; const seedSet = new Set(seeds); for (const seed of seeds) { if (!graph.get(seed)) continue; queue.push({ id: seed, strength: 1.0, hops: 0 }); bestStrength.set(seed, { strength: 1.0, hops: 0 }); } while (queue.length > 0) { let bestIdx = 0; for (let i = 1; i < queue.length; i++) { if (queue[i].strength > queue[bestIdx].strength) bestIdx = i; } const { id, strength, hops } = queue.splice(bestIdx, 1)[0]; if (hops >= maxDepth) continue; const node = graph.get(id); if (!node) continue; for (const edgeId of node.edges) { const edge = graph.edges.get(edgeId); if (!edge) continue; const target = graph.get(edge.to); if (!target) continue; const targetStrength = strength * edge.weight * Math.max(0, target.importance); if (targetStrength <= pruneThreshold) continue; const prev = bestStrength.get(edge.to); if (!prev || targetStrength > prev.strength) { const nextHops = hops + 1; bestStrength.set(edge.to, { strength: targetStrength, hops: nextHops }); queue.push({ id: edge.to, strength: targetStrength, hops: nextHops }); } } } const results = []; for (const [nodeId, { strength, hops }] of bestStrength) { if (seedSet.has(nodeId)) continue; const node = graph.get(nodeId); if (node) results.push({ nodeId, strength, hops, node }); } results.sort((a, b) => b.strength - a.strength); return results.slice(0, limit); } export function activationStrength(graph, fromId, toId, maxDepth = 3) { const results = spreadActivation(graph, [fromId], { maxDepth }); const found = results.find(r => r.nodeId === toId); return found ? found.strength : 0; } export function reachableNodes(graph, seedId, maxDepth = 3) { return graph.activate(seedId, maxDepth); } // ── renderer.js ─────────────────────────────────────────────────────────────── export class Renderer { constructor(root, component) { this.root = root; this.component = component; this._currentHtml = ''; this._boundHandlers = new WeakMap(); } mount() { this._currentHtml = this.component.render(); this.root.innerHTML = this._currentHtml; this._bindEvents(); if (typeof this.component.onMount === 'function') { this.component.onMount(); } } patch(_activatedNodes) { const newHtml = this.component.render(); if (newHtml !== this._currentHtml) { const focused = document.activeElement; const focusedId = focused && focused !== document.body ? focused.id : null; this.root.innerHTML = newHtml; this._bindEvents(); this._currentHtml = newHtml; if (focusedId) { const el = this.root.querySelector(`#${CSS.escape(focusedId)}`); if (el) el.focus(); } } } _bindEvents() { const comp = this.component; const eventNames = [ 'click', 'input', 'change', 'mouseenter', 'mouseleave', 'keydown', 'keyup', 'keypress', 'focus', 'blur', 'submit', 'mousedown', 'mouseup', 'dblclick', 'contextmenu', ]; for (const eventName of eventNames) { const attr = `data-el-${eventName}`; this.root.querySelectorAll(`[${attr}]`).forEach(el => { const handlerExpr = el.getAttribute(attr); if (!handlerExpr) return; const handler = this._compileHandler(handlerExpr, comp); if (handler) { const prev = this._boundHandlers.get(el); if (prev && prev[eventName]) el.removeEventListener(eventName, prev[eventName]); el.addEventListener(eventName, handler); if (!this._boundHandlers.has(el)) this._boundHandlers.set(el, {}); this._boundHandlers.get(el)[eventName] = handler; } }); } } _compileHandler(expr, comp) { try { const decoded = expr .replace(/"/g, '"') .replace(/'/g, "'") .replace(/&/g, '&') .replace(/</g, '<') .replace(/>/g, '>'); if (decoded.includes('=>')) { // eslint-disable-next-line no-new-func const fn = new Function('__self', `return ${decoded}`)(comp); return typeof fn === 'function' ? fn : null; } else if (decoded.includes('(')) { // eslint-disable-next-line no-new-func return new Function('__self', 'event', `${decoded}`).bind(null, comp); } else { return comp[decoded] ? comp[decoded].bind(comp) : null; } } catch (e) { console.warn(`el-ui: failed to compile handler: ${expr}`, e); return null; } } } // ── router.js ───────────────────────────────────────────────────────────────── export class Router { constructor(graph, routes) { this.graph = graph; this.routes = routes; this.currentPath = window.location.pathname; this._routeNodes = {}; this._subscribers = []; for (const [path] of Object.entries(routes)) { const id = graph.seed({ type: 'route', name: path, content: path, importance: 0.3 }); this._routeNodes[path] = id; } const paths = Object.keys(routes).sort(); for (let i = 0; i < paths.length - 1; i++) { const parent = paths[i]; const child = paths[i + 1]; if (child.startsWith(parent) && parent !== child) { graph.connect(this._routeNodes[parent], this._routeNodes[child], { weight: 0.8, relation: 'subroute', }); } } window.addEventListener('popstate', () => this._activate(window.location.pathname)); } navigate(path, replace = false) { if (path === this.currentPath) return; if (replace) history.replaceState({}, '', path); else history.pushState({}, '', path); this._activate(path); } currentComponent() { return this.routes[this.currentPath] ?? this.routes['*'] ?? null; } subscribe(callback) { this._subscribers.push(callback); return () => { const idx = this._subscribers.indexOf(callback); if (idx >= 0) this._subscribers.splice(idx, 1); }; } href(path) { return path; } _activate(path) { const prevPath = this.currentPath; this.currentPath = path; const matchedPath = this._matchPath(path); if (matchedPath && this._routeNodes[matchedPath]) { this.graph.activate(this._routeNodes[matchedPath]); const node = this.graph.get(this._routeNodes[matchedPath]); if (node) node.importance = Math.min(1.0, node.importance + 0.2); } if (path !== prevPath) this._subscribers.forEach(cb => cb(path)); } _matchPath(path) { if (this.routes[path]) return path; const candidates = Object.keys(this.routes) .filter(r => r !== '*' && path.startsWith(r)) .sort((a, b) => b.length - a.length); if (candidates.length > 0) return candidates[0]; return '*'; } } // ── Component base class & mount() ─────────────────────────────────────────── export class Component { constructor(props = {}) { this._graph = new Graph(); this._stateNodes = {}; this._state = {}; this.props = props; this._renderer = null; } setState(name, value) { if (this._stateNodes[name] !== undefined) { this._graph.update(this._stateNodes[name], value); if (this._renderer) this._renderer.patch(); } } _child(ComponentClass, props = {}) { try { const instance = new ComponentClass(props); instance._graph = this._graph; return instance.render(); } catch (e) { console.warn(`el-ui: failed to render child ${ComponentClass?.name}`, e); return ``; } } render() { return ''; } onMount() {} } export function mount(ComponentClass, selector, props = {}) { const root = document.querySelector(selector); if (!root) throw new Error(`el-ui: no element found for selector '${selector}'`); const component = new ComponentClass(props); const renderer = new Renderer(root, component); component._renderer = renderer; renderer.mount(); return component; } export default { mount, Component, Graph, Renderer, Router };