@design.estate/dees-catalog 9.5.0 → 9.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist_bundle/bundle.js +1319 -1452
- package/dist_bundle/bundle.js.map +1 -1
- package/dist_ts_web/00_commitinfo_data.js +1 -1
- package/dist_ts_web/elements/00group-dataview/dees-table/dees-table.js +5 -4
- package/dist_ts_web/elements/00group-input/dees-input-base/dees-input-base.d.ts +1 -8
- package/dist_ts_web/elements/00group-input/dees-input-base/dees-input-base.js +7 -2
- package/dist_ts_web/elements/00group-layout/dees-stepper/dees-stepper.d.ts +21 -2
- package/dist_ts_web/elements/00group-layout/dees-stepper/dees-stepper.demo.d.ts +4 -0
- package/dist_ts_web/elements/00group-layout/dees-stepper/dees-stepper.demo.js +218 -193
- package/dist_ts_web/elements/00group-layout/dees-stepper/dees-stepper.js +288 -403
- package/dist_ts_web/elements/00group-layout/dees-stepper/styles.d.ts +1 -0
- package/dist_ts_web/elements/00group-layout/dees-stepper/styles.js +41 -0
- package/dist_ts_web/elements/00group-overlay/dees-contextmenu/dees-contextmenu.d.ts +1 -1
- package/dist_ts_web/elements/00group-overlay/dees-contextmenu/dees-contextmenu.js +5 -4
- package/package.json +2 -2
- package/readme.md +52 -3
- package/scripts/check-bdtheme-ratchet.cjs +2 -2
- package/scripts/check-packed-consumer.mjs +45 -0
- package/ts_web/00_commitinfo_data.ts +1 -1
- package/ts_web/elements/00group-dataview/dees-table/dees-table.ts +3 -2
- package/ts_web/elements/00group-input/dees-input-base/dees-input-base.ts +6 -1
- package/ts_web/elements/00group-layout/dees-stepper/dees-stepper.demo.ts +142 -200
- package/ts_web/elements/00group-layout/dees-stepper/dees-stepper.ts +216 -400
- package/ts_web/elements/00group-layout/dees-stepper/styles.ts +41 -0
- package/ts_web/elements/00group-overlay/dees-contextmenu/dees-contextmenu.ts +3 -3
- package/readme.hints.md +0 -1233
- package/readme.icons.md +0 -1849
- package/readme.info.md +0 -80
- package/readme.plan.md +0 -671
- package/readme.playbook.md +0 -820
- package/readme.theme-migration.md +0 -168
package/readme.playbook.md
DELETED
|
@@ -1,820 +0,0 @@
|
|
|
1
|
-
# UI Components Playbook
|
|
2
|
-
|
|
3
|
-
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.
|
|
4
|
-
|
|
5
|
-
## Table of Contents
|
|
6
|
-
|
|
7
|
-
1. [Component Creation Checklist](#component-creation-checklist)
|
|
8
|
-
2. [Architectural Patterns](#architectural-patterns)
|
|
9
|
-
3. [Component Types and Base Classes](#component-types-and-base-classes)
|
|
10
|
-
4. [Theming System](#theming-system)
|
|
11
|
-
5. [Event Handling](#event-handling)
|
|
12
|
-
6. [State Management](#state-management)
|
|
13
|
-
7. [Form Components](#form-components)
|
|
14
|
-
8. [Overlay Components](#overlay-components)
|
|
15
|
-
9. [Complex Components](#complex-components)
|
|
16
|
-
10. [Performance Optimization](#performance-optimization)
|
|
17
|
-
11. [Focus Management](#focus-management)
|
|
18
|
-
12. [Demo System](#demo-system)
|
|
19
|
-
13. [Common Pitfalls and Anti-patterns](#common-pitfalls-and-anti-patterns)
|
|
20
|
-
14. [Code Examples](#code-examples)
|
|
21
|
-
|
|
22
|
-
## Component Creation Checklist
|
|
23
|
-
|
|
24
|
-
When creating a new component, follow this checklist:
|
|
25
|
-
|
|
26
|
-
- [ ] Choose the appropriate base class (`DeesElement` or `DeesInputBase`)
|
|
27
|
-
- [ ] Use `@customElement('dees-componentname')` decorator
|
|
28
|
-
- [ ] Import `themeDefaultStyles` from `00theme.js` and style with `--dees-*` tokens (`cssManager.bdTheme()` is legacy — see [Theming System](#theming-system))
|
|
29
|
-
- [ ] Create demo function in separate `.demo.ts` file
|
|
30
|
-
- [ ] Export component from `ts_web/elements/index.ts`
|
|
31
|
-
- [ ] Use proper TypeScript types and interfaces (prefix with `I` for interfaces, `T` for types)
|
|
32
|
-
- [ ] Implement proper event handling with bubbling and composition
|
|
33
|
-
- [ ] Consider mobile responsiveness
|
|
34
|
-
- [ ] Add focus states for accessibility
|
|
35
|
-
- [ ] Clean up resources in `destroy()` method
|
|
36
|
-
- [ ] Follow lowercase naming convention for files
|
|
37
|
-
- [ ] Add z-index registry support if it's an overlay component
|
|
38
|
-
|
|
39
|
-
## Architectural Patterns
|
|
40
|
-
|
|
41
|
-
### Base Component Structure
|
|
42
|
-
|
|
43
|
-
```typescript
|
|
44
|
-
import { customElement, property, state, css, TemplateResult, html, cssManager } from '@design.estate/dees-element';
|
|
45
|
-
import { DeesElement } from '@design.estate/dees-element';
|
|
46
|
-
import { themeDefaultStyles } from '../00theme.js';
|
|
47
|
-
import * as demoFunc from './dees-componentname.demo.js';
|
|
48
|
-
|
|
49
|
-
@customElement('dees-componentname')
|
|
50
|
-
export class DeesComponentName extends DeesElement {
|
|
51
|
-
// Static demo reference
|
|
52
|
-
public static demo = demoFunc.demoFunc;
|
|
53
|
-
|
|
54
|
-
// Public properties (reactive, can be set via attributes)
|
|
55
|
-
@property({ type: String })
|
|
56
|
-
public label: string = '';
|
|
57
|
-
|
|
58
|
-
@property({ type: Boolean, reflect: true })
|
|
59
|
-
public disabled: boolean = false;
|
|
60
|
-
|
|
61
|
-
// Internal state (reactive, but not exposed as attributes)
|
|
62
|
-
@state()
|
|
63
|
-
private internalState: string = '';
|
|
64
|
-
|
|
65
|
-
// Static styles with theme support
|
|
66
|
-
public static styles = [
|
|
67
|
-
themeDefaultStyles,
|
|
68
|
-
cssManager.defaultStyles,
|
|
69
|
-
css`
|
|
70
|
-
:host {
|
|
71
|
-
display: block;
|
|
72
|
-
background: var(--dees-color-bg-primary);
|
|
73
|
-
}
|
|
74
|
-
`
|
|
75
|
-
];
|
|
76
|
-
|
|
77
|
-
// Render method
|
|
78
|
-
public render(): TemplateResult {
|
|
79
|
-
return html`
|
|
80
|
-
<div class="main-container">
|
|
81
|
-
<!-- Component content -->
|
|
82
|
-
</div>
|
|
83
|
-
`;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// Lifecycle methods
|
|
87
|
-
public connectedCallback() {
|
|
88
|
-
super.connectedCallback();
|
|
89
|
-
// Setup that needs DOM access
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
public async firstUpdated() {
|
|
93
|
-
// One-time initialization after first render
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// Cleanup
|
|
97
|
-
public destroy() {
|
|
98
|
-
// Clean up listeners, observers, registrations
|
|
99
|
-
super.destroy();
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
```
|
|
103
|
-
|
|
104
|
-
### Advanced Patterns
|
|
105
|
-
|
|
106
|
-
#### 1. Separation of Concerns (Complex Components)
|
|
107
|
-
|
|
108
|
-
For complex components like WYSIWYG editors, separate concerns into handler classes:
|
|
109
|
-
|
|
110
|
-
```typescript
|
|
111
|
-
export class DeesComplexComponent extends DeesElement {
|
|
112
|
-
// Orchestrator pattern - main component coordinates handlers
|
|
113
|
-
private inputHandler: InputHandler;
|
|
114
|
-
private stateHandler: StateHandler;
|
|
115
|
-
private renderHandler: RenderHandler;
|
|
116
|
-
|
|
117
|
-
constructor() {
|
|
118
|
-
super();
|
|
119
|
-
this.inputHandler = new InputHandler(this);
|
|
120
|
-
this.stateHandler = new StateHandler(this);
|
|
121
|
-
this.renderHandler = new RenderHandler(this);
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
```
|
|
125
|
-
|
|
126
|
-
#### 2. Singleton Pattern (Global Components)
|
|
127
|
-
|
|
128
|
-
For global UI elements like menus:
|
|
129
|
-
|
|
130
|
-
```typescript
|
|
131
|
-
export class DeesGlobalMenu extends DeesElement {
|
|
132
|
-
private static instance: DeesGlobalMenu;
|
|
133
|
-
|
|
134
|
-
public static getInstance(): DeesGlobalMenu {
|
|
135
|
-
if (!DeesGlobalMenu.instance) {
|
|
136
|
-
DeesGlobalMenu.instance = new DeesGlobalMenu();
|
|
137
|
-
document.body.appendChild(DeesGlobalMenu.instance);
|
|
138
|
-
}
|
|
139
|
-
return DeesGlobalMenu.instance;
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
```
|
|
143
|
-
|
|
144
|
-
#### 3. Registry Pattern (Z-Index Management)
|
|
145
|
-
|
|
146
|
-
Use centralized registries for global state:
|
|
147
|
-
|
|
148
|
-
```typescript
|
|
149
|
-
class ComponentRegistry {
|
|
150
|
-
private static instance: ComponentRegistry;
|
|
151
|
-
private registry = new WeakMap<HTMLElement, number>();
|
|
152
|
-
|
|
153
|
-
public register(element: HTMLElement, value: number) {
|
|
154
|
-
this.registry.set(element, value);
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
public unregister(element: HTMLElement) {
|
|
158
|
-
this.registry.delete(element);
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
```
|
|
162
|
-
|
|
163
|
-
## Component Types and Base Classes
|
|
164
|
-
|
|
165
|
-
### Standard Component (extends DeesElement)
|
|
166
|
-
|
|
167
|
-
Use for most UI components:
|
|
168
|
-
- Buttons, badges, icons
|
|
169
|
-
- Layout components
|
|
170
|
-
- Data display components
|
|
171
|
-
- Overlay components
|
|
172
|
-
|
|
173
|
-
### Form Input Component (extends DeesInputBase)
|
|
174
|
-
|
|
175
|
-
Use for all form inputs:
|
|
176
|
-
- Text inputs, dropdowns, checkboxes
|
|
177
|
-
- Date pickers, file uploads
|
|
178
|
-
- Rich text editors
|
|
179
|
-
|
|
180
|
-
**Required implementations:**
|
|
181
|
-
```typescript
|
|
182
|
-
export class DeesInputCustom extends DeesInputBase<ValueType> {
|
|
183
|
-
// Required: Get current value
|
|
184
|
-
public getValue(): ValueType {
|
|
185
|
-
return this.value;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
// Required: Set value programmatically
|
|
189
|
-
public setValue(value: ValueType): void {
|
|
190
|
-
this.value = value;
|
|
191
|
-
this.changeSubject.next(this); // Notify form
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
// Optional: Custom validation
|
|
195
|
-
public async validate(): Promise<boolean> {
|
|
196
|
-
// Custom validation logic
|
|
197
|
-
return true;
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
```
|
|
201
|
-
|
|
202
|
-
## Theming System
|
|
203
|
-
|
|
204
|
-
### DO: Use Design Tokens
|
|
205
|
-
|
|
206
|
-
The canonical theming system is the `--dees-*` token vocabulary defined in `ts_web/elements/00theme.ts`. Import `themeDefaultStyles` as the **first** entry of every component's static styles and style exclusively through tokens:
|
|
207
|
-
|
|
208
|
-
```typescript
|
|
209
|
-
import { themeDefaultStyles } from '../00theme.js';
|
|
210
|
-
|
|
211
|
-
public static styles = [
|
|
212
|
-
themeDefaultStyles,
|
|
213
|
-
cssManager.defaultStyles,
|
|
214
|
-
css`
|
|
215
|
-
:host {
|
|
216
|
-
background: var(--dees-color-bg-primary);
|
|
217
|
-
color: var(--dees-color-text-primary);
|
|
218
|
-
border: 1px solid var(--dees-color-border-default);
|
|
219
|
-
border-radius: var(--dees-radius-md);
|
|
220
|
-
padding: var(--dees-spacing-md) var(--dees-spacing-lg);
|
|
221
|
-
transition: background var(--dees-transition-default);
|
|
222
|
-
}
|
|
223
|
-
:host(:hover) {
|
|
224
|
-
background: var(--dees-color-hover);
|
|
225
|
-
}
|
|
226
|
-
`
|
|
227
|
-
];
|
|
228
|
-
|
|
229
|
-
// ❌ INCORRECT
|
|
230
|
-
background: #ffffff; // hard-coded color
|
|
231
|
-
background: ${cssManager.bdTheme('#fff', '#09090b')}; // legacy per-call theming
|
|
232
|
-
```
|
|
233
|
-
|
|
234
|
-
Token families: `--dees-color-bg-*`, `--dees-color-text-*` (incl. `-success`/`-warning`/`-error`), `--dees-color-border-*`, `--dees-color-accent-*`, `--dees-color-badge-*`, interactive states (`--dees-color-hover/active/pressed/row-hover`), `--dees-color-focus-ring`, tooltip/link/code/selection/scrollbar colors, plus `--dees-spacing-*`, `--dees-radius-*`, `--dees-shadow-*`, `--dees-transition-*`, and `--dees-control-height-*`. Check `00theme.ts` for the full list before inventing a value.
|
|
235
|
-
|
|
236
|
-
### DO: Derive Tints with color-mix
|
|
237
|
-
|
|
238
|
-
For alpha tints of a semantic color (hover washes, selection tints), derive from the token instead of hard-coding another color pair:
|
|
239
|
-
|
|
240
|
-
```typescript
|
|
241
|
-
background: color-mix(in srgb, var(--dees-color-accent-primary) 10%, transparent);
|
|
242
|
-
```
|
|
243
|
-
|
|
244
|
-
### Legacy: cssManager.bdTheme()
|
|
245
|
-
|
|
246
|
-
`cssManager.bdTheme(lightValue, darkValue)` is the legacy per-call theming mechanism. It remains functional (the token layer in `00theme.ts` is itself implemented through it — that file is the single sanctioned usage), but **no new `bdTheme()` call sites are allowed** outside `00theme.ts`. A ratchet check (`node scripts/check-bdtheme-ratchet.cjs`, part of `pnpm test`) enforces this: per-file counts may only shrink. See `readme.theme-migration.md` for the bdTheme → token mapping used during migration.
|
|
247
|
-
|
|
248
|
-
## Event Handling
|
|
249
|
-
|
|
250
|
-
### DO: Dispatch Custom Events Properly
|
|
251
|
-
|
|
252
|
-
```typescript
|
|
253
|
-
// ✅ CORRECT - Events bubble and cross shadow DOM
|
|
254
|
-
this.dispatchEvent(new CustomEvent('dees-componentname-change', {
|
|
255
|
-
detail: { value: this.value },
|
|
256
|
-
bubbles: true,
|
|
257
|
-
composed: true
|
|
258
|
-
}));
|
|
259
|
-
|
|
260
|
-
// ❌ INCORRECT - Event won't propagate properly
|
|
261
|
-
this.dispatchEvent(new CustomEvent('change', {
|
|
262
|
-
detail: { value: this.value }
|
|
263
|
-
// Missing bubbles and composed
|
|
264
|
-
}));
|
|
265
|
-
```
|
|
266
|
-
|
|
267
|
-
### DO: Use Event Delegation
|
|
268
|
-
|
|
269
|
-
For dynamic content, use event delegation:
|
|
270
|
-
|
|
271
|
-
```typescript
|
|
272
|
-
// ✅ CORRECT - Single listener for all items
|
|
273
|
-
this.addEventListener('click', (e: MouseEvent) => {
|
|
274
|
-
const item = (e.target as HTMLElement).closest('.item');
|
|
275
|
-
if (item) {
|
|
276
|
-
this.handleItemClick(item);
|
|
277
|
-
}
|
|
278
|
-
});
|
|
279
|
-
|
|
280
|
-
// ❌ INCORRECT - Multiple listeners
|
|
281
|
-
this.items.forEach(item => {
|
|
282
|
-
item.addEventListener('click', () => this.handleItemClick(item));
|
|
283
|
-
});
|
|
284
|
-
```
|
|
285
|
-
|
|
286
|
-
## State Management
|
|
287
|
-
|
|
288
|
-
### DO: Use Appropriate Property Decorators
|
|
289
|
-
|
|
290
|
-
```typescript
|
|
291
|
-
// Public API - use @property
|
|
292
|
-
@property({ type: String })
|
|
293
|
-
public label: string;
|
|
294
|
-
|
|
295
|
-
// Internal state - use @state
|
|
296
|
-
@state()
|
|
297
|
-
private isLoading: boolean = false;
|
|
298
|
-
|
|
299
|
-
// Reflect to attribute when needed
|
|
300
|
-
@property({ type: Boolean, reflect: true })
|
|
301
|
-
public disabled: boolean = false;
|
|
302
|
-
```
|
|
303
|
-
|
|
304
|
-
### DON'T: Manipulate State in Render
|
|
305
|
-
|
|
306
|
-
```typescript
|
|
307
|
-
// ❌ INCORRECT - Side effects in render
|
|
308
|
-
public render() {
|
|
309
|
-
this.counter++; // Don't modify state
|
|
310
|
-
return html`<div>${this.counter}</div>`;
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
// ✅ CORRECT - Pure render function
|
|
314
|
-
public render() {
|
|
315
|
-
return html`<div>${this.counter}</div>`;
|
|
316
|
-
}
|
|
317
|
-
```
|
|
318
|
-
|
|
319
|
-
## Form Components
|
|
320
|
-
|
|
321
|
-
### DO: Extend DeesInputBase
|
|
322
|
-
|
|
323
|
-
All form inputs must extend the base class:
|
|
324
|
-
|
|
325
|
-
```typescript
|
|
326
|
-
export class DeesInputNew extends DeesInputBase<string> {
|
|
327
|
-
// Inherits: key, label, value, required, disabled, validationState
|
|
328
|
-
}
|
|
329
|
-
```
|
|
330
|
-
|
|
331
|
-
### DO: Emit Changes Consistently
|
|
332
|
-
|
|
333
|
-
```typescript
|
|
334
|
-
private handleInput(e: Event) {
|
|
335
|
-
this.value = (e.target as HTMLInputElement).value;
|
|
336
|
-
this.changeSubject.next(this); // Notify form system
|
|
337
|
-
}
|
|
338
|
-
```
|
|
339
|
-
|
|
340
|
-
### DO: Support Standard Form Properties
|
|
341
|
-
|
|
342
|
-
```typescript
|
|
343
|
-
// All form inputs should support:
|
|
344
|
-
@property() public key: string;
|
|
345
|
-
@property() public label: string;
|
|
346
|
-
@property() public required: boolean = false;
|
|
347
|
-
@property() public disabled: boolean = false;
|
|
348
|
-
@property() public validationState: 'valid' | 'warn' | 'invalid';
|
|
349
|
-
```
|
|
350
|
-
|
|
351
|
-
## Overlay Components
|
|
352
|
-
|
|
353
|
-
### DO: Use Z-Index Registry
|
|
354
|
-
|
|
355
|
-
Never hardcode z-index values:
|
|
356
|
-
|
|
357
|
-
```typescript
|
|
358
|
-
// ✅ CORRECT
|
|
359
|
-
import { zIndexRegistry } from './00zindex.js';
|
|
360
|
-
|
|
361
|
-
public async show() {
|
|
362
|
-
this.modalZIndex = zIndexRegistry.getNextZIndex();
|
|
363
|
-
zIndexRegistry.register(this, this.modalZIndex);
|
|
364
|
-
this.style.zIndex = `${this.modalZIndex}`;
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
public async hide() {
|
|
368
|
-
zIndexRegistry.unregister(this);
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
// ❌ INCORRECT
|
|
372
|
-
public async show() {
|
|
373
|
-
this.style.zIndex = '9999'; // Hardcoded z-index
|
|
374
|
-
}
|
|
375
|
-
```
|
|
376
|
-
|
|
377
|
-
### DO: Use Window Layers
|
|
378
|
-
|
|
379
|
-
For modal backdrops:
|
|
380
|
-
|
|
381
|
-
```typescript
|
|
382
|
-
import { DeesWindowLayer } from './dees-windowlayer.js';
|
|
383
|
-
|
|
384
|
-
private windowLayer: DeesWindowLayer;
|
|
385
|
-
|
|
386
|
-
public async show() {
|
|
387
|
-
this.windowLayer = new DeesWindowLayer();
|
|
388
|
-
this.windowLayer.zIndex = zIndexRegistry.getNextZIndex();
|
|
389
|
-
document.body.append(this.windowLayer);
|
|
390
|
-
}
|
|
391
|
-
```
|
|
392
|
-
|
|
393
|
-
## Complex Components
|
|
394
|
-
|
|
395
|
-
### DO: Use Handler Classes
|
|
396
|
-
|
|
397
|
-
For complex logic, separate into specialized handlers:
|
|
398
|
-
|
|
399
|
-
```typescript
|
|
400
|
-
// wysiwyg/handlers/input.handler.ts
|
|
401
|
-
export class InputHandler {
|
|
402
|
-
constructor(private component: DeesInputWysiwyg) {}
|
|
403
|
-
|
|
404
|
-
public handleInput(event: InputEvent) {
|
|
405
|
-
// Specialized input handling
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
// Main component orchestrates
|
|
410
|
-
export class DeesInputWysiwyg extends DeesInputBase {
|
|
411
|
-
private inputHandler = new InputHandler(this);
|
|
412
|
-
}
|
|
413
|
-
```
|
|
414
|
-
|
|
415
|
-
### DO: Use Programmatic Rendering
|
|
416
|
-
|
|
417
|
-
For performance-critical updates that shouldn't trigger re-renders:
|
|
418
|
-
|
|
419
|
-
```typescript
|
|
420
|
-
// ✅ CORRECT - Direct DOM manipulation when needed
|
|
421
|
-
private updateBlockContent(blockId: string, content: string) {
|
|
422
|
-
const blockElement = this.shadowRoot.querySelector(`#${blockId}`);
|
|
423
|
-
if (blockElement) {
|
|
424
|
-
blockElement.textContent = content; // Direct update
|
|
425
|
-
}
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
// ❌ INCORRECT - Triggering full re-render
|
|
429
|
-
private updateBlockContent(blockId: string, content: string) {
|
|
430
|
-
this.blocks.find(b => b.id === blockId).content = content;
|
|
431
|
-
this.requestUpdate(); // Unnecessary re-render
|
|
432
|
-
}
|
|
433
|
-
```
|
|
434
|
-
|
|
435
|
-
## Streaming Components (harness pattern)
|
|
436
|
-
|
|
437
|
-
Components that render continuously arriving data (agent chat, logs) follow the harness pattern from `00group-harness`:
|
|
438
|
-
|
|
439
|
-
- **Props are the source of truth; deltas are the fast path.** The host owns the array (e.g. `.messages`). Incremental updates go through `applyDelta()`, which mutates the same object instance held in the array and normally calls `requestUpdate()` on the one affected child element. Top-level tool updates also refresh derived activity. A later wholesale re-assignment of the array is therefore idempotent — no divergence between streamed and re-set state.
|
|
440
|
-
- **Keyed `repeat` for identity.** List children render via `directives.repeat` keyed by a stable id so streaming never recreates sibling DOM (which would drop scroll position, expansion state, and selection).
|
|
441
|
-
- **Cheap while streaming, rich when done.** `markdownWhileStreaming` defaults to `true`, so active markdown is parsed on a throttled ≥150 ms, token-guarded path while syntax highlighting waits for the final parse. Set the policy to `false` on a chat, message list, message, or tool card to keep active text plain and perform one final parse at `message-end`; the assembled chat forwards the setting through outer messages and nested subtask transcripts.
|
|
442
|
-
- **Scroll follow with user override.** Auto-scroll only when the user is already pinned to the bottom (`scrollHeight - scrollTop - clientHeight < threshold`); otherwise show a "jump to latest" chip. Intent-based following remains active until explicit user scroll intent, while deferred frame checks and the `ResizeObserver` re-pin only when the scroll content grows. Streamed growth and the scroll therefore move as one motion without treating disclosure shrink as new content. Disconnect the observer and clear its size baseline in `disconnectedCallback`; retained hidden nested transcripts disable auto-follow observation until reopened.
|
|
443
|
-
- **Interaction state lives on the child.** Expand/collapse (reasoning, tool cards) is element-local state seeded from streaming state, descriptor defaults, errors, and authoritative subtask status; once the user toggles it, parent re-renders cannot reset that choice.
|
|
444
|
-
- **Nested streams replace authoritatively, then delta in place.** A subtask preview arrives through the parent tool call as a complete bounded snapshot. Existing child messages may then receive one-level deltas addressed by `parentMessageId`; a delta never invents a missing child message. The producer adds that message to its source-of-truth snapshot first.
|
|
445
|
-
- **Visual grouping never changes ownership.** Consecutive tool calls use one generic chronological grid pipeline partitioned by registry layout metadata: compact tools share compact rows, subagents share only subtask rows, and full-row tools span every column. Each segment's nested repeat remains keyed by message id. Status changes never move a card to another owner; untouched projected subtasks collapse individually while the grid remains in place. Group ids remain unique through segment splits/merges and survive front-window eviction as long as one card remains.
|
|
446
|
-
- **Disclosure animation is lazy and retained.** A collapsed tool body is not mounted until first use. Its first open starts from a painted closed state; later opens and closes animate the same body node, which is inert and aria-hidden while collapsed.
|
|
447
|
-
- **Inline recursion is explicit and bounded.** The outer list may render subtask previews; nested lists set `allowSubtaskStreams` false. Full descendant sessions remain reachable through the drill-in event rather than recursively expanding the transcript.
|
|
448
|
-
|
|
449
|
-
## Performance Optimization
|
|
450
|
-
|
|
451
|
-
### DO: Debounce Expensive Operations
|
|
452
|
-
|
|
453
|
-
```typescript
|
|
454
|
-
private resizeTimeout: number;
|
|
455
|
-
|
|
456
|
-
private handleResize = () => {
|
|
457
|
-
clearTimeout(this.resizeTimeout);
|
|
458
|
-
this.resizeTimeout = window.setTimeout(() => {
|
|
459
|
-
this.updateLayout();
|
|
460
|
-
}, 250);
|
|
461
|
-
};
|
|
462
|
-
```
|
|
463
|
-
|
|
464
|
-
### DO: Use Observers Efficiently
|
|
465
|
-
|
|
466
|
-
```typescript
|
|
467
|
-
// Clean up observers
|
|
468
|
-
public disconnectedCallback() {
|
|
469
|
-
super.disconnectedCallback();
|
|
470
|
-
this.resizeObserver?.disconnect();
|
|
471
|
-
this.mutationObserver?.disconnect();
|
|
472
|
-
}
|
|
473
|
-
```
|
|
474
|
-
|
|
475
|
-
### DO: Implement Virtual Scrolling
|
|
476
|
-
|
|
477
|
-
For large lists:
|
|
478
|
-
|
|
479
|
-
```typescript
|
|
480
|
-
// Only render visible items
|
|
481
|
-
private getVisibleItems() {
|
|
482
|
-
const scrollTop = this.scrollContainer.scrollTop;
|
|
483
|
-
const containerHeight = this.scrollContainer.clientHeight;
|
|
484
|
-
const itemHeight = 50;
|
|
485
|
-
|
|
486
|
-
const startIndex = Math.floor(scrollTop / itemHeight);
|
|
487
|
-
const endIndex = Math.ceil((scrollTop + containerHeight) / itemHeight);
|
|
488
|
-
|
|
489
|
-
return this.items.slice(startIndex, endIndex);
|
|
490
|
-
}
|
|
491
|
-
```
|
|
492
|
-
|
|
493
|
-
## Focus Management
|
|
494
|
-
|
|
495
|
-
### DO: Handle Focus Timing
|
|
496
|
-
|
|
497
|
-
```typescript
|
|
498
|
-
// ✅ CORRECT - Wait for render
|
|
499
|
-
async focusInput() {
|
|
500
|
-
await this.updateComplete;
|
|
501
|
-
await new Promise(resolve => requestAnimationFrame(resolve));
|
|
502
|
-
this.inputElement?.focus();
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
// ❌ INCORRECT - Focus too early
|
|
506
|
-
focusInput() {
|
|
507
|
-
this.inputElement?.focus(); // Element might not exist
|
|
508
|
-
}
|
|
509
|
-
```
|
|
510
|
-
|
|
511
|
-
### DO: Prevent Focus Loss
|
|
512
|
-
|
|
513
|
-
```typescript
|
|
514
|
-
// For global menus
|
|
515
|
-
constructor() {
|
|
516
|
-
super();
|
|
517
|
-
// Prevent focus loss when clicking menu
|
|
518
|
-
this.addEventListener('mousedown', (e) => {
|
|
519
|
-
e.preventDefault();
|
|
520
|
-
});
|
|
521
|
-
}
|
|
522
|
-
```
|
|
523
|
-
|
|
524
|
-
### DO: Implement Blur Debouncing
|
|
525
|
-
|
|
526
|
-
```typescript
|
|
527
|
-
private blurTimeout: number;
|
|
528
|
-
|
|
529
|
-
private handleBlur = () => {
|
|
530
|
-
clearTimeout(this.blurTimeout);
|
|
531
|
-
this.blurTimeout = window.setTimeout(() => {
|
|
532
|
-
// Check if truly blurred
|
|
533
|
-
if (!this.contains(document.activeElement)) {
|
|
534
|
-
this.handleTrueBlur();
|
|
535
|
-
}
|
|
536
|
-
}, 100);
|
|
537
|
-
};
|
|
538
|
-
```
|
|
539
|
-
|
|
540
|
-
## Demo System
|
|
541
|
-
|
|
542
|
-
### DO: Create Comprehensive Demos
|
|
543
|
-
|
|
544
|
-
Every component needs a demo:
|
|
545
|
-
|
|
546
|
-
```typescript
|
|
547
|
-
// dees-button.demo.ts
|
|
548
|
-
import { html } from '@design.estate/dees-element';
|
|
549
|
-
|
|
550
|
-
export const demoFunc = () => html`
|
|
551
|
-
<dees-button>Default Button</dees-button>
|
|
552
|
-
<dees-button type="primary">Primary Button</dees-button>
|
|
553
|
-
<dees-button type="danger" disabled>Disabled Danger</dees-button>
|
|
554
|
-
`;
|
|
555
|
-
|
|
556
|
-
// In component file
|
|
557
|
-
import * as demoFunc from './dees-button.demo.js';
|
|
558
|
-
|
|
559
|
-
export class DeesButton extends DeesElement {
|
|
560
|
-
public static demo = demoFunc.demoFunc;
|
|
561
|
-
}
|
|
562
|
-
```
|
|
563
|
-
|
|
564
|
-
### DO: Include All Variants
|
|
565
|
-
|
|
566
|
-
Show all component states and variations in demos:
|
|
567
|
-
- Default state
|
|
568
|
-
- Different types/variants
|
|
569
|
-
- Disabled state
|
|
570
|
-
- Loading state
|
|
571
|
-
- Error states
|
|
572
|
-
- Edge cases (long text, empty content)
|
|
573
|
-
|
|
574
|
-
## Common Pitfalls and Anti-patterns
|
|
575
|
-
|
|
576
|
-
### ❌ DON'T: Hardcode Z-Index Values
|
|
577
|
-
|
|
578
|
-
```typescript
|
|
579
|
-
// ❌ WRONG
|
|
580
|
-
this.style.zIndex = '9999';
|
|
581
|
-
|
|
582
|
-
// ✅ CORRECT
|
|
583
|
-
this.style.zIndex = `${zIndexRegistry.getNextZIndex()}`;
|
|
584
|
-
```
|
|
585
|
-
|
|
586
|
-
### ❌ DON'T: Skip Base Classes
|
|
587
|
-
|
|
588
|
-
```typescript
|
|
589
|
-
// ❌ WRONG - Form input without base class
|
|
590
|
-
export class DeesInputCustom extends DeesElement {
|
|
591
|
-
// Missing standard form functionality
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
// ✅ CORRECT
|
|
595
|
-
export class DeesInputCustom extends DeesInputBase<string> {
|
|
596
|
-
// Inherits all form functionality
|
|
597
|
-
}
|
|
598
|
-
```
|
|
599
|
-
|
|
600
|
-
### ❌ DON'T: Forget Theme Support
|
|
601
|
-
|
|
602
|
-
```typescript
|
|
603
|
-
// ❌ WRONG
|
|
604
|
-
background-color: #ffffff;
|
|
605
|
-
color: #000000;
|
|
606
|
-
|
|
607
|
-
// ✅ CORRECT
|
|
608
|
-
background-color: var(--dees-color-bg-primary);
|
|
609
|
-
color: var(--dees-color-text-primary);
|
|
610
|
-
```
|
|
611
|
-
|
|
612
|
-
### ❌ DON'T: Create Components Without Demos
|
|
613
|
-
|
|
614
|
-
```typescript
|
|
615
|
-
// ❌ WRONG
|
|
616
|
-
export class DeesComponent extends DeesElement {
|
|
617
|
-
// No demo property
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
// ✅ CORRECT
|
|
621
|
-
export class DeesComponent extends DeesElement {
|
|
622
|
-
public static demo = demoFunc.demoFunc;
|
|
623
|
-
}
|
|
624
|
-
```
|
|
625
|
-
|
|
626
|
-
### ❌ DON'T: Emit Non-Bubbling Events
|
|
627
|
-
|
|
628
|
-
```typescript
|
|
629
|
-
// ❌ WRONG
|
|
630
|
-
this.dispatchEvent(new CustomEvent('change', {
|
|
631
|
-
detail: this.value
|
|
632
|
-
}));
|
|
633
|
-
|
|
634
|
-
// ✅ CORRECT
|
|
635
|
-
this.dispatchEvent(new CustomEvent('change', {
|
|
636
|
-
detail: this.value,
|
|
637
|
-
bubbles: true,
|
|
638
|
-
composed: true
|
|
639
|
-
}));
|
|
640
|
-
```
|
|
641
|
-
|
|
642
|
-
### ❌ DON'T: Skip Cleanup
|
|
643
|
-
|
|
644
|
-
```typescript
|
|
645
|
-
// ❌ WRONG
|
|
646
|
-
public connectedCallback() {
|
|
647
|
-
window.addEventListener('resize', this.handleResize);
|
|
648
|
-
}
|
|
649
|
-
|
|
650
|
-
// ✅ CORRECT
|
|
651
|
-
public connectedCallback() {
|
|
652
|
-
super.connectedCallback();
|
|
653
|
-
window.addEventListener('resize', this.handleResize);
|
|
654
|
-
}
|
|
655
|
-
|
|
656
|
-
public disconnectedCallback() {
|
|
657
|
-
super.disconnectedCallback();
|
|
658
|
-
window.removeEventListener('resize', this.handleResize);
|
|
659
|
-
}
|
|
660
|
-
```
|
|
661
|
-
|
|
662
|
-
### ❌ DON'T: Use Inline Styles for Theming
|
|
663
|
-
|
|
664
|
-
```typescript
|
|
665
|
-
// ❌ WRONG
|
|
666
|
-
<div style="background-color: ${this.darkMode ? '#000' : '#fff'}">
|
|
667
|
-
|
|
668
|
-
// ✅ CORRECT
|
|
669
|
-
<div class="themed-container">
|
|
670
|
-
// In styles:
|
|
671
|
-
.themed-container {
|
|
672
|
-
background-color: var(--dees-color-bg-primary);
|
|
673
|
-
}
|
|
674
|
-
```
|
|
675
|
-
|
|
676
|
-
### ❌ DON'T: Forget Mobile Responsiveness
|
|
677
|
-
|
|
678
|
-
```typescript
|
|
679
|
-
// ❌ WRONG
|
|
680
|
-
:host {
|
|
681
|
-
width: 800px; // Fixed width
|
|
682
|
-
}
|
|
683
|
-
|
|
684
|
-
// ✅ CORRECT
|
|
685
|
-
:host {
|
|
686
|
-
width: 100%;
|
|
687
|
-
max-width: 800px;
|
|
688
|
-
}
|
|
689
|
-
|
|
690
|
-
@media (max-width: 768px) {
|
|
691
|
-
:host {
|
|
692
|
-
/* Mobile adjustments */
|
|
693
|
-
}
|
|
694
|
-
}
|
|
695
|
-
```
|
|
696
|
-
|
|
697
|
-
## Code Examples
|
|
698
|
-
|
|
699
|
-
### Example: Creating a New Button Variant
|
|
700
|
-
|
|
701
|
-
```typescript
|
|
702
|
-
// dees-special-button.ts
|
|
703
|
-
import { customElement, property, css, html, cssManager } from '@design.estate/dees-element';
|
|
704
|
-
import { DeesElement } from '@design.estate/dees-element';
|
|
705
|
-
import { themeDefaultStyles } from '../00theme.js';
|
|
706
|
-
import * as demoFunc from './dees-special-button.demo.js';
|
|
707
|
-
|
|
708
|
-
@customElement('dees-special-button')
|
|
709
|
-
export class DeesSpecialButton extends DeesElement {
|
|
710
|
-
public static demo = demoFunc.demoFunc;
|
|
711
|
-
|
|
712
|
-
@property({ type: String })
|
|
713
|
-
public text: string = 'Click me';
|
|
714
|
-
|
|
715
|
-
@property({ type: Boolean, reflect: true })
|
|
716
|
-
public loading: boolean = false;
|
|
717
|
-
|
|
718
|
-
public static styles = [
|
|
719
|
-
themeDefaultStyles,
|
|
720
|
-
cssManager.defaultStyles,
|
|
721
|
-
css`
|
|
722
|
-
:host {
|
|
723
|
-
display: inline-block;
|
|
724
|
-
}
|
|
725
|
-
|
|
726
|
-
.button {
|
|
727
|
-
padding: var(--dees-spacing-sm) var(--dees-spacing-lg);
|
|
728
|
-
background: var(--dees-color-accent-primary);
|
|
729
|
-
color: white;
|
|
730
|
-
border: none;
|
|
731
|
-
border-radius: var(--dees-radius-sm);
|
|
732
|
-
cursor: pointer;
|
|
733
|
-
transition: all var(--dees-transition-slow);
|
|
734
|
-
}
|
|
735
|
-
|
|
736
|
-
.button:hover {
|
|
737
|
-
transform: translateY(-2px);
|
|
738
|
-
box-shadow: var(--dees-shadow-md);
|
|
739
|
-
}
|
|
740
|
-
|
|
741
|
-
:host([loading]) .button {
|
|
742
|
-
opacity: 0.7;
|
|
743
|
-
cursor: not-allowed;
|
|
744
|
-
}
|
|
745
|
-
`
|
|
746
|
-
];
|
|
747
|
-
|
|
748
|
-
public render() {
|
|
749
|
-
return html`
|
|
750
|
-
<button class="button" ?disabled=${this.loading} @click=${this.handleClick}>
|
|
751
|
-
${this.loading ? html`<dees-spinner size="small"></dees-spinner>` : this.text}
|
|
752
|
-
</button>
|
|
753
|
-
`;
|
|
754
|
-
}
|
|
755
|
-
|
|
756
|
-
private handleClick() {
|
|
757
|
-
this.dispatchEvent(new CustomEvent('special-click', {
|
|
758
|
-
bubbles: true,
|
|
759
|
-
composed: true
|
|
760
|
-
}));
|
|
761
|
-
}
|
|
762
|
-
}
|
|
763
|
-
```
|
|
764
|
-
|
|
765
|
-
### Example: Creating a Form Input
|
|
766
|
-
|
|
767
|
-
```typescript
|
|
768
|
-
// dees-input-special.ts
|
|
769
|
-
export class DeesInputSpecial extends DeesInputBase<string> {
|
|
770
|
-
public static demo = demoFunc.demoFunc;
|
|
771
|
-
|
|
772
|
-
public render() {
|
|
773
|
-
return html`
|
|
774
|
-
<dees-label .label=${this.label} .required=${this.required}>
|
|
775
|
-
<input
|
|
776
|
-
type="text"
|
|
777
|
-
.value=${this.value || ''}
|
|
778
|
-
?disabled=${this.disabled}
|
|
779
|
-
@input=${this.handleInput}
|
|
780
|
-
@blur=${this.handleBlur}
|
|
781
|
-
/>
|
|
782
|
-
</dees-label>
|
|
783
|
-
`;
|
|
784
|
-
}
|
|
785
|
-
|
|
786
|
-
private handleInput(e: Event) {
|
|
787
|
-
this.value = (e.target as HTMLInputElement).value;
|
|
788
|
-
this.changeSubject.next(this);
|
|
789
|
-
}
|
|
790
|
-
|
|
791
|
-
private handleBlur() {
|
|
792
|
-
this.dispatchEvent(new CustomEvent('blur', {
|
|
793
|
-
bubbles: true,
|
|
794
|
-
composed: true
|
|
795
|
-
}));
|
|
796
|
-
}
|
|
797
|
-
|
|
798
|
-
public getValue(): string {
|
|
799
|
-
return this.value;
|
|
800
|
-
}
|
|
801
|
-
|
|
802
|
-
public setValue(value: string): void {
|
|
803
|
-
this.value = value;
|
|
804
|
-
this.changeSubject.next(this);
|
|
805
|
-
}
|
|
806
|
-
}
|
|
807
|
-
```
|
|
808
|
-
|
|
809
|
-
## Summary
|
|
810
|
-
|
|
811
|
-
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:
|
|
812
|
-
|
|
813
|
-
- **Consistent**: Following established patterns
|
|
814
|
-
- **Maintainable**: Easy to understand and modify
|
|
815
|
-
- **Performant**: Optimized for real-world use
|
|
816
|
-
- **Accessible**: Usable by everyone
|
|
817
|
-
- **Theme-aware**: Supporting light and dark modes
|
|
818
|
-
- **Well-integrated**: Working seamlessly with the component ecosystem
|
|
819
|
-
|
|
820
|
-
Remember: When in doubt, look at existing components for examples. The codebase itself is the best documentation of these patterns in action.
|