@mhmo91/schmancy 0.10.9 → 0.10.11

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mhmo91/schmancy",
3
- "version": "0.10.9",
3
+ "version": "0.10.11",
4
4
  "description": "UI library build with web components",
5
5
  "main": "./dist/index.js",
6
6
  "customElements": "custom-elements.json",
@@ -8,7 +8,7 @@ The framework pieces — touch before components.
8
8
 
9
9
  - [Area](./area.md) — `<schmancy-area>`, `<schmancy-route>`, `area.push()`, `lazy()` for routing.
10
10
  - [State](./state.md) — `state()` factory (memory / session / local / idb), variant write APIs (`Object` / `Map` / `Set` / `Array` / `Scalar`), `bindState`, `computed`, `stateFromObservable`.
11
- - [Mixins](./mixins.md) — `$LitElement` base class.
11
+ - [Mixins](./mixins.md) — `SchmancyElement` base class.
12
12
  - [Theme](./theme.md) — `<schmancy-theme>`, color scheme, CSS variables.
13
13
  - [Directives](./directives.md) — Lit directives for physics, effects, text, visibility, interaction.
14
14
  - [Animation](./animation.md) — Spring presets (`SPRING_SMOOTH`, etc.), `createAnimation`.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: schmancy
3
- description: UI patterns, component APIs, and conventions for the @mhmo91/schmancy web-component library (Lit + RxJS + Tailwind) — the exclusive UI stack in this repo's `web/` workspace. Fire this skill on ANY web-UI work, even when the user doesn't name schmancy explicitly — including adding or editing a component, building a form, showing a dialog / toast / side drawer / bottom sheet, wiring routing, reading or writing a context, styling with theme tokens, adding a drop zone / file input / date picker / autocomplete, working with `$LitElement`, or touching any `<schmancy-*>` tag. Also fire on prompts like "build a page", "add a modal", "wire a route", "save user prefs in storage", "animate this", "style with our theme", "make a notification", "how do I do X in Lit", "my drag-and-drop", "dark mode toggle".
3
+ description: UI patterns, component APIs, and conventions for the @mhmo91/schmancy web-component library (Lit + RxJS + Tailwind) — the exclusive UI stack in this repo's `web/` workspace. Fire this skill on ANY web-UI work, even when the user doesn't name schmancy explicitly — including adding or editing a component, building a form, showing a dialog / toast / side drawer / bottom sheet, wiring routing, reading or writing a state, styling with theme tokens, adding a drop zone / file input / date picker / autocomplete, working with `SchmancyElement`, or touching any `<schmancy-*>` tag. Also fire on prompts like "build a page", "add a modal", "wire a route", "save user prefs in storage", "animate this", "style with our theme", "make a notification", "how do I do X in Lit", "my drag-and-drop", "dark mode toggle".
4
4
  ---
5
5
 
6
6
  # Schmancy
