Compare commits
1 Commits
Author | SHA1 | Date | |
---|---|---|---|
2f17dea480 |
@ -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`
|
|
||||||
<dees-appui-base
|
|
||||||
.appbarMenuItems=${this.getAppBarMenuItems()}
|
|
||||||
.appbarBreadcrumbs=${this.getBreadcrumbs()}
|
|
||||||
.appbarTheme=${'dark'}
|
|
||||||
.appbarUser=${{ name: 'User', status: 'online' }}
|
|
||||||
.mainmenuTabs=${this.getMainMenuTabs()}
|
|
||||||
.mainselectorOptions=${activeView?.menuItems || []}
|
|
||||||
@mainmenu-tab-select=${this.handleMainMenuSelect}
|
|
||||||
@mainselector-option-select=${this.handleSelectorSelect}
|
|
||||||
>
|
|
||||||
<dees-appui-view
|
|
||||||
slot="maincontent"
|
|
||||||
.viewConfig=${activeView}
|
|
||||||
@view-tab-select=${this.handleViewTabSelect}
|
|
||||||
></dees-appui-view>
|
|
||||||
</dees-appui-base>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 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`
|
|
||||||
<dashboard-overview></dashboard-overview>
|
|
||||||
`
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'metrics',
|
|
||||||
iconName: 'tachometer-alt',
|
|
||||||
action: () => console.log('Metrics selected'),
|
|
||||||
content: () => html`
|
|
||||||
<dashboard-metrics></dashboard-metrics>
|
|
||||||
`
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'alerts',
|
|
||||||
iconName: 'bell',
|
|
||||||
action: () => console.log('Alerts selected'),
|
|
||||||
content: () => html`
|
|
||||||
<dashboard-alerts></dashboard-alerts>
|
|
||||||
`
|
|
||||||
}
|
|
||||||
],
|
|
||||||
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<string, IAppView> = new Map();
|
|
||||||
private activeView: IAppView | null = null;
|
|
||||||
private viewCache: Map<string, any> = 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<string> = new Set();
|
|
||||||
|
|
||||||
async loadView(viewId: string): Promise<IAppView> {
|
|
||||||
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<string, any> = 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`
|
|
||||||
<div class="view-content ${getDeviceClass()}">
|
|
||||||
${tab.content()}
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
}))
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
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<any>,
|
|
||||||
componentName: string
|
|
||||||
) => {
|
|
||||||
let loaded = false;
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
if (!loaded) {
|
|
||||||
importFn().then(() => {
|
|
||||||
loaded = true;
|
|
||||||
});
|
|
||||||
return html`<dees-spinner></dees-spinner>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return html`<${componentName}></${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<void>;
|
|
||||||
onDeactivate?: () => Promise<void>;
|
|
||||||
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
|
|
@ -1,6 +1,9 @@
|
|||||||
# @design.estate/dees-catalog
|
# @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.
|
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
|
## Install
|
||||||
To install the `@design.estate/dees-catalog` library, you can use npm or any other compatible JavaScript package manager:
|
To install the `@design.estate/dees-catalog` library, you can use npm or any other compatible JavaScript package manager:
|
||||||
|
|
||||||
|
784
readme.playbook.md
Normal file
784
readme.playbook.md
Normal file
@ -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`
|
||||||
|
<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.
|
@ -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.
|
|
@ -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
|
|
@ -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
|
|
||||||
|
|
||||||
[38;5;231m
|
|
||||||
🔍 Test Discovery[0m
|
|
||||||
[38;5;231m Mode: file[0m
|
|
||||||
[38;5;231m Pattern: test/test.tabs-indicator.browser.ts[0m
|
|
||||||
[38;5;113m Found: 1 test file(s)[0m
|
|
||||||
[38;5;33m
|
|
||||||
▶️ test/test.tabs-indicator.browser.ts (1/1)[0m
|
|
||||||
[38;5;231m Runtime: chromium[0m
|
|
||||||
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
|
|
||||||
[38;5;231m [38;5;116mTest starting: tabs indicator positioning debug[0m[0m
|
|
||||||
[38;5;231m !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!![0m
|
|
||||||
[38;5;231m Using globalThis.tapPromise[0m
|
|
||||||
[38;5;231m !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!![0m
|
|
||||||
connection ended
|
|
||||||
removed connection. 0 sockets remaining.
|
|
||||||
[38;5;33m=> [0m Stopped [38;5;215mtest/test.tabs-indicator.browser.ts[0m chromium instance and server.
|
|
||||||
[38;5;196m
|
|
||||||
⚠️ Error[0m
|
|
||||||
[38;5;196m Only 0 out of 1 completed![0m
|
|
||||||
[38;5;196m
|
|
||||||
⚠️ Error[0m
|
|
||||||
[38;5;196m The amount of received tests and expectedTests is unequal! Therefore the testfile failed[0m
|
|
||||||
[38;5;196m Summary: -1 passed, 1 failed of 0 tests in 2.7s[0m
|
|
||||||
[38;5;231m
|
|
||||||
📊 Test Summary[0m
|
|
||||||
[38;5;231m┌────────────────────────────────┐[0m
|
|
||||||
[38;5;231m│ Total Files: 1 │[0m
|
|
||||||
[38;5;231m│ Total Tests: 0 │[0m
|
|
||||||
[38;5;113m│ Passed: 0 │[0m
|
|
||||||
[38;5;113m│ Failed: 0 │[0m
|
|
||||||
[38;5;231m│ Duration: 4.2s │[0m
|
|
||||||
[38;5;231m└────────────────────────────────┘[0m
|
|
||||||
[38;5;116m
|
|
||||||
⏱️ Performance Metrics:[0m
|
|
||||||
[38;5;231m Average per test: 0ms[0m
|
|
||||||
[38;5;113m
|
|
||||||
ALL TESTS PASSED! 🎉[0m
|
|
||||||
Exited NOT OK!
|
|
||||||
ELIFECYCLE Test failed. See above for more details.
|
|
Reference in New Issue
Block a user