784 lines
18 KiB
Markdown
784 lines
18 KiB
Markdown
![]() |
# 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`
|
||
|
<div class="main-container">
|
||
|
<!-- Component content -->
|
||
|
</div>
|
||
|
`;
|
||
|
}
|
||
|
|
||
|
// 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<HTMLElement, number>();
|
||
|
|
||
|
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<ValueType> {
|
||
|
// 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<boolean> {
|
||
|
// 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`<div>${this.counter}</div>`;
|
||
|
}
|
||
|
|
||
|
// ✅ CORRECT - Pure render function
|
||
|
public render() {
|
||
|
return html`<div>${this.counter}</div>`;
|
||
|
}
|
||
|
```
|
||
|
|
||
|
## Form Components
|
||
|
|
||
|
### DO: Extend DeesInputBase
|
||
|
|
||
|
All form inputs must extend the base class:
|
||
|
|
||
|
```typescript
|
||
|
export class DeesInputNew extends DeesInputBase<string> {
|
||
|
// 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`
|
||
|
<dees-button>Default Button</dees-button>
|
||
|
<dees-button type="primary">Primary Button</dees-button>
|
||
|
<dees-button type="danger" disabled>Disabled Danger</dees-button>
|
||
|
`;
|
||
|
|
||
|
// 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<string> {
|
||
|
// 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
|
||
|
<div style="background-color: ${this.darkMode ? '#000' : '#fff'}">
|
||
|
|
||
|
// ✅ CORRECT
|
||
|
<div class="themed-container">
|
||
|
// 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`
|
||
|
<button class="button" ?disabled=${this.loading} @click=${this.handleClick}>
|
||
|
${this.loading ? html`<dees-spinner size="small"></dees-spinner>` : this.text}
|
||
|
</button>
|
||
|
`;
|
||
|
}
|
||
|
|
||
|
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<string> {
|
||
|
public static demo = demoFunc.demoFunc;
|
||
|
|
||
|
public render() {
|
||
|
return html`
|
||
|
<dees-label .label=${this.label} .required=${this.required}>
|
||
|
<input
|
||
|
type="text"
|
||
|
.value=${this.value || ''}
|
||
|
?disabled=${this.disabled}
|
||
|
@input=${this.handleInput}
|
||
|
@blur=${this.handleBlur}
|
||
|
/>
|
||
|
</dees-label>
|
||
|
`;
|
||
|
}
|
||
|
|
||
|
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.
|