{"_id":"@andykarasek02/batts","_rev":"2-b5b624433c7f6201cabb22bf1e717dfd","name":"@andykarasek02/batts","dist-tags":{"latest":"0.1.1"},"versions":{"0.1.0":{"name":"@andykarasek02/batts","version":"0.1.0","keywords":["zustand","immer","state","store","slices","react","state-management","typescript"],"author":{"name":"Andrew Karasek","email":"hi@andrewkarasek.com"},"license":"MIT","_id":"@andykarasek02/batts@0.1.0","maintainers":[{"name":"andykarasek02","email":"hi@andrewkarasek.com"}],"homepage":"https://github.com/AndrewKarasek/batts#readme","bugs":{"url":"https://github.com/AndrewKarasek/batts/issues"},"dist":{"shasum":"38d49685e4c47abfd20e9c606db23060a237e12a","tarball":"https://registry.npmjs.org/@andykarasek02/batts/-/batts-0.1.0.tgz","fileCount":9,"integrity":"sha512-lKggNS13Jms/FhwtsV1YOCxOO5qkr56KdTO9EZn4jQHawB7YyAVcKmYe8M/V+3NL3oL2W+FCuJ94To7PguvEeA==","signatures":[{"sig":"MEUCIHTW8ezYxr+fjvE6upqe08aCqYJxa9Voor2LIonpFNJXAiEAi3xtWFvjNd3Ivmbwnat8KtCINPr59zeu/OZBk5dpNGw=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":62845},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}},"./package.json":"./package.json"},"gitHead":"37011fb7a5aea6a2c51c498d6f7d5765e1435729","scripts":{"test":"vitest run","build":"tsup","prepack":"npm run build","typecheck":"tsc --noEmit","test:watch":"vitest","prepublishOnly":"npm run typecheck && npm run test"},"_npmUser":{"name":"andykarasek02","email":"hi@andrewkarasek.com"},"repository":{"url":"git+https://github.com/AndrewKarasek/batts.git","type":"git"},"_npmVersion":"11.13.0","description":"Compose small zustand slices into larger stores — standalone, packed, nested, or parented.","directories":{},"_nodeVersion":"24.16.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.5.1","immer":"^11.1.8","react":"^19.2.7","vitest":"^4.1.8","typescript":"^6.0.3"},"peerDependencies":{"immer":"^10.0.0 || ^11.0.0","react":"^18.0.0 || ^19.0.0","zustand":"^5.0.0"},"_npmOperationalInternal":{"tmp":"tmp/batts_0.1.0_1781458856944_0.2512816211487485","host":"s3://npm-registry-packages-npm-production"}},"0.1.1":{"name":"@andykarasek02/batts","version":"0.1.1","description":"Compose small zustand slices into larger stores — standalone, packed, nested, or parented.","keywords":["zustand","immer","state","store","slices","react","state-management","typescript"],"license":"MIT","author":{"name":"Andrew Karasek","email":"hi@andrewkarasek.com"},"homepage":"https://github.com/AndrewKarasek/batts#readme","repository":{"type":"git","url":"git+https://github.com/AndrewKarasek/batts.git"},"bugs":{"url":"https://github.com/AndrewKarasek/batts/issues"},"type":"module","main":"./dist/index.cjs","module":"./dist/index.js","types":"./dist/index.d.ts","exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}},"./package.json":"./package.json"},"publishConfig":{"access":"public"},"scripts":{"build":"tsup","test":"vitest run","test:watch":"vitest","typecheck":"tsc --noEmit","prepublishOnly":"npm run typecheck && npm run test","prepack":"npm run build"},"peerDependencies":{"immer":"^10.0.0 || ^11.0.0","react":"^18.0.0 || ^19.0.0","zustand":"^5.0.0"},"devDependencies":{"immer":"^11.1.8","react":"^19.2.7","tsup":"^8.5.1","typescript":"^6.0.3","vitest":"^4.1.8"},"gitHead":"669db9ca9370bbca54eb97c039b26bac7c963c28","_id":"@andykarasek02/batts@0.1.1","_nodeVersion":"24.16.0","_npmVersion":"11.13.0","dist":{"integrity":"sha512-X2FKyPWbtO81g0QdD7vHXE2si15R1PtMBnkj3u0GupWK6igoXSHOGS6g6ey9ABXrDuRUxztcrsnE48Fplqo1ew==","shasum":"cf49ebcc665f7f37a04bdbde860869efdeecf9a3","tarball":"https://registry.npmjs.org/@andykarasek02/batts/-/batts-0.1.1.tgz","fileCount":9,"unpackedSize":67148,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIQCQ/m8nowBbv3L0H8pqLZQXGwFJPcvOOKodYlBsaNB6kwIgEOp2OPhNBBrC7Knt4DE5N+PnaZJ8jYmpwgUo8Yx/SEw="}]},"_npmUser":{"name":"andykarasek02","email":"hi@andrewkarasek.com"},"directories":{},"maintainers":[{"name":"andykarasek02","email":"hi@andrewkarasek.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/batts_0.1.1_1781461959894_0.05378543836156413"},"_hasShrinkwrap":false}},"time":{"created":"2026-06-14T17:40:56.834Z","modified":"2026-06-14T18:32:40.246Z","0.1.0":"2026-06-14T17:40:57.072Z","0.1.1":"2026-06-14T18:32:40.128Z"},"bugs":{"url":"https://github.com/AndrewKarasek/batts/issues"},"author":{"name":"Andrew Karasek","email":"hi@andrewkarasek.com"},"license":"MIT","homepage":"https://github.com/AndrewKarasek/batts#readme","keywords":["zustand","immer","state","store","slices","react","state-management","typescript"],"repository":{"type":"git","url":"git+https://github.com/AndrewKarasek/batts.git"},"description":"Compose small zustand slices into larger stores — standalone, packed, nested, or parented.","maintainers":[{"name":"andykarasek02","email":"hi@andrewkarasek.com"}],"readme":"# batteries\n\nSmall utilities for building [zustand](https://github.com/pmndrs/zustand) stores out of composable **slices**.\n\nThe idea: keep state in small, self-contained slices that live next to the\ncomponent that owns them. When that state needs to move further up the tree (so\na parent can read or coordinate it), you compose the slices into a larger store\ninstead of rewriting them — the slice keeps working standalone *or* packed.\n\nEvery slice is built on the [immer middleware](https://github.com/pmndrs/zustand#middleware),\nso `set` mutations are written directly (`state.count++`).\n\n## Install\n\n```bash\nnpm install @andykarasek02/batts zustand immer react\n```\n\n`zustand`, `immer`, and `react` are peer dependencies.\n\n## Concepts\n\n### `createSlice(ValuesClass)(methods)`\n\nDefine a slice. State defaults live on a class; methods are a normal zustand\n`(set, get)` creator.\n\n```ts\nimport { batteries } from '@andykarasek02/batts';\n\nclass CounterValues {\n  count = 0;\n}\n\nexport const counterSlice = batteries.createSlice(CounterValues)((set, get) => ({\n  increment: () => set((state) => void state.count++),\n  add: (n: number) => set((state) => void (state.count += n)),\n  double: () => set({ count: get().count * 2 }),\n}));\n```\n\n`createSlice` returns:\n\n| Property | What it is |\n| --- | --- |\n| `counterSlice.use` | A ready-to-use shared store/hook for this slice on its own. |\n| `counterSlice.create(initial?)` | A **fresh** store seeded with `initial` over the class defaults — one per call. |\n| `counterSlice.with(initial)` | A new **slice** pre-seeded with `initial`, for packing (see below). |\n| `counterSlice._slice` | The raw slice creator used when composing into a larger store. |\n\nEvery slice automatically gets a **`reset()`** method that restores the values\nthe store *started* with (defaults plus any seeded overrides).\n\n```ts\n// Use it standalone — e.g. local component state\nconst { count, increment } = counterSlice.use();\n\ncounterSlice.use.getState().add(5);\ncounterSlice.use.getState().reset(); // back to count: 0\n```\n\n### Seeding different initial values\n\nThe class supplies defaults; `create` / `with` seed *different* initial values\nover them — no constructor needed. Pass any subset of the class's fields.\n\n```ts\n// A fresh store per use — e.g. one store per component, pre-filled from props:\nconst store = counterSlice.create({ count: 10 });\nstore.getState().count; // 10\n\n// reset() goes back to the seeded value, not the class default:\nstore.getState().add(5);\nstore.getState().reset();\nstore.getState().count; // 10\n\n// Seed a slice *inside a pack* with `with` (chainable):\nconst packed = batteries.packStore({\n  counter: counterSlice.with({ count: 7 }),\n  user: userSlice.with({ name: 'ada' }),\n});\npacked.getState().slices.counter.count; // 7\n```\n\n`counterSlice.use` remains the shared, zero-config singleton; `create`/`with`\nnever touch it.\n\n### `packStore(sliceMap)`\n\nCombine several slices into one store. Each slice lives under `slices.<key>` and\ncan still only update *itself* — the wiring scopes each slice's `set`/`get` to\nits own corner of the store.\n\n```ts\nconst store = batteries.packStore({\n  counter: counterSlice,\n  user: userSlice,\n});\n\nstore.getState().slices.counter.increment();\nstore.getState().slices.user.rename('ada');\n\nstore.getState().slices.counter.count; // 1\nstore.getState().slices.user.name;     // 'ada'\n```\n\n### `packStore(sliceMap, ParentValuesClass)(parentMethods)` — with a parent\n\nPass a second argument (a parent values class) and `packStore` returns a\nfunction that takes the parent's **methods**. The parent sits alongside the\nslices and can read and write across all of them. Parent values come from a\nclass; parent methods get the full combined store via `set`/`get`.\n\n```ts\nclass ParentValues {\n  label = 'parent';\n}\n\nconst store = batteries.packStore(\n  { counter: counterSlice },\n  ParentValues,\n)((set, get) => ({\n  summary: () => `${get().label} = ${get().slices.counter.count}`,\n  bumpCounter: () =>\n    set((state) => {\n      state.slices.counter.count += 1;\n    }),\n}));\n\nstore.getState().bumpCounter();\nstore.getState().slices.counter.increment(); // children still control themselves\nstore.getState().summary(); // \"parent = 2\"\n```\n\nThe parent form stays curried (two calls) because the parent methods' input\ntype is the whole combined store — which includes the methods' own return type.\nSplitting the call lets TypeScript resolve the store type before inferring your\nmethods. `batteries.packParentStore(sliceMap, ParentValues)(methods)` is kept as\nan explicit alias for the exact same thing.\n\n### `packTuple([sliceA, sliceB] as const)` — positional slices\n\nCompose an ordered **list** of slices instead of a keyed map. Slices live under\nnumeric indices, and — unlike a map — you can include the **same** slice more\nthan once. Pass the list `as const` so each position keeps its own type.\n\n```ts\nconst store = batteries.packTuple([counterSlice, userSlice] as const);\n\nstore.getState().slices[0].increment(); // typed as the counter slice\nstore.getState().slices[1].rename('ada'); // typed as the user slice\n\n// Two independent instances of the same slice:\nconst pair = batteries.packTuple([counterSlice, counterSlice] as const);\npair.getState().slices[0].add(10);\npair.getState().slices[1].count; // still 0\n```\n\n> **Prototype — static only.** The number and order of slices are fixed at pack\n> time. Runtime add/remove (true dynamic collections) needs scoping by a stable\n> id rather than by index; the `select` seam in `scopeLens.ts` is where that\n> change lands.\n\n### `packSlice(sliceMap)` — a reusable, nestable pack\n\n`packSlice` is the composite analog of `createSlice`: it packs a map of slices\ninto a single **reusable slice**, returning the same `{ _slice, use }` shape. So\nthe result works standalone *and* drops into another pack as a slice — letting\nyou **nest a packed store inside a packed store**.\n\n```ts\nconst profile = batteries.packSlice({\n  counter: counterSlice,\n  user: userSlice,\n});\n\n// Standalone — it's a packed store on its own:\nprofile.use.getState().slices.counter.increment();\n\n// …or nested inside a larger store:\nconst root = batteries.packStore({ profile, settings: settingsSlice });\n\nroot.getState().slices.profile.slices.counter.increment();\n//                       ^ nested pack    ^ its child slice\n```\n\nNesting is fully typed and composes to any depth (`packSlice` inside\n`packSlice` inside `packStore`). Scoping still holds: a deeply nested child can\nonly ever write to itself, because each level writes to its own immer draft.\nA nested pack also slots into `packTuple` and the parent form of `packStore`,\nwhich can read and write nested children (`get().slices.profile.slices.counter`).\n\n> Each child keeps its own `reset()`. There's no group-level reset yet — a\n> `reset` that cascades to every child would be a natural addition.\n\n## Typing\n\nYou rarely need to hand-write store types. Two helpers cover the common cases\nand work the same whether or not the store has a parent:\n\n```ts\nimport { batteries, type StoreState, type SliceState } from '@andykarasek02/batts';\n\nconst useStore = batteries.packStore({ counter: counterSlice });\n\ntype Store = StoreState<typeof useStore>;        // { slices: { counter: ... } }\ntype Counter = SliceState<typeof counterSlice>;  // { count, increment, ..., reset }\n```\n\n`StoreState<T>` extracts the state from any store/hook (plain or parent), so\ndownstream code doesn't care which `pack*` produced it.\n\n## Why\n\nCo-locate state with the component that owns it as a slice. If a sibling or\nparent later needs that state, pack the slices upward instead of lifting and\nrewriting the state by hand. The same slice definition powers both the\nstandalone (`.use`) and composed (`packStore` / `packParentStore`) cases.\n\n## Development\n\n```bash\nnpm test          # run the test suite once (vitest)\nnpm run test:watch\nnpm run typecheck # tsc --noEmit\n```\n","readmeFilename":"README.md"}