micra.js 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 +166 -0
- package/dist/core/bus.d.ts +32 -0
- package/dist/core/mount.d.ts +27 -0
- package/dist/core/reactive.d.ts +31 -0
- package/dist/core/registry.d.ts +56 -0
- package/dist/core/start.d.ts +26 -0
- package/dist/dom/directives.d.ts +39 -0
- package/dist/dom/each.d.ts +25 -0
- package/dist/dom/events.d.ts +41 -0
- package/dist/dom/query.d.ts +23 -0
- package/dist/dom/refs.d.ts +23 -0
- package/dist/index.d.ts +29 -0
- package/dist/micra.cjs.js +576 -0
- package/dist/micra.cjs.js.map +7 -0
- package/dist/micra.esm.js +554 -0
- package/dist/micra.esm.js.map +7 -0
- package/dist/micra.js +578 -0
- package/dist/micra.js.map +7 -0
- package/dist/micra.min.js +2 -0
- package/dist/types.d.ts +138 -0
- package/dist/utils/expr.d.ts +27 -0
- package/dist/utils/fetch.d.ts +45 -0
- package/package.json +59 -0
- package/src/core/bus.ts +49 -0
- package/src/core/mount.ts +144 -0
- package/src/core/reactive.ts +50 -0
- package/src/core/registry.ts +100 -0
- package/src/core/start.ts +42 -0
- package/src/dom/directives.ts +207 -0
- package/src/dom/each.ts +167 -0
- package/src/dom/events.ts +145 -0
- package/src/dom/query.ts +36 -0
- package/src/dom/refs.ts +35 -0
- package/src/index.ts +42 -0
- package/src/types.ts +157 -0
- package/src/utils/expr.ts +72 -0
- package/src/utils/fetch.ts +100 -0
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/index.ts", "../src/utils/fetch.ts", "../src/core/registry.ts", "../src/utils/expr.ts", "../src/core/bus.ts", "../src/core/reactive.ts", "../src/dom/query.ts", "../src/dom/directives.ts", "../src/dom/events.ts", "../src/dom/each.ts", "../src/dom/refs.ts", "../src/core/mount.ts", "../src/core/start.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * Micra.js \u2014 Lightweight reactive framework for small sites and simple SaaS.\n *\n * Public surface \u2014 re-exports only.\n *\n * Features:\n * - JS expressions in directives (data-if=\"count > 0\")\n * - Keyed list diffing (data-each=\"items\" data-key=\"id\")\n * - Auto-mount via data-component (Micra.define + Micra.start)\n * - Props from SSR data-attributes (this.prop('page'))\n * - Built-in fetch helper (this.fetch('/api/...'))\n * - Global event bus (Micra.on / Micra.emit)\n * - DOM refs (data-ref=\"chart\" \u2192 this.refs.chart)\n * - Additive class toggling (data-class=\"active:isActive\")\n * - @event shorthand (@click=\"increment\")\n * - Lifecycle: onCreate, onDestroy\n * - SSR-friendly: Micra.start() is safe to call multiple times\n * - Directive cache: O(1) re-renders after first mount\n *\n * Size target: < 5 KB minified+gzipped\n *\n * @module Micra\n */\n\n// \u2500\u2500 Public types \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nexport type {\n StateRecord,\n UnsubFn,\n EventHandler,\n FetchOptions,\n ComponentInstance,\n ComponentDefinition,\n} from './types'\n\n// \u2500\u2500 Errors \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nexport { FetchError } from './utils/fetch'\n\n// \u2500\u2500 Public API \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nexport { define, defineComponent, instances, registry, debug } from './core/registry'\nexport { mount } from './core/mount'\nexport { start } from './core/start'\nexport { on, off, emit } from './core/bus'\n", "/**\n * src/utils/fetch.ts \u2014 HTTP fetch helper.\n *\n * Responsibilities:\n * - Auto-attach CSRF token from <meta name=\"csrf-token\">\n * - Serialize POST/PUT/PATCH body as JSON\n * - Serialize GET/HEAD options as query params\n * - Throw a typed FetchError on non-2xx responses\n * - Return parsed JSON or text\n *\n * LLM NOTE: This module is PURE (no DOM side effects beyond reading a meta tag).\n * It wraps the native fetch() API with SaaS-friendly defaults.\n */\n\nimport type { FetchOptions } from '../types'\n\n// \u2500\u2500 CSRF \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/** Read CSRF token from <meta name=\"csrf-token\"> (Rails, Laravel, Django\u2026). */\nfunction getCSRF(): string | null {\n return document.querySelector('meta[name=\"csrf-token\"]')?.getAttribute('content') ?? null\n}\n\n// \u2500\u2500 Typed error \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/**\n * Thrown by `this.fetch()` when the server returns a non-2xx status.\n *\n * @example\n * try {\n * await this.fetch('/api/data')\n * } catch (e) {\n * if (e instanceof FetchError && e.status === 404) { ... }\n * }\n */\nexport class FetchError extends Error {\n constructor(\n message: string,\n public readonly status: number,\n public readonly response: Response,\n ) {\n super(message)\n this.name = 'FetchError'\n }\n}\n\n// \u2500\u2500 Fetch helper \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/**\n * Fetch wrapper with SaaS defaults.\n *\n * - GET/HEAD: extra `options` keys become URL query params\n * - POST/PUT/PATCH/DELETE: `options.body` is JSON-serialized\n * - Attaches X-CSRF-Token header automatically\n * - Returns parsed JSON if Content-Type is application/json, else text\n *\n * @example\n * // GET with params \u2192 /api/users?page=2&status=active\n * const data = await this.fetch('/api/users', { page: 2, status: 'active' })\n *\n * // POST with JSON body\n * await this.fetch('/api/invite', { method: 'POST', body: { email, role } })\n */\nexport async function micraFetch(url: string, options: FetchOptions = {}): Promise<unknown> {\n const method = ((options.method as string | undefined) ?? 'GET').toUpperCase()\n const headers: Record<string, string> = {\n Accept: 'application/json',\n ...(options.headers as Record<string, string> | undefined),\n }\n\n const csrf = getCSRF()\n if (csrf) headers['X-CSRF-Token'] = csrf\n\n let finalUrl = url\n let body: string | undefined\n\n if (method === 'GET' || method === 'HEAD') {\n const params: Record<string, string> = {}\n for (const [k, v] of Object.entries(options)) {\n if (k !== 'method' && k !== 'headers' && v != null) params[k] = String(v)\n }\n if (Object.keys(params).length)\n finalUrl += (url.includes('?') ? '&' : '?') + new URLSearchParams(params)\n } else {\n headers['Content-Type'] = 'application/json'\n body = JSON.stringify(options.body !== undefined ? options.body : options)\n }\n\n const res = await fetch(finalUrl, {\n method,\n headers,\n ...(body !== undefined ? { body } : {}),\n })\n\n if (!res.ok)\n throw new FetchError(`[Micra] fetch: ${method} ${url} \u2192 ${res.status}`, res.status, res)\n\n const ct = res.headers.get('content-type') ?? ''\n return ct.includes('application/json') ? res.json() : res.text()\n}\n", "/**\n * src/core/registry.ts \u2014 Component definition registry and instance store.\n *\n * Responsibilities:\n * - Store named component definitions (define / registry)\n * - Store live component instances keyed by root HTMLElement (instances)\n *\n * LLM NOTE: Both maps are module-level singletons (one per page load).\n * They are intentionally mutable from mount.ts and start.ts.\n */\n\nimport type {\n ComponentDefinition,\n ComponentInstance,\n InternalInstance,\n StateRecord,\n} from '../types'\n\n// Named definition map \u2014 populated by define()\nexport const _registry = new Map<string, ComponentDefinition>()\n\n// Live instance map \u2014 populated by mount(), cleared by instance.destroy()\nexport const _instances = new Map<HTMLElement, InternalInstance>()\n\n// \u2500\u2500 Public accessors \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/**\n * Register a component definition under `name`.\n *\n * @example\n * define('counter', { state: { count: 0 }, inc() { this.state.count++ } })\n */\nexport function define<S extends StateRecord>(\n name: string,\n definition: ComponentDefinition<S>,\n): void {\n _registry.set(name, definition as ComponentDefinition)\n}\n\n/**\n * Type-helper \u2014 returns `definition` unchanged but lets TypeScript infer `S`\n * from the `state` literal so all methods are typed with the correct `this`.\n *\n * Use this when defining a component outside a `define()` call.\n *\n * @example\n * const counter = defineComponent({\n * state: { count: 0 },\n * increment() { this.state.count++ }, // this.state: { count: number } \u2713\n * })\n * Micra.define('counter', counter)\n */\nexport function defineComponent<S extends StateRecord>(\n definition: ComponentDefinition<S>,\n): ComponentDefinition<S> {\n return definition\n}\n\n/**\n * Returns a read-only view of all live instances (keyed by root element).\n * Useful for DevTools / debugging.\n */\nexport function instances(): ReadonlyMap<HTMLElement, ComponentInstance> {\n return _instances\n}\n\n/**\n * Returns a read-only view of all registered component definitions.\n * Useful for DevTools / debugging.\n */\nexport function registry(): ReadonlyMap<string, ComponentDefinition> {\n return _registry\n}\n\n/**\n * Print all live component instances to the browser console.\n * Shows component name, root element, and current state for each instance.\n *\n * @example\n * // In browser DevTools console:\n * Micra.debug()\n * // [Micra] 3 live component(s)\n * // counter $el: <div> state: { count: 5 }\n * // user-list $el: <div> state: { users: [...], loading: false }\n */\nexport function debug(): void {\n if (_instances.size === 0) {\n console.log('[Micra] No live components.')\n return\n }\n console.group(`[Micra] ${_instances.size} live component(s)`)\n for (const [el, instance] of _instances) {\n const name = el.getAttribute('data-component') ?? '(unnamed)'\n console.group(`%c${name}`, 'font-weight:bold;color:#6366f1')\n console.log('$el ', el)\n console.log('state', { ...instance.state })\n console.groupEnd()\n }\n console.groupEnd()\n}\n", "/**\n * src/utils/expr.ts \u2014 JS expression evaluator.\n *\n * Responsibilities:\n * - Compile expression strings into cached functions\n * - Evaluate them against a state object\n * - Fast-path for simple property lookups\n *\n * LLM NOTE: This module is PURE. It does not touch the DOM or mutate state.\n * All side effects are isolated to console.warn on invalid expressions.\n */\n\nimport type { StateRecord } from '../types'\n\n// \u2500\u2500 Expression cache \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// Compiled functions are keyed by expression string \u2014 Function() is only called\n// once per unique expression across the entire app lifetime.\n\n// LLM NOTE: exprCache is module-level (shared across all components).\n// This is intentional \u2014 most apps reuse the same expressions.\nconst exprCache = new Map<string, (state: StateRecord) => unknown>()\n\n// Simple identifier or dot-path: \"count\", \"user.name\", \"item.email\"\n// Matches: letter/$/_ followed by word chars, optionally with .property chains\nconst SIMPLE_PATH = /^[a-zA-Z_$][a-zA-Z0-9_$]*(\\.[a-zA-Z_$][a-zA-Z0-9_$]*)*$/\n\n/**\n * Evaluate a JS expression string against a state object.\n *\n * Results are cached by expression string \u2014 repeated evaluations hit the cache.\n * Uses a fast-path for simple dot-paths (e.g. \"count\", \"user.name\") that avoids\n * Function() overhead.\n *\n * @example\n * evalExpr('count > 0', { count: 5 }) // \u2192 true\n * evalExpr('user.name', { user: { name: 'Alice' } }) // \u2192 'Alice'\n * evalExpr('price * qty', { price: 9.99, qty: 3 }) // \u2192 29.97\n */\nexport function evalExpr(expr: string, state: StateRecord): unknown {\n // Fast-path: simple property access \u2014 no Function() needed\n if (SIMPLE_PATH.test(expr)) {\n return expr.split('.').reduce<unknown>((obj, key) =>\n obj != null ? (obj as StateRecord)[key] : undefined,\n state,\n )\n }\n\n if (!exprCache.has(expr)) {\n try {\n exprCache.set(\n expr,\n new Function('$s', `with($s){return (${expr})}`) as (s: StateRecord) => unknown,\n )\n } catch {\n warn(`invalid expression \"${expr}\"`)\n exprCache.set(expr, () => undefined)\n }\n }\n\n try {\n return exprCache.get(expr)!(state)\n } catch {\n return undefined\n }\n}\n\n// \u2500\u2500 Dev warnings \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/** @internal Consistent warning prefix. */\nexport function warn(msg: string): void {\n console.warn(`[Micra] ${msg}`)\n}\n", "/**\n * src/core/bus.ts \u2014 Global event bus.\n *\n * Responsibilities:\n * - Publish events (emit)\n * - Subscribe and unsubscribe (on / off)\n * - Provide unsubscribe tokens for component cleanup\n *\n * LLM NOTE: The bus is a module-level singleton.\n * Component instances subscribe via `instance.on()` which auto-registers\n * the unsub token in `instance.__micraSubs` for cleanup on destroy().\n */\n\nimport type { EventHandler, UnsubFn } from '../types'\n\n// Module-level bus state \u2014 one bus per page load.\nconst _bus = new Map<string, Set<EventHandler>>()\n\n/**\n * Subscribe to a named event. Returns an unsubscribe function.\n *\n * @example\n * const unsub = on('user:login', (user) => console.log(user))\n * unsub() // stop listening\n */\nexport function on<T = unknown>(event: string, handler: EventHandler<T>): UnsubFn {\n if (!_bus.has(event)) _bus.set(event, new Set())\n _bus.get(event)!.add(handler as EventHandler)\n return () => off(event, handler as EventHandler)\n}\n\n/**\n * Unsubscribe a specific handler from an event.\n */\nexport function off(event: string, handler: EventHandler): void {\n _bus.get(event)?.delete(handler)\n}\n\n/**\n * Publish an event to all subscribers. Errors are caught per-handler.\n *\n * @example\n * emit('user:updated', { id: 1, name: 'Alice' })\n */\nexport function emit(event: string, payload?: unknown): void {\n _bus.get(event)?.forEach(h => {\n try { h(payload) } catch (e) { console.error(`[Micra] bus error [${event}]:`, e) }\n })\n}\n", "/**\n * src/core/reactive.ts \u2014 Reactive state proxy and batch scheduler.\n *\n * Responsibilities:\n * - Wrap a plain state object in a Proxy that notifies on writes\n * - Batch multiple synchronous mutations into a single microtask render\n *\n * LLM NOTE: Both functions are PURE constructors \u2014 they have no side effects\n * beyond setting up a Proxy / Promise chain. No DOM access here.\n */\n\nimport type { StateRecord } from '../types'\n\n/**\n * Wrap `obj` in a shallow Proxy. Any property write calls `schedule()`.\n * Arrays: replace, don't mutate \u2014 `state.items = [...state.items, x]`.\n *\n * @example\n * const raw = { count: 0 }\n * const state = createReactiveState(raw, render)\n * state.count = 5 // triggers render() in next microtask\n */\nexport function createReactiveState<S extends StateRecord>(obj: S, schedule: () => void): S {\n return new Proxy(obj, {\n set(target, key: string, value: unknown) {\n // Cast through StateRecord \u2014 TypeScript cannot write through a generic index\n ;(target as StateRecord)[key] = value\n schedule()\n return true\n },\n })\n}\n\n/**\n * Return a debounce function that defers `render` to the next microtask.\n * Multiple calls within the same tick collapse to a single render.\n *\n * @example\n * const schedule = createScheduler(render)\n * schedule() // defers render\n * schedule() // no-op \u2014 already pending\n */\nexport function createScheduler(render: () => void): () => void {\n let pending = false\n return function schedule() {\n if (pending) return\n pending = true\n Promise.resolve().then(() => { pending = false; render() })\n }\n}\n", "/**\n * src/dom/query.ts \u2014 DOM query helpers.\n *\n * LLM NOTE: These are utility functions with no side effects.\n * queryOwn is the critical function that prevents a parent component from\n * accidentally processing directives belonging to a nested child component.\n */\n\n/**\n * querySelectorAll wrapper \u2014 returns a typed array.\n */\nexport function queryAll(root: ParentNode, sel: string): Element[] {\n return Array.from(root.querySelectorAll(sel))\n}\n\n/**\n * Like querySelectorAll, but EXCLUDES elements that live inside a nested\n * `[data-component]` subtree.\n *\n * This is what prevents a parent component's render() from clobbering\n * the DOM managed by a child component.\n *\n * LLM NOTE: The walk goes up parentElement until it hits `root` or null.\n * If any ancestor (between el and root) has data-component, the element is\n * owned by that nested component, not by root's component \u2014 so we skip it.\n */\nexport function queryOwn(root: Element, attr: string): Element[] {\n return queryAll(root, `[${attr}]`).filter(el => {\n let node: Element | null = el.parentElement\n while (node && node !== root) {\n if (node.hasAttribute('data-component')) return false\n node = node.parentElement\n }\n return true\n })\n}\n", "/**\n * src/dom/directives.ts \u2014 Apply DOM directives to a component subtree.\n *\n * Responsibilities:\n * - data-text, data-html, data-if, data-show, data-bind, data-model\n * - data-class (additive class toggling)\n * - Directive result cache (built once per element, reused on re-renders)\n *\n * LLM NOTE: applyDirectives() is called on every render. The directive cache\n * (DirectiveCache on el.__micraCache) avoids repeated querySelectorAll on\n * re-renders \u2014 cache is built lazily on the first call for each root element.\n *\n * Important: this module does NOT handle data-each \u2014 see dom/each.ts.\n */\n\nimport type {\n CachedBinding,\n DirectiveCache,\n InternalInstance,\n MicraElement,\n StateRecord,\n} from '../types'\nimport { evalExpr, warn } from '../utils/expr'\nimport { queryOwn, queryAll } from './query'\n\n// \u2500\u2500 Directive appliers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// Each function is PURE relative to state \u2014 reads state, writes DOM.\n\nfunction applyText(el: Element, expr: string, state: StateRecord): void {\n const text = String(evalExpr(expr, state) ?? '')\n if (el.textContent !== text) el.textContent = text\n}\n\nfunction applyHtml(el: Element, expr: string, state: StateRecord): void {\n el.innerHTML = String(evalExpr(expr, state) ?? '')\n}\n\nfunction applyIf(el: Element, expr: string, state: StateRecord): void {\n (el as HTMLElement).style.display = evalExpr(expr, state) ? '' : 'none'\n}\n\nfunction applyBind(el: Element, expr: string, state: StateRecord): void {\n for (const pair of expr.split(',')) {\n const colonIdx = pair.indexOf(':')\n if (colonIdx === -1) continue\n const attr = pair.slice(0, colonIdx).trim()\n const valExpr = pair.slice(colonIdx + 1).trim()\n const val = evalExpr(valExpr, state)\n\n if (attr === 'class') {\n (el as HTMLElement).className = String(val ?? '')\n } else if (attr === 'value') {\n if (document.activeElement !== el)\n (el as HTMLInputElement).value = String(val ?? '')\n } else if (attr === 'style') {\n if (typeof val === 'object' && val !== null) {\n Object.assign((el as HTMLElement).style, val)\n } else {\n el.setAttribute('style', String(val ?? ''))\n }\n } else if (typeof val === 'boolean') {\n val ? el.setAttribute(attr, '') : el.removeAttribute(attr)\n } else {\n val == null ? el.removeAttribute(attr) : el.setAttribute(attr, String(val))\n }\n }\n}\n\n/**\n * data-class=\"active:isActive, disabled:count === 0\"\n * Parses comma-separated `className:expression` pairs and toggles classes additively.\n * Unlike data-bind=\"class:expr\" this does NOT replace the full className.\n *\n * Syntax mirrors data-bind \u2014 split by comma, then by first colon.\n *\n * @example\n * <div data-class=\"active:tab === 'home', hidden:!loaded\">\n */\nfunction applyClass(el: Element, expr: string, state: StateRecord): void {\n for (const pair of expr.split(',')) {\n const colonIdx = pair.indexOf(':')\n if (colonIdx === -1) continue\n const cls = pair.slice(0, colonIdx).trim()\n const valExpr = pair.slice(colonIdx + 1).trim()\n if (!cls) continue\n el.classList.toggle(cls, Boolean(evalExpr(valExpr, state)))\n }\n}\n\nfunction applyModel(\n el: Element,\n key: string,\n rawState: StateRecord,\n): void {\n const html = el as HTMLInputElement\n if (document.activeElement !== el) {\n html.value = rawState[key] == null ? '' : String(rawState[key])\n }\n // listener is attached separately in events.ts \u2014 this only syncs the value\n}\n\n// \u2500\u2500 Directive cache \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/** @internal Collect all directive bindings for a root element. Built once. */\nfunction buildCache(root: Element): DirectiveCache {\n const pick = (attr: string): CachedBinding[] => {\n const els = queryOwn(root, attr)\n // Include root itself\n if ((root as HTMLElement).hasAttribute?.(attr)) els.unshift(root)\n return els\n .filter(el => !el.closest('template'))\n .map(el => ({ el, expr: el.getAttribute(attr)! }))\n }\n return {\n text: pick('data-text'),\n html: pick('data-html'),\n if: pick('data-if'),\n show: pick('data-show'),\n bind: pick('data-bind'),\n model: pick('data-model'),\n class: pick('data-class'),\n }\n}\n\n// \u2500\u2500 Main entry point \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/**\n * Apply all non-each directives to a component subtree.\n *\n * For regular Elements: directive bindings are cached in `el.__micraCache`\n * after the first call \u2014 subsequent re-renders skip querySelectorAll entirely.\n *\n * For DocumentFragments (no-key each clones): always re-scan because these\n * fragments are new clones on every render.\n *\n * @param root - Component root Element or DocumentFragment (no-key each clone)\n * @param state - Expression state (may include item/index for each rows)\n * @param rawState - Raw (non-proxy) state for model sync\n * @param instance - Component instance (unused here, kept for future hooks)\n */\nexport function applyDirectives<S extends StateRecord>(\n root: Element | DocumentFragment,\n state: StateRecord,\n rawState: StateRecord,\n _instance: InternalInstance<S>,\n): void {\n // DocumentFragments are temporary clones \u2014 always scan, never cache\n if (root.nodeType === Node.DOCUMENT_FRAGMENT_NODE) {\n applyFromList(buildFragmentList(root as DocumentFragment), state, rawState)\n return\n }\n\n const el = root as MicraElement\n if (!el.__micraCache) el.__micraCache = buildCache(el)\n applyFromList(el.__micraCache, state, rawState)\n}\n\n/** @internal Apply a pre-built cache / binding list to current state. */\nfunction applyFromList(\n cache: DirectiveCache,\n state: StateRecord,\n rawState: StateRecord,\n): void {\n cache.text.forEach(b => applyText(b.el, b.expr, state))\n cache.html.forEach(b => applyHtml(b.el, b.expr, state))\n cache.if.forEach(b => applyIf(b.el, b.expr, state))\n cache.show.forEach(b => applyIf(b.el, b.expr, state))\n cache.bind.forEach(b => applyBind(b.el, b.expr, state))\n cache.model.forEach(b => applyModel(b.el, b.expr.trim(), rawState))\n cache.class.forEach(b => applyClass(b.el, b.expr, state))\n}\n\n/** @internal Scan a DocumentFragment (no-key each clone) \u2014 returns a DirectiveCache. */\nfunction buildFragmentList(frag: DocumentFragment): DirectiveCache {\n const pick = (attr: string): CachedBinding[] =>\n queryAll(frag, `[${attr}]`)\n .filter(el => !el.closest('template'))\n .map(el => ({ el, expr: el.getAttribute(attr)! }))\n return {\n text: pick('data-text'),\n html: pick('data-html'),\n if: pick('data-if'),\n show: pick('data-show'),\n bind: pick('data-bind'),\n model: pick('data-model'),\n class: pick('data-class'),\n }\n}\n\n// \u2500\u2500 Dev warning helper \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/**\n * Validate directive usage and emit dev warnings.\n * Called once after the initial render of a component.\n *\n * @internal\n */\nexport function validateDirectives(root: Element): void {\n queryOwn(root, 'data-each').forEach(el => {\n if (!el.hasAttribute('data-key')) {\n warn(`data-each=\"${el.getAttribute('data-each')}\" has no data-key \u2014 keyed diff disabled. Add data-key=\"id\" for better performance.`)\n }\n })\n}\n\n// Re-export warn for use in other modules\nexport { warn }\n", "/**\n * src/dom/events.ts \u2014 DOM event binding.\n *\n * Responsibilities:\n * - Bind `data-on=\"event:method\"` listeners (once per element)\n * - Bind `@event=\"method\"` shorthand (scanned once per component root)\n *\n * LLM NOTE: Listeners are attached exactly once. The `__micraEvents` and\n * `__micraAtScanned` flags prevent duplicate bindings on re-renders.\n */\n\nimport type { InternalInstance, MicraElement, StateRecord } from '../types'\nimport { evalExpr, warn } from '../utils/expr'\nimport { queryOwn, queryAll } from './query'\n\n// \u2500\u2500 data-on \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/**\n * Bind `data-on=\"event:method[,event2:method2]\"` listeners.\n * Listeners are bound once \u2014 re-render calls are no-ops for already-bound elements.\n *\n * Supports modifiers: `click.prevent`, `click.stop`, `click.self`.\n *\n * @example\n * <button data-on=\"click:save\">Save</button>\n * <form data-on=\"submit.prevent:handleSubmit\">\n */\nexport function bindDataOn<S extends StateRecord>(\n root: Element,\n instance: InternalInstance<S>,\n): void {\n const isFragment = root.nodeType === 11\n const els = isFragment\n ? queryAll(root as unknown as ParentNode, '[data-on]')\n : queryOwn(root, 'data-on')\n\n // Include root itself if it carries data-on (e.g., the keyed item IS the button)\n if (!isFragment && (root as HTMLElement).hasAttribute?.('data-on') && !els.includes(root))\n els.unshift(root)\n\n for (const el of els) {\n const mEl = el as MicraElement\n if (mEl.__micraEvents) continue\n mEl.__micraEvents = true\n\n const spec = mEl.dataset['on'] ?? ''\n for (const part of spec.split(',')) {\n const [evSpec, method] = part.trim().split(':') as [string, string]\n if (!evSpec || !method) continue\n\n const [evName, ...mods] = evSpec.split('.')\n\n el.addEventListener(evName!, (e: Event) => {\n if (mods.includes('prevent')) e.preventDefault()\n if (mods.includes('stop')) e.stopPropagation()\n if (mods.includes('self') && e.target !== el) return\n\n const fn = instance[method.trim()]\n if (typeof fn === 'function') (fn as (e: Event) => void).call(instance, e)\n else warn(`method \"${method.trim()}\" not found`)\n })\n }\n }\n}\n\n// \u2500\u2500 @event shorthand \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/**\n * Bind `@event=\"method\"` shorthand attributes (Stimulus-style).\n * Scanned once per component root (guarded by `__micraAtScanned`).\n * Supports the same modifiers as data-on: `@click.prevent=\"submit\"`.\n *\n * @example\n * <button @click=\"increment\">+</button>\n * <form @submit.prevent=\"handleSubmit\">\n */\nexport function bindAtEvents<S extends StateRecord>(\n root: Element,\n instance: InternalInstance<S>,\n): void {\n const mRoot = root as MicraElement\n if (mRoot.__micraAtScanned) return\n mRoot.__micraAtScanned = true\n\n const all = queryAll(root, '*')\n for (const el of all) {\n for (const attr of Array.from(el.attributes)) {\n if (!attr.name.startsWith('@')) continue\n const [evSpec, ...rest] = attr.name.slice(1).split('.')\n const method = attr.value.trim()\n\n el.addEventListener(evSpec!, (e: Event) => {\n if (rest.includes('prevent')) e.preventDefault()\n if (rest.includes('stop')) e.stopPropagation()\n if (rest.includes('self') && e.target !== el) return\n\n const fn = instance[method]\n if (typeof fn === 'function') (fn as (e: Event) => void).call(instance, e)\n else warn(`method \"${method}\" not found`)\n })\n }\n }\n}\n\n// \u2500\u2500 data-model \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/**\n * Two-way binding: `data-model=\"key\"` wires <input>/<select>/<textarea>\n * to `state[key]`. Binding is attached once per element.\n *\n * @example\n * <input data-model=\"search\"> // updates state.search on every keystroke\n * <select data-model=\"sortBy\"> // updates state.sortBy on change\n */\nexport function bindModels<S extends StateRecord>(\n root: Element,\n instance: InternalInstance<S>,\n): void {\n const isFragment = root.nodeType === 11\n const els = isFragment\n ? queryAll(root as unknown as ParentNode, '[data-model]')\n : queryOwn(root, 'data-model')\n\n for (const el of els) {\n const mEl = el as MicraElement\n if (mEl.__micraModel) continue\n mEl.__micraModel = true\n\n const key = (el as HTMLInputElement).dataset['model'] ?? ''\n const tag = el.tagName\n\n const update = () => {\n const val = tag === 'INPUT' && (el as HTMLInputElement).type === 'checkbox'\n ? (el as HTMLInputElement).checked\n : (el as HTMLInputElement).value\n ;(instance.state as StateRecord)[key] = val\n }\n\n el.addEventListener(tag === 'SELECT' || (el as HTMLInputElement).type === 'radio'\n ? 'change'\n : 'input',\n update,\n )\n }\n}\n", "/**\n * src/dom/each.ts \u2014 Keyed and non-keyed list rendering (data-each).\n *\n * Responsibilities:\n * - Process `<template data-each=\"items\" data-key=\"id\">` elements\n * - Keyed diff: reuse/reorder DOM nodes by key \u2014 O(n) with a Map\n * - Non-keyed fallback: full replace (no key \u2192 warn in dev, full re-render)\n * - Apply directives to each row with a scoped itemState\n *\n * LLM NOTE: renderList() is called on every render cycle AFTER applyDirectives().\n * Only <template> elements with data-each are processed.\n * Keyed mode (data-key present) mutates the DOM in-place \u2014 nodes are\n * created once and reused. Non-keyed mode removes all nodes and re-clones.\n */\n\nimport type { InternalInstance, MicraElement, MicraTemplate, StateRecord } from '../types'\nimport { evalExpr, warn } from '../utils/expr'\nimport { applyDirectives } from './directives'\nimport { bindDataOn, bindAtEvents } from './events'\nimport { queryOwn, queryAll } from './query'\n\n/**\n * Process all `<template data-each>` elements owned by `root`.\n * Scoped itemState makes `item`, `index`, `$index` available in row expressions.\n *\n * @param root - Component root Element\n * @param state - Expression state (proxy merging rawState + instance)\n * @param rawState - Raw (non-proxy) state \u2014 used for model binding\n * @param instance - Component instance (for event binding)\n */\nexport function renderList<S extends StateRecord>(\n root: Element,\n state: StateRecord,\n rawState: StateRecord,\n instance: InternalInstance<S>,\n): void {\n queryOwn(root, 'data-each').forEach(tmplEl => {\n if (tmplEl.tagName !== 'TEMPLATE') return\n const tmpl = tmplEl as MicraTemplate\n\n const itemsExpr = tmpl.getAttribute('data-each')!\n const keyAttr = tmpl.getAttribute('data-key') ?? null\n const items = evalExpr(itemsExpr, state)\n\n // Ensure marker comment + internal state are initialized\n if (!tmpl.__micraMarker) {\n const m = document.createComment(`each:${itemsExpr}`)\n tmpl.after(m)\n tmpl.__micraMarker = m\n tmpl.__micraNodes = new Map()\n tmpl.__micraList = []\n }\n\n const marker = tmpl.__micraMarker\n const keyMap = tmpl.__micraNodes\n const parent = marker.parentNode!\n\n // Empty / non-array: clear all rendered rows\n if (!Array.isArray(items)) {\n tmpl.__micraList.forEach(n => n.remove())\n tmpl.__micraList = []\n keyMap.clear()\n return\n }\n\n if (keyAttr) {\n renderKeyed(tmpl, items as StateRecord[], keyAttr, marker, keyMap, parent, state, rawState, instance)\n } else {\n renderNoKey(tmpl, items as StateRecord[], marker, parent, state, rawState, instance)\n }\n })\n}\n\n// \u2500\u2500 Keyed diff \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nfunction renderKeyed<S extends StateRecord>(\n tmpl: MicraTemplate,\n items: StateRecord[],\n keyAttr: string,\n marker: Comment,\n keyMap: Map<unknown, MicraElement>,\n parent: Node,\n state: StateRecord,\n rawState: StateRecord,\n instance: InternalInstance<S>,\n): void {\n const nextKeys = new Set<unknown>()\n const nextNodes: MicraElement[] = []\n\n for (const [index, item] of items.entries()) {\n const key = item[keyAttr]\n if (key == null) warn(`data-key=\"${keyAttr}\" is null/undefined on item at index ${index}`)\n nextKeys.add(key)\n\n let node = keyMap.get(key) as MicraElement | undefined\n\n if (!node) {\n // Clone template and wrap multi-root fragments in a display:contents element\n const frag = tmpl.content.cloneNode(true) as DocumentFragment\n if (frag.childNodes.length === 1) {\n node = frag.firstElementChild as MicraElement\n } else {\n node = document.createElement('micra-each-item') as MicraElement\n node.style.display = 'contents'\n node.append(frag)\n }\n node.__micraKey = key\n keyMap.set(key, node)\n // Bind data-on and @event handlers on the freshly created node (once)\n bindDataOn(node, instance)\n bindAtEvents(node, instance)\n }\n\n const itemState = Object.assign(\n Object.create(state) as StateRecord,\n { item, index, $index: index },\n )\n applyDirectives(node, itemState, rawState, instance)\n nextNodes.push(node)\n }\n\n // Remove stale nodes\n for (const [key, node] of keyMap) {\n if (!nextKeys.has(key)) { node.remove(); keyMap.delete(key) }\n }\n\n // Insert / reorder nodes after marker (insertBefore is no-op if already in place)\n let cursor: Node = marker\n for (const node of nextNodes) {\n if (cursor.nextSibling !== node) parent.insertBefore(node, cursor.nextSibling)\n cursor = node\n }\n\n tmpl.__micraList = nextNodes\n}\n\n// \u2500\u2500 Non-keyed (full re-render) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nfunction renderNoKey<S extends StateRecord>(\n tmpl: MicraTemplate,\n items: StateRecord[],\n marker: Comment,\n parent: Node,\n state: StateRecord,\n rawState: StateRecord,\n instance: InternalInstance<S>,\n): void {\n tmpl.__micraList.forEach(n => n.remove())\n tmpl.__micraList = []\n\n const frag = document.createDocumentFragment()\n for (const [index, item] of items.entries()) {\n const clone = tmpl.content.cloneNode(true) as DocumentFragment\n const itemState = Object.assign(\n Object.create(state) as StateRecord,\n { item, index, $index: index },\n )\n applyDirectives(clone, itemState, rawState, instance)\n bindDataOn(clone as unknown as Element, instance)\n bindAtEvents(clone as unknown as Element, instance)\n\n const nodes = Array.from(clone.childNodes) as MicraElement[]\n nodes.forEach(n => { n.__micraEach = true; frag.append(n) })\n tmpl.__micraList.push(...nodes)\n }\n parent.insertBefore(frag, marker.nextSibling)\n}\n", "/**\n * src/dom/refs.ts \u2014 data-ref collection.\n *\n * Responsibilities:\n * - After each render, scan for `[data-ref]` elements (owned by this component)\n * - Populate `instance.refs` so methods can do `this.refs.chart` etc.\n *\n * LLM NOTE: This module is PURE relative to state \u2014 it only reads DOM attributes\n * and writes to instance.refs. It does NOT trigger renders.\n */\n\nimport type { InternalInstance, MicraElement, StateRecord } from '../types'\nimport { queryOwn } from './query'\n\n/**\n * Collect all `[data-ref=\"name\"]` elements owned by this component root into\n * `instance.refs`.\n *\n * Called once after the initial render and again on every re-render (refs may\n * point to newly created elements after an each-list update).\n *\n * @example\n * // HTML: <canvas data-ref=\"chart\">\n * // JS: this.refs.chart \u2192 HTMLCanvasElement\n */\nexport function collectRefs<S extends StateRecord>(\n root: Element,\n instance: InternalInstance<S>,\n): void {\n instance.refs = {}\n for (const el of queryOwn(root, 'data-ref')) {\n const name = (el as MicraElement).dataset['ref']\n if (name) instance.refs[name] = el as HTMLElement\n }\n}\n", "/**\n * src/core/mount.ts \u2014 Mount a component definition onto a DOM element.\n *\n * Responsibilities:\n * - Create and initialize an InternalInstance\n * - Set up reactive state + batch scheduler\n * - Wire render(), destroy(), prop(), fetch(), on(), emit()\n * - Run initial render + call onCreate() in a microtask\n *\n * LLM NOTE: This is the core of the Micra runtime.\n * mount() is called by both the public Micra.mount() API and by start()\n * (which scans the DOM for [data-component] elements).\n */\n\nimport type {\n ComponentDefinition,\n ComponentInstance,\n EventHandler,\n InternalInstance,\n StateRecord,\n UnsubFn,\n} from '../types'\nimport { warn } from '../utils/expr'\nimport { micraFetch } from '../utils/fetch'\nimport { on as busOn, emit as busEmit } from '../core/bus'\nimport { createReactiveState, createScheduler } from '../core/reactive'\nimport { applyDirectives, validateDirectives } from '../dom/directives'\nimport { renderList } from '../dom/each'\nimport { bindDataOn, bindAtEvents, bindModels } from '../dom/events'\nimport { collectRefs } from '../dom/refs'\nimport { _instances } from '../core/registry'\n\n/**\n * Mount a component definition onto a DOM element.\n * Returns the component instance, or null if the root element is not found.\n *\n * Already-mounted elements return the existing instance.\n *\n * @example\n * const instance = Micra.mount('#counter', {\n * state: { count: 0 },\n * inc() { this.state.count++ },\n * })\n */\nexport function mount<S extends StateRecord>(\n selector: string | HTMLElement,\n definition: ComponentDefinition<S>,\n): ComponentInstance<S> | null {\n const root =\n typeof selector === 'string'\n ? document.querySelector<HTMLElement>(selector)\n : selector\n\n if (!root) {\n warn(`\"${selector}\" not found`)\n return null\n }\n\n // Already mounted \u2014 return existing instance without re-mounting\n if (_instances.has(root)) return _instances.get(root) as ComponentInstance<S>\n\n const rawState: StateRecord = { ...(definition.state ?? {}) }\n const instance = { $el: root, refs: {} } as InternalInstance<S>\n\n // Copy user-defined methods from definition to instance\n for (const [key, val] of Object.entries(definition as Record<string, unknown>)) {\n if (key === 'state' || key === 'onCreate' || key === 'onDestroy') continue\n if (typeof val === 'function') instance[key] = val\n }\n\n // \u2500\u2500 prop() \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n // Read data-* attributes from the root element with auto-cast.\n instance.prop = function <T>(name: string, defaultVal?: T): T | undefined {\n const val = root.dataset[name]\n if (val === undefined) return defaultVal\n if (val === 'true') return true as unknown as T\n if (val === 'false') return false as unknown as T\n if (val !== '' && !isNaN(Number(val))) return Number(val) as unknown as T\n return val as unknown as T\n } as ComponentInstance<S>['prop']\n\n // \u2500\u2500 fetch(), emit(), on() \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n instance.fetch = micraFetch\n instance.emit = busEmit\n\n instance.on = <T = unknown>(event: string, handler: EventHandler<T>): UnsubFn => {\n const unsub = busOn(event, handler)\n if (!instance.__micraSubs) instance.__micraSubs = []\n instance.__micraSubs.push(unsub)\n return unsub\n }\n\n // \u2500\u2500 Render \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n let isRendering = false\n const schedule = createScheduler(() => instance.render())\n instance.state = createReactiveState(rawState, schedule) as S\n\n // Expression state: proxy that falls back to instance methods so expressions\n // like `data-text=\"formatDate(item.date)\"` can call component methods.\n const exprState = new Proxy(rawState, {\n get(target, key: string) {\n if (key in target) return target[key]\n if (key in instance) return instance[key]\n return undefined\n },\n })\n\n instance.render = function () {\n if (isRendering) return\n isRendering = true\n try {\n applyDirectives(root, exprState, rawState, instance)\n renderList(root, exprState, rawState, instance)\n bindDataOn(root, instance)\n bindAtEvents(root, instance)\n bindModels(root, instance)\n collectRefs(root, instance)\n } finally {\n isRendering = false\n }\n }\n\n // \u2500\u2500 Destroy \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n instance.destroy = function () {\n instance.__micraSubs?.forEach(unsub => unsub())\n if (typeof (definition as Record<string, unknown>).onDestroy === 'function')\n (definition.onDestroy as () => void).call(instance)\n _instances.delete(root)\n }\n\n // \u2500\u2500 Bootstrap \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n _instances.set(root, instance as InternalInstance)\n instance.render()\n\n // Validate directive usage and emit dev warnings\n validateDirectives(root)\n\n if (typeof (definition as Record<string, unknown>).onCreate === 'function')\n Promise.resolve().then(() =>\n (definition.onCreate as () => void | Promise<void>).call(instance),\n )\n\n return instance\n}\n", "/**\n * src/core/start.ts \u2014 Auto-mount via [data-component].\n *\n * Responsibilities:\n * - Scan the DOM (or a subtree) for [data-component] elements\n * - Mount each using the registered definition\n * - Skip already-mounted elements (safe to call multiple times)\n * - Warn clearly when a component name is not registered\n *\n * LLM NOTE: start() is SSR-friendly \u2014 calling it multiple times is safe\n * because mount() checks _instances before re-mounting.\n */\n\nimport { warn } from '../utils/expr'\nimport { _registry, _instances } from './registry'\nimport { mount } from './mount'\n\n/**\n * Scan for `[data-component]` elements and auto-mount registered definitions.\n *\n * Pass a subtree root to limit the scan (e.g., after a partial SSR update):\n * `Micra.start(document.getElementById('panel'))`\n *\n * @example\n * // Mount everything on the page (called once after DOM ready)\n * Micra.start()\n *\n * // Re-mount after injecting new HTML\n * Micra.start(document.querySelector('#dynamic-section'))\n */\nexport function start(root: Document | HTMLElement = document): void {\n root.querySelectorAll<HTMLElement>('[data-component]').forEach(el => {\n if (_instances.has(el)) return // already mounted \u2014 skip\n const name = el.getAttribute('data-component')!\n const def = _registry.get(name)\n if (!def) {\n warn(`component \"${name}\" not defined. Call Micra.define('${name}', {...}) first.`)\n return\n }\n mount(el, def)\n })\n}\n"],
|
|
5
|
+
"mappings": ";;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACmBA,WAAS,UAAyB;AAnBlC;AAoBE,YAAO,oBAAS,cAAc,yBAAyB,MAAhD,mBAAmD,aAAa,eAAhE,YAA8E;AAAA,EACvF;AAcO,MAAM,aAAN,cAAyB,MAAM;AAAA,IACpC,YACE,SACgB,QACA,UAChB;AACA,YAAM,OAAO;AAHG;AACA;AAGhB,WAAK,OAAO;AAAA,IACd;AAAA,EACF;AAmBA,iBAAsB,WAAW,KAAa,UAAwB,CAAC,GAAqB;AA/D5F;AAgEE,UAAM,WAAY,aAAQ,WAAR,YAAyC,OAAO,YAAY;AAC9E,UAAM,UAAkC;AAAA,MACtC,QAAQ;AAAA,MACR,GAAI,QAAQ;AAAA,IACd;AAEA,UAAM,OAAO,QAAQ;AACrB,QAAI,KAAM,SAAQ,cAAc,IAAI;AAEpC,QAAI,WAAW;AACf,QAAI;AAEJ,QAAI,WAAW,SAAS,WAAW,QAAQ;AACzC,YAAM,SAAiC,CAAC;AACxC,iBAAW,CAAC,GAAG,CAAC,KAAK,OAAO,QAAQ,OAAO,GAAG;AAC5C,YAAI,MAAM,YAAY,MAAM,aAAa,KAAK,KAAM,QAAO,CAAC,IAAI,OAAO,CAAC;AAAA,MAC1E;AACA,UAAI,OAAO,KAAK,MAAM,EAAE;AACtB,qBAAa,IAAI,SAAS,GAAG,IAAI,MAAM,OAAO,IAAI,gBAAgB,MAAM;AAAA,IAC5E,OAAO;AACL,cAAQ,cAAc,IAAI;AAC1B,aAAO,KAAK,UAAU,QAAQ,SAAS,SAAY,QAAQ,OAAO,OAAO;AAAA,IAC3E;AAEA,UAAM,MAAM,MAAM,MAAM,UAAU;AAAA,MAChC;AAAA,MACA;AAAA,MACA,GAAI,SAAS,SAAY,EAAE,KAAK,IAAI,CAAC;AAAA,IACvC,CAAC;AAED,QAAI,CAAC,IAAI;AACP,YAAM,IAAI,WAAW,kBAAkB,MAAM,IAAI,GAAG,WAAM,IAAI,MAAM,IAAI,IAAI,QAAQ,GAAG;AAEzF,UAAM,MAAK,SAAI,QAAQ,IAAI,cAAc,MAA9B,YAAmC;AAC9C,WAAO,GAAG,SAAS,kBAAkB,IAAI,IAAI,KAAK,IAAI,IAAI,KAAK;AAAA,EACjE;;;AChFO,MAAM,YAAa,oBAAI,IAAiC;AAGxD,MAAM,aAAa,oBAAI,IAAmC;AAU1D,WAAS,OACd,MACA,YACM;AACN,cAAU,IAAI,MAAM,UAAiC;AAAA,EACvD;AAeO,WAAS,gBACd,YACwB;AACxB,WAAO;AAAA,EACT;AAMO,WAAS,YAAyD;AACvE,WAAO;AAAA,EACT;AAMO,WAAS,WAAqD;AACnE,WAAO;AAAA,EACT;AAaO,WAAS,QAAc;AArF9B;AAsFE,QAAI,WAAW,SAAS,GAAG;AACzB,cAAQ,IAAI,6BAA6B;AACzC;AAAA,IACF;AACA,YAAQ,MAAM,WAAW,WAAW,IAAI,oBAAoB;AAC5D,eAAW,CAAC,IAAI,QAAQ,KAAK,YAAY;AACvC,YAAM,QAAO,QAAG,aAAa,gBAAgB,MAAhC,YAAqC;AAClD,cAAQ,MAAM,KAAK,IAAI,IAAI,gCAAgC;AAC3D,cAAQ,IAAI,SAAS,EAAE;AACvB,cAAQ,IAAI,SAAS,EAAE,GAAG,SAAS,MAAM,CAAC;AAC1C,cAAQ,SAAS;AAAA,IACnB;AACA,YAAQ,SAAS;AAAA,EACnB;;;AC/EA,MAAM,YAAY,oBAAI,IAA6C;AAInE,MAAM,cAAc;AAcb,WAAS,SAAS,MAAc,OAA6B;AAElE,QAAI,YAAY,KAAK,IAAI,GAAG;AAC1B,aAAO,KAAK,MAAM,GAAG,EAAE;AAAA,QAAgB,CAAC,KAAK,QAC3C,OAAO,OAAQ,IAAoB,GAAG,IAAI;AAAA,QAC1C;AAAA,MACF;AAAA,IACF;AAEA,QAAI,CAAC,UAAU,IAAI,IAAI,GAAG;AACxB,UAAI;AACF,kBAAU;AAAA,UACR;AAAA,UACA,IAAI,SAAS,MAAM,oBAAoB,IAAI,IAAI;AAAA,QACjD;AAAA,MACF,QAAQ;AACN,aAAK,uBAAuB,IAAI,GAAG;AACnC,kBAAU,IAAI,MAAM,MAAM,MAAS;AAAA,MACrC;AAAA,IACF;AAEA,QAAI;AACF,aAAO,UAAU,IAAI,IAAI,EAAG,KAAK;AAAA,IACnC,QAAQ;AACN,aAAO;AAAA,IACT;AAAA,EACF;AAKO,WAAS,KAAK,KAAmB;AACtC,YAAQ,KAAK,WAAW,GAAG,EAAE;AAAA,EAC/B;;;ACvDA,MAAM,OAAO,oBAAI,IAA+B;AASzC,WAAS,GAAgB,OAAe,SAAmC;AAChF,QAAI,CAAC,KAAK,IAAI,KAAK,EAAG,MAAK,IAAI,OAAO,oBAAI,IAAI,CAAC;AAC/C,SAAK,IAAI,KAAK,EAAG,IAAI,OAAuB;AAC5C,WAAO,MAAM,IAAI,OAAO,OAAuB;AAAA,EACjD;AAKO,WAAS,IAAI,OAAe,SAA6B;AAlChE;AAmCE,eAAK,IAAI,KAAK,MAAd,mBAAiB,OAAO;AAAA,EAC1B;AAQO,WAAS,KAAK,OAAe,SAAyB;AA5C7D;AA6CE,eAAK,IAAI,KAAK,MAAd,mBAAiB,QAAQ,OAAK;AAC5B,UAAI;AAAE,UAAE,OAAO;AAAA,MAAE,SAAS,GAAG;AAAE,gBAAQ,MAAM,sBAAsB,KAAK,MAAM,CAAC;AAAA,MAAE;AAAA,IACnF;AAAA,EACF;;;AC1BO,WAAS,oBAA2C,KAAQ,UAAyB;AAC1F,WAAO,IAAI,MAAM,KAAK;AAAA,MACpB,IAAI,QAAQ,KAAa,OAAgB;AAEvC;AAAC,QAAC,OAAuB,GAAG,IAAI;AAChC,iBAAS;AACT,eAAO;AAAA,MACT;AAAA,IACF,CAAC;AAAA,EACH;AAWO,WAAS,gBAAgB,QAAgC;AAC9D,QAAI,UAAU;AACd,WAAO,SAAS,WAAW;AACzB,UAAI,QAAS;AACb,gBAAU;AACV,cAAQ,QAAQ,EAAE,KAAK,MAAM;AAAE,kBAAU;AAAO,eAAO;AAAA,MAAE,CAAC;AAAA,IAC5D;AAAA,EACF;;;ACtCO,WAAS,SAAS,MAAkB,KAAwB;AACjE,WAAO,MAAM,KAAK,KAAK,iBAAiB,GAAG,CAAC;AAAA,EAC9C;AAaO,WAAS,SAAS,MAAe,MAAyB;AAC/D,WAAO,SAAS,MAAM,IAAI,IAAI,GAAG,EAAE,OAAO,QAAM;AAC9C,UAAI,OAAuB,GAAG;AAC9B,aAAO,QAAQ,SAAS,MAAM;AAC5B,YAAI,KAAK,aAAa,gBAAgB,EAAG,QAAO;AAChD,eAAO,KAAK;AAAA,MACd;AACA,aAAO;AAAA,IACT,CAAC;AAAA,EACH;;;ACPA,WAAS,UAAU,IAAa,MAAc,OAA0B;AA5BxE;AA6BE,UAAM,OAAO,QAAO,cAAS,MAAM,KAAK,MAApB,YAAyB,EAAE;AAC/C,QAAI,GAAG,gBAAgB,KAAM,IAAG,cAAc;AAAA,EAChD;AAEA,WAAS,UAAU,IAAa,MAAc,OAA0B;AAjCxE;AAkCE,OAAG,YAAY,QAAO,cAAS,MAAM,KAAK,MAApB,YAAyB,EAAE;AAAA,EACnD;AAEA,WAAS,QAAQ,IAAa,MAAc,OAA0B;AACpE,IAAC,GAAmB,MAAM,UAAU,SAAS,MAAM,KAAK,IAAI,KAAK;AAAA,EACnE;AAEA,WAAS,UAAU,IAAa,MAAc,OAA0B;AACtE,eAAW,QAAQ,KAAK,MAAM,GAAG,GAAG;AAClC,YAAM,WAAW,KAAK,QAAQ,GAAG;AACjC,UAAI,aAAa,GAAI;AACrB,YAAM,OAAU,KAAK,MAAM,GAAG,QAAQ,EAAE,KAAK;AAC7C,YAAM,UAAU,KAAK,MAAM,WAAW,CAAC,EAAE,KAAK;AAC9C,YAAM,MAAU,SAAS,SAAS,KAAK;AAEvC,UAAI,SAAS,SAAS;AACpB,QAAC,GAAmB,YAAY,OAAO,oBAAO,EAAE;AAAA,MAClD,WAAW,SAAS,SAAS;AAC3B,YAAI,SAAS,kBAAkB;AAC7B,UAAC,GAAwB,QAAQ,OAAO,oBAAO,EAAE;AAAA,MACrD,WAAW,SAAS,SAAS;AAC3B,YAAI,OAAO,QAAQ,YAAY,QAAQ,MAAM;AAC3C,iBAAO,OAAQ,GAAmB,OAAO,GAAG;AAAA,QAC9C,OAAO;AACL,aAAG,aAAa,SAAS,OAAO,oBAAO,EAAE,CAAC;AAAA,QAC5C;AAAA,MACF,WAAW,OAAO,QAAQ,WAAW;AACnC,cAAM,GAAG,aAAa,MAAM,EAAE,IAAI,GAAG,gBAAgB,IAAI;AAAA,MAC3D,OAAO;AACL,eAAO,OAAO,GAAG,gBAAgB,IAAI,IAAI,GAAG,aAAa,MAAM,OAAO,GAAG,CAAC;AAAA,MAC5E;AAAA,IACF;AAAA,EACF;AAYA,WAAS,WAAW,IAAa,MAAc,OAA0B;AACvE,eAAW,QAAQ,KAAK,MAAM,GAAG,GAAG;AAClC,YAAM,WAAW,KAAK,QAAQ,GAAG;AACjC,UAAI,aAAa,GAAI;AACrB,YAAM,MAAU,KAAK,MAAM,GAAG,QAAQ,EAAE,KAAK;AAC7C,YAAM,UAAU,KAAK,MAAM,WAAW,CAAC,EAAE,KAAK;AAC9C,UAAI,CAAC,IAAK;AACV,SAAG,UAAU,OAAO,KAAK,QAAQ,SAAS,SAAS,KAAK,CAAC,CAAC;AAAA,IAC5D;AAAA,EACF;AAEA,WAAS,WACP,IACA,KACA,UACM;AACN,UAAM,OAAO;AACb,QAAI,SAAS,kBAAkB,IAAI;AACjC,WAAK,QAAQ,SAAS,GAAG,KAAK,OAAO,KAAK,OAAO,SAAS,GAAG,CAAC;AAAA,IAChE;AAAA,EAEF;AAKA,WAAS,WAAW,MAA+B;AACjD,UAAM,OAAO,CAAC,SAAkC;AAzGlD;AA0GI,YAAM,MAAM,SAAS,MAAM,IAAI;AAE/B,WAAK,UAAqB,iBAArB,8BAAoC,MAAO,KAAI,QAAQ,IAAI;AAChE,aAAO,IACJ,OAAO,QAAM,CAAC,GAAG,QAAQ,UAAU,CAAC,EACpC,IAAI,SAAO,EAAE,IAAI,MAAM,GAAG,aAAa,IAAI,EAAG,EAAE;AAAA,IACrD;AACA,WAAO;AAAA,MACL,MAAO,KAAK,WAAW;AAAA,MACvB,MAAO,KAAK,WAAW;AAAA,MACvB,IAAO,KAAK,SAAS;AAAA,MACrB,MAAO,KAAK,WAAW;AAAA,MACvB,MAAO,KAAK,WAAW;AAAA,MACvB,OAAO,KAAK,YAAY;AAAA,MACxB,OAAO,KAAK,YAAY;AAAA,IAC1B;AAAA,EACF;AAkBO,WAAS,gBACd,MACA,OACA,UACA,WACM;AAEN,QAAI,KAAK,aAAa,KAAK,wBAAwB;AACjD,oBAAc,kBAAkB,IAAwB,GAAG,OAAO,QAAQ;AAC1E;AAAA,IACF;AAEA,UAAM,KAAK;AACX,QAAI,CAAC,GAAG,aAAc,IAAG,eAAe,WAAW,EAAE;AACrD,kBAAc,GAAG,cAAc,OAAO,QAAQ;AAAA,EAChD;AAGA,WAAS,cACP,OACA,OACA,UACM;AACN,UAAM,KAAK,QAAQ,OAAK,UAAU,EAAE,IAAI,EAAE,MAAM,KAAK,CAAC;AACtD,UAAM,KAAK,QAAQ,OAAK,UAAU,EAAE,IAAI,EAAE,MAAM,KAAK,CAAC;AACtD,UAAM,GAAG,QAAQ,OAAK,QAAQ,EAAE,IAAI,EAAE,MAAM,KAAK,CAAC;AAClD,UAAM,KAAK,QAAQ,OAAK,QAAQ,EAAE,IAAI,EAAE,MAAM,KAAK,CAAC;AACpD,UAAM,KAAK,QAAQ,OAAK,UAAU,EAAE,IAAI,EAAE,MAAM,KAAK,CAAC;AACtD,UAAM,MAAM,QAAQ,OAAK,WAAW,EAAE,IAAI,EAAE,KAAK,KAAK,GAAG,QAAQ,CAAC;AAClE,UAAM,MAAM,QAAQ,OAAK,WAAW,EAAE,IAAI,EAAE,MAAM,KAAK,CAAC;AAAA,EAC1D;AAGA,WAAS,kBAAkB,MAAwC;AACjE,UAAM,OAAO,CAAC,SACZ,SAAS,MAAM,IAAI,IAAI,GAAG,EACvB,OAAO,QAAM,CAAC,GAAG,QAAQ,UAAU,CAAC,EACpC,IAAI,SAAO,EAAE,IAAI,MAAM,GAAG,aAAa,IAAI,EAAG,EAAE;AACrD,WAAO;AAAA,MACL,MAAO,KAAK,WAAW;AAAA,MACvB,MAAO,KAAK,WAAW;AAAA,MACvB,IAAO,KAAK,SAAS;AAAA,MACrB,MAAO,KAAK,WAAW;AAAA,MACvB,MAAO,KAAK,WAAW;AAAA,MACvB,OAAO,KAAK,YAAY;AAAA,MACxB,OAAO,KAAK,YAAY;AAAA,IAC1B;AAAA,EACF;AAUO,WAAS,mBAAmB,MAAqB;AACtD,aAAS,MAAM,WAAW,EAAE,QAAQ,QAAM;AACxC,UAAI,CAAC,GAAG,aAAa,UAAU,GAAG;AAChC,aAAK,cAAc,GAAG,aAAa,WAAW,CAAC,yFAAoF;AAAA,MACrI;AAAA,IACF,CAAC;AAAA,EACH;;;AChLO,WAAS,WACd,MACA,UACM;AA9BR;AA+BE,UAAM,aAAa,KAAK,aAAa;AACrC,UAAM,MAAM,aACR,SAAS,MAA+B,WAAW,IACnD,SAAS,MAAM,SAAS;AAG5B,QAAI,CAAC,gBAAe,UAAqB,iBAArB,8BAAoC,eAAc,CAAC,IAAI,SAAS,IAAI;AACtF,UAAI,QAAQ,IAAI;AAElB,eAAW,MAAM,KAAK;AACpB,YAAM,MAAM;AACZ,UAAI,IAAI,cAAe;AACvB,UAAI,gBAAgB;AAEpB,YAAM,QAAO,SAAI,QAAQ,IAAI,MAAhB,YAAqB;AAClC,iBAAW,QAAQ,KAAK,MAAM,GAAG,GAAG;AAClC,cAAM,CAAC,QAAQ,MAAM,IAAI,KAAK,KAAK,EAAE,MAAM,GAAG;AAC9C,YAAI,CAAC,UAAU,CAAC,OAAQ;AAExB,cAAM,CAAC,QAAQ,GAAG,IAAI,IAAI,OAAO,MAAM,GAAG;AAE1C,WAAG,iBAAiB,QAAS,CAAC,MAAa;AACzC,cAAI,KAAK,SAAS,SAAS,EAAG,GAAE,eAAe;AAC/C,cAAI,KAAK,SAAS,MAAM,EAAG,GAAE,gBAAgB;AAC7C,cAAI,KAAK,SAAS,MAAM,KAAK,EAAE,WAAW,GAAI;AAE9C,gBAAM,KAAK,SAAS,OAAO,KAAK,CAAC;AACjC,cAAI,OAAO,OAAO,WAAY,CAAC,GAA0B,KAAK,UAAU,CAAC;AAAA,cACpE,MAAK,WAAW,OAAO,KAAK,CAAC,aAAa;AAAA,QACjD,CAAC;AAAA,MACH;AAAA,IACF;AAAA,EACF;AAaO,WAAS,aACd,MACA,UACM;AACN,UAAM,QAAQ;AACd,QAAI,MAAM,iBAAkB;AAC5B,UAAM,mBAAmB;AAEzB,UAAM,MAAM,SAAS,MAAM,GAAG;AAC9B,eAAW,MAAM,KAAK;AACpB,iBAAW,QAAQ,MAAM,KAAK,GAAG,UAAU,GAAG;AAC5C,YAAI,CAAC,KAAK,KAAK,WAAW,GAAG,EAAG;AAChC,cAAM,CAAC,QAAQ,GAAG,IAAI,IAAI,KAAK,KAAK,MAAM,CAAC,EAAE,MAAM,GAAG;AACtD,cAAM,SAAS,KAAK,MAAM,KAAK;AAE/B,WAAG,iBAAiB,QAAS,CAAC,MAAa;AACzC,cAAI,KAAK,SAAS,SAAS,EAAG,GAAE,eAAe;AAC/C,cAAI,KAAK,SAAS,MAAM,EAAG,GAAE,gBAAgB;AAC7C,cAAI,KAAK,SAAS,MAAM,KAAK,EAAE,WAAW,GAAI;AAE9C,gBAAM,KAAK,SAAS,MAAM;AAC1B,cAAI,OAAO,OAAO,WAAY,CAAC,GAA0B,KAAK,UAAU,CAAC;AAAA,cACpE,MAAK,WAAW,MAAM,aAAa;AAAA,QAC1C,CAAC;AAAA,MACH;AAAA,IACF;AAAA,EACF;AAYO,WAAS,WACd,MACA,UACM;AArHR;AAsHE,UAAM,aAAa,KAAK,aAAa;AACrC,UAAM,MAAM,aACR,SAAS,MAA+B,cAAc,IACtD,SAAS,MAAM,YAAY;AAE/B,eAAW,MAAM,KAAK;AACpB,YAAM,MAAM;AACZ,UAAI,IAAI,aAAc;AACtB,UAAI,eAAe;AAEnB,YAAM,OAAO,QAAwB,QAAQ,OAAO,MAAvC,YAA4C;AACzD,YAAM,MAAM,GAAG;AAEf,YAAM,SAAS,MAAM;AACnB,cAAM,MAAM,QAAQ,WAAY,GAAwB,SAAS,aAC5D,GAAwB,UACxB,GAAwB;AAC5B,QAAC,SAAS,MAAsB,GAAG,IAAI;AAAA,MAC1C;AAEA,SAAG;AAAA,QAAiB,QAAQ,YAAa,GAAwB,SAAS,UACtE,WACA;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAAA,EACF;;;AClHO,WAAS,WACd,MACA,OACA,UACA,UACM;AACN,aAAS,MAAM,WAAW,EAAE,QAAQ,YAAU;AApChD;AAqCI,UAAI,OAAO,YAAY,WAAY;AACnC,YAAM,OAAO;AAEb,YAAM,YAAY,KAAK,aAAa,WAAW;AAC/C,YAAM,WAAY,UAAK,aAAa,UAAU,MAA5B,YAAiC;AACnD,YAAM,QAAY,SAAS,WAAW,KAAK;AAG3C,UAAI,CAAC,KAAK,eAAe;AACvB,cAAM,IAAI,SAAS,cAAc,QAAQ,SAAS,EAAE;AACpD,aAAK,MAAM,CAAC;AACZ,aAAK,gBAAgB;AACrB,aAAK,eAAgB,oBAAI,IAAI;AAC7B,aAAK,cAAgB,CAAC;AAAA,MACxB;AAEA,YAAM,SAAS,KAAK;AACpB,YAAM,SAAS,KAAK;AACpB,YAAM,SAAS,OAAO;AAGtB,UAAI,CAAC,MAAM,QAAQ,KAAK,GAAG;AACzB,aAAK,YAAY,QAAQ,OAAK,EAAE,OAAO,CAAC;AACxC,aAAK,cAAc,CAAC;AACpB,eAAO,MAAM;AACb;AAAA,MACF;AAEA,UAAI,SAAS;AACX,oBAAY,MAAM,OAAwB,SAAS,QAAQ,QAAQ,QAAQ,OAAO,UAAU,QAAQ;AAAA,MACtG,OAAO;AACL,oBAAY,MAAM,OAAwB,QAAQ,QAAQ,OAAO,UAAU,QAAQ;AAAA,MACrF;AAAA,IACF,CAAC;AAAA,EACH;AAIA,WAAS,YACP,MACA,OACA,SACA,QACA,QACA,QACA,OACA,UACA,UACM;AACN,UAAM,WAAY,oBAAI,IAAa;AACnC,UAAM,YAA4B,CAAC;AAEnC,eAAW,CAAC,OAAO,IAAI,KAAK,MAAM,QAAQ,GAAG;AAC3C,YAAM,MAAM,KAAK,OAAO;AACxB,UAAI,OAAO,KAAM,MAAK,aAAa,OAAO,wCAAwC,KAAK,EAAE;AACzF,eAAS,IAAI,GAAG;AAEhB,UAAI,OAAO,OAAO,IAAI,GAAG;AAEzB,UAAI,CAAC,MAAM;AAET,cAAM,OAAO,KAAK,QAAQ,UAAU,IAAI;AACxC,YAAI,KAAK,WAAW,WAAW,GAAG;AAChC,iBAAO,KAAK;AAAA,QACd,OAAO;AACL,iBAAO,SAAS,cAAc,iBAAiB;AAC/C,eAAK,MAAM,UAAU;AACrB,eAAK,OAAO,IAAI;AAAA,QAClB;AACA,aAAK,aAAa;AAClB,eAAO,IAAI,KAAK,IAAI;AAEpB,mBAAW,MAAM,QAAQ;AACzB,qBAAa,MAAM,QAAQ;AAAA,MAC7B;AAEA,YAAM,YAAY,OAAO;AAAA,QACvB,OAAO,OAAO,KAAK;AAAA,QACnB,EAAE,MAAM,OAAO,QAAQ,MAAM;AAAA,MAC/B;AACA,sBAAgB,MAAM,WAAW,UAAU,QAAQ;AACnD,gBAAU,KAAK,IAAI;AAAA,IACrB;AAGA,eAAW,CAAC,KAAK,IAAI,KAAK,QAAQ;AAChC,UAAI,CAAC,SAAS,IAAI,GAAG,GAAG;AAAE,aAAK,OAAO;AAAG,eAAO,OAAO,GAAG;AAAA,MAAE;AAAA,IAC9D;AAGA,QAAI,SAAe;AACnB,eAAW,QAAQ,WAAW;AAC5B,UAAI,OAAO,gBAAgB,KAAM,QAAO,aAAa,MAAM,OAAO,WAAW;AAC7E,eAAS;AAAA,IACX;AAEA,SAAK,cAAc;AAAA,EACrB;AAIA,WAAS,YACP,MACA,OACA,QACA,QACA,OACA,UACA,UACM;AACN,SAAK,YAAY,QAAQ,OAAK,EAAE,OAAO,CAAC;AACxC,SAAK,cAAc,CAAC;AAEpB,UAAM,OAAO,SAAS,uBAAuB;AAC7C,eAAW,CAAC,OAAO,IAAI,KAAK,MAAM,QAAQ,GAAG;AAC3C,YAAM,QAAQ,KAAK,QAAQ,UAAU,IAAI;AACzC,YAAM,YAAY,OAAO;AAAA,QACvB,OAAO,OAAO,KAAK;AAAA,QACnB,EAAE,MAAM,OAAO,QAAQ,MAAM;AAAA,MAC/B;AACA,sBAAgB,OAAO,WAAW,UAAU,QAAQ;AACpD,iBAAW,OAA6B,QAAQ;AAChD,mBAAa,OAA6B,QAAQ;AAElD,YAAM,QAAQ,MAAM,KAAK,MAAM,UAAU;AACzC,YAAM,QAAQ,OAAK;AAAE,UAAE,cAAc;AAAM,aAAK,OAAO,CAAC;AAAA,MAAE,CAAC;AAC3D,WAAK,YAAY,KAAK,GAAG,KAAK;AAAA,IAChC;AACA,WAAO,aAAa,MAAM,OAAO,WAAW;AAAA,EAC9C;;;AC7IO,WAAS,YACd,MACA,UACM;AACN,aAAS,OAAO,CAAC;AACjB,eAAW,MAAM,SAAS,MAAM,UAAU,GAAG;AAC3C,YAAM,OAAQ,GAAoB,QAAQ,KAAK;AAC/C,UAAI,KAAM,UAAS,KAAK,IAAI,IAAI;AAAA,IAClC;AAAA,EACF;;;ACUO,WAAS,MACd,UACA,YAC6B;AA/C/B;AAgDE,UAAM,OACJ,OAAO,aAAa,WAChB,SAAS,cAA2B,QAAQ,IAC5C;AAEN,QAAI,CAAC,MAAM;AACT,WAAK,IAAI,QAAQ,aAAa;AAC9B,aAAO;AAAA,IACT;AAGA,QAAI,WAAW,IAAI,IAAI,EAAG,QAAO,WAAW,IAAI,IAAI;AAEpD,UAAM,WAAwB,EAAE,IAAI,gBAAW,UAAX,YAAoB,CAAC,EAAG;AAC5D,UAAM,WAAW,EAAE,KAAK,MAAM,MAAM,CAAC,EAAE;AAGvC,eAAW,CAAC,KAAK,GAAG,KAAK,OAAO,QAAQ,UAAqC,GAAG;AAC9E,UAAI,QAAQ,WAAW,QAAQ,cAAc,QAAQ,YAAa;AAClE,UAAI,OAAO,QAAQ,WAAY,UAAS,GAAG,IAAI;AAAA,IACjD;AAIA,aAAS,OAAO,SAAa,MAAc,YAA+B;AACxE,YAAM,MAAM,KAAK,QAAQ,IAAI;AAC7B,UAAI,QAAQ,OAAW,QAAO;AAC9B,UAAI,QAAQ,OAAS,QAAO;AAC5B,UAAI,QAAQ,QAAS,QAAO;AAC5B,UAAI,QAAQ,MAAM,CAAC,MAAM,OAAO,GAAG,CAAC,EAAG,QAAO,OAAO,GAAG;AACxD,aAAO;AAAA,IACT;AAGA,aAAS,QAAQ;AACjB,aAAS,OAAQ;AAEjB,aAAS,KAAK,CAAc,OAAe,YAAsC;AAC/E,YAAM,QAAQ,GAAM,OAAO,OAAO;AAClC,UAAI,CAAC,SAAS,YAAa,UAAS,cAAc,CAAC;AACnD,eAAS,YAAY,KAAK,KAAK;AAC/B,aAAO;AAAA,IACT;AAGA,QAAI,cAAc;AAClB,UAAM,WAAY,gBAAgB,MAAM,SAAS,OAAO,CAAC;AACzD,aAAS,QAAS,oBAAoB,UAAU,QAAQ;AAIxD,UAAM,YAAY,IAAI,MAAM,UAAU;AAAA,MACpC,IAAI,QAAQ,KAAa;AACvB,YAAI,OAAO,OAAU,QAAO,OAAO,GAAG;AACtC,YAAI,OAAO,SAAU,QAAO,SAAS,GAAG;AACxC,eAAO;AAAA,MACT;AAAA,IACF,CAAC;AAED,aAAS,SAAS,WAAY;AAC5B,UAAI,YAAa;AACjB,oBAAc;AACd,UAAI;AACF,wBAAgB,MAAM,WAAW,UAAU,QAAQ;AACnD,mBAAW,MAAM,WAAW,UAAU,QAAQ;AAC9C,mBAAW,MAAM,QAAQ;AACzB,qBAAa,MAAM,QAAQ;AAC3B,mBAAW,MAAM,QAAQ;AACzB,oBAAY,MAAM,QAAQ;AAAA,MAC5B,UAAE;AACA,sBAAc;AAAA,MAChB;AAAA,IACF;AAGA,aAAS,UAAU,WAAY;AA3HjC,UAAAA;AA4HI,OAAAA,MAAA,SAAS,gBAAT,gBAAAA,IAAsB,QAAQ,WAAS,MAAM;AAC7C,UAAI,OAAQ,WAAuC,cAAc;AAC/D,QAAC,WAAW,UAAyB,KAAK,QAAQ;AACpD,iBAAW,OAAO,IAAI;AAAA,IACxB;AAGA,eAAW,IAAI,MAAM,QAA4B;AACjD,aAAS,OAAO;AAGhB,uBAAmB,IAAI;AAEvB,QAAI,OAAQ,WAAuC,aAAa;AAC9D,cAAQ,QAAQ,EAAE;AAAA,QAAK,MACpB,WAAW,SAAwC,KAAK,QAAQ;AAAA,MACnE;AAEF,WAAO;AAAA,EACT;;;ACjHO,WAAS,MAAM,OAA+B,UAAgB;AACnE,SAAK,iBAA8B,kBAAkB,EAAE,QAAQ,QAAM;AACnE,UAAI,WAAW,IAAI,EAAE,EAAG;AACxB,YAAM,OAAO,GAAG,aAAa,gBAAgB;AAC7C,YAAM,MAAO,UAAU,IAAI,IAAI;AAC/B,UAAI,CAAC,KAAK;AACR,aAAK,cAAc,IAAI,qCAAqC,IAAI,kBAAkB;AAClF;AAAA,MACF;AACA,YAAM,IAAI,GAAG;AAAA,IACf,CAAC;AAAA,EACH;",
|
|
6
|
+
"names": ["_a"]
|
|
7
|
+
}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
/* Micra.js v1.0.0 — https://github.com/micra-js/micra — MIT */
|
|
2
|
+
"use strict";var Micra=(()=>{var L=Object.defineProperty;var X=Object.getOwnPropertyDescriptor;var Q=Object.getOwnPropertyNames;var V=Object.prototype.hasOwnProperty;var Y=(e,t)=>{for(var r in t)L(e,r,{get:t[r],enumerable:!0})},ee=(e,t,r,n)=>{if(t&&typeof t=="object"||typeof t=="function")for(let o of Q(t))!V.call(e,o)&&o!==r&&L(e,o,{get:()=>t[o],enumerable:!(n=X(t,o))||n.enumerable});return e};var te=e=>ee(L({},"__esModule",{value:!0}),e);var ue={};Y(ue,{FetchError:()=>x,debug:()=>P,define:()=>$,defineComponent:()=>N,emit:()=>C,instances:()=>F,mount:()=>A,off:()=>D,on:()=>T,registry:()=>O,start:()=>J});function ne(){var e,t;return(t=(e=document.querySelector('meta[name="csrf-token"]'))==null?void 0:e.getAttribute("content"))!=null?t:null}var x=class extends Error{constructor(r,n,o){super(r);this.status=n;this.response=o;this.name="FetchError"}};async function H(e,t={}){var c,s;let r=((c=t.method)!=null?c:"GET").toUpperCase(),n={Accept:"application/json",...t.headers},o=ne();o&&(n["X-CSRF-Token"]=o);let a=e,d;if(r==="GET"||r==="HEAD"){let l={};for(let[f,u]of Object.entries(t))f!=="method"&&f!=="headers"&&u!=null&&(l[f]=String(u));Object.keys(l).length&&(a+=(e.includes("?")?"&":"?")+new URLSearchParams(l))}else n["Content-Type"]="application/json",d=JSON.stringify(t.body!==void 0?t.body:t);let i=await fetch(a,{method:r,headers:n,...d!==void 0?{body:d}:{}});if(!i.ok)throw new x(`[Micra] fetch: ${r} ${e} \u2192 ${i.status}`,i.status,i);return((s=i.headers.get("content-type"))!=null?s:"").includes("application/json")?i.json():i.text()}var w=new Map,g=new Map;function $(e,t){w.set(e,t)}function N(e){return e}function F(){return g}function O(){return w}function P(){var e;if(g.size===0){console.log("[Micra] No live components.");return}console.group(`[Micra] ${g.size} live component(s)`);for(let[t,r]of g){let n=(e=t.getAttribute("data-component"))!=null?e:"(unnamed)";console.group(`%c${n}`,"font-weight:bold;color:#6366f1"),console.log("$el ",t),console.log("state",{...r.state}),console.groupEnd()}console.groupEnd()}var I=new Map,re=/^[a-zA-Z_$][a-zA-Z0-9_$]*(\.[a-zA-Z_$][a-zA-Z0-9_$]*)*$/;function h(e,t){if(re.test(e))return e.split(".").reduce((r,n)=>r!=null?r[n]:void 0,t);if(!I.has(e))try{I.set(e,new Function("$s",`with($s){return (${e})}`))}catch{E(`invalid expression "${e}"`),I.set(e,()=>{})}try{return I.get(e)(t)}catch{return}}function E(e){console.warn(`[Micra] ${e}`)}var R=new Map;function T(e,t){return R.has(e)||R.set(e,new Set),R.get(e).add(t),()=>D(e,t)}function D(e,t){var r;(r=R.get(e))==null||r.delete(t)}function C(e,t){var r;(r=R.get(e))==null||r.forEach(n=>{try{n(t)}catch(o){console.error(`[Micra] bus error [${e}]:`,o)}})}function q(e,t){return new Proxy(e,{set(r,n,o){return r[n]=o,t(),!0}})}function j(e){let t=!1;return function(){t||(t=!0,Promise.resolve().then(()=>{t=!1,e()}))}}function v(e,t){return Array.from(e.querySelectorAll(t))}function y(e,t){return v(e,`[${t}]`).filter(r=>{let n=r.parentElement;for(;n&&n!==e;){if(n.hasAttribute("data-component"))return!1;n=n.parentElement}return!0})}function oe(e,t,r){var o;let n=String((o=h(t,r))!=null?o:"");e.textContent!==n&&(e.textContent=n)}function ae(e,t,r){var n;e.innerHTML=String((n=h(t,r))!=null?n:"")}function U(e,t,r){e.style.display=h(t,r)?"":"none"}function ie(e,t,r){for(let n of t.split(",")){let o=n.indexOf(":");if(o===-1)continue;let a=n.slice(0,o).trim(),d=n.slice(o+1).trim(),i=h(d,r);a==="class"?e.className=String(i!=null?i:""):a==="value"?document.activeElement!==e&&(e.value=String(i!=null?i:"")):a==="style"?typeof i=="object"&&i!==null?Object.assign(e.style,i):e.setAttribute("style",String(i!=null?i:"")):typeof i=="boolean"?i?e.setAttribute(a,""):e.removeAttribute(a):i==null?e.removeAttribute(a):e.setAttribute(a,String(i))}}function se(e,t,r){for(let n of t.split(",")){let o=n.indexOf(":");if(o===-1)continue;let a=n.slice(0,o).trim(),d=n.slice(o+1).trim();a&&e.classList.toggle(a,!!h(d,r))}}function ce(e,t,r){let n=e;document.activeElement!==e&&(n.value=r[t]==null?"":String(r[t]))}function de(e){let t=r=>{var o;let n=y(e,r);return(o=e.hasAttribute)!=null&&o.call(e,r)&&n.unshift(e),n.filter(a=>!a.closest("template")).map(a=>({el:a,expr:a.getAttribute(r)}))};return{text:t("data-text"),html:t("data-html"),if:t("data-if"),show:t("data-show"),bind:t("data-bind"),model:t("data-model"),class:t("data-class")}}function _(e,t,r,n){if(e.nodeType===Node.DOCUMENT_FRAGMENT_NODE){B(le(e),t,r);return}let o=e;o.__micraCache||(o.__micraCache=de(o)),B(o.__micraCache,t,r)}function B(e,t,r){e.text.forEach(n=>oe(n.el,n.expr,t)),e.html.forEach(n=>ae(n.el,n.expr,t)),e.if.forEach(n=>U(n.el,n.expr,t)),e.show.forEach(n=>U(n.el,n.expr,t)),e.bind.forEach(n=>ie(n.el,n.expr,t)),e.model.forEach(n=>ce(n.el,n.expr.trim(),r)),e.class.forEach(n=>se(n.el,n.expr,t))}function le(e){let t=r=>v(e,`[${r}]`).filter(n=>!n.closest("template")).map(n=>({el:n,expr:n.getAttribute(r)}));return{text:t("data-text"),html:t("data-html"),if:t("data-if"),show:t("data-show"),bind:t("data-bind"),model:t("data-model"),class:t("data-class")}}function z(e){y(e,"data-each").forEach(t=>{t.hasAttribute("data-key")||E(`data-each="${t.getAttribute("data-each")}" has no data-key \u2014 keyed diff disabled. Add data-key="id" for better performance.`)})}function b(e,t){var o,a;let r=e.nodeType===11,n=r?v(e,"[data-on]"):y(e,"data-on");!r&&((o=e.hasAttribute)!=null&&o.call(e,"data-on"))&&!n.includes(e)&&n.unshift(e);for(let d of n){let i=d;if(i.__micraEvents)continue;i.__micraEvents=!0;let m=(a=i.dataset.on)!=null?a:"";for(let c of m.split(",")){let[s,l]=c.trim().split(":");if(!s||!l)continue;let[f,...u]=s.split(".");d.addEventListener(f,S=>{if(u.includes("prevent")&&S.preventDefault(),u.includes("stop")&&S.stopPropagation(),u.includes("self")&&S.target!==d)return;let p=t[l.trim()];typeof p=="function"?p.call(t,S):E(`method "${l.trim()}" not found`)})}}}function M(e,t){let r=e;if(r.__micraAtScanned)return;r.__micraAtScanned=!0;let n=v(e,"*");for(let o of n)for(let a of Array.from(o.attributes)){if(!a.name.startsWith("@"))continue;let[d,...i]=a.name.slice(1).split("."),m=a.value.trim();o.addEventListener(d,c=>{if(i.includes("prevent")&&c.preventDefault(),i.includes("stop")&&c.stopPropagation(),i.includes("self")&&c.target!==o)return;let s=t[m];typeof s=="function"?s.call(t,c):E(`method "${m}" not found`)})}}function K(e,t){var o;let n=e.nodeType===11?v(e,"[data-model]"):y(e,"data-model");for(let a of n){let d=a;if(d.__micraModel)continue;d.__micraModel=!0;let i=(o=a.dataset.model)!=null?o:"",m=a.tagName,c=()=>{let s=m==="INPUT"&&a.type==="checkbox"?a.checked:a.value;t.state[i]=s};a.addEventListener(m==="SELECT"||a.type==="radio"?"change":"input",c)}}function Z(e,t,r,n){y(e,"data-each").forEach(o=>{var f;if(o.tagName!=="TEMPLATE")return;let a=o,d=a.getAttribute("data-each"),i=(f=a.getAttribute("data-key"))!=null?f:null,m=h(d,t);if(!a.__micraMarker){let u=document.createComment(`each:${d}`);a.after(u),a.__micraMarker=u,a.__micraNodes=new Map,a.__micraList=[]}let c=a.__micraMarker,s=a.__micraNodes,l=c.parentNode;if(!Array.isArray(m)){a.__micraList.forEach(u=>u.remove()),a.__micraList=[],s.clear();return}i?me(a,m,i,c,s,l,t,r,n):fe(a,m,c,l,t,r,n)})}function me(e,t,r,n,o,a,d,i,m){let c=new Set,s=[];for(let[f,u]of t.entries()){let S=u[r];S==null&&E(`data-key="${r}" is null/undefined on item at index ${f}`),c.add(S);let p=o.get(S);if(!p){let k=e.content.cloneNode(!0);k.childNodes.length===1?p=k.firstElementChild:(p=document.createElement("micra-each-item"),p.style.display="contents",p.append(k)),p.__micraKey=S,o.set(S,p),b(p,m),M(p,m)}let W=Object.assign(Object.create(d),{item:u,index:f,$index:f});_(p,W,i,m),s.push(p)}for(let[f,u]of o)c.has(f)||(u.remove(),o.delete(f));let l=n;for(let f of s)l.nextSibling!==f&&a.insertBefore(f,l.nextSibling),l=f;e.__micraList=s}function fe(e,t,r,n,o,a,d){e.__micraList.forEach(m=>m.remove()),e.__micraList=[];let i=document.createDocumentFragment();for(let[m,c]of t.entries()){let s=e.content.cloneNode(!0),l=Object.assign(Object.create(o),{item:c,index:m,$index:m});_(s,l,a,d),b(s,d),M(s,d);let f=Array.from(s.childNodes);f.forEach(u=>{u.__micraEach=!0,i.append(u)}),e.__micraList.push(...f)}n.insertBefore(i,r.nextSibling)}function G(e,t){t.refs={};for(let r of y(e,"data-ref")){let n=r.dataset.ref;n&&(t.refs[n]=r)}}function A(e,t){var m;let r=typeof e=="string"?document.querySelector(e):e;if(!r)return E(`"${e}" not found`),null;if(g.has(r))return g.get(r);let n={...(m=t.state)!=null?m:{}},o={$el:r,refs:{}};for(let[c,s]of Object.entries(t))c==="state"||c==="onCreate"||c==="onDestroy"||typeof s=="function"&&(o[c]=s);o.prop=function(c,s){let l=r.dataset[c];return l===void 0?s:l==="true"?!0:l==="false"?!1:l!==""&&!isNaN(Number(l))?Number(l):l},o.fetch=H,o.emit=C,o.on=(c,s)=>{let l=T(c,s);return o.__micraSubs||(o.__micraSubs=[]),o.__micraSubs.push(l),l};let a=!1,d=j(()=>o.render());o.state=q(n,d);let i=new Proxy(n,{get(c,s){if(s in c)return c[s];if(s in o)return o[s]}});return o.render=function(){if(!a){a=!0;try{_(r,i,n,o),Z(r,i,n,o),b(r,o),M(r,o),K(r,o),G(r,o)}finally{a=!1}}},o.destroy=function(){var c;(c=o.__micraSubs)==null||c.forEach(s=>s()),typeof t.onDestroy=="function"&&t.onDestroy.call(o),g.delete(r)},g.set(r,o),o.render(),z(r),typeof t.onCreate=="function"&&Promise.resolve().then(()=>t.onCreate.call(o)),o}function J(e=document){e.querySelectorAll("[data-component]").forEach(t=>{if(g.has(t))return;let r=t.getAttribute("data-component"),n=w.get(r);if(!n){E(`component "${r}" not defined. Call Micra.define('${r}', {...}) first.`);return}A(t,n)})}return te(ue);})();
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/types.ts — All Micra.js type definitions.
|
|
3
|
+
*
|
|
4
|
+
* Public types are re-exported from src/index.ts.
|
|
5
|
+
* Internal types (MicraElement, MicraTemplate, InternalInstance) are used by
|
|
6
|
+
* implementation modules but are NOT part of the public API.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Constraint for component state objects.
|
|
10
|
+
* Use any plain object: `{ count: 0, items: [] as User[] }`.
|
|
11
|
+
*/
|
|
12
|
+
export type StateRecord = Record<string, unknown>;
|
|
13
|
+
/** Returns an unsubscribe function. */
|
|
14
|
+
export type UnsubFn = () => void;
|
|
15
|
+
/** Event bus handler. Generic `T` types the payload. */
|
|
16
|
+
export type EventHandler<T = unknown> = (payload: T) => void;
|
|
17
|
+
/** Options for `this.fetch()`. For GET/HEAD extra keys become query params. */
|
|
18
|
+
export interface FetchOptions {
|
|
19
|
+
method?: string;
|
|
20
|
+
headers?: Record<string, string>;
|
|
21
|
+
/** POST/PUT/PATCH body — serialized as JSON. */
|
|
22
|
+
body?: unknown;
|
|
23
|
+
[key: string]: unknown;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* The `this` context inside component methods and lifecycle hooks.
|
|
27
|
+
* `S` is inferred from the component's `state` object.
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* // state: { count: 0 } → S = { count: number }
|
|
31
|
+
* increment() { this.state.count++ } // count is number ✓
|
|
32
|
+
*/
|
|
33
|
+
export interface ComponentInstance<S extends StateRecord = StateRecord> {
|
|
34
|
+
/** The root DOM element this component is mounted on. */
|
|
35
|
+
readonly $el: HTMLElement;
|
|
36
|
+
/** Reactive state — any assignment triggers a batched re-render. */
|
|
37
|
+
state: S;
|
|
38
|
+
/**
|
|
39
|
+
* DOM refs: collect elements with `data-ref="name"` → `this.refs.name`.
|
|
40
|
+
* @example <canvas data-ref="chart"> → this.refs.chart
|
|
41
|
+
*/
|
|
42
|
+
refs: Record<string, HTMLElement>;
|
|
43
|
+
/** Force a synchronous re-render. Normally not needed — state mutations batch automatically. */
|
|
44
|
+
render(): void;
|
|
45
|
+
/** Unmount: clean up event bus subscriptions and call onDestroy. */
|
|
46
|
+
destroy(): void;
|
|
47
|
+
/**
|
|
48
|
+
* Read a `data-*` attribute from the root element with auto-cast.
|
|
49
|
+
* Casts "true"/"false" → boolean, numeric strings → number.
|
|
50
|
+
* @example this.prop('perPage', 10) // data-per-page="20" → 20
|
|
51
|
+
*/
|
|
52
|
+
prop(name: string): string | undefined;
|
|
53
|
+
prop<T>(name: string, defaultVal: T): T;
|
|
54
|
+
/** Fetch helper: CSRF header, JSON body, query params, typed errors. */
|
|
55
|
+
fetch(url: string, options?: FetchOptions): Promise<unknown>;
|
|
56
|
+
/** Publish an event on the global bus. */
|
|
57
|
+
emit(event: string, payload?: unknown): void;
|
|
58
|
+
/** Subscribe to the global bus. Subscription is auto-removed on destroy(). */
|
|
59
|
+
on<T = unknown>(event: string, handler: EventHandler<T>): UnsubFn;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Component definition passed to `Micra.define` or `Micra.mount`.
|
|
63
|
+
*
|
|
64
|
+
* `S` is inferred from the `state` property — all methods receive
|
|
65
|
+
* `this: ComponentInstance<S>` automatically via `ThisType<>`.
|
|
66
|
+
*
|
|
67
|
+
* @example
|
|
68
|
+
* Micra.define('counter', {
|
|
69
|
+
* state: { count: 0 },
|
|
70
|
+
* inc() { this.state.count++ }, // this.state.count: number ✓
|
|
71
|
+
* })
|
|
72
|
+
*/
|
|
73
|
+
export type ComponentDefinition<S extends StateRecord = StateRecord> = {
|
|
74
|
+
/** Initial flat state. Becomes reactive on mount. */
|
|
75
|
+
state?: S;
|
|
76
|
+
/**
|
|
77
|
+
* Called once after mount in a microtask — safe for async data fetching.
|
|
78
|
+
* @example async onCreate() { this.state.data = await this.fetch('/api/data') }
|
|
79
|
+
*/
|
|
80
|
+
onCreate?: () => void | Promise<void>;
|
|
81
|
+
/**
|
|
82
|
+
* Called on destroy — clean up DOM listeners, timers, etc.
|
|
83
|
+
* Event bus subscriptions added via `this.on()` are cleaned up automatically.
|
|
84
|
+
*/
|
|
85
|
+
onDestroy?: () => void;
|
|
86
|
+
[method: string]: unknown;
|
|
87
|
+
} & ThisType<ComponentInstance<S>>;
|
|
88
|
+
/**
|
|
89
|
+
* @internal Extended HTMLElement with Micra bookkeeping slots.
|
|
90
|
+
*/
|
|
91
|
+
export interface MicraElement extends HTMLElement {
|
|
92
|
+
__micraModel?: true;
|
|
93
|
+
__micraEvents?: true;
|
|
94
|
+
__micraAtScanned?: true;
|
|
95
|
+
__micraKey?: unknown;
|
|
96
|
+
__micraEach?: true;
|
|
97
|
+
__micraCache?: DirectiveCache;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* @internal Extended HTMLTemplateElement with keyed-diff state.
|
|
101
|
+
*/
|
|
102
|
+
export interface MicraTemplate extends HTMLTemplateElement {
|
|
103
|
+
__micraMarker?: Comment;
|
|
104
|
+
__micraNodes: Map<unknown, MicraElement>;
|
|
105
|
+
__micraList: ChildNode[];
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* @internal Per-element directive binding (element + expression string).
|
|
109
|
+
*/
|
|
110
|
+
export interface CachedBinding {
|
|
111
|
+
el: Element;
|
|
112
|
+
expr: string;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* @internal Directive scan result — built once per Element, reused every render.
|
|
116
|
+
* This is the core of the performance optimization.
|
|
117
|
+
*
|
|
118
|
+
* LLM NOTE: DirectiveCache is built lazily on first render and stored on the
|
|
119
|
+
* element. It avoids repeated querySelectorAll calls on every re-render.
|
|
120
|
+
*/
|
|
121
|
+
export interface DirectiveCache {
|
|
122
|
+
text: CachedBinding[];
|
|
123
|
+
html: CachedBinding[];
|
|
124
|
+
if: CachedBinding[];
|
|
125
|
+
show: CachedBinding[];
|
|
126
|
+
bind: CachedBinding[];
|
|
127
|
+
model: CachedBinding[];
|
|
128
|
+
class: CachedBinding[];
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* @internal Full instance as seen inside the runtime — extends the public
|
|
132
|
+
* interface with private bookkeeping slots and an index signature for
|
|
133
|
+
* dynamic method dispatch.
|
|
134
|
+
*/
|
|
135
|
+
export interface InternalInstance<S extends StateRecord = StateRecord> extends ComponentInstance<S> {
|
|
136
|
+
__micraSubs?: UnsubFn[];
|
|
137
|
+
[key: string]: unknown;
|
|
138
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/utils/expr.ts — JS expression evaluator.
|
|
3
|
+
*
|
|
4
|
+
* Responsibilities:
|
|
5
|
+
* - Compile expression strings into cached functions
|
|
6
|
+
* - Evaluate them against a state object
|
|
7
|
+
* - Fast-path for simple property lookups
|
|
8
|
+
*
|
|
9
|
+
* LLM NOTE: This module is PURE. It does not touch the DOM or mutate state.
|
|
10
|
+
* All side effects are isolated to console.warn on invalid expressions.
|
|
11
|
+
*/
|
|
12
|
+
import type { StateRecord } from '../types';
|
|
13
|
+
/**
|
|
14
|
+
* Evaluate a JS expression string against a state object.
|
|
15
|
+
*
|
|
16
|
+
* Results are cached by expression string — repeated evaluations hit the cache.
|
|
17
|
+
* Uses a fast-path for simple dot-paths (e.g. "count", "user.name") that avoids
|
|
18
|
+
* Function() overhead.
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* evalExpr('count > 0', { count: 5 }) // → true
|
|
22
|
+
* evalExpr('user.name', { user: { name: 'Alice' } }) // → 'Alice'
|
|
23
|
+
* evalExpr('price * qty', { price: 9.99, qty: 3 }) // → 29.97
|
|
24
|
+
*/
|
|
25
|
+
export declare function evalExpr(expr: string, state: StateRecord): unknown;
|
|
26
|
+
/** @internal Consistent warning prefix. */
|
|
27
|
+
export declare function warn(msg: string): void;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/utils/fetch.ts — HTTP fetch helper.
|
|
3
|
+
*
|
|
4
|
+
* Responsibilities:
|
|
5
|
+
* - Auto-attach CSRF token from <meta name="csrf-token">
|
|
6
|
+
* - Serialize POST/PUT/PATCH body as JSON
|
|
7
|
+
* - Serialize GET/HEAD options as query params
|
|
8
|
+
* - Throw a typed FetchError on non-2xx responses
|
|
9
|
+
* - Return parsed JSON or text
|
|
10
|
+
*
|
|
11
|
+
* LLM NOTE: This module is PURE (no DOM side effects beyond reading a meta tag).
|
|
12
|
+
* It wraps the native fetch() API with SaaS-friendly defaults.
|
|
13
|
+
*/
|
|
14
|
+
import type { FetchOptions } from '../types';
|
|
15
|
+
/**
|
|
16
|
+
* Thrown by `this.fetch()` when the server returns a non-2xx status.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* try {
|
|
20
|
+
* await this.fetch('/api/data')
|
|
21
|
+
* } catch (e) {
|
|
22
|
+
* if (e instanceof FetchError && e.status === 404) { ... }
|
|
23
|
+
* }
|
|
24
|
+
*/
|
|
25
|
+
export declare class FetchError extends Error {
|
|
26
|
+
readonly status: number;
|
|
27
|
+
readonly response: Response;
|
|
28
|
+
constructor(message: string, status: number, response: Response);
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Fetch wrapper with SaaS defaults.
|
|
32
|
+
*
|
|
33
|
+
* - GET/HEAD: extra `options` keys become URL query params
|
|
34
|
+
* - POST/PUT/PATCH/DELETE: `options.body` is JSON-serialized
|
|
35
|
+
* - Attaches X-CSRF-Token header automatically
|
|
36
|
+
* - Returns parsed JSON if Content-Type is application/json, else text
|
|
37
|
+
*
|
|
38
|
+
* @example
|
|
39
|
+
* // GET with params → /api/users?page=2&status=active
|
|
40
|
+
* const data = await this.fetch('/api/users', { page: 2, status: 'active' })
|
|
41
|
+
*
|
|
42
|
+
* // POST with JSON body
|
|
43
|
+
* await this.fetch('/api/invite', { method: 'POST', body: { email, role } })
|
|
44
|
+
*/
|
|
45
|
+
export declare function micraFetch(url: string, options?: FetchOptions): Promise<unknown>;
|
package/package.json
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "micra.js",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Lightweight reactive UI framework for server-rendered pages — reactive state, directives, event bus. < 4 KB gzip.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/micra.cjs.js",
|
|
7
|
+
"module": "./dist/micra.esm.js",
|
|
8
|
+
"browser": "./dist/micra.js",
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"import": "./dist/micra.esm.js",
|
|
14
|
+
"require": "./dist/micra.cjs.js"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"dist",
|
|
19
|
+
"src"
|
|
20
|
+
],
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "node build.mjs",
|
|
23
|
+
"typecheck": "tsc --noEmit",
|
|
24
|
+
"dev": "node build.mjs --watch",
|
|
25
|
+
"test": "vitest run",
|
|
26
|
+
"test:watch": "vitest",
|
|
27
|
+
"test:coverage": "vitest run --coverage"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@vitest/coverage-v8": "^4.1.7",
|
|
31
|
+
"esbuild": "^0.28.0",
|
|
32
|
+
"happy-dom": "^20.9.0",
|
|
33
|
+
"typescript": "^5.8.3",
|
|
34
|
+
"vitest": "^4.1.7"
|
|
35
|
+
},
|
|
36
|
+
"keywords": [
|
|
37
|
+
"framework",
|
|
38
|
+
"micra",
|
|
39
|
+
"lightweight",
|
|
40
|
+
"ssr",
|
|
41
|
+
"reactive",
|
|
42
|
+
"directives",
|
|
43
|
+
"alpine-alternative"
|
|
44
|
+
],
|
|
45
|
+
"author": "Denis Fl",
|
|
46
|
+
"license": "MIT",
|
|
47
|
+
"repository": {
|
|
48
|
+
"type": "git",
|
|
49
|
+
"url": "https://github.com/denisfl/micra.js.git"
|
|
50
|
+
},
|
|
51
|
+
"homepage": "https://github.com/denisfl/micra.js#readme",
|
|
52
|
+
"bugs": {
|
|
53
|
+
"url": "https://github.com/denisfl/micra.js/issues"
|
|
54
|
+
},
|
|
55
|
+
"publishConfig": {
|
|
56
|
+
"access": "public",
|
|
57
|
+
"provenance": true
|
|
58
|
+
}
|
|
59
|
+
}
|
package/src/core/bus.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/core/bus.ts — Global event bus.
|
|
3
|
+
*
|
|
4
|
+
* Responsibilities:
|
|
5
|
+
* - Publish events (emit)
|
|
6
|
+
* - Subscribe and unsubscribe (on / off)
|
|
7
|
+
* - Provide unsubscribe tokens for component cleanup
|
|
8
|
+
*
|
|
9
|
+
* LLM NOTE: The bus is a module-level singleton.
|
|
10
|
+
* Component instances subscribe via `instance.on()` which auto-registers
|
|
11
|
+
* the unsub token in `instance.__micraSubs` for cleanup on destroy().
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { EventHandler, UnsubFn } from '../types'
|
|
15
|
+
|
|
16
|
+
// Module-level bus state — one bus per page load.
|
|
17
|
+
const _bus = new Map<string, Set<EventHandler>>()
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Subscribe to a named event. Returns an unsubscribe function.
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* const unsub = on('user:login', (user) => console.log(user))
|
|
24
|
+
* unsub() // stop listening
|
|
25
|
+
*/
|
|
26
|
+
export function on<T = unknown>(event: string, handler: EventHandler<T>): UnsubFn {
|
|
27
|
+
if (!_bus.has(event)) _bus.set(event, new Set())
|
|
28
|
+
_bus.get(event)!.add(handler as EventHandler)
|
|
29
|
+
return () => off(event, handler as EventHandler)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Unsubscribe a specific handler from an event.
|
|
34
|
+
*/
|
|
35
|
+
export function off(event: string, handler: EventHandler): void {
|
|
36
|
+
_bus.get(event)?.delete(handler)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Publish an event to all subscribers. Errors are caught per-handler.
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* emit('user:updated', { id: 1, name: 'Alice' })
|
|
44
|
+
*/
|
|
45
|
+
export function emit(event: string, payload?: unknown): void {
|
|
46
|
+
_bus.get(event)?.forEach(h => {
|
|
47
|
+
try { h(payload) } catch (e) { console.error(`[Micra] bus error [${event}]:`, e) }
|
|
48
|
+
})
|
|
49
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/core/mount.ts — Mount a component definition onto a DOM element.
|
|
3
|
+
*
|
|
4
|
+
* Responsibilities:
|
|
5
|
+
* - Create and initialize an InternalInstance
|
|
6
|
+
* - Set up reactive state + batch scheduler
|
|
7
|
+
* - Wire render(), destroy(), prop(), fetch(), on(), emit()
|
|
8
|
+
* - Run initial render + call onCreate() in a microtask
|
|
9
|
+
*
|
|
10
|
+
* LLM NOTE: This is the core of the Micra runtime.
|
|
11
|
+
* mount() is called by both the public Micra.mount() API and by start()
|
|
12
|
+
* (which scans the DOM for [data-component] elements).
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type {
|
|
16
|
+
ComponentDefinition,
|
|
17
|
+
ComponentInstance,
|
|
18
|
+
EventHandler,
|
|
19
|
+
InternalInstance,
|
|
20
|
+
StateRecord,
|
|
21
|
+
UnsubFn,
|
|
22
|
+
} from '../types'
|
|
23
|
+
import { warn } from '../utils/expr'
|
|
24
|
+
import { micraFetch } from '../utils/fetch'
|
|
25
|
+
import { on as busOn, emit as busEmit } from '../core/bus'
|
|
26
|
+
import { createReactiveState, createScheduler } from '../core/reactive'
|
|
27
|
+
import { applyDirectives, validateDirectives } from '../dom/directives'
|
|
28
|
+
import { renderList } from '../dom/each'
|
|
29
|
+
import { bindDataOn, bindAtEvents, bindModels } from '../dom/events'
|
|
30
|
+
import { collectRefs } from '../dom/refs'
|
|
31
|
+
import { _instances } from '../core/registry'
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Mount a component definition onto a DOM element.
|
|
35
|
+
* Returns the component instance, or null if the root element is not found.
|
|
36
|
+
*
|
|
37
|
+
* Already-mounted elements return the existing instance.
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* const instance = Micra.mount('#counter', {
|
|
41
|
+
* state: { count: 0 },
|
|
42
|
+
* inc() { this.state.count++ },
|
|
43
|
+
* })
|
|
44
|
+
*/
|
|
45
|
+
export function mount<S extends StateRecord>(
|
|
46
|
+
selector: string | HTMLElement,
|
|
47
|
+
definition: ComponentDefinition<S>,
|
|
48
|
+
): ComponentInstance<S> | null {
|
|
49
|
+
const root =
|
|
50
|
+
typeof selector === 'string'
|
|
51
|
+
? document.querySelector<HTMLElement>(selector)
|
|
52
|
+
: selector
|
|
53
|
+
|
|
54
|
+
if (!root) {
|
|
55
|
+
warn(`"${selector}" not found`)
|
|
56
|
+
return null
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Already mounted — return existing instance without re-mounting
|
|
60
|
+
if (_instances.has(root)) return _instances.get(root) as ComponentInstance<S>
|
|
61
|
+
|
|
62
|
+
const rawState: StateRecord = { ...(definition.state ?? {}) }
|
|
63
|
+
const instance = { $el: root, refs: {} } as InternalInstance<S>
|
|
64
|
+
|
|
65
|
+
// Copy user-defined methods from definition to instance
|
|
66
|
+
for (const [key, val] of Object.entries(definition as Record<string, unknown>)) {
|
|
67
|
+
if (key === 'state' || key === 'onCreate' || key === 'onDestroy') continue
|
|
68
|
+
if (typeof val === 'function') instance[key] = val
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ── prop() ────────────────────────────────────────────────────────────────
|
|
72
|
+
// Read data-* attributes from the root element with auto-cast.
|
|
73
|
+
instance.prop = function <T>(name: string, defaultVal?: T): T | undefined {
|
|
74
|
+
const val = root.dataset[name]
|
|
75
|
+
if (val === undefined) return defaultVal
|
|
76
|
+
if (val === 'true') return true as unknown as T
|
|
77
|
+
if (val === 'false') return false as unknown as T
|
|
78
|
+
if (val !== '' && !isNaN(Number(val))) return Number(val) as unknown as T
|
|
79
|
+
return val as unknown as T
|
|
80
|
+
} as ComponentInstance<S>['prop']
|
|
81
|
+
|
|
82
|
+
// ── fetch(), emit(), on() ─────────────────────────────────────────────────
|
|
83
|
+
instance.fetch = micraFetch
|
|
84
|
+
instance.emit = busEmit
|
|
85
|
+
|
|
86
|
+
instance.on = <T = unknown>(event: string, handler: EventHandler<T>): UnsubFn => {
|
|
87
|
+
const unsub = busOn(event, handler)
|
|
88
|
+
if (!instance.__micraSubs) instance.__micraSubs = []
|
|
89
|
+
instance.__micraSubs.push(unsub)
|
|
90
|
+
return unsub
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ── Render ────────────────────────────────────────────────────────────────
|
|
94
|
+
let isRendering = false
|
|
95
|
+
const schedule = createScheduler(() => instance.render())
|
|
96
|
+
instance.state = createReactiveState(rawState, schedule) as S
|
|
97
|
+
|
|
98
|
+
// Expression state: proxy that falls back to instance methods so expressions
|
|
99
|
+
// like `data-text="formatDate(item.date)"` can call component methods.
|
|
100
|
+
const exprState = new Proxy(rawState, {
|
|
101
|
+
get(target, key: string) {
|
|
102
|
+
if (key in target) return target[key]
|
|
103
|
+
if (key in instance) return instance[key]
|
|
104
|
+
return undefined
|
|
105
|
+
},
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
instance.render = function () {
|
|
109
|
+
if (isRendering) return
|
|
110
|
+
isRendering = true
|
|
111
|
+
try {
|
|
112
|
+
applyDirectives(root, exprState, rawState, instance)
|
|
113
|
+
renderList(root, exprState, rawState, instance)
|
|
114
|
+
bindDataOn(root, instance)
|
|
115
|
+
bindAtEvents(root, instance)
|
|
116
|
+
bindModels(root, instance)
|
|
117
|
+
collectRefs(root, instance)
|
|
118
|
+
} finally {
|
|
119
|
+
isRendering = false
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// ── Destroy ───────────────────────────────────────────────────────────────
|
|
124
|
+
instance.destroy = function () {
|
|
125
|
+
instance.__micraSubs?.forEach(unsub => unsub())
|
|
126
|
+
if (typeof (definition as Record<string, unknown>).onDestroy === 'function')
|
|
127
|
+
(definition.onDestroy as () => void).call(instance)
|
|
128
|
+
_instances.delete(root)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// ── Bootstrap ─────────────────────────────────────────────────────────────
|
|
132
|
+
_instances.set(root, instance as InternalInstance)
|
|
133
|
+
instance.render()
|
|
134
|
+
|
|
135
|
+
// Validate directive usage and emit dev warnings
|
|
136
|
+
validateDirectives(root)
|
|
137
|
+
|
|
138
|
+
if (typeof (definition as Record<string, unknown>).onCreate === 'function')
|
|
139
|
+
Promise.resolve().then(() =>
|
|
140
|
+
(definition.onCreate as () => void | Promise<void>).call(instance),
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
return instance
|
|
144
|
+
}
|