@@ -56,19 +56,20 @@ Use component tags (`<schmancy-menu>`, `<schmancy-dropdown>`, `<schmancy-tooltip
56
56
  Remediation: before reporting any file compliant, run a grep against each forbidden pattern named by the rule under audit (e.g. `grep -nE '\[(?:[^]]+)\]' <file>` for `TOKEN_FIRST_NO_ARBITRARY`; `grep -nE 'class="[^"]*\b(text-|bg-|border-|rounded-|shadow-|tracking-|leading-|p[xy]?-)' <file>` plus a manual scan for raw `<div>`/`<span>` carrying those classes for `PRIMITIVE_FIRST`). The compliance report's body is the concatenation of those grep outputs annotated with their rule name; an empty body across every pattern under every rule is the only form of "compliant". A claim of compliance unaccompanied by the grep evidence is not a compliance report and is treated as an unverified assertion that the audit loop rejects.
57
57
  - **Schmancy primitive first** (`PRIMITIVE_FIRST`). Within `web/**`, every visible UI element is a custom element exported from `packages/schmancy/src/**`, and an element absent from that export set is added there before being imported into `web/**`. The rule sits above the styling rules: a `<div class="text-xs text-surface-on-variant">…</div>` whose role is typography is a violation even when every utility resolves to a registered token, because `<schmancy-typography>` already covers that role; the styling rules apply only to whatever class strings remain after the right primitive has been selected.
58
58
  Sources: [packages/schmancy/skills/schmancy/INDEX.md](../INDEX.md) catalogues the export set by job (foundations / atoms / forms / navigation / overlays / interaction / feedback / display); each role's reference file (`typography.md`, `surface.md`, `button.md`, `overlay.md`, …) names the props, slots, and events that displace the equivalent `<div>` + utility-class pattern. The export set is the single source — a primitive that is not exported from `packages/schmancy/src/**` does not satisfy this rule even if it lives in a private file inside the schmancy tree.
59
- Remediation: walk every `.ts` and `.html` file under `web/**` and list every raw HTML element whose class string carries design-system styling (typography, color, spacing-as-design-decision, surface, layout-as-design-decision, motion, overlay) — those are the violations. For each, look up the matching schmancy primitive in `INDEX.md` and rewrite the element through that primitive (`<schmancy-typography type=… token=…>` for type-scale text, `<schmancy-surface type=… fill=…>` for elevated/bounded surfaces, `<schmancy-grid>`/`<schmancy-flex>` for layout primitives with design intent, the imperative `show`/`$notify`/`schmancyContentDrawer.push` services for overlays, `<schmancy-scroll>` for scroll containers). When a needed primitive is absent from the export set, design and implement it as a new component under `packages/schmancy/src/<role>/` — extending `$LitElement(style?)`, registered in `HTMLElementTagNameMap`, exported through the package barrel, and documented with a sibling `.md` in the skill's reference set — and only then introduce the first call site in `web/**`. The audit subagent iterates the whole `web/**` tree, surfaces the violation list, applies the rewrites, runs `yarn workspace @momo/web tsc --noEmit` plus the colocated `*-view.test.ts` suites, and reports pre-existing violations that require a new schmancy primitive as a separate punch list for designer/architect approval before the implementation lands. The loop exits when every `web/**` file's visible UI elements are schmancy primitives and the typecheck plus the test suites pass.
59
+ Remediation: walk every `.ts` and `.html` file under `web/**` and list every raw HTML element whose class string carries design-system styling (typography, color, spacing-as-design-decision, surface, layout-as-design-decision, motion, overlay) — those are the violations. For each, look up the matching schmancy primitive in `INDEX.md` and rewrite the element through that primitive (`<schmancy-typography type=… token=…>` for type-scale text, `<schmancy-surface type=… fill=…>` for elevated/bounded surfaces, `<schmancy-grid>`/`<schmancy-flex>` for layout primitives with design intent, the imperative `show`/`$notify`/`schmancyContentDrawer.push` services for overlays, `<schmancy-scroll>` for scroll containers). When a needed primitive is absent from the export set, design and implement it as a new component under `packages/schmancy/src/<role>/` — extending `SchmancyElement` with `static styles = [css\`...\`]`, registered in `HTMLElementTagNameMap`, exported through the package barrel, and documented with a sibling `.md` in the skill's reference set — and only then introduce the first call site in `web/**`. The audit subagent iterates the whole `web/**` tree, surfaces the violation list, applies the rewrites, runs `yarn workspace @momo/web tsc --noEmit` plus the colocated `*-view.test.ts` suites, and reports pre-existing violations that require a new schmancy primitive as a separate punch list for designer/architect approval before the implementation lands. The loop exits when every `web/**` file's visible UI elements are schmancy primitives and the typecheck plus the test suites pass.
60
60
 
61
61
  ## Non-negotiable conventions
62
62
 
63
63
  **Component authoring**
64
- - Every component extends `SchmancyElement` and declares its component-local CSS via `static styles = [css\`...\`]`. Never raw `LitElement`. Never wrap with `SignalWatcher` — the base already includes it; double-wrapping creates two nested Computeds and panics with "Detected cycle in computations" at runtime. The deprecated `$LitElement(style?)` factory still works (it now just delegates to SchmancyElement) but should not appear in new code.
64
+ - Every component extends `SchmancyElement` and declares its component-local CSS via `static styles = [css\`...\`]`. Never raw `LitElement`. Never wrap with `SignalWatcher` — the base already includes it; double-wrapping creates two nested Computeds and panics with "Detected cycle in computations" at runtime.
65
65
  - Every RxJS subscription ends with `.pipe(takeUntil(this.disconnecting))`.
66
66
  - Register the tag in `HTMLElementTagNameMap` for TypeScript.
67
67
 
68
68
  **State**
69
- - Contexts live at module scope. Many small contexts beat one monolith.
70
- - Gate subscriptions with `filter(() => ctx.ready)` when reading persisted contexts.
71
- - Storage tiers: `'memory'` (regenerable) · `'session'` (per-tab) · `'local'` (user prefs) · `'indexeddb'` (>100-entry collections).
69
+ - States live at module scope. Many small states beat one monolith. Use `state('feature/name').{memory,session,local,idb}(initial)` from `@mhmo91/schmancy/state`.
70
+ - Reading `state.value` inside `render()` auto-tracks via the base class's `SignalWatcher` no decorator or binding needed for the default case.
71
+ - `await state.ready` (or `if (state.loaded)`) before reading persisted-backend values that hydrate asynchronously.
72
+ - Storage tiers: `.memory()` (regenerable) · `.session()` (per-tab) · `.local()` (user prefs) · `.idb()` (>100-entry collections).
72
73
 
73
74
  **Routing**
74
75
  - Route guards are `Observable<boolean>`, never cached booleans.
@@ -36,7 +36,7 @@ sound.muted$.subscribe(m => {})
36
36
  ```
37
37
 
38
38
  ## Settings Persistence
39
- Volume, mute, and custom theme persist to `localStorage` under key `schmancy-sound-settings` (via `createContext`).
39
+ Volume, mute, and custom theme persist to `localStorage` under key `schmancy-sound-settings` (via `state(...).local(...)` from `@mhmo91/schmancy/state`).
40
40
 
41
41
  ## AI-Generated Themes
42
42
  ```typescript
@@ -38,7 +38,7 @@ discoverAnyComponent('schmancy-navigation-rail', 'schmancy-navigation-bar')
38
38
  ```
39
39
 
40
40
  ### `discoverElement(selector, timeout = 150)`
41
- Finds any element by CSS selector across shadow DOM. Uses a request ID + universal `schmancy-discover` event. Every `$LitElement` responds if it finds a match in its shadow root.
41
+ Finds any element by CSS selector across shadow DOM. Uses a request ID + universal `schmancy-discover` event. Every `SchmancyElement` responds if it finds a match in its shadow root.
42
42
  ```typescript
43
43
  discoverElement('[data-section="pricing"]').subscribe(section => section?.scrollIntoView())
44
44
  ```
@@ -51,12 +51,12 @@ discoverAllElements('.flagged').subscribe(all => console.log(all.length))
51
51
 
52
52
  ## How the Handshake Works
53
53
  1. Caller creates a unique `requestId` and broadcasts `schmancy-discover` on `window` with `{ selector, requestId }`.
54
- 2. Every `$LitElement` listens for this event (wired up in the base class).
54
+ 2. Every `SchmancyElement` listens for this event (wired up in the base class).
55
55
  3. Any matching element dispatches `schmancy-discover-response` with `{ requestId, element }`.
56
56
  4. Caller collects responses for the timeout window and emits via RxJS.
57
57
 
58
58
  ## Pattern in Base Class
59
- Every `$LitElement` inherits auto-response: `discover<T>(tag)` (method on the component) and `{tagName}-where-are-you`/`{tagName}-here-i-am` events. See [mixins.md](./mixins.md).
59
+ Every `SchmancyElement` inherits auto-response: `discover<T>(tag)` (method on the component) and `{tagName}-where-are-you`/`{tagName}-here-i-am` events. See [mixins.md](./mixins.md).
60
60
 
61
61
  ## When to Use
62
62
  - Cross-shadow coordination between unrelated components.
@@ -36,4 +36,4 @@ Auto-dismisses the dialog when clicked. Renders as a `schmancy-list-item` intern
36
36
  </schmancy-menu>
37
37
  ```
38
38
 
39
- Uses `$dialog.component()` internally. Menu items auto-dismiss the dialog. Custom components in the default slot must call `$dialog.dismiss()` manually.
39
+ Uses `show(overlay, { anchor })` from `@mhmo91/schmancy/overlay` internally. `<schmancy-menu-item>` auto-closes the menu by dispatching a `'close'` event on click. Custom components in the default slot dismiss the menu the same way: `this.dispatchEvent(new CustomEvent('close', { bubbles: true, composed: true }))`.
@@ -40,7 +40,7 @@ Centered is the fallback (no anchor given). Sheet is the responsive adaptation (
40
40
 
41
41
  ## Subscription IS the overlay lifecycle
42
42
 
43
- Inside any `$LitElement`, pipe `takeUntil(this.disconnecting)`. When the caller unmounts, the overlay auto-dismisses — no handles to track, no leaks.
43
+ Inside any `SchmancyElement`, pipe `takeUntil(this.disconnecting)`. When the caller unmounts, the overlay auto-dismisses — no handles to track, no leaks.
44
44
 
45
45
  ```ts
46
46
  show(MyForm, { props: { id }, anchor: ev })
@@ -29,8 +29,8 @@ cart.replace({ items: [], total: 0 })
29
29
  cart.update(d => { d.items.push(item) }) // immer
30
30
  cart.delete('total')
31
31
 
32
- // Subscribe in a $LitElement — direct read auto-tracks via SignalWatcher
33
- class CartView extends $LitElement() {
32
+ // Subscribe in a SchmancyElement — direct read auto-tracks via SignalWatcher
33
+ class CartView extends SchmancyElement {
34
34
  render() { return html`Items: ${cart.value.items.length}` }
35
35
  }
36
36
 
@@ -207,18 +207,18 @@ with zero ceremony.
207
207
 
208
208
  ### (1) Default — direct read inside `render()`
209
209
 
210
- `$LitElement()` composes `SignalWatcher` from `@lit-labs/signals`.
210
+ `SchmancyElement` composes `SignalWatcher` from `@lit-labs/signals`.
211
211
  Every signal read inside `render()` auto-tracks; the host re-renders
212
212
  on change. No decorator, no field, no binding code.
213
213
 
214
214
  ```ts
215
- import { LitElement, html } from 'lit'
215
+ import { html } from 'lit'
216
216
  import { customElement } from 'lit/decorators.js'
217
- import { $LitElement } from '@mixins/index'
218
- import { cart } from './cart.context'
217
+ import { SchmancyElement } from '@mhmo91/schmancy/mixins'
218
+ import { cart } from './cart.state'
219
219
 
220
220
  @customElement('cart-view')
221
- export class CartView extends $LitElement() {
221
+ export class CartView extends SchmancyElement {
222
222
  render() {
223
223
  return html`<span>Items: ${cart.value.items.length}</span>`
224
224
  }
@@ -243,7 +243,7 @@ derived methods, DevTools inspection, or readability:
243
243
  import { observe } from '@mhmo91/schmancy/state'
244
244
 
245
245
  @customElement('cart-view')
246
- export class CartView extends $LitElement() {
246
+ export class CartView extends SchmancyElement {
247
247
  @observe(cart) cart!: CartState
248
248
 
249
249
  onClick() {
@@ -268,12 +268,12 @@ works under the existing tsconfig with no migration.
268
268
 
269
269
  ### (3) `bindState(host, source)` — imperative form
270
270
 
271
- For hosts that aren't `$LitElement` subclasses (rare):
271
+ For hosts that aren't `SchmancyElement` subclasses (rare):
272
272
 
273
273
  ```ts
274
274
  import { bindState } from '@mhmo91/schmancy/state'
275
275
 
276
- class CustomHost extends LitElement { // not a $LitElement subclass
276
+ class CustomHost extends LitElement { // not a SchmancyElement subclass
277
277
  cart = bindState(this, cart)
278
278
  render() {
279
279
  return html`<span>Items: ${this.cart.value.items.length}</span>`
@@ -376,7 +376,7 @@ two side-by-side checkout flows, an `<iframe>`-like wizard, an
376
376
  embedded preview — wrap the subtree in `<schmancy-context>`.
377
377
 
378
378
  ```ts
379
- class App extends $LitElement() {
379
+ class App extends SchmancyElement {
380
380
  render() {
381
381
  return html`
382
382
  <schmancy-context .provides=${[cart]}>
@@ -402,7 +402,7 @@ after an `await fetch(...)` — all of it auto-resolves to the right
402
402
  instance based on tree position.
403
403
 
404
404
  ```ts
405
- class CartView extends $LitElement() {
405
+ class CartView extends SchmancyElement {
406
406
  render() {
407
407
  return html`
408
408
  <button @click=${() => cart.set({ total: 0 })}>Clear</button>
@@ -418,22 +418,42 @@ class CartView extends $LitElement() {
418
418
  }
419
419
  ```
420
420
 
421
- Coverage of the call paths is provided by `SchmancyElement`:
421
+ Coverage of the call paths is provided by `SchmancyElement` and
422
+ `<schmancy-context>`:
422
423
 
423
- - `render()` and every Lit lifecycle hook — wrapped at construction.
424
+ - `render()` and every Lit lifecycle hook — wrapped at construction
425
+ via the `_activeHost.run(host, fn)` stack.
424
426
  - Class methods (`handleAdd`, `handleSubmit`) — wrapped at construction.
425
- - `await` continuations inside class methods — propagated via the
426
- `Promise.then` patch in `state/active-host.ts`.
427
427
  - `addEventListener(type, fn)` on the host — wrapped (and unwrapped on
428
428
  `removeEventListener`).
429
- - Inline arrow handlers in templates (`@click=${() => …}`) resolve
430
- via the `window.event.composedPath()` fallback in
431
- `resolveActiveHost()`.
429
+ - Explicit `.then(...)` continuations off a class-method Promise
430
+ propagated via the `Promise.prototype.then` patch in
431
+ `state/active-host.ts`, which captures the active host at chain time
432
+ and restores it inside the callback.
433
+ - Inline arrow handlers in templates (`@click=${() => …}`) and any
434
+ other DOM event handler attached inside a `<schmancy-context>`
435
+ subtree — resolved via the capture-phase event listener that
436
+ `<schmancy-context>` installs on itself for ~18 common event types.
437
+ The listener publishes the event's target as the active host through
438
+ the `_publishEventHost(node)` slot for the duration of the
439
+ synchronous handler chain (slot self-clears in the next microtask).
440
+
441
+ **Known limitation — native `await` on a native Promise.** V8's await
442
+ optimization (since 7.x) skips the spec-prescribed
443
+ `Promise.resolve(x).then(continuation)` step, so the `Promise.then`
444
+ patch does not see the resumption. Class methods that mutate state
445
+ across an `await` boundary fall back to the module-scoped global, not
446
+ the active-host's isolated copy. To preserve the host across awaits,
447
+ either keep the mutation in the synchronous prelude before the first
448
+ `await`, or chain explicitly with `.then(...)` (which still routes
449
+ through the patched method). A real fix requires either a build-time
450
+ async-function transform or native `AsyncContext.Variable` in the
451
+ runtime.
432
452
 
433
453
  Pure async callbacks with no DOM origin — a websocket `onmessage`,
434
- `setInterval` with no triggering user event — fall through to the
435
- module-scoped global. That is the correct semantic: those callbacks
436
- have no tree position to resolve to.
454
+ `setInterval` with no triggering user event — also fall through to
455
+ the module-scoped global. That is the correct semantic: those
456
+ callbacks have no tree position to resolve to.
437
457
 
438
458
  ### Lifecycle
439
459