{"id":50294363,"url":"https://github.com/chrisrobison/pan","last_synced_at":"2026-05-28T08:00:50.447Z","repository":{"id":318250523,"uuid":"1070497676","full_name":"chrisrobison/pan","owner":"chrisrobison","description":"A DOM‑native message bus for micro‑frontends and Web Components - no build required","archived":false,"fork":false,"pushed_at":"2025-11-09T17:36:44.000Z","size":1411,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-11-09T19:20:18.153Z","etag":null,"topics":["anti-framework","channels","communication","dom","dom-native","loosely-coupled","message-bus","messaging","no-build"],"latest_commit_sha":null,"homepage":"https://chrisrobison.github.io/pan/","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/chrisrobison.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":"docs/SECURITY.md","support":null,"governance":null,"roadmap":"docs/ROADMAP_TO_10.md","authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-10-06T02:45:12.000Z","updated_at":"2025-11-09T17:36:48.000Z","dependencies_parsed_at":"2025-10-06T04:30:21.460Z","dependency_job_id":"d7fa37e9-beb6-47b9-87fe-00f068a8f7bf","html_url":"https://github.com/chrisrobison/pan","commit_stats":null,"previous_names":["chrisrobison/pan"],"tags_count":3,"template":false,"template_full_name":null,"purl":"pkg:github/chrisrobison/pan","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/chrisrobison%2Fpan","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/chrisrobison%2Fpan/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/chrisrobison%2Fpan/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/chrisrobison%2Fpan/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/chrisrobison","download_url":"https://codeload.github.com/chrisrobison/pan/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/chrisrobison%2Fpan/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33599465,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-05-28T02:00:06.440Z","response_time":99,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["anti-framework","channels","communication","dom","dom-native","loosely-coupled","message-bus","messaging","no-build"],"created_at":"2026-05-28T08:00:49.571Z","updated_at":"2026-05-28T08:00:50.436Z","avatar_url":"https://github.com/chrisrobison.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# LARC — Lightweight Asynchronous Relay Core\n\n[![Version](https://img.shields.io/badge/version-1.0.0-blue.svg)](https://github.com/chrisrobison/pan)\n[![License](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)\n[![Status](https://img.shields.io/badge/status-production--ready-brightgreen.svg)](CHANGELOG.md)\n[![Security](https://img.shields.io/badge/security-audited-brightgreen.svg)](docs/COMPONENT_SECURITY_AUDIT.md)\n[![Browser Support](https://img.shields.io/badge/browser-chrome--tested-green.svg)](CHANGELOG.md#browser-support)\n[![Test Coverage](https://img.shields.io/badge/coverage-100%25-success.svg)](tests/)\n[![Performance](https://img.shields.io/badge/performance-exceptional-brightgreen.svg)](docs/PERFORMANCE.md)\n\n\u003e **A very lightweight DOM‑native message bus reference (PAN) and its reference implementation:** topics, request/reply, retained messages, lightweight, no build, with an Inspector!\n\n* **Zero build:** drop a `\u003cpan-bus\u003e` on the page; talk via `CustomEvent`s.\n* **Loose coupling:** components depend on topic contracts (JSON‑Schema), not imports.\n* **Interoperable:** works with vanilla, Web Components, React/Lit/Vue, iframes.\n* **Batteries included:** retained messages, req/rep, optional cross‑tab mirror, DevTools‑style inspector.\n\nLARC is the project and reference implementation for the Page Area Network (PAN) messaging bus. The PAN bus element and topic conventions remain named with the `pan-`/`pan:` prefixes (for example `\u003cpan-bus\u003e` and `pan:publish`) — this repo provides LARC as the lightweight implementation, docs, and examples.\n\nPAN (Page Area Network) is the messaging model and bus that enables a central communications hub for web components or micro-frontends. It works with any framework or no framework at all.\n\n**📋 [v1.0 Roadmap](docs/V1_ROADMAP.md)** | **✅ [v1.0 Checklist](docs/V1_CHECKLIST.md)** | **📖 [Full Documentation](#documentation)** | **🎉 [Changelog](CHANGELOG.md)**\n\n---\n\n## 🎉 v1.0.0 Released!\n\n**PAN v1.0 is production-ready!** The core messaging infrastructure is stable, tested, and performance-validated.\n\n**What's Production-Ready:**\n- ✅ **Core Infrastructure** (pan-bus, pan-client, pan-autoload) - Fully stable with locked APIs\n- ✅ **UI Components** - Security audit completed, 0 critical vulnerabilities ([audit report](docs/COMPONENT_SECURITY_AUDIT.md))\n- ✅ **100% Test Coverage** - 120+ comprehensive Playwright suites exercising every component\n- ✅ **Exceptional Performance** - 300k+ msg/sec, zero memory leaks ([benchmarks](docs/PERFORMANCE.md))\n- ✅ **Complete Documentation** - API reference, guides, and examples\n\n**Important Notes for v1.0:**\n- ✅ **Components security audited** - All high-risk components approved for production ([audit report](docs/COMPONENT_SECURITY_AUDIT.md))\n- ⚠️ **Browser support: Chrome-only** - Multi-browser testing planned for v1.1\n- ✅ **Production ready** - Both core and components can be used with confidence\n\nSee [CHANGELOG.md](CHANGELOG.md) for full details and version history.\n\n---\n\n## Architecture \u0026 Goals\n\nWe are building a suite of Web Components that communicate over PAN to form composable, framework-agnostic UIs.\n\n- Roles:\n  - Providers/Connectors: talk to backends (REST/GraphQL/IndexedDB), publish retained state, answer requests.\n  - Views: tables, forms, inspectors that subscribe to state and publish user intents (select, save, delete, filter).\n  - Adapters/Controllers: optional mappers that translate between domain topics and transports.\n- Message types:\n  - Commands: `*.get`, `*.save`, `*.delete` (request/reply; not retained).\n  - Events: `*.changed`, `*.error` (notifications; not retained).\n  - State: `*.state` (retained snapshot for late joiners).\n- Topic contracts (CRUD v1):\n  - List: `${resource}.list.get` → replies `{ items }` and publishes `${resource}.list.state` (retain:true).\n  - Select: `${resource}.item.select` with `{ id }` (view event, no reply).\n  - Get: `${resource}.item.get` with `{ id }` → replies `{ ok, item? }`.\n  - Save: `${resource}.item.save` with `{ item }` → replies `{ ok, item }`; provider updates list state.\n  - Delete: `${resource}.item.delete` with `{ id }` → replies `{ ok, id }`; provider updates list state.\n\nSecurity tips:\n\n- Mirror only non-sensitive topics across tabs; avoid secrets in mirrored traffic.\n- Keep payloads JSON-serializable; prefer headers for schema/version metadata.\n- Markdown rendering now enforces safe link and image protocols via `_sanitizeUrl`, blocking `javascript:` and `data:` payloads by default.\n\n## Quickstart (10‑second demo)\n\n**With autoload (recommended):**\n\nDrop one script tag, then use any component. That's it!\n\n```html\n\u003c!doctype html\u003e\u003cmeta charset=\"utf-8\"\u003e\n\u003c!-- Local development --\u003e\n\u003cscript type=\"module\" src=\"./src/pan.js\"\u003e\u003c/script\u003e\n\n\u003c!-- Or use CDN --\u003e\n\u003cscript type=\"module\" src=\"https://chrisrobison.github.io/pan/src/pan.js\"\u003e\u003c/script\u003e\n\n\u003c!-- Just declare the components you want - they load automatically --\u003e\n\u003cx-counter\u003e\u003c/x-counter\u003e\n\u003cpan-inspector\u003e\u003c/pan-inspector\u003e\n```\n\nAll components live in `./src/components/` and load on demand as they approach the viewport. **The `\u003cpan-bus\u003e` is automatically created for you.** No imports, no `customElements.define()`, no bundler.\n\n**📦 CDN Links:**\n- **Autoloader:** `https://chrisrobison.github.io/pan/src/pan.js`\n- **Components:** `https://chrisrobison.github.io/pan/src/components/\u003ccomponent-name\u003e.mjs`\n- **Examples:** `https://chrisrobison.github.io/pan/examples/`\n- **Documentation:** `https://chrisrobison.github.io/pan/site/`\n\n---\n\n**Manual setup (for learning):**\n\nCopy this into an `.html` file and open it to see the minimal PAN implementation:\n\n```html\n\u003c!doctype html\u003e\u003cmeta charset=\"utf-8\"\u003e\n\u003cpan-bus\u003e\u003c/pan-bus\u003e\n\u003cx-counter\u003e\u003c/x-counter\u003e\n\u003cscript type=\"module\"\u003e\n  // Minimal PanClient helper\n  class PanClient{constructor(h=document){this.h=h}\n    pub(m){this.h.dispatchEvent(new CustomEvent('pan:publish',{detail:m,bubbles:true,composed:true}))}\n    sub(t,fn){const on=e=\u003ee.detail?.topic\u0026\u0026PanClient.matches(e.detail.topic,t)\u0026\u0026fn(e.detail);\n      this.h.addEventListener('pan:deliver',on);\n      this.h.dispatchEvent(new CustomEvent('pan:subscribe',{detail:{topics:[t]},bubbles:true,composed:true}));\n      return ()=\u003e{this.h.removeEventListener('pan:deliver',on);\n        this.h.dispatchEvent(new CustomEvent('pan:unsubscribe',{detail:{topics:[t]},bubbles:true,composed:true}));};}\n    static matches(topic, pattern){ if(pattern==='*'||topic===pattern) return true;\n      if(pattern.includes('*')){const esc=s=\u003es.replace(/[|\\\\{}()\\[\\]^$+?.]/g,'\\\\$\u0026').replace(/\\*/g,'[^.]+' );return new RegExp(`^${esc(pattern)}$`).test(topic);} return false; }\n  }\n  // Tiny bus (reference)\n  customElements.define('pan-bus', class extends HTMLElement{ subs=[]; connectedCallback(){\n    document.addEventListener('pan:publish', e=\u003ethis.#pub(e), true);\n    document.addEventListener('pan:subscribe', e=\u003ethis.#sub(e), true);\n    document.addEventListener('pan:unsubscribe', e=\u003ethis.#unsub(e), true);\n    document.dispatchEvent(new CustomEvent('pan:sys.ready',{bubbles:true,composed:true}));\n  }\n  #sub(e){const {topics=[]}=e.detail||{}; const el=e.composedPath?.()[0]; topics.forEach(p=\u003ethis.subs.push({p,el}));}\n  #unsub(e){const {topics=[]}=e.detail||{}; const el=e.composedPath?.()[0]; this.subs=this.subs.filter(s=\u003es.el!==el||!topics.includes(s.p));}\n  #pub(e){const m=e.detail; this.subs.forEach(s=\u003e{ if(PanClient.matches(m.topic,s.p)) s.el.dispatchEvent(new CustomEvent('pan:deliver',{detail:m})); });}\n  });\n  // Demo component\n  customElements.define('x-counter', class extends HTMLElement{ pc=new PanClient(this); n=0; connectedCallback(){\n    this.innerHTML=`\u003cbutton\u003eClicked 0\u003c/button\u003e`;\n    this.querySelector('button').onclick=()=\u003ethis.pc.pub({topic:'demo:click',data:{n:++this.n},retain:true});\n    this.pc.sub('demo:click', m=\u003e this.querySelector('button').textContent = `Clicked ${m.data.n}`);\n  }});\n\u003c/script\u003e\n```\n\n---\n\n## Install\n\n**Recommended: Use autoload**\n\nClone the repo and drop a single script tag on your page:\n\n```html\n\u003cscript type=\"module\" src=\"./src/components/pan-autoload.mjs\"\u003e\u003c/script\u003e\n```\n\nThen just use components in your HTML - they load automatically on demand:\n\n```html\n\u003cpan-bus\u003e\u003c/pan-bus\u003e\n\u003ctodo-list\u003e\u003c/todo-list\u003e\n\u003cpan-inspector\u003e\u003c/pan-inspector\u003e\n```\n\nNo bundler required. Works from `file://`.\n\n---\n\n**Alternative: Manual imports**\n\nFor fine-grained control or CDN usage (when published):\n\n```html\n\u003cscript type=\"module\" src=\"./src/components/pan-bus.mjs\"\u003e\u003c/script\u003e\n\u003cscript type=\"module\" src=\"./src/components/pan-client.mjs\"\u003e\u003c/script\u003e\n\u003cscript type=\"module\" src=\"./pan/app/devtools/pan-inspector.mjs\"\u003e\u003c/script\u003e\n```\n\n---\n\n## Autoloading Custom Elements\n\n**This is the recommended way to use LARC.**\n\nDrop a single script tag on the page to progressively load Web Components from the\n`components/` folder. Tags with a dash (`\u003cmy-widget\u003e`) are auto-detected; when they\napproach the viewport, the loader imports `./src/components/\u003ctag\u003e.mjs` and registers them.\n\n**The `\u003cpan-bus\u003e` is automatically created** so you can start using components immediately:\n\n```html\n\u003cscript type=\"module\" src=\"./src/components/pan-autoload.mjs\"\u003e\u003c/script\u003e\n\n\u003c!-- All of these load automatically - no imports needed --\u003e\n\u003cmy-widget\u003e\u003c/my-widget\u003e\n\u003ctodo-list\u003e\u003c/todo-list\u003e\n\u003cpan-inspector\u003e\u003c/pan-inspector\u003e\n```\n\n**Configuration (optional):**\n\nOverride the components path or file extension:\n\n```html\n\u003cscript\u003e\n  window.panAutoload = {\n    componentsPath: './my-components/',\n    extension: '.js',\n    rootMargin: 600  // px from viewport to trigger load\n  };\n\u003c/script\u003e\n\u003cscript type=\"module\" src=\"./src/components/pan-autoload.mjs\"\u003e\u003c/script\u003e\n```\n\n**Per-element override:**\n\nPoint a specific element to a different module:\n\n```html\n\u003cmy-card data-module=./src/components/cards/my-card.mjs\"\u003e\u003c/my-card\u003e\n```\n\nComponents that don't self-register are defined automatically when they export a\ndefault class matching the tag name.\n\n---\n\n## Project Structure\n\nThe project has a clean, approachable top-level structure:\n\n```\npan/\n├── pan/           # 🎯 Component library (the heart of the project)\n│   ├── core/      # Core infrastructure (pan-bus, pan-client, pan-autoload)\n│   ├── ui/        # Simple, reusable UI building blocks\n│   ├── components/# Complex, feature-rich widgets\n│   ├── data/      # State management and data layer\n│   └── app/       # Domain-specific application components\n├── site/          # 🌐 Website (homepage, gallery, demos)\n├── apps/          # 📱 Demo applications\n├── examples/      # 📚 Example usage pages\n├── docs/          # 📖 Documentation\n│   ├── rfcs/      # Design proposals\n│   └── templates/ # Component starter templates\n├── assets/        # 🎨 Shared resources (theme.css, badges)\n├── scripts/       # 🔧 Build and utility scripts\n└── tests/         # ✅ Test suite\n```\n\n### Component Layers\n\n**Core** (`src/components/`) - Required infrastructure\n- `pan-bus.mjs` - Message bus\n- `pan-client.mjs` - Client library\n- `pan-autoload.mjs` - Component autoloader\n\n**UI** (`src/components/`) - Simple building blocks\n- Cards, modals, dropdowns, tabs, etc.\n- Lightweight, single-purpose components\n- Highly reusable across projects\n\n**Components** (`src/components/`) - Feature-rich widgets\n- Markdown editor/renderer\n- Data tables, charts, date pickers\n- File system manager\n- Theme system\n- Complex, production-ready tools\n\n**Data** (`pan/data/`) - State management\n- Store components for application state\n- Persistence logic (localStorage, IndexedDB)\n- Coordinates state via PAN bus\n\n**App** (`pan/app/`) - Domain-specific components\n- Built for specific applications\n- Tightly coupled to business logic\n- Not intended for general reuse\n\n**Apps** (`apps/`) - Complete demo applications\n- Invoice creator\n- Markdown notes editor\n- Data browser\n- Contact manager\n\nSee individual README files in each directory for detailed documentation.\n\n---\n\n## Core Concepts\n\n* **Topics**: strings like `todos.change`, `nav.goto`, `user.update@2`.\n* **Messages**: `{ topic, data, id?, ts?, replyTo?, correlationId?, retain? }`.\n* **Transport**: bubbling, composed `CustomEvent`s so they cross shadow DOM.\n* **Retained**: last message per topic is replayed to new subscribers.\n* **Req/Rep**: set `replyTo`+`correlationId`; reply on `replyTo`.\n\n---\n\n## API (PanClient)\n\n```ts\nclass PanClient {\n  constructor(host?: HTMLElement|Document, busSelector = 'pan-bus')\n  ready(): Promise\u003cvoid\u003e\n  publish\u003cT\u003e(msg: PanMessage\u003cT\u003e): void\n  subscribe(topics: string|string[], handler: (m: PanMessage)=\u003evoid, opts?: { retained?: boolean, signal?: AbortSignal }): () =\u003e void\n  request\u003cTReq,TRes\u003e(topic: string, data: TReq, opts?: { timeoutMs?: number }): Promise\u003cPanMessage\u003cTRes\u003e\u003e\n}\n```\n\n`PanMessage\u003cT\u003e` fields: `topic`, `data`, optional `id`, `ts`, `replyTo`, `correlationId`, `retain`, `headers`.\n\n---\n\n## Recipes\n\n### 1) Publish/Subscribe\n\n```js\npc.publish({ topic:'search.query', data:{ q:'punk' } });\npc.subscribe('search.results', m =\u003e render(m.data));\n```\n\n### 2) Request/Reply\n\n```js\nconst { data } = await pc.request('data.get', { key:'users' });\n// provider replies on a temporary reply topic created by PanClient\n```\n\n### 3) Retained State Snapshot\n\n```js\npc.publish({ topic:'settings.theme', data:'dark', retain:true });\npc.subscribe('settings.theme', m =\u003e applyTheme(m.data), { retained:true });\n```\n\n### 4) Cross‑tab sync (BroadcastChannel mirror)\n\nBus option to mirror topics across tabs using `BroadcastChannel('pan')`. Example in `examples/03-broadcastchannel.html`.\n\n---\n\n## Inspector\n\nDrop the Inspector to watch traffic, filter by topic, replay messages.\n\n```html\n\u003cpan-inspector style=\"height:400px\"\u003e\u003c/pan-inspector\u003e\n```\n\nFeatures: topic/text filters, pause, clear, export/import JSON, replay, view JSON.\n\n---\n\n## Interop\n\n* **React:** wrapper hook `usePan()`; components call `publish/subscribe`. See `examples/04-react-wrapper.html` (no build, CDN React).\n* **Lit:** mixin using `PanClient` inside a Lit element. See `examples/05-lit-wrapper.html`.\n* **Iframes:** use a gateway that validates `origin` and whitelists topics.\n\n---\n\n## Examples\n\n(Each example is a single self‑contained HTML file you can open directly.)\n\n### `examples/01-hello.html`\n\nMinimal counter (publish/subscribe).\n\n```html\n\u003c!DOCTYPE html\u003e\n\u003cmeta charset=\"utf-8\" /\u003e\n\u003ctitle\u003eLARC – 01 Hello\u003c/title\u003e\n\u003cpan-bus\u003e\u003c/pan-bus\u003e\n\u003cx-counter\u003e\u003c/x-counter\u003e\n\u003cscript type=\"module\"\u003e\n  class PanClient{constructor(h=document){this.h=h}\n    pub(m){this.h.dispatchEvent(new CustomEvent('pan:publish',{detail:m,bubbles:true,composed:true}))}\n    sub(t,fn){const on=e=\u003ee.detail?.topic\u0026\u0026PanClient.matches(e.detail.topic,t)\u0026\u0026fn(e.detail); this.h.addEventListener('pan:deliver',on);\n      this.h.dispatchEvent(new CustomEvent('pan:subscribe',{detail:{topics:[t]},bubbles:true,composed:true}));}\n    static matches(topic, pattern){ if(pattern==='*'||topic===pattern) return true; if(pattern.includes('*')){const esc=s=\u003es.replace(/[|\\\\{}()\\[\\]^$+?.]/g,'\\\\$\u0026').replace(/\\*/g,'[^.]+' );return new RegExp(`^${esc(pattern)}$`).test(topic);} return false; }\n  }\n  customElements.define('pan-bus', class extends HTMLElement{ subs=[]; connectedCallback(){\n    document.addEventListener('pan:publish', e=\u003ethis.#pub(e), true);\n    document.addEventListener('pan:subscribe', e=\u003ethis.#sub(e), true);\n    document.dispatchEvent(new CustomEvent('pan:sys.ready',{bubbles:true,composed:true})); }\n    #sub(e){const {topics=[]}=e.detail||{}; const el=e.composedPath?.()[0]; topics.forEach(p=\u003ethis.subs.push({p,el}));}\n    #pub(e){const m=e.detail; this.subs.forEach(s=\u003e{ if(PanClient.matches(m.topic,s.p)) s.el.dispatchEvent(new CustomEvent('pan:deliver',{detail:m})); });}\n  });\n  customElements.define('x-counter', class extends HTMLElement{ pc=new PanClient(this); n=0; connectedCallback(){ this.innerHTML=`\u003cbutton\u003eClicked 0\u003c/button\u003e`;\n    this.querySelector('button').onclick=()=\u003ethis.pc.pub({topic:'demo:click',data:{n:++this.n},retain:true});\n    this.pc.sub('demo:click', m=\u003e this.querySelector('button').textContent = `Clicked ${m.data.n}`);\n  }});\n\u003c/script\u003e\n```\n\n### `examples/02-todos-and-inspector.html`\n\nTodo list + retained state + DevTools‑style Inspector.\n\n```html\n\u003c!DOCTYPE html\u003e\u003cmeta charset=\"utf-8\" /\u003e\n\u003ctitle\u003eLARC – 02 Todos \u0026 Inspector\u003c/title\u003e\n\u003cpan-bus\u003e\u003c/pan-bus\u003e\n\u003ctodo-provider\u003e\u003c/todo-provider\u003e\n\u003ctodo-list\u003e\u003c/todo-list\u003e\n\u003cpan-inspector style=\"height:420px\"\u003e\u003c/pan-inspector\u003e\n\u003cscript type=\"module\"\u003e\n  // --- PanClient (minimal) \u0026 bus (same as above, with retained storage) ---\n  class PanClient{constructor(h=document){this.h=h}\n    pub(m){this.h.dispatchEvent(new CustomEvent('pan:publish',{detail:m,bubbles:true,composed:true}))}\n    sub(t,fn,o={}){const on=e=\u003ee.detail?.topic\u0026\u0026PanClient.matches(e.detail.topic,t)\u0026\u0026fn(e.detail); this.h.addEventListener('pan:deliver',on);\n      this.h.dispatchEvent(new CustomEvent('pan:subscribe',{detail:{topics:[t],options:o},bubbles:true,composed:true})); return ()=\u003ethis.h.removeEventListener('pan:deliver',on);}\n    static matches(topic, pattern){ if(pattern==='*'||topic===pattern) return true; if(pattern.includes('*')){const esc=s=\u003es.replace(/[|\\\\{}()\\[\\]^$+?.]/g,'\\\\$\u0026').replace(/\\*/g,'[^.]+' );return new RegExp(`^${esc(pattern)}$`).test(topic);} return false; }\n  }\n  customElements.define('pan-bus', class extends HTMLElement{ subs=[]; retained=new Map(); connectedCallback(){\n    document.addEventListener('pan:publish', e=\u003ethis.#pub(e), true);\n    document.addEventListener('pan:subscribe', e=\u003ethis.#sub(e), true);\n    document.dispatchEvent(new CustomEvent('pan:sys.ready',{bubbles:true,composed:true})); }\n    #sub(e){const {topics=[],options={}}=e.detail||{}; const el=e.composedPath?.()[0]; topics.forEach(p=\u003e{this.subs.push({p,el}); if(options.retained){ for(const [t,msg] of this.retained){ if(PanClient.matches(t,p)) el.dispatchEvent(new CustomEvent('pan:deliver',{detail:msg})); } }});}\n    #pub(e){const m={id:crypto.randomUUID(),ts:Date.now(),...e.detail}; if(m.retain) this.retained.set(m.topic,m); this.subs.forEach(s=\u003e{ if(PanClient.matches(m.topic,s.p)) s.el.dispatchEvent(new CustomEvent('pan:deliver',{detail:m})); });}\n  });\n  // --- Todo provider (stateful, publishes retained snapshot) ---\n  customElements.define('todo-provider', class extends HTMLElement{ pc=new PanClient(this); items=[]; connectedCallback(){\n    document.addEventListener('pan:sys.ready', ()=\u003e{\n      this.pc.sub('todos.change', m=\u003e{this.items.push(m.data.item); this.broadcast();});\n      this.pc.sub('todos.remove', m=\u003e{this.items=this.items.filter(t=\u003et.id!==m.data.id); this.broadcast();});\n      this.pc.sub('todos.toggle', m=\u003e{const t=this.items.find(x=\u003ex.id===m.data.id); if(t) t.done=!!m.data.done; this.broadcast();});\n      this.pc.pub({topic:'todos.state', data:{items:this.items}, retain:true});\n    }, {once:true});\n  }\n  broadcast(){ this.pc.pub({topic:'todos.state', data:{items:this.items}, retain:true}); }\n  });\n  // --- Todo list UI ---\n  customElements.define('todo-list', class extends HTMLElement{ pc=new PanClient(this); items=[]; connectedCallback(){\n    this.attachShadow({mode:'open'}); this.render();\n    this.pc.sub('todos.state', m=\u003e{this.items=m.data.items; this.render();}, {retained:true});\n  }\n  render(){ const h=String.raw; this.shadowRoot.innerHTML=h`\n    \u003cstyle\u003e .muted{color:#888} ul{list-style:none;padding:0} li{display:flex;gap:8px;padding:6px 0;border-bottom:1px dashed #ddd} li.done .t{ text-decoration:line-through; color:#888 } \u003c/style\u003e\n    \u003cform id=f\u003e\u003cinput id=title placeholder=\"Add a task…\"/\u003e\u003cbutton\u003eAdd\u003c/button\u003e\u003c/form\u003e\n    ${this.items.length? '' : '\u003cdiv class=muted\u003eNo tasks yet.\u003c/div\u003e'}\n    \u003cul\u003e${this.items.map(t=\u003e`\u003cli class=\"${t.done?'done':''}\" data-id=\"${t.id}\"\u003e\u003cinput type=checkbox ${t.done?'checked':''}\u003e\u003cspan class=t\u003e${t.title}\u003c/span\u003e\u003cspan style=\"flex:1\"\u003e\u003c/span\u003e\u003cbutton class=del\u003e✕\u003c/button\u003e\u003c/li\u003e`).join('')}\u003c/ul\u003e`;\n    const $=s=\u003ethis.shadowRoot.querySelector(s);\n    $('#f').onsubmit=(e)=\u003e{e.preventDefault(); const v=$('#title').value.trim(); if(!v) return; $('#title').value=''; this.pc.pub({topic:'todos.change', data:{item:{id:crypto.randomUUID(), title:v, done:false}}, retain:true}); };\n    this.shadowRoot.querySelectorAll('li input[type=checkbox]').forEach(cb=\u003ecb.addEventListener('change',e=\u003e{const id=e.target.closest('li').dataset.id; this.pc.pub({topic:'todos.toggle', data:{id, done:e.target.checked}, retain:true});}));\n    this.shadowRoot.querySelectorAll('li .del').forEach(b=\u003eb.addEventListener('click',e=\u003e{const id=e.target.closest('li').dataset.id; this.pc.pub({topic:'todos.remove', data:{id}, retain:true});}));\n  }\n  });\n  // --- Minimal Inspector (table view) ---\n  customElements.define('pan-inspector', class extends HTMLElement{ pc=new PanClient(this); events=[]; connectedCallback(){ this.attachShadow({mode:'open'}); this.render(); this.pc.sub('*', m=\u003e{this.events.push({ts:Date.now(),topic:m.topic,size:JSON.stringify(m).length}); this.render();}); }\n    render(){ const h=String.raw; this.shadowRoot.innerHTML=h`\u003cstyle\u003etable{width:100%;font:12px/1.4 monospace}th{ text-align:left; position:sticky; top:0; background:#f6f6f6 }\u003c/style\u003e\u003ctable\u003e\u003cthead\u003e\u003ctr\u003e\u003cth\u003etime\u003c/th\u003e\u003cth\u003etopic\u003c/th\u003e\u003cth\u003esize\u003c/th\u003e\u003c/tr\u003e\u003c/thead\u003e\u003ctbody\u003e${this.events.slice(-300).map(r=\u003e`\u003ctr\u003e\u003ctd\u003e${new Date(r.ts).toLocaleTimeString()}\u003c/td\u003e\u003ctd\u003e${r.topic}\u003c/td\u003e\u003ctd\u003e${r.size}\u003c/td\u003e\u003c/tr\u003e`).join('')}\u003c/tbody\u003e\u003c/table\u003e`; }\n  });\n\u003c/script\u003e\n```\n\n### `examples/03-broadcastchannel.html`\n\nMirror specific topics across tabs using `BroadcastChannel('pan')`.\n\n```html\n\u003c!doctype html\u003e\u003cmeta charset=\"utf-8\"\u003e\u003ctitle\u003eLARC – 03 BroadcastChannel\u003c/title\u003e\n\u003cpan-bus mirror=\"settings.*\"\u003e\u003c/pan-bus\u003e\n\u003cscript type=\"module\"\u003e\n  class PanClient{constructor(h=document){this.h=h}\n    pub(m){this.h.dispatchEvent(new CustomEvent('pan:publish',{detail:m,bubbles:true,composed:true}))}\n    sub(t,fn){const on=e=\u003ee.detail?.topic\u0026\u0026PanClient.matches(e.detail.topic,t)\u0026\u0026fn(e.detail); this.h.addEventListener('pan:deliver',on);\n      this.h.dispatchEvent(new CustomEvent('pan:subscribe',{detail:{topics:[t]},bubbles:true,composed:true}));}\n    static matches(topic, pattern){ if(pattern==='*'||topic===pattern) return true; if(pattern.includes('*')){const esc=s=\u003es.replace(/[|\\\\{}()\\[\\]^$+?.]/g,'\\\\$\u0026').replace(/\\*/g,'[^.]+' );return new RegExp(`^${esc(pattern)}$`).test(topic);} return false; }\n  }\n  customElements.define('pan-bus', class extends HTMLElement{ subs=[]; bc=null; connectedCallback(){ const allow=(this.getAttribute('mirror')||'').split(/\\s+/).filter(Boolean);\n    this.bc = new BroadcastChannel('pan'); this.bc.onmessage = (ev)=\u003e this.#deliver(ev.data);\n    document.addEventListener('pan:publish', e=\u003e{const m=e.detail; this.#deliver(m); if(allow.some(p=\u003ePanClient.matches(m.topic,p))) this.bc.postMessage(m);}, true);\n    document.addEventListener('pan:subscribe', e=\u003e{const {topics=[]}=e.detail||{}; const el=e.composedPath?.()[0]; topics.forEach(p=\u003ethis.subs.push({p,el}));}, true);\n  }\n  #deliver(m){ this.subs.forEach(s=\u003e PanClient.matches(m.topic,s.p) \u0026\u0026 s.el.dispatchEvent(new CustomEvent('pan:deliver',{detail:m}))); }\n  });\n  // Demo usage\n  const pc = new PanClient();\n  setInterval(()=\u003e pc.pub({ topic:'settings.clock', data: Date.now(), retain:true }), 1000);\n  pc.sub('settings.clock', m=\u003e console.log('tick', m.data));\n\u003c/script\u003e\n```\n\n### `examples/04-react-wrapper.html`\n\nUse PAN from React with no build (CDN React, plain JS, no JSX).\n\n```html\n\u003c!doctype html\u003e\u003cmeta charset=\"utf-8\"\u003e\u003ctitle\u003eLARC – 04 React\u003c/title\u003e\n\u003cscript crossorigin src=\"https://unpkg.com/react@18/umd/react.production.min.js\"\u003e\u003c/script\u003e\n\u003cscript crossorigin src=\"https://unpkg.com/react-dom@18/umd/react-dom.production.min.js\"\u003e\u003c/script\u003e\n\u003cpan-bus\u003e\u003c/pan-bus\u003e\n\u003cdiv id=\"app\"\u003e\u003c/div\u003e\n\u003cscript type=\"module\"\u003e\n  class PanClient{constructor(h=document){this.h=h}\n    pub(m){this.h.dispatchEvent(new CustomEvent('pan:publish',{detail:m,bubbles:true,composed:true}))}\n    sub(t,fn){const on=e=\u003ee.detail?.topic\u0026\u0026PanClient.matches(e.detail.topic,t)\u0026\u0026fn(e.detail); this.h.addEventListener('pan:deliver',on);\n      this.h.dispatchEvent(new CustomEvent('pan:subscribe',{detail:{topics:[t]},bubbles:true,composed:true})); return ()=\u003ethis.h.removeEventListener('pan:deliver',on);}\n    static matches(topic, pattern){ if(pattern==='*'||topic===pattern) return true; if(pattern.includes('*')){const esc=s=\u003es.replace(/[|\\\\{}()\\[\\]^$+?.]/g,'\\\\$\u0026').replace(/\\*/g,'[^.]+' );return new RegExp(`^${esc(pattern)}$`).test(topic);} return false; }\n  }\n  const pc = new PanClient();\n  function usePan(topic){ const [msg,setMsg]=React.useState(null);\n    React.useEffect(()=\u003e{ const off=pc.sub(topic, setMsg); return ()=\u003eoff\u0026\u0026off(); },[topic]);\n    return [msg, (m)=\u003epc.pub(m)]; }\n  function App(){ const [msg, send] = usePan('chat.message');\n    const [text,setText] = React.useState('');\n    return React.createElement('div',{},\n      React.createElement('h3',{},'React ↔ PAN'),\n      msg \u0026\u0026 React.createElement('pre',{}, JSON.stringify(msg.data,null,2)),\n      React.createElement('input',{value:text,onChange:e=\u003esetText(e.target.value)}),\n      React.createElement('button',{onClick:()=\u003e{send({topic:'chat.message', data:{text}}); setText('');}},'Send')\n    );\n  }\n  ReactDOM.createRoot(document.getElementById('app')).render(React.createElement(App));\n\u003c/script\u003e\n```\n\n### `examples/05-lit-wrapper.html`\n\nUse PAN from a Lit element (CDN Lit, no build).\n\n```html\n\u003c!doctype html\u003e\u003cmeta charset=\"utf-8\"\u003e\u003ctitle\u003eLARC – 05 Lit\u003c/title\u003e\n\u003cscript type=\"module\"\u003e\n  import { LitElement, html, css } from 'https://unpkg.com/lit?module';\n  class PanClient{constructor(h=document){this.h=h}\n    pub(m){this.h.dispatchEvent(new CustomEvent('pan:publish',{detail:m,bubbles:true,composed:true}))}\n    sub(t,fn){const on=e=\u003ee.detail?.topic\u0026\u0026PanClient.matches(e.detail.topic,t)\u0026\u0026fn(e.detail); this.h.addEventListener('pan:deliver',on);\n      this.h.dispatchEvent(new CustomEvent('pan:subscribe',{detail:{topics:[t]},bubbles:true,composed:true})); return ()=\u003ethis.h.removeEventListener('pan:deliver',on);}\n    static matches(topic, pattern){ if(pattern==='*'||topic===pattern) return true; if(pattern.includes('*')){const esc=s=\u003es.replace(/[|\\\\{}()\\[\\]^$+?.]/g,'\\\\$\u0026').replace(/\\*/g,'[^.]+' );return new RegExp(`^${esc(pattern)}$`).test(topic);} return false; }\n  }\n  customElements.define('pan-bus', class extends HTMLElement{ subs=[]; connectedCallback(){\n    document.addEventListener('pan:publish', e=\u003ethis.#pub(e), true);\n    document.addEventListener('pan:subscribe', e=\u003ethis.#sub(e), true);\n  } #sub(e){const {topics=[]}=e.detail||{}; const el=e.composedPath?.()[0]; topics.forEach(p=\u003ethis.subs.push({p,el}));}\n    #pub(e){const m=e.detail; this.subs.forEach(s=\u003e PanClient.matches(m.topic,s.p) \u0026\u0026 s.el.dispatchEvent(new CustomEvent('pan:deliver',{detail:m}))); }\n  });\n  class LitChat extends LitElement{\n    static styles = css`:host{display:block;padding:12px;border:1px solid #ccc;border-radius:8px}`;\n    pc = new PanClient(this);\n    firstUpdated(){ this.off = this.pc.sub('chat.message', m=\u003e{ this.last = m.data; this.requestUpdate(); }); }\n    disconnectedCallback(){ super.disconnectedCallback(); this.off \u0026\u0026 this.off(); }\n    render(){ return html`\n      \u003ch3\u003eLit ↔ PAN\u003c/h3\u003e\n      ${this.last ? html`\u003cpre\u003e${JSON.stringify(this.last,null,2)}\u003c/pre\u003e` : html`\u003cem\u003eno messages yet\u003c/em\u003e`}\n      \u003cform @submit=${e=\u003e{e.preventDefault(); const v=this.renderRoot.getElementById('t').value.trim(); if(!v) return; this.pc.pub({topic:'chat.message', data:{text:v}}); this.renderRoot.getElementById('t').value='';}}\u003e\n        \u003cinput id=\"t\" placeholder=\"Say hi\" /\u003e\n        \u003cbutton\u003eSend\u003c/button\u003e\n      \u003c/form\u003e` }\n  }\n  customElements.define('lit-chat', LitChat);\n\u003c/script\u003e\n\u003cpan-bus\u003e\u003c/pan-bus\u003e\n\u003clit-chat\u003e\u003c/lit-chat\u003e\n```\n\n### `examples/06-crud.html`\n\nBasic CRUD stack wired to a mock provider (local state, optional `localStorage` persistence).\n\n```html\n\u003c!doctype html\u003e\u003cmeta charset=\"utf-8\"\u003e\n\u003cpan-bus\u003e\u003c/pan-bus\u003e\n\u003cdiv class=\"row\"\u003e\n  \u003cpan-data-table resource=\"users\" columns=\"id,name,email\"\u003e\u003c/pan-data-table\u003e\n  \u003cpan-form resource=\"users\" fields=\"name,email\"\u003e\u003c/pan-form\u003e\n  \u003cpan-inspector style=\"height:320px\"\u003e\u003c/pan-inspector\u003e\n  \u003cpan-data-provider resource=\"users\" persist=\"localStorage\"\u003e\n    \u003cscript type=\"application/json\"\u003e[{\"id\":\"u1\",\"name\":\"Ada\",\"email\":\"ada@example.com\"}]\u003c/script\u003e\n  \u003c/pan-data-provider\u003e\n  \u003cscript type=\"module\"\u003e\n    import '../dist/pan-bus.js';\n    import '../dist/pan-client.js';\n    import '../dist/pan-inspector.js';\n    import '../dist/pan-data-provider-mock.js';\n    import '../dist/pan-data-table.js';\n    import '../dist/pan-form.js';\n  \u003c/script\u003e\n  \u003c!-- Open examples/06-crud.html to try it. --\u003e\n```\n\n### `examples/07-rest-connector.html`\n\nCRUD stack driving a remote REST API via `\u003cpan-data-connector\u003e`. Uses JSONPlaceholder for demo.\n\n```html\n\u003c!doctype html\u003e\u003cmeta charset=\"utf-8\"\u003e\n\u003cpan-bus\u003e\u003c/pan-bus\u003e\n\u003cpan-data-table resource=\"users\" columns=\"id,name,email\"\u003e\u003c/pan-data-table\u003e\n\u003cpan-form resource=\"users\" fields=\"name,email\"\u003e\u003c/pan-form\u003e\n\u003cpan-inspector style=\"height:340px\"\u003e\u003c/pan-inspector\u003e\n\u003cpan-data-connector resource=\"users\" base-url=\"https://jsonplaceholder.typicode.com\"\u003e\u003c/pan-data-connector\u003e\n\u003cscript type=\"module\"\u003e\n  import '../dist/pan-bus.js';\n  import '../dist/pan-client.js';\n  import '../dist/pan-inspector.js';\n  import '../dist/pan-data-table.js';\n  import '../dist/pan-form.js';\n  import '../dist/pan-data-connector.js';\n  // Optional refresh\n  import { PanClient } from '../dist/pan-client.js';\n  new PanClient().publish({ topic:'users.list.get', data:{} });\n  // Open examples/07-rest-connector.html to try it.\n\u003c/script\u003e\n```\n\n---\n\n### `examples/08-workers.html`\n\nOffload filter/sort of 10k records to a Web Worker via `\u003cpan-worker\u003e`; publishes computed `${resource}.list.state` for `\u003cpan-data-table\u003e`.\n\n```html\n\u003c!doctype html\u003e\u003cmeta charset=\"utf-8\"\u003e\n\u003cpan-bus\u003e\u003c/pan-bus\u003e\n\u003cpan-worker topics=\"users.list.get users.query.set\"\u003e\n  \u003cscript type=\"application/worker\"\u003e\n    // Worker: generate 10k users, compute filter/sort, publish users.list.state\n    let items=[]; for(let i=0;i\u003c10000;i++){ const id=`u${i+1}`; const name=`User ${String(i+1).padStart(5,'0')}`; items.push({id,name,email:`${name.toLowerCase().replace(/\\s+/g,'')}@example.com`}); }\n    let q={q:'',sort:'name:asc'};\n    function pub(){ const [k,d]=(q.sort||'name:asc').split(':'); let v=items; if(q.q){const s=q.q.toLowerCase(); v=v.filter(it=\u003eit.name.toLowerCase().includes(s)||it.email.toLowerCase().includes(s));} v=v.slice().sort((a,b)=\u003e{const av=a[k],bv=b[k];return (av\u003ebv?1:av\u003cbv?-1:0)*(d==='desc'?-1:1)}); postMessage({topic:'users.list.state',data:{items:v},retain:true}); }\n    onmessage=(e)=\u003e{ const m=e.data||{}; if(m.topic==='users.query.set'){ q=Object.assign({},q,m.data||{}); pub(); } if(m.topic==='users.list.get'){ pub(); } };\n  \u003c/script\u003e\n\u003c/pan-worker\u003e\n\u003cpan-data-table resource=\"users\" columns=\"id,name,email\"\u003e\u003c/pan-data-table\u003e\n\u003cscript type=\"module\"\u003e\n  import '../dist/pan-bus.js';\n  import '../dist/pan-client.js';\n  import '../dist/pan-data-table.js';\n  import '../dist/pan-worker.js';\n  import { PanClient } from '../dist/pan-client.js';\n  new PanClient().publish({ topic:'users.list.get', data:{} });\n\u003c/script\u003e\n```\n\n---\n\n## CRUD Components\n\n- `pan-data-table`: subscribes to `${resource}.list.state` and renders a table. Publishes row clicks as `${resource}.item.select`.\n- `pan-form`: listens for `${resource}.item.select`, requests `${resource}.item.get`, and submits to `${resource}.item.save` / `${resource}.item.delete`.\n- `pan-data-provider`: mock in‑memory provider. Seeds from child JSON script and can persist to `localStorage`. Handles get/save/delete topics and publishes `${resource}.list.state` (retained).\n- `pan-data-connector`: REST bridge. Maps PAN CRUD topics to HTTP endpoints. Publishes `${resource}.list.state` (retained).\n- `pan-query`: query orchestrator; retains `${resource}.query.state` and triggers `${resource}.list.get`. Supports URL sync via `sync-url=\"search|hash\"`.\n\n### Schema-driven Components\n\n- `pan-schema`: publishes retained `${resource}.schema.state` from a `src` URL or inline JSON.\n- `pan-schema-form`: renders a form from JSON Schema, validates locally, and performs `${resource}.item.get`/`.save`/`.delete`. Listens to `${resource}.item.state.*` for live updates.\n\n### Additional Connectors\n\n- `pan-php-connector`: bridges `${resource}.list.*` topics to a PHP endpoint shaped like `api.php` (supports paging, filters). Publishes aggregated `${resource}.list.state`.\n- `pan-graphql-connector`: maps CRUD topics to GraphQL queries/mutations provided as child `\u003cscript type=\"application/graphql\" data-op=\"...\"\u003e` and extracts results via a JSON `data-paths` map.\n\nRealtime bridges and stores:\n\n- `pan-sse`: bridges Server-Sent Events into PAN topics. Attributes: `src`, optional `topics` (space-separated), `persist-last-event`, and `backoff` (e.g., `1000,15000`). Emits events where either `event:` is the topic or JSON payload contains `{ topic, data }`.\n- `pan-forwarder`: forwards selected topics to an HTTP endpoint (e.g., `sse.php` POST). Attributes: `dest`, `topics`, optional `headers`, `with-credentials`.\n- `pan-store`: tiny reactive store and `bind()` helper for wiring form fields ↔ state.\n- `pan-store-pan`: helpers `syncItem()` and `syncList()` to connect stores to PAN topics (auto-save, live updates).\n\nDefaults and attributes:\n\n- `resource`: logical name (default `items`).\n- `pan-data-table` attributes: `columns=\"col1,col2\"`, optional `key` (id field, default `id`).\n- `pan-form` attributes: `fields=\"name,email\"`, optional `key` (id field, default `id`).\n- `pan-data-provider` attributes: `persist=\"localStorage\"`, optional `key`.\n- `pan-data-connector` attributes: `base-url`, optional `list-path` (default `/${resource}`), `item-path` (default `/${resource}/:id`), `update-method` (`PUT`|`PATCH`, default `PUT`), `credentials` (e.g. `include`). Optional child `\u003cscript type=\"application/json\"\u003e` supplies fetch options (e.g., headers).\n\nTopic contract (generic CRUD):\n\n- Request list: `${resource}.list.get` → replies `{ items }`; also causes `${resource}.list.state` to be retained.\n- Select row: `${resource}.item.select` with `{ id }`.\n- Get item: `${resource}.item.get` with `{ id }` → replies `{ ok, item? }`.\n- Save item: `${resource}.item.save` with `{ item }` → replies `{ ok, item }`.\n- Delete item: `${resource}.item.delete` with `{ id }` → replies `{ ok, id }`.\n\nRealtime updates (optional, generic):\n\n- Per‑item state: providers may publish retained `${resource}.item.state.${id}` with `{ item }` when an item changes.\n- Deletions: publish `${resource}.item.state.${id}` with `{ id, deleted: true }` (not retained).\n- `pan-data-table` and `pan-form` listen for these automatically when `live=\"true\"` (default).\n\n---\n\n## SSE Sidecar + Store Example\n\nRun a minimal SSE + REST sidecar (no deps):\n\n```\nnode examples/server/sse-server.js\n```\n\nThen open: `examples/10-sse-store.html`\n\n- The page uses `\u003cpan-sse\u003e` to receive server events and republish as PAN topics.\n- `\u003cpan-data-connector\u003e` points to the sidecar REST API for CRUD.\n- A small store auto-saves changes to `${resource}.item.save` and updates live from `${resource}.item.state.${id}`.\n\n---\n\n## Stores \u0026 Sync APIs\n\n`pan-store` (dist/pan-store.js)\n\n- createStore(initial)\n  - Returns `{ state, subscribe(fn), snapshot(), set(k,v), patch(obj), update(fn) }`.\n  - Proxy-backed `state` emits a `state` event on key change.\n- bind(el, store, map, opts?)\n  - Two-way binds form elements to store keys.\n  - `map`: `{ 'input[name=name]':'name', 'input[name=email]':'email' }`.\n  - `opts.events`: default `['input','change']`.\n\n`pan-store-pan` (dist/pan-store-pan.js)\n\n- syncItem(store, opts)\n  - Bridges a store to item topics; applies live updates and (optionally) auto-saves edits.\n  - Options:\n    - `resource='items'`, `key='id'`\n    - `id`: fixed id; if omitted and `followSelect=true`, follows `${resource}.item.select`.\n    - `live=true`: subscribe to `${resource}.item.state.${id}` (retained) and apply `{ item }`, `{ patch }`, or top-level patches; clear on `{ deleted:true }`.\n    - `autoSave=true`: debounce changes and request `${resource}.item.save` with `{ item: store.snapshot() }`.\n    - `debounceMs=300`, `followSelect=true`.\n  - Returns an `unsubscribe` function.\n- syncList(store, opts)\n  - Tracks `${resource}.list.state` and `${resource}.item.state.*` into an in-memory array.\n  - Options: `resource='items'`, `key='id'`, `live=true`.\n  - Expects a store with at least an `items` key; updates via `store._setAll({ items })`.\n\n`pan-sse` (dist/pan-sse.js)\n\n- Attributes\n  - `src`: SSE endpoint URL (absolute or relative).\n  - `topics`: optional space-separated list; added as `?topics=...` to the request.\n  - `persist-last-event`: key for localStorage to resume with `?lastEventId=`.\n  - `backoff`: `min,max` in ms (e.g., `1000,15000`).\n  - `with-credentials`: include cookies; default true if present.\n- Server payloads supported\n  - Event-as-topic: `event: users.item.state.u123` + `data: {\"item\":{...}}`.\n  - JSON envelope: `event: message` + `data: {\"topic\":\"users.item.state.u123\",\"data\":{...},\"retain\":true}`.\n  - The bridge republishes `{ topic, data, retain? }` onto the PAN bus.\n\n`pan-form` and `pan-data-table`\n\n- Both now support `live` (default `true`) and optional `key` for id field.\n- `pan-data-table` remains subscribed to `${resource}.list.state` and merges `${resource}.item.state.*` updates.\n- `pan-form` follows `${resource}.item.select` and keeps the selected item live-synced.\n\n`pan-table`\n\n- `dist/pan-table.js` defines `\u003cpan-table\u003e` as an alias for `\u003cpan-data-table\u003e`.\n\n---\n\n## Demo Browser (SPA)\n\n`index.html` hosts a SPA-style browser powered by PAN topics:\n\n- `\u003cpan-demo-nav\u003e`: renders the example list from inline JSON, publishes retained `nav.state` and `nav.goto` topics on selection (hash-synced).\n- `\u003cpan-demo-viewer\u003e`: subscribes to `nav.state` and loads the selected example in an iframe (or inline HTML in `mode=\"inline\"`).\n\nNavigation topics:\n\n- `nav.state` (retained): `{ href, id }` current selection\n- `nav.goto`: `{ href, id }` imperative navigation\n\nOpen `index.html` to browse all examples in a single page.\n\n---\n\n## More Examples\n\n- `examples/08-workers.html`: Workers + Query orchestrator (10k synthetic records).\n- `examples/09-schema-form.html`: Schema-driven form + mock provider.\n- `examples/11-graphql-connector.html`: GraphQL connector (GraphQLZero) with list/get/save/delete.\n- `examples/12-php-connector.html`: PHP connector against local `api.php`; list + paging.\n- `registry/index.html`: Component registry viewer (loads `registry/index.json`).\n- `conformance/index.html`: PAN v1 conformance tests for the reference implementation.\n- `templates/provider-kit/`: Starter kit for a minimal CRUD provider component.\n- `examples/13-sse-pan.html`: Local PHP SSE hub (`sse.php`) broadcasting into PAN.\n- `pan-grid.html`: DB grid wired to `api.php` using PAN.\n\nTopic patterns in use\n\n- Bulk: `${resource}.list.state` with `{ items: [...] }` (retained).\n- Per-item: `${resource}.item.state.${id}` with either `{ item }` (retained) or `{ patch }`.\n- Deletion: `${resource}.item.state.${id}` with `{ id, deleted:true }` (not retained).\n\nProviders\n\n- `pan-data-provider` (mock) and `pan-data-connector` (REST) now also publish per-item snapshots on get/save and deletion notices.\n  - Mock: dist/pan-data-provider-mock.js\n  - REST: dist/pan-data-connector.js\n\nOperational notes (PHP)\n\n- For PHP (mod_php or PHP-FPM), prefer using the small sidecar for SSE/WebSocket rather than holding long-lived connections in PHP workers.\n- If serving SSE from PHP directly, disable output buffering, avoid locking sessions, send keep-alives regularly, and ensure reverse proxies don’t buffer.\n\nLocal PHP SSE hub\n\n- `sse.php` implements a simple file-backed SSE stream and a `POST` endpoint:\n  - `GET /pan/sse.php?topics=users.*` streams events (with keepalives; respects `lastEventId`).\n  - `POST /pan/sse.php` with `{ \"topic\":\"chat.message\", \"data\":{...}, \"retain\":false }` appends and broadcasts.\n  - Use `\u003cpan-sse src=\"/pan/sse.php\" topics=\"chat.message demo.*\"\u003e` to bridge into PAN.\n\n\n---\n\n## Spec \u0026 Guarantees\n\n* Spec lives in `LARC_SPEC.v0.md` (topics, envelopes, versioning, compliance tests).\n* Backwards compatibility: topic schemas versioned with semver; minor bumps are additive.\n\n---\n\n## Roadmap\n\n* Inspector Pro: timeline, heatmaps, replay to Playwright tests.\n* Schema registry \u0026 TS typegen.\n* Cross‑origin gateway (`postMessage`) with allowlists.\n* IndexedDB/Yjs providers for offline + CRDT sync.\n\n---\n\n## Sample Data\n\nStatic JSON you can load in examples or serve via a simple static server:\n\n- `examples/data/users.json`\n- `examples/data/products.json`\n- `examples/data/todos.json`\n\n---\n\n## E2E Tests (Playwright)\n\n- Prereqs: Node 18+.\n- Install Playwright (downloads browsers):\n  - `npx -y playwright install`\n- Run tests:\n  - `npx playwright test` (headless)\n  - `npx playwright test --headed` (headed)\n  - `npx playwright test --ui` (watch mode)\n- Notes: tests open the example pages via `file://` URLs and validate UI actions and bus traffic.\n\n---\n\n## License\n\nMIT for core libraries. Pro/enterprise add‑ons under commercial license.\n\n---\n\n## PAN v1, Registry, and Packages\n\n- Spec: `PAN_SPEC.v1.md` (concise)\n- Conformance: open `conformance/index.html` — passing implementations may use `badges/pan-v1.svg`.\n- Registry: `registry/index.json` + `registry/index.html` to browse components by topic/type.\n- npm scaffold: `packages/` contains publishable entries for `@pan/bus`, `@pan/client`, `@pan/inspector`.\n  - Sync dist into packages: `npm run packages:sync`\n  - Then publish from each package folder (remove root `private:true` if you split repos).\n- Build registry from packages: `npm run registry:build` updates `registry/index.json` from `packages/*/package.json` `pan` metadata.\n- Conformance badge: `npm run conformance:badge` writes `conformance/badge.json` and `badges/pan-v1-status.svg` using Playwright.\n- RFCs: see `rfcs/README.md` and `.github/ISSUE_TEMPLATE/rfc.md`.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fchrisrobison%2Fpan","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fchrisrobison%2Fpan","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fchrisrobison%2Fpan/lists"}