update
BIN
.playwright-mcp/applauncher-peripherals-view.png
Normal file
|
After Width: | Height: | Size: 53 KiB |
|
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 46 KiB |
|
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 45 KiB |
BIN
.playwright-mcp/saasshare-apps-list.png
Normal file
|
After Width: | Height: | Size: 62 KiB |
BIN
.playwright-mcp/saasshare-devices.png
Normal file
|
After Width: | Height: | Size: 48 KiB |
BIN
.playwright-mcp/saasshare-view.png
Normal file
|
After Width: | Height: | Size: 62 KiB |
BIN
.playwright-mcp/system-view-working.png
Normal file
|
After Width: | Height: | Size: 63 KiB |
@@ -2,9 +2,28 @@
|
|||||||
import * as deesWccTools from '@design.estate/dees-wcctools';
|
import * as deesWccTools from '@design.estate/dees-wcctools';
|
||||||
import * as deesDomTools from '@design.estate/dees-domtools';
|
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 elements from '../ts_web/elements/index.js';
|
||||||
import * as pages from '../ts_web/pages/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();
|
deesDomTools.elementBasic.setup();
|
||||||
|
|||||||
@@ -3,37 +3,23 @@
|
|||||||
"projectType": "wcc",
|
"projectType": "wcc",
|
||||||
"module": {
|
"module": {
|
||||||
"githost": "code.foss.global",
|
"githost": "code.foss.global",
|
||||||
"gitscope": "design.estate",
|
"gitscope": "ecobridge.xyz",
|
||||||
"gitrepo": "dees-catalog",
|
"gitrepo": "catalog",
|
||||||
"description": "A comprehensive library that provides dynamic web components for building sophisticated and modern web applications using JavaScript and TypeScript.",
|
"description": "A web component catalog for building ecobridge application interfaces with specialized components for the ecobridge ecosystem.",
|
||||||
"npmPackagename": "@design.estate/dees-catalog",
|
"npmPackagename": "@ecobridge.xyz/catalog",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"projectDomain": "design.estate",
|
"projectDomain": "ecobridge.xyz",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"Web Components",
|
"Web Components",
|
||||||
"User Interface",
|
"User Interface",
|
||||||
"UI Library",
|
"UI Library",
|
||||||
"Component Library",
|
"Component Library",
|
||||||
"JavaScript",
|
|
||||||
"TypeScript",
|
"TypeScript",
|
||||||
"Dynamic Components",
|
"Ecobridge",
|
||||||
"Modular Architecture",
|
"App Launcher",
|
||||||
"Reusable Components",
|
"Desktop Interface",
|
||||||
"Web Development",
|
|
||||||
"Application UI",
|
|
||||||
"Custom Elements",
|
"Custom Elements",
|
||||||
"Shadow DOM",
|
"LitElement"
|
||||||
"UI Elements",
|
|
||||||
"Dashboard Interfaces",
|
|
||||||
"Form Handling",
|
|
||||||
"Data Display",
|
|
||||||
"Visualization",
|
|
||||||
"Charting",
|
|
||||||
"Interactive Components",
|
|
||||||
"Responsive Design",
|
|
||||||
"Web Applications",
|
|
||||||
"Modern Web",
|
|
||||||
"Frontend Development"
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"release": {
|
"release": {
|
||||||
@@ -45,9 +31,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@git.zone/tsdoc": {
|
"@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": {
|
"@ship.zone/szci": {
|
||||||
"npmGlobalTools": []
|
"npmGlobalTools": []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.
|
|
||||||
@@ -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();
|
|
||||||
@@ -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();
|
|
||||||
@@ -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();
|
|
||||||
@@ -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();
|
|
||||||
@@ -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();
|
|
||||||
@@ -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();
|
|
||||||
35
test/test.eco-catalog.chromium.ts
Normal 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();
|
||||||
@@ -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();
|
|
||||||
@@ -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();
|
|
||||||
@@ -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();
|
|
||||||
@@ -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();
|
|
||||||
@@ -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();
|
|
||||||
@@ -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();
|
|
||||||
@@ -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();
|
|
||||||
@@ -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();
|
|
||||||
@@ -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();
|
|
||||||
@@ -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();
|
|
||||||
@@ -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();
|
|
||||||
@@ -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();
|
|
||||||
@@ -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();
|
|
||||||
@@ -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();
|
|
||||||
@@ -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();
|
|
||||||
@@ -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();
|
|
||||||
@@ -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();
|
|
||||||
@@ -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();
|
|
||||||
@@ -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();
|
|
||||||
@@ -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();
|
|
||||||
@@ -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>
|
||||||
|
`;
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from './eco-applauncher-powermenu.js';
|
||||||
@@ -1,21 +1,18 @@
|
|||||||
import { html } from '@design.estate/dees-element';
|
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 { IWifiNetwork } from '../eco-applauncher-wifimenu/index.js';
|
||||||
import type { IAudioDevice } from '../eco-applauncher-soundmenu/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[] = [
|
const mockApps: IAppIcon[] = [
|
||||||
{ name: 'Settings', icon: 'lucide:settings', action: () => console.log('Settings clicked') },
|
{ name: 'SaaS Share', icon: 'lucide:share2', view: html`<eco-view-saasshare></eco-view-saasshare>` },
|
||||||
{ name: 'Browser', icon: 'lucide:globe', action: () => console.log('Browser clicked') },
|
{ name: 'System', icon: 'lucide:activity', view: html`<eco-view-system></eco-view-system>` },
|
||||||
{ name: 'Terminal', icon: 'lucide:terminal', action: () => console.log('Terminal clicked') },
|
{ name: 'Peripherals', icon: 'lucide:monitor', view: html`<eco-view-peripherals></eco-view-peripherals>` },
|
||||||
{ name: 'Files', icon: 'lucide:folder', action: () => console.log('Files clicked') },
|
{ name: 'Settings', icon: 'lucide:settings', view: html`<eco-view-settings></eco-view-settings>` },
|
||||||
{ 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') },
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const mockNetworks: IWifiNetwork[] = [
|
const mockNetworks: IWifiNetwork[] = [
|
||||||
@@ -32,7 +29,33 @@ const mockAudioDevices: IAudioDevice[] = [
|
|||||||
{ id: 'hdmi', name: 'LG Monitor', type: 'hdmi' },
|
{ 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>
|
<style>
|
||||||
.demo-container {
|
.demo-container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -41,6 +64,8 @@ export const demo = () => html`
|
|||||||
</style>
|
</style>
|
||||||
<div class="demo-container">
|
<div class="demo-container">
|
||||||
<eco-applauncher
|
<eco-applauncher
|
||||||
|
.mode=${'home' as TApplauncherMode}
|
||||||
|
.loginConfig=${loginConfig}
|
||||||
.apps=${mockApps}
|
.apps=${mockApps}
|
||||||
.batteryLevel=${85}
|
.batteryLevel=${85}
|
||||||
.networkStatus=${'online'}
|
.networkStatus=${'online'}
|
||||||
@@ -54,6 +79,9 @@ export const demo = () => html`
|
|||||||
.muted=${false}
|
.muted=${false}
|
||||||
.userName=${'John Doe'}
|
.userName=${'John Doe'}
|
||||||
.notificationCount=${3}
|
.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)}
|
@wifi-toggle=${(e: CustomEvent) => console.log('WiFi toggle:', e.detail)}
|
||||||
@network-select=${(e: CustomEvent) => console.log('Network selected:', e.detail)}
|
@network-select=${(e: CustomEvent) => console.log('Network selected:', e.detail)}
|
||||||
@wifi-settings-click=${() => console.log('WiFi settings clicked')}
|
@wifi-settings-click=${() => console.log('WiFi settings clicked')}
|
||||||
@@ -69,3 +97,37 @@ export const demo = () => html`
|
|||||||
></eco-applauncher>
|
></eco-applauncher>
|
||||||
</div>
|
</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];
|
||||||
|
|||||||
@@ -14,6 +14,9 @@ import { EcoApplauncherWifimenu, type IWifiNetwork } from '../eco-applauncher-wi
|
|||||||
import { EcoApplauncherBatterymenu } from '../eco-applauncher-batterymenu/index.js';
|
import { EcoApplauncherBatterymenu } from '../eco-applauncher-batterymenu/index.js';
|
||||||
import { EcoApplauncherSoundmenu, type IAudioDevice } from '../eco-applauncher-soundmenu/index.js';
|
import { EcoApplauncherSoundmenu, type IAudioDevice } from '../eco-applauncher-soundmenu/index.js';
|
||||||
import { EcoApplauncherKeyboard } from '../eco-applauncher-keyboard/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
|
// Ensure components are registered
|
||||||
DeesIcon;
|
DeesIcon;
|
||||||
@@ -21,6 +24,9 @@ EcoApplauncherWifimenu;
|
|||||||
EcoApplauncherBatterymenu;
|
EcoApplauncherBatterymenu;
|
||||||
EcoApplauncherSoundmenu;
|
EcoApplauncherSoundmenu;
|
||||||
EcoApplauncherKeyboard;
|
EcoApplauncherKeyboard;
|
||||||
|
EcoApplauncherPowermenu;
|
||||||
|
EcoViewHome;
|
||||||
|
EcoViewLogin;
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface HTMLElementTagNameMap {
|
interface HTMLElementTagNameMap {
|
||||||
@@ -28,12 +34,18 @@ declare global {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type TApplauncherMode = 'login' | 'home';
|
||||||
|
|
||||||
export interface IAppIcon {
|
export interface IAppIcon {
|
||||||
name: string;
|
name: string;
|
||||||
icon: string;
|
icon: string;
|
||||||
action?: () => void;
|
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 {
|
export interface IStatusBarConfig {
|
||||||
showTime?: boolean;
|
showTime?: boolean;
|
||||||
showNetwork?: boolean;
|
showNetwork?: boolean;
|
||||||
@@ -153,6 +165,10 @@ export class EcoApplauncher extends DeesElement {
|
|||||||
background: ${cssManager.bdTheme('hsl(220 15% 88%)', 'hsl(240 5% 15%)')};
|
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 {
|
.user-avatar {
|
||||||
width: 32px;
|
width: 32px;
|
||||||
height: 32px;
|
height: 32px;
|
||||||
@@ -195,9 +211,6 @@ export class EcoApplauncher extends DeesElement {
|
|||||||
|
|
||||||
.apps-area {
|
.apps-area {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
padding: 48px;
|
padding: 48px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
@@ -206,7 +219,6 @@ export class EcoApplauncher extends DeesElement {
|
|||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
|
||||||
gap: 32px;
|
gap: 32px;
|
||||||
max-width: 800px;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -415,6 +427,18 @@ export class EcoApplauncher extends DeesElement {
|
|||||||
pointer-events: none;
|
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 {
|
.keyboard-area {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
height: 0;
|
height: 0;
|
||||||
@@ -430,6 +454,53 @@ export class EcoApplauncher extends DeesElement {
|
|||||||
overflow: visible;
|
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) {
|
@media (max-width: 600px) {
|
||||||
.apps-area {
|
.apps-area {
|
||||||
padding: 24px;
|
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 })
|
@property({ type: Array })
|
||||||
accessor apps: IAppIcon[] = [];
|
accessor apps: IAppIcon[] = [];
|
||||||
|
|
||||||
@@ -512,9 +593,18 @@ export class EcoApplauncher extends DeesElement {
|
|||||||
@state()
|
@state()
|
||||||
accessor soundMenuOpen = false;
|
accessor soundMenuOpen = false;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
accessor powerMenuOpen = false;
|
||||||
|
|
||||||
@state()
|
@state()
|
||||||
accessor keyboardVisible = false;
|
accessor keyboardVisible = false;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
accessor activeView: TemplateResult | null = null;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
accessor activeViewName: string | null = null;
|
||||||
|
|
||||||
@property({ type: Array })
|
@property({ type: Array })
|
||||||
accessor networks: IWifiNetwork[] = [];
|
accessor networks: IWifiNetwork[] = [];
|
||||||
|
|
||||||
@@ -552,12 +642,8 @@ export class EcoApplauncher extends DeesElement {
|
|||||||
public render(): TemplateResult {
|
public render(): TemplateResult {
|
||||||
return html`
|
return html`
|
||||||
<div class="launcher-container">
|
<div class="launcher-container">
|
||||||
${this.renderTopBar()}
|
${this.mode === 'login' ? '' : this.renderTopBar()}
|
||||||
<div class="apps-area">
|
${this.renderMainContent()}
|
||||||
<div class="apps-grid">
|
|
||||||
${this.apps.map((app) => this.renderAppIcon(app))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="keyboard-area ${this.keyboardVisible ? 'visible' : ''}">
|
<div class="keyboard-area ${this.keyboardVisible ? 'visible' : ''}">
|
||||||
<eco-applauncher-keyboard
|
<eco-applauncher-keyboard
|
||||||
?visible=${this.keyboardVisible}
|
?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 {
|
private renderAppIcon(app: IAppIcon): TemplateResult {
|
||||||
return html`
|
return html`
|
||||||
<div class="app-icon" @click=${() => this.handleAppClick(app)}>
|
<div class="app-icon" @click=${() => this.handleAppClick(app)}>
|
||||||
@@ -607,7 +745,13 @@ export class EcoApplauncher extends DeesElement {
|
|||||||
return html`
|
return html`
|
||||||
<div class="top-bar">
|
<div class="top-bar">
|
||||||
<div class="top-left">
|
<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>
|
<span class="top-date">${this.currentDate}</span>
|
||||||
` : ''}
|
` : ''}
|
||||||
</div>
|
</div>
|
||||||
@@ -636,6 +780,21 @@ export class EcoApplauncher extends DeesElement {
|
|||||||
${userInitials}
|
${userInitials}
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
@@ -831,15 +990,91 @@ export class EcoApplauncher extends DeesElement {
|
|||||||
composed: true,
|
composed: true,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// If app has a view, open it inside the applauncher
|
||||||
|
if (app.view) {
|
||||||
|
this.activeView = app.view;
|
||||||
|
this.activeViewName = app.name;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise execute the action
|
||||||
if (app.action) {
|
if (app.action) {
|
||||||
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 {
|
private handleNetworkClick(e: MouseEvent): void {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
this.batteryMenuOpen = false;
|
this.batteryMenuOpen = false;
|
||||||
this.soundMenuOpen = false;
|
this.soundMenuOpen = false;
|
||||||
|
this.powerMenuOpen = false;
|
||||||
this.wifiMenuOpen = !this.wifiMenuOpen;
|
this.wifiMenuOpen = !this.wifiMenuOpen;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -847,6 +1082,7 @@ export class EcoApplauncher extends DeesElement {
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
this.wifiMenuOpen = false;
|
this.wifiMenuOpen = false;
|
||||||
this.soundMenuOpen = false;
|
this.soundMenuOpen = false;
|
||||||
|
this.powerMenuOpen = false;
|
||||||
this.batteryMenuOpen = !this.batteryMenuOpen;
|
this.batteryMenuOpen = !this.batteryMenuOpen;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -854,9 +1090,18 @@ export class EcoApplauncher extends DeesElement {
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
this.wifiMenuOpen = false;
|
this.wifiMenuOpen = false;
|
||||||
this.batteryMenuOpen = false;
|
this.batteryMenuOpen = false;
|
||||||
|
this.powerMenuOpen = false;
|
||||||
this.soundMenuOpen = !this.soundMenuOpen;
|
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 {
|
private handleWifiMenuClose(): void {
|
||||||
this.wifiMenuOpen = false;
|
this.wifiMenuOpen = false;
|
||||||
}
|
}
|
||||||
@@ -869,6 +1114,19 @@ export class EcoApplauncher extends DeesElement {
|
|||||||
this.soundMenuOpen = false;
|
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 {
|
private handleVolumeChange(e: CustomEvent): void {
|
||||||
this.soundLevel = e.detail.volume;
|
this.soundLevel = e.detail.volume;
|
||||||
this.dispatchEvent(new CustomEvent('volume-change', {
|
this.dispatchEvent(new CustomEvent('volume-change', {
|
||||||
@@ -953,6 +1211,7 @@ export class EcoApplauncher extends DeesElement {
|
|||||||
this.wifiMenuOpen = false;
|
this.wifiMenuOpen = false;
|
||||||
this.batteryMenuOpen = false;
|
this.batteryMenuOpen = false;
|
||||||
this.soundMenuOpen = false;
|
this.soundMenuOpen = false;
|
||||||
|
this.powerMenuOpen = false;
|
||||||
}
|
}
|
||||||
this.dispatchEvent(new CustomEvent('keyboard-toggle', {
|
this.dispatchEvent(new CustomEvent('keyboard-toggle', {
|
||||||
detail: { visible: this.keyboardVisible },
|
detail: { visible: this.keyboardVisible },
|
||||||
|
|||||||
@@ -4,5 +4,4 @@ export * from './eco-applauncher-wifimenu/index.js';
|
|||||||
export * from './eco-applauncher-batterymenu/index.js';
|
export * from './eco-applauncher-batterymenu/index.js';
|
||||||
export * from './eco-applauncher-soundmenu/index.js';
|
export * from './eco-applauncher-soundmenu/index.js';
|
||||||
export * from './eco-applauncher-keyboard/index.js';
|
export * from './eco-applauncher-keyboard/index.js';
|
||||||
export * from './eco-settings/index.js';
|
export * from './eco-applauncher-powermenu/index.js';
|
||||||
export * from './eco-peripherals/index.js';
|
|
||||||
|
|||||||
31
ts_web/views/eco-view-home/eco-view-home.demo.ts
Normal 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>
|
||||||
|
`;
|
||||||
157
ts_web/views/eco-view-home/eco-view-home.ts
Normal 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,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
1
ts_web/views/eco-view-home/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from './eco-view-home.js';
|
||||||
48
ts_web/views/eco-view-login/eco-view-login.demo.ts
Normal 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>
|
||||||
|
`;
|
||||||
749
ts_web/views/eco-view-login/eco-view-login.ts
Normal 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 = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
1
ts_web/views/eco-view-login/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from './eco-view-login.js';
|
||||||
@@ -4,18 +4,18 @@ export const demo = () => html`
|
|||||||
<style>
|
<style>
|
||||||
.demo-container {
|
.demo-container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 600px;
|
height: 100%;
|
||||||
background: hsl(240 10% 4%);
|
background: hsl(240 10% 4%);
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<div class="demo-container">
|
<div class="demo-container">
|
||||||
<eco-peripherals
|
<eco-view-peripherals
|
||||||
.activeCategory=${'all'}
|
.activeCategory=${'all'}
|
||||||
@device-select=${(e: CustomEvent) => console.log('Device selected:', e.detail)}
|
@device-select=${(e: CustomEvent) => console.log('Device selected:', e.detail)}
|
||||||
@scan-start=${() => console.log('Scanning started')}
|
@scan-start=${() => console.log('Scanning started')}
|
||||||
@scan-complete=${() => console.log('Scanning complete')}
|
@scan-complete=${() => console.log('Scanning complete')}
|
||||||
></eco-peripherals>
|
></eco-view-peripherals>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ import {
|
|||||||
state,
|
state,
|
||||||
} from '@design.estate/dees-element';
|
} from '@design.estate/dees-element';
|
||||||
import { DeesAppuiSecondarymenu, DeesIcon } from '@design.estate/dees-catalog';
|
import { DeesAppuiSecondarymenu, DeesIcon } from '@design.estate/dees-catalog';
|
||||||
import type { ISecondaryMenuGroup, ISecondaryMenuItem } from '../../interfaces/secondarymenu.js';
|
import type { ISecondaryMenuGroup, ISecondaryMenuItem } from '../../elements/interfaces/secondarymenu.js';
|
||||||
import { demo } from './eco-peripherals.demo.js';
|
import { demo } from './eco-view-peripherals.demo.js';
|
||||||
|
|
||||||
// Ensure components are registered
|
// Ensure components are registered
|
||||||
DeesAppuiSecondarymenu;
|
DeesAppuiSecondarymenu;
|
||||||
@@ -18,7 +18,7 @@ DeesIcon;
|
|||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface HTMLElementTagNameMap {
|
interface HTMLElementTagNameMap {
|
||||||
'eco-peripherals': EcoPeripherals;
|
'eco-view-peripherals': EcoViewPeripherals;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,10 +47,10 @@ export interface IPeripheralDevice {
|
|||||||
isDefault?: boolean;
|
isDefault?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@customElement('eco-peripherals')
|
@customElement('eco-view-peripherals')
|
||||||
export class EcoPeripherals extends DeesElement {
|
export class EcoViewPeripherals extends DeesElement {
|
||||||
public static demo = demo;
|
public static demo = demo;
|
||||||
public static demoGroup = 'App Launcher';
|
public static demoGroup = 'Views';
|
||||||
|
|
||||||
public static styles = [
|
public static styles = [
|
||||||
cssManager.defaultStyles,
|
cssManager.defaultStyles,
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
export * from './eco-peripherals.js';
|
export * from './eco-view-peripherals.js';
|
||||||
|
|||||||
20
ts_web/views/eco-view-saasshare/eco-view-saasshare.demo.ts
Normal 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>
|
||||||
|
`;
|
||||||
1288
ts_web/views/eco-view-saasshare/eco-view-saasshare.ts
Normal file
1
ts_web/views/eco-view-saasshare/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from './eco-view-saasshare.js';
|
||||||
@@ -4,15 +4,15 @@ export const demo = () => html`
|
|||||||
<style>
|
<style>
|
||||||
.demo-container {
|
.demo-container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 600px;
|
height: 100%;
|
||||||
background: hsl(240 10% 4%);
|
background: hsl(240 10% 4%);
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<div class="demo-container">
|
<div class="demo-container">
|
||||||
<eco-settings
|
<eco-view-settings
|
||||||
.activePanel=${'general'}
|
.activePanel=${'general'}
|
||||||
></eco-settings>
|
></eco-view-settings>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ import {
|
|||||||
state,
|
state,
|
||||||
} from '@design.estate/dees-element';
|
} from '@design.estate/dees-element';
|
||||||
import { DeesAppuiSecondarymenu, DeesIcon } from '@design.estate/dees-catalog';
|
import { DeesAppuiSecondarymenu, DeesIcon } from '@design.estate/dees-catalog';
|
||||||
import type { ISecondaryMenuGroup, ISecondaryMenuItem } from '../../interfaces/secondarymenu.js';
|
import type { ISecondaryMenuGroup, ISecondaryMenuItem } from '../../elements/interfaces/secondarymenu.js';
|
||||||
import { demo } from './eco-settings.demo.js';
|
import { demo } from './eco-view-settings.demo.js';
|
||||||
|
|
||||||
// Ensure components are registered
|
// Ensure components are registered
|
||||||
DeesAppuiSecondarymenu;
|
DeesAppuiSecondarymenu;
|
||||||
@@ -18,7 +18,7 @@ DeesIcon;
|
|||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface HTMLElementTagNameMap {
|
interface HTMLElementTagNameMap {
|
||||||
'eco-settings': EcoSettings;
|
'eco-view-settings': EcoViewSettings;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,10 +35,10 @@ export type TSettingsPanel =
|
|||||||
| 'updates'
|
| 'updates'
|
||||||
| 'about';
|
| 'about';
|
||||||
|
|
||||||
@customElement('eco-settings')
|
@customElement('eco-view-settings')
|
||||||
export class EcoSettings extends DeesElement {
|
export class EcoViewSettings extends DeesElement {
|
||||||
public static demo = demo;
|
public static demo = demo;
|
||||||
public static demoGroup = 'App Launcher';
|
public static demoGroup = 'Views';
|
||||||
|
|
||||||
public static styles = [
|
public static styles = [
|
||||||
cssManager.defaultStyles,
|
cssManager.defaultStyles,
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
export * from './eco-settings.js';
|
export * from './eco-view-settings.js';
|
||||||
|
|||||||
18
ts_web/views/eco-view-system/eco-view-system.demo.ts
Normal 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>
|
||||||
|
`;
|
||||||
877
ts_web/views/eco-view-system/eco-view-system.ts
Normal 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>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
1
ts_web/views/eco-view-system/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from './eco-view-system.js';
|
||||||
6
ts_web/views/index.ts
Normal 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';
|
||||||