diff --git a/.claude/skills/le-truc/SKILL.md b/.claude/skills/le-truc/SKILL.md deleted file mode 100644 index 54070af..0000000 --- a/.claude/skills/le-truc/SKILL.md +++ /dev/null @@ -1,87 +0,0 @@ ---- -name: le-truc -description: > - Expert guidance for building reactive web components with the @zeix/le-truc library. - Use when creating, reviewing, or debugging a Le Truc component. -user_invocable: true ---- - - -This skill is for **developers building components** with the `@zeix/le-truc` public API. - -For development work on the le-truc library itself, use the `le-truc-dev` skill instead. -For deep signal-level questions, use the `cause-effect` skill — `@zeix/cause-effect` is re-exported by le-truc, no separate install needed. - - - -**`defineComponent(name, props, select, setup)`** — every component is defined with these four arguments. No other entry point exists. - -**`host` is the only external interface.** Components read and write state through `host.propName`. No querying outside the host's subtree, no direct property access on child components. - -**Reactivity flows in one direction:** -``` -attribute change → parser → host.prop (signal) - ↓ - effect reads prop - ↓ - DOM update on target - ↓ - event handler → { prop: value } - ↓ - signal updated → effect re-runs -``` - -**`@zeix/cause-effect` is re-exported.** Signal types (`State`, `Memo`, `Sensor`, `Slot`, etc.) and utilities (`batch`, `match`, `untrack`) are available directly from `@zeix/le-truc`. No separate install or import needed. - - - -What kind of task is this? - -1. **Build** — create a new component (TypeScript, HTML, CSS, documentation) -2. **Review** — review or extend an existing component -3. **Debug** — trace broken or unexpected reactive behavior - -**Wait for response before proceeding.** - - - -| Response | Workflow | -|---|---| -| 1, "build", "create", "new", "add", "write" | workflows/build.md | -| 2, "review", "extend", "refactor", "improve", "check" | workflows/review.md | -| 3, "debug", "fix", "broken", "not working", "wrong", "unexpected" | workflows/debug.md | - -**Intent-based routing** (if the user provides clear context without selecting): -- Describes a component to create → workflows/build.md -- Describes an existing component to change or check → workflows/review.md -- Describes something behaving unexpectedly → workflows/debug.md - -**After identifying the workflow, read it and follow it exactly.** - - - -All in `references/`: - -| File | Contents | -|---|---| -| component-model.md | `defineComponent` args, reactivity flow, re-exported signal API | -| effects.md | Which effect to use when: `setText`, `setAttribute`, `on`, `pass`, etc. | -| parsers.md | Which parser to use when: `asString`, `asBoolean`, `asInteger`, `read`, etc. | -| coordination.md | `pass()`, `provideContexts`/`requestContext`, `on()` on host, `all()` | -| markup.md | HTML structure: progressive enhancement, semantic nesting, variant examples | -| styling.md | CSS: host scoping, nesting, custom properties, variant modifier classes | -| documentation.md | What to document and how: property tables, descendant tables, standard Markdown | -| testing.md | Framework-agnostic testing patterns for Le Truc components | -| anti-patterns.md | What to avoid: TypeScript, HTML, CSS, and documentation anti-patterns | -| accessibility.md | ARIA roles, native semantics, ARIA APG patterns | - - - -All in `workflows/`: - -| Workflow | Purpose | -|---|---| -| build.md | Create a new component (all four files) | -| review.md | Review or extend an existing component | -| debug.md | Diagnose and fix unexpected reactive behavior | - diff --git a/.claude/skills/le-truc/references/accessibility.md b/.claude/skills/le-truc/references/accessibility.md deleted file mode 100644 index 695fa83..0000000 --- a/.claude/skills/le-truc/references/accessibility.md +++ /dev/null @@ -1,101 +0,0 @@ - -Accessibility requirements for Le Truc components. - - -## Prefer native semantics over custom ARIA - -Use native elements whenever they exist for the widget type. Native elements come with built-in keyboard behavior, focus management, form participation, and AT support: - -| Widget | Use this | Not this | -|---|---|---| -| Button | ` - -``` - -## Focus management - -- Dialogs: move focus to the first focusable element when opened; restore focus to the trigger when closed; trap focus while open -- Menus: focus moves with arrow keys; Escape closes and returns focus to trigger -- Tab panels: Tab moves between the tablist and the active panel; arrow keys move between tabs - -Use `host.focus()` or `element.focus()` inside event handlers when focus needs to move programmatically. - -## Focus styles - -Never suppress focus outlines globally. Use `:focus-visible` to show focus styles only for keyboard navigation: - -```css -my-component { - & label:has(:focus-visible) { - box-shadow: 0 0 var(--space-xxs) 2px var(--color-selection); - } - - & input:focus { - outline: none; /* only acceptable when replaced by the label's box-shadow above */ - } -} -``` - -## Live regions - -When content updates without a page navigation (loading states, validation messages, notifications), use `aria-live` so screen readers announce the change: - -```html - - - -``` - -`role="status"` is equivalent to `aria-live="polite"`. Use `aria-live="assertive"` (or `role="alert"`) only for urgent errors that interrupt the user. diff --git a/.claude/skills/le-truc/references/anti-patterns.md b/.claude/skills/le-truc/references/anti-patterns.md deleted file mode 100644 index 1c3f310..0000000 --- a/.claude/skills/le-truc/references/anti-patterns.md +++ /dev/null @@ -1,201 +0,0 @@ - -Patterns to avoid in Le Truc components — TypeScript, HTML, CSS, and documentation. - - -## TypeScript anti-patterns - -### Querying outside the host's subtree - -```typescript -// ✗ Never: reaches outside the component's DOM boundary -document.querySelector('.other-component') -this.parentElement.querySelector('button') -document.getElementById('some-id') -``` - -The only legal interface between a component and the outside world is `host` and (for consumer components) context. - -### Directly reading or writing inner elements of a child component - -```typescript -// ✗ Never: bypasses the child's reactive system -const child = first('child-component') -child.querySelector('button').disabled = true - -// ✅ Use pass() or setProperty() on the child component itself -``` - -### Sibling communication - -```typescript -// ✗ Never: siblings cannot talk to each other -const sibling = document.querySelector('other-component') -sibling.value = this.value -``` - -Lift shared state to a common ancestor and pass it down. - -### Unbranded custom parsers - -```typescript -// ✗ Unreliable: default parameters reduce fn.length -const myParser = (ui, value = '') => value.trim() - -// ✅ Always use asParser() -const myParser = asParser((ui, value = '') => value.trim()) -``` - -### Unbranded method producers - -```typescript -// ✗ Not recognised as a MethodProducer — treated as a Reader -{ - add: (ui) => { ui.host.add = () => { /* … */ } } -} - -// ✅ Wrap with asMethod() -{ - add: asMethod((ui) => { ui.host.add = () => { /* … */ } }) -} -``` - -### Custom effects without a cleanup function - -```typescript -// ✗ Memory leak: listener never removed -(host, target) => { - target.addEventListener('resize', handler) -} - -// ✅ Return a cleanup function -(host, target) => { - target.addEventListener('resize', handler) - return () => target.removeEventListener('resize', handler) -} -``` - -### Missing TypeScript types on Props and UI - -```typescript -// ✗ No type safety — parsers and UI queries are the main type-safety mechanism -defineComponent('my-component', { label: asString() }, …) - -// ✅ Explicit generic types -defineComponent('my-component', …) -``` - -### Overloading one component with unrelated state - -```typescript -// ✗ "God component" — split into focused components -{ - userAvatar: asString(), - cartItemCount: asInteger(), - notificationCount: asInteger(), - isMenuOpen: asBoolean(), -} -``` - -### `dangerouslySetInnerHTML` on untrusted content - -```typescript -// ✗ XSS risk -content: dangerouslySetInnerHTML('userGeneratedHtml') - -// ✅ Only on server-rendered or pre-sanitised HTML -content: dangerouslySetInnerHTML('highlightedCode') -``` - -### `pass()` on non-Le-Truc elements - -```typescript -// ✗ pass() bypasses non-Le-Truc change detection — the child never updates -'lit-element': pass({ disabled: hostSignal }) - -// ✅ Use setProperty() for anything that isn't a Le Truc component -'lit-element': setProperty('disabled') -``` - -## HTML anti-patterns - -### Empty custom element (no descendant content) - -```html - - - - - -``` - -### `
` where a semantic element exists - -```html - - -
Submit
-
- - - - - -``` - -### Inline styles or event handlers - -```html - - -``` - -## CSS anti-patterns - -### Hardcoded values - -```css -/* ✗ */ -my-component button { background-color: #4a90d9; padding: 8px 16px; } - -/* ✅ */ -my-component button { background-color: var(--color-primary); padding: var(--space-s) var(--space-m); } -``` - -### Bare selectors (not scoped to the host) - -```css -/* ✗ Leaks to the whole page */ -button { border-radius: 4px; } - -/* ✅ Scoped */ -my-component button { border-radius: var(--space-xs); } -``` - -### Styling inner elements of a child component - -```css -/* ✗ Encapsulation violation */ -my-parent child-component button { color: red; } - -/* ✅ Use a CSS class or custom property on the child host */ -my-parent child-component { --button-color: red; } -``` - -## Documentation anti-patterns - -### Documenting implementation details - -```markdown - -Uses createSlot internally to manage the backing signal. - - -``` - -### Incorrect defaults - -The "Default" column in the Reactive Properties table must match the actual fallback in the TypeScript source. Check `asString('')`, `asInteger(0)`, `asBoolean()` (→ `false`), etc. - -### Missing required sections - -Every component docs file must include Description, Tag Name, and Descendant Elements (even if empty — though a component with no descendants would be unusual). diff --git a/.claude/skills/le-truc/references/component-model.md b/.claude/skills/le-truc/references/component-model.md deleted file mode 100644 index 4d53509..0000000 --- a/.claude/skills/le-truc/references/component-model.md +++ /dev/null @@ -1,84 +0,0 @@ - -The Le Truc component model: `defineComponent`, the reactivity flow, and the signal types re-exported from `@zeix/cause-effect`. - - -## `defineComponent(name, props, select, setup)` - -The single entry point for creating a reactive custom element. - -| Argument | Type | Purpose | -|---|---|---| -| `name` | `string` | Tag name — lowercase, must contain a hyphen | -| `props` | `Record` | Reactive property definitions (see below) | -| `select` | `({ first, all }) => UI` | Queries the host's subtree; returns named DOM element references | -| `setup` | `(ui) => Effects` | Returns reactive effects keyed by UI element name | - -The `ui` object passed to `setup` contains everything from `select` plus `host` (the element itself). - -### Props initializers - -| Initializer kind | How to recognize | When to use | -|---|---|---| -| Parser | Two-argument function; always wrap with `asParser()` | Attribute-driven prop: attribute string → typed value; auto-added to `observedAttributes` | -| Reader | One-argument function (not wrapped with `asParser()`) | DOM-derived initial value read once at connect time | -| MethodProducer | Function wrapped with `asMethod()` | Side-effect initializer that installs a method on `host` | -| Signal | Already a `Signal` | Re-use an existing signal from a parent or context | -| Static value | Anything else | Fixed initial value for the prop | - -### Effects return map - -`setup` returns a plain object. Keys are UI element names (from `select`) plus `host`. Values are one Effect or an array of Effects for that element. - -```typescript -({ host }) => ({ - button: setProperty('disabled'), // one effect - label: [setText('label'), toggleClass('active', 'isActive')], // multiple effects - host: on('keydown', handler), // effect on the host element itself -}) -``` - -## Reactivity flow - -``` -attribute change - ↓ - parser(ui, attrValue) - ↓ - host.prop = parsed value ← Signal backed by a Slot - ↓ - effect reads host.prop ← registers dependency automatically - ↓ - DOM update on target element - ↓ - event handler fires - ↓ - { prop: value } returned ← or host.prop = value directly - ↓ - signal.set(value) → effect re-runs -``` - -Key timing: effects run after all child custom elements in the subtree are defined (or after a 200ms timeout). - -## `undefined` vs `null` from effects - -- `undefined` — restore the original DOM value captured at setup time (not blank/null) -- `null` — delete the DOM value (remove the attribute, remove the style property) - -## Re-exported signal types - -Le Truc re-exports the full `@zeix/cause-effect` public API. Import everything from `@zeix/le-truc`: - -```typescript -import { - createState, createMemo, createSensor, createTask, - createEffect, createScope, createSlot, createStore, - createList, createCollection, deriveCollection, - batch, untrack, unown, match, - type State, type Memo, type Sensor, type Slot, -} from '@zeix/le-truc' -``` - -For detailed signal type guidance, use the `cause-effect` skill. Essential constraints: -- All signal generics require `T extends {}` — no `null` or `undefined` in the type parameter -- `createEffect` must be inside a `createScope` or another effect -- Use wrapper types or sentinel values to represent absence diff --git a/.claude/skills/le-truc/references/coordination.md b/.claude/skills/le-truc/references/coordination.md deleted file mode 100644 index 0504a4f..0000000 --- a/.claude/skills/le-truc/references/coordination.md +++ /dev/null @@ -1,98 +0,0 @@ - -How Le Truc components communicate. Choose one mechanism per relationship. - - -## Decision guide - -| Relationship | Use | -|---|---| -| Parent owns a named Le Truc child, shares a signal directly | `pass()` | -| Ancestor provides shared state to a subtree of unknown depth | `provideContexts` / `requestContext` | -| Parent receives events that bubble from any child | `on(type, handler)` on `host` | -| Parent drives effects on all current and future matching descendants | `all(selector)` | -| Two siblings need to share state | **Not possible directly** — lift state to a common ancestor | - -## `pass(props)` — parent controls a Le Truc child - -Replaces the backing `Slot` signal of a descendant Le Truc component's prop with a signal from the parent. The child's prop then tracks the parent signal directly — zero intermediate effect. - -```typescript -// In the parent's setup function: -({ host }) => ({ - 'child-component': pass({ disabled: () => host.disabled }), -}) -``` - -`pass()` is an Effect: it belongs in the `setup` return, keyed to the child's selector or a UI map key that resolves to the child element. - -**Scope: Le Truc components only.** For Lit, Stencil, FAST, plain custom elements, or native elements, use `setProperty()` instead — `pass()` bypasses external frameworks' change-detection and has no effect on them. - -When the parent disconnects, the original signal is restored to the child (the child regains its independent state). - -## `provideContexts` / `requestContext` — shared ancestor state - -Implements the [W3C Community Protocol for Context](https://github.com/webcomponents-cg/community-protocols/blob/main/proposals/context.md). Descendants do not know the depth of the provider. - -**Provider** (ancestor): - -```typescript -// In props, expose the properties to share -{ - theme: asString('light'), - locale: asString('en'), -} - -// In setup, return provideContexts on host -({ host }) => ({ - host: provideContexts(['theme', 'locale']), -}) -``` - -**Consumer** (descendant): - -```typescript -// In props, request the context value -{ - theme: requestContext('theme', 'light'), // fallback if no provider -} - -// No setup entry needed — the prop is a Memo backed by the provider -``` - -Use for: data-fetch scopes, auth state, locale, theme, any value shared across an unknown subtree depth. - -## `on(type, handler)` on `host` — receive bubbled events - -An `on()` effect keyed to `host` receives any event of that type that bubbles from within the component's subtree, regardless of which child dispatched it. - -```typescript -({ host }) => ({ - host: on('item-selected', (e: CustomEvent<{ id: string }>) => { - host.selectedId = e.detail.id - }), -}) -``` - -Use when: a parent needs to respond to any child of a particular type without knowing exactly which child fired. - -## `all(selector)` — drive effects on a dynamic set of descendants - -`all(selector)` returns a `Memo` backed by a lazy `MutationObserver`. When used as a UI map key, `runEffects` wraps the effect loop in a `createEffect` — effects run for every current and future matching element. - -```typescript -// In select: -({ all }) => ({ - items: all('[role="option"]'), -}) - -// In setup: -({ host }) => ({ - items: toggleClass('selected', el => host.selectedId === el.dataset.id), -}) -``` - -The MutationObserver is lazy: it only activates when the Memo is read inside a reactive effect. It watches attribute changes implied by the selector (class, id, `[attr]` patterns) plus `childList`/`subtree`. - -## No sibling communication - -If two components in different branches need to share state, lift the state to a common ancestor and use `pass()` or context downward. Direct sibling-to-sibling communication is not supported and is an anti-pattern. diff --git a/.claude/skills/le-truc/references/documentation.md b/.claude/skills/le-truc/references/documentation.md deleted file mode 100644 index 335e383..0000000 --- a/.claude/skills/le-truc/references/documentation.md +++ /dev/null @@ -1,134 +0,0 @@ - -What to document for each Le Truc component and how to structure it. -Use standard Markdown only — no Markdoc tags or custom syntax. - - -## Document structure - -A component documentation file uses the following sections. Include a section only when it applies to the component. - -### Required sections (always include) - -**Description** — a single paragraph explaining what the component does and which Le Truc patterns it demonstrates. - -**Tag name** — the custom element tag name, formatted as an inline code block. - -**Descendant elements** — a table listing every element the component queries via `first()` or `all()`. Required elements are essential for the component to function; optional elements unlock additional behavior. - -### Conditional sections (include when applicable) - -**Reactive properties** — when the component has JS-accessible reactive props beyond what attributes cover. List all props. - -**Attributes** — when one or more props are driven by HTML attributes (parser-initialised). List the attribute names and behavior. This is separate from reactive properties because HTML authors set attributes; JS consumers set properties. - -**CSS classes** — when the component supports modifier classes on the host for visual variants. - -**Methods** — when the component installs imperative methods on `host` via `asMethod()`. - -**Events** — when the component dispatches custom events. - -## Section formats - -### Description - -```markdown -### My Component - -One paragraph. State what it does, then name the key patterns it demonstrates -(e.g., `read()` with `asInteger()`, `on('click')`, `pass()`, `provideContexts`). -``` - -### Tag name - -```markdown -#### Tag Name - -`my-component` -``` - -### Reactive properties - -```markdown -#### Reactive Properties - -| Name | Type | Default | Description | -|---|---|---|---| -| `count` | `number` (integer) | `0` | Current count | -| `disabled` | `boolean` | `false` | Whether the control is disabled | -| `label` | `string` | `''` | Label text | -``` - -`readonly` properties (sensor-driven) should note they cannot be set from outside: - -```markdown -| `checked` | `boolean` (readonly) | `false` | Reflects the checked state of the native input | -``` - -### Attributes - -```markdown -#### Attributes - -| Name | Description | -|---|---| -| `disabled` | Boolean attribute; presence sets `disabled` to `true` | -| `max` | Maximum number of items (integer); default `1000` | -| `variant` | One of `"default"`, `"primary"`, `"danger"`; default `"default"` | -``` - -List only attributes that map to parser-initialised props (i.e., the ones in `observedAttributes`). - -### CSS classes - -```markdown -#### CSS Classes - -| Class | Description | -|---|---| -| `primary` | Applies primary action styling to the button | -| `danger` | Applies danger/destructive action styling | -| `ghost` | Transparent background with border | -``` - -### Descendant elements - -```markdown -#### Descendant Elements - -| Selector | Type | Required | Description | -|---|---|---|---| -| `first('button')` | `HTMLButtonElement` | required | The interactive button | -| `first('span.label')` | `HTMLSpanElement` | required | Displays the label text | -| `first('span.badge')` | `HTMLSpanElement` | optional | Displays a secondary count badge | -| `all('[role="option"]')` | `HTMLElement[]` | required | The list of selectable options | -``` - -Use `first(…)` or `all(…)` in the Selector column to match the `select` function in the TypeScript source. Required/optional reflects whether the component logs an error when the element is missing. - -### Methods - -```markdown -#### Methods - -| Name | Signature | Description | -|---|---|---| -| `add` | `(process?: (item: HTMLElement) => void) => void` | Appends a new item cloned from the `