# Component Development Patterns This document covers standard patterns and conventions for developing Lit components in Common UI. ## Component Categories Common UI components fall into distinct categories, each with specific patterns: ### 1. Layout Components Components that arrange other components without providing content themselves. **Examples:** `ct-vstack`, `ct-hstack`, `ct-screen`, `ct-autolayout` **Characteristics:** - Use flexbox or grid - Accept child elements via slots - Provide gap, alignment, and spacing controls - No theme color consumption (mostly) - Simple property-based configuration **Pattern:** ```typescript export class CTVStack extends BaseElement { static override properties = { gap: { type: String }, align: { type: String }, justify: { type: String }, }; declare gap: string; declare align: string; declare justify: string; override render() { const classes = { stack: true, [`gap-${this.gap}`]: true, [`align-${this.align}`]: true, [`justify-${this.justify}`]: true, }; return html`
`; } } ``` ### 2. Visual Components Components that display content with styling. **Examples:** `ct-label`, `ct-separator`, `ct-skeleton` **Characteristics:** - May consume theme - Provide visual feedback or decoration - Usually simple with few properties **Pattern:** ```typescript export class CTSeparator extends BaseElement { static override properties = { orientation: { type: String }, decorative: { type: Boolean }, }; declare orientation: "horizontal" | "vertical"; declare decorative: boolean; override render() { return html`
`; } } ``` ### 3. Input Components Components that capture user input. **Examples:** `ct-button`, `ct-input`, `ct-checkbox`, `ct-textarea` **Characteristics:** - Consume theme for consistent styling - Emit custom events (use `this.emit()`) - May use `InputTimingController` for debouncing - Handle disabled states **Pattern:** ```typescript export class CTButton extends BaseElement { @consume({ context: themeContext, subscribe: true }) @property({ attribute: false }) declare theme?: CTTheme; static override properties = { variant: { type: String }, disabled: { type: Boolean, reflect: true }, }; override firstUpdated(changed: Map) { super.firstUpdated(changed); this._updateThemeProperties(); } override updated(changed: Map) { super.updated(changed); if (changed.has("theme")) { this._updateThemeProperties(); } } private _updateThemeProperties() { const currentTheme = this.theme || defaultTheme; applyThemeToElement(this, currentTheme); } private _handleClick(e: Event) { if (this.disabled) { e.preventDefault(); e.stopPropagation(); return; } // Emit custom event this.emit("ct-click", { /* detail */ }); } } ``` ### 4. Complex/Integrated Components Components that deeply integrate with the runtime and Cell abstractions. **Examples:** `ct-render`, `ct-list`, `ct-code-editor`, `ct-outliner` **Characteristics:** - Work with Cell properties - Manage subscriptions - Handle transactions for mutations - Complex lifecycle management - May use reactive controllers **Pattern:** See `references/cell-integration.md` for detailed patterns. ## File Structure Each component should follow this structure: ``` ct-component-name/ ├── ct-component-name.ts # Component implementation ├── index.ts # Export and registration └── styles.ts # Optional: extracted styles (for complex components) ``` ### Component Implementation File ```typescript // ct-button.ts import { css, html } from "lit"; import { property } from "lit/decorators.js"; import { BaseElement } from "../../core/base-element.ts"; export type ButtonVariant = "primary" | "secondary" | "destructive"; export class CTButton extends BaseElement { static override styles = [ BaseElement.baseStyles, css` /* component styles */ `, ]; static override properties = { variant: { type: String }, }; declare variant: ButtonVariant; constructor() { super(); this.variant = "primary"; } override render() { return html``; } } globalThis.customElements.define("ct-button", CTButton); ``` ### Index File ```typescript // index.ts import { CTButton, ButtonVariant } from "./ct-button.ts"; if (!customElements.get("ct-button")) { customElements.define("ct-button", CTButton); } export { CTButton }; export type { ButtonVariant }; ``` Note: The conditional check prevents duplicate registration errors during hot module replacement. ## Type Safety ### Export Types Always export types separately: ```typescript export type { ButtonVariant, ButtonSize }; ``` ### Property Type Declarations Use `declare` for typed properties: ```typescript static override properties = { variant: { type: String }, size: { type: String }, disabled: { type: Boolean, reflect: true }, }; declare variant: ButtonVariant; declare size: ButtonSize; declare disabled: boolean; ``` ### Type Imports Import types with `type` keyword when possible: ```typescript import type { Cell } from "@commontools/runner"; import type { CTTheme } from "../theme-context.ts"; ``` ## Styling Conventions ### Base Styles Always extend `BaseElement.baseStyles` for CSS variables: ```typescript static override styles = [ BaseElement.baseStyles, css` /* component styles */ `, ]; ``` ### Box Sizing Always include box-sizing reset: ```css :host { display: block; box-sizing: border-box; } *, *::before, *::after { box-sizing: inherit; } ``` ### CSS Parts Expose major elements as parts for external styling: ```typescript return html` `; ``` ### Class Organization Use `classMap` from `lit/directives/class-map.js` for dynamic classes: ```typescript import { classMap } from "lit/directives/class-map.js"; const classes = { button: true, [this.variant]: true, [this.size]: true, disabled: this.disabled, }; return html``; ``` ## Event Handling ### Emitting Events Use the `emit()` helper from `BaseElement`: ```typescript protected emit( eventName: string, detail?: T, options?: EventInit, ): boolean ``` Events are automatically `bubbles: true` and `composed: true`. **Pattern:** ```typescript private handleChange(newValue: string) { this.emit("ct-change", { value: newValue }); } ``` ### Event Naming - Prefix custom events with `ct-` - Use present tense: `ct-change`, not `ct-changed` - Be specific: `ct-add-item`, `ct-remove-item` ### Event Documentation Document events in JSDoc: ```typescript /** * @fires ct-change - Fired when value changes with detail: { value } * @fires ct-submit - Fired when form is submitted with detail: { formData } */ ``` ## Property Conventions ### Reflecting Properties Use `reflect: true` for boolean states that should be visible in DOM: ```typescript static override properties = { disabled: { type: Boolean, reflect: true }, readonly: { type: Boolean, reflect: true }, }; ``` ### Default Values Set defaults in constructor: ```typescript constructor() { super(); this.variant = "primary"; this.size = "default"; this.disabled = false; } ``` ### Non-Attribute Properties Use `attribute: false` for objects, arrays, and Cells: ```typescript static override properties = { theme: { type: Object, attribute: false }, cell: { attribute: false }, items: { type: Array, attribute: false }, }; ``` ## Lifecycle Methods ### Common Lifecycle Pattern ```typescript override firstUpdated(changed: Map) { super.firstUpdated(changed); // One-time setup this._updateThemeProperties(); this._setupEventListeners(); } override updated(changed: Map) { super.updated(changed); // React to property changes if (changed.has("theme")) { this._updateThemeProperties(); } } override disconnectedCallback() { super.disconnectedCallback(); // Cleanup this._cleanup(); } ``` ## Documentation ### JSDoc Comments Provide comprehensive JSDoc: ```typescript /** * CTButton - Interactive button element with multiple variants * * @element ct-button * * @attr {string} variant - Visual style: "primary" | "secondary" | "destructive" * @attr {string} size - Button size: "default" | "sm" | "lg" | "icon" * @attr {boolean} disabled - Whether the button is disabled * * @slot - Default slot for button content * * @fires ct-click - Fired when button is clicked * * @example * Click Me */ ``` ## Testing Tests should be colocated with components: ``` ct-component/ ├── ct-component.ts ├── ct-component.test.ts └── index.ts ``` ### Basic Test Structure ```typescript import { describe, it } from "@std/testing/bdd"; import { expect } from "@std/expect"; import { CTButton } from "./ct-button.ts"; describe("CTButton", () => { it("should be defined", () => { expect(CTButton).toBeDefined(); }); it("should create element instance", () => { const element = new CTButton(); expect(element).toBeInstanceOf(CTButton); }); it("should have default properties", () => { const element = new CTButton(); expect(element.variant).toBe("primary"); expect(element.disabled).toBe(false); }); }); ``` Run tests with: `deno task test` (NOT `deno test` - the task includes important flags)