Compare commits

..

16 Commits

Author SHA1 Message Date
edc15a727c 1.11.4
Some checks failed
Default (tags) / security (push) Failing after 24s
Default (tags) / test (push) Failing after 14s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-09-14 19:23:23 +00:00
960085145d fix(readme): Rewrite and expand README with Quick Start, feature highlights, demos and usage examples; add local Claude settings file 2025-09-14 19:23:23 +00:00
7fdb4f19a8 1.11.3
Some checks failed
Default (tags) / security (push) Failing after 23s
Default (tags) / test (push) Failing after 13s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-09-09 11:18:56 +00:00
e21fb79731 fix(dees-input-list): Prevent list animations from affecting scroll bounds and fix content-visibility issues in dees-input-list; add local developer settings 2025-09-09 11:18:56 +00:00
05f669a7bd feat(dees-input-list): add new input list component with demo and validation features 2025-09-08 19:21:37 +00:00
8137d79e18 1.11.2
Some checks failed
Default (tags) / security (push) Failing after 22s
Default (tags) / test (push) Failing after 12s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-09-07 09:02:25 +00:00
3b474b7dcc fix(DeesFormSubmit): Make form submit robust by locating nearest dees-form via closest(); add local CLAUDE settings 2025-09-07 09:02:25 +00:00
e449b413d1 1.11.1
Some checks failed
Default (tags) / security (push) Failing after 23s
Default (tags) / test (push) Failing after 13s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-09-06 13:29:39 +00:00
8918dc94bd fix(dees-input-text): Normalize Lucide icon names for password toggle 2025-09-06 13:29:38 +00:00
2c595bf803 1.11.0
Some checks failed
Default (tags) / security (push) Failing after 22s
Default (tags) / test (push) Failing after 13s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-09-05 15:37:31 +00:00
75f31a6cec feat(dees-icon): Add full icon list and improve dees-icon demo with copy-all functionality and UI tweaks 2025-09-05 15:37:31 +00:00
b211c0d068 1.10.12
Some checks failed
Default (tags) / security (push) Failing after 22s
Default (tags) / test (push) Failing after 14s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-09-05 15:26:19 +00:00
911159ee55 fix(dees-simple-appdash): Fix icon rendering in dees-simple-appdash to respect provided icon strings 2025-09-05 15:26:19 +00:00
c0dbc3c0d0 1.10.11
Some checks failed
Default (tags) / security (push) Failing after 24s
Default (tags) / test (push) Failing after 14s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-09-05 15:00:00 +00:00
7eea21c9d4 fix(dees-simple-appdash): Bump deps and fix dees-simple-appdash icon binding and terminal sizing 2025-09-05 15:00:00 +00:00
2f17dea480 feat(playbook): add PlayBook 2025-07-04 18:42:53 +00:00
18 changed files with 6160 additions and 3713 deletions

View File

