@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 +11 -11
- package/dist/agent/schmancy.manifest.json +1 -1
- package/dist/handover/agent-runtime-followups.md +1 -1
- package/dist/handover/agent-runtime-v1.md +3 -3
- package/dist/skills/schmancy/state.md +0 -26
- package/dist/skills/state.md +0 -26
- package/package.json +1 -1
- package/skills/schmancy/state.md +0 -26
- package/src/CLAUDE.md +1 -5
- package/src/state/CLAUDE.md +0 -2
- package/src/state/MIGRATION.md +0 -258
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##
|
|
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.
|
|
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
|
-
https://esm.sh/@mhmo91/schmancy/agent/manifest@0.10.
|
|
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.
|
|
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
|
package/dist/skills/state.md
CHANGED
|
@@ -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
package/skills/schmancy/state.md
CHANGED
|
@@ -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.
|
|
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`.
|
package/src/state/CLAUDE.md
CHANGED
|
@@ -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`.
|
package/src/state/MIGRATION.md
DELETED
|
@@ -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`
|