17 Commits

Author SHA1 Message Date
bf4fcfac71 3.36.4
Some checks failed
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-01-12 14:00:36 +00:00
ab601bfd7a 3.36.3
Some checks failed
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-01-12 13:54:44 +00:00
c0ef860bd6 3.36.2
Some checks failed
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-01-12 13:38:08 +00:00
debf9829af v3.36.1
Some checks failed
Default (tags) / security (push) Failing after 1s
Default (tags) / test (push) Failing after 0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-01-12 13:31:56 +00:00
e27141c686 fix(): no changes 2026-01-12 13:31:56 +00:00
e346d582ee fix(applauncher): call action after setting view, not instead 2026-01-12 13:31:35 +00:00
5d858d722c v3.36.0
Some checks failed
Default (tags) / security (push) Failing after 1s
Default (tags) / test (push) Failing after 1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-01-12 12:04:13 +00:00
0e9a025d95 feat(eco-view-peripherals): encapsulate networkRanges as internal @state and add public getter/setter methods 2026-01-12 12:04:13 +00:00
aa7dcf5339 v3.35.0
Some checks failed
Default (tags) / security (push) Failing after 1s
Default (tags) / test (push) Failing after 1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-01-12 11:47:54 +00:00
bc170d5d75 feat(peripherals): Add peripherals settings panel with network range management, network scanning, and manual device probe; update peripheral types and adjust UI/styling; overhaul README with expanded docs, quick start, and updated company/contact information 2026-01-12 11:47:54 +00:00
fb39ff161a v3.34.4
Some checks failed
Default (tags) / security (push) Failing after 1s
Default (tags) / test (push) Failing after 1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-01-12 11:00:43 +00:00
2f6b9e0e5c fix(catalog): no changes (empty diff) — no files modified 2026-01-12 11:00:43 +00:00
6c0a4b3e6e v3.34.3
Some checks failed
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-01-12 10:58:08 +00:00
0a11c3e65e fix(catalog): no changes detected 2026-01-12 10:58:08 +00:00
72900086cd update 2026-01-12 10:57:54 +00:00
c55cd25a88 update 2026-01-12 10:57:46 +00:00
fafa98bc19 update 2026-01-06 10:17:05 +00:00
66 changed files with 6747 additions and 5019 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

View File

@@ -1,5 +1,42 @@
# Changelog
## 2026-01-12 - 3.36.1 - fix()
no changes
- No files changed in this commit
- No release required
## 2026-01-12 - 3.36.0 - feat(eco-view-peripherals)
encapsulate networkRanges as internal @state and add public getter/setter methods
- Replaced @property({ type: Array }) with @state() for networkRanges to treat it as internal reactive state.
- Added setNetworkRanges(ranges: INetworkRange[]) and getNetworkRanges(): INetworkRange[] to provide a controlled public API for updating and reading network ranges.
- Methods use array spread to return/copy arrays, preventing external mutation of internal state.
- Added JSDoc comments for the new methods.
## 2026-01-12 - 3.35.0 - feat(peripherals)
Add peripherals settings panel with network range management, network scanning, and manual device probe; update peripheral types and adjust UI/styling; overhaul README with expanded docs, quick start, and updated company/contact information
- Add 'settings' peripheral category and new INetworkRange type; change IPeripheralDevice.type to exclude 'settings' to separate settings metadata from actual devices
- Implement network range management UI: add/remove ranges, display network list, scan networks (with isScanning state) and related controls
- Add "Add Device by IP" probe workflow and input UI to allow manual device discovery by IP
- Introduce new styles and header/button UI (settings-section, icon-button, network-list/network-item, etc.) for eco-view-peripherals
- Adjust categoryLabels type to exclude 'settings' to keep device grouping consistent
- Major README rewrite: expanded features list, installation examples (pnpm/npm), quick start code samples, developer commands, and updated company information to Task Venture Capital GmbH with new contact email
## 2026-01-12 - 3.34.4 - fix(catalog)
no changes (empty diff) — no files modified
- No files were modified; git diff is empty
- No dependency or source changes detected
- No version bump required
## 2026-01-12 - 3.34.3 - fix(catalog)
no changes detected
- Git diff contains no changes; no files modified
- No version bump required
## 2026-01-06 - 3.34.2 - fix(applauncher)
throttle inactivity timer resets in menus, optimize sound slider updates, and adjust keyboard layout/keys

View File

@@ -2,9 +2,28 @@
import * as deesWccTools from '@design.estate/dees-wcctools';
import * as deesDomTools from '@design.estate/dees-domtools';
// elements and pages
// elements, pages, and views
import * as elements from '../ts_web/elements/index.js';
import * as pages from '../ts_web/pages/index.js';
import * as views from '../ts_web/views/index.js';
deesWccTools.setupWccTools(elements as any, pages);
deesWccTools.setupWccTools({
sections: [
{
name: 'Pages',
type: 'pages',
items: pages,
},
{
name: 'Elements',
type: 'elements',
items: elements,
},
{
name: 'Views',
type: 'elements',
items: views,
},
],
});
deesDomTools.elementBasic.setup();

View File

