# UI Components Playbook This playbook provides comprehensive guidance for creating and maintaining UI components in the @design.estate/dees-catalog library. Follow these patterns and best practices to ensure consistency, maintainability, and quality. ## Table of Contents 1. [Component Creation Checklist](#component-creation-checklist) 2. [Architectural Patterns](#architectural-patterns) 3. [Component Types and Base Classes](#component-types-and-base-classes) 4. [Theming System](#theming-system) 5. [Event Handling](#event-handling) 6. [State Management](#state-management) 7. [Form Components](#form-components) 8. [Overlay Components](#overlay-components) 9. [Complex Components](#complex-components) 10. [Performance Optimization](#performance-optimization) 11. [Focus Management](#focus-management) 12. [Demo System](#demo-system) 13. [Common Pitfalls and Anti-patterns](#common-pitfalls-and-anti-patterns) 14. [Code Examples](#code-examples) ## Component Creation Checklist When creating a new component, follow this checklist: - [ ] Choose the appropriate base class (`DeesElement` or `DeesInputBase`) - [ ] Use `@customElement('dees-componentname')` decorator - [ ] Implement consistent theming with `cssManager.bdTheme()` - [ ] Create demo function in separate `.demo.ts` file - [ ] Export component from `ts_web/elements/index.ts` - [ ] Use proper TypeScript types and interfaces (prefix with `I` for interfaces, `T` for types) - [ ] Implement proper event handling with bubbling and composition - [ ] Consider mobile responsiveness - [ ] Add focus states for accessibility - [ ] Clean up resources in `destroy()` method - [ ] Follow lowercase naming convention for files - [ ] Add z-index registry support if it's an overlay component ## Architectural Patterns ### Base Component Structure ```typescript import { customElement, property, state, css, TemplateResult, html } from '@design.estate/dees-element'; import { DeesElement } from '@design.estate/dees-element'; import * as cssManager from './00colors.js'; import * as demoFunc from './dees-componentname.demo.js'; @customElement('dees-componentname') export class DeesComponentName extends DeesElement { // Static demo reference public static demo = demoFunc.demoFunc; // Public properties (reactive, can be set via attributes) @property({ type: String }) public label: string = ''; @property({ type: Boolean, reflect: true }) public disabled: boolean = false; // Internal state (reactive, but not exposed as attributes) @state() private internalState: string = ''; // Static styles with theme support public static styles = [ cssManager.defaultStyles, css` :host { display: block; background: ${cssManager.bdTheme('#ffffff', '#09090b')}; } ` ]; // Render method public render(): TemplateResult { return html`
`; } // Lifecycle methods public connectedCallback() { super.connectedCallback(); // Setup that needs DOM access } public async firstUpdated() { // One-time initialization after first render } // Cleanup public destroy() { // Clean up listeners, observers, registrations super.destroy(); } } ``` ### Advanced Patterns #### 1. Separation of Concerns (Complex Components) For complex components like WYSIWYG editors, separate concerns into handler classes: ```typescript export class DeesComplexComponent extends DeesElement { // Orchestrator pattern - main component coordinates handlers private inputHandler: InputHandler; private stateHandler: StateHandler; private renderHandler: RenderHandler; constructor() { super(); this.inputHandler = new InputHandler(this); this.stateHandler = new StateHandler(this); this.renderHandler = new RenderHandler(this); } } ``` #### 2. Singleton Pattern (Global Components) For global UI elements like menus: ```typescript export class DeesGlobalMenu extends DeesElement { private static instance: DeesGlobalMenu; public static getInstance(): DeesGlobalMenu { if (!DeesGlobalMenu.instance) { DeesGlobalMenu.instance = new DeesGlobalMenu(); document.body.appendChild(DeesGlobalMenu.instance); } return DeesGlobalMenu.instance; } } ``` #### 3. Registry Pattern (Z-Index Management) Use centralized registries for global state: ```typescript class ComponentRegistry { private static instance: ComponentRegistry; private registry = new WeakMap(); public register(element: HTMLElement, value: number) { this.registry.set(element, value); } public unregister(element: HTMLElement) { this.registry.delete(element); } } ``` ## Component Types and Base Classes ### Standard Component (extends DeesElement) Use for most UI components: - Buttons, badges, icons - Layout components - Data display components - Overlay components ### Form Input Component (extends DeesInputBase) Use for all form inputs: - Text inputs, dropdowns, checkboxes - Date pickers, file uploads - Rich text editors **Required implementations:** ```typescript export class DeesInputCustom extends DeesInputBase { // Required: Get current value public getValue(): ValueType { return this.value; } // Required: Set value programmatically public setValue(value: ValueType): void { this.value = value; this.changeSubject.next(this); // Notify form } // Optional: Custom validation public async validate(): Promise { // Custom validation logic return true; } } ``` ## Theming System ### DO: Use Theme Functions Always use `cssManager.bdTheme()` for colors that change between themes: ```typescript // ✅ CORRECT background: ${cssManager.bdTheme('#ffffff', '#09090b')}; color: ${cssManager.bdTheme('#000000', '#ffffff')}; border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#333333')}; // ❌ INCORRECT background: #ffffff; // Hard-coded color color: var(--custom-color); // Custom CSS variable ``` ### DO: Use Consistent Color Values Reference shared color constants when possible: ```typescript // From 00colors.ts background: ${cssManager.bdTheme(colors.bright.background, colors.dark.background)}; ``` ## Event Handling ### DO: Dispatch Custom Events Properly ```typescript // ✅ CORRECT - Events bubble and cross shadow DOM this.dispatchEvent(new CustomEvent('dees-componentname-change', { detail: { value: this.value }, bubbles: true, composed: true })); // ❌ INCORRECT - Event won't propagate properly this.dispatchEvent(new CustomEvent('change', { detail: { value: this.value } // Missing bubbles and composed })); ``` ### DO: Use Event Delegation For dynamic content, use event delegation: ```typescript // ✅ CORRECT - Single listener for all items this.addEventListener('click', (e: MouseEvent) => { const item = (e.target as HTMLElement).closest('.item'); if (item) { this.handleItemClick(item); } }); // ❌ INCORRECT - Multiple listeners this.items.forEach(item => { item.addEventListener('click', () => this.handleItemClick(item)); }); ``` ## State Management ### DO: Use Appropriate Property Decorators ```typescript // Public API - use @property @property({ type: String }) public label: string; // Internal state - use @state @state() private isLoading: boolean = false; // Reflect to attribute when needed @property({ type: Boolean, reflect: true }) public disabled: boolean = false; ``` ### DON'T: Manipulate State in Render ```typescript // ❌ INCORRECT - Side effects in render public render() { this.counter++; // Don't modify state return html`
${this.counter}
`; } // ✅ CORRECT - Pure render function public render() { return html`
${this.counter}
`; } ``` ## Form Components ### DO: Extend DeesInputBase All form inputs must extend the base class: ```typescript export class DeesInputNew extends DeesInputBase { // Inherits: key, label, value, required, disabled, validationState } ``` ### DO: Emit Changes Consistently ```typescript private handleInput(e: Event) { this.value = (e.target as HTMLInputElement).value; this.changeSubject.next(this); // Notify form system } ``` ### DO: Support Standard Form Properties ```typescript // All form inputs should support: @property() public key: string; @property() public label: string; @property() public required: boolean = false; @property() public disabled: boolean = false; @property() public validationState: 'valid' | 'warn' | 'invalid'; ``` ## Overlay Components ### DO: Use Z-Index Registry Never hardcode z-index values: ```typescript // ✅ CORRECT import { zIndexRegistry } from './00zindex.js'; public async show() { this.modalZIndex = zIndexRegistry.getNextZIndex(); zIndexRegistry.register(this, this.modalZIndex); this.style.zIndex = `${this.modalZIndex}`; } public async hide() { zIndexRegistry.unregister(this); } // ❌ INCORRECT public async show() { this.style.zIndex = '9999'; // Hardcoded z-index } ``` ### DO: Use Window Layers For modal backdrops: ```typescript import { DeesWindowLayer } from './dees-windowlayer.js'; private windowLayer: DeesWindowLayer; public async show() { this.windowLayer = new DeesWindowLayer(); this.windowLayer.zIndex = zIndexRegistry.getNextZIndex(); document.body.append(this.windowLayer); } ``` ## Complex Components ### DO: Use Handler Classes For complex logic, separate into specialized handlers: ```typescript // wysiwyg/handlers/input.handler.ts export class InputHandler { constructor(private component: DeesInputWysiwyg) {} public handleInput(event: InputEvent) { // Specialized input handling } } // Main component orchestrates export class DeesInputWysiwyg extends DeesInputBase { private inputHandler = new InputHandler(this); } ``` ### DO: Use Programmatic Rendering For performance-critical updates that shouldn't trigger re-renders: ```typescript // ✅ CORRECT - Direct DOM manipulation when needed private updateBlockContent(blockId: string, content: string) { const blockElement = this.shadowRoot.querySelector(`#${blockId}`); if (blockElement) { blockElement.textContent = content; // Direct update } } // ❌ INCORRECT - Triggering full re-render private updateBlockContent(blockId: string, content: string) { this.blocks.find(b => b.id === blockId).content = content; this.requestUpdate(); // Unnecessary re-render } ``` ## Performance Optimization ### DO: Debounce Expensive Operations ```typescript private resizeTimeout: number; private handleResize = () => { clearTimeout(this.resizeTimeout); this.resizeTimeout = window.setTimeout(() => { this.updateLayout(); }, 250); }; ``` ### DO: Use Observers Efficiently ```typescript // Clean up observers public disconnectedCallback() { super.disconnectedCallback(); this.resizeObserver?.disconnect(); this.mutationObserver?.disconnect(); } ``` ### DO: Implement Virtual Scrolling For large lists: ```typescript // Only render visible items private getVisibleItems() { const scrollTop = this.scrollContainer.scrollTop; const containerHeight = this.scrollContainer.clientHeight; const itemHeight = 50; const startIndex = Math.floor(scrollTop / itemHeight); const endIndex = Math.ceil((scrollTop + containerHeight) / itemHeight); return this.items.slice(startIndex, endIndex); } ``` ## Focus Management ### DO: Handle Focus Timing ```typescript // ✅ CORRECT - Wait for render async focusInput() { await this.updateComplete; await new Promise(resolve => requestAnimationFrame(resolve)); this.inputElement?.focus(); } // ❌ INCORRECT - Focus too early focusInput() { this.inputElement?.focus(); // Element might not exist } ``` ### DO: Prevent Focus Loss ```typescript // For global menus constructor() { super(); // Prevent focus loss when clicking menu this.addEventListener('mousedown', (e) => { e.preventDefault(); }); } ``` ### DO: Implement Blur Debouncing ```typescript private blurTimeout: number; private handleBlur = () => { clearTimeout(this.blurTimeout); this.blurTimeout = window.setTimeout(() => { // Check if truly blurred if (!this.contains(document.activeElement)) { this.handleTrueBlur(); } }, 100); }; ``` ## Demo System ### DO: Create Comprehensive Demos Every component needs a demo: ```typescript // dees-button.demo.ts import { html } from '@design.estate/dees-element'; export const demoFunc = () => html` Default Button Primary Button Disabled Danger `; // In component file import * as demoFunc from './dees-button.demo.js'; export class DeesButton extends DeesElement { public static demo = demoFunc.demoFunc; } ``` ### DO: Include All Variants Show all component states and variations in demos: - Default state - Different types/variants - Disabled state - Loading state - Error states - Edge cases (long text, empty content) ## Common Pitfalls and Anti-patterns ### ❌ DON'T: Hardcode Z-Index Values ```typescript // ❌ WRONG this.style.zIndex = '9999'; // ✅ CORRECT this.style.zIndex = `${zIndexRegistry.getNextZIndex()}`; ``` ### ❌ DON'T: Skip Base Classes ```typescript // ❌ WRONG - Form input without base class export class DeesInputCustom extends DeesElement { // Missing standard form functionality } // ✅ CORRECT export class DeesInputCustom extends DeesInputBase { // Inherits all form functionality } ``` ### ❌ DON'T: Forget Theme Support ```typescript // ❌ WRONG background-color: #ffffff; color: #000000; // ✅ CORRECT background-color: ${cssManager.bdTheme('#ffffff', '#09090b')}; color: ${cssManager.bdTheme('#000000', '#ffffff')}; ``` ### ❌ DON'T: Create Components Without Demos ```typescript // ❌ WRONG export class DeesComponent extends DeesElement { // No demo property } // ✅ CORRECT export class DeesComponent extends DeesElement { public static demo = demoFunc.demoFunc; } ``` ### ❌ DON'T: Emit Non-Bubbling Events ```typescript // ❌ WRONG this.dispatchEvent(new CustomEvent('change', { detail: this.value })); // ✅ CORRECT this.dispatchEvent(new CustomEvent('change', { detail: this.value, bubbles: true, composed: true })); ``` ### ❌ DON'T: Skip Cleanup ```typescript // ❌ WRONG public connectedCallback() { window.addEventListener('resize', this.handleResize); } // ✅ CORRECT public connectedCallback() { super.connectedCallback(); window.addEventListener('resize', this.handleResize); } public disconnectedCallback() { super.disconnectedCallback(); window.removeEventListener('resize', this.handleResize); } ``` ### ❌ DON'T: Use Inline Styles for Theming ```typescript // ❌ WRONG
// ✅ CORRECT
// In styles: .themed-container { background-color: ${cssManager.bdTheme('#ffffff', '#000000')}; } ``` ### ❌ DON'T: Forget Mobile Responsiveness ```typescript // ❌ WRONG :host { width: 800px; // Fixed width } // ✅ CORRECT :host { width: 100%; max-width: 800px; } @media (max-width: 768px) { :host { /* Mobile adjustments */ } } ``` ## Code Examples ### Example: Creating a New Button Variant ```typescript // dees-special-button.ts import { customElement, property, css, html } from '@design.estate/dees-element'; import { DeesElement } from '@design.estate/dees-element'; import * as cssManager from './00colors.js'; import * as demoFunc from './dees-special-button.demo.js'; @customElement('dees-special-button') export class DeesSpecialButton extends DeesElement { public static demo = demoFunc.demoFunc; @property({ type: String }) public text: string = 'Click me'; @property({ type: Boolean, reflect: true }) public loading: boolean = false; public static styles = [ cssManager.defaultStyles, css` :host { display: inline-block; } .button { padding: 8px 16px; background: ${cssManager.bdTheme('#0066ff', '#0044cc')}; color: white; border: none; border-radius: 4px; cursor: pointer; transition: all 0.2s; } .button:hover { transform: translateY(-2px); box-shadow: 0 4px 8px ${cssManager.bdTheme('rgba(0,0,0,0.1)', 'rgba(0,0,0,0.3)')}; } :host([loading]) .button { opacity: 0.7; cursor: not-allowed; } ` ]; public render() { return html` `; } private handleClick() { this.dispatchEvent(new CustomEvent('special-click', { bubbles: true, composed: true })); } } ``` ### Example: Creating a Form Input ```typescript // dees-input-special.ts export class DeesInputSpecial extends DeesInputBase { public static demo = demoFunc.demoFunc; public render() { return html` `; } private handleInput(e: Event) { this.value = (e.target as HTMLInputElement).value; this.changeSubject.next(this); } private handleBlur() { this.dispatchEvent(new CustomEvent('blur', { bubbles: true, composed: true })); } public getValue(): string { return this.value; } public setValue(value: string): void { this.value = value; this.changeSubject.next(this); } } ``` ## Summary This playbook represents the collective wisdom and patterns found in the @design.estate/dees-catalog component library. Following these guidelines will help you create components that are: - **Consistent**: Following established patterns - **Maintainable**: Easy to understand and modify - **Performant**: Optimized for real-world use - **Accessible**: Usable by everyone - **Theme-aware**: Supporting light and dark modes - **Well-integrated**: Working seamlessly with the component ecosystem Remember: When in doubt, look at existing components for examples. The codebase itself is the best documentation of these patterns in action.