@mhmo91/schmancy 0.10.11 → 0.10.13

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 CHANGED
@@ -41,17 +41,6 @@ import '@mhmo91/schmancy'
41
41
  import { magnetic, cursorGlow, gravity } from '@mhmo91/schmancy/directives'
42
42
  ```
43
43
 
44
- ## Advanced components — `@mhmo91/schmancy-lab`
45
-
46
- Reusable-but-opinionated components live in a sibling package: QR scanner,
47
- charts, country/timezone selects, map embed. Install separately when you
48
- need them. Lab governance (acceptance criterion + quarterly graduation
49
- clock) lives in [`lab/README.md`](./lab/README.md).
50
-
51
- ```bash
52
- npm install @mhmo91/schmancy @mhmo91/schmancy-lab
53
- ```
54
-
55
44
  ## Use with Claude Code
56
45
 
57
46
  Schmancy ships a Claude Code plugin (manifest at `.claude-plugin/plugin.json`,
@@ -106,6 +95,17 @@ Schmancy is organized in four layers:
106
95
 
107
96
  [Lit](https://lit.dev) · [RxJS](https://rxjs.dev) · [Tailwind CSS v4](https://tailwindcss.com) · [Blackbird](./src/utils/animation.ts)
108
97
 
98
+ ## Advanced components — `@mhmo91/schmancy-lab`
99
+
100
+ Reusable-but-opinionated components live in a sibling package: QR scanner,
101
+ charts, country/timezone selects, map embed. Install separately when you
102
+ need them. Lab governance (acceptance criterion + quarterly graduation
103
+ clock) lives in [`lab/README.md`](./lab/README.md).
104
+
105
+ ```bash
106
+ npm install @mhmo91/schmancy @mhmo91/schmancy-lab
107
+ ```
108
+
109
109
  ## License
110
110
 
111
111
  Apache-2.0
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schemaVersion": "1.0.0",
3
- "readme": "# Schmancy\n\nA Web Component UI library built on Lit, RxJS, and Tailwind CSS. Surfaces are glass. Depth is light. Interactions are physics.\n\n## Agent runtime\n\nFor sandboxed-iframe agents (Claude Design, Claude Artifacts, any LLM that can\nonly write HTML), schmancy ships a single-URL runtime at `@mhmo91/schmancy/agent`.\nDrop one `<script type=\"module\">` tag and every `<schmancy-*>` element is\nregistered. No bundler, no bare specifiers, no npm install.\n\n```html\n<script type=\"module\">\n import 'https://esm.sh/@mhmo91/schmancy/agent';\n</script>\n<schmancy-theme root scheme=\"dark\">\n <schmancy-surface type=\"solid\" fill=\"all\">\n <schmancy-button>Hi</schmancy-button>\n </schmancy-surface>\n</schmancy-theme>\n```\n\nThe same entry re-exports the full library surface for in-page script\ncode (`theme`, `area`, `state`, `show`, `lazy`, every directive, every\nservice, `SchmancyElement`). Import from the same URL.\n\nFor introspection — every tag's attributes, events, slots, CSS parts,\nplus the enum `values` array on every typed attribute (so agents don't\nhave to parse `\"'filled' | 'tonal' | ...\"` strings) — read the static\nmanifest at `@mhmo91/schmancy/agent/manifest`. It's a JSON file, shape\nfollows Custom Elements Manifest v1.\n\n## Install\n\n```bash\nnpm install @mhmo91/schmancy\n```\n\n```typescript\nimport '@mhmo91/schmancy'\nimport { magnetic, cursorGlow, gravity } from '@mhmo91/schmancy/directives'\n```\n\n## Advanced components — `@mhmo91/schmancy-lab`\n\nReusable-but-opinionated components live in a sibling package: QR scanner,\ncharts, country/timezone selects, map embed. Install separately when you\nneed them. Lab governance (acceptance criterion + quarterly graduation\nclock) lives in [`lab/README.md`](./lab/README.md).\n\n```bash\nnpm install @mhmo91/schmancy @mhmo91/schmancy-lab\n```\n\n## Use with Claude Code\n\nSchmancy ships a Claude Code plugin (manifest at `.claude-plugin/plugin.json`,\nskill source under `skills/schmancy/`). The npm tarball includes both, so\nafter `npm install @mhmo91/schmancy`, point Claude at the package directory\nwhen launching:\n\n```\nclaude --plugin-dir node_modules/@mhmo91/schmancy\n```\n\nSet a shell alias / `.envrc` so every session in the project picks it up.\nAfter editing plugin files, run `/reload-plugins` inside the session.\n\nClaude now knows every Schmancy component, foundation pattern, and\nconvention; the skill activates automatically when you work on schmancy\ncode — no CLAUDE.md edits, no symlinks.\n\n## Quick Start\n\n```html\n<schmancy-theme root scheme=\"dark\">\n <schmancy-surface type=\"solid\" fill=\"all\">\n <schmancy-area name=\"root\" .default=${lazy(() => import('./home.page'))}>\n <schmancy-route when=\"home-page\" .component=${lazy(() => import('./home.page'))} />\n </schmancy-area>\n </schmancy-surface>\n</schmancy-theme>\n```\n\n## Design: Luminous Glass\n\n| Surface | Opacity | Blur | Purpose |\n|---------|---------|------|---------|\n| `solid` | 92% | — | Dense glass, high readability |\n| `subtle` | 78% | 8px | Frosted panel (default) |\n| `glass` | 55% | 16px | Overlays, dialogs, dropdowns |\n| `luminous` | 42% | 20px | Hero panels with glow halo |\n\n## Docs\n\nSchmancy is organized in four layers:\n\n- **Foundations** — [Area](./skills/schmancy/area.md) · [State](./skills/schmancy/state.md) · [Mixins (SchmancyElement)](./skills/schmancy/mixins.md) · [Theme](./skills/schmancy/theme.md) · [Directives](./skills/schmancy/directives.md)\n- **Atoms** — [Typography](./skills/schmancy/typography.md) · [Icons](./skills/schmancy/icons.md) · [Button](./skills/schmancy/button.md) · [Surface](./skills/schmancy/surface.md) · [Divider](./skills/schmancy/divider.md) · [Avatar](./skills/schmancy/avatar.md)\n- **Composites (by job)** — Forms, Navigation, Overlays, Interaction, Feedback, Display\n- **Utilities** — [Animation](./skills/schmancy/animation.md) · [Audio](./skills/schmancy/audio.md) · [Discovery](./skills/schmancy/discovery.md) · [RxJS Utils](./skills/schmancy/rxjs-utils.md) · [Utils](./skills/schmancy/utils.md)\n\n**Full component index:** [skills/schmancy/INDEX.md](./skills/schmancy/INDEX.md) — the single-file map with every tag, service, and convention. Written primarily for AI agents; humans welcome.\n\n## Tech Stack\n\n[Lit](https://lit.dev) · [RxJS](https://rxjs.dev) · [Tailwind CSS v4](https://tailwindcss.com) · [Blackbird](./src/utils/animation.ts)\n\n## License\n\nApache-2.0\n",
3
+ "readme": "# Schmancy\n\nA Web Component UI library built on Lit, RxJS, and Tailwind CSS. Surfaces are glass. Depth is light. Interactions are physics.\n\n## Agent runtime\n\nFor sandboxed-iframe agents (Claude Design, Claude Artifacts, any LLM that can\nonly write HTML), schmancy ships a single-URL runtime at `@mhmo91/schmancy/agent`.\nDrop one `<script type=\"module\">` tag and every `<schmancy-*>` element is\nregistered. No bundler, no bare specifiers, no npm install.\n\n```html\n<script type=\"module\">\n import 'https://esm.sh/@mhmo91/schmancy/agent';\n</script>\n<schmancy-theme root scheme=\"dark\">\n <schmancy-surface type=\"solid\" fill=\"all\">\n <schmancy-button>Hi</schmancy-button>\n </schmancy-surface>\n</schmancy-theme>\n```\n\nThe same entry re-exports the full library surface for in-page script\ncode (`theme`, `area`, `state`, `show`, `lazy`, every directive, every\nservice, `SchmancyElement`). Import from the same URL.\n\nFor introspection — every tag's attributes, events, slots, CSS parts,\nplus the enum `values` array on every typed attribute (so agents don't\nhave to parse `\"'filled' | 'tonal' | ...\"` strings) — read the static\nmanifest at `@mhmo91/schmancy/agent/manifest`. It's a JSON file, shape\nfollows Custom Elements Manifest v1.\n\n## Install\n\n```bash\nnpm install @mhmo91/schmancy\n```\n\n```typescript\nimport '@mhmo91/schmancy'\nimport { magnetic, cursorGlow, gravity } from '@mhmo91/schmancy/directives'\n```\n\n## Use with Claude Code\n\nSchmancy ships a Claude Code plugin (manifest at `.claude-plugin/plugin.json`,\nskill source under `skills/schmancy/`). The npm tarball includes both, so\nafter `npm install @mhmo91/schmancy`, point Claude at the package directory\nwhen launching:\n\n```\nclaude --plugin-dir node_modules/@mhmo91/schmancy\n```\n\nSet a shell alias / `.envrc` so every session in the project picks it up.\nAfter editing plugin files, run `/reload-plugins` inside the session.\n\nClaude now knows every Schmancy component, foundation pattern, and\nconvention; the skill activates automatically when you work on schmancy\ncode — no CLAUDE.md edits, no symlinks.\n\n## Quick Start\n\n```html\n<schmancy-theme root scheme=\"dark\">\n <schmancy-surface type=\"solid\" fill=\"all\">\n <schmancy-area name=\"root\" .default=${lazy(() => import('./home.page'))}>\n <schmancy-route when=\"home-page\" .component=${lazy(() => import('./home.page'))} />\n </schmancy-area>\n </schmancy-surface>\n</schmancy-theme>\n```\n\n## Design: Luminous Glass\n\n| Surface | Opacity | Blur | Purpose |\n|---------|---------|------|---------|\n| `solid` | 92% | — | Dense glass, high readability |\n| `subtle` | 78% | 8px | Frosted panel (default) |\n| `glass` | 55% | 16px | Overlays, dialogs, dropdowns |\n| `luminous` | 42% | 20px | Hero panels with glow halo |\n\n## Docs\n\nSchmancy is organized in four layers:\n\n- **Foundations** — [Area](./skills/schmancy/area.md) · [State](./skills/schmancy/state.md) · [Mixins (SchmancyElement)](./skills/schmancy/mixins.md) · [Theme](./skills/schmancy/theme.md) · [Directives](./skills/schmancy/directives.md)\n- **Atoms** — [Typography](./skills/schmancy/typography.md) · [Icons](./skills/schmancy/icons.md) · [Button](./skills/schmancy/button.md) · [Surface](./skills/schmancy/surface.md) · [Divider](./skills/schmancy/divider.md) · [Avatar](./skills/schmancy/avatar.md)\n- **Composites (by job)** — Forms, Navigation, Overlays, Interaction, Feedback, Display\n- **Utilities** — [Animation](./skills/schmancy/animation.md) · [Audio](./skills/schmancy/audio.md) · [Discovery](./skills/schmancy/discovery.md) · [RxJS Utils](./skills/schmancy/rxjs-utils.md) · [Utils](./skills/schmancy/utils.md)\n\n**Full component index:** [skills/schmancy/INDEX.md](./skills/schmancy/INDEX.md) — the single-file map with every tag, service, and convention. Written primarily for AI agents; humans welcome.\n\n## Tech Stack\n\n[Lit](https://lit.dev) · [RxJS](https://rxjs.dev) · [Tailwind CSS v4](https://tailwindcss.com) · [Blackbird](./src/utils/animation.ts)\n\n## Advanced components — `@mhmo91/schmancy-lab`\n\nReusable-but-opinionated components live in a sibling package: QR scanner,\ncharts, country/timezone selects, map embed. Install separately when you\nneed them. Lab governance (acceptance criterion + quarterly graduation\nclock) lives in [`lab/README.md`](./lab/README.md).\n\n```bash\nnpm install @mhmo91/schmancy @mhmo91/schmancy-lab\n```\n\n## License\n\nApache-2.0\n",
4
4
  "modules": [
5
5
  {
6
6
  "kind": "javascript-module",
@@ -203,7 +203,7 @@ The manifest already has everything needed; the package is just the JSON-RPC wra
203
203
 
204
204
  **Problem.** `handover/agent-runtime-v1.md` had `<PENDING>` placeholders that we manually replaced with `0.9.13` after the first publish. Future handover docs will have the same issue.
205
205
 
206
- **Fix.** A build step that substitutes `0.10.11` in `handover/**/*.md` against `package.json`'s `version` field on every build. `dist/handover/**/*.md` gets the rendered version; the source stays templated.
206
+ **Fix.** A build step that substitutes `0.10.13` in `handover/**/*.md` against `package.json`'s `version` field on every build. `dist/handover/**/*.md` gets the rendered version; the source stays templated.
207
207
 
208
208
  **Effort:** ~30 min (one sed step or a tiny script).
209
209
 
@@ -7,8 +7,8 @@
7
7
  ## The URLs you asked for
8
8
 
9
9
  ```
10
- https://esm.sh/@mhmo91/schmancy/agent@0.10.11
11
- https://esm.sh/@mhmo91/schmancy/agent/manifest@0.10.11
10
+ https://esm.sh/@mhmo91/schmancy/agent@0.10.13
11
+ https://esm.sh/@mhmo91/schmancy/agent/manifest@0.10.13
12
12
  ```
13
13
 
14
14
  `0.9.13` is the first release containing `/agent`; every subsequent publish serves the same subpath. `npm view @mhmo91/schmancy version` always returns the current pin if you want to float forward.
@@ -20,7 +20,7 @@ One script tag. No bundler, no bare specifiers, no npm install.
20
20
  ```html
21
21
  <!doctype html>
22
22
  <script type="module">
23
- import { $dialog, theme } from 'https://esm.sh/@mhmo91/schmancy/agent@0.10.11';
23
+ import { $dialog, theme } from 'https://esm.sh/@mhmo91/schmancy/agent@0.10.13';
24
24
  </script>
25
25
  <schmancy-theme root scheme="dark">
26
26
  <schmancy-surface type="solid" fill="all">
@@ -4,10 +4,6 @@ Reactive state primitive. Module-scoped singletons keyed by namespace.
4
4
  Every state exposes a TC39 signal, an RxJS Observable, a sync getter,
5
5
  and a hydration promise — pick the surface that fits the call site.
6
6
 
7
- This module replaced the v1 `createContext` / `@select` /
8
- `createCompoundSelector` system. There is no parallel API; use `state()`
9
- for everything.
10
-
11
7
  ## Quick reference
12
8
 
13
9
  ```ts
@@ -477,28 +473,6 @@ callbacks have no tree position to resolve to.
477
473
  States not listed in `provides` fall through to the global from inside
478
474
  the element — useful when only one or two namespaces need scoping.
479
475
 
480
- ## Migration from v1 `createContext`
481
-
482
- ```ts
483
- // v1
484
- const cart = createContext<CartState>(initial, 'session', 'hannah_cart')
485
- class CartView extends LitElement {
486
- @select(cart) accessor cart!: CartState
487
- }
488
- const total = createCompoundSelector([cart], [c => c.total], t => t)
489
-
490
- // v2
491
- const cart = state<CartState>('hannah/cart').session(initial)
492
- class CartView extends LitElement {
493
- @observe(cart) cart!: CartState // or: cart = bindState(this, cart)
494
- }
495
- const total = computed(() => cart.value.total)
496
- ```
497
-
498
- Surface-equivalent: `.value`, `.set(partial)`, `.replace(next)`, `.$`,
499
- `.delete(key)` all map directly. Storage keys move from arbitrary
500
- strings (`'hannah_cart'`) to namespace strings (`'hannah/cart'`).
501
-
502
476
  ## Rules
503
477
 
504
478
  - **One state per namespace.** The factory throws on a duplicate
@@ -4,10 +4,6 @@ Reactive state primitive. Module-scoped singletons keyed by namespace.
4
4
  Every state exposes a TC39 signal, an RxJS Observable, a sync getter,
5
5
  and a hydration promise — pick the surface that fits the call site.
6
6
 
7
- This module replaced the v1 `createContext` / `@select` /
8
- `createCompoundSelector` system. There is no parallel API; use `state()`
9
- for everything.
10
-
11
7
  ## Quick reference
12
8
 
13
9
  ```ts
@@ -477,28 +473,6 @@ callbacks have no tree position to resolve to.
477
473
  States not listed in `provides` fall through to the global from inside
478
474
  the element — useful when only one or two namespaces need scoping.
479
475
 
480
- ## Migration from v1 `createContext`
481
-
482
- ```ts
483
- // v1
484
- const cart = createContext<CartState>(initial, 'session', 'hannah_cart')
485
- class CartView extends LitElement {
486
- @select(cart) accessor cart!: CartState
487
- }
488
- const total = createCompoundSelector([cart], [c => c.total], t => t)
489
-
490
- // v2
491
- const cart = state<CartState>('hannah/cart').session(initial)
492
- class CartView extends LitElement {
493
- @observe(cart) cart!: CartState // or: cart = bindState(this, cart)
494
- }
495
- const total = computed(() => cart.value.total)
496
- ```
497
-
498
- Surface-equivalent: `.value`, `.set(partial)`, `.replace(next)`, `.$`,
499
- `.delete(key)` all map directly. Storage keys move from arbitrary
500
- strings (`'hannah_cart'`) to namespace strings (`'hannah/cart'`).
501
-
502
476
  ## Rules
503
477
 
504
478
  - **One state per namespace.** The factory throws on a duplicate
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mhmo91/schmancy",
3
- "version": "0.10.11",
3
+ "version": "0.10.13",
4
4
  "description": "UI library build with web components",
5
5
  "main": "./dist/index.js",
6
6
  "customElements": "custom-elements.json",
@@ -4,10 +4,6 @@ Reactive state primitive. Module-scoped singletons keyed by namespace.
4
4
  Every state exposes a TC39 signal, an RxJS Observable, a sync getter,
5
5
  and a hydration promise — pick the surface that fits the call site.
6
6
 
7
- This module replaced the v1 `createContext` / `@select` /
8
- `createCompoundSelector` system. There is no parallel API; use `state()`
9
- for everything.
10
-
11
7
  ## Quick reference
12
8
 
13
9
  ```ts
@@ -477,28 +473,6 @@ callbacks have no tree position to resolve to.
477
473
  States not listed in `provides` fall through to the global from inside
478
474
  the element — useful when only one or two namespaces need scoping.
479
475
 
480
- ## Migration from v1 `createContext`
481
-
482
- ```ts
483
- // v1
484
- const cart = createContext<CartState>(initial, 'session', 'hannah_cart')
485
- class CartView extends LitElement {
486
- @select(cart) accessor cart!: CartState
487
- }
488
- const total = createCompoundSelector([cart], [c => c.total], t => t)
489
-
490
- // v2
491
- const cart = state<CartState>('hannah/cart').session(initial)
492
- class CartView extends LitElement {
493
- @observe(cart) cart!: CartState // or: cart = bindState(this, cart)
494
- }
495
- const total = computed(() => cart.value.total)
496
- ```
497
-
498
- Surface-equivalent: `.value`, `.set(partial)`, `.replace(next)`, `.$`,
499
- `.delete(key)` all map directly. Storage keys move from arbitrary
500
- strings (`'hannah_cart'`) to namespace strings (`'hannah/cart'`).
501
-
502
476
  ## Rules
503
477
 
504
478
  - **One state per namespace.** The factory throws on a duplicate
package/src/CLAUDE.md CHANGED
@@ -144,10 +144,7 @@ protected static shadowRootOptions = {
144
144
  Module-scoped reactive state lives on `state(...)` from
145
145
  `@mhmo91/schmancy/state` — see `skills/schmancy/state.md`. Inside a
146
146
  component instance, use `@state` (Lit) for private template-driving
147
- fields and `@property` for public attributes. There is no
148
- `createContext` / `@select` / `createCompoundSelector`; those v1 APIs
149
- were removed and replaced wholesale by `state()` / `@observe` /
150
- `computed`. The migration cheatsheet is `src/state/MIGRATION.md`.
147
+ fields and `@property` for public attributes.
151
148
 
152
149
  ### Theme consumption
153
150
 
@@ -182,5 +179,4 @@ public reportValidity(): boolean {
182
179
  ## Pointers
183
180
 
184
181
  - **State module brief:** `src/state/CLAUDE.md` — invariants for code under `src/state/`.
185
- - **Migration off v1 contexts:** `src/state/MIGRATION.md`.
186
182
  - **Lab acceptance criterion:** `lab/README.md` — what does and doesn't belong in `@mhmo91/schmancy-lab`.
@@ -165,5 +165,3 @@ before the first `await`, or chain explicitly with `.then(...)`.
165
165
  `packages/schmancy/_scratch/spikes/findings.md` (gitignored).
166
166
  - **Skill doc for downstream consumers:**
167
167
  `packages/schmancy/skills/schmancy/state.md`.
168
- - **Migration cheatsheet** for moving off the v1 `createContext`:
169
- `packages/schmancy/src/state/MIGRATION.md`.
@@ -1,258 +0,0 @@
1
- # Migrating from `createContext` to `state()`
2
-
3
- Cheatsheet for moving call sites off the v1 `createContext` /
4
- `@select` / `createCompoundSelector` / `selectItem` family to
5
- `@mhmo91/schmancy/state`.
6
-
7
- The v1 surface has been deleted from `@mhmo91/schmancy`. There is no
8
- parallel API; every consumer needs to switch.
9
-
10
- ## Imports
11
-
12
- ```ts
13
- // Before
14
- import { createContext, select, selectItem, createCompoundSelector } from '@mhmo91/schmancy'
15
-
16
- // After
17
- import { state, computed, observe, bindState, stateFromObservable } from '@mhmo91/schmancy/state'
18
- ```
19
-
20
- ## Defining a state
21
-
22
- | v1 | v2 |
23
- |---|---|
24
- | `createContext<T>(initial, 'memory', 'foo')` | `state<T>('feature/foo').memory(initial)` |
25
- | `createContext<T>(initial, 'session', 'foo')` | `state<T>('feature/foo').session(initial)` |
26
- | `createContext<T>(initial, 'local', 'foo')` | `state<T>('feature/foo').local(initial)` |
27
- | `createContext<T>(initial, 'indexeddb', 'foo')` | `state<T>('feature/foo').idb(initial)` |
28
-
29
- The string namespace replaces the v1 third-argument storage key. It
30
- must contain a `/` (compile-time enforced via the
31
- `${string}/${string}` template literal). Storage is keyed off the
32
- namespace string.
33
-
34
- If your file already declares the initial value in a typed const, drop
35
- the type arg:
36
-
37
- ```ts
38
- const initial: CartState = { items: [], total: 0 }
39
- const cart = state('hannah/cart').session(initial) // T inferred from `initial`
40
- ```
41
-
42
- ## Reading
43
-
44
- | v1 | v2 |
45
- |---|---|
46
- | `cart.value` | `cart.value` (unchanged) |
47
- | `cart.$.subscribe(...)` | `cart.$.subscribe(...)` (unchanged) |
48
- | `cart.ready` (boolean) | `cart.loaded` (boolean) + `await cart.ready` (Promise<void>) |
49
-
50
- `cart.ready` is now a Promise that resolves when initial load completes
51
- (success or fallback). `cart.loaded` is the boolean runtime flag. Use
52
- whichever fits the call site.
53
-
54
- ## Writing
55
-
56
- | v1 | v2 |
57
- |---|---|
58
- | `cart.set({ total: 12 })` | `cart.set({ total: 12 })` (unchanged) |
59
- | `cart.set({ items: [] }, false)` | `cart.set({ items: [] }, false)` (unchanged) |
60
- | `cart.replace(next)` | `cart.replace(next)` (unchanged) |
61
- | `cart.delete('total')` | `cart.delete('total')` (unchanged) |
62
- | Manual `replace({ ...current, ...patch })` cascade | `cart.update(d => { … })` — immer recipe, single notification |
63
-
64
- For `Map<K, V>` shapes:
65
-
66
- | v1 | v2 |
67
- |---|---|
68
- | `docs.set('id', doc)` | `docs.set('id', doc)` (unchanged — MapAPI dispatch) |
69
-
70
- For `Set<U>`:
71
-
72
- | v1 | v2 |
73
- |---|---|
74
- | Manual `replace(new Set([...current, item]))` | `sel.add('item')` |
75
- | Manual replace-without-item | `sel.delete('item')` (returns boolean) |
76
- | | `sel.toggle('item')` (new) |
77
-
78
- For arrays:
79
-
80
- | v1 | v2 |
81
- |---|---|
82
- | `array.push(...items)` | `array.push(...items)` (unchanged) |
83
- | Manual `replace(current.filter(...))` | `array.update(d => d.splice(...))` (immer) |
84
-
85
- For nullable references and primitives:
86
-
87
- | v1 | v2 |
88
- |---|---|
89
- | `editing.set({ id: 'x' })` then `editing.set(null)` | Same (ScalarAPI accepts the union directly) |
90
-
91
- ## Subscribing in a component
92
-
93
- The v1 `@select` decorator is gone. Three options in v2, in order of
94
- preference:
95
-
96
- ### (1) Default — direct read in render (zero ceremony)
97
-
98
- `$LitElement()` composes `SignalWatcher`, so signal reads in `render()`
99
- auto-track. Just import the state and use it inline:
100
-
101
- ```ts
102
- @customElement('cart-view')
103
- class CartView extends $LitElement() {
104
- render() { return html`Items: ${cart.value.items.length}` }
105
- }
106
- ```
107
-
108
- No decorator, no field, no binding code. The imported `cart` singleton
109
- IS the binding.
110
-
111
- ### (2) `@observe(source)` — when you need a class field
112
-
113
- ```ts
114
- class CartView extends $LitElement() {
115
- @observe(cart) cart!: CartState
116
-
117
- onClick() {
118
- console.log(this.cart) // event handler needs `this.cart`
119
- }
120
-
121
- render() {
122
- return html`Items: ${this.cart.items.length}`
123
- }
124
- }
125
- ```
126
-
127
- Reads return the latest value. Caller writes are dropped with a dev
128
- warning. Same decorator shape as `@property` — works under the existing
129
- tsconfig.
130
-
131
- ### (3) `bindState(host, source)` — for hosts that aren't `$LitElement`
132
-
133
- ```ts
134
- class CustomHost extends LitElement {
135
- cart = bindState(this, cart)
136
- render() { return html`Items: ${this.cart.value.items.length}` }
137
- }
138
- ```
139
-
140
- ## Compound selectors → `computed`
141
-
142
- ```ts
143
- // Before
144
- const cartItemCount = createCompoundSelector(
145
- [cartContext],
146
- [cart => cart.items.length],
147
- count => count,
148
- )
149
-
150
- // After
151
- import { computed } from '@mhmo91/schmancy/state'
152
- const cartItemCount = computed(() => cart.value.items.length)
153
- ```
154
-
155
- Cross-state composition just works:
156
-
157
- ```ts
158
- const orderTotal = computed(() => cart.value.subtotal + tip.value.amount)
159
- ```
160
-
161
- Reading any `state.value` inside `computed(fn)` auto-tracks. No
162
- explicit dependency array.
163
-
164
- ## Side effects → `effect`
165
-
166
- ```ts
167
- import { effect } from '@mhmo91/schmancy/state'
168
-
169
- const stop = effect(() => {
170
- document.title = `${cart.value.items.length} items`
171
- })
172
-
173
- // later, when no longer needed:
174
- stop[Symbol.dispose]()
175
- ```
176
-
177
- Eager run + microtask-coalesced re-runs. Returns a `Disposable`.
178
-
179
- ## Observable bridges
180
-
181
- ```ts
182
- // Before
183
- const userPresence$ = new BehaviorSubject({ online: false, since: 0 })
184
- // (read with .value, subscribe with .$, no persistence story)
185
-
186
- // After
187
- import { stateFromObservable } from '@mhmo91/schmancy/state'
188
-
189
- const userPresence = stateFromObservable(
190
- presence$, // Observable<PresenceState>
191
- 'app/presence',
192
- { online: false, since: 0 },
193
- )
194
- // userPresence has .value, .$, all variant API methods, lifecycle.
195
- ```
196
-
197
- ## Lifecycle / disposal
198
-
199
- ```ts
200
- // Module scope — same as v1
201
- export const cart = state('hannah/cart').session(initial)
202
-
203
- // Test scope — using
204
- it('cart updates total on add', () => {
205
- using cart = state('test/cart').memory(initial)
206
- cart.update(d => { d.items.push(item) })
207
- expect(cart.value.total).toBe(item.price)
208
- }) // [Symbol.dispose] runs here, even on assertion failure
209
-
210
- // IDB-backed in tests — await using flushes pending writes
211
- it('persists across reloads', async () => {
212
- await using cart = state('test/cart').idb(initial)
213
- // ...
214
- })
215
-
216
- // Imperative cleanup (samwa back-compat alias)
217
- cart.destroy()
218
- ```
219
-
220
- ## Things that went away
221
-
222
- - **`createCompoundSelector`** — use `computed()`.
223
- - **`@select` and `@selectItem` decorators** — use direct render reads
224
- via `$LitElement` (default), `@observe` (field-level), or `bindState`
225
- (non-Lit hosts).
226
- - **`IStore` / `ICollectionStore` / `IArrayStore` interfaces** — no
227
- longer needed. The variant write API dispatch is type-level.
228
- - **`error$` per-store Subject** — errors flow through console + the
229
- `StateStorageError` thrown from `save()` for IDB write failures.
230
- - **Tree-scoped `<state-provider>`** — replaced by
231
- `<schmancy-context provides={[…]}>`. Same intent (per-subtree
232
- isolation of a state), different API: in v1 the provider was a
233
- primitive; in v2 the state surface stays unchanged and the
234
- `<schmancy-context>` element is what scopes a subtree. Consumer
235
- code (`cart.value`, `cart.set(...)`) is identical inside and
236
- outside the element. See `SCOPING.md` for the details.
237
-
238
- ## Footguns
239
-
240
- - **Don't use `using` at module scope.** It disposes on module GC,
241
- which is roughly never until the tab closes. Plain `const` for
242
- module singletons, `using` for test/function scope only.
243
- - **Namespaces always contain `/`.** `state('cart')` is a TypeError;
244
- use `state('feature/cart')`.
245
- - **Don't `state.signal.set(...)` directly.** Go through the variant
246
- API so write-coalescing and persistence stay coherent.
247
- - **Inline literals without a typed const narrow T.**
248
- `state('app/x').memory({ items: [], total: 0 })` infers
249
- `{ items: never[]; total: number }`. Either use a typed const first
250
- or pass the type arg: `state<CartState>('app/x').memory({...})`.
251
-
252
- ## Pointers
253
-
254
- - Skill / API reference: `packages/schmancy/skills/schmancy/state.md`
255
- - Agent brief: `packages/schmancy/src/state/CLAUDE.md`
256
- - Tree-scoping reference: `packages/schmancy/src/state/SCOPING.md`
257
- - Original plan: `~/.claude/plans/indexed-twirling-stroustrup.md`
258
- - Scoping plan: `~/.claude/plans/federated-petting-penguin.md`