--- name: lit-component description: Guide for developing Lit web components in the Common UI v2 system (@commonfabric/ui/v2). Use when creating or modifying cf- prefixed components, implementing theme integration, working with Cell abstractions, or building reactive UI components that integrate with the Common Fabric runtime. --- # Lit Component Development for Common UI This skill provides guidance for developing Lit web components within the Common UI v2 component library (`packages/ui/src/v2`). ## When to Use This Skill Use this skill when: - Creating new `cf-` prefixed components in the UI package - Modifying existing Common UI v2 components - Implementing theme-aware components - Integrating components with Cell abstractions from the runtime - Building reactive components for pattern UIs - Debugging component lifecycle or reactivity issues Do NOT use this skill when authoring or styling pattern UIs that consume `cf-` components — that's `pattern-ui`'s job. Patterns must never touch component internals, and pattern JSX uses `theme={...}`, not Lit's `.theme=${...}`. ## Core Philosophy Common UI is inspired by SwiftUI and emphasizes: 1. **Default Configuration Works**: Components should work together with minimal configuration 2. **Composition Over Control**: Emphasize composing components rather than granular styling 3. **Adaptive to User Preferences**: Respect system preferences and theme settings (theme is ambient context, not explicit props) 4. **Reactive Binding Model**: Integration with FRP-style Cell abstractions from the runtime 5. **Progressive Enhancement**: Components work with plain values but enhance with Cells for reactivity 6. **Separation of Concerns**: Presentation components, theme-aware inputs, Cell-aware state, runtime-integrated operations ## Quick Start Pattern ### 1. Choose Component Category Identify which category the component falls into: - **Layout**: Arranges other components (vstack, hstack, screen) - **Visual**: Displays styled content (separator, skeleton, label) - **Input**: Captures user interaction (button, input, checkbox) - **Complex/Integrated**: Deep runtime integration with Cells (render, code-editor, outliner) **Complexity spectrum:** Components range from pure presentation (no runtime) to deeply integrated (Cell operations, pattern execution, backlink resolution). Choose the simplest pattern that meets requirements. See `references/component-patterns.md` for detailed patterns for each category and `references/advanced-patterns.md` for complex integration patterns. ### 2. Create Component Files Create the component directory structure: ``` packages/ui/src/v2/components/cf-component-name/ ├── cf-component-name.ts # Component implementation ├── index.ts # Export and registration └── styles.ts # Optional: for complex components ``` ### 3. Implement Component Basic template: ```typescript import { css, html } from "lit"; import { BaseElement } from "../../core/base-element.ts"; export class CFComponentName extends BaseElement { static override styles = [ BaseElement.baseStyles, css` :host { display: block; box-sizing: border-box; } *, *::before, *::after { box-sizing: inherit; } `, ]; static override properties = { // Define reactive properties }; constructor() { super(); // Set defaults } override render() { return html` `; } } globalThis.customElements.define("cf-component-name", CFComponentName); ``` ### 4. Create Index File ```typescript import { CFComponentName } from "./cf-component-name.ts"; if (!customElements.get("cf-component-name")) { customElements.define("cf-component-name", CFComponentName); } export { CFComponentName }; export type {}; /* exported types */ ``` Both registrations are the codebase convention, not a contradiction: the component file registers unconditionally when it is imported, and the index file's guarded define is a safe no-op in that case — it only registers when the component module didn't (and prevents duplicate-registration errors during hot module replacement). Keep both. ## Theme Integration Most components should use `var(--cf-theme-*)` CSS variables with fallbacks (`--cf-theme-*` first, then `--cf-*` base token, then a literal). Consume `cfThemeContext` (with `applyThemeToElement`) only when JavaScript needs the theme object for runtime logic, derived values, or applying theme variables to dynamically created elements. **Theme consumption code and complete reference:** See `references/theme-system.md` for the `@consume` boilerplate, all available CSS variables, and helper functions. ## Cell Integration For components that work with reactive runtime data, declare the cell as `@property({ attribute: false })`, subscribe with `cell.sink(() => this.requestUpdate())` when the `cell` property changes, and read with `cell.get()` in `render()` (guarding the no-cell case). The pitfalls that matter: - Clean up the previous subscription before subscribing to a new cell, and unsubscribe in `disconnectedCallback()` (memory leaks) - Check `isCell(this.cell)` before subscribing - Mutate cells through transactions, never directly **Subscription boilerplate and complete Cell patterns:** See `references/cell-integration.md` for subscription management, nested property access with `.key()`, array cell manipulation, transaction-based mutations, and finding cells by equality. ## Reactive Controllers For reusable component behaviors, use reactive controllers. Example: `InputTimingController` for debouncing/throttling: ```typescript import { InputTimingController } from "../../core/input-timing-controller.ts"; export class CFInput extends BaseElement { @property() timingStrategy: "immediate" | "debounce" | "throttle" | "blur" = "debounce"; @property() timingDelay: number = 500; private inputTiming = new InputTimingController(this, { strategy: this.timingStrategy, delay: this.timingDelay, }); private handleInput(event: Event) { const value = (event.target as HTMLInputElement).value; this.inputTiming.schedule(() => { this.emit("cf-change", { value }); }); } } ``` ## Common Patterns ### Event Emission Use the `emit()` helper from `BaseElement`: ```typescript private handleChange(newValue: string) { this.emit("cf-change", { value: newValue }); } ``` Events are automatically `bubbles: true` and `composed: true`. ### Dynamic Classes Use `classMap` for conditional classes: ```typescript import { classMap } from "lit/directives/class-map.js"; const classes = { button: true, [this.variant]: true, disabled: this.disabled, }; return html` `; ``` ### List Rendering Use `repeat` directive with stable keys: ```typescript import { repeat } from "lit/directives/repeat.js"; return html` ${repeat( items, (item) => item.id, // stable key (item) => html`