@@ -1,5 +1,52 @@
# Changelog
## 2025-09-10 - 1.11.4 - fix(readme)
Rewrite and expand README with Quick Start, feature highlights, demos and usage examples; add local Claude settings file
- Completely rewritten and reorganized README: added Quick Start, component highlights, usage examples, demos, development workflow, troubleshooting and links.
- Added .claude/settings.local.json with local Claude permission configuration.
## 2025-09-08 - 1.11.3 - fix(dees-input-list)
Prevent list animations from affecting scroll bounds and fix content-visibility issues in dees-input-list; add local developer settings
- dees-input-list: add overflow:hidden to list items to prevent animations from altering scroll bounds and causing visual/scroll glitches
- dees-input-list: force content-visibility/contain to visible/none to avoid unexpected scrolling/layout issues when items animate
- Add .claude/settings.local.json with local developer permissions (allows running pnpm scripts via Claude-local tooling)
## 2025-09-07 - 1.11.2 - fix(DeesFormSubmit)
Make form submit robust by locating nearest dees-form via closest(); add local CLAUDE settings
- Fix: DeesFormSubmit.submit now walks up the DOM with closest('dees-form') to find and call gatherAndDispatch on the parent form. This fixes cases where the submit button is slotted or not a direct child of the form.
- Chore: Add .claude/settings.local.json to permit running pnpm scripts in the local CLAUDE environment (allows Bash(pnpm run:*)).
## 2025-09-06 - 1.11.1 - fix(dees-input-text)
Normalize Lucide icon names for password toggle
- Updated password visibility toggle icons in dees-input-text from 'lucide:eye'/'lucide:eye-off' to 'lucide:Eye'/'lucide:EyeOff' to match Lucide exports and avoid missing icon rendering.
## 2025-09-05 - 1.11.0 - feat(dees-icon)
Add full icon list and improve dees-icon demo with copy-all functionality and UI tweaks
- Added readme.icons.md containing 1900+ icon identifiers (FontAwesome + Lucide) for easy reference and tooling
- Enhanced ts_web/elements/dees-icon.demo.ts: added a 'Copy All Icon Names' button that copies prefixed icon names (fa:..., lucide:...) to the clipboard and shows temporary feedback
- Updated demo presentation: prefixed displayed icon names (fa: / lucide:), improved search-container spacing and added button styling for better UX
- Changes are documentation/demo only — no production runtime component logic changed
## 2025-09-05 - 1.10.12 - fix(dees-simple-appdash)
Fix icon rendering in dees-simple-appdash to respect provided icon strings
- dees-simple-appdash: stop forcing a 'lucide:' prefix when rendering view icons — use the icon string as provided.
- Prevents incorrect/missing icons when the iconName already includes a library prefix (e.g. 'fa:' or 'lucide:').
## 2025-09-05 - 1.10.11 - fix(dees-simple-appdash)
Bump deps and fix dees-simple-appdash icon binding and terminal sizing
- Updated runtime dependencies: @design.estate/dees-element -> ^2.1.2, @design.estate/dees-wcctools -> ^1.1.1, @fortawesome/* -> ^7.0.1, apexcharts -> ^5.3.4, lucide -> ^0.542.0 (compatibility/security/stability updates)
- Updated dev tooling: @git.zone/tsbuild -> ^2.6.8, @git.zone/tstest -> ^2.3.6, @git.zone/tswatch -> ^2.2.1
- Fix: dees-simple-appdash — use proper string interpolation for lucide icon properties (prevents incorrect icon rendering)
- Fix: dees-simple-appdash — enforce terminal maxWidth/maxHeight to avoid overflow and improve layout stability
- Cosmetic: small style/behavior tweaks to dees-simple-appdash (logout/terminal/wifi icon bindings corrected)
## 2025-06-29 - 1.10.10 - improve(dees-dashboardgrid, dees-input-wysiwyg)
Enhanced dashboard grid component with advanced spacing and layout features inspired by gridstack.js

View File

@@ -1,6 +1,6 @@
{
"name": "@design.estate/dees-catalog",
"version": "1.10.10",
"version": "1.11.4",
"private": false,
"description": "A comprehensive library that provides dynamic web components for building sophisticated and modern web applications using JavaScript and TypeScript.",
"main": "dist_ts_web/index.js",
@@ -16,12 +16,12 @@
"license": "MIT",
"dependencies": {
"@design.estate/dees-domtools": "^2.3.3",
"@design.estate/dees-element": "^2.0.45",
"@design.estate/dees-wcctools": "^1.1.0",
"@fortawesome/fontawesome-svg-core": "^6.7.2",
"@fortawesome/free-brands-svg-icons": "^6.7.2",
"@fortawesome/free-regular-svg-icons": "^6.7.2",
"@fortawesome/free-solid-svg-icons": "^6.7.2",
"@design.estate/dees-element": "^2.1.2",
"@design.estate/dees-wcctools": "^1.1.1",
"@fortawesome/fontawesome-svg-core": "^7.0.1",
"@fortawesome/free-brands-svg-icons": "^7.0.1",
"@fortawesome/free-regular-svg-icons": "^7.0.1",
"@fortawesome/free-solid-svg-icons": "^7.0.1",
"@push.rocks/smarti18n": "^1.0.4",
"@push.rocks/smartpromise": "^4.2.0",
"@push.rocks/smartstring": "^4.0.15",
@@ -33,20 +33,20 @@
"@tiptap/starter-kit": "^2.23.0",
"@tsclass/tsclass": "^9.2.0",
"@webcontainer/api": "1.2.0",
"apexcharts": "^4.7.0",
"apexcharts": "^5.3.4",
"highlight.js": "11.11.1",
"ibantools": "^4.5.1",
"lucide": "^0.525.0",
"lucide": "^0.542.0",
"monaco-editor": "^0.52.2",
"pdfjs-dist": "^4.10.38",
"xterm": "^5.3.0",
"xterm-addon-fit": "^0.8.0"
},
"devDependencies": {
"@git.zone/tsbuild": "^2.6.4",
"@git.zone/tsbuild": "^2.6.8",
"@git.zone/tsbundle": "^2.5.1",
"@git.zone/tstest": "^2.3.1",
"@git.zone/tswatch": "^2.1.2",
"@git.zone/tstest": "^2.3.6",
"@git.zone/tswatch": "^2.2.1",
"@push.rocks/projectinfo": "^5.0.2",
"@push.rocks/tapbundle": "^6.0.3",
"@types/node": "^22.0.0"

3016
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -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

1906
readme.icons.md Normal file

File diff suppressed because it is too large Load Diff

2323
readme.md

File diff suppressed because it is too large Load Diff

784
readme.playbook.md Normal file
View 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.

View File

@@ -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.

View File

@@ -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

View File

@@ -1,72 +0,0 @@
> @design.estate/dees-catalog@1.10.8 test /mnt/data/lossless/design.estate/dees-catalog
> tstest test/ --web --verbose --timeout 30 --logfile test/test.tabs-indicator.browser.ts

🔍 Test Discovery
 Mode: file
 Pattern: test/test.tabs-indicator.browser.ts
 Found: 1 test file(s)

▶️ test/test.tabs-indicator.browser.ts (1/1)
 Runtime: chromium
running spawned compilation process
=======> ESBUILD
{
cwd: '/mnt/data/lossless/design.estate/dees-catalog',
from: 'test/test.tabs-indicator.browser.ts',
to: '/mnt/data/lossless/design.estate/dees-catalog/.nogit/tstest_cache/test__test.tabs-indicator.browser.ts.js',
mode: 'test',
argv: { bundler: 'esbuild' }
}
switched to /mnt/data/lossless/design.estate/dees-catalog
building for test:
Got no SSL certificates. Please ensure encryption using e.g. a reverse proxy
"/test" maps to 1 handlers
-> GET
"*" maps to 1 handlers
-> GET
now listening on 3007!
Launching puppeteer browser with arguments:
[]
Using executable: /usr/bin/google-chrome
added connection. now 1 sockets connected.
added connection. now 2 sockets connected.
connection ended
removed connection. 1 sockets remaining.
connection ended
removed connection. 0 sockets remaining.
added connection. now 1 sockets connected.
/favicon.ico
could not resolve /mnt/data/lossless/design.estate/dees-catalog/.nogit/tstest_cache/favicon.ico
/test__test.tabs-indicator.browser.ts.js
 Test starting: tabs indicator positioning debug
 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
 Using globalThis.tapPromise
 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
connection ended
removed connection. 0 sockets remaining.
=>  Stopped test/test.tabs-indicator.browser.ts chromium instance and server.

⚠️ Error
 Only 0 out of 1 completed!

⚠️ Error
 The amount of received tests and expectedTests is unequal! Therefore the testfile failed
 Summary: -1 passed, 1 failed of 0 tests in 2.7s

📊 Test Summary
┌────────────────────────────────┐
│ Total Files: 1 │
│ Total Tests: 0 │
│ Passed: 0 │
│ Failed: 0 │
│ Duration: 4.2s │
└────────────────────────────────┘

⏱️ Performance Metrics:
 Average per test: 0ms

ALL TESTS PASSED! 🎉
Exited NOT OK!
ELIFECYCLE Test failed. See above for more details.

View File

@@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@design.estate/dees-catalog',
version: '1.10.1',
version: '1.11.4',
description: 'A comprehensive library that provides dynamic web components for building sophisticated and modern web applications using JavaScript and TypeScript.'
}

View File

@@ -57,9 +57,10 @@ export class DeesFormSubmit extends DeesElement {
if (this.disabled) {
return;
}
const parentElement: DeesForm = this.parentElement as DeesForm;
if (parentElement && parentElement.gatherAndDispatch) {
parentElement.gatherAndDispatch();
// Walk up the DOM tree to find the nearest dees-form element
const parentFormElement = this.closest('dees-form') as DeesForm;
if (parentFormElement && parentFormElement.gatherAndDispatch) {
parentFormElement.gatherAndDispatch();
}
}

View File

@@ -40,6 +40,26 @@ export const demoFunc = () => {
}
// Define the functions in TS scope instead of script tags
const copyAllIconNames = () => {
// Generate complete list of all icon names with prefixes
const faIconsList = faIcons.map(name => `fa:${name}`);
const lucideIconsListPrefixed = lucideIconsList.map(name => `lucide:${name}`);
const allIcons = [...faIconsList, ...lucideIconsListPrefixed];
const textToCopy = allIcons.join('\n');
navigator.clipboard.writeText(textToCopy).then(() => {
// Show feedback
const currentEvent = window.event as MouseEvent;
const button = currentEvent.currentTarget as HTMLElement;
const originalText = button.textContent;
button.textContent = `✓ Copied ${allIcons.length} icon names!`;
setTimeout(() => {
button.textContent = originalText;
}, 2000);
});
};
const searchIcons = (event: InputEvent) => {
const searchTerm = (event.target as HTMLInputElement).value.toLowerCase().trim();
// Get the demo container first, then search within it
@@ -111,6 +131,7 @@ export const demoFunc = () => {
width: 100%;
margin-bottom: 20px;
display: flex;
gap: 10px;
}
#iconSearch {
@@ -129,6 +150,27 @@ export const demoFunc = () => {
border-color: #e4002b;
}
.copy-all-button {
padding: 12px 20px;
font-size: 16px;
border: none;
border-radius: 4px;
background: #e4002b;
color: #fff;
cursor: pointer;
transition: all 0.2s;
white-space: nowrap;
}
.copy-all-button:hover {
background: #c4001b;
transform: translateY(-1px);
}
.copy-all-button:active {
transform: translateY(0);
}
dees-icon {
transition: all 0.2s ease;
color: #ffffff;
@@ -239,6 +281,7 @@ export const demoFunc = () => {
<div class="demoContainer">
<div class="search-container">
<input type="text" id="iconSearch" placeholder="Search icons..." @input=${searchIcons}>
<button class="copy-all-button" @click=${copyAllIconNames}>📋 Copy All Icon Names</button>
</div>
<div class="api-note">
@@ -258,7 +301,7 @@ export const demoFunc = () => {
return html`
<div class="iconContainer fa-icon" data-name=${iconName.toLowerCase()} @click=${() => copyIconName(iconName, 'fa')}>
<dees-icon .icon=${prefixedName as IconWithPrefix} iconSize="24"></dees-icon>
<div class="iconName">${iconName}</div>
<div class="iconName">fa:${iconName}</div>
<span class="copy-tooltip">Click to copy</span>
</div>
`;
@@ -279,7 +322,7 @@ export const demoFunc = () => {
return html`
<div class="iconContainer lucide-icon" data-name=${iconName.toLowerCase()} @click=${() => copyIconName(iconName, 'lucide')}>
<dees-icon .icon=${prefixedName as IconWithPrefix} iconSize="24"></dees-icon>
<div class="iconName">${iconName}</div>
<div class="iconName">lucide:${iconName}</div>
<span class="copy-tooltip">Click to copy</span>
</div>
`;

View File

@@ -0,0 +1,275 @@
import { html, css } from '@design.estate/dees-element';
import '@design.estate/dees-wcctools/demotools';
import './dees-panel.js';
import './dees-form.js';
import './dees-input-text.js';
import './dees-form-submit.js';
export const demoFunc = () => html`
<dees-demowrapper>
<style>
${css`
.demo-container {
display: flex;
flex-direction: column;
gap: 24px;
padding: 24px;
max-width: 1200px;
margin: 0 auto;
}
dees-panel {
margin-bottom: 24px;
}
dees-panel:last-child {
margin-bottom: 0;
}
.grid-layout {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
}
@media (max-width: 768px) {
.grid-layout {
grid-template-columns: 1fr;
}
}
.output-preview {
margin-top: 16px;
padding: 16px;
background: #f3f4f6;
border-radius: 4px;
font-size: 12px;
color: #374151;
word-break: break-all;
max-height: 200px;
overflow-y: auto;
}
@media (prefers-color-scheme: dark) {
.output-preview {
background: #2c2c2c;
color: #e4e4e7;
}
}
.feature-note {
margin-top: 12px;
padding: 12px;
background: #eff6ff;
border-left: 3px solid #3b82f6;
border-radius: 4px;
font-size: 13px;
color: #1e40af;
}
@media (prefers-color-scheme: dark) {
.feature-note {
background: #1e3a5f;
color: #93c5fd;
}
}
`}
</style>
<div class="demo-container">
<dees-panel .title=${'1. Basic List Input'} .subtitle=${'Simple list management with add, edit, and delete'}>
<dees-input-list
.label=${'Shopping List'}
.placeholder=${'Add item to your list...'}
.value=${['Milk', 'Bread', 'Eggs', 'Cheese']}
.description=${'Double-click to edit items, or use the edit button'}
></dees-input-list>
<div class="feature-note">
💡 Double-click any item to quickly edit it inline
</div>
</dees-panel>
<dees-panel .title=${'2. Sortable List'} .subtitle=${'Drag and drop to reorder items'}>
<dees-input-list
.label=${'Task Priority'}
.placeholder=${'Add a task...'}
.sortable=${true}
.value=${[
'Review pull requests',
'Fix critical bug',
'Update documentation',
'Deploy to production',
'Team standup meeting'
]}
.description=${'Drag items using the handle to reorder them'}
></dees-input-list>
<div class="feature-note">
🔄 Drag the grip handle to reorder tasks by priority
</div>
</dees-panel>
<dees-panel .title=${'3. Validation & Constraints'} .subtitle=${'Lists with minimum/maximum items and duplicate prevention'}>
<div class="grid-layout">
<dees-input-list
.label=${'Team Members (Min 2, Max 5)'}
.placeholder=${'Add team member...'}
.minItems=${2}
.maxItems=${5}
.value=${['Alice', 'Bob']}
.required=${true}
.description=${'Add 2-5 team members'}
></dees-input-list>
<dees-input-list
.label=${'Unique Tags (No Duplicates)'}
.placeholder=${'Add unique tag...'}
.allowDuplicates=${false}
.value=${['frontend', 'backend', 'database']}
.description=${'Duplicate items are not allowed'}
></dees-input-list>
</div>
</dees-panel>
<dees-panel .title=${'4. Delete Confirmation'} .subtitle=${'Require confirmation before deleting items'}>
<dees-input-list
.label=${'Important Documents'}
.placeholder=${'Add document name...'}
.confirmDelete=${true}
.value=${[
'Contract_2024.pdf',
'Financial_Report_Q3.xlsx',
'Project_Proposal.docx',
'Meeting_Notes.txt'
]}
.description=${'Deletion requires confirmation for safety'}
></dees-input-list>
</dees-panel>
<dees-panel .title=${'5. Disabled State'} .subtitle=${'Read-only list display'}>
<dees-input-list
.label=${'System Defaults'}
.value=${['Default Setting 1', 'Default Setting 2', 'Default Setting 3']}
.disabled=${true}
.description=${'These items cannot be modified'}
></dees-input-list>
</dees-panel>
<dees-panel .title=${'6. Form Integration'} .subtitle=${'List input working within a form context'}>
<dees-form>
<dees-input-text
.label=${'Recipe Name'}
.placeholder=${'My Amazing Recipe'}
.required=${true}
.key=${'name'}
></dees-input-text>
<div class="grid-layout">
<dees-input-list
.label=${'Ingredients'}
.placeholder=${'Add ingredient...'}
.required=${true}
.minItems=${3}
.key=${'ingredients'}
.sortable=${true}
.value=${[
'2 cups flour',
'1 cup sugar',
'3 eggs'
]}
.description=${'Add at least 3 ingredients'}
></dees-input-list>
<dees-input-list
.label=${'Instructions'}
.placeholder=${'Add instruction step...'}
.required=${true}
.minItems=${2}
.key=${'instructions'}
.sortable=${true}
.value=${[
'Preheat oven to 350°F',
'Mix dry ingredients'
]}
.description=${'Add cooking instructions in order'}
></dees-input-list>
</div>
<dees-input-text
.label=${'Notes'}
.inputType=${'textarea'}
.placeholder=${'Any special notes or tips...'}
.key=${'notes'}
></dees-input-text>
<dees-form-submit .text=${'Save Recipe'}></dees-form-submit>
</dees-form>
</dees-panel>
<dees-panel .title=${'7. Interactive Demo'} .subtitle=${'Build your own feature list and see the data'}>
<dees-input-list
id="interactive-list"
.label=${'Product Features'}
.placeholder=${'Add a feature...'}
.sortable=${true}
.confirmDelete=${false}
.allowDuplicates=${false}
.maxItems=${10}
@change=${(e: CustomEvent) => {
const preview = document.querySelector('#list-json');
if (preview) {
const data = {
items: e.detail.value,
count: e.detail.value.length,
timestamp: new Date().toISOString()
};
preview.textContent = JSON.stringify(data, null, 2);
}
}}
></dees-input-list>
<div class="output-preview" id="list-json">
{
"items": [],
"count": 0,
"timestamp": "${new Date().toISOString()}"
}
</div>
<div class="feature-note">
✨ Add, edit, remove, and reorder items to see the JSON output update in real-time
</div>
</dees-panel>
<dees-panel .title=${'8. Advanced Configuration'} .subtitle=${'Combine all features for complex use cases'}>
<dees-input-list
.label=${'Project Milestones'}
.placeholder=${'Add milestone...'}
.value=${[
'Project Kickoff - Week 1',
'Requirements Gathering - Week 2-3',
'Design Phase - Week 4-6',
'Development Sprint 1 - Week 7-9',
'Testing & QA - Week 10-11',
'Deployment - Week 12'
]}
.sortable=${true}
.confirmDelete=${true}
.allowDuplicates=${false}
.minItems=${3}
.maxItems=${12}
.required=${true}
.description=${'Manage project milestones (3-12 items, sortable, no duplicates)'}
></dees-input-list>
</dees-panel>
<dees-panel .title=${'9. Empty State'} .subtitle=${'How the component looks with no items'}>
<dees-input-list
.label=${'Your Ideas'}
.placeholder=${'Share your ideas...'}
.value=${[]}
.description=${'Start adding items to build your list'}
></dees-input-list>
</dees-panel>
</div>
</dees-demowrapper>
`;

View File

@@ -0,0 +1,622 @@
import {
customElement,
html,
css,
cssManager,
property,
state,
type TemplateResult,
} from '@design.estate/dees-element';
import { DeesInputBase } from './dees-input-base.js';
import './dees-icon.js';
import './dees-button.js';
import { demoFunc } from './dees-input-list.demo.js';
declare global {
interface HTMLElementTagNameMap {
'dees-input-list': DeesInputList;
}
}
@customElement('dees-input-list')
export class DeesInputList extends DeesInputBase<DeesInputList> {
// STATIC
public static demo = demoFunc;
// INSTANCE
@property({ type: Array })
public value: string[] = [];
@property({ type: String })
public placeholder: string = 'Add new item...';
@property({ type: Number })
public maxItems: number = 0; // 0 means unlimited
@property({ type: Number })
public minItems: number = 0;
@property({ type: Boolean })
public allowDuplicates: boolean = false;
@property({ type: Boolean })
public sortable: boolean = false;
@property({ type: Boolean })
public confirmDelete: boolean = false;
@property({ type: String })
public validationText: string = '';
@state()
private inputValue: string = '';
@state()
private editingIndex: number = -1;
@state()
private editingValue: string = '';
@state()
private draggedIndex: number = -1;
@state()
private dragOverIndex: number = -1;
public static styles = [
...DeesInputBase.baseStyles,
cssManager.defaultStyles,
css`
:host {
display: block;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
}
.input-wrapper {
width: 100%;
}
.list-container {
background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(0 0% 3.9%)')};
border: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
border-radius: 6px;
overflow: hidden;
transition: all 0.15s ease;
}
.list-container:hover:not(.disabled) {
border-color: ${cssManager.bdTheme('hsl(0 0% 79.8%)', 'hsl(0 0% 20.9%)')};
}
.list-container:focus-within {
border-color: ${cssManager.bdTheme('hsl(222.2 47.4% 51.2%)', 'hsl(217.2 91.2% 59.8%)')};
box-shadow: 0 0 0 3px ${cssManager.bdTheme('hsl(222.2 47.4% 51.2% / 0.1)', 'hsl(217.2 91.2% 59.8% / 0.1)')};
}
.list-container.disabled {
opacity: 0.6;
cursor: not-allowed;
}
.list-items {
max-height: 400px;
overflow-y: auto;
}
.list-item {
display: flex;
align-items: center;
gap: 8px;
padding: 12px 16px;
border-bottom: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(0 0% 3.9%)')};
transition: all 0.15s ease;
position: relative;
overflow: hidden; /* Prevent animation from affecting scroll bounds */
}
.list-item:last-of-type {
border-bottom: none;
}
.list-item:hover:not(.disabled) {
background: ${cssManager.bdTheme('hsl(0 0% 97.5%)', 'hsl(0 0% 6.9%)')};
}
.list-item.dragging {
opacity: 0.4;
background: ${cssManager.bdTheme('hsl(210 40% 96.1%)', 'hsl(215 20.2% 10.8%)')};
}
.list-item.drag-over {
background: ${cssManager.bdTheme('hsl(210 40% 93.1%)', 'hsl(215 20.2% 13.8%)')};
border-color: ${cssManager.bdTheme('hsl(222.2 47.4% 51.2%)', 'hsl(217.2 91.2% 59.8%)')};
}
.drag-handle {
display: flex;
align-items: center;
cursor: move;
color: ${cssManager.bdTheme('hsl(0 0% 63.9%)', 'hsl(0 0% 45.1%)')};
transition: color 0.15s ease;
}
.drag-handle:hover {
color: ${cssManager.bdTheme('hsl(0 0% 45.1%)', 'hsl(0 0% 63.9%)')};
}
.drag-handle dees-icon {
width: 16px;
height: 16px;
}
.item-content {
flex: 1;
display: flex;
align-items: center;
min-width: 0;
}
.item-text {
flex: 1;
color: ${cssManager.bdTheme('hsl(0 0% 9%)', 'hsl(0 0% 95%)')};
font-size: 14px;
line-height: 20px;
word-break: break-word;
}
.item-edit-input {
flex: 1;
padding: 4px 8px;
font-size: 14px;
font-family: inherit;
background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(0 0% 9%)')};
border: 1px solid ${cssManager.bdTheme('hsl(222.2 47.4% 51.2%)', 'hsl(217.2 91.2% 59.8%)')};
border-radius: 4px;
outline: none;
color: ${cssManager.bdTheme('hsl(0 0% 9%)', 'hsl(0 0% 95%)')};
}
.item-actions {
display: flex;
gap: 4px;
align-items: center;
}
.action-button {
display: flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
border-radius: 4px;
background: transparent;
border: none;
cursor: pointer;
transition: all 0.15s ease;
color: ${cssManager.bdTheme('hsl(0 0% 45.1%)', 'hsl(0 0% 63.9%)')};
}
.action-button:hover {
background: ${cssManager.bdTheme('hsl(0 0% 95.1%)', 'hsl(0 0% 14.9%)')};
color: ${cssManager.bdTheme('hsl(0 0% 9%)', 'hsl(0 0% 95%)')};
}
.action-button.save {
color: ${cssManager.bdTheme('hsl(142.1 76.2% 36.3%)', 'hsl(142.1 70.6% 45.3%)')};
}
.action-button.save:hover {
background: ${cssManager.bdTheme('hsl(142.1 76.2% 36.3% / 0.1)', 'hsl(142.1 70.6% 45.3% / 0.1)')};
}
.action-button.cancel {
color: ${cssManager.bdTheme('hsl(0 72.2% 50.6%)', 'hsl(0 62.8% 50.6%)')};
}
.action-button.cancel:hover {
background: ${cssManager.bdTheme('hsl(0 72.2% 50.6% / 0.1)', 'hsl(0 62.8% 50.6% / 0.1)')};
}
.action-button.delete {
color: ${cssManager.bdTheme('hsl(0 72.2% 50.6%)', 'hsl(0 62.8% 50.6%)')};
}
.action-button.delete:hover {
background: ${cssManager.bdTheme('hsl(0 72.2% 50.6% / 0.1)', 'hsl(0 62.8% 50.6% / 0.1)')};
}
.action-button dees-icon {
width: 14px;
height: 14px;
}
.add-item-container {
display: flex;
gap: 8px;
padding: 12px 16px;
background: ${cssManager.bdTheme('hsl(0 0% 97.5%)', 'hsl(0 0% 6.9%)')};
border-top: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
}
.add-input {
flex: 1;
padding: 8px 12px;
font-size: 14px;
font-family: inherit;
background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(0 0% 9%)')};
border: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
border-radius: 4px;
outline: none;
color: ${cssManager.bdTheme('hsl(0 0% 9%)', 'hsl(0 0% 95%)')};
transition: all 0.15s ease;
}
.add-input:focus {
border-color: ${cssManager.bdTheme('hsl(222.2 47.4% 51.2%)', 'hsl(217.2 91.2% 59.8%)')};
box-shadow: 0 0 0 3px ${cssManager.bdTheme('hsl(222.2 47.4% 51.2% / 0.1)', 'hsl(217.2 91.2% 59.8% / 0.1)')};
}
.add-input::placeholder {
color: ${cssManager.bdTheme('hsl(0 0% 63.9%)', 'hsl(0 0% 45.1%)')};
}
.add-input:disabled {
cursor: not-allowed;
opacity: 0.5;
}
.add-button {
padding: 8px 16px;
}
.empty-state {
padding: 32px 16px;
text-align: center;
color: ${cssManager.bdTheme('hsl(0 0% 63.9%)', 'hsl(0 0% 45.1%)')};
font-size: 14px;
font-style: italic;
}
.validation-message {
color: ${cssManager.bdTheme('hsl(0 72.2% 50.6%)', 'hsl(0 62.8% 30.6%)')};
font-size: 13px;
margin-top: 6px;
line-height: 1.5;
}
.description {
color: ${cssManager.bdTheme('hsl(215.4 16.3% 56.9%)', 'hsl(215 20.2% 55.1%)')};
font-size: 13px;
margin-top: 6px;
line-height: 1.5;
}
/* Scrollbar styling */
.list-items::-webkit-scrollbar {
width: 8px;
}
.list-items::-webkit-scrollbar-track {
background: transparent;
}
.list-items::-webkit-scrollbar-thumb {
background: ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 24.9%)')};
border-radius: 4px;
}
.list-items::-webkit-scrollbar-thumb:hover {
background: ${cssManager.bdTheme('hsl(0 0% 79.8%)', 'hsl(0 0% 34.9%)')};
}
/* Animation for adding/removing items */
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.list-item {
animation: slideIn 0.2s ease;
}
/* Override any inherited contain/content-visibility that might cause scrolling issues */
.list-items, .list-item {
content-visibility: visible !important;
contain: none !important;
contain-intrinsic-size: auto !important;
}
`,
];
public render(): TemplateResult {
return html`
<div class="input-wrapper">
${this.label ? html`<dees-label .label=${this.label} .required=${this.required}></dees-label>` : ''}
<div class="list-container ${this.disabled ? 'disabled' : ''}">
<div class="list-items">
${this.value.length > 0 ? this.value.map((item, index) => html`
<div
class="list-item ${this.draggedIndex === index ? 'dragging' : ''} ${this.dragOverIndex === index ? 'drag-over' : ''}"
draggable="${this.sortable && !this.disabled}"
@dragstart=${(e: DragEvent) => this.handleDragStart(e, index)}
@dragend=${this.handleDragEnd}
@dragover=${(e: DragEvent) => this.handleDragOver(e, index)}
@dragleave=${this.handleDragLeave}
@drop=${(e: DragEvent) => this.handleDrop(e, index)}
>
${this.sortable && !this.disabled ? html`
<div class="drag-handle">
<dees-icon .icon=${'lucide:gripVertical'}></dees-icon>
</div>
` : ''}
<div class="item-content">
${this.editingIndex === index ? html`
<input
type="text"
class="item-edit-input"
.value=${this.editingValue}
@input=${(e: InputEvent) => this.editingValue = (e.target as HTMLInputElement).value}
@keydown=${(e: KeyboardEvent) => this.handleEditKeyDown(e, index)}
@blur=${() => this.saveEdit(index)}
/>
` : html`
<div class="item-text" @dblclick=${() => !this.disabled && this.startEdit(index)}>
${item}
</div>
`}
</div>
<div class="item-actions">
${this.editingIndex === index ? html`
<button class="action-button save" @click=${() => this.saveEdit(index)}>
<dees-icon .icon=${'lucide:check'}></dees-icon>
</button>
<button class="action-button cancel" @click=${() => this.cancelEdit()}>
<dees-icon .icon=${'lucide:x'}></dees-icon>
</button>
` : html`
${!this.disabled ? html`
<button class="action-button" @click=${() => this.startEdit(index)}>
<dees-icon .icon=${'lucide:pencil'}></dees-icon>
</button>
<button class="action-button delete" @click=${() => this.removeItem(index)}>
<dees-icon .icon=${'lucide:trash2'}></dees-icon>
</button>
` : ''}
`}
</div>
</div>
`) : html`
<div class="empty-state">
No items added yet
</div>
`}
</div>
${!this.disabled && (!this.maxItems || this.value.length < this.maxItems) ? html`
<div class="add-item-container">
<input
type="text"
class="add-input"
.placeholder=${this.placeholder}
.value=${this.inputValue}
@input=${this.handleInput}
@keydown=${this.handleAddKeyDown}
?disabled=${this.disabled}
/>
<dees-button
class="add-button"
@click=${this.addItem}
?disabled=${!this.inputValue.trim()}
>
<dees-icon .icon=${'lucide:plus'}></dees-icon> Add
</dees-button>
</div>
` : ''}
</div>
${this.validationText ? html`
<div class="validation-message">${this.validationText}</div>
` : ''}
${this.description ? html`
<div class="description">${this.description}</div>
` : ''}
</div>
`;
}
private handleInput(e: InputEvent) {
this.inputValue = (e.target as HTMLInputElement).value;
}
private handleAddKeyDown(e: KeyboardEvent) {
if (e.key === 'Enter' && this.inputValue.trim()) {
e.preventDefault();
this.addItem();
}
}
private handleEditKeyDown(e: KeyboardEvent, index: number) {
if (e.key === 'Enter') {
e.preventDefault();
this.saveEdit(index);
} else if (e.key === 'Escape') {
e.preventDefault();
this.cancelEdit();
}
}
private addItem() {
const trimmedValue = this.inputValue.trim();
if (!trimmedValue) return;
if (!this.allowDuplicates && this.value.includes(trimmedValue)) {
this.validationText = 'This item already exists in the list';
setTimeout(() => this.validationText = '', 3000);
return;
}
if (this.maxItems && this.value.length >= this.maxItems) {
this.validationText = `Maximum ${this.maxItems} items allowed`;
setTimeout(() => this.validationText = '', 3000);
return;
}
this.value = [...this.value, trimmedValue];
this.inputValue = '';
this.validationText = '';
// Clear the input
const input = this.shadowRoot?.querySelector('.add-input') as HTMLInputElement;
if (input) {
input.value = '';
input.focus();
}
this.emitChange();
}
private startEdit(index: number) {
this.editingIndex = index;
this.editingValue = this.value[index];
// Focus the input after render
this.updateComplete.then(() => {
const input = this.shadowRoot?.querySelector('.item-edit-input') as HTMLInputElement;
if (input) {
input.focus();
input.select();
}
});
}
private saveEdit(index: number) {
const trimmedValue = this.editingValue.trim();
if (!trimmedValue) {
this.cancelEdit();
return;
}
if (!this.allowDuplicates && trimmedValue !== this.value[index] && this.value.includes(trimmedValue)) {
this.validationText = 'This item already exists in the list';
setTimeout(() => this.validationText = '', 3000);
return;
}
const newValue = [...this.value];
newValue[index] = trimmedValue;
this.value = newValue;
this.editingIndex = -1;
this.editingValue = '';
this.validationText = '';
this.emitChange();
}
private cancelEdit() {
this.editingIndex = -1;
this.editingValue = '';
}
private async removeItem(index: number) {
if (this.confirmDelete) {
const confirmed = await this.showConfirmDialog(`Delete "${this.value[index]}"?`);
if (!confirmed) return;
}
this.value = this.value.filter((_, i) => i !== index);
this.emitChange();
}
private async showConfirmDialog(message: string): Promise<boolean> {
// For now, use native confirm. In production, this should use a proper modal
return confirm(message);
}
// Drag and drop handlers
private handleDragStart(e: DragEvent, index: number) {
if (!this.sortable || this.disabled) return;
this.draggedIndex = index;
e.dataTransfer!.effectAllowed = 'move';
e.dataTransfer!.setData('text/plain', index.toString());
}
private handleDragEnd() {
this.draggedIndex = -1;
this.dragOverIndex = -1;
}
private handleDragOver(e: DragEvent, index: number) {
if (!this.sortable || this.disabled) return;
e.preventDefault();
e.dataTransfer!.dropEffect = 'move';
this.dragOverIndex = index;
}
private handleDragLeave() {
this.dragOverIndex = -1;
}
private handleDrop(e: DragEvent, dropIndex: number) {
if (!this.sortable || this.disabled) return;
e.preventDefault();
const draggedIndex = parseInt(e.dataTransfer!.getData('text/plain'));
if (draggedIndex !== dropIndex) {
const newValue = [...this.value];
const [draggedItem] = newValue.splice(draggedIndex, 1);
newValue.splice(dropIndex, 0, draggedItem);
this.value = newValue;
this.emitChange();
}
this.draggedIndex = -1;
this.dragOverIndex = -1;
}
private emitChange() {
this.dispatchEvent(new CustomEvent('change', {
detail: { value: this.value },
bubbles: true,
composed: true
}));
this.changeSubject.next(this);
}
public getValue(): string[] {
return this.value;
}
public setValue(value: string[]): void {
this.value = value || [];
}
public async validate(): Promise<boolean> {
if (this.required && (!this.value || this.value.length === 0)) {
this.validationText = 'At least one item is required';
return false;
}
if (this.minItems && this.value.length < this.minItems) {
this.validationText = `At least ${this.minItems} items required`;
return false;
}
this.validationText = '';
return true;
}
}

View File

@@ -231,7 +231,7 @@ export class DeesInputText extends DeesInputBase {
${this.isPasswordBool
? html`
<div class="showPassword" @click=${this.togglePasswordView}>
<dees-icon .icon=${this.showPasswordBool ? 'lucide:eye' : 'lucide:eye-off'}></dees-icon>
<dees-icon .icon=${this.showPasswordBool ? 'lucide:Eye' : 'lucide:EyeOff'}></dees-icon>
</div>
`
: html``}

View File

@@ -238,7 +238,7 @@ export class DeesSimpleAppDash extends DeesElement {
<div class="appbar">
<div>
<div class="sidebar-header">
<dees-icon .icon="lucide:grid3x3" style="font-size: 18px;"></dees-icon>
<dees-icon .icon="${'lucide:grid3x3'}" style="font-size: 18px;"></dees-icon>
<div class="appName">${this.name}</div>
</div>
<div class="viewTabs-container">
@@ -250,7 +250,7 @@ export class DeesSimpleAppDash extends DeesElement {
@click=${() => this.loadView(view)}
>
${view.iconName ? html`
<dees-icon .icon="${`lucide:${view.iconName}`}"></dees-icon>
<dees-icon .icon="${`${view.iconName}`}"></dees-icon>
` : ''}
<span style="flex: 1;">${view.name}</span>
</div>
@@ -263,7 +263,7 @@ export class DeesSimpleAppDash extends DeesElement {
<div class="action" @click=${() => {
this.dispatchEvent(new CustomEvent('logout', { bubbles: true, composed: true }));
}}>
<dees-icon .icon="lucide:logOut"></dees-icon>
<dees-icon .icon="${'lucide:logOut'}"></dees-icon>
<span>Logout</span>
</div>
</div>
@@ -273,11 +273,11 @@ export class DeesSimpleAppDash extends DeesElement {
</div>
<div class="controlbar">
<div class="control">
<dees-icon .icon="lucide:wifi"></dees-icon>
<dees-icon .icon="${'lucide:wifi'}"></dees-icon>
<span>Connected</span>
</div>
<div class="control" @click=${this.launchTerminal}>
<dees-icon .icon="lucide:terminal"></dees-icon>
<dees-icon .icon="${'lucide:terminal'}"></dees-icon>
<span>Terminal</span>
</div>
</div>
@@ -319,6 +319,8 @@ export class DeesSimpleAppDash extends DeesElement {
terminal.style.transition = 'all 0.2s';
terminal.style.background = '#000';
terminal.style.boxShadow = '0 4px 20px rgba(0, 0, 0, 0.3)';
terminal.style.maxWidth = `calc(${maincontainer.clientWidth}px -240px)`;
terminal.style.maxHeight = `calc(${maincontainer.clientHeight}px - 24px)`;
// Add close button to terminal
terminal.addEventListener('close', () => this.closeTerminal());

View File

@@ -32,6 +32,7 @@ export * from './dees-input-datepicker.js';
export * from './dees-input-dropdown.js';
export * from './dees-input-fileupload.js';
export * from './dees-input-iban.js';
export * from './dees-input-list.js';
export * from './profilepicture/dees-input-profilepicture.js';
export * from './dees-input-typelist.js';
export * from './dees-input-phone.js';