diff --git a/readme.appui-architecture.md b/readme.appui-architecture.md deleted file mode 100644 index 7f1f7ac..0000000 --- a/readme.appui-architecture.md +++ /dev/null @@ -1,513 +0,0 @@ -# Building Applications with dees-appui Architecture - -## Overview - -The dees-appui system provides a comprehensive framework for building desktop-style web applications with a consistent layout, navigation, and view management system. This document outlines the architecture and best practices for building applications using these components. - -## Core Architecture - -### Component Hierarchy - -``` -dees-appui-base -├── dees-appui-appbar (top menu bar) -├── dees-appui-mainmenu (left sidebar - primary navigation) -├── dees-appui-mainselector (second sidebar - contextual navigation) -├── dees-appui-maincontent (main content area) -│ └── dees-appui-view (view container) -│ └── dees-appui-tabs (tab navigation within views) -└── dees-appui-activitylog (right sidebar - optional) -``` - -### View-Based Architecture - -The system is built around the concept of **Views** - self-contained modules that represent different sections of your application. Each view can have: - -- Its own tabs for sub-navigation -- Menu items for the selector (contextual navigation) -- Content areas with dynamic loading -- State management -- Event handling - -## Implementation Plan - -### Phase 1: Application Shell Setup - -```typescript -// app-shell.ts -import { LitElement, html, css } from 'lit'; -import { customElement, property } from 'lit/decorators.js'; -import type { IAppView } from '@design.estate/dees-catalog'; - -@customElement('my-app-shell') -export class MyAppShell extends LitElement { - @property({ type: Array }) - views: IAppView[] = []; - - @property({ type: String }) - activeViewId: string = ''; - - render() { - const activeView = this.views.find(v => v.id === this.activeViewId); - - return html` - - - - `; - } -} -``` - -### Phase 2: View Definition - -```typescript -// views/dashboard-view.ts -export const dashboardView: IAppView = { - id: 'dashboard', - name: 'Dashboard', - description: 'System overview and metrics', - iconName: 'home', - tabs: [ - { - key: 'overview', - iconName: 'chart-line', - action: () => console.log('Overview selected'), - content: () => html` - - ` - }, - { - key: 'metrics', - iconName: 'tachometer-alt', - action: () => console.log('Metrics selected'), - content: () => html` - - ` - }, - { - key: 'alerts', - iconName: 'bell', - action: () => console.log('Alerts selected'), - content: () => html` - - ` - } - ], - menuItems: [ - { key: 'Time Range', action: () => showTimeRangeSelector() }, - { key: 'Refresh Rate', action: () => showRefreshSettings() }, - { key: 'Export Data', action: () => exportDashboardData() } - ] -}; -``` - -### Phase 3: View Management System - -```typescript -// services/view-manager.ts -export class ViewManager { - private views: Map = new Map(); - private activeView: IAppView | null = null; - private viewCache: Map = new Map(); - - registerView(view: IAppView) { - this.views.set(view.id, view); - } - - async activateView(viewId: string) { - const view = this.views.get(viewId); - if (!view) throw new Error(`View ${viewId} not found`); - - // Deactivate current view - if (this.activeView) { - await this.deactivateView(this.activeView.id); - } - - // Activate new view - this.activeView = view; - - // Update navigation - this.updateMainSelector(view.menuItems); - this.updateBreadcrumbs(view); - - // Load view data if needed - if (!this.viewCache.has(viewId)) { - await this.loadViewData(view); - } - - return view; - } - - private async loadViewData(view: IAppView) { - // Implement lazy loading of view data - const viewData = await import(`./views/${view.id}/data.js`); - this.viewCache.set(view.id, viewData); - } -} -``` - -### Phase 4: Navigation Integration - -```typescript -// navigation/app-navigation.ts -export class AppNavigation { - constructor( - private viewManager: ViewManager, - private appShell: MyAppShell - ) {} - - setupMainMenu(): ITab[] { - return [ - { - key: 'dashboard', - iconName: 'home', - action: () => this.navigateToView('dashboard') - }, - { - key: 'projects', - iconName: 'folder', - action: () => this.navigateToView('projects') - }, - { - key: 'analytics', - iconName: 'chart-bar', - action: () => this.navigateToView('analytics') - }, - { - key: 'settings', - iconName: 'cog', - action: () => this.navigateToView('settings') - } - ]; - } - - async navigateToView(viewId: string) { - const view = await this.viewManager.activateView(viewId); - this.appShell.activeViewId = viewId; - - // Update URL - window.history.pushState( - { viewId }, - view.name, - `/${viewId}` - ); - } - - handleBrowserNavigation() { - window.addEventListener('popstate', (event) => { - if (event.state?.viewId) { - this.navigateToView(event.state.viewId); - } - }); - } -} -``` - -### Phase 5: Dynamic View Loading - -```typescript -// views/view-loader.ts -export class ViewLoader { - private loadedViews: Set = new Set(); - - async loadView(viewId: string): Promise { - if (this.loadedViews.has(viewId)) { - return this.getViewConfig(viewId); - } - - // Dynamic import - const viewModule = await import(`./views/${viewId}/index.js`); - const viewConfig = viewModule.default as IAppView; - - // Register custom elements if needed - if (viewModule.registerElements) { - await viewModule.registerElements(); - } - - this.loadedViews.add(viewId); - return viewConfig; - } - - async preloadViews(viewIds: string[]) { - const promises = viewIds.map(id => this.loadView(id)); - await Promise.all(promises); - } -} -``` - -## Best Practices - -### 1. View Organization - -``` -src/ -├── views/ -│ ├── dashboard/ -│ │ ├── index.ts # View configuration -│ │ ├── data.ts # Data fetching/management -│ │ ├── components/ # View-specific components -│ │ │ ├── dashboard-overview.ts -│ │ │ ├── dashboard-metrics.ts -│ │ │ └── dashboard-alerts.ts -│ │ └── styles.ts # View-specific styles -│ ├── projects/ -│ │ └── ... -│ └── settings/ -│ └── ... -├── services/ -│ ├── view-manager.ts -│ ├── navigation.ts -│ └── state-manager.ts -└── app-shell.ts -``` - -### 2. State Management - -```typescript -// services/state-manager.ts -export class StateManager { - private viewStates: Map = new Map(); - - saveViewState(viewId: string, state: any) { - this.viewStates.set(viewId, { - ...this.getViewState(viewId), - ...state, - lastUpdated: Date.now() - }); - } - - getViewState(viewId: string): any { - return this.viewStates.get(viewId) || {}; - } - - // Persist to localStorage - persistState() { - const serialized = JSON.stringify( - Array.from(this.viewStates.entries()) - ); - localStorage.setItem('app-state', serialized); - } - - restoreState() { - const saved = localStorage.getItem('app-state'); - if (saved) { - const entries = JSON.parse(saved); - this.viewStates = new Map(entries); - } - } -} -``` - -### 3. View Communication - -```typescript -// events/view-events.ts -export class ViewEventBus { - private eventTarget = new EventTarget(); - - emit(eventName: string, detail: any) { - this.eventTarget.dispatchEvent( - new CustomEvent(eventName, { detail }) - ); - } - - on(eventName: string, handler: (detail: any) => void) { - this.eventTarget.addEventListener(eventName, (e: CustomEvent) => { - handler(e.detail); - }); - } - - // Cross-view communication - sendMessage(fromView: string, toView: string, message: any) { - this.emit('view-message', { - from: fromView, - to: toView, - message - }); - } -} -``` - -### 4. Responsive Design - -```typescript -// views/responsive-view.ts -export const createResponsiveView = (config: IAppView): IAppView => { - return { - ...config, - tabs: config.tabs.map(tab => ({ - ...tab, - content: () => html` -
- ${tab.content()} -
- ` - })) - }; -}; - -function getDeviceClass(): string { - const width = window.innerWidth; - if (width < 768) return 'mobile'; - if (width < 1024) return 'tablet'; - return 'desktop'; -} -``` - -### 5. Performance Optimization - -```typescript -// optimization/lazy-components.ts -export const lazyComponent = ( - importFn: () => Promise, - componentName: string -) => { - let loaded = false; - - return () => { - if (!loaded) { - importFn().then(() => { - loaded = true; - }); - return html``; - } - - return html`<${componentName}>`; - }; -}; - -// Usage in view -tabs: [ - { - key: 'heavy-component', - content: lazyComponent( - () => import('./components/heavy-component.js'), - 'heavy-component' - ) - } -] -``` - -## Advanced Features - -### 1. View Permissions - -```typescript -interface IAppViewWithPermissions extends IAppView { - requiredPermissions?: string[]; - visibleTo?: (user: User) => boolean; -} - -class PermissionManager { - canAccessView(view: IAppViewWithPermissions, user: User): boolean { - if (view.visibleTo) { - return view.visibleTo(user); - } - - if (view.requiredPermissions) { - return view.requiredPermissions.every( - perm => user.permissions.includes(perm) - ); - } - - return true; - } -} -``` - -### 2. View Lifecycle Hooks - -```typescript -interface IAppViewLifecycle extends IAppView { - onActivate?: () => Promise; - onDeactivate?: () => Promise; - onTabChange?: (oldTab: string, newTab: string) => void; - onDestroy?: () => void; -} -``` - -### 3. Dynamic Menu Generation - -```typescript -class DynamicMenuBuilder { - buildMainMenu(views: IAppView[], user: User): ITab[] { - return views - .filter(view => this.canShowInMenu(view, user)) - .map(view => ({ - key: view.id, - iconName: view.iconName || 'file', - action: () => this.navigation.navigateToView(view.id) - })); - } - - buildSelectorMenu(view: IAppView, context: any): ISelectionOption[] { - const baseItems = view.menuItems || []; - const contextItems = this.getContextualItems(view, context); - - return [...baseItems, ...contextItems]; - } -} -``` - -## Migration Strategy - -For existing applications: - -1. **Identify Views**: Map existing routes/pages to views -2. **Extract Components**: Move page-specific components into view folders -3. **Define View Configs**: Create IAppView configurations -4. **Update Navigation**: Replace existing routing with view navigation -5. **Migrate State**: Move page state to ViewManager -6. **Test & Optimize**: Ensure smooth transitions and performance - -## Example Application Structure - -```typescript -// main.ts -import { ViewManager } from './services/view-manager.js'; -import { AppNavigation } from './services/navigation.js'; -import { dashboardView } from './views/dashboard/index.js'; -import { projectsView } from './views/projects/index.js'; -import { settingsView } from './views/settings/index.js'; - -const app = new MyAppShell(); -const viewManager = new ViewManager(); -const navigation = new AppNavigation(viewManager, app); - -// Register views -viewManager.registerView(dashboardView); -viewManager.registerView(projectsView); -viewManager.registerView(settingsView); - -// Setup navigation -app.views = [dashboardView, projectsView, settingsView]; -navigation.setupMainMenu(); -navigation.handleBrowserNavigation(); - -// Initial navigation -navigation.navigateToView('dashboard'); - -document.body.appendChild(app); -``` - -This architecture provides: -- **Modularity**: Each view is self-contained -- **Scalability**: Easy to add new views -- **Performance**: Lazy loading and caching -- **Consistency**: Unified navigation and layout -- **Flexibility**: Customizable per view -- **Maintainability**: Clear separation of concerns \ No newline at end of file diff --git a/readme.md b/readme.md index f7eda81..cc833a6 100644 --- a/readme.md +++ b/readme.md @@ -1,6 +1,9 @@ # @design.estate/dees-catalog A comprehensive web components library built with TypeScript and LitElement, providing 75+ UI components for building modern web applications with consistent design and behavior. +## Development Guide +For developers working on this library, please refer to the [UI Components Playbook](readme.playbook.md) for comprehensive patterns, best practices, and architectural guidelines. + ## Install To install the `@design.estate/dees-catalog` library, you can use npm or any other compatible JavaScript package manager: diff --git a/readme.playbook.md b/readme.playbook.md new file mode 100644 index 0000000..0d4cd47 --- /dev/null +++ b/readme.playbook.md @@ -0,0 +1,784 @@ +# 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. \ No newline at end of file diff --git a/readme.refactoring-summary.md b/readme.refactoring-summary.md deleted file mode 100644 index d8443b5..0000000 --- a/readme.refactoring-summary.md +++ /dev/null @@ -1,138 +0,0 @@ -# WYSIWYG Editor Refactoring Progress Summary - -## Latest Updates - -### Selection Highlighting Fix ✅ -- **Issue**: "Paragraphs are not highlighted consistently, headings are always highlighted" -- **Root Cause**: The `shouldUpdate` method in `dees-wysiwyg-block.ts` was using a generic `.block` selector that would match the first element with that class, not necessarily the correct block element -- **Solution**: Changed the selector to be more specific: `.block.${blockType}` which ensures the correct element is found for each block type -- **Result**: All block types now highlight consistently when selected - -### Enter Key Block Creation Fix ✅ -- **Issue**: "When pressing enter and jumping to new block then typing something: The cursor is not at the beginning of the new block and there is content" -- **Root Cause**: Block handlers were rendering content with template syntax `${block.content || ''}` in their render methods, which violates the static HTML principle -- **Solution**: - - Removed all `${block.content}` from render methods in paragraph, heading, quote, and code block handlers - - Content is now set programmatically in the setup() method only when needed - - Fixed `setCursorToStart` and `setCursorToEnd` to always find elements fresh instead of relying on cached `blockElement` -- **Result**: New empty blocks remain truly empty, cursor positioning works correctly - -### Backspace Key Deletion Fix ✅ -- **Issue**: "After typing in a new block, pressing backspace deletes the whole block instead of just the last character" -- **Root Cause**: - 1. `getCursorPositionInElement` was using `element.contains()` which doesn't work across Shadow DOM boundaries - 2. The backspace handler was checking `block.content === ''` which only contains the stored content, not the actual DOM content -- **Solution**: - 1. Fixed `getCursorPositionInElement` to use `containsAcrossShadowDOM` for proper Shadow DOM support - 2. Updated backspace handler to get actual content from DOM using `blockComponent.getContent()` instead of relying on stored `block.content` - 3. Added debug logging to track cursor position and content state -- **Result**: Backspace now correctly deletes individual characters instead of the whole block - -### Arrow Left Navigation Fix ✅ -- **Issue**: "When jumping to the previous block from the beginning of a block with arrow left, the cursor should be at the end of the previous block, not at the start" -- **Root Cause**: Browser's default focus behavior places cursor at the beginning of contenteditable elements, overriding our cursor positioning -- **Solution**: For 'end' position, set up the selection range BEFORE focusing the element: - 1. Create a range pointing to the end of content - 2. Apply the selection - 3. Then focus the element (which preserves the existing selection) - 4. Only use setCursorToEnd for empty blocks -- **Result**: Arrow left navigation now correctly places cursor at the end of the previous block - -## Completed Phases - -### Phase 1: Infrastructure ✅ -- Created modular block handler architecture -- Implemented `IBlockHandler` interface and `BaseBlockHandler` class -- Created `BlockRegistry` for dynamic block type registration -- Set up proper file structure under `blocks/` directory - -### Phase 2: Proof of Concept ✅ -- Successfully migrated divider block as the simplest example -- Validated the architecture works correctly -- Established patterns for block migration - -### Phase 3: Text Blocks ✅ -- **Paragraph Block**: Full editing support with text splitting, selection handling, and cursor tracking -- **Heading Blocks**: All three heading levels (h1, h2, h3) with unified handler -- **Quote Block**: Italic styling with border, full editing capabilities -- **Code Block**: Monospace font, tab handling, plain text paste support -- **List Block**: Bullet/numbered lists with proper list item management - -## Key Achievements - -### 1. Preserved Critical Knowledge -- **Static Rendering**: Blocks use `innerHTML` in `firstUpdated` to prevent focus loss during typing -- **Shadow DOM Selection**: Implemented `containsAcrossShadowDOM` utility for proper selection detection -- **Cursor Position Tracking**: All editable blocks track cursor position across multiple events -- **Content Splitting**: HTML-aware splitting using Range API preserves formatting -- **Focus Management**: Microtask-based focus restoration ensures reliable cursor placement - -### 2. Enhanced Architecture -- Each block type is now self-contained in its own file -- Block handlers are dynamically registered and loaded -- Common functionality is shared through base classes -- Styles are co-located with their block handlers - -### 3. Maintained Functionality -- All keyboard navigation works (arrows, backspace, delete, enter) -- Text selection across Shadow DOM boundaries functions correctly -- Block merging and splitting behave as before -- IME (Input Method Editor) support is preserved -- Formatting shortcuts (Cmd/Ctrl+B/I/U/K) continue to work - -## Code Organization - -``` -ts_web/elements/wysiwyg/ -├── dees-wysiwyg-block.ts (simplified main component) -├── wysiwyg.selection.ts (Shadow DOM selection utilities) -├── wysiwyg.blockregistration.ts (handler registration) -└── blocks/ - ├── index.ts (exports and registry) - ├── block.base.ts (base handler interface) - ├── decorative/ - │ └── divider.block.ts - └── text/ - ├── paragraph.block.ts - ├── heading.block.ts - ├── quote.block.ts - ├── code.block.ts - └── list.block.ts -``` - -## Next Steps - -### Phase 4: Media Blocks (In Progress) -- Image block with upload/drag-drop support -- YouTube block with video embedding -- Attachment block for file uploads - -### Phase 5: Content Blocks -- Markdown block with preview toggle -- HTML block with raw HTML editing - -### Phase 6: Cleanup -- Remove old code from main component -- Optimize bundle size -- Update documentation - -## Technical Improvements - -1. **Modularity**: Each block type is now completely self-contained -2. **Extensibility**: New blocks can be added by creating a handler and registering it -3. **Maintainability**: Files are smaller and focused on single responsibilities -4. **Type Safety**: Strong TypeScript interfaces ensure consistent implementation -5. **Performance**: No degradation in performance; potential for lazy loading in future - -## Migration Pattern - -For future block migrations, follow this pattern: - -1. Create block handler extending `BaseBlockHandler` -2. Implement required methods: `render()`, `setup()`, `getStyles()` -3. Add helper methods for cursor/content management -4. Handle Shadow DOM selection properly using utilities -5. Register handler in `wysiwyg.blockregistration.ts` -6. Test all interactions (typing, selection, navigation) - -The refactoring has been successful in making the codebase more maintainable while preserving all the hard-won functionality and edge case handling from the original implementation. \ No newline at end of file diff --git a/readme.refactoring.md b/readme.refactoring.md deleted file mode 100644 index 737c33b..0000000 --- a/readme.refactoring.md +++ /dev/null @@ -1,82 +0,0 @@ -# WYSIWYG Editor Refactoring - -## Summary of Changes - -This refactoring cleaned up the wysiwyg editor implementation to fix focus, cursor position, and selection issues. - -### Phase 1: Code Organization - -#### 1. Removed Duplicate Code -- Removed duplicate `handleBlockInput` method from main component (was already in inputHandler) -- Removed duplicate `handleBlockKeyDown` method from main component (was already in keyboardHandler) -- Consolidated all input handling in the respective handler classes - -#### 2. Simplified Focus Management -- Removed complex `updated` lifecycle method that was trying to maintain focus -- Simplified `handleBlockBlur` to not immediately close menus -- Added `requestAnimationFrame` to focus operations for better timing -- Removed `slashMenuOpenTime` tracking which was no longer needed - -#### 3. Fixed Slash Menu Behavior -- Changed from `@mousedown` to `@click` events for better UX -- Added proper event prevention to avoid focus loss -- Menu now closes when clicking outside -- Simplified the insertBlock method to close menu first - -### Phase 2: Cursor & Selection Fixes - -#### 4. Enhanced Cursor Position Management -- Added `focusWithCursor()` method to block component for precise cursor positioning -- Improved `handleSlashCommand` to preserve cursor position when menu opens -- Added `getCaretCoordinates()` for accurate menu positioning based on cursor location -- Updated `focusBlock()` to support numeric cursor positions - -#### 5. Fixed Selection Across Shadow DOM -- Added custom `block-text-selected` event to communicate selections across shadow boundaries -- Implemented `handleMouseUp()` in block component to detect selections -- Updated main component to listen for selection events from blocks -- Selection now works properly even with nested shadow DOMs - -#### 6. Improved Slash Menu Close Behavior -- Added optional `clearSlash` parameter to `closeSlashMenu()` -- Escape key now properly clears the slash command -- Clicking outside clears the slash if menu is open -- Selecting an item preserves content and just transforms the block - -### Technical Improvements - -#### Block Component (`dees-wysiwyg-block`) -- Better focus management with immediate focus (removed unnecessary requestAnimationFrame) -- Added cursor position control methods -- Custom event dispatching for cross-shadow-DOM communication -- Improved content handling for different block types - -#### Input Handler -- Preserves cursor position when showing slash menu -- Better caret coordinate calculation for menu positioning -- Ensures focus stays in the block when menu appears - -#### Block Operations -- Enhanced `focusBlock()` to support start/end/numeric positions -- Better timing with requestAnimationFrame for focus operations - -### Key Benefits -- Slash menu no longer causes focus or cursor position loss -- Text selection works properly across shadow DOM boundaries -- Cursor position is preserved when interacting with menus -- Cleaner, more maintainable code structure -- Better separation of concerns - -## Testing - -Use the test files in `.nogit/debug/`: -- `test-slash-menu.html` - Tests slash menu focus behavior -- `test-wysiwyg-formatting.html` - Tests text formatting - -## Known Issues Fixed -- Slash menu disappearing immediately on first "/" -- Focus lost when slash menu opens -- Cursor position lost when typing "/" -- Text selection not working properly -- Selection events not propagating across shadow DOM -- Duplicate event handling causing conflicts \ No newline at end of file diff --git a/test-output.log b/test-output.log deleted file mode 100644 index 25476f4..0000000 --- a/test-output.log +++ /dev/null @@ -1,72 +0,0 @@ - -> @design.estate/dees-catalog@1.10.8 test /mnt/data/lossless/design.estate/dees-catalog -> tstest test/ --web --verbose --timeout 30 --logfile test/test.tabs-indicator.browser.ts - - -🔍 Test Discovery - Mode: file - Pattern: test/test.tabs-indicator.browser.ts - Found: 1 test file(s) - -▶️ test/test.tabs-indicator.browser.ts (1/1) - Runtime: chromium -running spawned compilation process -=======> ESBUILD -{ - cwd: '/mnt/data/lossless/design.estate/dees-catalog', - from: 'test/test.tabs-indicator.browser.ts', - to: '/mnt/data/lossless/design.estate/dees-catalog/.nogit/tstest_cache/test__test.tabs-indicator.browser.ts.js', - mode: 'test', - argv: { bundler: 'esbuild' } -} -switched to /mnt/data/lossless/design.estate/dees-catalog -building for test: -Got no SSL certificates. Please ensure encryption using e.g. a reverse proxy -"/test" maps to 1 handlers - -> GET -"*" maps to 1 handlers - -> GET -now listening on 3007! -Launching puppeteer browser with arguments: -[] -Using executable: /usr/bin/google-chrome -added connection. now 1 sockets connected. -added connection. now 2 sockets connected. -connection ended -removed connection. 1 sockets remaining. -connection ended -removed connection. 0 sockets remaining. -added connection. now 1 sockets connected. -/favicon.ico -could not resolve /mnt/data/lossless/design.estate/dees-catalog/.nogit/tstest_cache/favicon.ico -/test__test.tabs-indicator.browser.ts.js - Test starting: tabs indicator positioning debug - !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - Using globalThis.tapPromise - !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -connection ended -removed connection. 0 sockets remaining. -=>  Stopped test/test.tabs-indicator.browser.ts chromium instance and server. - -⚠️ Error - Only 0 out of 1 completed! - -⚠️ Error - The amount of received tests and expectedTests is unequal! Therefore the testfile failed - Summary: -1 passed, 1 failed of 0 tests in 2.7s - -📊 Test Summary -┌────────────────────────────────┐ -│ Total Files: 1 │ -│ Total Tests: 0 │ -│ Passed: 0 │ -│ Failed: 0 │ -│ Duration: 4.2s │ -└────────────────────────────────┘ - -⏱️ Performance Metrics: - Average per test: 0ms - -ALL TESTS PASSED! 🎉 -Exited NOT OK! - ELIFECYCLE  Test failed. See above for more details.