{"_id":"@calvinjs/active-state","name":"@calvinjs/active-state","dist-tags":{"latest":"0.1.0"},"versions":{"0.1.0":{"name":"@calvinjs/active-state","version":"0.1.0","description":"Tiny keyed pub/sub client store with React and static HTML bindings","type":"module","license":"MIT","author":{"name":"Calvin Maighan"},"repository":{"type":"git","url":"git+https://github.com/CalvinMaighan/active-state.git"},"sideEffects":["./dist/react/index.js"],"main":"./dist/index.js","module":"./dist/index.js","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"},"./react":{"types":"./dist/react/index.d.ts","import":"./dist/react/index.js"},"./dom":{"types":"./dist/dom/index.d.ts","import":"./dist/dom/index.js"},"./eslint":{"types":"./dist/eslint/index.d.ts","import":"./dist/eslint/index.js"},"./package.json":"./package.json"},"unpkg":"./dist/active-state.min.js","jsdelivr":"./dist/active-state.min.js","publishConfig":{"access":"public"},"scripts":{"build":"rm -rf dist && tsup","prepare":"bun run build","test":"bun test","size":"bun run build && bun scripts/size.ts","size:brotli":"bun run build && bun scripts/size.ts --brotli","examples:html":"bun run build && bun examples/html/serve.ts","examples:htmx":"bun run build && bun examples/htmx/serve.ts","examples:react":"bun run build && cd examples/react && bun install && bun run dev","examples:astro":"bun run build && cd examples/astro && bun install && bun run dev","prepublishOnly":"bun run build && bun test"},"peerDependencies":{"eslint":">=9","react":">=18"},"peerDependenciesMeta":{"eslint":{"optional":true},"react":{"optional":true}},"devDependencies":{"@types/bun":"^1.3.14","@types/react":"^19.2.17","eslint":"^9.39.2","happy-dom":"^20.11.1","react":"^19.2.8","react-dom":"^19.2.8","tsup":"^8.5.1","typescript":"5.9.3"},"_id":"@calvinjs/active-state@0.1.0","gitHead":"b6dcd9d5fdbf96c4ecc4a08ce0b9d5425106d43f","bugs":{"url":"https://github.com/CalvinMaighan/active-state/issues"},"homepage":"https://github.com/CalvinMaighan/active-state#readme","_nodeVersion":"22.23.1","_npmVersion":"10.9.8","dist":{"integrity":"sha512-u34dLej41z6+zJdww47zFBWAjxW9CcO84J5V/ne1whV/StdsBzYVBpdfLxn2k7BA6dNhJbMR5Fki3T3KpMUpQA==","shasum":"20122f53ec8e44b6f01a8adb3cbfdf4fe894dbf1","tarball":"https://registry.npmjs.org/@calvinjs/active-state/-/active-state-0.1.0.tgz","fileCount":12,"unpackedSize":83998,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIQDS43YFgTi9kpHMRk81MML7Z5EG2aqJQBGKN7X0B3aW7wIgSNLtGs9oHrYD6aD3bYH3LF+zN528j2MTqg/DDWak4LY="}]},"_npmUser":{"name":"calvinjs","email":"calvinmaighan@gmail.com"},"directories":{},"maintainers":[{"name":"calvinjs","email":"calvinmaighan@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/active-state_0.1.0_1785030444583_0.4971528608152427"},"_hasShrinkwrap":false}},"time":{"created":"2026-07-26T01:47:24.429Z","0.1.0":"2026-07-26T01:47:24.751Z","modified":"2026-07-26T01:47:24.947Z"},"maintainers":[{"name":"calvinjs","email":"calvinmaighan@gmail.com"}],"description":"Tiny keyed pub/sub client store with React and static HTML bindings","homepage":"https://github.com/CalvinMaighan/active-state#readme","repository":{"type":"git","url":"git+https://github.com/CalvinMaighan/active-state.git"},"author":{"name":"Calvin Maighan"},"bugs":{"url":"https://github.com/CalvinMaighan/active-state/issues"},"license":"MIT","readme":"# active-state\n\nTiny keyed pub/sub store for shared client UI state — React, Next.js, Astro, HTMX, and light static HTML.\n\nNo Providers. Theme-agnostic (bring your own keys).\n\n## Install\n\n```bash\nbunx add @calvinjs/active-state\n```\n\n## 1. Define keys\n\n`key()` types the id **and** registers its defaults on `ActiveState.state`.\n\n```ts\n// client/state/user.ts\nimport { key } from \"@calvinjs/active-state\";\n\nexport const USER = key(\"USER\", {\n  id: null as string | null,\n  name: null as string | null,\n});\n```\n\n```ts\n// client/state/layout.ts\nimport { key } from \"@calvinjs/active-state\";\n\nexport const LAYOUT = key(\"LAYOUT\", { nav: false });\n```\n\n```ts\n// client/state/theme.ts\nimport { key } from \"@calvinjs/active-state\";\n\n// Survives refresh (localStorage). With <ActiveState init={state} ssr />, applies after first paint.\nexport const THEME = key(\"THEME\", { dark: false }, { persist: true });\n```\n\n```ts\n// client/state/index.ts\nimport { catalog } from \"@calvinjs/active-state\";\nimport { USER } from \"./user\";\nimport { LAYOUT } from \"./layout\";\nimport { THEME } from \"./theme\";\n\nexport { USER, LAYOUT, THEME };\n\n/** Snapshot for `<ActiveState init={state} />` — importing this runs every `key()`. */\nexport const state = catalog(USER, LAYOUT, THEME);\n```\n\n```ts\n// client/index.ts — barrel for the whole client folder\nexport * from \"./state\";\n```\n\nKeys should be `UPPERCASE_IDS` unless you pass `{ any: true }`.\n\n- `LAYOUT.$` → `\"LAYOUT\"`\n- `LAYOUT.nav` → `\"LAYOUT.nav\"`\n- `state` / `ActiveState.state` → `{ USER: {…}, LAYOUT: { nav: false }, THEME: {…} }`\n\nPath alias — point at the `client` folder:\n\n```jsonc\n// tsconfig.json\n{\n  \"compilerOptions\": {\n    \"paths\": {\n      \"client\": [\"./client\"],\n      \"client/*\": [\"./client/*\"]\n    }\n  }\n}\n```\n\n```ts\nimport { state, USER, LAYOUT } from \"client\";\nimport { LAYOUT } from \"client/state/layout\"; // single key module\n```\n\nNext.js picks up `paths` from `tsconfig.json` automatically.\n\n## 2. Init once\n\n`<ActiveState init={…} />` is **required**. Import your catalog and pass the snapshot — that import is what runs `key()` (persist marks, registry) and makes the map you hand to boot.\n\nDon’t mount `<ActiveState />` from a lazy route that defines keys later — `init` is idempotent and will not pick up keys registered after the first boot.\n\n### Next.js / React\n\n```tsx\n// app/layout.tsx\nimport { state } from \"client/state\";\nimport { ActiveState } from \"@calvinjs/active-state/react\";\n\nexport default function RootLayout({ children }: { children: React.ReactNode }) {\n  return (\n    <html lang=\"en\">\n      <body>\n        <ActiveState init={state} ssr />\n        {children}\n      </body>\n    </html>\n  );\n}\n```\n\n| Prop | Meaning |\n| --- | --- |\n| `init` | **Required.** Non-empty map (usually `state` from your catalog). |\n| `ssr` | App Router / static export — safe hydration. |\n| `any` | Allow non-`UPPERCASE_IDS` keys. |\n\nSame-file / one-off apps can pass the live registry:\n\n```tsx\n<ActiveState init={ActiveState.state} />\n```\n\nClient-only apps can omit `ssr`:\n\n```tsx\nimport { state } from \"client/state\";\n<ActiveState init={state} />\n```\n\n### Without React\n\n```ts\nimport { state } from \"client/state\";\nimport { init } from \"@calvinjs/active-state\";\n\ninit(state);\n// init(state, { ssr: true })\n```\n\n## 3. Use it in React\n\n```tsx\n// components/nav-toggle.tsx\n\"use client\";\n\nimport { useActiveState } from \"@calvinjs/active-state/react\";\nimport { LAYOUT } from \"client\";\n\n\nexport function NavToggle() {\n  const [layout, setLayout] = useActiveState<typeof LAYOUT.defaults>(LAYOUT);\n\n  return (\n    <button\n      type=\"button\"\n      onClick={() => setLayout((l) => ({ ...l!, nav: !l?.nav }))}\n    >\n      {layout?.nav ? \"Close\" : \"Menu\"}\n    </button>\n  );\n}\n```\n\n```ts\nActiveState.set(LAYOUT, (l) => ({ ...l!, nav: true }));\nActiveState.get(LAYOUT);\n```\n\n## 4. Public / static HTML\n\nPaths + verbs in HTML — **no JS expressions** (not Alpine). Keep attr strings aligned with your `key()` paths.\n\n```html\n<script src=\"https://unpkg.com/@calvinjs/active-state/dist/active-state.min.js\"></script>\n<script>\n  ActiveState.init({ LAYOUT: { nav: false }, THEME: { dark: false } });\n  ActiveState.bind();\n</script>\n\n<button type=\"button\" active-click=\"toggle:LAYOUT.nav\">Menu</button>\nNav: <span active-text=\"LAYOUT.nav\"></span>\n\n<input active-model=\"THEME.label\" />\n<template active-each=\"BOARD.columns\" active-as=\"col\">\n  <section active-drop=\"move→col.cards\">\n    <h2 active-text=\"col.title\"></h2>\n    <template active-each=\"col.cards\" active-as=\"card\">\n      <article active-drag=\"card.id\">\n        <span active-text=\"card.title\"></span>\n      </article>\n    </template>\n    <form active-submit=\"push:col.cards\">\n      <input name=\"title\" />\n      <button type=\"submit\">Add</button>\n    </form>\n  </section>\n</template>\n```\n\n| Attribute | Behavior |\n| --- | --- |\n| `active-text` | `textContent` ← path |\n| `active-show` | show when path is truthy |\n| `active-model` | two-way on input/textarea/select/checkbox |\n| `active-click` | command: `toggle:path`, `set:path:value`, `push:path:json` |\n| `active-toggle` | shorthand → `toggle:path` |\n| `active-submit` | form → `push:path` (FormData object; auto `id` if `title` set) |\n| `active-each` + `active-as` | `<template>` list; alias paths like `col.title` |\n| `active-drag` | drag payload ← path (e.g. `card.id`) |\n| `active-drop` | `move→arrayPath` moves `{id}` into that array |\n\nSee `examples/html` for a full kanban.\n\nWith a bundler:\n\n```ts\nimport \"client\";\nimport { init, registeredState } from \"@calvinjs/active-state\";\nimport { bind } from \"@calvinjs/active-state/dom\";\n\ninit(registeredState());\nbind();\n```\n\n## 5. ESLint (agent guardrails)\n\nErrors include fix instructions so coding agents self-correct.\n\n```bash\nbunx add -d eslint @calvinjs/active-state\n```\n\n```js\n// eslint.config.mjs\nimport { recommended, publicPages } from \"@calvinjs/active-state/eslint\";\n\nexport default [\n  ...recommended,\n  // Hook ban on public/marketing only — omit authenticated app routes.\n  ...publicPages({\n    files: [\n      \"app/(public)/**/*.{js,jsx,ts,tsx}\",\n      \"app/(marketing)/**/*.{js,jsx,ts,tsx}\",\n    ],\n  }),\n];\n```\n\n| Rule | Catches |\n| --- | --- |\n| `no-string-keys` | `get(\"LAYOUT\")` / `useActiveState(\"…\")` — use a `key()` slice |\n| `valid-active-attr` | Bad `active-*` path / command shape |\n| `no-hooks-in-files` | Any React hook under your `publicPages` globs |\n\n`recommended` = string keys + attr shape. `publicPages({ files })` = hook ban on those paths only — list public/marketing globs; leave `app/(app)/**` (or whatever your product shell is) out so agents can use hooks there.\n\n## API reference\n\n\n| | |\n| --- | --- |\n| `key(id, defaults, { persist? })` | Typed paths + register defaults; `persist: true` → `localStorage` |\n| `hydratePersisted()` | Re-load persisted keys (usually automatic) |\n| `ActiveState.state` / `registeredState()` | Current registered map |\n| `catalog()` | Same as `registeredState()`; `catalog(A, B)` builds a custom map |\n| `init(state, opts?)` / `<ActiveState init={state} ssr />` | Create the store (idempotent; `init` map required) |\n| `get` / `set` / `subscribe` | Accept string or `key()` slice |\n| `bind()` | Wire path + verb attrs (`text`, `model`, `click`, `each`, drag/drop, …) |\n| `reset()` | Clear store **and** key registry (tests / hot reload) |\n| `recommended` / `publicPages` from `@calvinjs/active-state/eslint` | Flat-config guardrails (named exports) |\n\n## Scope\n\n**In**\n\n- Keyed observables + singleton bus\n- Auto-registering `key()` → `ActiveState.state` (`persist: true` → localStorage)\n- React / Next.js: `<ActiveState init={state} />`, `useActiveState`, `ssr`\n- Astro / HTMX / static HTML: same DOM path + verb bindings (see `examples/`)\n- DOM: `each`, `model`, `click`, drag/drop, …\n- CDN build\n- ESLint guardrails for AI-assisted codebases\n\n**Out** (later / other packages)\n\n- Theme engines / Alpine-style expression JS in HTML / component system\n- First-party Alpine adapter (works alongside today; no dedicated bridge yet)\n- Scroll/hover helpers\n- HTML catalog sync beyond path/command-shape checks\n\n## Bundle size\n\n| Import | gzip | brotli | Use when |\n| --- | ---: | ---: | --- |\n| `@calvinjs/active-state` | ~2.3KB | ~2.0KB | Vanilla JS/TS — `init` / `get` / `set` / `subscribe` / `key()` |\n| `@calvinjs/active-state/dom` | ~5.1KB | ~4.6KB | HTML verbs — `each` / `model` / `click` / drag-drop (+ core) |\n| `@calvinjs/active-state/react` | ~0.7KB | ~0.6KB | Next.js / React — `<ActiveState init={state} />` + `useActiveState` (+ core) |\n| CDN IIFE | ~5.3KB | ~4.8KB | core + dom in one browser build |\n\nSizes are per entry (gzip level 9 / brotli quality 11). `/react` and `/dom` depend on core (one shared singleton). Importing `ActiveState` from `/react` also pulls `/dom` for `bind`. ESLint (`@calvinjs/active-state/eslint`) is opt-in. CDNs typically serve brotli when the browser accepts it.\n\n## Examples\n\nEach demo is a full **kanban board** (drag / add / theme) — not published to npm:\n\n```bash\nbun run examples:html    # IIFE kanban\nbun run examples:htmx    # kanban + HTMX suggest\nbun run examples:react   # useActiveState kanban\nbun run examples:astro   # Astro + same board\n```\n\nSee [`examples/README.md`](./examples/README.md).\n\n## Scripts\n\n```bash\nbun run build\nbun test\nbun run size          # gzip attribution\nbun run size:brotli   # brotli totals + attribution\n```\n\n## License\n\nMIT\n\n\n\n\n","readmeFilename":"README.md","_rev":"1-c43c109dda91bb3c962d24d3032f9e4c"}