A comprehensive library that provides dynamic web components for building sophisticated and modern web applications using JavaScript and TypeScript.
npm install @design.estate/dees-catalogThis 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.
1. Component Creation Checklist
2. Architectural Patterns
3. Component Types and Base Classes
4. Theming System
5. Event Handling
6. State Management
7. Form Components
8. Overlay Components
9. Complex Components
10. Performance Optimization
11. Focus Management
12. Demo System
13. Common Pitfalls and Anti-patterns
14. Code Examples
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
``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();
}
}
`$3
#### 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
$3
Use for most UI components:
- Buttons, badges, icons
- Layout components
- Data display components
- Overlay components
$3
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
$3
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
`$3
Reference shared color constants when possible:
`typescript
// From 00colors.ts
background: ${cssManager.bdTheme(colors.bright.background, colors.dark.background)};
`Event Handling
$3
`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
}));
`$3
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
$3
`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;
`$3
`typescript
// ❌ INCORRECT - Side effects in render
public render() {
this.counter++; // Don't modify state
return html;
}// ✅ CORRECT - Pure render function
public render() {
return html
;
}
`Form Components
$3
All form inputs must extend the base class:
`typescript
export class DeesInputNew extends DeesInputBase {
// Inherits: key, label, value, required, disabled, validationState
}
`$3
`typescript
private handleInput(e: Event) {
this.value = (e.target as HTMLInputElement).value;
this.changeSubject.next(this); // Notify form system
}
`$3
`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
$3
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
}
`$3
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
$3
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);
}
`$3
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
$3
`typescript
private resizeTimeout: number;private handleResize = () => {
clearTimeout(this.resizeTimeout);
this.resizeTimeout = window.setTimeout(() => {
this.updateLayout();
}, 250);
};
`$3
`typescript
// Clean up observers
public disconnectedCallback() {
super.disconnectedCallback();
this.resizeObserver?.disconnect();
this.mutationObserver?.disconnect();
}
`$3
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
$3
`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
}
`$3
`typescript
// For global menus
constructor() {
super();
// Prevent focus loss when clicking menu
this.addEventListener('mousedown', (e) => {
e.preventDefault();
});
}
`$3
`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
$3
Every component needs a demo:
`typescript
// dees-button.demo.ts
import { html } from '@design.estate/dees-element';export const demoFunc = () => html
;// In component file
import * as demoFunc from './dees-button.demo.js';
export class DeesButton extends DeesElement {
public static demo = demoFunc.demoFunc;
}
`$3
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
$3
`typescript
// ❌ WRONG
this.style.zIndex = '9999';// ✅ CORRECT
this.style.zIndex =
${zIndexRegistry.getNextZIndex()};
`$3
`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
}
`$3
`typescript
// ❌ WRONG
background-color: #ffffff;
color: #000000;// ✅ CORRECT
background-color: ${cssManager.bdTheme('#ffffff', '#09090b')};
color: ${cssManager.bdTheme('#000000', '#ffffff')};
`$3
`typescript
// ❌ WRONG
export class DeesComponent extends DeesElement {
// No demo property
}// ✅ CORRECT
export class DeesComponent extends DeesElement {
public static demo = demoFunc.demoFunc;
}
`$3
`typescript
// ❌ WRONG
this.dispatchEvent(new CustomEvent('change', {
detail: this.value
}));// ✅ CORRECT
this.dispatchEvent(new CustomEvent('change', {
detail: this.value,
bubbles: true,
composed: true
}));
`$3
`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);
}
`$3
`typescript
// ❌ WRONG
// ✅ CORRECT
// In styles:
.themed-container {
background-color: ${cssManager.bdTheme('#ffffff', '#000000')};
}
`$3
`typescript
// ❌ WRONG
:host {
width: 800px; // Fixed width
}// ✅ CORRECT
:host {
width: 100%;
max-width: 800px;
}
@media (max-width: 768px) {
:host {
/ Mobile adjustments /
}
}
`Code Examples
$3
`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
}));
}
}
`
$3
`typescript
// dees-input-special.ts
export class DeesInputSpecial extends DeesInputBase {
public static demo = demoFunc.demoFunc;
public render() {
return html
type="text"
.value=${this.value || ''}
?disabled=${this.disabled}
@input=${this.handleInput}
@blur=${this.handleBlur}
/>
;
}
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.