@mhmo91/schmancy 0.10.10 → 0.10.12
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/dist/agent/schmancy.agent.js.map +1 -1
- package/dist/handover/agent-runtime-followups.md +1 -1
- package/dist/handover/agent-runtime-v1.md +3 -3
- package/dist/handover/claude-design-brief.md +86 -46
- package/dist/handover/claude-design-setup.md +11 -7
- package/dist/handover/schmancy-token-reference.md +12 -6
- package/dist/skills/INDEX.md +1 -1
- package/dist/skills/SKILL.md +7 -6
- package/dist/skills/audio.md +1 -1
- package/dist/skills/discovery.md +3 -3
- package/dist/skills/menu.md +1 -1
- package/dist/skills/overlay.md +1 -1
- package/dist/skills/schmancy/INDEX.md +1 -1
- package/dist/skills/schmancy/SKILL.md +7 -6
- package/dist/skills/schmancy/audio.md +1 -1
- package/dist/skills/schmancy/discovery.md +3 -3
- package/dist/skills/schmancy/menu.md +1 -1
- package/dist/skills/schmancy/overlay.md +1 -1
- package/dist/skills/schmancy/state.md +42 -22
- package/dist/skills/state.md +42 -22
- package/dist/state-BusMG6sM.js.map +1 -1
- package/dist/state-DNdCPITt.cjs.map +1 -1
- package/package.json +1 -1
- package/skills/schmancy/INDEX.md +1 -1
- package/skills/schmancy/SKILL.md +7 -6
- package/skills/schmancy/audio.md +1 -1
- package/skills/schmancy/discovery.md +3 -3
- package/skills/schmancy/menu.md +1 -1
- package/skills/schmancy/overlay.md +1 -1
- package/skills/schmancy/state.md +42 -22
- package/src/CLAUDE.md +112 -354
- package/src/state/CLAUDE.md +26 -18
- package/src/state/SCOPING.md +23 -9
package/src/CLAUDE.md
CHANGED
|
@@ -1,20 +1,36 @@
|
|
|
1
|
-
# Schmancy
|
|
1
|
+
# Schmancy library source — agent brief
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
This file lives in the source tree. It tells you what's specific to
|
|
4
|
+
**authoring schmancy components**. For the public surface (every tag,
|
|
5
|
+
every service, every convention downstream consumers see), the
|
|
6
|
+
authoritative docs are in `skills/schmancy/`. Cross-link rather than
|
|
7
|
+
duplicate — those docs are checked into the npm tarball and rendered
|
|
8
|
+
by the Claude Code plugin. Drift here means drift everywhere.
|
|
4
9
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
- `
|
|
8
|
-
- `
|
|
9
|
-
- `
|
|
10
|
-
- `
|
|
11
|
-
-
|
|
10
|
+
## Where to read first
|
|
11
|
+
|
|
12
|
+
- `skills/schmancy/INDEX.md` — full catalog of public tags and services.
|
|
13
|
+
- `skills/schmancy/SKILL.md` — non-negotiable conventions every consumer follows.
|
|
14
|
+
- `skills/schmancy/mixins.md` — `SchmancyElement` base class (what every component extends).
|
|
15
|
+
- `skills/schmancy/state.md` — `state()` factory + `<schmancy-context>` scoping.
|
|
16
|
+
- `skills/schmancy/overlay.md` — `show()` + `confirm()` + `prompt()`; the only overlay primitives.
|
|
17
|
+
- `skills/schmancy/directives.md` — Lit + schmancy directives.
|
|
18
|
+
|
|
19
|
+
## Source-author rules (not in the consumer docs)
|
|
20
|
+
|
|
21
|
+
### Component skeleton
|
|
22
|
+
|
|
23
|
+
Every concrete component:
|
|
12
24
|
|
|
13
|
-
### Component Registration
|
|
14
25
|
```typescript
|
|
26
|
+
import { SchmancyElement } from '@mixins/index'
|
|
27
|
+
import { customElement } from 'lit/decorators.js'
|
|
28
|
+
import { css, html } from 'lit'
|
|
29
|
+
|
|
15
30
|
@customElement('schmancy-{name}')
|
|
16
|
-
export class Schmancy{Name} extends
|
|
17
|
-
|
|
31
|
+
export class Schmancy{Name} extends SchmancyElement {
|
|
32
|
+
static styles = [css`:host { display: block }`]
|
|
33
|
+
render() { return html`<slot></slot>` }
|
|
18
34
|
}
|
|
19
35
|
|
|
20
36
|
declare global {
|
|
@@ -24,396 +40,132 @@ declare global {
|
|
|
24
40
|
}
|
|
25
41
|
```
|
|
26
42
|
|
|
27
|
-
|
|
43
|
+
- Never extend raw `LitElement`.
|
|
44
|
+
- Never wrap with `SignalWatcher` — `SchmancyElement` already does. Double-wrapping panics with "Detected cycle in computations" at runtime; the `NO_SIGNAL_WATCHER_WRAP` pre-edit lint rule blocks it.
|
|
45
|
+
- The `$LitElement(style?)` factory in `mixins/litElement.mixin.ts` is a deprecated alias kept for the migration window. **Do not use it in new files.** The `PREFER_SCHMANCY_ELEMENT` pre-edit lint rule flags new uses; the migration section in `skills/schmancy/mixins.md` documents the rewrite.
|
|
46
|
+
- Register the tag in `HTMLElementTagNameMap` so consumers get TypeScript completion on `<schmancy-{name}>`.
|
|
47
|
+
- Export through `src/{name}/index.ts` and surface from `src/index.ts`.
|
|
28
48
|
|
|
29
|
-
###
|
|
30
|
-
```typescript
|
|
31
|
-
import style from './component.scss?inline'
|
|
32
|
-
export class Component extends $LitElement(style) {}
|
|
33
|
-
```
|
|
34
|
-
- Components with complex styling use `.scss` files
|
|
35
|
-
- Simple components use inline CSS template literals
|
|
36
|
-
- All Tailwind classes work via TailwindMixin in base
|
|
49
|
+
### Styling
|
|
37
50
|
|
|
38
|
-
|
|
39
|
-
- Theme CSS variables: `--schmancy-sys-color-{path}`
|
|
40
|
-
- Tailwind config maps to theme tokens
|
|
41
|
-
- Use Tailwind utility classes directly in templates
|
|
51
|
+
Two patterns, both end up in `static styles`:
|
|
42
52
|
|
|
43
|
-
## State Management
|
|
44
|
-
|
|
45
|
-
### RxJS Patterns
|
|
46
53
|
```typescript
|
|
47
|
-
//
|
|
48
|
-
|
|
54
|
+
// Simple: inline css template literal
|
|
55
|
+
static styles = [css`:host { display: block }`]
|
|
49
56
|
|
|
50
|
-
//
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
.subscribe(...)
|
|
57
|
+
// Complex: external SCSS via Vite ?inline import
|
|
58
|
+
import style from './component.scss?inline'
|
|
59
|
+
static styles = [unsafeCSS(style)] // unsafeCSS converts the string → CSSResult
|
|
54
60
|
```
|
|
55
61
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
2. Implement `_updateInputDisplay()` to sync label with value
|
|
60
|
-
3. Call in `firstUpdated()` for initial sync
|
|
61
|
-
4. Call when value changes programmatically
|
|
62
|
+
Tailwind utilities work in templates without import — `SchmancyElement`
|
|
63
|
+
injects the project Tailwind sheet into every shadow root. `static
|
|
64
|
+
styles` only carries component-local CSS.
|
|
62
65
|
|
|
63
|
-
###
|
|
64
|
-
```typescript
|
|
65
|
-
// Create typed context
|
|
66
|
-
const Context = createContext<T>(initial, 'local|memory|indexeddb', 'key')
|
|
67
|
-
|
|
68
|
-
// Use in component
|
|
69
|
-
@select(Context)
|
|
70
|
-
property!: T
|
|
71
|
-
|
|
72
|
-
// Or create compound selectors
|
|
73
|
-
const selector = createCompoundSelector(
|
|
74
|
-
[Context1, Context2],
|
|
75
|
-
[a => a.field, b => b.field],
|
|
76
|
-
(val1, val2) => ({ combined: val1 + val2 })
|
|
77
|
-
)
|
|
78
|
-
```
|
|
66
|
+
### Slot content processing
|
|
79
67
|
|
|
80
|
-
|
|
68
|
+
Components that consume slotted children (select, autocomplete, chips,
|
|
69
|
+
menu) use `@queryAssignedElements` and wire handlers in `firstUpdated`:
|
|
81
70
|
|
|
82
|
-
### Custom Event Types
|
|
83
71
|
```typescript
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
additionalData?: any
|
|
87
|
-
}>
|
|
72
|
+
@queryAssignedElements({ flatten: true })
|
|
73
|
+
private _options!: SchmancyOption[]
|
|
88
74
|
|
|
89
|
-
|
|
90
|
-
this.
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
bubbles: true,
|
|
94
|
-
composed: true,
|
|
75
|
+
firstUpdated() {
|
|
76
|
+
this._options.forEach((option, index) => {
|
|
77
|
+
option.tabIndex = -1
|
|
78
|
+
if (!option.id) option.id = `${this.id}-option-${index}`
|
|
95
79
|
})
|
|
96
|
-
)
|
|
97
|
-
```
|
|
98
|
-
|
|
99
|
-
### Discovery Events
|
|
100
|
-
Components respond to `{tag}-where-are-you` by emitting `{tag}-here-i-am` with self reference.
|
|
101
|
-
|
|
102
|
-
## Accessibility Patterns
|
|
103
|
-
|
|
104
|
-
### Form Components
|
|
105
|
-
```typescript
|
|
106
|
-
// ARIA attributes
|
|
107
|
-
role="combobox"
|
|
108
|
-
aria-haspopup="listbox"
|
|
109
|
-
aria-expanded=${this._open}
|
|
110
|
-
aria-controls="listbox-id"
|
|
111
|
-
|
|
112
|
-
// Screen reader announcements
|
|
113
|
-
private _announceToScreenReader(message: string) {
|
|
114
|
-
const liveRegion = this.shadowRoot?.querySelector('#live-status')
|
|
115
|
-
if (liveRegion) liveRegion.textContent = message
|
|
116
80
|
}
|
|
117
|
-
|
|
118
|
-
// Live region in template
|
|
119
|
-
<div id="live-status" role="status" aria-live="polite" class="sr-only"></div>
|
|
120
|
-
```
|
|
121
|
-
|
|
122
|
-
### Focus Management
|
|
123
|
-
```typescript
|
|
124
|
-
protected static shadowRootOptions = {
|
|
125
|
-
...LitElement.shadowRootOptions,
|
|
126
|
-
mode: 'open',
|
|
127
|
-
delegatesFocus: true, // Auto-focus first focusable element
|
|
128
|
-
}
|
|
129
|
-
```
|
|
130
|
-
|
|
131
|
-
## Lit Directives - Complete Reference
|
|
132
|
-
|
|
133
|
-
### Core Principles
|
|
134
|
-
- Import only directives you use (modular design prevents bundle bloat)
|
|
135
|
-
- Directives are functions that customize rendering behavior
|
|
136
|
-
- Each directive is a separate module from `lit/directives/`
|
|
137
|
-
|
|
138
|
-
### Conditional Rendering Directives
|
|
139
|
-
|
|
140
|
-
**`when(condition, trueCase, falseCase?)`** - Best for clean inline conditionals
|
|
141
|
-
```typescript
|
|
142
|
-
import { when } from 'lit/directives/when.js'
|
|
143
|
-
${when(this.isExpanded, () => html`<div>Content</div>`, () => html`<div>Summary</div>`)}
|
|
144
|
-
```
|
|
145
|
-
- Cleaner than ternary operators
|
|
146
|
-
- Lazy evaluation of templates
|
|
147
|
-
- Use when readability matters
|
|
148
|
-
|
|
149
|
-
**`choose(value, cases, default?)`** - Template-level switch statement
|
|
150
|
-
```typescript
|
|
151
|
-
import { choose } from 'lit/directives/choose.js'
|
|
152
|
-
${choose(this.state, [
|
|
153
|
-
['loading', () => html`<spinner></spinner>`],
|
|
154
|
-
['error', () => html`<error-msg></error-msg>`],
|
|
155
|
-
['success', () => html`<content></content>`]
|
|
156
|
-
])}
|
|
157
|
-
```
|
|
158
|
-
- Strict equality matching
|
|
159
|
-
- Better than nested ternaries
|
|
160
|
-
|
|
161
|
-
**`ifDefined(value)`** - Conditional attributes
|
|
162
|
-
```typescript
|
|
163
|
-
import { ifDefined } from 'lit/directives/if-defined.js'
|
|
164
|
-
<img src=${ifDefined(this.imageUrl)}>
|
|
165
|
-
```
|
|
166
|
-
- Sets attribute only when value is defined
|
|
167
|
-
- Essential for URL attributes where undefined should prevent rendering
|
|
168
|
-
|
|
169
|
-
### List Rendering Directives
|
|
170
|
-
|
|
171
|
-
**`repeat(items, keyFn?, templateFn)`** - **USE THIS FOR ALL LISTS**
|
|
172
|
-
```typescript
|
|
173
|
-
import { repeat } from 'lit/directives/repeat.js'
|
|
174
|
-
${repeat(
|
|
175
|
-
this.items,
|
|
176
|
-
(item) => item.id, // Key function for DOM stability
|
|
177
|
-
(item, index) => html`<div>${item.name}</div>`
|
|
178
|
-
)}
|
|
179
|
-
```
|
|
180
|
-
- **MANDATORY for Schmancy**: Enables DOM diffing and stability
|
|
181
|
-
- Maintains DOM node association during list updates
|
|
182
|
-
- Most efficient for insertions/removals/reordering
|
|
183
|
-
- Prevents unnecessary re-renders
|
|
184
|
-
|
|
185
|
-
**`map(items, templateFn)`** - Simple iteration (use sparingly)
|
|
186
|
-
```typescript
|
|
187
|
-
import { map } from 'lit/directives/map.js'
|
|
188
|
-
${map(this.items, (item) => html`<div>${item}</div>`)}
|
|
189
|
-
```
|
|
190
|
-
- Smaller and faster than repeat, but NO keying
|
|
191
|
-
- Use ONLY when list never changes order
|
|
192
|
-
- Prefer `repeat` in 99% of cases
|
|
193
|
-
|
|
194
|
-
**`join(items, joiner)`** - Interleave with separator
|
|
195
|
-
```typescript
|
|
196
|
-
import { join } from 'lit/directives/join.js'
|
|
197
|
-
${join(this.tags.map(t => html`<span>${t}</span>`), html`, `)}
|
|
198
|
-
```
|
|
199
|
-
|
|
200
|
-
**`range(start, end?, step?)`** - Sequential integers
|
|
201
|
-
```typescript
|
|
202
|
-
import { range } from 'lit/directives/range.js'
|
|
203
|
-
${map(range(5), i => html`<item>${i}</item>`)}
|
|
204
|
-
```
|
|
205
|
-
|
|
206
|
-
### Performance Optimization Directives
|
|
207
|
-
|
|
208
|
-
**`cache(value)`** - **CRITICAL FOR TEMPLATE SWITCHING**
|
|
209
|
-
```typescript
|
|
210
|
-
import { cache } from 'lit/directives/cache.js'
|
|
211
|
-
${cache(
|
|
212
|
-
this.view === 'detail'
|
|
213
|
-
? html`<detail-view>${content}</detail-view>`
|
|
214
|
-
: html`<summary-view>${summary}</summary-view>`
|
|
215
|
-
)}
|
|
216
|
-
```
|
|
217
|
-
- Preserves DOM nodes when switching between templates
|
|
218
|
-
- Avoids re-creation costs for large, complex templates
|
|
219
|
-
- **USE WHEN:** Frequently toggling between views
|
|
220
|
-
- **AVOID WHEN:** Templates are simple or rarely toggle
|
|
221
|
-
- **PITFALL:** Cached DOM retains internal state (form values, scroll position)
|
|
222
|
-
- **WITH REFS:** Refs remain valid across switches since DOM is preserved
|
|
223
|
-
- **MEMORY:** Trades memory for rendering speed
|
|
224
|
-
|
|
225
|
-
**`guard(dependencies, valueFn)`** - Prevents unnecessary computation
|
|
226
|
-
```typescript
|
|
227
|
-
import { guard } from 'lit/directives/guard.js'
|
|
228
|
-
${guard([this.data], () => this.expensiveCalculation(this.data))}
|
|
229
|
-
```
|
|
230
|
-
- Only re-runs when dependency **identity** changes
|
|
231
|
-
- Perfect for immutable data patterns
|
|
232
|
-
- Implements memoization at template level
|
|
233
|
-
- Use for expensive calculations, transformations, hashing
|
|
234
|
-
|
|
235
|
-
**`keyed(key, value)`** - Forces DOM recreation on key change
|
|
236
|
-
```typescript
|
|
237
|
-
import { keyed } from 'lit/directives/keyed.js'
|
|
238
|
-
${keyed(this.userId, html`<user-profile .user=${this.user}></user-profile>`)}
|
|
239
|
-
```
|
|
240
|
-
- Removes old DOM before rendering new value
|
|
241
|
-
- Useful for clearing element state or resetting animations
|
|
242
|
-
- Opposite of cache - forces fresh DOM
|
|
243
|
-
|
|
244
|
-
### DOM Reference Directives
|
|
245
|
-
|
|
246
|
-
**`ref(refObject)`** - Access rendered elements
|
|
247
|
-
```typescript
|
|
248
|
-
import { createRef, ref, Ref } from 'lit/directives/ref.js'
|
|
249
|
-
private containerRef: Ref<HTMLDivElement> = createRef()
|
|
250
|
-
|
|
251
|
-
// In template
|
|
252
|
-
<div ${ref(this.containerRef)}></div>
|
|
253
|
-
|
|
254
|
-
// Access via
|
|
255
|
-
this.containerRef.value?.focus()
|
|
256
81
|
```
|
|
257
|
-
- Enables imperative DOM manipulation
|
|
258
|
-
- Use for focus management, third-party library integration
|
|
259
|
-
- Callback form available: `ref((el) => { /* use el */ })`
|
|
260
|
-
|
|
261
|
-
### State Synchronization Directives
|
|
262
82
|
|
|
263
|
-
|
|
264
|
-
```typescript
|
|
265
|
-
import { live } from 'lit/directives/live.js'
|
|
266
|
-
<input .value=${live(this.value)}>
|
|
267
|
-
```
|
|
268
|
-
- Critical for input elements that modify their own state
|
|
269
|
-
- Compares against actual DOM value, not last-rendered value
|
|
270
|
-
- Use with strict equality checks
|
|
271
|
-
- Essential for contenteditable or custom elements with external state
|
|
83
|
+
### RxJS internals
|
|
272
84
|
|
|
273
|
-
|
|
85
|
+
Long-lived component state is a `BehaviorSubject` plus a derived stream:
|
|
274
86
|
|
|
275
|
-
**`until(...values)`** - Renders while promises resolve
|
|
276
87
|
```typescript
|
|
277
|
-
|
|
278
|
-
${until(
|
|
279
|
-
fetch('/api/data').then(r => r.json()),
|
|
280
|
-
html`<spinner></spinner>` // Placeholder
|
|
281
|
-
)}
|
|
282
|
-
```
|
|
283
|
-
- Highest-priority promises render on resolution
|
|
284
|
-
- Lower-priority values show during pending states
|
|
88
|
+
private _value$ = new BehaviorSubject<T>(initial)
|
|
285
89
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
90
|
+
connectedCallback() {
|
|
91
|
+
super.connectedCallback()
|
|
92
|
+
combineLatest([this._value$, this._open$])
|
|
93
|
+
.pipe(takeUntil(this.disconnecting))
|
|
94
|
+
.subscribe(([value, open]) => { /* … */ })
|
|
95
|
+
}
|
|
290
96
|
```
|
|
291
97
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
98
|
+
- Every subscription ends with `.pipe(takeUntil(this.disconnecting))`.
|
|
99
|
+
- Form components (`select`, `autocomplete`, `chips`) track explicit
|
|
100
|
+
property assignment with flags (`_valueSet`, `_valuesSet`) and
|
|
101
|
+
resync the visible label via `_updateInputDisplay()` in
|
|
102
|
+
`firstUpdated()` and on every programmatic value change.
|
|
297
103
|
|
|
298
|
-
###
|
|
104
|
+
### Custom events
|
|
299
105
|
|
|
300
|
-
|
|
301
|
-
```typescript
|
|
302
|
-
import { unsafeHTML } from 'lit/directives/unsafe-html.js'
|
|
303
|
-
${unsafeHTML(this.trustedContent)}
|
|
304
|
-
```
|
|
305
|
-
- **ONLY for developer-controlled content**
|
|
306
|
-
- Enables XSS, CSS injection if misused
|
|
307
|
-
- Use for database-stored HTML from trusted sources
|
|
106
|
+
Type the detail and dispatch with `bubbles: true, composed: true`:
|
|
308
107
|
|
|
309
|
-
**`unsafeSVG(string)`** - Render trusted SVG strings
|
|
310
108
|
```typescript
|
|
311
|
-
|
|
312
|
-
${unsafeSVG(this.svgContent)}
|
|
313
|
-
```
|
|
314
|
-
- Same security constraints as unsafeHTML
|
|
109
|
+
export type SchmancyChangeEvent = CustomEvent<{ value: string }>
|
|
315
110
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
111
|
+
this.dispatchEvent(
|
|
112
|
+
new CustomEvent<SchmancyChangeEvent['detail']>('change', {
|
|
113
|
+
detail: { value: this.value },
|
|
114
|
+
bubbles: true,
|
|
115
|
+
composed: true,
|
|
116
|
+
}),
|
|
117
|
+
)
|
|
320
118
|
```
|
|
321
119
|
|
|
322
|
-
###
|
|
120
|
+
### Accessibility
|
|
323
121
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
4. **Use `when` over ternaries** for readability
|
|
328
|
-
5. **Use `ref` for animations** and imperative DOM access
|
|
329
|
-
6. **Combine `cache` + `ref` carefully** - refs stay valid, but ensure proper lifecycle
|
|
330
|
-
7. **Use `live` for form inputs** that modify themselves
|
|
331
|
-
8. **Avoid `map`** unless you're certain list order never changes
|
|
332
|
-
|
|
333
|
-
### Common Patterns in Schmancy
|
|
334
|
-
|
|
335
|
-
**List with stable DOM:**
|
|
336
|
-
```typescript
|
|
337
|
-
${repeat(
|
|
338
|
-
this.employees,
|
|
339
|
-
(e) => e.id,
|
|
340
|
-
(e, i) => html`<employee-card .employee=${e}></employee-card>`
|
|
341
|
-
)}
|
|
342
|
-
```
|
|
122
|
+
Form components carry the ARIA combobox pattern (`role="combobox"`,
|
|
123
|
+
`aria-haspopup="listbox"`, `aria-expanded`, `aria-controls`) and a
|
|
124
|
+
visually-hidden live region for status announcements:
|
|
343
125
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
${guard([this.data], () => this.calculateComplexStats(this.data))}
|
|
126
|
+
```html
|
|
127
|
+
<div id="live-status" role="status" aria-live="polite" class="sr-only"></div>
|
|
347
128
|
```
|
|
348
129
|
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
this.isExpanded
|
|
353
|
-
? html`<expanded-content ${ref(this.contentRef)}></expanded-content>`
|
|
354
|
-
: html``
|
|
355
|
-
)}
|
|
356
|
-
```
|
|
130
|
+
Use `delegatesFocus: true` in `shadowRootOptions` for components whose
|
|
131
|
+
internal first-focusable element should receive focus when the host
|
|
132
|
+
does:
|
|
357
133
|
|
|
358
|
-
**Animation target with ref:**
|
|
359
134
|
```typescript
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
135
|
+
protected static shadowRootOptions = {
|
|
136
|
+
...LitElement.shadowRootOptions,
|
|
137
|
+
mode: 'open',
|
|
138
|
+
delegatesFocus: true,
|
|
139
|
+
}
|
|
363
140
|
```
|
|
364
141
|
|
|
365
|
-
|
|
142
|
+
### State within a component
|
|
366
143
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
144
|
+
Module-scoped reactive state lives on `state(...)` from
|
|
145
|
+
`@mhmo91/schmancy/state` — see `skills/schmancy/state.md`. Inside a
|
|
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`.
|
|
371
151
|
|
|
372
|
-
|
|
373
|
-
this._options.forEach((option, index) => {
|
|
374
|
-
option.tabIndex = -1
|
|
375
|
-
if (!option.id) option.id = `${this.id}-option-${index}`
|
|
376
|
-
// Add event listeners
|
|
377
|
-
})
|
|
378
|
-
```
|
|
379
|
-
|
|
380
|
-
### Debouncing & Filtering
|
|
381
|
-
```typescript
|
|
382
|
-
this._inputValue$.pipe(
|
|
383
|
-
distinctUntilChanged(),
|
|
384
|
-
debounceTime(this.debounceMs),
|
|
385
|
-
tap(value => this._handleChange(value)),
|
|
386
|
-
takeUntil(this.disconnecting)
|
|
387
|
-
).subscribe()
|
|
388
|
-
```
|
|
152
|
+
### Theme consumption
|
|
389
153
|
|
|
390
|
-
|
|
154
|
+
Components that need theme tokens consume the Lit context:
|
|
391
155
|
|
|
392
|
-
Components consuming theme:
|
|
393
156
|
```typescript
|
|
394
157
|
@consume({ context: themeContext })
|
|
395
158
|
theme!: Partial<TSchmancyTheme>
|
|
396
159
|
```
|
|
397
160
|
|
|
398
|
-
Theme CSS
|
|
161
|
+
Theme CSS variables are auto-generated as `--schmancy-{path}`. Prefer
|
|
162
|
+
the Tailwind shortcut utilities (`bg-surface-default`,
|
|
163
|
+
`text-error-default`, `border-outline-variant`) over raw
|
|
164
|
+
`var(--schmancy-sys-color-X)` references — the token map lives in
|
|
165
|
+
`skills/schmancy/theme.md § Tailwind utilities`.
|
|
399
166
|
|
|
400
|
-
##
|
|
167
|
+
## Validation patterns (form components)
|
|
401
168
|
|
|
402
|
-
```typescript
|
|
403
|
-
// Navigate programmatically
|
|
404
|
-
area.push({
|
|
405
|
-
area: 'main',
|
|
406
|
-
component: MyComponent,
|
|
407
|
-
params?: { id: '123' }
|
|
408
|
-
})
|
|
409
|
-
|
|
410
|
-
// Lazy load
|
|
411
|
-
const LazyComponent = lazy(() => import('./component'))
|
|
412
|
-
```
|
|
413
|
-
|
|
414
|
-
## Testing Helpers
|
|
415
|
-
|
|
416
|
-
### Value Validation
|
|
417
169
|
```typescript
|
|
418
170
|
public checkValidity(): boolean {
|
|
419
171
|
if (!this.required) return true
|
|
@@ -426,3 +178,9 @@ public reportValidity(): boolean {
|
|
|
426
178
|
return this._inputElementRef.value?.reportValidity() ?? this.checkValidity()
|
|
427
179
|
}
|
|
428
180
|
```
|
|
181
|
+
|
|
182
|
+
## Pointers
|
|
183
|
+
|
|
184
|
+
- **State module brief:** `src/state/CLAUDE.md` — invariants for code under `src/state/`.
|
|
185
|
+
- **Migration off v1 contexts:** `src/state/MIGRATION.md`.
|
|
186
|
+
- **Lab acceptance criterion:** `lab/README.md` — what does and doesn't belong in `@mhmo91/schmancy-lab`.
|
package/src/state/CLAUDE.md
CHANGED
|
@@ -19,7 +19,7 @@ A reactive state primitive for the schmancy library:
|
|
|
19
19
|
needed AS a class field (event handlers, derived methods, DevTools).
|
|
20
20
|
- `bindState(host, source)` — `ReactiveController` helper. Same
|
|
21
21
|
guarantees as `@observe`, no decorator. Use when the host isn't a
|
|
22
|
-
|
|
22
|
+
`SchmancyElement` subclass.
|
|
23
23
|
- `stateFromObservable(observable, namespace, initial)` — bridges an
|
|
24
24
|
RxJS source into a `state()`.
|
|
25
25
|
|
|
@@ -28,13 +28,13 @@ type exports.
|
|
|
28
28
|
|
|
29
29
|
## Default subscription pattern
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
`SchmancyElement` already composes `SignalWatcher` over its mixin chain.
|
|
32
32
|
Every signal read inside `render()` auto-tracks. **The default consumer
|
|
33
33
|
needs zero binding code:**
|
|
34
34
|
|
|
35
35
|
```ts
|
|
36
36
|
@customElement('cart-view')
|
|
37
|
-
class CartView extends
|
|
37
|
+
class CartView extends SchmancyElement {
|
|
38
38
|
render() { return html`Items: ${cart.value.items.length}` }
|
|
39
39
|
}
|
|
40
40
|
```
|
|
@@ -59,25 +59,33 @@ the element auto-resolve to a per-element isolated copy via the
|
|
|
59
59
|
Two infrastructure files own the mechanics:
|
|
60
60
|
|
|
61
61
|
- `state/active-host.ts` — `_activeHost` Variable + `Promise.then`
|
|
62
|
-
patch + `
|
|
63
|
-
|
|
64
|
-
TC39 AsyncContext.Variable polyfill
|
|
65
|
-
|
|
66
|
-
|
|
62
|
+
patch + `_publishEventHost(node)` slot + `resolveActiveHost()`
|
|
63
|
+
4-tier fallback (stack → event-host slot → `document.activeElement`
|
|
64
|
+
→ undefined). Hand-rolled TC39 AsyncContext.Variable polyfill
|
|
65
|
+
(~30 lines for the Promise patch alone). The patch is idempotent and
|
|
66
|
+
uses `_origThen.call(this, …)` so Promise subclassing /
|
|
67
|
+
`Symbol.species` are untouched. Decommissions the day a real
|
|
67
68
|
polyfill or native AsyncContext lands — drop the file, swap the
|
|
68
69
|
`_activeHost` export.
|
|
69
70
|
- `state/schmancy-context.ts` — the `<schmancy-context>` element. One
|
|
70
71
|
`ContextProvider` per state in `provides`; all destroyed on
|
|
71
|
-
disconnect.
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
72
|
+
disconnect. Also installs capture-phase listeners on itself for
|
|
73
|
+
~18 common event types and calls `_publishEventHost(target)` from
|
|
74
|
+
each — that's how inline arrow handlers attached to descendants
|
|
75
|
+
resolve to the closest enclosing context.
|
|
76
|
+
|
|
77
|
+
`mixins/SchmancyElement.ts` carries the host-side integration points:
|
|
78
|
+
prototype-chain wrap of every concrete subclass at first construction
|
|
79
|
+
(caches in a `WeakSet`), and `addEventListener` / `removeEventListener`
|
|
80
|
+
overrides that wrap host listeners.
|
|
81
|
+
|
|
82
|
+
**Known limitation — native `await`.** V8's await optimization (since
|
|
83
|
+
7.x) skips the spec-prescribed `Promise.resolve(x).then(continuation)`
|
|
84
|
+
step, so the Promise.then patch does not see the resumption of an
|
|
85
|
+
`await` on a native Promise. Class methods that mutate state across an
|
|
86
|
+
`await` boundary fall back to the module-scoped global. To preserve
|
|
87
|
+
the host across awaits, keep the mutation in the synchronous prelude
|
|
88
|
+
before the first `await`, or chain explicitly with `.then(...)`.
|
|
81
89
|
|
|
82
90
|
## Rules for code in this directory
|
|
83
91
|
|