@@ -3,37 +3,23 @@
"projectType": "wcc",
"module": {
"githost": "code.foss.global",
"gitscope": "design.estate",
"gitrepo": "dees-catalog",
"description": "A comprehensive library that provides dynamic web components for building sophisticated and modern web applications using JavaScript and TypeScript.",
"npmPackagename": "@design.estate/dees-catalog",
"gitscope": "ecobridge.xyz",
"gitrepo": "catalog",
"description": "A web component catalog for building ecobridge application interfaces with specialized components for the ecobridge ecosystem.",
"npmPackagename": "@ecobridge.xyz/catalog",
"license": "MIT",
"projectDomain": "design.estate",
"projectDomain": "ecobridge.xyz",
"keywords": [
"Web Components",
"User Interface",
"UI Library",
"Component Library",
"JavaScript",
"TypeScript",
"Dynamic Components",
"Modular Architecture",
"Reusable Components",
"Web Development",
"Application UI",
"Ecobridge",
"App Launcher",
"Desktop Interface",
"Custom Elements",
"Shadow DOM",
"UI Elements",
"Dashboard Interfaces",
"Form Handling",
"Data Display",
"Visualization",
"Charting",
"Interactive Components",
"Responsive Design",
"Web Applications",
"Modern Web",
"Frontend Development"
"LitElement"
]
},
"release": {
@@ -45,7 +31,7 @@
}
},
"@git.zone/tsdoc": {
"legal": "\n## License and Legal Information\n\nThis repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the [license](license) file within this repository. \n\n**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.\n\n### Trademarks\n\nThis project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH and are not included within the scope of the MIT license granted herein. Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines, and any usage must be approved in writing by Task Venture Capital GmbH.\n\n### Company Information\n\nTask Venture Capital GmbH \nRegistered at District court Bremen HRB 35230 HB, Germany\n\nFor any legal inquiries or if you require further information, please contact us via email at hello@task.vc.\n\nBy using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.\n"
"legal": "\n## License and Legal Information\n\nThis repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the [license](license) file within this repository. \n\n**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.\n\n### Trademarks\n\nThis project is owned and maintained by Lossless GmbH. The names and logos associated with Lossless GmbH and any related products or services are trademarks of Lossless GmbH and are not included within the scope of the MIT license granted herein. Use of these trademarks must comply with Lossless GmbH's Trademark Guidelines, and any usage must be approved in writing by Lossless GmbH.\n\n### Company Information\n\nLossless GmbH\n\nFor any legal inquiries or if you require further information, please contact us via the official channels.\n\nBy using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Lossless GmbH of any derivative works.\n"
},
"@ship.zone/szci": {
"npmGlobalTools": []

View File

@@ -1,6 +1,6 @@
{
"name": "@ecobridge.xyz/catalog",
"version": "3.34.2",
"version": "3.36.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",

1653
readme.md

File diff suppressed because it is too large Load Diff

View File

@@ -1,784 +0,0 @@
# 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,12 +0,0 @@
import { tap, expect, webhelpers } from '@git.zone/tstest/tapbundle';
import * as deesCatalog from '../ts_web/index.js';
tap.test('should create a working button', async () => {
const button: deesCatalog.DeesButton = await webhelpers.fixture(
webhelpers.html`<dees-button></dees-button>`
);
expect(button).toBeInstanceOf(deesCatalog.DeesButton);
});
export default tap.start();

View File

@@ -1,35 +0,0 @@
import { expect, tap } from '@git.zone/tstest/tapbundle';
import { DeesContextmenu } from '../ts_web/elements/dees-contextmenu/dees-contextmenu.js';
import { demoFunc } from '../ts_web/elements/dees-contextmenu/dees-contextmenu.demo.js';
tap.test('should render context menu demo', async () => {
// Create demo container
const demoContainer = document.createElement('div');
document.body.appendChild(demoContainer);
// Render the demo
const demoContent = demoFunc();
// Create a temporary element to hold the rendered template
const tempDiv = document.createElement('div');
tempDiv.innerHTML = demoContent.strings.join('');
// Check that panels are rendered
const panels = tempDiv.querySelectorAll('dees-panel');
expect(panels.length).toEqual(4);
// Check panel headings
expect(panels[0].getAttribute('heading')).toEqual('Basic Context Menu with Nested Submenus');
expect(panels[1].getAttribute('heading')).toEqual('Component-Specific Context Menu');
expect(panels[2].getAttribute('heading')).toEqual('Advanced Context Menu Example');
expect(panels[3].getAttribute('heading')).toEqual('Static Context Menu (Always Visible)');
// Check that static context menu exists
const staticMenu = tempDiv.querySelector('dees-contextmenu');
expect(staticMenu).toBeTruthy();
// Clean up
demoContainer.remove();
});
export default tap.start();

View File

@@ -1,93 +0,0 @@
import { expect, tap } from '@git.zone/tstest/tapbundle';
import { DeesContextmenu } from '../ts_web/elements/dees-contextmenu/dees-contextmenu.js';
tap.test('should close all parent menus when clicking action in nested submenu', async () => {
let actionCalled = false;
// Create a test element
const testDiv = document.createElement('div');
testDiv.style.width = '300px';
testDiv.style.height = '300px';
testDiv.style.background = '#f0f0f0';
testDiv.innerHTML = 'Right-click for nested menu test';
document.body.appendChild(testDiv);
// Simulate right-click to open context menu
const contextMenuEvent = new MouseEvent('contextmenu', {
clientX: 150,
clientY: 150,
bubbles: true,
cancelable: true
});
// Open context menu with nested structure
DeesContextmenu.openContextMenuWithOptions(contextMenuEvent, [
{
name: 'Parent Item',
iconName: 'folder',
action: async () => {}, // Parent items with submenus need an action
submenu: [
{
name: 'Child Item',
iconName: 'file',
action: async () => {
actionCalled = true;
console.log('Child action called');
}
},
{
name: 'Another Child',
iconName: 'fileText',
action: async () => console.log('Another child')
}
]
},
{
name: 'Regular Item',
iconName: 'box',
action: async () => console.log('Regular item')
}
]);
// Wait for main menu to appear
await new Promise(resolve => setTimeout(resolve, 150));
// Check main menu exists
const mainMenu = document.querySelector('dees-contextmenu');
expect(mainMenu).toBeInstanceOf(DeesContextmenu);
// Hover over "Parent Item" to trigger submenu
const parentItem = mainMenu!.shadowRoot!.querySelector('.menuitem');
expect(parentItem).toBeTruthy();
parentItem!.dispatchEvent(new MouseEvent('mouseenter', { bubbles: true }));
// Wait for submenu to appear
await new Promise(resolve => setTimeout(resolve, 300));
// Check submenu exists
const allMenus = document.querySelectorAll('dees-contextmenu');
expect(allMenus.length).toEqual(2); // Main menu and submenu
const submenu = allMenus[1];
expect(submenu).toBeTruthy();
// Click on "Child Item" in submenu
const childItem = submenu.shadowRoot!.querySelector('.menuitem');
expect(childItem).toBeTruthy();
childItem!.click();
// Wait for menus to close (windowLayer destruction takes 300ms + context menu 100ms)
await new Promise(resolve => setTimeout(resolve, 600));
// Verify action was called
expect(actionCalled).toEqual(true);
// Verify all menus are closed
const remainingMenus = document.querySelectorAll('dees-contextmenu');
expect(remainingMenus.length).toEqual(0);
// Clean up
testDiv.remove();
});
export default tap.start();

View File

@@ -1,71 +0,0 @@
import { expect, tap } from '@git.zone/tstest/tapbundle';
import { DeesContextmenu } from '../ts_web/elements/dees-contextmenu/dees-contextmenu.js';
import { DeesElement, customElement, html } from '@design.estate/dees-element';
// Create a test element with shadow DOM
@customElement('test-shadow-element')
class TestShadowElement extends DeesElement {
public getContextMenuItems() {
return [
{ name: 'Shadow Item 1', iconName: 'box', action: async () => console.log('Shadow 1') },
{ name: 'Shadow Item 2', iconName: 'package', action: async () => console.log('Shadow 2') }
];
}
render() {
return html`
<div style="padding: 40px; background: #eee; border-radius: 8px;">
<h3>Shadow DOM Content</h3>
<p>Right-click anywhere inside this shadow DOM</p>
</div>
`;
}
}
tap.test('should show context menu when right-clicking inside shadow DOM', async () => {
// Create the shadow DOM element
const shadowElement = document.createElement('test-shadow-element');
document.body.appendChild(shadowElement);
// Wait for element to be ready
await shadowElement.updateComplete;
// Get the content inside shadow DOM
const shadowContent = shadowElement.shadowRoot!.querySelector('div');
expect(shadowContent).toBeTruthy();
// Simulate right-click on content inside shadow DOM
const contextMenuEvent = new MouseEvent('contextmenu', {
clientX: 100,
clientY: 100,
bubbles: true,
cancelable: true,
composed: true // Important for shadow DOM
});
shadowContent!.dispatchEvent(contextMenuEvent);
// Wait for context menu to appear
await new Promise(resolve => setTimeout(resolve, 100));
// Check if context menu is created
const contextMenu = document.querySelector('dees-contextmenu');
expect(contextMenu).toBeInstanceOf(DeesContextmenu);
// Check if menu items from shadow element are rendered
const menuItems = contextMenu!.shadowRoot!.querySelectorAll('.menuitem');
expect(menuItems.length).toBeGreaterThanOrEqual(2);
// Check menu item text
const menuTexts = Array.from(menuItems).map(item =>
item.querySelector('.menuitem-text')?.textContent
);
expect(menuTexts).toContain('Shadow Item 1');
expect(menuTexts).toContain('Shadow Item 2');
// Clean up
contextMenu!.remove();
shadowElement.remove();
});
export default tap.start();

View File

@@ -1,77 +0,0 @@
import { expect, tap } from '@git.zone/tstest/tapbundle';
import { DeesContextmenu } from '../ts_web/elements/dees-contextmenu/dees-contextmenu.js';
tap.test('should show context menu with nested submenu', async () => {
// Create a test element with context menu items
const testDiv = document.createElement('div');
testDiv.style.width = '200px';
testDiv.style.height = '200px';
testDiv.style.background = '#eee';
testDiv.innerHTML = 'Right-click me';
// Add getContextMenuItems method
(testDiv as any).getContextMenuItems = () => {
return [
{
name: 'Change Type',
iconName: 'type',
submenu: [
{ name: 'Paragraph', iconName: 'text', action: () => console.log('Paragraph') },
{ name: 'Heading 1', iconName: 'heading1', action: () => console.log('Heading 1') },
{ name: 'Heading 2', iconName: 'heading2', action: () => console.log('Heading 2') },
{ divider: true },
{ name: 'Code Block', iconName: 'fileCode', action: () => console.log('Code') },
{ name: 'Quote', iconName: 'quote', action: () => console.log('Quote') }
]
},
{ divider: true },
{
name: 'Delete',
iconName: 'trash2',
action: () => console.log('Delete')
}
];
};
document.body.appendChild(testDiv);
// Simulate right-click
const contextMenuEvent = new MouseEvent('contextmenu', {
clientX: 100,
clientY: 100,
bubbles: true,
cancelable: true
});
testDiv.dispatchEvent(contextMenuEvent);
// Wait for context menu to appear
await new Promise(resolve => setTimeout(resolve, 100));
// Check if context menu is created
const contextMenu = document.querySelector('dees-contextmenu');
expect(contextMenu).toBeInstanceOf(DeesContextmenu);
// Check if menu items are rendered
const menuItems = contextMenu!.shadowRoot!.querySelectorAll('.menuitem');
expect(menuItems.length).toEqual(2); // "Change Type" and "Delete"
// Hover over "Change Type" to trigger submenu
const changeTypeItem = menuItems[0] as HTMLElement;
changeTypeItem.dispatchEvent(new MouseEvent('mouseenter', { bubbles: true }));
// Wait for submenu to appear
await new Promise(resolve => setTimeout(resolve, 300));
// Check if submenu is created
const submenus = document.querySelectorAll('dees-contextmenu');
expect(submenus.length).toEqual(2); // Main menu and submenu
// Clean up
contextMenu!.remove();
const submenu = submenus[1];
if (submenu) submenu.remove();
testDiv.remove();
});
export default tap.start();

View File

@@ -1,28 +0,0 @@
import { tap, expect } from '@git.zone/tstest/tapbundle';
import {
resolveWidgetPlacement,
collectCollisions,
} from '../ts_web/elements/dees-dashboardgrid/layout.ts';
import type { DashboardWidget } from '../ts_web/elements/dees-dashboardgrid/types.ts';
tap.test('dashboardgrid does not overlap widgets after swap attempt', async () => {
const widgets: DashboardWidget[] = [
{ id: 'w0', x: 6, y: 5, w: 1, h: 3 },
{ id: 'w1', x: 6, y: 1, w: 1, h: 3 },
{ id: 'w2', x: 3, y: 0, w: 2, h: 2 },
{ id: 'w3', x: 9, y: 0, w: 1, h: 2 },
{ id: 'w4', x: 4, y: 3, w: 1, h: 2 },
];
const placement = resolveWidgetPlacement(widgets, 'w0', { x: 6, y: 3 }, 12);
expect(placement).toBeTruthy();
const layout = placement!.widgets;
for (const widget of layout) {
const collisions = collectCollisions(layout, widget, widget.x, widget.y, widget.w, widget.h);
expect(collisions).toBeEmptyArray();
}
});
export default tap.start();

View File

@@ -0,0 +1,35 @@
import { tap, expect, webhelpers } from '@git.zone/tstest/tapbundle';
import * as ecoCatalog from '../ts_web/index.js';
tap.test('should export EcoApplauncher component', async () => {
expect(ecoCatalog.EcoApplauncher).toBeTypeOf('function');
});
tap.test('should export EcoScreensaver component', async () => {
expect(ecoCatalog.EcoScreensaver).toBeTypeOf('function');
});
tap.test('should export applauncher sub-components', async () => {
expect(ecoCatalog.EcoApplauncherWifimenu).toBeTypeOf('function');
expect(ecoCatalog.EcoApplauncherBatterymenu).toBeTypeOf('function');
expect(ecoCatalog.EcoApplauncherSoundmenu).toBeTypeOf('function');
expect(ecoCatalog.EcoApplauncherKeyboard).toBeTypeOf('function');
expect(ecoCatalog.EcoApplauncherPowermenu).toBeTypeOf('function');
});
tap.test('should create a working EcoApplauncher instance', async () => {
const applauncher: ecoCatalog.EcoApplauncher = await webhelpers.fixture(
webhelpers.html`<eco-applauncher></eco-applauncher>`
);
expect(applauncher).toBeInstanceOf(ecoCatalog.EcoApplauncher);
});
tap.test('should create a working EcoScreensaver instance', async () => {
const screensaver: ecoCatalog.EcoScreensaver = await webhelpers.fixture(
webhelpers.html`<eco-screensaver></eco-screensaver>`
);
expect(screensaver).toBeInstanceOf(ecoCatalog.EcoScreensaver);
});
export default tap.start();

View File

@@ -1,183 +0,0 @@
import { expect, tap, webhelpers } from '@git.zone/tstest/tapbundle';
import { DeesWysiwygBlock } from '../ts_web/elements/00group-input/dees-input-wysiwyg/dees-wysiwyg-block.js';
import { WysiwygSelection } from '../ts_web/elements/00group-input/dees-input-wysiwyg/wysiwyg.selection.js';
tap.test('Shadow DOM containment should work correctly', async () => {
console.log('=== Testing Shadow DOM Containment ===');
// Wait for custom element to be defined
await customElements.whenDefined('dees-wysiwyg-block');
// Create a WYSIWYG block component - set properties BEFORE attaching to DOM
const block = document.createElement('dees-wysiwyg-block') as DeesWysiwygBlock;
// Set the block data before attaching to DOM so firstUpdated() sees them
block.block = {
id: 'test-1',
type: 'paragraph',
content: 'Hello world test content'
};
block.handlers = {
onInput: () => {},
onKeyDown: () => {},
onFocus: () => {},
onBlur: () => {},
onCompositionStart: () => {},
onCompositionEnd: () => {}
};
// Now attach to DOM and wait for render
document.body.appendChild(block);
await block.updateComplete;
// Wait for firstUpdated to populate the container
await new Promise(resolve => setTimeout(resolve, 50));
// Get the paragraph element inside Shadow DOM
const container = block.shadowRoot?.querySelector('.wysiwyg-block-container') as HTMLElement;
const paragraphBlock = container?.querySelector('.block.paragraph') as HTMLElement;
expect(paragraphBlock).toBeTruthy();
console.log('Found paragraph block:', paragraphBlock);
console.log('Paragraph text content:', paragraphBlock.textContent);
// Focus the paragraph
paragraphBlock.focus();
// Manually set cursor position
const textNode = paragraphBlock.firstChild;
if (textNode && textNode.nodeType === Node.TEXT_NODE) {
const range = document.createRange();
const selection = window.getSelection();
// Set cursor at position 11 (after "Hello world")
range.setStart(textNode, 11);
range.setEnd(textNode, 11);
selection?.removeAllRanges();
selection?.addRange(range);
console.log('Set cursor at position 11');
// Test the containment check
console.log('\n--- Testing containment ---');
const currentSelection = window.getSelection();
if (currentSelection && currentSelection.rangeCount > 0) {
const selRange = currentSelection.getRangeAt(0);
console.log('Selection range:', {
startContainer: selRange.startContainer,
startOffset: selRange.startOffset,
containerText: selRange.startContainer.textContent
});
// Test regular contains (should fail across Shadow DOM)
const regularContains = paragraphBlock.contains(selRange.startContainer);
console.log('Regular contains:', regularContains);
// Test Shadow DOM-aware contains
const shadowDOMContains = WysiwygSelection.containsAcrossShadowDOM(paragraphBlock, selRange.startContainer);
console.log('Shadow DOM contains:', shadowDOMContains);
// Since we're setting selection within the same shadow DOM, both should be true
expect(regularContains).toBeTrue();
expect(shadowDOMContains).toBeTrue();
}
// Test getSplitContent
console.log('\n--- Testing getSplitContent ---');
const splitResult = block.getSplitContent();
console.log('Split result:', splitResult);
expect(splitResult).toBeTruthy();
if (splitResult) {
console.log('Before:', JSON.stringify(splitResult.before));
console.log('After:', JSON.stringify(splitResult.after));
// Expected split at position 11
expect(splitResult.before).toEqual('Hello world');
expect(splitResult.after).toEqual(' test content');
}
}
// Clean up
document.body.removeChild(block);
});
tap.test('Shadow DOM containment across different shadow roots', async () => {
console.log('=== Testing Cross Shadow Root Containment ===');
// Create parent component with WYSIWYG editor
const parentDiv = document.createElement('div');
parentDiv.innerHTML = `
<dees-input-wysiwyg>
<dees-wysiwyg-block></dees-wysiwyg-block>
</dees-input-wysiwyg>
`;
document.body.appendChild(parentDiv);
// Wait for components to be ready
await new Promise(resolve => setTimeout(resolve, 100));
const wysiwygInput = parentDiv.querySelector('dees-input-wysiwyg') as any;
const blockElement = wysiwygInput?.shadowRoot?.querySelector('dees-wysiwyg-block') as DeesWysiwygBlock;
if (blockElement) {
// Set block data
blockElement.block = {
id: 'test-2',
type: 'paragraph',
content: 'Cross shadow DOM test'
};
blockElement.handlers = {
onInput: () => {},
onKeyDown: () => {},
onFocus: () => {},
onBlur: () => {},
onCompositionStart: () => {},
onCompositionEnd: () => {}
};
await blockElement.updateComplete;
// Get the paragraph inside the nested shadow DOM
const container = blockElement.shadowRoot?.querySelector('.wysiwyg-block-container') as HTMLElement;
const paragraphBlock = container?.querySelector('.block.paragraph') as HTMLElement;
if (paragraphBlock) {
console.log('Found nested paragraph block');
// Focus and set selection
paragraphBlock.focus();
const textNode = paragraphBlock.firstChild;
if (textNode && textNode.nodeType === Node.TEXT_NODE) {
const range = document.createRange();
range.setStart(textNode, 5);
range.setEnd(textNode, 5);
const selection = window.getSelection();
selection?.removeAllRanges();
selection?.addRange(range);
// Test containment from parent's perspective
const selRange = selection?.getRangeAt(0);
if (selRange) {
// This should fail because it crosses shadow DOM boundary
const regularContains = wysiwygInput.contains(selRange.startContainer);
console.log('Parent regular contains:', regularContains);
expect(regularContains).toBeFalse();
// This should work with our Shadow DOM-aware method
const shadowDOMContains = WysiwygSelection.containsAcrossShadowDOM(wysiwygInput, selRange.startContainer);
console.log('Parent shadow DOM contains:', shadowDOMContains);
expect(shadowDOMContains).toBeTrue();
}
}
}
}
// Clean up
document.body.removeChild(parentDiv);
});
export default tap.start();

View File

@@ -1,146 +0,0 @@
import { expect, tap } from '@git.zone/tstest/tapbundle';
import * as deesCatalog from '../ts_web/index.js';
tap.test('tabs indicator positioning - detailed measurements', async () => {
// Create tabs element with different length labels
const tabsElement = new deesCatalog.DeesAppuiTabs();
tabsElement.tabs = [
{ key: 'Home', iconName: 'lucide:home', action: () => {} },
{ key: 'Analytics Dashboard', iconName: 'lucide:lineChart', action: () => {} },
{ key: 'User Settings', iconName: 'lucide:settings', action: () => {} },
];
document.body.appendChild(tabsElement);
await tabsElement.updateComplete;
// Wait for fonts and indicator initialization
await new Promise(resolve => setTimeout(resolve, 200));
// Get all elements
const shadowRoot = tabsElement.shadowRoot;
const wrapper = shadowRoot.querySelector('.tabs-wrapper') as HTMLElement;
const container = shadowRoot.querySelector('.tabsContainer') as HTMLElement;
const tabs = shadowRoot.querySelectorAll('.tab');
const firstTab = tabs[0] as HTMLElement;
const firstContent = firstTab.querySelector('.tab-content') as HTMLElement;
const indicator = shadowRoot.querySelector('.tabIndicator') as HTMLElement;
// Verify all elements exist
expect(wrapper).toBeInstanceOf(HTMLElement);
expect(container).toBeInstanceOf(HTMLElement);
expect(firstTab).toBeInstanceOf(HTMLElement);
expect(firstContent).toBeInstanceOf(HTMLElement);
expect(indicator).toBeInstanceOf(HTMLElement);
// Get all measurements
const wrapperRect = wrapper.getBoundingClientRect();
const containerRect = container.getBoundingClientRect();
const tabRect = firstTab.getBoundingClientRect();
const contentRect = firstContent.getBoundingClientRect();
const indicatorRect = indicator.getBoundingClientRect();
console.log('\n=== DETAILED MEASUREMENTS ===');
console.log('Document body left:', document.body.getBoundingClientRect().left);
console.log('Wrapper left:', wrapperRect.left);
console.log('Container left:', containerRect.left);
console.log('Tab left:', tabRect.left);
console.log('Content left:', contentRect.left);
console.log('Indicator left (actual):', indicatorRect.left);
console.log('\n=== RELATIVE POSITIONS ===');
console.log('Container padding (container - wrapper):', containerRect.left - wrapperRect.left);
console.log('Tab position in container:', tabRect.left - containerRect.left);
console.log('Content position in tab:', contentRect.left - tabRect.left);
console.log('Content relative to wrapper:', contentRect.left - wrapperRect.left);
console.log('Indicator relative to wrapper (actual):', indicatorRect.left - wrapperRect.left);
console.log('\n=== WIDTHS ===');
console.log('Tab width:', tabRect.width);
console.log('Content width:', contentRect.width);
console.log('Indicator width:', indicatorRect.width);
console.log('\n=== STYLES (what we set) ===');
console.log('Indicator style.left:', indicator.style.left);
console.log('Indicator style.width:', indicator.style.width);
console.log('\n=== CALCULATIONS ===');
const expectedIndicatorLeft = contentRect.left - wrapperRect.left - 4; // We subtract 4 to center
const expectedIndicatorWidth = contentRect.width + 8; // We add 8 in the code
console.log('Expected indicator left:', expectedIndicatorLeft);
console.log('Expected indicator width:', expectedIndicatorWidth);
console.log('Actual indicator left (from style):', parseFloat(indicator.style.left));
console.log('Actual indicator width (from style):', parseFloat(indicator.style.width));
console.log('\n=== VISUAL ALIGNMENT CHECK ===');
const tabCenter = tabRect.left + (tabRect.width / 2);
const contentCenter = contentRect.left + (contentRect.width / 2);
const indicatorCenter = indicatorRect.left + (indicatorRect.width / 2);
console.log('Tab center:', tabCenter);
console.log('Content center:', contentCenter);
console.log('Indicator center:', indicatorCenter);
console.log('Content offset from tab center:', contentCenter - tabCenter);
console.log('Indicator offset from content center:', indicatorCenter - contentCenter);
console.log('Indicator offset from tab center:', indicatorCenter - tabCenter);
console.log('---');
console.log('Indicator extends left of content by:', contentRect.left - indicatorRect.left);
console.log('Indicator extends right of content by:', (indicatorRect.left + indicatorRect.width) - (contentRect.left + contentRect.width));
// Check if icons are rendering
const icon = firstContent.querySelector('dees-icon');
console.log('\n=== ICON CHECK ===');
console.log('Icon element found:', icon ? 'YES' : 'NO');
if (icon) {
const iconRect = icon.getBoundingClientRect();
console.log('Icon width:', iconRect.width);
console.log('Icon height:', iconRect.height);
console.log('Icon visible:', iconRect.width > 0 && iconRect.height > 0 ? 'YES' : 'NO');
}
// Verify indicator is visible
expect(indicator.style.opacity).toEqual('1');
// Verify positioning calculations
expect(parseFloat(indicator.style.left)).toBeCloseTo(expectedIndicatorLeft, 1);
expect(parseFloat(indicator.style.width)).toBeCloseTo(expectedIndicatorWidth, 1);
// Verify visual centering on content (should be perfectly centered)
expect(Math.abs(indicatorCenter - contentCenter)).toBeLessThan(1);
document.body.removeChild(tabsElement);
});
tap.test('tabs indicator should move when tab is clicked', async () => {
// Create tabs element
const tabsElement = new deesCatalog.DeesAppuiTabs();
tabsElement.tabs = [
{ key: 'Home', iconName: 'lucide:home', action: () => {} },
{ key: 'Analytics', iconName: 'lucide:barChart', action: () => {} },
{ key: 'Settings', iconName: 'lucide:settings', action: () => {} },
];
document.body.appendChild(tabsElement);
await tabsElement.updateComplete;
await new Promise(resolve => setTimeout(resolve, 100));
const shadowRoot = tabsElement.shadowRoot;
const tabs = shadowRoot.querySelectorAll('.tab');
const indicator = shadowRoot.querySelector('.tabIndicator') as HTMLElement;
// Get initial position
const initialLeft = parseFloat(indicator.style.left);
// Click second tab
(tabs[1] as HTMLElement).click();
await tabsElement.updateComplete;
await new Promise(resolve => setTimeout(resolve, 100));
// Position should have changed
const newLeft = parseFloat(indicator.style.left);
expect(newLeft).not.toEqual(initialLeft);
expect(newLeft).toBeGreaterThan(initialLeft);
document.body.removeChild(tabsElement);
});
export default tap.start();

View File

@@ -1,9 +0,0 @@
import { expect, tap } from '@git.zone/tstest/tapbundle';
import { DeesInputWysiwyg } from '../ts_web/elements/00group-input/dees-input-wysiwyg/dees-input-wysiwyg.js';
tap.test('should create wysiwyg editor', async () => {
const editor = new DeesInputWysiwyg();
expect(editor).toBeInstanceOf(DeesInputWysiwyg);
});
export default tap.start();

View File

@@ -1,85 +0,0 @@
import { expect, tap } from '@git.zone/tstest/tapbundle';
import { DeesInputWysiwyg } from '../ts_web/elements/00group-input/dees-input-wysiwyg/dees-input-wysiwyg.js';
// Initialize the element
DeesInputWysiwyg;
tap.test('wysiwyg block movement during drag', async () => {
const element = document.createElement('dees-input-wysiwyg');
document.body.appendChild(element);
await element.updateComplete;
// Set initial content
element.blocks = [
{ id: 'block1', type: 'paragraph', content: 'Block 1' },
{ id: 'block2', type: 'paragraph', content: 'Block 2' },
{ id: 'block3', type: 'paragraph', content: 'Block 3' },
];
element.renderBlocksProgrammatically();
await element.updateComplete;
const editorContent = element.shadowRoot!.querySelector('.editor-content') as HTMLDivElement;
const block1 = editorContent.querySelector('[data-block-id="block1"]') as HTMLElement;
// Start dragging block 1
const mockDragEvent = {
dataTransfer: {
effectAllowed: '',
setData: () => {},
setDragImage: () => {}
},
clientY: 50,
preventDefault: () => {},
} as any;
element.dragDropHandler.handleDragStart(mockDragEvent, element.blocks[0]);
// Wait for dragging class
await new Promise(resolve => setTimeout(resolve, 20));
// Verify drag state
expect(element.dragDropHandler.dragState.draggedBlockId).toEqual('block1');
// Check that drag height was calculated
console.log('Checking drag height...');
const dragHandler = element.dragDropHandler as any;
console.log('draggedBlockHeight:', dragHandler.draggedBlockHeight);
console.log('draggedBlockContentHeight:', dragHandler.draggedBlockContentHeight);
// Manually call updateBlockPositions to simulate drag movement
console.log('Simulating drag movement...');
const updateBlockPositions = dragHandler.updateBlockPositions.bind(dragHandler);
// Simulate dragging down past block 2
const block2 = editorContent.querySelector('[data-block-id="block2"]') as HTMLElement;
const block2Rect = block2.getBoundingClientRect();
const dragToY = block2Rect.bottom + 10;
console.log('Dragging to Y position:', dragToY);
updateBlockPositions(dragToY);
// Check if blocks have moved
await new Promise(resolve => setTimeout(resolve, 50));
const blocks = Array.from(editorContent.querySelectorAll('.block-wrapper'));
console.log('Block states after drag:');
blocks.forEach((block, i) => {
const classes = block.className;
const offset = (block as HTMLElement).style.getPropertyValue('--drag-offset');
console.log(`Block ${i}: classes="${classes}", offset="${offset}"`);
});
// Check that at least one block has move class
const movedUpBlocks = editorContent.querySelectorAll('.block-wrapper.move-up');
const movedDownBlocks = editorContent.querySelectorAll('.block-wrapper.move-down');
console.log('Moved up blocks:', movedUpBlocks.length);
console.log('Moved down blocks:', movedDownBlocks.length);
// Clean up
element.dragDropHandler.handleDragEnd();
document.body.removeChild(element);
});
export default tap.start();

View File

@@ -1,69 +0,0 @@
import { tap, expect, webhelpers } from '@git.zone/tstest/tapbundle';
import * as deesCatalog from '../ts_web/index.js';
import { BlockRegistry } from '../ts_web/elements/00group-input/dees-input-wysiwyg/blocks/block.registry.js';
import { DeesWysiwygBlock } from '../ts_web/elements/00group-input/dees-input-wysiwyg/dees-wysiwyg-block.js';
// Import block registration to ensure handlers are registered
import '../ts_web/elements/00group-input/dees-input-wysiwyg/wysiwyg.blockregistration.js';
tap.test('Debug: should create empty wysiwyg block component', async () => {
try {
console.log('Creating DeesWysiwygBlock...');
const block: DeesWysiwygBlock = await webhelpers.fixture(
webhelpers.html`<dees-wysiwyg-block></dees-wysiwyg-block>`
);
console.log('Block created:', block);
expect(block).toBeDefined();
expect(block).toBeInstanceOf(DeesWysiwygBlock);
console.log('Initial block property:', block.block);
console.log('Initial handlers property:', block.handlers);
} catch (error) {
console.error('Error creating block:', error);
throw error;
}
});
tap.test('Debug: should set properties step by step', async () => {
try {
console.log('Step 1: Creating component...');
const block: DeesWysiwygBlock = document.createElement('dees-wysiwyg-block') as DeesWysiwygBlock;
expect(block).toBeDefined();
console.log('Step 2: Setting handlers...');
block.handlers = {
onInput: () => console.log('onInput'),
onKeyDown: () => console.log('onKeyDown'),
onFocus: () => console.log('onFocus'),
onBlur: () => console.log('onBlur'),
onCompositionStart: () => console.log('onCompositionStart'),
onCompositionEnd: () => console.log('onCompositionEnd')
};
console.log('Handlers set:', block.handlers);
console.log('Step 3: Setting block data...');
block.block = {
id: 'test-block',
type: 'divider',
content: ' '
};
console.log('Block set:', block.block);
console.log('Step 4: Appending to body...');
document.body.appendChild(block);
console.log('Step 5: Waiting for update...');
await block.updateComplete;
console.log('Update complete');
console.log('Step 6: Checking shadowRoot...');
expect(block.shadowRoot).toBeDefined();
console.log('ShadowRoot exists');
} catch (error) {
console.error('Error in step-by-step test:', error);
throw error;
}
});
export default tap.start();

View File

@@ -1,240 +0,0 @@
import { tap, expect, webhelpers } from '@git.zone/tstest/tapbundle';
import * as deesCatalog from '../ts_web/index.js';
import { BlockRegistry } from '../ts_web/elements/00group-input/dees-input-wysiwyg/blocks/block.registry.js';
import { DeesWysiwygBlock } from '../ts_web/elements/00group-input/dees-input-wysiwyg/dees-wysiwyg-block.js';
// Import block registration to ensure handlers are registered
import '../ts_web/elements/00group-input/dees-input-wysiwyg/wysiwyg.blockregistration.js';
tap.test('BlockRegistry should have registered handlers', async () => {
// Test divider handler
const dividerHandler = BlockRegistry.getHandler('divider');
expect(dividerHandler).toBeDefined();
expect(dividerHandler?.type).toEqual('divider');
// Test paragraph handler
const paragraphHandler = BlockRegistry.getHandler('paragraph');
expect(paragraphHandler).toBeDefined();
expect(paragraphHandler?.type).toEqual('paragraph');
// Test heading handlers
const heading1Handler = BlockRegistry.getHandler('heading-1');
expect(heading1Handler).toBeDefined();
expect(heading1Handler?.type).toEqual('heading-1');
const heading2Handler = BlockRegistry.getHandler('heading-2');
expect(heading2Handler).toBeDefined();
expect(heading2Handler?.type).toEqual('heading-2');
const heading3Handler = BlockRegistry.getHandler('heading-3');
expect(heading3Handler).toBeDefined();
expect(heading3Handler?.type).toEqual('heading-3');
// Test that getAllTypes returns all registered types
const allTypes = BlockRegistry.getAllTypes();
expect(allTypes).toContain('divider');
expect(allTypes).toContain('paragraph');
expect(allTypes).toContain('heading-1');
expect(allTypes).toContain('heading-2');
expect(allTypes).toContain('heading-3');
});
tap.test('should render divider block using handler', async () => {
// Wait for custom element to be defined
await customElements.whenDefined('dees-wysiwyg-block');
// Create element and set properties BEFORE attaching to DOM
const dividerBlock = document.createElement('dees-wysiwyg-block') as DeesWysiwygBlock;
// Set required handlers
dividerBlock.handlers = {
onInput: () => {},
onKeyDown: () => {},
onFocus: () => {},
onBlur: () => {},
onCompositionStart: () => {},
onCompositionEnd: () => {}
};
// Set a divider block
dividerBlock.block = {
id: 'test-divider',
type: 'divider',
content: ' '
};
// Attach to DOM and wait for render
document.body.appendChild(dividerBlock);
await dividerBlock.updateComplete;
// Wait for firstUpdated to populate the container
await new Promise(resolve => setTimeout(resolve, 50));
// Check that the divider is rendered
const dividerElement = dividerBlock.shadowRoot?.querySelector('.block.divider');
expect(dividerElement).toBeTruthy();
expect(dividerElement?.getAttribute('tabindex')).toEqual('0');
// Check for the hr element (divider uses <hr> not .divider-icon)
const hr = dividerBlock.shadowRoot?.querySelector('hr');
expect(hr).toBeTruthy();
// Clean up
document.body.removeChild(dividerBlock);
});
tap.test('should render paragraph block using handler', async () => {
// Wait for custom element to be defined
await customElements.whenDefined('dees-wysiwyg-block');
// Create element and set properties BEFORE attaching to DOM
const paragraphBlock = document.createElement('dees-wysiwyg-block') as DeesWysiwygBlock;
// Set required handlers
paragraphBlock.handlers = {
onInput: () => {},
onKeyDown: () => {},
onFocus: () => {},
onBlur: () => {},
onCompositionStart: () => {},
onCompositionEnd: () => {},
onMouseUp: () => {}
};
// Set a paragraph block
paragraphBlock.block = {
id: 'test-paragraph',
type: 'paragraph',
content: 'Test paragraph content'
};
// Attach to DOM and wait for render
document.body.appendChild(paragraphBlock);
await paragraphBlock.updateComplete;
// Wait for firstUpdated to populate the container
await new Promise(resolve => setTimeout(resolve, 50));
// Check that the paragraph is rendered
const paragraphElement = paragraphBlock.shadowRoot?.querySelector('.block.paragraph');
expect(paragraphElement).toBeTruthy();
expect(paragraphElement?.getAttribute('contenteditable')).toEqual('true');
expect(paragraphElement?.textContent).toEqual('Test paragraph content');
// Clean up
document.body.removeChild(paragraphBlock);
});
tap.test('should render heading blocks using handler', async () => {
// Wait for custom element to be defined
await customElements.whenDefined('dees-wysiwyg-block');
// Test heading-1 - set properties BEFORE attaching to DOM
const heading1Block = document.createElement('dees-wysiwyg-block') as DeesWysiwygBlock;
heading1Block.handlers = {
onInput: () => {},
onKeyDown: () => {},
onFocus: () => {},
onBlur: () => {},
onCompositionStart: () => {},
onCompositionEnd: () => {},
onMouseUp: () => {}
};
heading1Block.block = {
id: 'test-h1',
type: 'heading-1',
content: 'Heading 1 Test'
};
document.body.appendChild(heading1Block);
await heading1Block.updateComplete;
// Wait for firstUpdated to populate the container
await new Promise(resolve => setTimeout(resolve, 50));
const h1Element = heading1Block.shadowRoot?.querySelector('.block.heading-1');
expect(h1Element).toBeTruthy();
expect(h1Element?.textContent).toEqual('Heading 1 Test');
// Clean up heading-1
document.body.removeChild(heading1Block);
// Test heading-2 - set properties BEFORE attaching to DOM
const heading2Block = document.createElement('dees-wysiwyg-block') as DeesWysiwygBlock;
heading2Block.handlers = {
onInput: () => {},
onKeyDown: () => {},
onFocus: () => {},
onBlur: () => {},
onCompositionStart: () => {},
onCompositionEnd: () => {},
onMouseUp: () => {}
};
heading2Block.block = {
id: 'test-h2',
type: 'heading-2',
content: 'Heading 2 Test'
};
document.body.appendChild(heading2Block);
await heading2Block.updateComplete;
// Wait for firstUpdated to populate the container
await new Promise(resolve => setTimeout(resolve, 50));
const h2Element = heading2Block.shadowRoot?.querySelector('.block.heading-2');
expect(h2Element).toBeTruthy();
expect(h2Element?.textContent).toEqual('Heading 2 Test');
// Clean up heading-2
document.body.removeChild(heading2Block);
});
tap.test('paragraph block handler methods should work', async () => {
// Wait for custom element to be defined
await customElements.whenDefined('dees-wysiwyg-block');
// Create element and set properties BEFORE attaching to DOM
const paragraphBlock = document.createElement('dees-wysiwyg-block') as DeesWysiwygBlock;
// Set required handlers
paragraphBlock.handlers = {
onInput: () => {},
onKeyDown: () => {},
onFocus: () => {},
onBlur: () => {},
onCompositionStart: () => {},
onCompositionEnd: () => {},
onMouseUp: () => {}
};
paragraphBlock.block = {
id: 'test-methods',
type: 'paragraph',
content: 'Initial content'
};
document.body.appendChild(paragraphBlock);
await paragraphBlock.updateComplete;
// Wait for firstUpdated to populate the container
await new Promise(resolve => setTimeout(resolve, 50));
// Test getContent
const content = paragraphBlock.getContent();
expect(content).toEqual('Initial content');
// Test setContent
paragraphBlock.setContent('Updated content');
await paragraphBlock.updateComplete;
expect(paragraphBlock.getContent()).toEqual('Updated content');
// Test that the DOM is updated
const paragraphElement = paragraphBlock.shadowRoot?.querySelector('.block.paragraph');
expect(paragraphElement?.textContent).toEqual('Updated content');
// Clean up
document.body.removeChild(paragraphBlock);
});
export default tap.start();

View File

@@ -1,98 +0,0 @@
import { expect, tap } from '@git.zone/tstest/tapbundle';
import { DeesInputWysiwyg } from '../ts_web/elements/00group-input/dees-input-wysiwyg/dees-input-wysiwyg.js';
import { DeesContextmenu } from '../ts_web/elements/dees-contextmenu/dees-contextmenu.js';
tap.test('should change block type via context menu', async () => {
// Create WYSIWYG editor with a paragraph
const wysiwygEditor = new DeesInputWysiwyg();
wysiwygEditor.value = '<p>This is a test paragraph</p>';
document.body.appendChild(wysiwygEditor);
// Wait for editor to be ready
await wysiwygEditor.updateComplete;
await new Promise(resolve => setTimeout(resolve, 100));
// Get the first block
const firstBlock = wysiwygEditor.blocks[0];
expect(firstBlock.type).toEqual('paragraph');
// Get the block element
const firstBlockWrapper = wysiwygEditor.shadowRoot!.querySelector('.block-wrapper');
expect(firstBlockWrapper).toBeTruthy();
const blockComponent = firstBlockWrapper!.querySelector('dees-wysiwyg-block') as any;
expect(blockComponent).toBeTruthy();
await blockComponent.updateComplete;
// Get the editable content inside the block's shadow DOM
const editableBlock = blockComponent.shadowRoot!.querySelector('.block');
expect(editableBlock).toBeTruthy();
// Simulate right-click on the editable block
const contextMenuEvent = new MouseEvent('contextmenu', {
clientX: 200,
clientY: 200,
bubbles: true,
cancelable: true,
composed: true
});
editableBlock!.dispatchEvent(contextMenuEvent);
// Wait for context menu to appear
await new Promise(resolve => setTimeout(resolve, 100));
// Check if context menu is created
const contextMenu = document.querySelector('dees-contextmenu');
expect(contextMenu).toBeInstanceOf(DeesContextmenu);
// Find "Change Type" menu item
const menuItems = Array.from(contextMenu!.shadowRoot!.querySelectorAll('.menuitem'));
const changeTypeItem = menuItems.find(item =>
item.querySelector('.menuitem-text')?.textContent?.trim() === 'Change Type'
);
expect(changeTypeItem).toBeTruthy();
// Hover over "Change Type" to trigger submenu
changeTypeItem!.dispatchEvent(new MouseEvent('mouseenter', { bubbles: true }));
// Wait for submenu to appear
await new Promise(resolve => setTimeout(resolve, 300));
// Check if submenu is created
const allMenus = document.querySelectorAll('dees-contextmenu');
expect(allMenus.length).toEqual(2);
const submenu = allMenus[1];
const submenuItems = Array.from(submenu.shadowRoot!.querySelectorAll('.menuitem'));
// Find "Heading 1" option
const heading1Item = submenuItems.find(item =>
item.querySelector('.menuitem-text')?.textContent?.trim() === 'Heading 1'
);
expect(heading1Item).toBeTruthy();
// Click on "Heading 1"
(heading1Item as HTMLElement).click();
// Wait for menu to close and block to update
await new Promise(resolve => setTimeout(resolve, 300));
// Verify block type has changed
expect(wysiwygEditor.blocks[0].type).toEqual('heading-1');
// Verify DOM has been updated
const updatedBlockComponent = wysiwygEditor.shadowRoot!
.querySelector('.block-wrapper')!
.querySelector('dees-wysiwyg-block') as any;
await updatedBlockComponent.updateComplete;
const updatedBlock = updatedBlockComponent.shadowRoot!.querySelector('.block');
expect(updatedBlock?.classList.contains('heading-1')).toEqual(true);
// Clean up
wysiwygEditor.remove();
});
export default tap.start();

View File

@@ -1,68 +0,0 @@
import { expect, tap } from '@git.zone/tstest/tapbundle';
import { DeesInputWysiwyg } from '../ts_web/elements/00group-input/dees-input-wysiwyg/dees-input-wysiwyg.js';
import { DeesContextmenu } from '../ts_web/elements/dees-contextmenu/dees-contextmenu.js';
tap.test('should show context menu on WYSIWYG blocks', async () => {
// Create WYSIWYG editor
const wysiwygEditor = new DeesInputWysiwyg();
wysiwygEditor.value = '<p>Test paragraph</p><h1>Test heading</h1>';
document.body.appendChild(wysiwygEditor);
// Wait for editor to be ready
await wysiwygEditor.updateComplete;
await new Promise(resolve => setTimeout(resolve, 100));
// Get the first block element
const firstBlockWrapper = wysiwygEditor.shadowRoot!.querySelector('.block-wrapper');
expect(firstBlockWrapper).toBeTruthy();
const blockComponent = firstBlockWrapper!.querySelector('dees-wysiwyg-block') as any;
expect(blockComponent).toBeTruthy();
// Wait for block to be ready
await blockComponent.updateComplete;
// Get the editable content inside the block's shadow DOM
const editableBlock = blockComponent.shadowRoot!.querySelector('.block');
expect(editableBlock).toBeTruthy();
// Simulate right-click on the editable block
const contextMenuEvent = new MouseEvent('contextmenu', {
clientX: 200,
clientY: 200,
bubbles: true,
cancelable: true,
composed: true // Important for shadow DOM
});
editableBlock!.dispatchEvent(contextMenuEvent);
// Wait for context menu to appear
await new Promise(resolve => setTimeout(resolve, 100));
// Check if context menu is created
const contextMenu = document.querySelector('dees-contextmenu');
expect(contextMenu).toBeInstanceOf(DeesContextmenu);
// Check if menu items from WYSIWYG block are rendered
const menuItems = contextMenu!.shadowRoot!.querySelectorAll('.menuitem');
const menuTexts = Array.from(menuItems).map(item =>
item.querySelector('.menuitem-text')?.textContent?.trim()
);
// Should have "Change Type" and "Delete Block" items
expect(menuTexts).toContain('Change Type');
expect(menuTexts).toContain('Delete Block');
// Check if "Change Type" has submenu indicator
const changeTypeItem = Array.from(menuItems).find(item =>
item.querySelector('.menuitem-text')?.textContent?.trim() === 'Change Type'
);
expect(changeTypeItem?.classList.contains('has-submenu')).toEqual(true);
// Clean up
contextMenu!.remove();
wysiwygEditor.remove();
});
export default tap.start();

View File

@@ -1,95 +0,0 @@
import { expect, tap } from '@git.zone/tstest/tapbundle';
import { DeesInputWysiwyg } from '../ts_web/elements/00group-input/dees-input-wysiwyg/dees-input-wysiwyg.js';
// Initialize the element
DeesInputWysiwyg;
tap.test('wysiwyg drag handler initialization', async () => {
const element = document.createElement('dees-input-wysiwyg');
document.body.appendChild(element);
// Wait for element to be ready
await element.updateComplete;
// Check that drag handler is initialized
expect(element.dragDropHandler).toBeTruthy();
// Set initial content with multiple blocks
element.blocks = [
{ id: 'block1', type: 'paragraph', content: 'First paragraph' },
{ id: 'block2', type: 'paragraph', content: 'Second paragraph' },
];
element.renderBlocksProgrammatically();
await element.updateComplete;
// Check that editor content ref exists
console.log('editorContentRef:', element.editorContentRef);
expect(element.editorContentRef).toBeTruthy();
// Check that blocks are rendered
const blockWrappers = element.shadowRoot!.querySelectorAll('.block-wrapper');
console.log('Number of block wrappers:', blockWrappers.length);
expect(blockWrappers.length).toEqual(2);
// Check drag handles
const dragHandles = element.shadowRoot!.querySelectorAll('.drag-handle');
console.log('Number of drag handles:', dragHandles.length);
expect(dragHandles.length).toEqual(2);
// Clean up
document.body.removeChild(element);
});
tap.test('wysiwyg drag start behavior', async () => {
const element = document.createElement('dees-input-wysiwyg');
document.body.appendChild(element);
await element.updateComplete;
// Set initial content
element.blocks = [
{ id: 'block1', type: 'paragraph', content: 'Test block' },
];
element.renderBlocksProgrammatically();
await element.updateComplete;
const dragHandle = element.shadowRoot!.querySelector('.drag-handle') as HTMLElement;
expect(dragHandle).toBeTruthy();
// Check that drag handle has draggable attribute
console.log('Drag handle draggable:', dragHandle.draggable);
expect(dragHandle.draggable).toBeTrue();
// Test drag handler state before drag
console.log('Initial drag state:', element.dragDropHandler.dragState);
expect(element.dragDropHandler.dragState.draggedBlockId).toBeNull();
// Try to manually call handleDragStart
const mockDragEvent = {
dataTransfer: {
effectAllowed: '',
setData: (type: string, data: string) => {
console.log('setData called with:', type, data);
},
setDragImage: (img: any, x: number, y: number) => {
console.log('setDragImage called');
}
},
clientY: 100,
preventDefault: () => {},
} as any;
element.dragDropHandler.handleDragStart(mockDragEvent, element.blocks[0]);
// Check drag state after drag start
console.log('Drag state after start:', element.dragDropHandler.dragState);
expect(element.dragDropHandler.dragState.draggedBlockId).toEqual('block1');
// Clean up
element.dragDropHandler.handleDragEnd();
document.body.removeChild(element);
});
export default tap.start();

View File

@@ -1,133 +0,0 @@
import { expect, tap } from '@git.zone/tstest/tapbundle';
import { DeesInputWysiwyg } from '../ts_web/elements/00group-input/dees-input-wysiwyg/dees-input-wysiwyg.js';
// Initialize the element
DeesInputWysiwyg;
tap.test('wysiwyg drag visual feedback - block movement', async () => {
const element = document.createElement('dees-input-wysiwyg');
document.body.appendChild(element);
await element.updateComplete;
// Set initial content
element.blocks = [
{ id: 'block1', type: 'paragraph', content: 'Block 1' },
{ id: 'block2', type: 'paragraph', content: 'Block 2' },
{ id: 'block3', type: 'paragraph', content: 'Block 3' },
];
element.renderBlocksProgrammatically();
await element.updateComplete;
const editorContent = element.shadowRoot!.querySelector('.editor-content') as HTMLDivElement;
const block1 = editorContent.querySelector('[data-block-id="block1"]') as HTMLElement;
// Manually start drag
const mockDragEvent = {
dataTransfer: {
effectAllowed: '',
setData: (type: string, data: string) => {},
setDragImage: (img: any, x: number, y: number) => {}
},
clientY: 50,
preventDefault: () => {},
} as any;
element.dragDropHandler.handleDragStart(mockDragEvent, element.blocks[0]);
// Wait for dragging class
await new Promise(resolve => setTimeout(resolve, 20));
// Check dragging state
console.log('Block 1 classes:', block1.className);
console.log('Editor content classes:', editorContent.className);
expect(block1.classList.contains('dragging')).toBeTrue();
expect(editorContent.classList.contains('dragging')).toBeTrue();
// Check drop indicator exists
const dropIndicator = editorContent.querySelector('.drop-indicator') as HTMLElement;
console.log('Drop indicator:', dropIndicator);
expect(dropIndicator).toBeTruthy();
// Test block movement calculation
console.log('Testing updateBlockPositions...');
// Access private method for testing
const updateBlockPositions = element.dragDropHandler['updateBlockPositions'].bind(element.dragDropHandler);
// Simulate dragging to different position
updateBlockPositions(150); // Move down
// Check if blocks have move classes
const blocks = Array.from(editorContent.querySelectorAll('.block-wrapper'));
console.log('Block classes after move:');
blocks.forEach((block, i) => {
console.log(`Block ${i}:`, block.className, 'transform:', (block as HTMLElement).style.getPropertyValue('--drag-offset'));
});
// Clean up
element.dragDropHandler.handleDragEnd();
document.body.removeChild(element);
});
tap.test('wysiwyg drop indicator positioning', async () => {
const element = document.createElement('dees-input-wysiwyg');
document.body.appendChild(element);
await element.updateComplete;
// Set initial content
element.blocks = [
{ id: 'block1', type: 'paragraph', content: 'Paragraph 1' },
{ id: 'block2', type: 'heading-2', content: 'Heading 2' },
];
element.renderBlocksProgrammatically();
await element.updateComplete;
const editorContent = element.shadowRoot!.querySelector('.editor-content') as HTMLDivElement;
// Start dragging first block
const mockDragEvent = {
dataTransfer: {
effectAllowed: '',
setData: (type: string, data: string) => {},
setDragImage: (img: any, x: number, y: number) => {}
},
clientY: 50,
preventDefault: () => {},
} as any;
element.dragDropHandler.handleDragStart(mockDragEvent, element.blocks[0]);
// Wait for initialization
await new Promise(resolve => setTimeout(resolve, 20));
// Get drop indicator
const dropIndicator = editorContent.querySelector('.drop-indicator') as HTMLElement;
expect(dropIndicator).toBeTruthy();
// Check initial display state
console.log('Drop indicator initial display:', dropIndicator.style.display);
// Trigger updateBlockPositions to see drop indicator
const updateBlockPositions = element.dragDropHandler['updateBlockPositions'].bind(element.dragDropHandler);
updateBlockPositions(100);
// Check drop indicator position
console.log('Drop indicator after update:');
console.log('- display:', dropIndicator.style.display);
console.log('- top:', dropIndicator.style.top);
console.log('- height:', dropIndicator.style.height);
expect(dropIndicator.style.display).toEqual('block');
expect(dropIndicator.style.top).toBeTruthy();
expect(dropIndicator.style.height).toBeTruthy();
// Clean up
element.dragDropHandler.handleDragEnd();
document.body.removeChild(element);
});
export default tap.start();

View File

@@ -1,145 +0,0 @@
import { expect, tap } from '@git.zone/tstest/tapbundle';
import { DeesInputWysiwyg } from '../ts_web/elements/00group-input/dees-input-wysiwyg/dees-input-wysiwyg.js';
// Initialize the element
DeesInputWysiwyg;
tap.test('wysiwyg drag and drop should work correctly', async () => {
const element = document.createElement('dees-input-wysiwyg');
document.body.appendChild(element);
// Wait for element to be ready
await element.updateComplete;
// Set initial content with multiple blocks
element.blocks = [
{ id: 'block1', type: 'paragraph', content: 'First paragraph' },
{ id: 'block2', type: 'heading-2', content: 'Test Heading' },
{ id: 'block3', type: 'paragraph', content: 'Second paragraph' },
];
element.renderBlocksProgrammatically();
await element.updateComplete;
// Wait for nested block components to also complete their updates
await new Promise(resolve => setTimeout(resolve, 50));
// Check that blocks are rendered
const editorContent = element.shadowRoot!.querySelector('.editor-content') as HTMLDivElement;
expect(editorContent).toBeTruthy();
const blockWrappers = editorContent.querySelectorAll('.block-wrapper');
expect(blockWrappers.length).toEqual(3);
// Test drag handles exist for non-divider blocks
const dragHandles = editorContent.querySelectorAll('.drag-handle');
expect(dragHandles.length).toEqual(3);
// Get references to specific blocks
const firstBlock = editorContent.querySelector('[data-block-id="block1"]') as HTMLElement;
const secondBlock = editorContent.querySelector('[data-block-id="block2"]') as HTMLElement;
const firstDragHandle = firstBlock.querySelector('.drag-handle') as HTMLElement;
expect(firstBlock).toBeTruthy();
expect(secondBlock).toBeTruthy();
expect(firstDragHandle).toBeTruthy();
// Verify drag drop handler exists
expect(element.dragDropHandler).toBeTruthy();
expect(element.dragDropHandler.dragState).toBeTruthy();
// Test drag initialization - synthetic DragEvents may not fully work in all browsers
console.log('Testing drag initialization...');
// Create drag event
const dragStartEvent = new DragEvent('dragstart', {
dataTransfer: new DataTransfer(),
clientY: 100,
bubbles: true
});
// Simulate drag start
firstDragHandle.dispatchEvent(dragStartEvent);
// Wait for setTimeout in drag start
await new Promise(resolve => setTimeout(resolve, 50));
// Note: Synthetic DragEvents may not fully initialize drag state in all test environments
// The test verifies the structure and that events can be dispatched
console.log('Drag state after start:', element.dragDropHandler.dragState.draggedBlockId);
// Test drag end cleanup
const dragEndEvent = new DragEvent('dragend', {
bubbles: true
});
document.dispatchEvent(dragEndEvent);
// Wait for cleanup
await new Promise(resolve => setTimeout(resolve, 150));
// Clean up
document.body.removeChild(element);
});
tap.test('wysiwyg drag and drop visual feedback', async () => {
const element = document.createElement('dees-input-wysiwyg');
document.body.appendChild(element);
await element.updateComplete;
// Set initial content
element.blocks = [
{ id: 'block1', type: 'paragraph', content: 'Block 1' },
{ id: 'block2', type: 'paragraph', content: 'Block 2' },
{ id: 'block3', type: 'paragraph', content: 'Block 3' },
];
element.renderBlocksProgrammatically();
await element.updateComplete;
// Wait for nested block components to also complete their updates
await new Promise(resolve => setTimeout(resolve, 50));
const editorContent = element.shadowRoot!.querySelector('.editor-content') as HTMLDivElement;
const block1 = editorContent.querySelector('[data-block-id="block1"]') as HTMLElement;
const dragHandle1 = block1.querySelector('.drag-handle') as HTMLElement;
// Start dragging block 1
const dragStartEvent = new DragEvent('dragstart', {
dataTransfer: new DataTransfer(),
clientY: 50,
bubbles: true
});
dragHandle1.dispatchEvent(dragStartEvent);
// Wait for dragging class
await new Promise(resolve => setTimeout(resolve, 20));
// Simulate dragging down
const dragOverEvent = new DragEvent('dragover', {
dataTransfer: new DataTransfer(),
clientY: 150, // Move down past block 2
bubbles: true,
cancelable: true
});
// Trigger the global drag over handler
element.dragDropHandler['handleGlobalDragOver'](dragOverEvent);
// Check that transform is applied to dragged block
const transform = block1.style.transform;
console.log('Dragged block transform:', transform);
expect(transform).toContain('translateY');
// Check drop indicator position
const dropIndicator = editorContent.querySelector('.drop-indicator') as HTMLElement;
if (dropIndicator) {
const indicatorStyle = dropIndicator.style;
console.log('Drop indicator position:', indicatorStyle.top, 'display:', indicatorStyle.display);
}
// Clean up
document.body.removeChild(element);
});
export default tap.start();

View File

@@ -1,124 +0,0 @@
import { expect, tap } from '@git.zone/tstest/tapbundle';
import { DeesInputWysiwyg } from '../ts_web/elements/00group-input/dees-input-wysiwyg/dees-input-wysiwyg.js';
// Initialize the element
DeesInputWysiwyg;
tap.test('wysiwyg drag full flow without await', async () => {
const element = document.createElement('dees-input-wysiwyg');
document.body.appendChild(element);
await element.updateComplete;
// Set initial content
element.blocks = [
{ id: 'block1', type: 'paragraph', content: 'Test block' },
];
element.renderBlocksProgrammatically();
await element.updateComplete;
// Mock drag event
const mockDragEvent = {
dataTransfer: {
effectAllowed: '',
setData: (type: string, data: string) => {
console.log('setData:', type, data);
},
setDragImage: (img: any, x: number, y: number) => {
console.log('setDragImage');
}
},
clientY: 100,
preventDefault: () => {},
} as any;
console.log('Starting drag...');
element.dragDropHandler.handleDragStart(mockDragEvent, element.blocks[0]);
console.log('Drag started');
// Check immediate state
expect(element.dragDropHandler.dragState.draggedBlockId).toEqual('block1');
// Instead of await with setTimeout, use a done callback
return new Promise<void>((resolve) => {
console.log('Setting up delayed check...');
// Use regular setTimeout
setTimeout(() => {
console.log('In setTimeout callback');
try {
const block1 = element.shadowRoot!.querySelector('[data-block-id="block1"]') as HTMLElement;
const editorContent = element.shadowRoot!.querySelector('.editor-content') as HTMLDivElement;
console.log('Block has dragging class:', block1?.classList.contains('dragging'));
console.log('Editor has dragging class:', editorContent?.classList.contains('dragging'));
// Clean up
element.dragDropHandler.handleDragEnd();
document.body.removeChild(element);
resolve();
} catch (error) {
console.error('Error in setTimeout:', error);
throw error;
}
}, 50);
});
});
tap.test('identify the crash point', async () => {
console.log('Test started');
const element = document.createElement('dees-input-wysiwyg');
document.body.appendChild(element);
console.log('Element created');
await element.updateComplete;
console.log('Setting blocks');
element.blocks = [{ id: 'block1', type: 'paragraph', content: 'Test' }];
element.renderBlocksProgrammatically();
console.log('Waiting for update');
await element.updateComplete;
console.log('Creating mock event');
const mockDragEvent = {
dataTransfer: {
effectAllowed: '',
setData: () => {},
setDragImage: () => {}
},
clientY: 100,
preventDefault: () => {},
} as any;
console.log('Calling handleDragStart');
element.dragDropHandler.handleDragStart(mockDragEvent, element.blocks[0]);
console.log('handleDragStart completed');
// Try different wait methods
console.log('About to wait...');
// Method 1: Direct promise
await Promise.resolve();
console.log('Promise.resolve completed');
// Method 2: setTimeout 0
await new Promise(resolve => setTimeout(resolve, 0));
console.log('setTimeout 0 completed');
// Method 3: requestAnimationFrame
await new Promise(resolve => requestAnimationFrame(() => resolve(undefined)));
console.log('requestAnimationFrame completed');
// Clean up
element.dragDropHandler.handleDragEnd();
document.body.removeChild(element);
console.log('Cleanup completed');
});
export default tap.start();

View File

@@ -1,108 +0,0 @@
import { expect, tap } from '@git.zone/tstest/tapbundle';
import { DeesInputWysiwyg } from '../ts_web/elements/00group-input/dees-input-wysiwyg/dees-input-wysiwyg.js';
// Initialize the element
DeesInputWysiwyg;
tap.test('wysiwyg drop indicator creation', async () => {
const element = document.createElement('dees-input-wysiwyg');
document.body.appendChild(element);
await element.updateComplete;
// Set initial content
element.blocks = [
{ id: 'block1', type: 'paragraph', content: 'Test block' },
];
element.renderBlocksProgrammatically();
await element.updateComplete;
// Check editorContentRef
console.log('editorContentRef exists:', !!element.editorContentRef);
console.log('editorContentRef tagName:', element.editorContentRef?.tagName);
expect(element.editorContentRef).toBeTruthy();
// Check initial state - no drop indicator
let dropIndicator = element.shadowRoot!.querySelector('.drop-indicator');
console.log('Drop indicator before drag:', dropIndicator);
expect(dropIndicator).toBeFalsy();
// Manually call createDropIndicator
try {
console.log('Calling createDropIndicator...');
element.dragDropHandler['createDropIndicator']();
console.log('createDropIndicator succeeded');
} catch (error) {
console.error('Error creating drop indicator:', error);
throw error;
}
// Check drop indicator was created
dropIndicator = element.shadowRoot!.querySelector('.drop-indicator');
console.log('Drop indicator after creation:', dropIndicator);
console.log('Drop indicator parent:', dropIndicator?.parentElement?.className);
expect(dropIndicator).toBeTruthy();
expect(dropIndicator!.style.display).toEqual('none');
// Clean up
document.body.removeChild(element);
});
tap.test('wysiwyg drag initialization with drop indicator', async () => {
const element = document.createElement('dees-input-wysiwyg');
document.body.appendChild(element);
await element.updateComplete;
// Set initial content
element.blocks = [
{ id: 'block1', type: 'paragraph', content: 'Test block' },
];
element.renderBlocksProgrammatically();
await element.updateComplete;
// Mock drag event
const mockDragEvent = {
dataTransfer: {
effectAllowed: '',
setData: (type: string, data: string) => {
console.log('setData:', type, data);
},
setDragImage: (img: any, x: number, y: number) => {
console.log('setDragImage');
}
},
clientY: 100,
preventDefault: () => {},
} as any;
console.log('Starting drag...');
try {
element.dragDropHandler.handleDragStart(mockDragEvent, element.blocks[0]);
console.log('Drag start succeeded');
} catch (error) {
console.error('Error during drag start:', error);
throw error;
}
// Wait for async operations
await new Promise(resolve => setTimeout(resolve, 20));
// Check drop indicator exists
const dropIndicator = element.shadowRoot!.querySelector('.drop-indicator');
console.log('Drop indicator after drag start:', dropIndicator);
expect(dropIndicator).toBeTruthy();
// Check drag state
console.log('Drag state:', element.dragDropHandler.dragState);
expect(element.dragDropHandler.dragState.draggedBlockId).toEqual('block1');
// Clean up
element.dragDropHandler.handleDragEnd();
document.body.removeChild(element);
});
export default tap.start();

View File

@@ -1,114 +0,0 @@
import { expect, tap } from '@git.zone/tstest/tapbundle';
import { DeesInputWysiwyg } from '../ts_web/elements/00group-input/dees-input-wysiwyg/dees-input-wysiwyg.js';
// Initialize the element
DeesInputWysiwyg;
tap.test('wysiwyg global event listeners', async () => {
const element = document.createElement('dees-input-wysiwyg');
document.body.appendChild(element);
await element.updateComplete;
// Set initial content
element.blocks = [
{ id: 'block1', type: 'paragraph', content: 'Test block' },
];
element.renderBlocksProgrammatically();
await element.updateComplete;
const block1 = element.shadowRoot!.querySelector('[data-block-id="block1"]') as HTMLElement;
console.log('Block 1 found:', !!block1);
// Set up drag state manually without using handleDragStart
element.dragDropHandler['draggedBlockId'] = 'block1';
element.dragDropHandler['draggedBlockElement'] = block1;
element.dragDropHandler['initialMouseY'] = 100;
// Create drop indicator manually
element.dragDropHandler['createDropIndicator']();
// Test adding global event listeners
console.log('Adding event listeners...');
const handleGlobalDragOver = element.dragDropHandler['handleGlobalDragOver'];
const handleGlobalDragEnd = element.dragDropHandler['handleGlobalDragEnd'];
try {
document.addEventListener('dragover', handleGlobalDragOver);
console.log('dragover listener added');
document.addEventListener('dragend', handleGlobalDragEnd);
console.log('dragend listener added');
} catch (error) {
console.error('Error adding event listeners:', error);
throw error;
}
// Test firing a dragover event
console.log('Creating dragover event...');
const dragOverEvent = new Event('dragover', {
bubbles: true,
cancelable: true
});
Object.defineProperty(dragOverEvent, 'clientY', { value: 150 });
console.log('Dispatching dragover event...');
document.dispatchEvent(dragOverEvent);
console.log('dragover event dispatched');
// Clean up
document.removeEventListener('dragover', handleGlobalDragOver);
document.removeEventListener('dragend', handleGlobalDragEnd);
document.body.removeChild(element);
});
tap.test('wysiwyg setTimeout in drag start', async () => {
const element = document.createElement('dees-input-wysiwyg');
document.body.appendChild(element);
await element.updateComplete;
// Set initial content
element.blocks = [
{ id: 'block1', type: 'paragraph', content: 'Test block' },
];
element.renderBlocksProgrammatically();
await element.updateComplete;
const block1 = element.shadowRoot!.querySelector('[data-block-id="block1"]') as HTMLElement;
const editorContent = element.shadowRoot!.querySelector('.editor-content') as HTMLDivElement;
// Set drag state
element.dragDropHandler['draggedBlockId'] = 'block1';
element.dragDropHandler['draggedBlockElement'] = block1;
console.log('Testing setTimeout callback...');
// Test the setTimeout callback directly
try {
if (block1) {
console.log('Adding dragging class to block...');
block1.classList.add('dragging');
console.log('Block classes:', block1.className);
}
if (editorContent) {
console.log('Adding dragging class to editor...');
editorContent.classList.add('dragging');
console.log('Editor classes:', editorContent.className);
}
} catch (error) {
console.error('Error in setTimeout callback:', error);
throw error;
}
expect(block1.classList.contains('dragging')).toBeTrue();
expect(editorContent.classList.contains('dragging')).toBeTrue();
// Clean up
document.body.removeChild(element);
});
export default tap.start();

View File

@@ -1,329 +0,0 @@
import { expect, tap, webhelpers } from '@git.zone/tstest/tapbundle';
import { DeesInputWysiwyg } from '../ts_web/elements/00group-input/dees-input-wysiwyg/dees-input-wysiwyg.js';
import { DeesWysiwygBlock } from '../ts_web/elements/00group-input/dees-input-wysiwyg/dees-wysiwyg-block.js';
tap.test('Keyboard: Arrow navigation between blocks', async () => {
const editor: DeesInputWysiwyg = await webhelpers.fixture(
webhelpers.html`<dees-input-wysiwyg></dees-input-wysiwyg>`
);
// Import multiple blocks
editor.importBlocks([
{ id: 'block-1', type: 'paragraph', content: 'First paragraph' },
{ id: 'block-2', type: 'paragraph', content: 'Second paragraph' },
{ id: 'block-3', type: 'paragraph', content: 'Third paragraph' }
]);
await editor.updateComplete;
await new Promise(resolve => setTimeout(resolve, 100));
// Focus first block at end
const firstBlockWrapper = editor.shadowRoot?.querySelector('[data-block-id="block-1"]');
const firstBlockComponent = firstBlockWrapper?.querySelector('dees-wysiwyg-block') as DeesWysiwygBlock;
const firstBlockContainer = firstBlockComponent?.shadowRoot?.querySelector('.wysiwyg-block-container') as HTMLElement;
const firstParagraph = firstBlockContainer?.querySelector('.block.paragraph') as HTMLElement;
// Focus and set cursor at end of first block
firstParagraph.focus();
const textNode = firstParagraph.firstChild;
if (textNode && textNode.nodeType === Node.TEXT_NODE) {
const range = document.createRange();
const selection = window.getSelection();
range.setStart(textNode, textNode.textContent?.length || 0);
range.setEnd(textNode, textNode.textContent?.length || 0);
selection?.removeAllRanges();
selection?.addRange(range);
}
await new Promise(resolve => setTimeout(resolve, 100));
// Press ArrowRight to move to second block
const arrowRightEvent = new KeyboardEvent('keydown', {
key: 'ArrowRight',
code: 'ArrowRight',
bubbles: true,
cancelable: true,
composed: true
});
firstParagraph.dispatchEvent(arrowRightEvent);
await new Promise(resolve => setTimeout(resolve, 200));
// Check if second block is focused
const secondBlockWrapper = editor.shadowRoot?.querySelector('[data-block-id="block-2"]');
const secondBlockComponent = secondBlockWrapper?.querySelector('dees-wysiwyg-block') as DeesWysiwygBlock;
const secondBlockContainer = secondBlockComponent?.shadowRoot?.querySelector('.wysiwyg-block-container') as HTMLElement;
const secondParagraph = secondBlockContainer?.querySelector('.block.paragraph') as HTMLElement;
// Check if the second paragraph has focus
const activeElement = secondBlockComponent.shadowRoot?.activeElement;
expect(activeElement).toEqual(secondParagraph);
console.log('Arrow navigation test complete');
});
tap.test('Keyboard: Backspace merges blocks', async () => {
const editor: DeesInputWysiwyg = await webhelpers.fixture(
webhelpers.html`<dees-input-wysiwyg></dees-input-wysiwyg>`
);
// Import two blocks
editor.importBlocks([
{ id: 'merge-1', type: 'paragraph', content: 'First' },
{ id: 'merge-2', type: 'paragraph', content: 'Second' }
]);
await editor.updateComplete;
await new Promise(resolve => setTimeout(resolve, 100));
// Focus second block at beginning
const secondBlockWrapper = editor.shadowRoot?.querySelector('[data-block-id="merge-2"]');
const secondBlockComponent = secondBlockWrapper?.querySelector('dees-wysiwyg-block') as DeesWysiwygBlock;
const secondBlockContainer = secondBlockComponent?.shadowRoot?.querySelector('.wysiwyg-block-container') as HTMLElement;
const secondParagraph = secondBlockContainer?.querySelector('.block.paragraph') as HTMLElement;
// Focus and set cursor at beginning
secondParagraph.focus();
const textNode = secondParagraph.firstChild;
if (textNode && textNode.nodeType === Node.TEXT_NODE) {
const range = document.createRange();
const selection = window.getSelection();
range.setStart(textNode, 0);
range.setEnd(textNode, 0);
selection?.removeAllRanges();
selection?.addRange(range);
}
await new Promise(resolve => setTimeout(resolve, 100));
// Press Backspace to merge with previous block
const backspaceEvent = new KeyboardEvent('keydown', {
key: 'Backspace',
code: 'Backspace',
bubbles: true,
cancelable: true,
composed: true
});
secondParagraph.dispatchEvent(backspaceEvent);
await new Promise(resolve => setTimeout(resolve, 200));
// Check if blocks were merged
expect(editor.blocks.length).toEqual(1);
expect(editor.blocks[0].content).toContain('First');
expect(editor.blocks[0].content).toContain('Second');
console.log('Backspace merge test complete');
});
tap.test('Keyboard: Delete key on non-editable blocks', async () => {
const editor: DeesInputWysiwyg = await webhelpers.fixture(
webhelpers.html`<dees-input-wysiwyg></dees-input-wysiwyg>`
);
// Import blocks including a divider
editor.importBlocks([
{ id: 'para-1', type: 'paragraph', content: 'Before divider' },
{ id: 'div-1', type: 'divider', content: '' },
{ id: 'para-2', type: 'paragraph', content: 'After divider' }
]);
await editor.updateComplete;
await new Promise(resolve => setTimeout(resolve, 100));
// Focus the divider block
const dividerBlockWrapper = editor.shadowRoot?.querySelector('[data-block-id="div-1"]');
const dividerBlockComponent = dividerBlockWrapper?.querySelector('dees-wysiwyg-block') as DeesWysiwygBlock;
const dividerBlockContainer = dividerBlockComponent?.shadowRoot?.querySelector('.wysiwyg-block-container') as HTMLElement;
const dividerElement = dividerBlockContainer?.querySelector('.block.divider') as HTMLElement;
// Non-editable blocks need to be focused differently
dividerElement?.focus();
await new Promise(resolve => setTimeout(resolve, 100));
// Press Delete to remove the divider
const deleteEvent = new KeyboardEvent('keydown', {
key: 'Delete',
code: 'Delete',
bubbles: true,
cancelable: true,
composed: true
});
dividerElement.dispatchEvent(deleteEvent);
await new Promise(resolve => setTimeout(resolve, 200));
// Check if divider was removed
expect(editor.blocks.length).toEqual(2);
expect(editor.blocks.find(b => b.type === 'divider')).toBeUndefined();
console.log('Delete key on non-editable block test complete');
});
tap.test('Keyboard: Tab key in code block', async () => {
const editor: DeesInputWysiwyg = await webhelpers.fixture(
webhelpers.html`<dees-input-wysiwyg></dees-input-wysiwyg>`
);
// Import a code block
editor.importBlocks([
{ id: 'code-1', type: 'code', content: 'function test() {', metadata: { language: 'javascript' } }
]);
await editor.updateComplete;
await new Promise(resolve => setTimeout(resolve, 100));
// Focus code block - code blocks use .code-editor instead of .block.code
const codeBlockWrapper = editor.shadowRoot?.querySelector('[data-block-id="code-1"]');
const codeBlockComponent = codeBlockWrapper?.querySelector('dees-wysiwyg-block') as DeesWysiwygBlock;
const codeBlockContainer = codeBlockComponent?.shadowRoot?.querySelector('.wysiwyg-block-container') as HTMLElement;
const codeElement = codeBlockContainer?.querySelector('.code-editor') as HTMLElement;
expect(codeElement).toBeTruthy();
// Focus and set cursor at end
codeElement.focus();
const textNode = codeElement.firstChild;
if (textNode && textNode.nodeType === Node.TEXT_NODE) {
const range = document.createRange();
const selection = window.getSelection();
range.setStart(textNode, textNode.textContent?.length || 0);
range.setEnd(textNode, textNode.textContent?.length || 0);
selection?.removeAllRanges();
selection?.addRange(range);
}
await new Promise(resolve => setTimeout(resolve, 100));
// Press Tab to insert spaces
const tabEvent = new KeyboardEvent('keydown', {
key: 'Tab',
code: 'Tab',
bubbles: true,
cancelable: true,
composed: true
});
codeElement.dispatchEvent(tabEvent);
await new Promise(resolve => setTimeout(resolve, 200));
// Check if spaces were inserted
const updatedContent = codeElement.textContent || '';
expect(updatedContent).toContain(' '); // Tab should insert 2 spaces
console.log('Tab in code block test complete');
});
tap.test('Keyboard: ArrowUp/Down navigation', async () => {
const editor: DeesInputWysiwyg = await webhelpers.fixture(
webhelpers.html`<dees-input-wysiwyg></dees-input-wysiwyg>`
);
// Import multiple blocks
editor.importBlocks([
{ id: 'nav-1', type: 'paragraph', content: 'First line' },
{ id: 'nav-2', type: 'paragraph', content: 'Second line' },
{ id: 'nav-3', type: 'paragraph', content: 'Third line' }
]);
await editor.updateComplete;
await new Promise(resolve => setTimeout(resolve, 100));
// Verify blocks were created
expect(editor.blocks.length).toEqual(3);
// Focus second block
const secondBlockWrapper = editor.shadowRoot?.querySelector('[data-block-id="nav-2"]');
const secondBlockComponent = secondBlockWrapper?.querySelector('dees-wysiwyg-block') as DeesWysiwygBlock;
const secondBlockContainer = secondBlockComponent?.shadowRoot?.querySelector('.wysiwyg-block-container') as HTMLElement;
const secondParagraph = secondBlockContainer?.querySelector('.block.paragraph') as HTMLElement;
expect(secondParagraph).toBeTruthy();
secondParagraph.focus();
await new Promise(resolve => setTimeout(resolve, 100));
// Verify keyboard handler exists
expect(editor.keyboardHandler).toBeTruthy();
// Press ArrowUp - event is dispatched (focus change may not occur in synthetic events)
const arrowUpEvent = new KeyboardEvent('keydown', {
key: 'ArrowUp',
code: 'ArrowUp',
bubbles: true,
cancelable: true,
composed: true
});
secondParagraph.dispatchEvent(arrowUpEvent);
await new Promise(resolve => setTimeout(resolve, 200));
// Get first block references
const firstBlockWrapper = editor.shadowRoot?.querySelector('[data-block-id="nav-1"]');
const firstBlockComponent = firstBlockWrapper?.querySelector('dees-wysiwyg-block') as DeesWysiwygBlock;
const firstBlockContainer = firstBlockComponent?.shadowRoot?.querySelector('.wysiwyg-block-container') as HTMLElement;
const firstParagraph = firstBlockContainer?.querySelector('.block.paragraph') as HTMLElement;
expect(firstParagraph).toBeTruthy();
// Note: Synthetic keyboard events don't reliably trigger native browser focus changes
// in automated tests. The handler is invoked but focus may not actually move.
// This test verifies the structure exists and events can be dispatched.
console.log('ArrowUp/Down navigation test complete');
});
tap.test('Keyboard: Formatting shortcuts', async () => {
const editor: DeesInputWysiwyg = await webhelpers.fixture(
webhelpers.html`<dees-input-wysiwyg></dees-input-wysiwyg>`
);
// Import a paragraph
editor.importBlocks([
{ id: 'format-1', type: 'paragraph', content: 'Test formatting' }
]);
await editor.updateComplete;
await new Promise(resolve => setTimeout(resolve, 100));
// Focus and select text
const blockWrapper = editor.shadowRoot?.querySelector('[data-block-id="format-1"]');
const blockComponent = blockWrapper?.querySelector('dees-wysiwyg-block') as DeesWysiwygBlock;
const blockContainer = blockComponent?.shadowRoot?.querySelector('.wysiwyg-block-container') as HTMLElement;
const paragraph = blockContainer?.querySelector('.block.paragraph') as HTMLElement;
paragraph.focus();
// Select "formatting"
const textNode = paragraph.firstChild;
if (textNode && textNode.nodeType === Node.TEXT_NODE) {
const range = document.createRange();
const selection = window.getSelection();
range.setStart(textNode, 5); // After "Test "
range.setEnd(textNode, 15); // After "formatting"
selection?.removeAllRanges();
selection?.addRange(range);
}
await new Promise(resolve => setTimeout(resolve, 100));
// Press Cmd/Ctrl+B for bold
const boldEvent = new KeyboardEvent('keydown', {
key: 'b',
code: 'KeyB',
metaKey: true, // Use metaKey for Mac, ctrlKey for Windows/Linux
bubbles: true,
cancelable: true,
composed: true
});
paragraph.dispatchEvent(boldEvent);
await new Promise(resolve => setTimeout(resolve, 200));
// Check if bold was applied
const content = paragraph.innerHTML;
expect(content).toContain('<strong>') || expect(content).toContain('<b>');
console.log('Formatting shortcuts test complete');
});
export default tap.start();

View File

@@ -1,152 +0,0 @@
import { expect, tap, webhelpers } from '@git.zone/tstest/tapbundle';
import { DeesInputWysiwyg } from '../ts_web/elements/00group-input/dees-input-wysiwyg/dees-input-wysiwyg.js';
import { DeesWysiwygBlock } from '../ts_web/elements/00group-input/dees-input-wysiwyg/dees-wysiwyg-block.js';
tap.test('Phase 3: Quote block should render and work correctly', async () => {
const editor: DeesInputWysiwyg = await webhelpers.fixture(
webhelpers.html`<dees-input-wysiwyg></dees-input-wysiwyg>`
);
// Import a quote block
editor.importBlocks([
{ id: 'quote-1', type: 'quote', content: 'This is a famous quote' }
]);
await editor.updateComplete;
await new Promise(resolve => setTimeout(resolve, 100));
// Check if quote block was rendered
const quoteBlockWrapper = editor.shadowRoot?.querySelector('[data-block-id="quote-1"]');
const quoteBlockComponent = quoteBlockWrapper?.querySelector('dees-wysiwyg-block') as DeesWysiwygBlock;
expect(quoteBlockComponent).toBeTruthy();
const quoteContainer = quoteBlockComponent?.shadowRoot?.querySelector('.wysiwyg-block-container') as HTMLElement;
const quoteElement = quoteContainer?.querySelector('.block.quote') as HTMLElement;
expect(quoteElement).toBeTruthy();
expect(quoteElement?.textContent).toEqual('This is a famous quote');
// Check if styles are applied (border-left for quote)
const computedStyle = window.getComputedStyle(quoteElement);
expect(computedStyle.borderLeftStyle).toEqual('solid');
expect(computedStyle.fontStyle).toEqual('italic');
});
tap.test('Phase 3: Code block should render and handle tab correctly', async () => {
const editor: DeesInputWysiwyg = await webhelpers.fixture(
webhelpers.html`<dees-input-wysiwyg></dees-input-wysiwyg>`
);
// Import a code block
editor.importBlocks([
{ id: 'code-1', type: 'code', content: 'const x = 42;', metadata: { language: 'javascript' } }
]);
await editor.updateComplete;
await new Promise(resolve => setTimeout(resolve, 100));
// Check if code block was rendered - code blocks use .code-editor instead of .block.code
const codeBlockWrapper = editor.shadowRoot?.querySelector('[data-block-id="code-1"]');
const codeBlockComponent = codeBlockWrapper?.querySelector('dees-wysiwyg-block') as DeesWysiwygBlock;
const codeContainer = codeBlockComponent?.shadowRoot?.querySelector('.wysiwyg-block-container') as HTMLElement;
const codeElement = codeContainer?.querySelector('.code-editor') as HTMLElement;
expect(codeElement).toBeTruthy();
expect(codeElement?.textContent).toEqual('const x = 42;');
// Check if language selector is shown
const languageSelector = codeContainer?.querySelector('.language-selector') as HTMLSelectElement;
expect(languageSelector).toBeTruthy();
expect(languageSelector?.value).toEqual('javascript');
// Check if monospace font is applied - code-editor is a <code> element
const computedStyle = window.getComputedStyle(codeElement);
// Font family may vary by platform, so just check it contains something
expect(computedStyle.fontFamily).toBeTruthy();
});
tap.test('Phase 3: List block should render correctly', async () => {
const editor: DeesInputWysiwyg = await webhelpers.fixture(
webhelpers.html`<dees-input-wysiwyg></dees-input-wysiwyg>`
);
// Import a list block
editor.importBlocks([
{ id: 'list-1', type: 'list', content: 'First item\nSecond item\nThird item' }
]);
await editor.updateComplete;
await new Promise(resolve => setTimeout(resolve, 100));
// Check if list block was rendered
const listBlockWrapper = editor.shadowRoot?.querySelector('[data-block-id="list-1"]');
const listBlockComponent = listBlockWrapper?.querySelector('dees-wysiwyg-block') as DeesWysiwygBlock;
const listContainer = listBlockComponent?.shadowRoot?.querySelector('.wysiwyg-block-container') as HTMLElement;
const listElement = listContainer?.querySelector('.block.list') as HTMLElement;
expect(listElement).toBeTruthy();
// Check if list items were created
const listItems = listElement?.querySelectorAll('li');
expect(listItems?.length).toEqual(3);
expect(listItems?.[0].textContent).toEqual('First item');
expect(listItems?.[1].textContent).toEqual('Second item');
expect(listItems?.[2].textContent).toEqual('Third item');
// Check if it's an unordered list by default
const ulElement = listElement?.querySelector('ul');
expect(ulElement).toBeTruthy();
});
tap.test('Phase 3: Quote block split should work', async () => {
const editor: DeesInputWysiwyg = await webhelpers.fixture(
webhelpers.html`<dees-input-wysiwyg></dees-input-wysiwyg>`
);
// Import a quote block
editor.importBlocks([
{ id: 'quote-split', type: 'quote', content: 'To be or not to be' }
]);
await editor.updateComplete;
await new Promise(resolve => setTimeout(resolve, 100));
// Get the quote block
const quoteBlockWrapper = editor.shadowRoot?.querySelector('[data-block-id="quote-split"]');
const quoteBlockComponent = quoteBlockWrapper?.querySelector('dees-wysiwyg-block') as DeesWysiwygBlock;
const quoteContainer = quoteBlockComponent?.shadowRoot?.querySelector('.wysiwyg-block-container') as HTMLElement;
const quoteElement = quoteContainer?.querySelector('.block.quote') as HTMLElement;
// Focus and set cursor after "To be"
quoteElement.focus();
const textNode = quoteElement.firstChild;
if (textNode && textNode.nodeType === Node.TEXT_NODE) {
const range = document.createRange();
const selection = window.getSelection();
range.setStart(textNode, 5); // After "To be"
range.setEnd(textNode, 5);
selection?.removeAllRanges();
selection?.addRange(range);
await new Promise(resolve => setTimeout(resolve, 100));
// Press Enter to split
const enterEvent = new KeyboardEvent('keydown', {
key: 'Enter',
code: 'Enter',
bubbles: true,
cancelable: true,
composed: true
});
quoteElement.dispatchEvent(enterEvent);
await new Promise(resolve => setTimeout(resolve, 200));
// Check if split happened correctly
expect(editor.blocks.length).toEqual(2);
expect(editor.blocks[0].content).toEqual('To be');
expect(editor.blocks[1].content).toEqual(' or not to be');
expect(editor.blocks[1].type).toEqual('paragraph'); // New block should be paragraph
}
});
export default tap.start();

View File

@@ -1,112 +0,0 @@
import { tap, expect, webhelpers } from '@git.zone/tstest/tapbundle';
import { BlockRegistry } from '../ts_web/elements/00group-input/dees-input-wysiwyg/blocks/block.registry.js';
import { DividerBlockHandler } from '../ts_web/elements/00group-input/dees-input-wysiwyg/blocks/content/divider.block.js';
import { ParagraphBlockHandler } from '../ts_web/elements/00group-input/dees-input-wysiwyg/blocks/text/paragraph.block.js';
import { HeadingBlockHandler } from '../ts_web/elements/00group-input/dees-input-wysiwyg/blocks/text/heading.block.js';
// Import block registration to ensure handlers are registered
import '../ts_web/elements/00group-input/dees-input-wysiwyg/wysiwyg.blockregistration.js';
tap.test('BlockRegistry should register and retrieve handlers', async () => {
// Test divider handler
const dividerHandler = BlockRegistry.getHandler('divider');
expect(dividerHandler).toBeDefined();
expect(dividerHandler).toBeInstanceOf(DividerBlockHandler);
expect(dividerHandler?.type).toEqual('divider');
// Test paragraph handler
const paragraphHandler = BlockRegistry.getHandler('paragraph');
expect(paragraphHandler).toBeDefined();
expect(paragraphHandler).toBeInstanceOf(ParagraphBlockHandler);
expect(paragraphHandler?.type).toEqual('paragraph');
// Test heading handlers
const heading1Handler = BlockRegistry.getHandler('heading-1');
expect(heading1Handler).toBeDefined();
expect(heading1Handler).toBeInstanceOf(HeadingBlockHandler);
expect(heading1Handler?.type).toEqual('heading-1');
const heading2Handler = BlockRegistry.getHandler('heading-2');
expect(heading2Handler).toBeDefined();
expect(heading2Handler).toBeInstanceOf(HeadingBlockHandler);
expect(heading2Handler?.type).toEqual('heading-2');
const heading3Handler = BlockRegistry.getHandler('heading-3');
expect(heading3Handler).toBeDefined();
expect(heading3Handler).toBeInstanceOf(HeadingBlockHandler);
expect(heading3Handler?.type).toEqual('heading-3');
});
tap.test('Block handlers should render content correctly', async () => {
const testBlock = {
id: 'test-1',
type: 'paragraph' as const,
content: 'Test paragraph content'
};
const handler = BlockRegistry.getHandler('paragraph');
expect(handler).toBeDefined();
if (handler) {
const rendered = handler.render(testBlock, false);
// The render() method returns the HTML template structure
// Content is set later in setup()
expect(rendered).toContain('contenteditable="true"');
expect(rendered).toContain('data-block-type="paragraph"');
expect(rendered).toContain('data-block-id="test-1"');
expect(rendered).toContain('class="block paragraph"');
}
});
tap.test('Divider handler should render correctly', async () => {
const dividerBlock = {
id: 'test-divider',
type: 'divider' as const,
content: ' '
};
const handler = BlockRegistry.getHandler('divider');
expect(handler).toBeDefined();
if (handler) {
const rendered = handler.render(dividerBlock, false);
expect(rendered).toContain('class="block divider"');
expect(rendered).toContain('tabindex="0"');
expect(rendered).toContain('<hr>');
expect(rendered).toContain('data-block-id="test-divider"');
}
});
tap.test('Heading handlers should render with correct levels', async () => {
const headingBlock = {
id: 'test-h1',
type: 'heading-1' as const,
content: 'Test Heading'
};
const handler = BlockRegistry.getHandler('heading-1');
expect(handler).toBeDefined();
if (handler) {
const rendered = handler.render(headingBlock, false);
// The render() method returns the HTML template structure
// Content is set later in setup()
expect(rendered).toContain('class="block heading-1"');
expect(rendered).toContain('contenteditable="true"');
expect(rendered).toContain('data-block-id="test-h1"');
expect(rendered).toContain('data-block-type="heading-1"');
}
});
tap.test('getAllTypes should return all registered types', async () => {
const allTypes = BlockRegistry.getAllTypes();
expect(allTypes).toContain('divider');
expect(allTypes).toContain('paragraph');
expect(allTypes).toContain('heading-1');
expect(allTypes).toContain('heading-2');
expect(allTypes).toContain('heading-3');
expect(allTypes.length).toBeGreaterThanOrEqual(5);
});
export default tap.start();

View File

@@ -1,158 +0,0 @@
import { expect, tap, webhelpers } from '@git.zone/tstest/tapbundle';
import { DeesInputWysiwyg } from '../ts_web/elements/00group-input/dees-input-wysiwyg/dees-input-wysiwyg.js';
import { DeesWysiwygBlock } from '../ts_web/elements/00group-input/dees-input-wysiwyg/dees-wysiwyg-block.js';
tap.test('Selection highlighting should work consistently for all block types', async () => {
const editor: DeesInputWysiwyg = await webhelpers.fixture(
webhelpers.html`<dees-input-wysiwyg></dees-input-wysiwyg>`
);
// Import various block types
editor.importBlocks([
{ id: 'para-1', type: 'paragraph', content: 'This is a paragraph' },
{ id: 'heading-1', type: 'heading-1', content: 'This is a heading' },
{ id: 'quote-1', type: 'quote', content: 'This is a quote' },
{ id: 'code-1', type: 'code', content: 'const x = 42;' },
{ id: 'list-1', type: 'list', content: 'Item 1\nItem 2' }
]);
await editor.updateComplete;
await new Promise(resolve => setTimeout(resolve, 100));
// Test paragraph highlighting
console.log('Testing paragraph highlighting...');
const paraWrapper = editor.shadowRoot?.querySelector('[data-block-id="para-1"]');
const paraComponent = paraWrapper?.querySelector('dees-wysiwyg-block') as DeesWysiwygBlock;
const paraContainer = paraComponent?.shadowRoot?.querySelector('.wysiwyg-block-container') as HTMLElement;
const paraElement = paraContainer?.querySelector('.block.paragraph') as HTMLElement;
// Focus paragraph to select it
paraElement.focus();
await new Promise(resolve => setTimeout(resolve, 100));
// Check if paragraph has selected class
const paraHasSelected = paraElement.classList.contains('selected');
console.log('Paragraph has selected class:', paraHasSelected);
// Check computed styles
const paraStyle = window.getComputedStyle(paraElement);
console.log('Paragraph background:', paraStyle.background);
console.log('Paragraph box-shadow:', paraStyle.boxShadow);
// Test heading highlighting
console.log('\nTesting heading highlighting...');
const headingWrapper = editor.shadowRoot?.querySelector('[data-block-id="heading-1"]');
const headingComponent = headingWrapper?.querySelector('dees-wysiwyg-block') as DeesWysiwygBlock;
const headingContainer = headingComponent?.shadowRoot?.querySelector('.wysiwyg-block-container') as HTMLElement;
const headingElement = headingContainer?.querySelector('.block.heading-1') as HTMLElement;
// Focus heading to select it
headingElement.focus();
await new Promise(resolve => setTimeout(resolve, 100));
// Check if heading has selected class
const headingHasSelected = headingElement.classList.contains('selected');
console.log('Heading has selected class:', headingHasSelected);
// Check computed styles
const headingStyle = window.getComputedStyle(headingElement);
console.log('Heading background:', headingStyle.background);
console.log('Heading box-shadow:', headingStyle.boxShadow);
// Test quote highlighting
console.log('\nTesting quote highlighting...');
const quoteWrapper = editor.shadowRoot?.querySelector('[data-block-id="quote-1"]');
const quoteComponent = quoteWrapper?.querySelector('dees-wysiwyg-block') as DeesWysiwygBlock;
const quoteContainer = quoteComponent?.shadowRoot?.querySelector('.wysiwyg-block-container') as HTMLElement;
const quoteElement = quoteContainer?.querySelector('.block.quote') as HTMLElement;
// Focus quote to select it
quoteElement.focus();
await new Promise(resolve => setTimeout(resolve, 100));
// Check if quote has selected class
const quoteHasSelected = quoteElement.classList.contains('selected');
console.log('Quote has selected class:', quoteHasSelected);
// Test code highlighting - code blocks use .code-editor instead of .block.code
console.log('\nTesting code highlighting...');
const codeWrapper = editor.shadowRoot?.querySelector('[data-block-id="code-1"]');
const codeComponent = codeWrapper?.querySelector('dees-wysiwyg-block') as DeesWysiwygBlock;
const codeContainer = codeComponent?.shadowRoot?.querySelector('.wysiwyg-block-container') as HTMLElement;
const codeElement = codeContainer?.querySelector('.code-editor') as HTMLElement;
const codeBlockContainer = codeContainer?.querySelector('.code-block-container') as HTMLElement;
// Focus code to select it
codeElement.focus();
await new Promise(resolve => setTimeout(resolve, 100));
// For code blocks, the selection is on the container, not the editor
const codeHasSelected = codeBlockContainer?.classList.contains('selected');
console.log('Code container has selected class:', codeHasSelected);
// Focus back on paragraph and check if others are deselected
console.log('\nFocusing back on paragraph...');
paraElement.focus();
await new Promise(resolve => setTimeout(resolve, 100));
// Check that only paragraph is selected
expect(paraElement.classList.contains('selected')).toBeTrue();
expect(headingElement.classList.contains('selected')).toBeFalse();
expect(quoteElement.classList.contains('selected')).toBeFalse();
// Code blocks use different selection structure
expect(codeBlockContainer?.classList.contains('selected') || false).toBeFalse();
console.log('Selection highlighting test complete');
});
tap.test('Selected class should toggle correctly when clicking between blocks', async () => {
const editor: DeesInputWysiwyg = await webhelpers.fixture(
webhelpers.html`<dees-input-wysiwyg></dees-input-wysiwyg>`
);
// Import two blocks
editor.importBlocks([
{ id: 'block-1', type: 'paragraph', content: 'First block' },
{ id: 'block-2', type: 'paragraph', content: 'Second block' }
]);
await editor.updateComplete;
await new Promise(resolve => setTimeout(resolve, 100));
// Get both blocks
const block1Wrapper = editor.shadowRoot?.querySelector('[data-block-id="block-1"]');
const block1Component = block1Wrapper?.querySelector('dees-wysiwyg-block') as DeesWysiwygBlock;
const block1Container = block1Component?.shadowRoot?.querySelector('.wysiwyg-block-container') as HTMLElement;
const block1Element = block1Container?.querySelector('.block.paragraph') as HTMLElement;
const block2Wrapper = editor.shadowRoot?.querySelector('[data-block-id="block-2"]');
const block2Component = block2Wrapper?.querySelector('dees-wysiwyg-block') as DeesWysiwygBlock;
const block2Container = block2Component?.shadowRoot?.querySelector('.wysiwyg-block-container') as HTMLElement;
const block2Element = block2Container?.querySelector('.block.paragraph') as HTMLElement;
// Initially neither should be selected
expect(block1Element.classList.contains('selected')).toBeFalse();
expect(block2Element.classList.contains('selected')).toBeFalse();
// Click on first block
block1Element.click();
block1Element.focus();
await new Promise(resolve => setTimeout(resolve, 100));
// First block should be selected
expect(block1Element.classList.contains('selected')).toBeTrue();
expect(block2Element.classList.contains('selected')).toBeFalse();
// Click on second block
block2Element.click();
block2Element.focus();
await new Promise(resolve => setTimeout(resolve, 100));
// Second block should be selected, first should not
expect(block1Element.classList.contains('selected')).toBeFalse();
expect(block2Element.classList.contains('selected')).toBeTrue();
console.log('Toggle test complete');
});
export default tap.start();

View File

@@ -1,62 +0,0 @@
import { expect, tap, webhelpers } from '@git.zone/tstest/tapbundle';
import { DeesInputWysiwyg } from '../ts_web/elements/00group-input/dees-input-wysiwyg/dees-input-wysiwyg.js';
import { DeesWysiwygBlock } from '../ts_web/elements/00group-input/dees-input-wysiwyg/dees-wysiwyg-block.js';
tap.test('Selection highlighting basic test', async () => {
const editor: DeesInputWysiwyg = await webhelpers.fixture(
webhelpers.html`<dees-input-wysiwyg></dees-input-wysiwyg>`
);
// Import two blocks
editor.importBlocks([
{ id: 'para-1', type: 'paragraph', content: 'First paragraph' },
{ id: 'head-1', type: 'heading-1', content: 'First heading' }
]);
await editor.updateComplete;
await new Promise(resolve => setTimeout(resolve, 500));
// Get paragraph element
const paraWrapper = editor.shadowRoot?.querySelector('[data-block-id="para-1"]');
const paraComponent = paraWrapper?.querySelector('dees-wysiwyg-block') as DeesWysiwygBlock;
const paraBlock = paraComponent?.shadowRoot?.querySelector('.block.paragraph') as HTMLElement;
// Get heading element
const headWrapper = editor.shadowRoot?.querySelector('[data-block-id="head-1"]');
const headComponent = headWrapper?.querySelector('dees-wysiwyg-block') as DeesWysiwygBlock;
const headBlock = headComponent?.shadowRoot?.querySelector('.block.heading-1') as HTMLElement;
console.log('Found elements:', {
paraBlock: !!paraBlock,
headBlock: !!headBlock
});
// Focus paragraph
paraBlock.focus();
await new Promise(resolve => setTimeout(resolve, 100));
// Check classes
console.log('Paragraph classes:', paraBlock.className);
console.log('Heading classes:', headBlock.className);
// Check isSelected property
console.log('Paragraph component isSelected:', paraComponent.isSelected);
console.log('Heading component isSelected:', headComponent.isSelected);
// Focus heading
headBlock.focus();
await new Promise(resolve => setTimeout(resolve, 100));
// Check classes again
console.log('\nAfter focusing heading:');
console.log('Paragraph classes:', paraBlock.className);
console.log('Heading classes:', headBlock.className);
console.log('Paragraph component isSelected:', paraComponent.isSelected);
console.log('Heading component isSelected:', headComponent.isSelected);
// Check that heading is selected
expect(headBlock.classList.contains('selected')).toBeTrue();
expect(paraBlock.classList.contains('selected')).toBeFalse();
});
export default tap.start();

View File

@@ -1,98 +0,0 @@
import { tap, expect, webhelpers } from '@git.zone/tstest/tapbundle';
import { DeesInputWysiwyg } from '../ts_web/elements/00group-input/dees-input-wysiwyg/dees-input-wysiwyg.js';
import { DeesWysiwygBlock } from '../ts_web/elements/00group-input/dees-input-wysiwyg/dees-wysiwyg-block.js';
tap.test('should split paragraph content on Enter key', async () => {
// Create the wysiwyg editor
const editor: DeesInputWysiwyg = await webhelpers.fixture(
webhelpers.html`<dees-input-wysiwyg></dees-input-wysiwyg>`
);
// Import a test paragraph
editor.importBlocks([{
id: 'test-para-1',
type: 'paragraph',
content: 'Hello World'
}]);
await editor.updateComplete;
// Wait for blocks to render
await new Promise(resolve => setTimeout(resolve, 100));
// Get the block wrapper and component
const blockWrapper = editor.shadowRoot?.querySelector('[data-block-id="test-para-1"]');
expect(blockWrapper).toBeDefined();
const blockComponent = blockWrapper?.querySelector('dees-wysiwyg-block') as DeesWysiwygBlock;
expect(blockComponent).toBeDefined();
expect(blockComponent.block.type).toEqual('paragraph');
// Wait for block to render
await blockComponent.updateComplete;
// Test getSplitContent
console.log('Testing getSplitContent...');
const splitResult = blockComponent.getSplitContent();
console.log('Split result:', splitResult);
// Since we haven't set cursor position, it might return null or split at start
// This is just to test if the method is callable
expect(typeof blockComponent.getSplitContent).toEqual('function');
});
tap.test('should handle Enter key press in paragraph', async () => {
// Create the wysiwyg editor
const editor: DeesInputWysiwyg = await webhelpers.fixture(
webhelpers.html`<dees-input-wysiwyg></dees-input-wysiwyg>`
);
// Import a test paragraph
editor.importBlocks([{
id: 'test-enter-1',
type: 'paragraph',
content: 'First part|Second part' // | marks where we'll simulate cursor
}]);
await editor.updateComplete;
await new Promise(resolve => setTimeout(resolve, 100));
// Check initial state
expect(editor.blocks.length).toEqual(1);
expect(editor.blocks[0].content).toEqual('First part|Second part');
// Get the block element
const blockWrapper = editor.shadowRoot?.querySelector('[data-block-id="test-enter-1"]');
const blockComponent = blockWrapper?.querySelector('dees-wysiwyg-block') as DeesWysiwygBlock;
const blockElement = blockComponent.shadowRoot?.querySelector('.block.paragraph') as HTMLDivElement;
expect(blockElement).toBeDefined();
// Set content without the | marker
blockElement.textContent = 'First partSecond part';
// Focus the block
blockElement.focus();
// Create and dispatch Enter key event
const enterEvent = new KeyboardEvent('keydown', {
key: 'Enter',
code: 'Enter',
bubbles: true,
cancelable: true,
composed: true
});
// Dispatch the event
blockElement.dispatchEvent(enterEvent);
// Wait for processing
await new Promise(resolve => setTimeout(resolve, 200));
// Check if block was split (this might not work perfectly in test environment)
console.log('Blocks after Enter:', editor.blocks.length);
console.log('Block contents:', editor.blocks.map(b => b.content));
});
export default tap.start();

View File

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

View File

@@ -0,0 +1,20 @@
import { html } from '@design.estate/dees-element';
export const demo = () => html`
<style>
.demo-container {
padding: 48px;
background: hsl(240 10% 4%);
min-height: 300px;
display: flex;
justify-content: center;
}
</style>
<div class="demo-container">
<eco-applauncher-powermenu
open
@power-action=${(e: CustomEvent) => console.log('Power action:', e.detail.action)}
@menu-close=${() => console.log('Menu closed')}
></eco-applauncher-powermenu>
</div>
`;

View File

@@ -0,0 +1,261 @@
import {
customElement,
DeesElement,
type TemplateResult,
html,
property,
css,
cssManager,
} from '@design.estate/dees-element';
import { DeesIcon } from '@design.estate/dees-catalog';
import { demo } from './eco-applauncher-powermenu.demo.js';
// Ensure dees-icon is registered
DeesIcon;
declare global {
interface HTMLElementTagNameMap {
'eco-applauncher-powermenu': EcoApplauncherPowermenu;
}
}
export type TPowerAction = 'lock' | 'lock-sleep' | 'reboot';
@customElement('eco-applauncher-powermenu')
export class EcoApplauncherPowermenu extends DeesElement {
public static demo = demo;
public static demoGroup = 'App Launcher';
public static styles = [
cssManager.defaultStyles,
css`
:host {
display: block;
position: relative;
pointer-events: none;
}
:host([open]) {
pointer-events: auto;
}
.menu-container {
background: ${cssManager.bdTheme('#ffffff', 'hsl(240 6% 10%)')};
border: 1px solid ${cssManager.bdTheme('hsl(0 0% 90%)', 'hsl(240 5% 20%)')};
border-radius: 12px;
box-shadow: ${cssManager.bdTheme(
'0 8px 32px rgba(0, 0, 0, 0.15)',
'0 8px 32px rgba(0, 0, 0, 0.4)'
)};
min-width: 200px;
overflow: hidden;
opacity: 0;
transform: scale(0.95) translateY(-8px);
transition: all 0.2s ease-out;
pointer-events: none;
}
:host([open]) .menu-container {
opacity: 1;
transform: scale(1) translateY(0);
pointer-events: auto;
}
.menu-header {
padding: 12px 16px;
border-bottom: 1px solid ${cssManager.bdTheme('hsl(0 0% 92%)', 'hsl(240 5% 15%)')};
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
color: ${cssManager.bdTheme('hsl(0 0% 50%)', 'hsl(0 0% 60%)')};
}
.menu-options {
padding: 8px 0;
}
.menu-option {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 16px;
cursor: pointer;
transition: background 0.15s ease;
color: ${cssManager.bdTheme('hsl(0 0% 20%)', 'hsl(0 0% 90%)')};
}
.menu-option:hover {
background: ${cssManager.bdTheme('hsl(0 0% 96%)', 'hsl(240 5% 15%)')};
}
.menu-option:active {
background: ${cssManager.bdTheme('hsl(0 0% 92%)', 'hsl(240 5% 18%)')};
}
.menu-option.danger {
color: ${cssManager.bdTheme('hsl(0 72% 45%)', 'hsl(0 72% 60%)')};
}
.menu-option.danger:hover {
background: ${cssManager.bdTheme('hsl(0 72% 97%)', 'hsl(0 50% 15%)')};
}
.option-icon {
display: flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
border-radius: 6px;
background: ${cssManager.bdTheme('hsl(0 0% 94%)', 'hsl(240 5% 18%)')};
}
.menu-option.danger .option-icon {
background: ${cssManager.bdTheme('hsl(0 72% 94%)', 'hsl(0 50% 18%)')};
}
.option-text {
display: flex;
flex-direction: column;
gap: 2px;
}
.option-label {
font-size: 14px;
font-weight: 500;
}
.option-description {
font-size: 11px;
color: ${cssManager.bdTheme('hsl(0 0% 55%)', 'hsl(0 0% 50%)')};
}
.menu-divider {
height: 1px;
background: ${cssManager.bdTheme('hsl(0 0% 92%)', 'hsl(240 5% 15%)')};
margin: 4px 0;
}
`,
];
@property({ type: Boolean, reflect: true })
accessor open = false;
private boundHandleClickOutside = this.handleClickOutside.bind(this);
private inactivityTimeout: ReturnType<typeof setTimeout> | null = null;
private readonly INACTIVITY_TIMEOUT = 60000; // 1 minute
private lastActivityTime = 0;
public render(): TemplateResult {
return html`
<div class="menu-container"
@click=${(e: MouseEvent) => e.stopPropagation()}
@mousedown=${this.resetInactivityTimer}
>
<div class="menu-header">Power</div>
<div class="menu-options">
<div class="menu-option" @click=${() => this.handleAction('lock')}>
<div class="option-icon">
<dees-icon .icon=${'lucide:lock'} .iconSize=${16}></dees-icon>
</div>
<div class="option-text">
<span class="option-label">Lock</span>
<span class="option-description">Lock the screen</span>
</div>
</div>
<div class="menu-option" @click=${() => this.handleAction('lock-sleep')}>
<div class="option-icon">
<dees-icon .icon=${'lucide:moon'} .iconSize=${16}></dees-icon>
</div>
<div class="option-text">
<span class="option-label">Lock + Sleep</span>
<span class="option-description">Lock and turn off display</span>
</div>
</div>
<div class="menu-divider"></div>
<div class="menu-option danger" @click=${() => this.handleAction('reboot')}>
<div class="option-icon">
<dees-icon .icon=${'lucide:refreshCw'} .iconSize=${16}></dees-icon>
</div>
<div class="option-text">
<span class="option-label">Reboot</span>
<span class="option-description">Restart the system</span>
</div>
</div>
</div>
</div>
`;
}
private handleAction(action: TPowerAction): void {
this.dispatchEvent(new CustomEvent('power-action', {
detail: { action },
bubbles: true,
composed: true,
}));
this.closeMenu();
}
private handleClickOutside(e: MouseEvent): void {
if (this.open && !this.contains(e.target as Node)) {
this.closeMenu();
}
}
private resetInactivityTimer(): void {
const now = Date.now();
// Throttle: only reset if 5+ seconds since last reset
if (now - this.lastActivityTime < 5000) {
return;
}
this.lastActivityTime = now;
this.clearInactivityTimer();
if (this.open) {
this.inactivityTimeout = setTimeout(() => {
this.closeMenu();
}, this.INACTIVITY_TIMEOUT);
}
}
private clearInactivityTimer(): void {
if (this.inactivityTimeout) {
clearTimeout(this.inactivityTimeout);
this.inactivityTimeout = null;
}
}
private closeMenu(): void {
this.open = false;
this.dispatchEvent(new CustomEvent('menu-close', {
bubbles: true,
composed: true,
}));
}
protected updated(changedProperties: Map<string, unknown>): void {
if (changedProperties.has('open')) {
if (this.open) {
this.resetInactivityTimer();
} else {
this.clearInactivityTimer();
}
}
}
async connectedCallback(): Promise<void> {
await super.connectedCallback();
setTimeout(() => {
document.addEventListener('click', this.boundHandleClickOutside);
}, 0);
}
async disconnectedCallback(): Promise<void> {
await super.disconnectedCallback();
document.removeEventListener('click', this.boundHandleClickOutside);
this.clearInactivityTimer();
}
}

View File

@@ -0,0 +1 @@
export * from './eco-applauncher-powermenu.js';

View File

@@ -1,21 +1,18 @@
import { html } from '@design.estate/dees-element';
import type { IAppIcon } from './eco-applauncher.js';
import type { IAppIcon, ILoginConfig, ILoginCredentials, TApplauncherMode } from './eco-applauncher.js';
import type { IWifiNetwork } from '../eco-applauncher-wifimenu/index.js';
import type { IAudioDevice } from '../eco-applauncher-soundmenu/index.js';
import '../../../views/eco-view-settings/eco-view-settings.js';
import '../../../views/eco-view-peripherals/eco-view-peripherals.js';
import '../../../views/eco-view-saasshare/eco-view-saasshare.js';
import '../../../views/eco-view-system/eco-view-system.js';
import type { EcoApplauncher } from './eco-applauncher.js';
const mockApps: IAppIcon[] = [
{ name: 'Settings', icon: 'lucide:settings', action: () => console.log('Settings clicked') },
{ name: 'Browser', icon: 'lucide:globe', action: () => console.log('Browser clicked') },
{ name: 'Terminal', icon: 'lucide:terminal', action: () => console.log('Terminal clicked') },
{ name: 'Files', icon: 'lucide:folder', action: () => console.log('Files clicked') },
{ name: 'Calendar', icon: 'lucide:calendar', action: () => console.log('Calendar clicked') },
{ name: 'Mail', icon: 'lucide:mail', action: () => console.log('Mail clicked') },
{ name: 'Music', icon: 'lucide:music', action: () => console.log('Music clicked') },
{ name: 'Photos', icon: 'lucide:image', action: () => console.log('Photos clicked') },
{ name: 'Notes', icon: 'lucide:fileText', action: () => console.log('Notes clicked') },
{ name: 'Calculator', icon: 'lucide:calculator', action: () => console.log('Calculator clicked') },
{ name: 'Weather', icon: 'lucide:cloudSun', action: () => console.log('Weather clicked') },
{ name: 'Maps', icon: 'lucide:map', action: () => console.log('Maps clicked') },
{ name: 'SaaS Share', icon: 'lucide:share2', view: html`<eco-view-saasshare></eco-view-saasshare>` },
{ name: 'System', icon: 'lucide:activity', view: html`<eco-view-system></eco-view-system>` },
{ name: 'Peripherals', icon: 'lucide:monitor', view: html`<eco-view-peripherals></eco-view-peripherals>` },
{ name: 'Settings', icon: 'lucide:settings', view: html`<eco-view-settings></eco-view-settings>` },
];
const mockNetworks: IWifiNetwork[] = [
@@ -32,7 +29,33 @@ const mockAudioDevices: IAudioDevice[] = [
{ id: 'hdmi', name: 'LG Monitor', type: 'hdmi' },
];
export const demo = () => html`
const loginConfig: ILoginConfig = {
allowedMethods: ['pin', 'password', 'qr'],
pinLength: 4,
welcomeMessage: 'Welcome to EcoBridge',
};
const handleLoginAttempt = (e: CustomEvent) => {
const credentials = e.detail as ILoginCredentials;
const applauncher = e.target as EcoApplauncher;
console.log('Login attempt:', credentials);
// Demo validation: PIN "1234" or password "demo"
if (
(credentials.method === 'pin' && credentials.value === '1234') ||
(credentials.method === 'password' && credentials.value === 'demo')
) {
console.log('Login successful!');
applauncher.setLoginResult(true);
} else {
console.log('Login failed');
applauncher.setLoginResult(false, 'Invalid credentials. Try PIN: 1234 or Password: demo');
}
};
// Home mode demo
const demoHome = () => html`
<style>
.demo-container {
width: 100%;
@@ -41,6 +64,8 @@ export const demo = () => html`
</style>
<div class="demo-container">
<eco-applauncher
.mode=${'home' as TApplauncherMode}
.loginConfig=${loginConfig}
.apps=${mockApps}
.batteryLevel=${85}
.networkStatus=${'online'}
@@ -54,6 +79,9 @@ export const demo = () => html`
.muted=${false}
.userName=${'John Doe'}
.notificationCount=${3}
@login-attempt=${handleLoginAttempt}
@login-success=${() => console.log('Login success event received')}
@login-failure=${(e: CustomEvent) => console.log('Login failure:', e.detail)}
@wifi-toggle=${(e: CustomEvent) => console.log('WiFi toggle:', e.detail)}
@network-select=${(e: CustomEvent) => console.log('Network selected:', e.detail)}
@wifi-settings-click=${() => console.log('WiFi settings clicked')}
@@ -69,3 +97,37 @@ export const demo = () => html`
></eco-applauncher>
</div>
`;
demoHome.demoTitle = 'Home Mode';
// Login mode demo
const demoLogin = () => html`
<style>
.demo-container {
width: 100%;
height: 100%;
}
</style>
<div class="demo-container">
<eco-applauncher
.mode=${'login' as TApplauncherMode}
.loginConfig=${loginConfig}
.apps=${mockApps}
.batteryLevel=${85}
.networkStatus=${'online'}
.soundLevel=${70}
.networks=${mockNetworks}
.connectedNetwork=${'HomeNetwork'}
.wifiEnabled=${true}
.outputDevices=${mockAudioDevices}
.activeDeviceId=${'speakers'}
.muted=${false}
@login-attempt=${handleLoginAttempt}
@login-success=${() => console.log('Login success event received')}
@login-failure=${(e: CustomEvent) => console.log('Login failure:', e.detail)}
></eco-applauncher>
</div>
`;
demoLogin.demoTitle = 'Login Mode';
// Export array of demo functions
export const demo = [demoHome, demoLogin];

View File

@@ -14,6 +14,9 @@ import { EcoApplauncherWifimenu, type IWifiNetwork } from '../eco-applauncher-wi
import { EcoApplauncherBatterymenu } from '../eco-applauncher-batterymenu/index.js';
import { EcoApplauncherSoundmenu, type IAudioDevice } from '../eco-applauncher-soundmenu/index.js';
import { EcoApplauncherKeyboard } from '../eco-applauncher-keyboard/index.js';
import { EcoApplauncherPowermenu, type TPowerAction } from '../eco-applauncher-powermenu/index.js';
import { EcoViewHome } from '../../../views/eco-view-home/index.js';
import { EcoViewLogin, type ILoginConfig, type ILoginCredentials } from '../../../views/eco-view-login/index.js';
// Ensure components are registered
DeesIcon;
@@ -21,6 +24,9 @@ EcoApplauncherWifimenu;
EcoApplauncherBatterymenu;
EcoApplauncherSoundmenu;
EcoApplauncherKeyboard;
EcoApplauncherPowermenu;
EcoViewHome;
EcoViewLogin;
declare global {
interface HTMLElementTagNameMap {
@@ -28,12 +34,18 @@ declare global {
}
}
export type TApplauncherMode = 'login' | 'home';
export interface IAppIcon {
name: string;
icon: string;
action?: () => void;
view?: TemplateResult;
}
export type { ILoginConfig, ILoginCredentials } from '../../../views/eco-view-login/index.js';
export type { TPowerAction } from '../eco-applauncher-powermenu/index.js';
export interface IStatusBarConfig {
showTime?: boolean;
showNetwork?: boolean;
@@ -153,6 +165,10 @@ export class EcoApplauncher extends DeesElement {
background: ${cssManager.bdTheme('hsl(220 15% 88%)', 'hsl(240 5% 15%)')};
}
.top-icon-button.active {
background: ${cssManager.bdTheme('hsl(220 15% 85%)', 'hsl(240 5% 18%)')};
}
.user-avatar {
width: 32px;
height: 32px;
@@ -195,9 +211,6 @@ export class EcoApplauncher extends DeesElement {
.apps-area {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
padding: 48px;
overflow-y: auto;
}
@@ -206,7 +219,6 @@ export class EcoApplauncher extends DeesElement {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
gap: 32px;
max-width: 800px;
width: 100%;
}
@@ -415,6 +427,18 @@ export class EcoApplauncher extends DeesElement {
pointer-events: none;
}
.topbar-menu-wrapper {
position: relative;
}
.topbar-menu-popup {
position: absolute;
top: calc(100% + 8px);
right: 0;
z-index: 100;
pointer-events: none;
}
.keyboard-area {
flex-shrink: 0;
height: 0;
@@ -430,6 +454,53 @@ export class EcoApplauncher extends DeesElement {
overflow: visible;
}
.view-area {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
.view-header {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 24px;
background: ${cssManager.bdTheme('hsl(220 15% 94%)', 'hsl(240 6% 8%)')};
border-bottom: 1px solid ${cssManager.bdTheme('hsl(220 15% 88%)', 'hsl(240 5% 15%)')};
flex-shrink: 0;
}
.back-button {
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
padding: 8px 14px;
background: ${cssManager.bdTheme('hsl(220 15% 88%)', 'hsl(240 5% 14%)')};
border-radius: 8px;
cursor: pointer;
transition: background 0.15s ease;
color: ${cssManager.bdTheme('hsl(0 0% 30%)', 'hsl(0 0% 80%)')};
font-size: 13px;
font-weight: 500;
}
.back-button:hover {
background: ${cssManager.bdTheme('hsl(220 15% 82%)', 'hsl(240 5% 18%)')};
}
.view-title {
font-size: 16px;
font-weight: 600;
color: ${cssManager.bdTheme('hsl(0 0% 20%)', 'hsl(0 0% 90%)')};
}
.view-content {
flex: 1;
overflow: auto;
}
@media (max-width: 600px) {
.apps-area {
padding: 24px;
@@ -462,6 +533,16 @@ export class EcoApplauncher extends DeesElement {
`,
];
@property({ type: String })
accessor mode: TApplauncherMode = 'home';
@property({ type: Object })
accessor loginConfig: ILoginConfig = {
allowedMethods: ['pin', 'password', 'qr'],
pinLength: 4,
welcomeMessage: 'Welcome',
};
@property({ type: Array })
accessor apps: IAppIcon[] = [];
@@ -512,9 +593,18 @@ export class EcoApplauncher extends DeesElement {
@state()
accessor soundMenuOpen = false;
@state()
accessor powerMenuOpen = false;
@state()
accessor keyboardVisible = false;
@state()
accessor activeView: TemplateResult | null = null;
@state()
accessor activeViewName: string | null = null;
@property({ type: Array })
accessor networks: IWifiNetwork[] = [];
@@ -552,12 +642,8 @@ export class EcoApplauncher extends DeesElement {
public render(): TemplateResult {
return html`
<div class="launcher-container">
${this.renderTopBar()}
<div class="apps-area">
<div class="apps-grid">
${this.apps.map((app) => this.renderAppIcon(app))}
</div>
</div>
${this.mode === 'login' ? '' : this.renderTopBar()}
${this.renderMainContent()}
<div class="keyboard-area ${this.keyboardVisible ? 'visible' : ''}">
<eco-applauncher-keyboard
?visible=${this.keyboardVisible}
@@ -585,6 +671,58 @@ export class EcoApplauncher extends DeesElement {
`;
}
private renderMainContent(): TemplateResult {
if (this.mode === 'login') {
return this.renderLoginView();
}
// Home mode
if (this.activeView) {
return this.renderActiveView();
}
return this.renderHomeView();
}
private renderLoginView(): TemplateResult {
return html`
<eco-view-login
.config=${this.loginConfig}
@login-attempt=${this.handleLoginAttempt}
@key-press=${this.handleKeyboardKeyPress}
@backspace=${this.handleKeyboardBackspace}
style="flex: 1;"
></eco-view-login>
`;
}
private renderHomeView(): TemplateResult {
return html`
<eco-view-home
.apps=${this.apps}
@app-click=${this.handleHomeAppClick}
></eco-view-home>
`;
}
private renderAppsArea(): TemplateResult {
return html`
<div class="apps-area">
<div class="apps-grid">
${this.apps.map((app) => this.renderAppIcon(app))}
</div>
</div>
`;
}
private renderActiveView(): TemplateResult {
return html`
<div class="view-area">
<div class="view-content">
${this.activeView}
</div>
</div>
`;
}
private renderAppIcon(app: IAppIcon): TemplateResult {
return html`
<div class="app-icon" @click=${() => this.handleAppClick(app)}>
@@ -607,7 +745,13 @@ export class EcoApplauncher extends DeesElement {
return html`
<div class="top-bar">
<div class="top-left">
${this.topBarConfig.showDate ? html`
${this.activeView ? html`
<div class="back-button" @click=${this.handleBackClick}>
<dees-icon .icon=${'lucide:arrowLeft'} .iconSize=${16}></dees-icon>
<span>Back</span>
</div>
<span class="view-title">${this.activeViewName}</span>
` : this.topBarConfig.showDate ? html`
<span class="top-date">${this.currentDate}</span>
` : ''}
</div>
@@ -636,6 +780,21 @@ export class EcoApplauncher extends DeesElement {
${userInitials}
</div>
` : ''}
<div class="topbar-menu-wrapper">
<div
class="top-icon-button ${this.powerMenuOpen ? 'active' : ''}"
@click=${this.handlePowerClick}
>
<dees-icon .icon=${'lucide:power'} .iconSize=${18}></dees-icon>
</div>
<div class="topbar-menu-popup">
<eco-applauncher-powermenu
?open=${this.powerMenuOpen}
@menu-close=${this.handlePowerMenuClose}
@power-action=${this.handlePowerAction}
></eco-applauncher-powermenu>
</div>
</div>
</div>
</div>
`;
@@ -831,15 +990,90 @@ export class EcoApplauncher extends DeesElement {
composed: true,
})
);
// If app has a view, open it inside the applauncher
if (app.view) {
this.activeView = app.view;
this.activeViewName = app.name;
}
// Execute the action (runs after setting view for initialization callbacks)
if (app.action) {
app.action();
}
}
private handleHomeAppClick(e: CustomEvent): void {
const app = e.detail.app as IAppIcon;
this.handleAppClick(app);
}
private handleLoginAttempt(e: CustomEvent): void {
const credentials = e.detail as ILoginCredentials;
// Dispatch event for parent to handle validation
this.dispatchEvent(new CustomEvent('login-attempt', {
detail: credentials,
bubbles: true,
composed: true,
}));
}
/**
* Set the login result after validation
* @param success Whether login was successful
* @param errorMessage Optional error message to display
*/
public setLoginResult(success: boolean, errorMessage?: string): void {
const loginView = this.shadowRoot?.querySelector('eco-view-login') as EcoViewLogin | null;
if (success) {
this.mode = 'home';
this.dispatchEvent(new CustomEvent('login-success', {
bubbles: true,
composed: true,
}));
} else {
if (loginView && errorMessage) {
loginView.showErrorMessage(errorMessage);
}
this.dispatchEvent(new CustomEvent('login-failure', {
detail: { error: errorMessage },
bubbles: true,
composed: true,
}));
}
}
/**
* Switch to login mode
*/
public showLogin(): void {
this.mode = 'login';
this.activeView = null;
this.activeViewName = null;
}
/**
* Switch to home mode
*/
public showHome(): void {
this.mode = 'home';
}
private handleBackClick(): void {
this.activeView = null;
this.activeViewName = null;
this.dispatchEvent(new CustomEvent('view-close', {
bubbles: true,
composed: true,
}));
}
private handleNetworkClick(e: MouseEvent): void {
e.stopPropagation();
this.batteryMenuOpen = false;
this.soundMenuOpen = false;
this.powerMenuOpen = false;
this.wifiMenuOpen = !this.wifiMenuOpen;
}
@@ -847,6 +1081,7 @@ export class EcoApplauncher extends DeesElement {
e.stopPropagation();
this.wifiMenuOpen = false;
this.soundMenuOpen = false;
this.powerMenuOpen = false;
this.batteryMenuOpen = !this.batteryMenuOpen;
}
@@ -854,9 +1089,18 @@ export class EcoApplauncher extends DeesElement {
e.stopPropagation();
this.wifiMenuOpen = false;
this.batteryMenuOpen = false;
this.powerMenuOpen = false;
this.soundMenuOpen = !this.soundMenuOpen;
}
private handlePowerClick(e: MouseEvent): void {
e.stopPropagation();
this.wifiMenuOpen = false;
this.batteryMenuOpen = false;
this.soundMenuOpen = false;
this.powerMenuOpen = !this.powerMenuOpen;
}
private handleWifiMenuClose(): void {
this.wifiMenuOpen = false;
}
@@ -869,6 +1113,19 @@ export class EcoApplauncher extends DeesElement {
this.soundMenuOpen = false;
}
private handlePowerMenuClose(): void {
this.powerMenuOpen = false;
}
private handlePowerAction(e: CustomEvent): void {
const action = e.detail.action as TPowerAction;
this.dispatchEvent(new CustomEvent('power-action', {
detail: { action },
bubbles: true,
composed: true,
}));
}
private handleVolumeChange(e: CustomEvent): void {
this.soundLevel = e.detail.volume;
this.dispatchEvent(new CustomEvent('volume-change', {
@@ -953,6 +1210,7 @@ export class EcoApplauncher extends DeesElement {
this.wifiMenuOpen = false;
this.batteryMenuOpen = false;
this.soundMenuOpen = false;
this.powerMenuOpen = false;
}
this.dispatchEvent(new CustomEvent('keyboard-toggle', {
detail: { visible: this.keyboardVisible },

View File

@@ -4,3 +4,4 @@ export * from './eco-applauncher-wifimenu/index.js';
export * from './eco-applauncher-batterymenu/index.js';
export * from './eco-applauncher-soundmenu/index.js';
export * from './eco-applauncher-keyboard/index.js';
export * from './eco-applauncher-powermenu/index.js';

View File

@@ -0,0 +1,31 @@
import { html } from '@design.estate/dees-element';
import type { IAppIcon } from './eco-view-home.js';
const mockApps: IAppIcon[] = [
{ name: 'SaaS Share', icon: 'lucide:share2' },
{ name: 'System', icon: 'lucide:activity' },
{ name: 'Peripherals', icon: 'lucide:monitor' },
{ name: 'Settings', icon: 'lucide:settings' },
{ name: 'Files', icon: 'lucide:folder' },
{ name: 'Terminal', icon: 'lucide:terminal' },
{ name: 'Browser', icon: 'lucide:globe' },
{ name: 'Camera', icon: 'lucide:camera' },
];
export const demo = () => html`
<style>
.demo-container {
width: 100%;
height: 100%;
background: hsl(240 10% 4%);
border-radius: 12px;
overflow: hidden;
}
</style>
<div class="demo-container">
<eco-view-home
.apps=${mockApps}
@app-click=${(e: CustomEvent) => console.log('App clicked:', e.detail.app)}
></eco-view-home>
</div>
`;

View File

@@ -0,0 +1,157 @@
import {
customElement,
DeesElement,
type TemplateResult,
html,
property,
css,
cssManager,
} from '@design.estate/dees-element';
import { DeesIcon } from '@design.estate/dees-catalog';
// Ensure icon component is registered
DeesIcon;
declare global {
interface HTMLElementTagNameMap {
'eco-view-home': EcoViewHome;
}
}
export interface IAppIcon {
name: string;
icon: string;
action?: () => void;
view?: TemplateResult;
}
@customElement('eco-view-home')
export class EcoViewHome extends DeesElement {
public static styles = [
cssManager.defaultStyles,
css`
:host {
display: block;
width: 100%;
height: 100%;
overflow-y: auto;
}
.apps-area {
padding: 48px;
min-height: 100%;
box-sizing: border-box;
}
.apps-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
gap: 32px;
width: 100%;
}
.app-icon {
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
padding: 16px;
border-radius: 16px;
cursor: pointer;
transition: background 0.2s ease, transform 0.15s ease;
user-select: none;
-webkit-tap-highlight-color: transparent;
}
.app-icon:hover {
background: ${cssManager.bdTheme('hsl(220 15% 92%)', 'hsl(240 5% 12%)')};
}
.app-icon:active {
transform: scale(0.95);
background: ${cssManager.bdTheme('hsl(220 15% 88%)', 'hsl(240 5% 16%)')};
}
.app-icon-circle {
width: 64px;
height: 64px;
border-radius: 16px;
background: ${cssManager.bdTheme('hsl(220 15% 90%)', 'hsl(240 5% 15%)')};
display: flex;
align-items: center;
justify-content: center;
color: ${cssManager.bdTheme('hsl(0 0% 40%)', 'hsl(0 0% 80%)')};
}
.app-icon-circle dees-icon {
--dees-icon-size: 28px;
}
.app-icon-name {
font-size: 13px;
font-weight: 500;
color: ${cssManager.bdTheme('hsl(0 0% 25%)', 'hsl(0 0% 85%)')};
text-align: center;
max-width: 90px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
@media (max-width: 600px) {
.apps-area {
padding: 24px;
}
.apps-grid {
grid-template-columns: repeat(auto-fill, minmax(80px, 1fr));
gap: 16px;
}
.app-icon-circle {
width: 56px;
height: 56px;
font-size: 24px;
}
.app-icon-name {
font-size: 12px;
}
}
`,
];
@property({ type: Array })
accessor apps: IAppIcon[] = [];
public render(): TemplateResult {
return html`
<div class="apps-area">
<div class="apps-grid">
${this.apps.map((app) => this.renderAppIcon(app))}
</div>
</div>
`;
}
private renderAppIcon(app: IAppIcon): TemplateResult {
return html`
<div class="app-icon" @click=${() => this.handleAppClick(app)}>
<div class="app-icon-circle">
<dees-icon .icon=${app.icon} .iconSize=${28}></dees-icon>
</div>
<span class="app-icon-name">${app.name}</span>
</div>
`;
}
private handleAppClick(app: IAppIcon): void {
this.dispatchEvent(
new CustomEvent('app-click', {
detail: { app },
bubbles: true,
composed: true,
})
);
}
}

View File

@@ -0,0 +1 @@
export * from './eco-view-home.js';

View File

@@ -0,0 +1,48 @@
import { html } from '@design.estate/dees-element';
import type { ILoginConfig, ILoginCredentials } from './eco-view-login.js';
const handleLoginAttempt = (e: CustomEvent<ILoginCredentials>) => {
const { method, value } = e.detail;
console.log(`Login attempt via ${method}:`, value);
// Demo: Show success for PIN "1234" or password "demo"
const loginView = e.target as HTMLElement & { showErrorMessage: (msg: string) => void; clearInput: () => void };
if ((method === 'pin' && value === '1234') || (method === 'password' && value === 'demo')) {
console.log('Login successful!');
alert('Login successful! (Demo)');
loginView.clearInput();
} else {
loginView.showErrorMessage('Invalid credentials. Try PIN: 1234 or Password: demo');
}
};
const pinOnlyConfig: ILoginConfig = {
allowedMethods: ['pin'],
pinLength: 4,
welcomeMessage: 'Enter PIN',
};
const allMethodsConfig: ILoginConfig = {
allowedMethods: ['pin', 'password', 'qr'],
pinLength: 6,
welcomeMessage: 'Sign In',
};
export const demo = () => html`
<style>
.demo-container {
width: 100%;
height: 100%;
background: hsl(240 10% 4%);
border-radius: 12px;
overflow: hidden;
}
</style>
<div class="demo-container">
<eco-view-login
.config=${allMethodsConfig}
@login-attempt=${handleLoginAttempt}
></eco-view-login>
</div>
`;

View File

@@ -0,0 +1,749 @@
import {
customElement,
DeesElement,
type TemplateResult,
html,
property,
css,
cssManager,
state,
} from '@design.estate/dees-element';
import { DeesIcon } from '@design.estate/dees-catalog';
// Ensure icon component is registered
DeesIcon;
declare global {
interface HTMLElementTagNameMap {
'eco-view-login': EcoViewLogin;
}
}
export type TAuthMethod = 'pin' | 'password' | 'qr';
export interface ILoginConfig {
allowedMethods: TAuthMethod[];
pinLength?: number;
qrCodeData?: string;
logoUrl?: string;
welcomeMessage?: string;
subtitle?: string;
}
export interface ILoginCredentials {
method: TAuthMethod;
value: string;
}
@customElement('eco-view-login')
export class EcoViewLogin extends DeesElement {
public static styles = [
cssManager.defaultStyles,
css`
:host {
display: flex;
width: 100%;
height: 100%;
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
}
.login-container {
display: flex;
width: 100%;
height: 100%;
}
/* Left Panel - Branding & Method Selection */
.left-panel {
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
padding: 64px;
background: ${cssManager.bdTheme('hsl(220 15% 96%)', 'hsl(240 6% 10%)')};
border-right: 1px solid ${cssManager.bdTheme('hsl(220 15% 90%)', 'hsl(240 5% 16%)')};
}
.branding {
margin-bottom: 48px;
}
.logo {
width: 72px;
height: 72px;
border-radius: 18px;
background: ${cssManager.bdTheme('hsl(217 91% 60%)', 'hsl(217 91% 50%)')};
display: flex;
align-items: center;
justify-content: center;
color: white;
margin-bottom: 24px;
}
.logo img {
width: 100%;
height: 100%;
object-fit: contain;
border-radius: 18px;
}
.welcome-message {
font-size: 32px;
font-weight: 700;
color: ${cssManager.bdTheme('hsl(0 0% 10%)', 'hsl(0 0% 95%)')};
margin-bottom: 8px;
line-height: 1.2;
}
.subtitle {
font-size: 16px;
color: ${cssManager.bdTheme('hsl(0 0% 45%)', 'hsl(0 0% 55%)')};
line-height: 1.5;
}
.method-selector {
display: flex;
flex-direction: column;
gap: 12px;
}
.method-selector-label {
font-size: 13px;
font-weight: 600;
color: ${cssManager.bdTheme('hsl(0 0% 40%)', 'hsl(0 0% 60%)')};
text-transform: uppercase;
letter-spacing: 0.5px;
margin-bottom: 4px;
}
.method-option {
display: flex;
align-items: center;
gap: 16px;
padding: 16px 20px;
background: ${cssManager.bdTheme('white', 'hsl(240 5% 14%)')};
border: 2px solid ${cssManager.bdTheme('hsl(220 15% 90%)', 'hsl(240 5% 20%)')};
border-radius: 12px;
cursor: pointer;
transition: all 0.2s ease;
}
.method-option:hover {
border-color: ${cssManager.bdTheme('hsl(220 15% 80%)', 'hsl(240 5% 28%)')};
background: ${cssManager.bdTheme('hsl(220 15% 98%)', 'hsl(240 5% 16%)')};
}
.method-option.active {
border-color: ${cssManager.bdTheme('hsl(217 91% 60%)', 'hsl(217 91% 50%)')};
background: ${cssManager.bdTheme('hsl(217 91% 97%)', 'hsl(217 91% 15%)')};
}
.method-icon {
width: 44px;
height: 44px;
border-radius: 10px;
background: ${cssManager.bdTheme('hsl(220 15% 94%)', 'hsl(240 5% 20%)')};
display: flex;
align-items: center;
justify-content: center;
color: ${cssManager.bdTheme('hsl(0 0% 40%)', 'hsl(0 0% 70%)')};
transition: all 0.2s ease;
}
.method-option.active .method-icon {
background: ${cssManager.bdTheme('hsl(217 91% 60%)', 'hsl(217 91% 50%)')};
color: white;
}
.method-info {
flex: 1;
}
.method-name {
font-size: 15px;
font-weight: 600;
color: ${cssManager.bdTheme('hsl(0 0% 20%)', 'hsl(0 0% 90%)')};
margin-bottom: 2px;
}
.method-description {
font-size: 13px;
color: ${cssManager.bdTheme('hsl(0 0% 50%)', 'hsl(0 0% 55%)')};
}
.method-check {
width: 24px;
height: 24px;
border-radius: 50%;
border: 2px solid ${cssManager.bdTheme('hsl(220 15% 85%)', 'hsl(240 5% 25%)')};
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
}
.method-option.active .method-check {
background: ${cssManager.bdTheme('hsl(217 91% 60%)', 'hsl(217 91% 50%)')};
border-color: ${cssManager.bdTheme('hsl(217 91% 60%)', 'hsl(217 91% 50%)')};
color: white;
}
/* Right Panel - Auth Input */
.right-panel {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 64px;
background: ${cssManager.bdTheme('white', 'hsl(240 6% 6%)')};
}
.auth-content {
width: 100%;
max-width: 320px;
display: flex;
flex-direction: column;
align-items: center;
gap: 32px;
}
.auth-title {
font-size: 20px;
font-weight: 600;
color: ${cssManager.bdTheme('hsl(0 0% 20%)', 'hsl(0 0% 90%)')};
text-align: center;
}
/* Error message */
.error-message {
color: hsl(0 72% 51%);
font-size: 14px;
text-align: center;
padding: 12px 16px;
background: hsla(0, 72%, 51%, 0.1);
border-radius: 8px;
width: 100%;
box-sizing: border-box;
}
/* PIN Input */
.pin-display {
display: flex;
gap: 16px;
}
.pin-dot {
width: 18px;
height: 18px;
border-radius: 50%;
background: ${cssManager.bdTheme('hsl(220 15% 85%)', 'hsl(240 5% 20%)')};
transition: all 0.15s ease;
}
.pin-dot.filled {
background: ${cssManager.bdTheme('hsl(217 91% 60%)', 'hsl(217 91% 50%)')};
transform: scale(1.15);
}
.pin-dot.error {
background: hsl(0 72% 51%);
animation: shake 0.3s ease;
}
@keyframes shake {
0%, 100% { transform: translateX(0); }
25% { transform: translateX(-4px); }
75% { transform: translateX(4px); }
}
.numpad {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 16px;
}
.numpad-button {
width: 76px;
height: 76px;
border-radius: 50%;
background: ${cssManager.bdTheme('hsl(220 15% 95%)', 'hsl(240 5% 14%)')};
border: none;
font-size: 28px;
font-weight: 500;
color: ${cssManager.bdTheme('hsl(0 0% 20%)', 'hsl(0 0% 85%)')};
cursor: pointer;
transition: all 0.15s ease;
display: flex;
align-items: center;
justify-content: center;
}
.numpad-button:hover {
background: ${cssManager.bdTheme('hsl(220 15% 90%)', 'hsl(240 5% 20%)')};
}
.numpad-button:active {
transform: scale(0.95);
background: ${cssManager.bdTheme('hsl(220 15% 85%)', 'hsl(240 5% 24%)')};
}
.numpad-button.action {
background: transparent;
font-size: 18px;
}
.numpad-button.action:hover {
background: ${cssManager.bdTheme('hsl(220 15% 95%)', 'hsl(240 5% 14%)')};
}
.numpad-button.submit {
background: ${cssManager.bdTheme('hsl(217 91% 60%)', 'hsl(217 91% 50%)')};
color: white;
}
.numpad-button.submit:hover {
background: ${cssManager.bdTheme('hsl(217 91% 55%)', 'hsl(217 91% 45%)')};
}
/* Password Input */
.password-form {
width: 100%;
display: flex;
flex-direction: column;
gap: 20px;
}
.password-input-wrapper {
width: 100%;
position: relative;
}
.password-input {
width: 100%;
padding: 18px 52px 18px 18px;
font-size: 16px;
background: ${cssManager.bdTheme('hsl(220 15% 96%)', 'hsl(240 5% 12%)')};
border: 2px solid ${cssManager.bdTheme('hsl(220 15% 88%)', 'hsl(240 5% 20%)')};
border-radius: 12px;
color: ${cssManager.bdTheme('hsl(0 0% 20%)', 'hsl(0 0% 90%)')};
outline: none;
box-sizing: border-box;
transition: border-color 0.2s ease;
}
.password-input:focus {
border-color: ${cssManager.bdTheme('hsl(217 91% 60%)', 'hsl(217 91% 50%)')};
}
.password-input.error {
border-color: hsl(0 72% 51%);
animation: shake 0.3s ease;
}
.password-toggle {
position: absolute;
right: 14px;
top: 50%;
transform: translateY(-50%);
background: none;
border: none;
cursor: pointer;
color: ${cssManager.bdTheme('hsl(0 0% 50%)', 'hsl(0 0% 55%)')};
padding: 6px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 6px;
transition: all 0.15s ease;
}
.password-toggle:hover {
background: ${cssManager.bdTheme('hsl(220 15% 90%)', 'hsl(240 5% 18%)')};
color: ${cssManager.bdTheme('hsl(0 0% 30%)', 'hsl(0 0% 80%)')};
}
.submit-button {
width: 100%;
padding: 18px;
font-size: 16px;
font-weight: 600;
background: ${cssManager.bdTheme('hsl(217 91% 60%)', 'hsl(217 91% 50%)')};
color: white;
border: none;
border-radius: 12px;
cursor: pointer;
transition: all 0.2s ease;
}
.submit-button:hover {
background: ${cssManager.bdTheme('hsl(217 91% 55%)', 'hsl(217 91% 45%)')};
}
.submit-button:active {
transform: scale(0.98);
}
/* QR Code */
.qr-container {
display: flex;
flex-direction: column;
align-items: center;
gap: 24px;
}
.qr-code {
width: 220px;
height: 220px;
background: white;
border-radius: 16px;
padding: 20px;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.1);
}
.qr-code img {
width: 100%;
height: 100%;
object-fit: contain;
}
.qr-placeholder {
width: 100%;
height: 100%;
background: repeating-linear-gradient(
45deg,
hsl(0 0% 92%),
hsl(0 0% 92%) 10px,
hsl(0 0% 88%) 10px,
hsl(0 0% 88%) 20px
);
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
color: hsl(0 0% 50%);
font-size: 14px;
}
.qr-instruction {
font-size: 14px;
color: ${cssManager.bdTheme('hsl(0 0% 50%)', 'hsl(0 0% 55%)')};
text-align: center;
line-height: 1.6;
max-width: 280px;
}
/* Responsive */
@media (max-width: 800px) {
.login-container {
flex-direction: column;
}
.left-panel {
padding: 32px;
border-right: none;
border-bottom: 1px solid ${cssManager.bdTheme('hsl(220 15% 90%)', 'hsl(240 5% 16%)')};
}
.right-panel {
padding: 32px;
}
.branding {
margin-bottom: 32px;
}
.welcome-message {
font-size: 24px;
}
.method-selector {
flex-direction: row;
flex-wrap: wrap;
}
.method-option {
flex: 1;
min-width: 140px;
flex-direction: column;
text-align: center;
padding: 12px;
}
.method-info {
text-align: center;
}
.method-check {
display: none;
}
}
`,
];
@property({ type: Object })
accessor config: ILoginConfig = {
allowedMethods: ['pin', 'password', 'qr'],
pinLength: 4,
welcomeMessage: 'Welcome',
subtitle: 'Sign in to continue',
};
@state()
accessor selectedMethod: TAuthMethod = 'pin';
@state()
accessor pinValue = '';
@state()
accessor passwordValue = '';
@state()
accessor showPassword = false;
@state()
accessor error = '';
@state()
accessor showError = false;
public render(): TemplateResult {
const effectivePinLength = this.config.pinLength || 4;
return html`
<div class="login-container">
<div class="left-panel">
<div class="branding">
${this.config.logoUrl
? html`<div class="logo"><img src=${this.config.logoUrl} alt="Logo" /></div>`
: html`<div class="logo"><dees-icon .icon=${'lucide:shield'} .iconSize=${32}></dees-icon></div>`
}
<h1 class="welcome-message">${this.config.welcomeMessage || 'Welcome'}</h1>
<p class="subtitle">${this.config.subtitle || 'Sign in to continue'}</p>
</div>
${this.config.allowedMethods.length > 1 ? this.renderMethodSelector() : ''}
</div>
<div class="right-panel">
<div class="auth-content">
<h2 class="auth-title">${this.getAuthTitle()}</h2>
${this.showError ? html`<div class="error-message">${this.error}</div>` : ''}
${this.selectedMethod === 'pin' ? this.renderPinInput(effectivePinLength) : ''}
${this.selectedMethod === 'password' ? this.renderPasswordInput() : ''}
${this.selectedMethod === 'qr' ? this.renderQrCode() : ''}
</div>
</div>
</div>
`;
}
private getAuthTitle(): string {
switch (this.selectedMethod) {
case 'pin':
return 'Enter your PIN';
case 'password':
return 'Enter your password';
case 'qr':
return 'Scan to sign in';
default:
return 'Sign in';
}
}
private renderMethodSelector(): TemplateResult {
const methods: Array<{ id: TAuthMethod; icon: string; name: string; description: string }> = [
{ id: 'pin', icon: 'lucide:keySquare', name: 'PIN Code', description: 'Quick numeric access' },
{ id: 'password', icon: 'lucide:key', name: 'Password', description: 'Traditional password' },
{ id: 'qr', icon: 'lucide:qrCode', name: 'QR Code', description: 'Scan with mobile app' },
];
const availableMethods = methods.filter((m) => this.config.allowedMethods.includes(m.id));
return html`
<div class="method-selector">
<span class="method-selector-label">Sign in method</span>
${availableMethods.map((method) => html`
<div
class="method-option ${this.selectedMethod === method.id ? 'active' : ''}"
@click=${() => this.selectMethod(method.id)}
>
<div class="method-icon">
<dees-icon .icon=${method.icon} .iconSize=${22}></dees-icon>
</div>
<div class="method-info">
<div class="method-name">${method.name}</div>
<div class="method-description">${method.description}</div>
</div>
<div class="method-check">
${this.selectedMethod === method.id
? html`<dees-icon .icon=${'lucide:check'} .iconSize=${14}></dees-icon>`
: ''
}
</div>
</div>
`)}
</div>
`;
}
private renderPinInput(length: number): TemplateResult {
return html`
<div class="pin-display">
${Array.from({ length }, (_, i) => html`
<div class="pin-dot ${i < this.pinValue.length ? 'filled' : ''} ${this.showError ? 'error' : ''}"></div>
`)}
</div>
<div class="numpad">
${[1, 2, 3, 4, 5, 6, 7, 8, 9].map((num) => html`
<button class="numpad-button" @click=${() => this.handlePinInput(String(num))}>${num}</button>
`)}
<button class="numpad-button action" @click=${this.handleBackspace}>
<dees-icon .icon=${'lucide:delete'} .iconSize=${24}></dees-icon>
</button>
<button class="numpad-button" @click=${() => this.handlePinInput('0')}>0</button>
<button class="numpad-button action submit" @click=${this.handlePinSubmit}>
<dees-icon .icon=${'lucide:arrowRight'} .iconSize=${24}></dees-icon>
</button>
</div>
`;
}
private renderPasswordInput(): TemplateResult {
return html`
<div class="password-form">
<div class="password-input-wrapper">
<input
class="password-input ${this.showError ? 'error' : ''}"
type=${this.showPassword ? 'text' : 'password'}
placeholder="Enter your password"
.value=${this.passwordValue}
@input=${this.handlePasswordInput}
@keydown=${this.handlePasswordKeydown}
/>
<button class="password-toggle" @click=${this.togglePasswordVisibility}>
<dees-icon
.icon=${this.showPassword ? 'lucide:eyeOff' : 'lucide:eye'}
.iconSize=${20}
></dees-icon>
</button>
</div>
<button class="submit-button" @click=${this.handlePasswordSubmit}>
Sign In
</button>
</div>
`;
}
private renderQrCode(): TemplateResult {
return html`
<div class="qr-container">
<div class="qr-code">
${this.config.qrCodeData
? html`<img src=${this.config.qrCodeData} alt="Login QR Code" />`
: html`<div class="qr-placeholder">QR Code</div>`
}
</div>
<p class="qr-instruction">
Open your authenticator app and scan this code to sign in securely without typing a password.
</p>
</div>
`;
}
private selectMethod(method: TAuthMethod): void {
this.selectedMethod = method;
this.clearError();
this.pinValue = '';
this.passwordValue = '';
}
private handlePinInput(digit: string): void {
this.clearError();
const maxLength = this.config.pinLength || 4;
if (this.pinValue.length < maxLength) {
this.pinValue += digit;
this.dispatchKeyPress(digit);
}
}
private handleBackspace(): void {
this.clearError();
if (this.pinValue.length > 0) {
this.pinValue = this.pinValue.slice(0, -1);
this.dispatchEvent(new CustomEvent('backspace', {
bubbles: true,
composed: true,
}));
}
}
private handlePinSubmit(): void {
if (this.pinValue.length === 0) {
this.showErrorMessage('Please enter your PIN');
return;
}
this.dispatchLoginAttempt('pin', this.pinValue);
}
private handlePasswordInput(e: InputEvent): void {
this.clearError();
const input = e.target as HTMLInputElement;
this.passwordValue = input.value;
}
private handlePasswordKeydown(e: KeyboardEvent): void {
if (e.key === 'Enter') {
this.handlePasswordSubmit();
}
}
private handlePasswordSubmit(): void {
if (this.passwordValue.length === 0) {
this.showErrorMessage('Please enter your password');
return;
}
this.dispatchLoginAttempt('password', this.passwordValue);
}
private togglePasswordVisibility(): void {
this.showPassword = !this.showPassword;
}
private dispatchKeyPress(key: string): void {
this.dispatchEvent(new CustomEvent('key-press', {
detail: { key },
bubbles: true,
composed: true,
}));
}
private dispatchLoginAttempt(method: TAuthMethod, value: string): void {
this.dispatchEvent(new CustomEvent('login-attempt', {
detail: { method, value } as ILoginCredentials,
bubbles: true,
composed: true,
}));
}
public showErrorMessage(message: string): void {
this.error = message;
this.showError = true;
}
public clearError(): void {
this.error = '';
this.showError = false;
}
public clearInput(): void {
this.pinValue = '';
this.passwordValue = '';
}
}

View File

@@ -0,0 +1 @@
export * from './eco-view-login.js';

View File

@@ -0,0 +1,21 @@
import { html } from '@design.estate/dees-element';
export const demo = () => html`
<style>
.demo-container {
width: 100%;
height: 100%;
background: hsl(240 10% 4%);
border-radius: 12px;
overflow: hidden;
}
</style>
<div class="demo-container">
<eco-view-peripherals
.activeCategory=${'all'}
@device-select=${(e: CustomEvent) => console.log('Device selected:', e.detail)}
@scan-start=${() => console.log('Scanning started')}
@scan-complete=${() => console.log('Scanning complete')}
></eco-view-peripherals>
</div>
`;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1 @@
export * from './eco-view-peripherals.js';

View File

@@ -0,0 +1,20 @@
import { html } from '@design.estate/dees-element';
export const demo = () => html`
<style>
.demo-container {
width: 100%;
height: 100%;
background: hsl(240 10% 4%);
border-radius: 12px;
overflow: hidden;
}
</style>
<div class="demo-container">
<eco-view-saasshare
.activePanel=${'apps'}
@request-approved=${(e: CustomEvent) => console.log('Request approved:', e.detail)}
@request-denied=${(e: CustomEvent) => console.log('Request denied:', e.detail)}
></eco-view-saasshare>
</div>
`;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1 @@
export * from './eco-view-saasshare.js';

View File

@@ -0,0 +1,18 @@
import { html } from '@design.estate/dees-element';
export const demo = () => html`
<style>
.demo-container {
width: 100%;
height: 100%;
background: hsl(240 10% 4%);
border-radius: 12px;
overflow: hidden;
}
</style>
<div class="demo-container">
<eco-view-settings
.activePanel=${'general'}
></eco-view-settings>
</div>
`;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1 @@
export * from './eco-view-settings.js';

View File

@@ -0,0 +1,18 @@
import { html } from '@design.estate/dees-element';
export const demo = () => html`
<style>
.demo-container {
width: 100%;
height: 100%;
background: hsl(240 10% 4%);
border-radius: 12px;
overflow: hidden;
}
</style>
<div class="demo-container">
<eco-view-system
.activePanel=${'overview'}
></eco-view-system>
</div>
`;

View File

@@ -0,0 +1,877 @@
import {
customElement,
DeesElement,
type TemplateResult,
html,
property,
css,
cssManager,
state,
} from '@design.estate/dees-element';
import { DeesAppuiSecondarymenu, DeesIcon, DeesStatsGrid } from '@design.estate/dees-catalog';
import type { ISecondaryMenuGroup, ISecondaryMenuItem } from '../../elements/interfaces/secondarymenu.js';
import { demo } from './eco-view-system.demo.js';
// Ensure components are registered
DeesAppuiSecondarymenu;
DeesIcon;
DeesStatsGrid;
declare global {
interface HTMLElementTagNameMap {
'eco-view-system': EcoViewSystem;
}
}
export type TSystemPanel =
| 'overview'
| 'cpu'
| 'memory'
| 'storage'
| 'network'
| 'processes';
@customElement('eco-view-system')
export class EcoViewSystem extends DeesElement {
public static demo = demo;
public static demoGroup = 'Views';
public static styles = [
cssManager.defaultStyles,
css`
:host {
display: block;
width: 100%;
height: 100%;
background: ${cssManager.bdTheme('#f5f5f7', 'hsl(240 6% 10%)')};
color: ${cssManager.bdTheme('hsl(0 0% 10%)', 'hsl(0 0% 98%)')};
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
}
.system-container {
display: flex;
height: 100%;
}
dees-appui-secondarymenu {
flex-shrink: 0;
background: ${cssManager.bdTheme('#ffffff', 'hsl(240 6% 8%)')};
border-right: 1px solid ${cssManager.bdTheme('hsl(0 0% 90%)', 'hsl(240 5% 15%)')};
}
.content {
flex: 1;
overflow-y: auto;
padding: 32px 48px;
}
.panel-header {
margin-bottom: 32px;
}
.panel-title {
font-size: 28px;
font-weight: 600;
color: ${cssManager.bdTheme('hsl(0 0% 10%)', 'hsl(0 0% 98%)')};
margin-bottom: 8px;
}
.panel-description {
font-size: 14px;
color: ${cssManager.bdTheme('hsl(0 0% 50%)', 'hsl(0 0% 60%)')};
}
.stats-section {
margin-bottom: 32px;
}
.section-title {
font-size: 13px;
font-weight: 600;
color: ${cssManager.bdTheme('hsl(0 0% 50%)', 'hsl(0 0% 50%)')};
text-transform: uppercase;
letter-spacing: 0.5px;
margin-bottom: 16px;
}
dees-statsgrid {
--dees-statsgrid-gap: 16px;
}
.process-list {
background: ${cssManager.bdTheme('#ffffff', 'hsl(240 6% 12%)')};
border: 1px solid ${cssManager.bdTheme('hsl(0 0% 90%)', 'hsl(240 5% 18%)')};
border-radius: 12px;
overflow: hidden;
}
.process-header {
display: grid;
grid-template-columns: 2fr 1fr 1fr 1fr;
padding: 12px 16px;
background: ${cssManager.bdTheme('hsl(0 0% 97%)', 'hsl(240 5% 14%)')};
border-bottom: 1px solid ${cssManager.bdTheme('hsl(0 0% 90%)', 'hsl(240 5% 18%)')};
font-size: 12px;
font-weight: 600;
color: ${cssManager.bdTheme('hsl(0 0% 50%)', 'hsl(0 0% 55%)')};
text-transform: uppercase;
letter-spacing: 0.5px;
}
.process-row {
display: grid;
grid-template-columns: 2fr 1fr 1fr 1fr;
padding: 12px 16px;
border-bottom: 1px solid ${cssManager.bdTheme('hsl(0 0% 94%)', 'hsl(240 5% 15%)')};
font-size: 14px;
color: ${cssManager.bdTheme('hsl(0 0% 20%)', 'hsl(0 0% 85%)')};
}
.process-row:last-child {
border-bottom: none;
}
.process-name {
font-weight: 500;
}
.process-value {
color: ${cssManager.bdTheme('hsl(0 0% 40%)', 'hsl(0 0% 65%)')};
}
.process-value.high {
color: hsl(0 84% 60%);
font-weight: 500;
}
`,
];
@property({ type: String })
accessor activePanel: TSystemPanel = 'overview';
// Mock system data
@state()
accessor cpuUsage = 42;
@state()
accessor memoryUsage = 67;
@state()
accessor diskUsage = 54;
@state()
accessor cpuTemp = 58;
@state()
accessor uptime = '14d 7h 32m';
@state()
accessor networkIn = [45, 52, 38, 65, 72, 68, 75, 82, 79, 85, 88, 72];
@state()
accessor networkOut = [32, 28, 35, 42, 38, 45, 52, 48, 55, 62, 58, 65];
private getMenuGroups(): ISecondaryMenuGroup[] {
return [
{
name: 'Monitor',
iconName: 'lucide:activity',
items: [
{
key: 'overview',
iconName: 'lucide:layoutDashboard',
action: () => this.activePanel = 'overview',
},
],
},
{
name: 'Hardware',
iconName: 'lucide:cpu',
items: [
{
key: 'cpu',
iconName: 'lucide:cpu',
action: () => this.activePanel = 'cpu',
},
{
key: 'memory',
iconName: 'lucide:memoryStick',
action: () => this.activePanel = 'memory',
},
{
key: 'storage',
iconName: 'lucide:hardDrive',
action: () => this.activePanel = 'storage',
},
],
},
{
name: 'Network',
iconName: 'lucide:network',
items: [
{
key: 'network',
iconName: 'lucide:wifi',
action: () => this.activePanel = 'network',
},
],
},
{
name: 'Software',
iconName: 'lucide:layers',
items: [
{
key: 'processes',
iconName: 'lucide:listTree',
action: () => this.activePanel = 'processes',
},
],
},
];
}
private getSelectedItem(): ISecondaryMenuItem | null {
for (const group of this.getMenuGroups()) {
for (const item of group.items) {
if ('key' in item && item.key === this.activePanel) {
return item;
}
}
}
return null;
}
public render(): TemplateResult {
return html`
<div class="system-container">
<dees-appui-secondarymenu
.heading=${'System'}
.groups=${this.getMenuGroups()}
.selectedItem=${this.getSelectedItem()}
></dees-appui-secondarymenu>
<div class="content">
${this.renderActivePanel()}
</div>
</div>
`;
}
private renderActivePanel(): TemplateResult {
switch (this.activePanel) {
case 'overview':
return this.renderOverviewPanel();
case 'cpu':
return this.renderCpuPanel();
case 'memory':
return this.renderMemoryPanel();
case 'storage':
return this.renderStoragePanel();
case 'network':
return this.renderNetworkPanel();
case 'processes':
return this.renderProcessesPanel();
default:
return this.renderOverviewPanel();
}
}
private renderOverviewPanel(): TemplateResult {
const overviewTiles = [
{
id: 'cpu',
title: 'CPU Usage',
value: this.cpuUsage,
type: 'gauge' as const,
icon: 'lucide:cpu',
gaugeOptions: {
min: 0,
max: 100,
thresholds: [
{ value: 0, color: 'hsl(142 71% 45%)' },
{ value: 60, color: 'hsl(45 93% 47%)' },
{ value: 80, color: 'hsl(0 84% 60%)' },
],
},
},
{
id: 'memory',
title: 'Memory Usage',
value: this.memoryUsage,
type: 'gauge' as const,
icon: 'lucide:memoryStick',
gaugeOptions: {
min: 0,
max: 100,
thresholds: [
{ value: 0, color: 'hsl(142 71% 45%)' },
{ value: 70, color: 'hsl(45 93% 47%)' },
{ value: 85, color: 'hsl(0 84% 60%)' },
],
},
},
{
id: 'disk',
title: 'Disk Usage',
value: this.diskUsage,
type: 'gauge' as const,
icon: 'lucide:hardDrive',
gaugeOptions: {
min: 0,
max: 100,
thresholds: [
{ value: 0, color: 'hsl(142 71% 45%)' },
{ value: 75, color: 'hsl(45 93% 47%)' },
{ value: 90, color: 'hsl(0 84% 60%)' },
],
},
},
{
id: 'temp',
title: 'CPU Temp',
value: this.cpuTemp,
unit: '°C',
type: 'gauge' as const,
icon: 'lucide:thermometer',
gaugeOptions: {
min: 0,
max: 100,
thresholds: [
{ value: 0, color: 'hsl(217 91% 60%)' },
{ value: 50, color: 'hsl(142 71% 45%)' },
{ value: 70, color: 'hsl(45 93% 47%)' },
{ value: 85, color: 'hsl(0 84% 60%)' },
],
},
},
{
id: 'network-in',
title: 'Network In',
value: '85',
unit: 'Mbps',
type: 'trend' as const,
icon: 'lucide:download',
trendData: this.networkIn,
color: 'hsl(142 71% 45%)',
},
{
id: 'network-out',
title: 'Network Out',
value: '65',
unit: 'Mbps',
type: 'trend' as const,
icon: 'lucide:upload',
trendData: this.networkOut,
color: 'hsl(217 91% 60%)',
},
{
id: 'uptime',
title: 'System Uptime',
value: this.uptime,
type: 'text' as const,
icon: 'lucide:clock',
color: 'hsl(142 71% 45%)',
description: 'Since last reboot',
},
{
id: 'processes',
title: 'Processes',
value: 247,
type: 'number' as const,
icon: 'lucide:layers',
description: '12 running, 235 sleeping',
},
];
return html`
<div class="panel-header">
<div class="panel-title">System Overview</div>
<div class="panel-description">Real-time system performance metrics</div>
</div>
<div class="stats-section">
<dees-statsgrid
.tiles=${overviewTiles}
.minTileWidth=${220}
.gap=${16}
></dees-statsgrid>
</div>
`;
}
private renderCpuPanel(): TemplateResult {
const cpuTiles = [
{
id: 'cpu-total',
title: 'Total CPU Usage',
value: this.cpuUsage,
type: 'gauge' as const,
icon: 'lucide:cpu',
gaugeOptions: {
min: 0,
max: 100,
thresholds: [
{ value: 0, color: 'hsl(142 71% 45%)' },
{ value: 60, color: 'hsl(45 93% 47%)' },
{ value: 80, color: 'hsl(0 84% 60%)' },
],
},
},
{
id: 'core-0',
title: 'Core 0',
value: 38,
type: 'gauge' as const,
gaugeOptions: {
min: 0,
max: 100,
thresholds: [
{ value: 0, color: 'hsl(142 71% 45%)' },
{ value: 60, color: 'hsl(45 93% 47%)' },
{ value: 80, color: 'hsl(0 84% 60%)' },
],
},
},
{
id: 'core-1',
title: 'Core 1',
value: 52,
type: 'gauge' as const,
gaugeOptions: {
min: 0,
max: 100,
thresholds: [
{ value: 0, color: 'hsl(142 71% 45%)' },
{ value: 60, color: 'hsl(45 93% 47%)' },
{ value: 80, color: 'hsl(0 84% 60%)' },
],
},
},
{
id: 'core-2',
title: 'Core 2',
value: 45,
type: 'gauge' as const,
gaugeOptions: {
min: 0,
max: 100,
thresholds: [
{ value: 0, color: 'hsl(142 71% 45%)' },
{ value: 60, color: 'hsl(45 93% 47%)' },
{ value: 80, color: 'hsl(0 84% 60%)' },
],
},
},
{
id: 'core-3',
title: 'Core 3',
value: 33,
type: 'gauge' as const,
gaugeOptions: {
min: 0,
max: 100,
thresholds: [
{ value: 0, color: 'hsl(142 71% 45%)' },
{ value: 60, color: 'hsl(45 93% 47%)' },
{ value: 80, color: 'hsl(0 84% 60%)' },
],
},
},
{
id: 'load-avg',
title: 'Load Average',
value: '2.45',
type: 'trend' as const,
icon: 'lucide:activity',
trendData: [1.8, 2.1, 2.4, 2.2, 2.5, 2.3, 2.6, 2.4, 2.45],
description: '1m: 2.45, 5m: 2.32, 15m: 2.18',
},
{
id: 'cpu-temp',
title: 'Temperature',
value: this.cpuTemp,
unit: '°C',
type: 'gauge' as const,
icon: 'lucide:thermometer',
gaugeOptions: {
min: 0,
max: 100,
thresholds: [
{ value: 0, color: 'hsl(217 91% 60%)' },
{ value: 50, color: 'hsl(142 71% 45%)' },
{ value: 70, color: 'hsl(45 93% 47%)' },
{ value: 85, color: 'hsl(0 84% 60%)' },
],
},
},
{
id: 'freq',
title: 'Clock Speed',
value: '3.2',
unit: 'GHz',
type: 'number' as const,
icon: 'lucide:gauge',
description: 'Max: 4.2 GHz',
},
];
return html`
<div class="panel-header">
<div class="panel-title">CPU</div>
<div class="panel-description">Processor usage and performance</div>
</div>
<div class="stats-section">
<dees-statsgrid
.tiles=${cpuTiles}
.minTileWidth=${200}
.gap=${16}
></dees-statsgrid>
</div>
`;
}
private renderMemoryPanel(): TemplateResult {
const memoryTiles = [
{
id: 'ram-usage',
title: 'RAM Usage',
value: this.memoryUsage,
type: 'gauge' as const,
icon: 'lucide:memoryStick',
gaugeOptions: {
min: 0,
max: 100,
thresholds: [
{ value: 0, color: 'hsl(142 71% 45%)' },
{ value: 70, color: 'hsl(45 93% 47%)' },
{ value: 85, color: 'hsl(0 84% 60%)' },
],
},
description: '10.7 GB of 16 GB',
},
{
id: 'swap-usage',
title: 'Swap Usage',
value: 12,
type: 'gauge' as const,
icon: 'lucide:hardDrive',
gaugeOptions: {
min: 0,
max: 100,
thresholds: [
{ value: 0, color: 'hsl(142 71% 45%)' },
{ value: 50, color: 'hsl(45 93% 47%)' },
{ value: 75, color: 'hsl(0 84% 60%)' },
],
},
description: '0.5 GB of 4 GB',
},
{
id: 'mem-trend',
title: 'Memory History',
value: '67%',
type: 'trend' as const,
icon: 'lucide:trendingUp',
trendData: [58, 62, 65, 63, 68, 72, 70, 65, 67],
description: 'Last hour',
},
{
id: 'cached',
title: 'Cached',
value: '3.2',
unit: 'GB',
type: 'number' as const,
icon: 'lucide:database',
color: 'hsl(217 91% 60%)',
},
{
id: 'buffers',
title: 'Buffers',
value: '512',
unit: 'MB',
type: 'number' as const,
icon: 'lucide:layers',
color: 'hsl(262 83% 58%)',
},
{
id: 'available',
title: 'Available',
value: '5.3',
unit: 'GB',
type: 'number' as const,
icon: 'lucide:checkCircle',
color: 'hsl(142 71% 45%)',
},
];
return html`
<div class="panel-header">
<div class="panel-title">Memory</div>
<div class="panel-description">RAM and swap usage details</div>
</div>
<div class="stats-section">
<dees-statsgrid
.tiles=${memoryTiles}
.minTileWidth=${220}
.gap=${16}
></dees-statsgrid>
</div>
`;
}
private renderStoragePanel(): TemplateResult {
const storageTiles = [
{
id: 'disk-main',
title: 'System Drive',
value: this.diskUsage,
type: 'percentage' as const,
icon: 'lucide:hardDrive',
description: '275 GB of 512 GB used',
color: 'hsl(217 91% 60%)',
},
{
id: 'disk-data',
title: 'Data Drive',
value: 38,
type: 'percentage' as const,
icon: 'lucide:hardDrive',
description: '380 GB of 1 TB used',
color: 'hsl(142 71% 45%)',
},
{
id: 'read-speed',
title: 'Read Speed',
value: '245',
unit: 'MB/s',
type: 'trend' as const,
icon: 'lucide:download',
trendData: [180, 220, 195, 280, 245, 210, 265, 230, 245],
color: 'hsl(142 71% 45%)',
},
{
id: 'write-speed',
title: 'Write Speed',
value: '128',
unit: 'MB/s',
type: 'trend' as const,
icon: 'lucide:upload',
trendData: [95, 110, 85, 145, 120, 105, 138, 115, 128],
color: 'hsl(217 91% 60%)',
},
{
id: 'iops-read',
title: 'Read IOPS',
value: '12.4k',
type: 'number' as const,
icon: 'lucide:gauge',
description: 'Operations/sec',
},
{
id: 'iops-write',
title: 'Write IOPS',
value: '8.2k',
type: 'number' as const,
icon: 'lucide:gauge',
description: 'Operations/sec',
},
];
return html`
<div class="panel-header">
<div class="panel-title">Storage</div>
<div class="panel-description">Disk usage and I/O performance</div>
</div>
<div class="stats-section">
<dees-statsgrid
.tiles=${storageTiles}
.minTileWidth=${220}
.gap=${16}
></dees-statsgrid>
</div>
`;
}
private renderNetworkPanel(): TemplateResult {
const networkTiles = [
{
id: 'download',
title: 'Download',
value: '85.2',
unit: 'Mbps',
type: 'trend' as const,
icon: 'lucide:download',
trendData: this.networkIn,
color: 'hsl(142 71% 45%)',
},
{
id: 'upload',
title: 'Upload',
value: '64.8',
unit: 'Mbps',
type: 'trend' as const,
icon: 'lucide:upload',
trendData: this.networkOut,
color: 'hsl(217 91% 60%)',
},
{
id: 'latency',
title: 'Latency',
value: 12,
unit: 'ms',
type: 'gauge' as const,
icon: 'lucide:activity',
gaugeOptions: {
min: 0,
max: 100,
thresholds: [
{ value: 0, color: 'hsl(142 71% 45%)' },
{ value: 30, color: 'hsl(45 93% 47%)' },
{ value: 60, color: 'hsl(0 84% 60%)' },
],
},
},
{
id: 'packets-in',
title: 'Packets In',
value: '1.2M',
type: 'number' as const,
icon: 'lucide:arrowDownCircle',
description: 'Per second',
},
{
id: 'packets-out',
title: 'Packets Out',
value: '892k',
type: 'number' as const,
icon: 'lucide:arrowUpCircle',
description: 'Per second',
},
{
id: 'connections',
title: 'Active Connections',
value: 48,
type: 'number' as const,
icon: 'lucide:link',
description: '12 established, 36 waiting',
},
{
id: 'total-down',
title: 'Total Downloaded',
value: '24.5',
unit: 'GB',
type: 'number' as const,
icon: 'lucide:database',
description: 'This session',
color: 'hsl(142 71% 45%)',
},
{
id: 'total-up',
title: 'Total Uploaded',
value: '8.2',
unit: 'GB',
type: 'number' as const,
icon: 'lucide:database',
description: 'This session',
color: 'hsl(217 91% 60%)',
},
];
return html`
<div class="panel-header">
<div class="panel-title">Network</div>
<div class="panel-description">Network traffic and connectivity</div>
</div>
<div class="stats-section">
<dees-statsgrid
.tiles=${networkTiles}
.minTileWidth=${220}
.gap=${16}
></dees-statsgrid>
</div>
`;
}
private renderProcessesPanel(): TemplateResult {
const processTiles = [
{
id: 'total-processes',
title: 'Total Processes',
value: 247,
type: 'number' as const,
icon: 'lucide:layers',
},
{
id: 'running',
title: 'Running',
value: 12,
type: 'number' as const,
icon: 'lucide:play',
color: 'hsl(142 71% 45%)',
},
{
id: 'sleeping',
title: 'Sleeping',
value: 235,
type: 'number' as const,
icon: 'lucide:moon',
color: 'hsl(217 91% 60%)',
},
{
id: 'threads',
title: 'Threads',
value: 1842,
type: 'number' as const,
icon: 'lucide:gitBranch',
},
];
const topProcesses = [
{ name: 'node', pid: 1234, cpu: 12.5, memory: 8.2 },
{ name: 'chrome', pid: 2345, cpu: 8.3, memory: 15.4 },
{ name: 'code', pid: 3456, cpu: 5.2, memory: 12.1 },
{ name: 'docker', pid: 4567, cpu: 4.8, memory: 6.8 },
{ name: 'postgres', pid: 5678, cpu: 3.2, memory: 4.5 },
{ name: 'nginx', pid: 6789, cpu: 1.5, memory: 2.1 },
{ name: 'redis', pid: 7890, cpu: 0.8, memory: 1.8 },
];
return html`
<div class="panel-header">
<div class="panel-title">Processes</div>
<div class="panel-description">Running processes and resource usage</div>
</div>
<div class="stats-section">
<dees-statsgrid
.tiles=${processTiles}
.minTileWidth=${180}
.gap=${16}
></dees-statsgrid>
</div>
<div class="stats-section">
<div class="section-title">Top Processes by CPU</div>
<div class="process-list">
<div class="process-header">
<span>Process</span>
<span>PID</span>
<span>CPU %</span>
<span>Memory %</span>
</div>
${topProcesses.map(proc => html`
<div class="process-row">
<span class="process-name">${proc.name}</span>
<span class="process-value">${proc.pid}</span>
<span class="process-value ${proc.cpu > 10 ? 'high' : ''}">${proc.cpu}%</span>
<span class="process-value ${proc.memory > 10 ? 'high' : ''}">${proc.memory}%</span>
</div>
`)}
</div>
</div>
`;
}
}

View File

@@ -0,0 +1 @@
export * from './eco-view-system.js';

6
ts_web/views/index.ts Normal file
View File

@@ -0,0 +1,6 @@
export * from './eco-view-settings/index.js';
export * from './eco-view-peripherals/index.js';
export * from './eco-view-saasshare/index.js';
export * from './eco-view-system/index.js';
export * from './eco-view-home/index.js';
export * from './eco-view-login/index.js';