diff --git a/.playwright-mcp/applauncher-peripherals-view.png b/.playwright-mcp/applauncher-peripherals-view.png new file mode 100644 index 0000000..84550a3 Binary files /dev/null and b/.playwright-mcp/applauncher-peripherals-view.png differ diff --git a/.playwright-mcp/applauncher-topbar-bright.png b/.playwright-mcp/applauncher-topbar-bright.png index e22a486..750e15f 100644 Binary files a/.playwright-mcp/applauncher-topbar-bright.png and b/.playwright-mcp/applauncher-topbar-bright.png differ diff --git a/.playwright-mcp/applauncher-topbar-dark.png b/.playwright-mcp/applauncher-topbar-dark.png index ba4a342..9515495 100644 Binary files a/.playwright-mcp/applauncher-topbar-dark.png and b/.playwright-mcp/applauncher-topbar-dark.png differ diff --git a/.playwright-mcp/saasshare-apps-list.png b/.playwright-mcp/saasshare-apps-list.png new file mode 100644 index 0000000..7ab6534 Binary files /dev/null and b/.playwright-mcp/saasshare-apps-list.png differ diff --git a/.playwright-mcp/saasshare-devices.png b/.playwright-mcp/saasshare-devices.png new file mode 100644 index 0000000..4eb47fe Binary files /dev/null and b/.playwright-mcp/saasshare-devices.png differ diff --git a/.playwright-mcp/saasshare-view.png b/.playwright-mcp/saasshare-view.png new file mode 100644 index 0000000..7ab6534 Binary files /dev/null and b/.playwright-mcp/saasshare-view.png differ diff --git a/.playwright-mcp/system-view-working.png b/.playwright-mcp/system-view-working.png new file mode 100644 index 0000000..6fa6783 Binary files /dev/null and b/.playwright-mcp/system-view-working.png differ diff --git a/html/index.ts b/html/index.ts index 7b1101a..915d92f 100644 --- a/html/index.ts +++ b/html/index.ts @@ -2,9 +2,28 @@ import * as deesWccTools from '@design.estate/dees-wcctools'; import * as deesDomTools from '@design.estate/dees-domtools'; -// elements and pages +// elements, pages, and views import * as elements from '../ts_web/elements/index.js'; import * as pages from '../ts_web/pages/index.js'; +import * as views from '../ts_web/views/index.js'; -deesWccTools.setupWccTools(elements as any, pages); +deesWccTools.setupWccTools({ + sections: [ + { + name: 'Pages', + type: 'pages', + items: pages, + }, + { + name: 'Elements', + type: 'elements', + items: elements, + }, + { + name: 'Views', + type: 'elements', + items: views, + }, + ], +}); deesDomTools.elementBasic.setup(); diff --git a/npmextra.json b/npmextra.json index fb69d1b..8fd5008 100644 --- a/npmextra.json +++ b/npmextra.json @@ -3,37 +3,23 @@ "projectType": "wcc", "module": { "githost": "code.foss.global", - "gitscope": "design.estate", - "gitrepo": "dees-catalog", - "description": "A comprehensive library that provides dynamic web components for building sophisticated and modern web applications using JavaScript and TypeScript.", - "npmPackagename": "@design.estate/dees-catalog", + "gitscope": "ecobridge.xyz", + "gitrepo": "catalog", + "description": "A web component catalog for building ecobridge application interfaces with specialized components for the ecobridge ecosystem.", + "npmPackagename": "@ecobridge.xyz/catalog", "license": "MIT", - "projectDomain": "design.estate", + "projectDomain": "ecobridge.xyz", "keywords": [ "Web Components", "User Interface", "UI Library", "Component Library", - "JavaScript", "TypeScript", - "Dynamic Components", - "Modular Architecture", - "Reusable Components", - "Web Development", - "Application UI", + "Ecobridge", + "App Launcher", + "Desktop Interface", "Custom Elements", - "Shadow DOM", - "UI Elements", - "Dashboard Interfaces", - "Form Handling", - "Data Display", - "Visualization", - "Charting", - "Interactive Components", - "Responsive Design", - "Web Applications", - "Modern Web", - "Frontend Development" + "LitElement" ] }, "release": { @@ -45,9 +31,9 @@ } }, "@git.zone/tsdoc": { - "legal": "\n## License and Legal Information\n\nThis repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the [license](license) file within this repository. \n\n**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.\n\n### Trademarks\n\nThis project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH and are not included within the scope of the MIT license granted herein. Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines, and any usage must be approved in writing by Task Venture Capital GmbH.\n\n### Company Information\n\nTask Venture Capital GmbH \nRegistered at District court Bremen HRB 35230 HB, Germany\n\nFor any legal inquiries or if you require further information, please contact us via email at hello@task.vc.\n\nBy using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.\n" + "legal": "\n## License and Legal Information\n\nThis repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the [license](license) file within this repository. \n\n**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.\n\n### Trademarks\n\nThis project is owned and maintained by Lossless GmbH. The names and logos associated with Lossless GmbH and any related products or services are trademarks of Lossless GmbH and are not included within the scope of the MIT license granted herein. Use of these trademarks must comply with Lossless GmbH's Trademark Guidelines, and any usage must be approved in writing by Lossless GmbH.\n\n### Company Information\n\nLossless GmbH\n\nFor any legal inquiries or if you require further information, please contact us via the official channels.\n\nBy using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Lossless GmbH of any derivative works.\n" }, "@ship.zone/szci": { "npmGlobalTools": [] } -} \ No newline at end of file +} diff --git a/readme.md b/readme.md index aa575d7..8e426b6 100644 --- a/readme.md +++ b/readme.md @@ -1,1401 +1,77 @@ -# @design.estate/dees-catalog +# @ecobridge.xyz/catalog -A comprehensive web components library built with TypeScript and LitElement, providing **80+ production-ready UI components** for building modern web applications with consistent design and behavior. 🚀 +A web component catalog for building ecobridge application interfaces. Built on top of `@design.estate/dees-catalog` and extending it with specialized components for the ecobridge ecosystem. -[![TypeScript](https://img.shields.io/badge/TypeScript-5.0+-blue.svg)](https://www.typescriptlang.org/) -[![LitElement](https://img.shields.io/badge/LitElement-4.0+-orange.svg)](https://lit.dev/) - -## Issue Reporting and Security - -For reporting bugs, issues, or security vulnerabilities, please visit [community.foss.global/](https://community.foss.global/). This is the central community hub for all issue reporting. Developers who sign and comply with our contribution agreement and go through identification can also get a [code.foss.global/](https://code.foss.global/) account to submit Pull Requests directly. - -## ✨ Features - -- 🎨 **Consistent Design System** - Beautiful, cohesive components following modern UI/UX principles -- 🌙 **Dark/Light Theme Support** - All components automatically adapt to your theme -- ⌨️ **Keyboard Accessible** - Full keyboard navigation and ARIA support -- 📱 **Responsive** - Mobile-first design that works across all screen sizes -- 🔧 **TypeScript-First** - Fully typed APIs with excellent IDE support -- 🧩 **Modular** - Use only what you need, tree-shakeable architecture - -## 📦 Installation +## Installation ```bash -npm install @design.estate/dees-catalog -# or -pnpm add @design.estate/dees-catalog +pnpm add @ecobridge.xyz/catalog ``` -## 🚀 Quick Start +## Components -```typescript -import { html, DeesElement, customElement } from '@design.estate/dees-element'; -import '@design.estate/dees-catalog'; - -@customElement('my-app') -class MyApp extends DeesElement { - render() { - return html` - alert('Hello!')}> - Click me! - - `; - } -} -``` - -## 📖 Development Guide - -For developers working on this library, please refer to the [UI Components Playbook](readme.playbook.md) for comprehensive patterns, best practices, and architectural guidelines. - -## 📚 Components Overview - -| Category | Components | -|----------|------------| -| **Core UI** | [`DeesButton`](#deesbutton), [`DeesButtonExit`](#deesbuttonexit), [`DeesButtonGroup`](#deesbuttongroup), [`DeesBadge`](#deesbadge), [`DeesChips`](#deeschips), [`DeesHeading`](#deesheading), [`DeesHint`](#deeshint), [`DeesIcon`](#deesicon), [`DeesLabel`](#deeslabel), [`DeesPanel`](#deespanel), [`DeesSearchbar`](#deessearchbar), [`DeesSpinner`](#deesspinner), [`DeesToast`](#deestoast), [`DeesWindowcontrols`](#deeswindowcontrols) | -| **Forms** | [`DeesForm`](#deesform), [`DeesInputText`](#deesinputtext), [`DeesInputCheckbox`](#deesinputcheckbox), [`DeesInputDropdown`](#deesinputdropdown), [`DeesInputRadiogroup`](#deesinputradiogroup), [`DeesInputFileupload`](#deesinputfileupload), [`DeesInputIban`](#deesinputiban), [`DeesInputPhone`](#deesinputphone), [`DeesInputQuantitySelector`](#deesinputquantityselector), [`DeesInputMultitoggle`](#deesinputmultitoggle), [`DeesInputTags`](#deesinputtags), [`DeesInputTypelist`](#deesinputtypelist), [`DeesInputList`](#deesinputlist), [`DeesInputProfilepicture`](#deesinputprofilepicture), [`DeesInputRichtext`](#deesinputrichtext), [`DeesInputWysiwyg`](#deesinputwysiwyg), [`DeesInputDatepicker`](#deesinputdatepicker), [`DeesInputSearchselect`](#deesinputsearchselect), [`DeesFormSubmit`](#deesformsubmit) | -| **Layout** | [`DeesAppui`](#deesappui), [`DeesAppuiMainmenu`](#deesappuimainmenu), [`DeesAppuiSecondarymenu`](#deesappuisecondarymenu), [`DeesAppuiMaincontent`](#deesappuimaincontent), [`DeesAppuiAppbar`](#deesappuiappbar), [`DeesAppuiActivitylog`](#deesappuiactivitylog), [`DeesAppuiProfiledropdown`](#deesappuiprofiledropdown), [`DeesAppuiTabs`](#deesappuitabs), [`DeesMobileNavigation`](#deesmobilenavigation), [`DeesDashboardGrid`](#deesdashboardgrid) | -| **Data Display** | [`DeesTable`](#deestable), [`DeesDataviewCodebox`](#deesdataviewcodebox), [`DeesDataviewStatusobject`](#deesdataviewstatusobject), [`DeesPdf`](#deespdf), [`DeesStatsGrid`](#deesstatsgrid), [`DeesPagination`](#deespagination) | -| **Visualization** | [`DeesChartArea`](#deeschartarea), [`DeesChartLog`](#deeschartlog) | -| **Dialogs & Overlays** | [`DeesModal`](#deesmodal), [`DeesContextmenu`](#deescontextmenu), [`DeesSpeechbubble`](#deesspeechbubble), [`DeesWindowlayer`](#deeswindowlayer) | -| **Navigation** | [`DeesStepper`](#deesstepper), [`DeesProgressbar`](#deesprogressbar) | -| **Development** | [`DeesEditor`](#deeseditor), [`DeesEditorMarkdown`](#deeseditormarkdown), [`DeesEditorMarkdownoutlet`](#deeseditormarkdownoutlet), [`DeesTerminal`](#deesterminal), [`DeesUpdater`](#deesupdater) | -| **Theming** | [`DeesTheme`](#deestheme) | -| **Auth & Utilities** | [`DeesSimpleAppdash`](#deessimpleappdash), [`DeesSimpleLogin`](#deessimplelogin) | -| **Shopping** | [`DeesShoppingProductcard`](#deesshoppingproductcard) | - ---- - -## 🎯 Detailed Component Documentation - -### Core UI Components - -#### `DeesButton` -A versatile button component supporting multiple styles and states. - -```typescript -// Basic usage -const button = document.createElement('dees-button'); -button.text = 'Click me'; - -// With options -Click me -``` - -#### `DeesBadge` -Display status indicators or counts with customizable styles. - -```typescript - -``` - -#### `DeesChips` -Interactive chips/tags with selection capabilities. - -```typescript - -``` - -#### `DeesIcon` -Display icons from FontAwesome and Lucide icon libraries with library prefixes. - -```typescript -// FontAwesome icons - use 'fa:' prefix - - -// Lucide icons - use 'lucide:' prefix - - -// Legacy API (deprecated but still supported) - -``` - -#### `DeesLabel` -Text label component with optional icon and status indicators. - -```typescript - -``` - -#### `DeesSpinner` -Loading indicator with customizable appearance. - -```typescript - -``` - -#### `DeesToast` -Notification toast messages with various styles, positions, and auto-dismiss functionality. - -```typescript -// Programmatic usage -DeesToast.show({ - message: 'Operation successful', - type: 'success', // Options: info, success, warning, error - duration: 3000, // Time in milliseconds before auto-dismiss - position: 'top-right' // Options: top-right, top-left, bottom-right, bottom-left, top-center, bottom-center -}); - -// Convenience methods -DeesToast.info('Information message'); -DeesToast.success('Success message'); -DeesToast.warning('Warning message'); -DeesToast.error('Error message'); - -// Advanced control -const toast = await DeesToast.show({ - message: 'Processing...', - type: 'info', - duration: 0 // No auto-dismiss -}); - -// Later dismiss programmatically -toast.dismiss(); -``` - -**Key Features:** -- Multiple toast types with distinct icons and colors -- 6 position options for flexible placement -- Auto-dismiss with visual progress indicator -- Manual dismiss by clicking -- Smooth animations and transitions -- Automatic stacking of multiple toasts -- Theme-aware styling -- Programmatic control - -#### `DeesButtonExit` -Exit/close button component with consistent styling. - -```typescript - -``` - -#### `DeesButtonGroup` -Container for grouping related buttons together. - -```typescript - -``` - -#### `DeesHeading` -Consistent heading component with level and styling options. - -```typescript - -``` - -#### `DeesHint` -Hint/tooltip component for providing contextual help. - -```typescript - -``` - -#### `DeesPanel` -Container component for grouping related content with optional title and actions. - -```typescript - - - -``` - -#### `DeesSearchbar` -Search input component with suggestions and search handling. - -```typescript - -``` - -#### `DeesWindowcontrols` -Window control buttons (minimize, maximize, close) for desktop-like applications. - -```typescript - -``` - ---- - -### Form Components - -#### `DeesForm` -Container component for form elements with built-in validation and data handling. - -```typescript - handleFormData(e.detail)} // Emitted when form is submitted - @formValidation=${(e) => handleValidation(e.detail)} // Emitted during validation -> - - Submit - -``` - -#### `DeesInputText` -Text input field with validation and formatting options. - -```typescript - -``` - -#### `DeesInputCheckbox` -Checkbox input component for boolean values. - -```typescript - -``` - -#### `DeesInputDropdown` -Dropdown selection component with search and filtering capabilities. - -```typescript - -``` - -#### `DeesInputFileupload` -File upload component with drag-and-drop support. - -```typescript - -``` - -#### `DeesInputIban` -Specialized input for IBAN (International Bank Account Number) with validation. - -```typescript - -``` - -#### `DeesInputPhone` -Phone number input with country code selection and formatting. - -```typescript - -``` - -#### `DeesInputQuantitySelector` -Numeric input with increment/decrement controls. - -```typescript - -``` - -#### `DeesInputMultitoggle` -Multi-state toggle button group. - -```typescript - -``` - -#### `DeesInputRadiogroup` -Radio button group for single-choice selections with internal state management. - -```typescript - - -// With custom option objects - -``` - -#### `DeesInputTags` -Tag input component for managing lists of tags with auto-complete and validation. - -```typescript - -``` - -**Key Features:** -- Add tags by pressing Enter or typing comma/semicolon -- Remove tags with click or backspace -- Auto-complete suggestions with keyboard navigation -- Maximum tag limit support -- Full theme support -- Form validation integration - -#### `DeesInputTypelist` -Dynamic list input for managing arrays of typed values. - -```typescript - -``` - -#### `DeesInputList` -Advanced list input with drag-and-drop reordering, inline editing, and validation. - -```typescript - -``` - -**Key Features:** -- Add, edit, and remove items inline -- Drag-and-drop reordering with visual feedback -- Optional duplicate prevention -- Min/max item constraints -- Delete confirmation dialog -- Full keyboard support -- Form validation integration - -#### `DeesInputProfilepicture` -Profile picture input with cropping, zoom, and image processing. - -```typescript - -``` - -**Key Features:** -- Interactive cropping modal with zoom and pan -- Drag-and-drop file upload -- Round or square output shapes -- Configurable output size and quality -- File size and format validation -- Delete functionality -- Preview on hover - -#### `DeesInputDatepicker` -Date and time picker component with calendar interface and manual typing support. - -```typescript - -``` - -**Key Features:** -- Interactive calendar popup -- Manual date typing with multiple formats -- Optional time selection -- Configurable date format -- Min/max date constraints -- Disable specific dates -- Keyboard navigation -- Today button -- Clear functionality -- 12/24 hour time formats -- Theme-aware styling -- Live parsing and validation - -**Manual Input Formats:** -```typescript -// Date formats supported -"2023-12-20" // ISO format (YYYY-MM-DD) -"20.12.2023" // European format (DD.MM.YYYY) -"12/20/2023" // US format (MM/DD/YYYY) - -// Date with time (add space and time after any date format) -"2023-12-20 14:30" -"20.12.2023 9:45" -"12/20/2023 16:00" -``` - -#### `DeesInputSearchselect` -Search-enabled dropdown selection component. - -```typescript - -``` - -#### `DeesInputRichtext` -Rich text editor with formatting toolbar powered by TipTap. - -```typescript - -``` - -**Key Features:** -- Full formatting toolbar (bold, italic, underline, strike, etc.) -- Heading levels (H1-H6) -- Lists (bullet, ordered) -- Links with URL editing -- Code blocks and inline code -- Blockquotes -- Horizontal rules -- Undo/redo support -- Word and character count -- HTML output - -#### `DeesInputWysiwyg` -Advanced block-based editor with slash commands and rich content blocks. - -```typescript - -``` - -**Key Features:** -- Slash commands for quick formatting -- Block-based editing (paragraphs, headings, lists, etc.) -- Drag and drop block reordering -- Multiple output formats -- Keyboard shortcuts -- Collaborative editing ready -- Extensible block types - -#### `DeesFormSubmit` -Submit button component specifically designed for `DeesForm`. - -```typescript -Submit Form -``` - ---- - -### Layout Components - -#### `DeesAppui` -A comprehensive application shell component providing a complete UI framework with navigation, menus, activity logging, and view management. - -> **Full API Documentation**: See [ts_web/elements/00group-appui/dees-appui/readme.md](./ts_web/elements/00group-appui/dees-appui/readme.md) for complete documentation including all programmatic APIs, view lifecycle hooks, and TypeScript interfaces. - -**Quick Start:** - -```typescript -import { html, DeesElement, customElement } from '@design.estate/dees-element'; -import { DeesAppui } from '@design.estate/dees-catalog'; - -@customElement('my-app') -class MyApp extends DeesElement { - private appui: DeesAppui; - - async firstUpdated() { - this.appui = this.shadowRoot.querySelector('dees-appui'); - - // Configure with views and menu - this.appui.configure({ - branding: { logoIcon: 'lucide:box', logoText: 'My App' }, - views: [ - { id: 'dashboard', name: 'Dashboard', iconName: 'lucide:home', content: 'my-dashboard' }, - { id: 'settings', name: 'Settings', iconName: 'lucide:settings', content: 'my-settings' }, - ], - mainMenu: { - sections: [{ name: 'Main', views: ['dashboard', 'settings'] }] - }, - defaultView: 'dashboard' - }); - } - - render() { - return html``; - } -} -``` - -**Key Features:** -- **Configure API**: Single `configure()` method for complete app setup -- **View Management**: Automatic view caching, lazy loading, and lifecycle hooks -- **Programmatic APIs**: Full control over AppBar, Main Menu, Secondary Menu, Content Tabs, and Activity Log -- **View Lifecycle Hooks**: `onActivate()`, `onDeactivate()`, and `canDeactivate()` for view components -- **Hash-based Routing**: Automatic URL synchronization with view navigation -- **RxJS Observables**: `viewChanged$` and `viewLifecycle$` for reactive programming -- **TypeScript-first**: Typed `IViewActivationContext` passed to views on activation -- **Activity Log Toggle**: Built-in appbar button to show/hide activity panel with entry count badge - -**Programmatic APIs include:** -- `navigateToView(viewId, params?)` - Navigate between views -- `setAppBarMenus()`, `setBreadcrumbs()`, `setUser()` - Control the app bar -- `setMainMenu()`, `setMainMenuSelection()`, `setMainMenuBadge()` - Control main navigation -- `setMainMenuCollapsed()`, `setMainMenuVisible()` - Control main menu visibility -- `setSecondaryMenu()`, `setSecondaryMenuCollapsed()`, `setSecondaryMenuVisible()` - Control secondary menu -- `setContentTabs()`, `setContentTabsVisible()` - Control view-specific UI -- `activityLog.add()`, `activityLog.addMany()`, `activityLog.clear()` - Manage activity entries -- `setActivityLogVisible()`, `toggleActivityLog()`, `getActivityLogVisible()` - Control activity panel - -**View Visibility Control:** -```typescript -// In your view's onActivate hook -onActivate(context: IViewActivationContext) { - // Hide secondary menu for a fullscreen view - context.appui.setSecondaryMenuVisible(false); - - // Hide content tabs - context.appui.setContentTabsVisible(false); - - // Collapse main menu to give more space - context.appui.setMainMenuCollapsed(true); -} -``` - -#### `DeesAppuiMainmenu` -Main navigation menu component for application-wide navigation. - -```typescript - navigate('dashboard') }, - { key: 'settings', iconName: 'lucide:settings', action: () => navigate('settings') } - ] - } - ]} - collapsed // Optional: show collapsed version -> -``` - -#### `DeesAppuiSecondarymenu` -Secondary navigation component for sub-section selection with collapsible groups and badges. - -```typescript - select('frontend'), badge: 3, badgeVariant: 'warning' }, - { key: 'API Server', iconName: 'lucide:server', action: () => select('api') } - ] - } - ]} - @item-select=${handleSectionChange} -> -``` - -#### `DeesAppuiMaincontent` -Main content area with tab management support. - -```typescript - selectTab('overview') }, - { key: 'Details', iconName: 'lucide:info', action: () => selectTab('details') } - ]} - @tab-select=${handleTabChange} -> - - -``` - -#### `DeesAppuiAppbar` -Professional application bar component with hierarchical menus, breadcrumb navigation, user account management, and activity log toggle. +### EcoApplauncher -```typescript - {}, // No-op for parent menu items - submenu: [ - { - name: 'New File', - shortcut: 'Cmd+N', - iconName: 'file-plus', - action: async () => handleNewFile() - }, - { - name: 'Open...', - shortcut: 'Cmd+O', - iconName: 'folder-open', - action: async () => handleOpen() - }, - { divider: true }, // Menu separator - { - name: 'Save', - shortcut: 'Cmd+S', - iconName: 'save', - action: async () => handleSave(), - disabled: true // Disabled state - } - ] - } - ]} - .breadcrumbs=${'Project > src > components'} - .showWindowControls=${true} - .showSearch=${true} - .showActivityLogToggle=${true} - .activityLogCount=${5} - .activityLogActive=${false} - .user=${{ - name: 'John Doe', - avatar: '/path/to/avatar.jpg', - status: 'online' // Options: 'online' | 'offline' | 'busy' | 'away' - }} - @menu-select=${(e) => handleMenuSelect(e.detail.item)} - @breadcrumb-navigate=${(e) => handleBreadcrumbClick(e.detail)} - @activity-toggle=${() => handleActivityToggle()} -> -``` - -**Key Features:** -- **Hierarchical Menu System** - Top-level menus with dropdown submenus, icons, and keyboard shortcuts -- **Keyboard Navigation** - Full keyboard support (Tab, Arrow keys, Enter, Escape) -- **Breadcrumb Navigation** - Customizable breadcrumb trail with click events -- **User Account Section** - Avatar with status indicator and profile dropdown -- **Activity Log Toggle** - Button with badge count to show/hide activity panel -- **Accessibility** - Full ARIA support with menubar roles +The main application launcher component providing a complete desktop-like interface with: -#### `DeesAppuiActivitylog` -Real-time activity log panel for displaying user actions and system events. +- Login view with customizable authentication +- Home view with app grid +- Top bar with date, search, notifications, and user info +- Status bar with network, battery, sound, and keyboard indicators +- Power menu with shutdown, restart, sleep, and lock options ```typescript - +import { EcoApplauncher } from '@ecobridge.xyz/catalog'; -// Programmatic API -activityLog.add({ - type: 'update', // Options: login, logout, view, create, update, delete, custom - user: 'John Doe', - message: 'Updated project settings', - iconName: 'lucide:settings' // Optional: custom icon -}); - -activityLog.addMany(entries); // Add multiple entries -activityLog.clear(); // Clear all entries -activityLog.getEntries(); // Get all entries -activityLog.filter({ user: 'John' }); // Filter by user/type -activityLog.search('settings'); // Search by message +const launcher = document.createElement('eco-applauncher'); +launcher.mode = 'login'; // or 'home' +launcher.apps = [ + { name: 'Settings', icon: 'lucide:settings', action: () => openSettings() }, + { name: 'Files', icon: 'lucide:folder', action: () => openFiles() }, +]; +document.body.appendChild(launcher); ``` -**Key Features:** -- Stacked entry layout with icon, user, timestamp, and message -- Date grouping (Today, Yesterday, etc.) -- Search and filter functionality -- Context menu for entry actions -- Live streaming indicator -- Animated slide-in/out panel -- Theme-aware styling - -#### `DeesAppuiTabs` -Reusable tab component with horizontal/vertical layout support. +### Sub-Components -```typescript - console.log('Home') }, - { key: 'Settings', iconName: 'lucide:settings', action: () => console.log('Settings') } - ]} - tabStyle="horizontal" // Options: horizontal, vertical - showTabIndicator={true} - @tab-select=${handleTabSelect} -> -``` - ---- +| Component | Description | +|-----------|-------------| +| `EcoApplauncherWifimenu` | WiFi network selection menu | +| `EcoApplauncherBatterymenu` | Battery status and power mode menu | +| `EcoApplauncherSoundmenu` | Audio device and volume control menu | +| `EcoApplauncherKeyboard` | Virtual on-screen keyboard | +| `EcoApplauncherPowermenu` | Power actions menu (shutdown, restart, etc.) | -### Data Display Components +### EcoScreensaver -#### `DeesTable` -Advanced table component with sorting, filtering, and action support. +A subtle animated screensaver component with flowing geometric patterns. ```typescript - ({ - name: item.name, - date: item.date, - amount: item.amount, - description: item.description - })} - .dataActions=${[ - { - name: 'Edit', - icon: 'edit', - action: (item) => handleEdit(item) - }, - { - name: 'Delete', - icon: 'trash', - action: (item) => handleDelete(item) - } - ]} - heading1="Transactions" - heading2="Recent Activity" - searchable // Enable search functionality - dataName="transaction" // Name for single data item - @selection-change=${handleSelectionChange} -> -``` - -**Advanced Features:** -- Schema-first columns or `displayFunction` rendering -- Sorting via header clicks with `aria-sort` + `sortChange` -- Global search with Lucene-like syntax; modes: `table`, `data`, `server` -- Per-column quick filters row; `showColumnFilters` and `column.filterable=false` -- Selection: `none` | `single` | `multi`, with select-all and `selectionChange` -- Sticky header + internal scroll (`stickyHeader`, `--table-max-height`) -- Rich actions: header/in-row/contextmenu/footer/doubleClick; pinned Actions column -- Editable cells via `editableFields` -- Drag & drop files onto rows +import { EcoScreensaver } from '@ecobridge.xyz/catalog'; -#### `DeesDataviewCodebox` -Code display component with syntax highlighting and line numbers. +// Show screensaver +await EcoScreensaver.show(); -```typescript - { - return html\`
Hello World
\`; - }; - `} ->
+// Hide screensaver +EcoScreensaver.hide(); ``` -#### `DeesDataviewStatusobject` -Status display component for complex objects with nested status indicators. - -```typescript - -``` +## Development -#### `DeesPdf` -PDF viewer component with navigation and zoom controls. +```bash +# Install dependencies +pnpm install -```typescript - -``` +# Watch mode for development +pnpm run watch -#### `DeesStatsGrid` -A responsive grid component for displaying statistical data with various visualization types. +# Build +pnpm run build -```typescript - +# Run tests +pnpm test ``` -#### `DeesPagination` -Pagination component for navigating through large datasets. - -```typescript - -``` - ---- - -### Visualization Components - -#### `DeesChartArea` -Area chart component built on ApexCharts for visualizing time-series data. - -```typescript - -``` - -#### `DeesChartLog` -Specialized chart component for visualizing log data and events. - -```typescript - -``` - ---- - -### Dialogs & Overlays Components - -#### `DeesModal` -Modal dialog component with customizable content and actions. - -```typescript -// Programmatic usage -DeesModal.createAndShow({ - heading: 'Confirm Action', - content: html` - - - - `, - menuOptions: [ - { name: 'Cancel', action: async (modal) => { modal.destroy(); return null; } }, - { name: 'Confirm', action: async (modal) => { /* handle */ modal.destroy(); return null; } } - ] -}); -``` - -#### `DeesContextmenu` -Context menu component for right-click actions. - -```typescript - handleEdit() }, - { label: 'Delete', icon: 'trash', action: () => handleDelete() } - ]} - position="right" -> -``` - -#### `DeesSpeechbubble` -Tooltip-style speech bubble component for contextual information. - -```typescript -// Programmatic usage -const bubble = await DeesSpeechbubble.createAndShow( - referenceElement, - 'Helpful information about this feature' -); -``` - -#### `DeesWindowlayer` -Base overlay component used by modal dialogs and other overlay components. - -```typescript -const layer = await DeesWindowLayer.createAndShow({ - blur: true, -}); -``` - ---- - -### Navigation Components - -#### `DeesStepper` -Multi-step navigation component for guided user flows. - -```typescript -Form 1` }, - { key: 'address', label: 'Address', content: html`
Form 2
` }, - { key: 'confirm', label: 'Confirmation', content: html`
Review
` } - ]} - currentStep="personal" - @step-change=${handleStepChange} - @complete=${handleComplete} ->
-``` - -#### `DeesProgressbar` -Progress indicator component for tracking completion status. - -```typescript - -``` - ---- - -### Theming Components - -#### `DeesTheme` -Theme provider component that wraps children and provides CSS custom properties for consistent theming. - -```typescript -// Basic usage - wrap your app - - - - -// With custom overrides - - - -``` - -**Key Features:** -- Provides CSS custom properties for colors, spacing, radius, shadows, and transitions -- Can be nested for section-specific theming -- Works with dark/light mode -- Overrides cascade to all child components - ---- - -### Development Components - -#### `DeesEditor` -Code editor component with syntax highlighting and code completion, powered by Monaco Editor. - -```typescript - -``` - -#### `DeesEditorMarkdown` -Markdown editor component with live preview. - -```typescript - -``` - -#### `DeesEditorMarkdownoutlet` -Markdown preview component for rendering markdown content. - -```typescript - -``` - -#### `DeesTerminal` -Terminal emulator component for command-line interface. - -```typescript - `Echo: ${args.join(' ')}`, - 'help': () => 'Available commands: echo, help' - }} - .prompt=${'$'} - .welcomeMessage=${'Welcome! Type "help" for available commands.'} -> -``` - -#### `DeesUpdater` -Component for managing application updates and version control. - -```typescript - -``` - ---- - -### Auth & Utilities Components - -#### `DeesSimpleAppdash` -Simple application dashboard component for quick prototyping. - -```typescript - - - -``` - -#### `DeesSimpleLogin` -Simple login form component with validation and customization. - -```typescript - -``` - ---- - -### Shopping Components - -#### `DeesShoppingProductcard` -Product card component for e-commerce applications. - -```typescript - -``` - ---- - -## 🔧 TypeScript Interfaces - -The library exports unified interfaces for consistent API patterns: - -```typescript -// Base menu item interface (used by tabs, menus, etc.) -interface IMenuItem { - key: string; - iconName?: string; - action: () => void; - badge?: string | number; - badgeVariant?: 'default' | 'success' | 'warning' | 'error'; -} - -// Menu group interface for organized menus -interface IMenuGroup { - name: string; - items: IMenuItem[]; - collapsed?: boolean; - iconName?: string; -} - -// View definition for app navigation -interface IViewDefinition { - id: string; - name: string; - iconName?: string; - content: string | HTMLElement | (() => TemplateResult); - secondaryMenu?: ISecondaryMenuGroup[]; - contentTabs?: IMenuItem[]; - route?: string; - badge?: string | number; -} - -// Activity log entry -interface IActivityEntry { - id?: string; - timestamp?: Date; - type: 'login' | 'logout' | 'view' | 'create' | 'update' | 'delete' | 'custom'; - user: string; - message: string; - iconName?: string; - data?: Record; -} -``` - ---- - ## License and Legal Information This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [LICENSE](./LICENSE) file. @@ -1404,15 +80,12 @@ This repository contains open-source code licensed under the MIT License. A copy ### Trademarks -This 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 or third parties, 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 or the guidelines of the respective third-party owners, and any usage must be approved in writing. Third-party trademarks used herein are the property of their respective owners and used only in a descriptive manner, e.g. for an implementation of an API or similar. +This 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 or third parties, and are not included within the scope of the MIT license granted herein. ### Company Information -Task Venture Capital GmbH -Registered at District Court Bremen HRB 35230 HB, Germany +Lossless GmbH -For any legal inquiries or further information, please contact us via email at hello@task.vc. +For any legal inquiries or further information, please contact us via the official channels. -By 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. +By 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. diff --git a/readme.playbook.md b/readme.playbook.md deleted file mode 100644 index 0d4cd47..0000000 --- a/readme.playbook.md +++ /dev/null @@ -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` -
- -
- `; - } - - // 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(); - - 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 { - // 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 { - // 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`
${this.counter}
`; -} - -// ✅ CORRECT - Pure render function -public render() { - return html`
${this.counter}
`; -} -``` - -## Form Components - -### DO: Extend DeesInputBase - -All form inputs must extend the base class: - -```typescript -export class DeesInputNew extends DeesInputBase { - // 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` - Default Button - Primary Button - Disabled Danger -`; - -// 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 { - // 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 -
- -// ✅ CORRECT -
-// 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` - - `; - } - - 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 { - public static demo = demoFunc.demoFunc; - - public render() { - return html` - - - - `; - } - - 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. \ No newline at end of file diff --git a/test/test.chromium.ts b/test/test.chromium.ts deleted file mode 100644 index 082cf4d..0000000 --- a/test/test.chromium.ts +++ /dev/null @@ -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`` - ); - expect(button).toBeInstanceOf(deesCatalog.DeesButton); -}); - -export default tap.start(); diff --git a/test/test.contextmenu-demo.chromium.ts b/test/test.contextmenu-demo.chromium.ts deleted file mode 100644 index 7a362aa..0000000 --- a/test/test.contextmenu-demo.chromium.ts +++ /dev/null @@ -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(); \ No newline at end of file diff --git a/test/test.contextmenu-nested-close.chromium.ts b/test/test.contextmenu-nested-close.chromium.ts deleted file mode 100644 index d93ab6c..0000000 --- a/test/test.contextmenu-nested-close.chromium.ts +++ /dev/null @@ -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(); \ No newline at end of file diff --git a/test/test.contextmenu-shadowdom.chromium.ts b/test/test.contextmenu-shadowdom.chromium.ts deleted file mode 100644 index 0c18630..0000000 --- a/test/test.contextmenu-shadowdom.chromium.ts +++ /dev/null @@ -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` -
-

Shadow DOM Content

-

Right-click anywhere inside this shadow DOM

-
- `; - } -} - -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(); \ No newline at end of file diff --git a/test/test.contextmenu.chromium.ts b/test/test.contextmenu.chromium.ts deleted file mode 100644 index 0e4e151..0000000 --- a/test/test.contextmenu.chromium.ts +++ /dev/null @@ -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(); \ No newline at end of file diff --git a/test/test.dashboardgrid-layout.node.ts b/test/test.dashboardgrid-layout.node.ts deleted file mode 100644 index fe4ac5a..0000000 --- a/test/test.dashboardgrid-layout.node.ts +++ /dev/null @@ -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(); diff --git a/test/test.eco-catalog.chromium.ts b/test/test.eco-catalog.chromium.ts new file mode 100644 index 0000000..49c2988 --- /dev/null +++ b/test/test.eco-catalog.chromium.ts @@ -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`` + ); + expect(applauncher).toBeInstanceOf(ecoCatalog.EcoApplauncher); +}); + +tap.test('should create a working EcoScreensaver instance', async () => { + const screensaver: ecoCatalog.EcoScreensaver = await webhelpers.fixture( + webhelpers.html`` + ); + expect(screensaver).toBeInstanceOf(ecoCatalog.EcoScreensaver); +}); + +export default tap.start(); diff --git a/test/test.shadow-dom-containment.chromium.ts b/test/test.shadow-dom-containment.chromium.ts deleted file mode 100644 index 5c4904d..0000000 --- a/test/test.shadow-dom-containment.chromium.ts +++ /dev/null @@ -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 = ` - - - - `; - 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(); \ No newline at end of file diff --git a/test/test.tabs-indicator.chromium.ts b/test/test.tabs-indicator.chromium.ts deleted file mode 100644 index f9d1251..0000000 --- a/test/test.tabs-indicator.chromium.ts +++ /dev/null @@ -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(); \ No newline at end of file diff --git a/test/test.wysiwyg-basic.chromium.ts b/test/test.wysiwyg-basic.chromium.ts deleted file mode 100644 index 4df522c..0000000 --- a/test/test.wysiwyg-basic.chromium.ts +++ /dev/null @@ -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(); \ No newline at end of file diff --git a/test/test.wysiwyg-blockmovement.chromium.ts b/test/test.wysiwyg-blockmovement.chromium.ts deleted file mode 100644 index b2a552a..0000000 --- a/test/test.wysiwyg-blockmovement.chromium.ts +++ /dev/null @@ -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(); \ No newline at end of file diff --git a/test/test.wysiwyg-blocks-debug.chromium.ts b/test/test.wysiwyg-blocks-debug.chromium.ts deleted file mode 100644 index 5cdd083..0000000 --- a/test/test.wysiwyg-blocks-debug.chromium.ts +++ /dev/null @@ -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`` - ); - 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(); \ No newline at end of file diff --git a/test/test.wysiwyg-blocks.chromium.ts b/test/test.wysiwyg-blocks.chromium.ts deleted file mode 100644 index 0a38f62..0000000 --- a/test/test.wysiwyg-blocks.chromium.ts +++ /dev/null @@ -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
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(); \ No newline at end of file diff --git a/test/test.wysiwyg-blocktype-change.chromium.ts b/test/test.wysiwyg-blocktype-change.chromium.ts deleted file mode 100644 index b5f4a5d..0000000 --- a/test/test.wysiwyg-blocktype-change.chromium.ts +++ /dev/null @@ -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 = '

This is a test paragraph

'; - 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(); \ No newline at end of file diff --git a/test/test.wysiwyg-contextmenu.chromium.ts b/test/test.wysiwyg-contextmenu.chromium.ts deleted file mode 100644 index 5a71a5a..0000000 --- a/test/test.wysiwyg-contextmenu.chromium.ts +++ /dev/null @@ -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 = '

Test paragraph

Test heading

'; - 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(); \ No newline at end of file diff --git a/test/test.wysiwyg-dragdrop-simple.chromium.ts b/test/test.wysiwyg-dragdrop-simple.chromium.ts deleted file mode 100644 index 89e7dc7..0000000 --- a/test/test.wysiwyg-dragdrop-simple.chromium.ts +++ /dev/null @@ -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(); \ No newline at end of file diff --git a/test/test.wysiwyg-dragdrop-visual.chromium.ts b/test/test.wysiwyg-dragdrop-visual.chromium.ts deleted file mode 100644 index b8bfcc5..0000000 --- a/test/test.wysiwyg-dragdrop-visual.chromium.ts +++ /dev/null @@ -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(); \ No newline at end of file diff --git a/test/test.wysiwyg-dragdrop.chromium.ts b/test/test.wysiwyg-dragdrop.chromium.ts deleted file mode 100644 index 9c29375..0000000 --- a/test/test.wysiwyg-dragdrop.chromium.ts +++ /dev/null @@ -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(); \ No newline at end of file diff --git a/test/test.wysiwyg-dragissue.chromium.ts b/test/test.wysiwyg-dragissue.chromium.ts deleted file mode 100644 index 6c4cff4..0000000 --- a/test/test.wysiwyg-dragissue.chromium.ts +++ /dev/null @@ -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((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(); \ No newline at end of file diff --git a/test/test.wysiwyg-dropindicator.chromium.ts b/test/test.wysiwyg-dropindicator.chromium.ts deleted file mode 100644 index 9ad6715..0000000 --- a/test/test.wysiwyg-dropindicator.chromium.ts +++ /dev/null @@ -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(); \ No newline at end of file diff --git a/test/test.wysiwyg-eventlisteners.chromium.ts b/test/test.wysiwyg-eventlisteners.chromium.ts deleted file mode 100644 index 4657968..0000000 --- a/test/test.wysiwyg-eventlisteners.chromium.ts +++ /dev/null @@ -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(); \ No newline at end of file diff --git a/test/test.wysiwyg-keyboard.chromium.ts b/test/test.wysiwyg-keyboard.chromium.ts deleted file mode 100644 index ec8181b..0000000 --- a/test/test.wysiwyg-keyboard.chromium.ts +++ /dev/null @@ -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`` - ); - - // 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`` - ); - - // 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`` - ); - - // 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`` - ); - - // 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`` - ); - - // 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`` - ); - - // 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('') || expect(content).toContain(''); - - console.log('Formatting shortcuts test complete'); -}); - -export default tap.start(); \ No newline at end of file diff --git a/test/test.wysiwyg-phase3.chromium.ts b/test/test.wysiwyg-phase3.chromium.ts deleted file mode 100644 index 3d6303d..0000000 --- a/test/test.wysiwyg-phase3.chromium.ts +++ /dev/null @@ -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`` - ); - - // 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`` - ); - - // 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 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`` - ); - - // 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`` - ); - - // 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(); \ No newline at end of file diff --git a/test/test.wysiwyg-registry.both.ts b/test/test.wysiwyg-registry.both.ts deleted file mode 100644 index 8937f8f..0000000 --- a/test/test.wysiwyg-registry.both.ts +++ /dev/null @@ -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('
'); - 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(); \ No newline at end of file diff --git a/test/test.wysiwyg-selection-highlight.chromium.ts b/test/test.wysiwyg-selection-highlight.chromium.ts deleted file mode 100644 index f212251..0000000 --- a/test/test.wysiwyg-selection-highlight.chromium.ts +++ /dev/null @@ -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`` - ); - - // 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`` - ); - - // 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(); \ No newline at end of file diff --git a/test/test.wysiwyg-selection-simple.chromium.ts b/test/test.wysiwyg-selection-simple.chromium.ts deleted file mode 100644 index 84dcfe3..0000000 --- a/test/test.wysiwyg-selection-simple.chromium.ts +++ /dev/null @@ -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`` - ); - - // 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(); \ No newline at end of file diff --git a/test/test.wysiwyg-split.chromium.ts b/test/test.wysiwyg-split.chromium.ts deleted file mode 100644 index 2247347..0000000 --- a/test/test.wysiwyg-split.chromium.ts +++ /dev/null @@ -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`` - ); - - // 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`` - ); - - // 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(); \ No newline at end of file diff --git a/ts_web/elements/00group-applauncher/eco-applauncher-powermenu/eco-applauncher-powermenu.demo.ts b/ts_web/elements/00group-applauncher/eco-applauncher-powermenu/eco-applauncher-powermenu.demo.ts new file mode 100644 index 0000000..8471c80 --- /dev/null +++ b/ts_web/elements/00group-applauncher/eco-applauncher-powermenu/eco-applauncher-powermenu.demo.ts @@ -0,0 +1,20 @@ +import { html } from '@design.estate/dees-element'; + +export const demo = () => html` + +
+ console.log('Power action:', e.detail.action)} + @menu-close=${() => console.log('Menu closed')} + > +
+`; diff --git a/ts_web/elements/00group-applauncher/eco-applauncher-powermenu/eco-applauncher-powermenu.ts b/ts_web/elements/00group-applauncher/eco-applauncher-powermenu/eco-applauncher-powermenu.ts new file mode 100644 index 0000000..84388f8 --- /dev/null +++ b/ts_web/elements/00group-applauncher/eco-applauncher-powermenu/eco-applauncher-powermenu.ts @@ -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 | null = null; + private readonly INACTIVITY_TIMEOUT = 60000; // 1 minute + private lastActivityTime = 0; + + public render(): TemplateResult { + return html` + + `; + } + + 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): void { + if (changedProperties.has('open')) { + if (this.open) { + this.resetInactivityTimer(); + } else { + this.clearInactivityTimer(); + } + } + } + + async connectedCallback(): Promise { + await super.connectedCallback(); + setTimeout(() => { + document.addEventListener('click', this.boundHandleClickOutside); + }, 0); + } + + async disconnectedCallback(): Promise { + await super.disconnectedCallback(); + document.removeEventListener('click', this.boundHandleClickOutside); + this.clearInactivityTimer(); + } +} diff --git a/ts_web/elements/00group-applauncher/eco-applauncher-powermenu/index.ts b/ts_web/elements/00group-applauncher/eco-applauncher-powermenu/index.ts new file mode 100644 index 0000000..8b36cfc --- /dev/null +++ b/ts_web/elements/00group-applauncher/eco-applauncher-powermenu/index.ts @@ -0,0 +1 @@ +export * from './eco-applauncher-powermenu.js'; diff --git a/ts_web/elements/00group-applauncher/eco-applauncher/eco-applauncher.demo.ts b/ts_web/elements/00group-applauncher/eco-applauncher/eco-applauncher.demo.ts index 9ccd9b5..6eb40ea 100644 --- a/ts_web/elements/00group-applauncher/eco-applauncher/eco-applauncher.demo.ts +++ b/ts_web/elements/00group-applauncher/eco-applauncher/eco-applauncher.demo.ts @@ -1,21 +1,18 @@ import { html } from '@design.estate/dees-element'; -import type { IAppIcon } from './eco-applauncher.js'; +import type { IAppIcon, ILoginConfig, ILoginCredentials, TApplauncherMode } from './eco-applauncher.js'; import type { IWifiNetwork } from '../eco-applauncher-wifimenu/index.js'; import type { IAudioDevice } from '../eco-applauncher-soundmenu/index.js'; +import '../../../views/eco-view-settings/eco-view-settings.js'; +import '../../../views/eco-view-peripherals/eco-view-peripherals.js'; +import '../../../views/eco-view-saasshare/eco-view-saasshare.js'; +import '../../../views/eco-view-system/eco-view-system.js'; +import type { EcoApplauncher } from './eco-applauncher.js'; const mockApps: IAppIcon[] = [ - { name: 'Settings', icon: 'lucide:settings', action: () => console.log('Settings clicked') }, - { name: 'Browser', icon: 'lucide:globe', action: () => console.log('Browser clicked') }, - { name: 'Terminal', icon: 'lucide:terminal', action: () => console.log('Terminal clicked') }, - { name: 'Files', icon: 'lucide:folder', action: () => console.log('Files clicked') }, - { name: 'Calendar', icon: 'lucide:calendar', action: () => console.log('Calendar clicked') }, - { name: 'Mail', icon: 'lucide:mail', action: () => console.log('Mail clicked') }, - { name: 'Music', icon: 'lucide:music', action: () => console.log('Music clicked') }, - { name: 'Photos', icon: 'lucide:image', action: () => console.log('Photos clicked') }, - { name: 'Notes', icon: 'lucide:fileText', action: () => console.log('Notes clicked') }, - { name: 'Calculator', icon: 'lucide:calculator', action: () => console.log('Calculator clicked') }, - { name: 'Weather', icon: 'lucide:cloudSun', action: () => console.log('Weather clicked') }, - { name: 'Maps', icon: 'lucide:map', action: () => console.log('Maps clicked') }, + { name: 'SaaS Share', icon: 'lucide:share2', view: html`` }, + { name: 'System', icon: 'lucide:activity', view: html`` }, + { name: 'Peripherals', icon: 'lucide:monitor', view: html`` }, + { name: 'Settings', icon: 'lucide:settings', view: html`` }, ]; const mockNetworks: IWifiNetwork[] = [ @@ -32,7 +29,33 @@ const mockAudioDevices: IAudioDevice[] = [ { id: 'hdmi', name: 'LG Monitor', type: 'hdmi' }, ]; -export const demo = () => html` +const loginConfig: ILoginConfig = { + allowedMethods: ['pin', 'password', 'qr'], + pinLength: 4, + welcomeMessage: 'Welcome to EcoBridge', +}; + +const handleLoginAttempt = (e: CustomEvent) => { + const credentials = e.detail as ILoginCredentials; + const applauncher = e.target as EcoApplauncher; + + console.log('Login attempt:', credentials); + + // Demo validation: PIN "1234" or password "demo" + if ( + (credentials.method === 'pin' && credentials.value === '1234') || + (credentials.method === 'password' && credentials.value === 'demo') + ) { + console.log('Login successful!'); + applauncher.setLoginResult(true); + } else { + console.log('Login failed'); + applauncher.setLoginResult(false, 'Invalid credentials. Try PIN: 1234 or Password: demo'); + } +}; + +// Home mode demo +const demoHome = () => html`
html` .muted=${false} .userName=${'John Doe'} .notificationCount=${3} + @login-attempt=${handleLoginAttempt} + @login-success=${() => console.log('Login success event received')} + @login-failure=${(e: CustomEvent) => console.log('Login failure:', e.detail)} @wifi-toggle=${(e: CustomEvent) => console.log('WiFi toggle:', e.detail)} @network-select=${(e: CustomEvent) => console.log('Network selected:', e.detail)} @wifi-settings-click=${() => console.log('WiFi settings clicked')} @@ -69,3 +97,37 @@ export const demo = () => html` >
`; +demoHome.demoTitle = 'Home Mode'; + +// Login mode demo +const demoLogin = () => html` + +
+ console.log('Login success event received')} + @login-failure=${(e: CustomEvent) => console.log('Login failure:', e.detail)} + > +
+`; +demoLogin.demoTitle = 'Login Mode'; + +// Export array of demo functions +export const demo = [demoHome, demoLogin]; diff --git a/ts_web/elements/00group-applauncher/eco-applauncher/eco-applauncher.ts b/ts_web/elements/00group-applauncher/eco-applauncher/eco-applauncher.ts index 37e2cf8..e7c8ada 100644 --- a/ts_web/elements/00group-applauncher/eco-applauncher/eco-applauncher.ts +++ b/ts_web/elements/00group-applauncher/eco-applauncher/eco-applauncher.ts @@ -14,6 +14,9 @@ import { EcoApplauncherWifimenu, type IWifiNetwork } from '../eco-applauncher-wi import { EcoApplauncherBatterymenu } from '../eco-applauncher-batterymenu/index.js'; import { EcoApplauncherSoundmenu, type IAudioDevice } from '../eco-applauncher-soundmenu/index.js'; import { EcoApplauncherKeyboard } from '../eco-applauncher-keyboard/index.js'; +import { EcoApplauncherPowermenu, type TPowerAction } from '../eco-applauncher-powermenu/index.js'; +import { EcoViewHome } from '../../../views/eco-view-home/index.js'; +import { EcoViewLogin, type ILoginConfig, type ILoginCredentials } from '../../../views/eco-view-login/index.js'; // Ensure components are registered DeesIcon; @@ -21,6 +24,9 @@ EcoApplauncherWifimenu; EcoApplauncherBatterymenu; EcoApplauncherSoundmenu; EcoApplauncherKeyboard; +EcoApplauncherPowermenu; +EcoViewHome; +EcoViewLogin; declare global { interface HTMLElementTagNameMap { @@ -28,12 +34,18 @@ declare global { } } +export type TApplauncherMode = 'login' | 'home'; + export interface IAppIcon { name: string; icon: string; action?: () => void; + view?: TemplateResult; } +export type { ILoginConfig, ILoginCredentials } from '../../../views/eco-view-login/index.js'; +export type { TPowerAction } from '../eco-applauncher-powermenu/index.js'; + export interface IStatusBarConfig { showTime?: boolean; showNetwork?: boolean; @@ -153,6 +165,10 @@ export class EcoApplauncher extends DeesElement { background: ${cssManager.bdTheme('hsl(220 15% 88%)', 'hsl(240 5% 15%)')}; } + .top-icon-button.active { + background: ${cssManager.bdTheme('hsl(220 15% 85%)', 'hsl(240 5% 18%)')}; + } + .user-avatar { width: 32px; height: 32px; @@ -195,9 +211,6 @@ export class EcoApplauncher extends DeesElement { .apps-area { flex: 1; - display: flex; - align-items: center; - justify-content: center; padding: 48px; overflow-y: auto; } @@ -206,7 +219,6 @@ export class EcoApplauncher extends DeesElement { display: grid; grid-template-columns: repeat(auto-fill, minmax(100px, 1fr)); gap: 32px; - max-width: 800px; width: 100%; } @@ -415,6 +427,18 @@ export class EcoApplauncher extends DeesElement { pointer-events: none; } + .topbar-menu-wrapper { + position: relative; + } + + .topbar-menu-popup { + position: absolute; + top: calc(100% + 8px); + right: 0; + z-index: 100; + pointer-events: none; + } + .keyboard-area { flex-shrink: 0; height: 0; @@ -430,6 +454,53 @@ export class EcoApplauncher extends DeesElement { overflow: visible; } + .view-area { + flex: 1; + display: flex; + flex-direction: column; + overflow: hidden; + } + + .view-header { + display: flex; + align-items: center; + gap: 12px; + padding: 12px 24px; + background: ${cssManager.bdTheme('hsl(220 15% 94%)', 'hsl(240 6% 8%)')}; + border-bottom: 1px solid ${cssManager.bdTheme('hsl(220 15% 88%)', 'hsl(240 5% 15%)')}; + flex-shrink: 0; + } + + .back-button { + display: flex; + align-items: center; + justify-content: center; + gap: 6px; + padding: 8px 14px; + background: ${cssManager.bdTheme('hsl(220 15% 88%)', 'hsl(240 5% 14%)')}; + border-radius: 8px; + cursor: pointer; + transition: background 0.15s ease; + color: ${cssManager.bdTheme('hsl(0 0% 30%)', 'hsl(0 0% 80%)')}; + font-size: 13px; + font-weight: 500; + } + + .back-button:hover { + background: ${cssManager.bdTheme('hsl(220 15% 82%)', 'hsl(240 5% 18%)')}; + } + + .view-title { + font-size: 16px; + font-weight: 600; + color: ${cssManager.bdTheme('hsl(0 0% 20%)', 'hsl(0 0% 90%)')}; + } + + .view-content { + flex: 1; + overflow: auto; + } + @media (max-width: 600px) { .apps-area { padding: 24px; @@ -462,6 +533,16 @@ export class EcoApplauncher extends DeesElement { `, ]; + @property({ type: String }) + accessor mode: TApplauncherMode = 'home'; + + @property({ type: Object }) + accessor loginConfig: ILoginConfig = { + allowedMethods: ['pin', 'password', 'qr'], + pinLength: 4, + welcomeMessage: 'Welcome', + }; + @property({ type: Array }) accessor apps: IAppIcon[] = []; @@ -512,9 +593,18 @@ export class EcoApplauncher extends DeesElement { @state() accessor soundMenuOpen = false; + @state() + accessor powerMenuOpen = false; + @state() accessor keyboardVisible = false; + @state() + accessor activeView: TemplateResult | null = null; + + @state() + accessor activeViewName: string | null = null; + @property({ type: Array }) accessor networks: IWifiNetwork[] = []; @@ -552,12 +642,8 @@ export class EcoApplauncher extends DeesElement { public render(): TemplateResult { return html`
- ${this.renderTopBar()} -
-
- ${this.apps.map((app) => this.renderAppIcon(app))} -
-
+ ${this.mode === 'login' ? '' : this.renderTopBar()} + ${this.renderMainContent()}
+ `; + } + + private renderHomeView(): TemplateResult { + return html` + + `; + } + + private renderAppsArea(): TemplateResult { + return html` +
+
+ ${this.apps.map((app) => this.renderAppIcon(app))} +
+
+ `; + } + + private renderActiveView(): TemplateResult { + return html` +
+
+ ${this.activeView} +
+
+ `; + } + private renderAppIcon(app: IAppIcon): TemplateResult { return html`
this.handleAppClick(app)}> @@ -607,7 +745,13 @@ export class EcoApplauncher extends DeesElement { return html`
- ${this.topBarConfig.showDate ? html` + ${this.activeView ? html` +
+ + Back +
+ ${this.activeViewName} + ` : this.topBarConfig.showDate ? html` ${this.currentDate} ` : ''}
@@ -636,6 +780,21 @@ export class EcoApplauncher extends DeesElement { ${userInitials}
` : ''} +
+
+ +
+
+ +
+
`; @@ -831,15 +990,91 @@ export class EcoApplauncher extends DeesElement { composed: true, }) ); + + // If app has a view, open it inside the applauncher + if (app.view) { + this.activeView = app.view; + this.activeViewName = app.name; + return; + } + + // Otherwise execute the action if (app.action) { app.action(); } } + private handleHomeAppClick(e: CustomEvent): void { + const app = e.detail.app as IAppIcon; + this.handleAppClick(app); + } + + private handleLoginAttempt(e: CustomEvent): void { + const credentials = e.detail as ILoginCredentials; + // Dispatch event for parent to handle validation + this.dispatchEvent(new CustomEvent('login-attempt', { + detail: credentials, + bubbles: true, + composed: true, + })); + } + + /** + * Set the login result after validation + * @param success Whether login was successful + * @param errorMessage Optional error message to display + */ + public setLoginResult(success: boolean, errorMessage?: string): void { + const loginView = this.shadowRoot?.querySelector('eco-view-login') as EcoViewLogin | null; + + if (success) { + this.mode = 'home'; + this.dispatchEvent(new CustomEvent('login-success', { + bubbles: true, + composed: true, + })); + } else { + if (loginView && errorMessage) { + loginView.showErrorMessage(errorMessage); + } + this.dispatchEvent(new CustomEvent('login-failure', { + detail: { error: errorMessage }, + bubbles: true, + composed: true, + })); + } + } + + /** + * Switch to login mode + */ + public showLogin(): void { + this.mode = 'login'; + this.activeView = null; + this.activeViewName = null; + } + + /** + * Switch to home mode + */ + public showHome(): void { + this.mode = 'home'; + } + + private handleBackClick(): void { + this.activeView = null; + this.activeViewName = null; + this.dispatchEvent(new CustomEvent('view-close', { + bubbles: true, + composed: true, + })); + } + private handleNetworkClick(e: MouseEvent): void { e.stopPropagation(); this.batteryMenuOpen = false; this.soundMenuOpen = false; + this.powerMenuOpen = false; this.wifiMenuOpen = !this.wifiMenuOpen; } @@ -847,6 +1082,7 @@ export class EcoApplauncher extends DeesElement { e.stopPropagation(); this.wifiMenuOpen = false; this.soundMenuOpen = false; + this.powerMenuOpen = false; this.batteryMenuOpen = !this.batteryMenuOpen; } @@ -854,9 +1090,18 @@ export class EcoApplauncher extends DeesElement { e.stopPropagation(); this.wifiMenuOpen = false; this.batteryMenuOpen = false; + this.powerMenuOpen = false; this.soundMenuOpen = !this.soundMenuOpen; } + private handlePowerClick(e: MouseEvent): void { + e.stopPropagation(); + this.wifiMenuOpen = false; + this.batteryMenuOpen = false; + this.soundMenuOpen = false; + this.powerMenuOpen = !this.powerMenuOpen; + } + private handleWifiMenuClose(): void { this.wifiMenuOpen = false; } @@ -869,6 +1114,19 @@ export class EcoApplauncher extends DeesElement { this.soundMenuOpen = false; } + private handlePowerMenuClose(): void { + this.powerMenuOpen = false; + } + + private handlePowerAction(e: CustomEvent): void { + const action = e.detail.action as TPowerAction; + this.dispatchEvent(new CustomEvent('power-action', { + detail: { action }, + bubbles: true, + composed: true, + })); + } + private handleVolumeChange(e: CustomEvent): void { this.soundLevel = e.detail.volume; this.dispatchEvent(new CustomEvent('volume-change', { @@ -953,6 +1211,7 @@ export class EcoApplauncher extends DeesElement { this.wifiMenuOpen = false; this.batteryMenuOpen = false; this.soundMenuOpen = false; + this.powerMenuOpen = false; } this.dispatchEvent(new CustomEvent('keyboard-toggle', { detail: { visible: this.keyboardVisible }, diff --git a/ts_web/elements/00group-applauncher/index.ts b/ts_web/elements/00group-applauncher/index.ts index 1ce25b4..958b416 100644 --- a/ts_web/elements/00group-applauncher/index.ts +++ b/ts_web/elements/00group-applauncher/index.ts @@ -4,5 +4,4 @@ export * from './eco-applauncher-wifimenu/index.js'; export * from './eco-applauncher-batterymenu/index.js'; export * from './eco-applauncher-soundmenu/index.js'; export * from './eco-applauncher-keyboard/index.js'; -export * from './eco-settings/index.js'; -export * from './eco-peripherals/index.js'; +export * from './eco-applauncher-powermenu/index.js'; diff --git a/ts_web/views/eco-view-home/eco-view-home.demo.ts b/ts_web/views/eco-view-home/eco-view-home.demo.ts new file mode 100644 index 0000000..1295645 --- /dev/null +++ b/ts_web/views/eco-view-home/eco-view-home.demo.ts @@ -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` + +
+ console.log('App clicked:', e.detail.app)} + > +
+`; diff --git a/ts_web/views/eco-view-home/eco-view-home.ts b/ts_web/views/eco-view-home/eco-view-home.ts new file mode 100644 index 0000000..c03b44d --- /dev/null +++ b/ts_web/views/eco-view-home/eco-view-home.ts @@ -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` +
+
+ ${this.apps.map((app) => this.renderAppIcon(app))} +
+
+ `; + } + + private renderAppIcon(app: IAppIcon): TemplateResult { + return html` +
this.handleAppClick(app)}> +
+ +
+ ${app.name} +
+ `; + } + + private handleAppClick(app: IAppIcon): void { + this.dispatchEvent( + new CustomEvent('app-click', { + detail: { app }, + bubbles: true, + composed: true, + }) + ); + } +} diff --git a/ts_web/views/eco-view-home/index.ts b/ts_web/views/eco-view-home/index.ts new file mode 100644 index 0000000..453039d --- /dev/null +++ b/ts_web/views/eco-view-home/index.ts @@ -0,0 +1 @@ +export * from './eco-view-home.js'; diff --git a/ts_web/views/eco-view-login/eco-view-login.demo.ts b/ts_web/views/eco-view-login/eco-view-login.demo.ts new file mode 100644 index 0000000..28d4149 --- /dev/null +++ b/ts_web/views/eco-view-login/eco-view-login.demo.ts @@ -0,0 +1,48 @@ +import { html } from '@design.estate/dees-element'; +import type { ILoginConfig, ILoginCredentials } from './eco-view-login.js'; + +const handleLoginAttempt = (e: CustomEvent) => { + 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` + +
+ +
+`; diff --git a/ts_web/views/eco-view-login/eco-view-login.ts b/ts_web/views/eco-view-login/eco-view-login.ts new file mode 100644 index 0000000..140ccc4 --- /dev/null +++ b/ts_web/views/eco-view-login/eco-view-login.ts @@ -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` + + `; + } + + 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` +
+ Sign in method + ${availableMethods.map((method) => html` +
this.selectMethod(method.id)} + > +
+ +
+
+
${method.name}
+
${method.description}
+
+
+ ${this.selectedMethod === method.id + ? html`` + : '' + } +
+
+ `)} +
+ `; + } + + private renderPinInput(length: number): TemplateResult { + return html` +
+ ${Array.from({ length }, (_, i) => html` +
+ `)} +
+ +
+ ${[1, 2, 3, 4, 5, 6, 7, 8, 9].map((num) => html` + + `)} + + + +
+ `; + } + + private renderPasswordInput(): TemplateResult { + return html` +
+
+ + +
+ +
+ `; + } + + private renderQrCode(): TemplateResult { + return html` +
+
+ ${this.config.qrCodeData + ? html`Login QR Code` + : html`
QR Code
` + } +
+

+ Open your authenticator app and scan this code to sign in securely without typing a password. +

+
+ `; + } + + 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 = ''; + } +} diff --git a/ts_web/views/eco-view-login/index.ts b/ts_web/views/eco-view-login/index.ts new file mode 100644 index 0000000..e078d7a --- /dev/null +++ b/ts_web/views/eco-view-login/index.ts @@ -0,0 +1 @@ +export * from './eco-view-login.js'; diff --git a/ts_web/views/eco-view-peripherals/eco-view-peripherals.demo.ts b/ts_web/views/eco-view-peripherals/eco-view-peripherals.demo.ts index 26ae86d..9bd18ed 100644 --- a/ts_web/views/eco-view-peripherals/eco-view-peripherals.demo.ts +++ b/ts_web/views/eco-view-peripherals/eco-view-peripherals.demo.ts @@ -4,18 +4,18 @@ export const demo = () => html`
- console.log('Device selected:', e.detail)} @scan-start=${() => console.log('Scanning started')} @scan-complete=${() => console.log('Scanning complete')} - > + >
`; diff --git a/ts_web/views/eco-view-peripherals/eco-view-peripherals.ts b/ts_web/views/eco-view-peripherals/eco-view-peripherals.ts index f70df0c..c5e2884 100644 --- a/ts_web/views/eco-view-peripherals/eco-view-peripherals.ts +++ b/ts_web/views/eco-view-peripherals/eco-view-peripherals.ts @@ -9,8 +9,8 @@ import { state, } from '@design.estate/dees-element'; import { DeesAppuiSecondarymenu, DeesIcon } from '@design.estate/dees-catalog'; -import type { ISecondaryMenuGroup, ISecondaryMenuItem } from '../../interfaces/secondarymenu.js'; -import { demo } from './eco-peripherals.demo.js'; +import type { ISecondaryMenuGroup, ISecondaryMenuItem } from '../../elements/interfaces/secondarymenu.js'; +import { demo } from './eco-view-peripherals.demo.js'; // Ensure components are registered DeesAppuiSecondarymenu; @@ -18,7 +18,7 @@ DeesIcon; declare global { interface HTMLElementTagNameMap { - 'eco-peripherals': EcoPeripherals; + 'eco-view-peripherals': EcoViewPeripherals; } } @@ -47,10 +47,10 @@ export interface IPeripheralDevice { isDefault?: boolean; } -@customElement('eco-peripherals') -export class EcoPeripherals extends DeesElement { +@customElement('eco-view-peripherals') +export class EcoViewPeripherals extends DeesElement { public static demo = demo; - public static demoGroup = 'App Launcher'; + public static demoGroup = 'Views'; public static styles = [ cssManager.defaultStyles, diff --git a/ts_web/views/eco-view-peripherals/index.ts b/ts_web/views/eco-view-peripherals/index.ts index 030b4cb..3c6d074 100644 --- a/ts_web/views/eco-view-peripherals/index.ts +++ b/ts_web/views/eco-view-peripherals/index.ts @@ -1 +1 @@ -export * from './eco-peripherals.js'; +export * from './eco-view-peripherals.js'; diff --git a/ts_web/views/eco-view-saasshare/eco-view-saasshare.demo.ts b/ts_web/views/eco-view-saasshare/eco-view-saasshare.demo.ts new file mode 100644 index 0000000..2f7e663 --- /dev/null +++ b/ts_web/views/eco-view-saasshare/eco-view-saasshare.demo.ts @@ -0,0 +1,20 @@ +import { html } from '@design.estate/dees-element'; + +export const demo = () => html` + +
+ console.log('Request approved:', e.detail)} + @request-denied=${(e: CustomEvent) => console.log('Request denied:', e.detail)} + > +
+`; diff --git a/ts_web/views/eco-view-saasshare/eco-view-saasshare.ts b/ts_web/views/eco-view-saasshare/eco-view-saasshare.ts new file mode 100644 index 0000000..5b289d3 --- /dev/null +++ b/ts_web/views/eco-view-saasshare/eco-view-saasshare.ts @@ -0,0 +1,1288 @@ +import { + customElement, + DeesElement, + type TemplateResult, + html, + property, + css, + cssManager, + state, +} from '@design.estate/dees-element'; +import { DeesAppuiSecondarymenu, DeesIcon } from '@design.estate/dees-catalog'; +import type { ISecondaryMenuGroup, ISecondaryMenuItem } from '../../elements/interfaces/secondarymenu.js'; +import { demo } from './eco-view-saasshare.demo.js'; + +// Ensure components are registered +DeesAppuiSecondarymenu; +DeesIcon; + +declare global { + interface HTMLElementTagNameMap { + 'eco-view-saasshare': EcoViewSaasshare; + } +} + +export type TSharePanel = + | 'apps' + | 'devices' + | 'permissions' + | 'requests' + | 'activity' + | 'security'; + +export type TPermissionType = + | 'print' + | 'scan' + | 'storage' + | 'camera' + | 'audio' + | 'display' + | 'network'; + +export interface ISaasApp { + id: string; + name: string; + domain: string; + icon?: string; + color?: string; + verified: boolean; + lastAccess?: Date; + permissions: ISaasPermission[]; +} + +export interface ISaasPermission { + type: TPermissionType; + deviceId?: string; + deviceName?: string; + granted: boolean; + grantedAt?: Date; + expiresAt?: Date; +} + +export interface IAccessRequest { + id: string; + appId: string; + appName: string; + appDomain: string; + permissionType: TPermissionType; + deviceId?: string; + deviceName?: string; + requestedAt: Date; + status: 'pending' | 'approved' | 'denied'; +} + +export interface IAccessActivity { + id: string; + appId: string; + appName: string; + permissionType: TPermissionType; + deviceName?: string; + action: string; + timestamp: Date; +} + +@customElement('eco-view-saasshare') +export class EcoViewSaasshare 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; + } + + .share-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; + display: flex; + align-items: flex-start; + justify-content: space-between; + } + + .panel-header-left { + flex: 1; + } + + .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%)')}; + } + + .header-action { + display: flex; + align-items: center; + gap: 8px; + padding: 10px 20px; + background: hsl(217 91% 60%); + color: white; + border: none; + border-radius: 10px; + font-size: 14px; + font-weight: 500; + cursor: pointer; + transition: background 0.15s ease; + } + + .header-action:hover { + background: hsl(217 91% 55%); + } + + .section { + background: ${cssManager.bdTheme('#ffffff', 'hsl(240 6% 12%)')}; + border: 1px solid ${cssManager.bdTheme('hsl(0 0% 90%)', 'hsl(240 5% 18%)')}; + border-radius: 12px; + margin-bottom: 24px; + overflow: hidden; + } + + .section-title { + padding: 16px 20px 12px; + font-size: 13px; + font-weight: 600; + color: ${cssManager.bdTheme('hsl(0 0% 50%)', 'hsl(0 0% 50%)')}; + text-transform: uppercase; + letter-spacing: 0.5px; + display: flex; + align-items: center; + justify-content: space-between; + } + + .section-count { + background: ${cssManager.bdTheme('hsl(0 0% 90%)', 'hsl(240 5% 20%)')}; + padding: 2px 8px; + border-radius: 10px; + font-size: 12px; + font-weight: 600; + } + + .app-item { + display: flex; + align-items: center; + justify-content: space-between; + padding: 16px 20px; + border-top: 1px solid ${cssManager.bdTheme('hsl(0 0% 94%)', 'hsl(240 5% 15%)')}; + transition: background 0.15s ease; + cursor: pointer; + } + + .app-item:first-child { + border-top: none; + } + + .app-item:hover { + background: ${cssManager.bdTheme('hsl(0 0% 97%)', 'hsl(240 5% 14%)')}; + } + + .app-left { + display: flex; + align-items: center; + gap: 14px; + } + + .app-icon { + width: 44px; + height: 44px; + border-radius: 10px; + display: flex; + align-items: center; + justify-content: center; + color: white; + font-size: 20px; + font-weight: 600; + } + + .app-info { + display: flex; + flex-direction: column; + gap: 4px; + } + + .app-name { + display: flex; + align-items: center; + gap: 8px; + font-size: 15px; + font-weight: 500; + color: ${cssManager.bdTheme('hsl(0 0% 10%)', 'hsl(0 0% 98%)')}; + } + + .verified-badge { + display: flex; + align-items: center; + gap: 4px; + padding: 2px 8px; + background: hsl(142 71% 45% / 0.15); + color: hsl(142 71% 40%); + border-radius: 4px; + font-size: 11px; + font-weight: 600; + } + + .app-domain { + font-size: 13px; + color: ${cssManager.bdTheme('hsl(0 0% 50%)', 'hsl(0 0% 55%)')}; + } + + .app-right { + display: flex; + align-items: center; + gap: 16px; + } + + .permission-badges { + display: flex; + gap: 6px; + } + + .permission-badge { + 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%)')}; + color: ${cssManager.bdTheme('hsl(0 0% 50%)', 'hsl(0 0% 60%)')}; + } + + .permission-badge.active { + background: hsl(217 91% 60% / 0.15); + color: hsl(217 91% 55%); + } + + .last-access { + font-size: 12px; + color: ${cssManager.bdTheme('hsl(0 0% 60%)', 'hsl(0 0% 50%)')}; + min-width: 100px; + text-align: right; + } + + .request-item { + display: flex; + align-items: center; + justify-content: space-between; + padding: 16px 20px; + border-top: 1px solid ${cssManager.bdTheme('hsl(0 0% 94%)', 'hsl(240 5% 15%)')}; + background: ${cssManager.bdTheme('hsl(45 100% 97%)', 'hsl(45 30% 12%)')}; + } + + .request-item:first-child { + border-top: none; + } + + .request-info { + display: flex; + flex-direction: column; + gap: 4px; + } + + .request-title { + font-size: 14px; + font-weight: 500; + color: ${cssManager.bdTheme('hsl(0 0% 10%)', 'hsl(0 0% 98%)')}; + } + + .request-detail { + font-size: 13px; + color: ${cssManager.bdTheme('hsl(0 0% 50%)', 'hsl(0 0% 55%)')}; + } + + .request-actions { + display: flex; + gap: 8px; + } + + .btn { + display: flex; + align-items: center; + gap: 6px; + padding: 8px 16px; + border: none; + border-radius: 8px; + font-size: 13px; + font-weight: 500; + cursor: pointer; + transition: all 0.15s ease; + } + + .btn-approve { + background: hsl(142 71% 45%); + color: white; + } + + .btn-approve:hover { + background: hsl(142 71% 40%); + } + + .btn-deny { + background: ${cssManager.bdTheme('hsl(0 0% 90%)', 'hsl(240 5% 20%)')}; + color: ${cssManager.bdTheme('hsl(0 0% 40%)', 'hsl(0 0% 70%)')}; + } + + .btn-deny:hover { + background: hsl(0 72% 51%); + color: white; + } + + .device-group { + margin-bottom: 24px; + } + + .device-header { + display: flex; + align-items: center; + gap: 12px; + padding: 16px 20px; + 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%)')}; + } + + .device-icon { + width: 36px; + height: 36px; + border-radius: 8px; + display: flex; + align-items: center; + justify-content: center; + background: ${cssManager.bdTheme('hsl(0 0% 90%)', 'hsl(240 5% 20%)')}; + color: ${cssManager.bdTheme('hsl(0 0% 40%)', 'hsl(0 0% 70%)')}; + } + + .device-info h4 { + margin: 0; + font-size: 15px; + font-weight: 500; + color: ${cssManager.bdTheme('hsl(0 0% 10%)', 'hsl(0 0% 98%)')}; + } + + .device-info span { + font-size: 13px; + color: ${cssManager.bdTheme('hsl(0 0% 50%)', 'hsl(0 0% 55%)')}; + } + + .device-apps { + padding: 0; + } + + .device-app { + display: flex; + align-items: center; + justify-content: space-between; + padding: 12px 20px 12px 68px; + border-top: 1px solid ${cssManager.bdTheme('hsl(0 0% 94%)', 'hsl(240 5% 15%)')}; + } + + .device-app:first-child { + border-top: none; + } + + .device-app-name { + font-size: 14px; + color: ${cssManager.bdTheme('hsl(0 0% 30%)', 'hsl(0 0% 80%)')}; + } + + .toggle-switch { + position: relative; + width: 44px; + height: 24px; + background: ${cssManager.bdTheme('hsl(0 0% 85%)', 'hsl(240 5% 25%)')}; + border-radius: 12px; + cursor: pointer; + transition: background 0.2s ease; + } + + .toggle-switch.active { + background: hsl(217 91% 60%); + } + + .toggle-switch::after { + content: ''; + position: absolute; + top: 2px; + left: 2px; + width: 20px; + height: 20px; + background: white; + border-radius: 50%; + transition: transform 0.2s ease; + box-shadow: ${cssManager.bdTheme('0 1px 3px rgba(0,0,0,0.2)', 'none')}; + } + + .toggle-switch.active::after { + transform: translateX(20px); + } + + .activity-item { + display: flex; + align-items: flex-start; + gap: 14px; + padding: 14px 20px; + border-top: 1px solid ${cssManager.bdTheme('hsl(0 0% 94%)', 'hsl(240 5% 15%)')}; + } + + .activity-item:first-child { + border-top: none; + } + + .activity-icon { + width: 32px; + height: 32px; + border-radius: 8px; + display: flex; + align-items: center; + justify-content: center; + background: ${cssManager.bdTheme('hsl(0 0% 94%)', 'hsl(240 5% 18%)')}; + color: ${cssManager.bdTheme('hsl(0 0% 50%)', 'hsl(0 0% 60%)')}; + flex-shrink: 0; + } + + .activity-content { + flex: 1; + } + + .activity-title { + font-size: 14px; + color: ${cssManager.bdTheme('hsl(0 0% 20%)', 'hsl(0 0% 90%)')}; + margin-bottom: 2px; + } + + .activity-title strong { + font-weight: 600; + } + + .activity-time { + font-size: 12px; + color: ${cssManager.bdTheme('hsl(0 0% 55%)', 'hsl(0 0% 50%)')}; + } + + .settings-item { + display: flex; + align-items: center; + justify-content: space-between; + padding: 14px 20px; + border-top: 1px solid ${cssManager.bdTheme('hsl(0 0% 94%)', 'hsl(240 5% 15%)')}; + } + + .settings-item:first-child { + border-top: none; + } + + .item-left { + display: flex; + align-items: center; + gap: 14px; + } + + .item-icon { + width: 32px; + height: 32px; + border-radius: 8px; + display: flex; + align-items: center; + justify-content: center; + color: white; + } + + .item-icon.blue { background: hsl(217 91% 60%); } + .item-icon.green { background: hsl(142 71% 45%); } + .item-icon.orange { background: hsl(25 95% 53%); } + .item-icon.red { background: hsl(0 72% 51%); } + .item-icon.purple { background: hsl(262 83% 58%); } + .item-icon.gray { background: hsl(220 9% 46%); } + + .item-info { + display: flex; + flex-direction: column; + gap: 2px; + } + + .item-label { + font-size: 15px; + font-weight: 500; + color: ${cssManager.bdTheme('hsl(0 0% 10%)', 'hsl(0 0% 98%)')}; + } + + .item-sublabel { + font-size: 13px; + color: ${cssManager.bdTheme('hsl(0 0% 50%)', 'hsl(0 0% 55%)')}; + } + + .empty-state { + text-align: center; + padding: 48px 20px; + color: ${cssManager.bdTheme('hsl(0 0% 50%)', 'hsl(0 0% 55%)')}; + } + + .empty-state dees-icon { + margin-bottom: 16px; + opacity: 0.5; + } + + .empty-state h3 { + margin: 0 0 8px; + font-size: 16px; + font-weight: 500; + color: ${cssManager.bdTheme('hsl(0 0% 30%)', 'hsl(0 0% 70%)')}; + } + + .empty-state p { + margin: 0; + font-size: 14px; + } + + .permission-type-row { + display: flex; + align-items: center; + justify-content: space-between; + padding: 14px 20px; + border-top: 1px solid ${cssManager.bdTheme('hsl(0 0% 94%)', 'hsl(240 5% 15%)')}; + } + + .permission-type-row:first-child { + border-top: none; + } + + .permission-type-info { + display: flex; + align-items: center; + gap: 14px; + } + + .permission-type-details h4 { + margin: 0; + font-size: 15px; + font-weight: 500; + color: ${cssManager.bdTheme('hsl(0 0% 10%)', 'hsl(0 0% 98%)')}; + } + + .permission-type-details span { + font-size: 13px; + color: ${cssManager.bdTheme('hsl(0 0% 50%)', 'hsl(0 0% 55%)')}; + } + + .permission-apps-count { + display: flex; + align-items: center; + gap: 8px; + padding: 6px 12px; + background: ${cssManager.bdTheme('hsl(0 0% 94%)', 'hsl(240 5% 18%)')}; + border-radius: 6px; + font-size: 13px; + color: ${cssManager.bdTheme('hsl(0 0% 40%)', 'hsl(0 0% 65%)')}; + cursor: pointer; + transition: background 0.15s ease; + } + + .permission-apps-count:hover { + background: ${cssManager.bdTheme('hsl(0 0% 90%)', 'hsl(240 5% 22%)')}; + } + `, + ]; + + @property({ type: String }) + accessor activePanel: TSharePanel = 'apps'; + + @state() + accessor saasApps: ISaasApp[] = [ + { + id: 'google-docs', + name: 'Google Docs', + domain: 'docs.google.com', + color: '#4285F4', + verified: true, + lastAccess: new Date(Date.now() - 1000 * 60 * 5), + permissions: [ + { type: 'print', deviceName: 'HP LaserJet Pro', granted: true }, + { type: 'storage', deviceName: 'Synology NAS', granted: true }, + ], + }, + { + id: 'figma', + name: 'Figma', + domain: 'figma.com', + color: '#F24E1E', + verified: true, + lastAccess: new Date(Date.now() - 1000 * 60 * 30), + permissions: [ + { type: 'display', granted: true }, + ], + }, + { + id: 'zoom', + name: 'Zoom', + domain: 'zoom.us', + color: '#2D8CFF', + verified: true, + lastAccess: new Date(Date.now() - 1000 * 60 * 60 * 2), + permissions: [ + { type: 'camera', deviceName: 'Logitech C920', granted: true }, + { type: 'audio', deviceName: 'Built-in Microphone', granted: true }, + { type: 'display', granted: true }, + ], + }, + { + id: 'notion', + name: 'Notion', + domain: 'notion.so', + color: '#000000', + verified: true, + lastAccess: new Date(Date.now() - 1000 * 60 * 60 * 24), + permissions: [ + { type: 'print', deviceName: 'HP LaserJet Pro', granted: true }, + ], + }, + { + id: 'dropbox', + name: 'Dropbox', + domain: 'dropbox.com', + color: '#0061FF', + verified: true, + lastAccess: new Date(Date.now() - 1000 * 60 * 60 * 48), + permissions: [ + { type: 'storage', deviceName: 'Synology NAS', granted: true }, + { type: 'scan', deviceName: 'Epson Scanner', granted: true }, + ], + }, + ]; + + @state() + accessor accessRequests: IAccessRequest[] = [ + { + id: 'req-1', + appId: 'slack', + appName: 'Slack', + appDomain: 'slack.com', + permissionType: 'camera', + deviceName: 'Logitech C920', + requestedAt: new Date(Date.now() - 1000 * 60 * 10), + status: 'pending', + }, + { + id: 'req-2', + appId: 'canva', + appName: 'Canva', + appDomain: 'canva.com', + permissionType: 'print', + deviceName: 'HP LaserJet Pro', + requestedAt: new Date(Date.now() - 1000 * 60 * 25), + status: 'pending', + }, + ]; + + @state() + accessor activities: IAccessActivity[] = [ + { + id: 'act-1', + appId: 'google-docs', + appName: 'Google Docs', + permissionType: 'print', + deviceName: 'HP LaserJet Pro', + action: 'printed document "Q4 Report.pdf"', + timestamp: new Date(Date.now() - 1000 * 60 * 5), + }, + { + id: 'act-2', + appId: 'zoom', + appName: 'Zoom', + permissionType: 'camera', + deviceName: 'Logitech C920', + action: 'accessed camera for video call', + timestamp: new Date(Date.now() - 1000 * 60 * 60 * 2), + }, + { + id: 'act-3', + appId: 'dropbox', + appName: 'Dropbox', + permissionType: 'scan', + deviceName: 'Epson Scanner', + action: 'scanned 3 pages', + timestamp: new Date(Date.now() - 1000 * 60 * 60 * 5), + }, + { + id: 'act-4', + appId: 'figma', + appName: 'Figma', + permissionType: 'display', + action: 'shared screen to external display', + timestamp: new Date(Date.now() - 1000 * 60 * 60 * 24), + }, + ]; + + @state() + accessor requireApproval = true; + + @state() + accessor autoRevokeInactive = true; + + @state() + accessor activityLogging = true; + + private getMenuGroups(): ISecondaryMenuGroup[] { + const pendingCount = this.accessRequests.filter(r => r.status === 'pending').length; + + return [ + { + name: 'Overview', + iconName: 'lucide:share2', + items: [ + { + key: 'apps', + iconName: 'lucide:layoutGrid', + action: () => this.activePanel = 'apps', + badge: this.saasApps.length.toString(), + }, + { + key: 'requests', + iconName: 'lucide:inbox', + action: () => this.activePanel = 'requests', + badge: pendingCount > 0 ? pendingCount.toString() : undefined, + }, + ], + }, + { + name: 'Browse', + iconName: 'lucide:folder', + items: [ + { + key: 'devices', + iconName: 'lucide:hardDrive', + action: () => this.activePanel = 'devices', + }, + { + key: 'permissions', + iconName: 'lucide:key', + action: () => this.activePanel = 'permissions', + }, + ], + }, + { + name: 'Monitor', + iconName: 'lucide:activity', + items: [ + { + key: 'activity', + iconName: 'lucide:clock', + action: () => this.activePanel = 'activity', + }, + { + key: 'security', + iconName: 'lucide:shield', + action: () => this.activePanel = 'security', + }, + ], + }, + ]; + } + + 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` + + `; + } + + private renderActivePanel(): TemplateResult { + switch (this.activePanel) { + case 'apps': + return this.renderAppsPanel(); + case 'devices': + return this.renderDevicesPanel(); + case 'permissions': + return this.renderPermissionsPanel(); + case 'requests': + return this.renderRequestsPanel(); + case 'activity': + return this.renderActivityPanel(); + case 'security': + return this.renderSecurityPanel(); + default: + return this.renderAppsPanel(); + } + } + + private renderAppsPanel(): TemplateResult { + return html` +
+
+
Connected Apps
+
Manage SaaS applications with access to your peripherals
+
+ +
+ + ${this.accessRequests.filter(r => r.status === 'pending').length > 0 ? html` +
+
+ Pending Requests + ${this.accessRequests.filter(r => r.status === 'pending').length} +
+ ${this.accessRequests.filter(r => r.status === 'pending').map(request => html` +
+
+
+ ${request.appName} wants access to ${this.getPermissionLabel(request.permissionType)} +
+
+ ${request.deviceName ? `Device: ${request.deviceName} • ` : ''} + Requested ${this.formatTimeAgo(request.requestedAt)} +
+
+
+ + +
+
+ `)} +
+ ` : ''} + +
+
+ All Apps + ${this.saasApps.length} +
+ ${this.saasApps.map(app => html` +
+
+
+ ${app.name.charAt(0)} +
+
+
+ ${app.name} + ${app.verified ? html` + + + Verified + + ` : ''} +
+
${app.domain}
+
+
+
+
+ ${this.renderPermissionBadges(app.permissions)} +
+
+ ${app.lastAccess ? this.formatTimeAgo(app.lastAccess) : 'Never'} +
+ +
+
+ `)} +
+ `; + } + + private renderDevicesPanel(): TemplateResult { + const devices = [ + { + name: 'HP LaserJet Pro', + type: 'print', + icon: 'lucide:printer', + apps: ['Google Docs', 'Notion'], + }, + { + name: 'Epson Scanner', + type: 'scan', + icon: 'lucide:scan', + apps: ['Dropbox'], + }, + { + name: 'Logitech C920', + type: 'camera', + icon: 'lucide:camera', + apps: ['Zoom'], + }, + { + name: 'Built-in Microphone', + type: 'audio', + icon: 'lucide:mic', + apps: ['Zoom'], + }, + { + name: 'Synology NAS', + type: 'storage', + icon: 'lucide:hardDrive', + apps: ['Google Docs', 'Dropbox'], + }, + { + name: 'External Display', + type: 'display', + icon: 'lucide:monitor', + apps: ['Zoom', 'Figma'], + }, + ]; + + return html` +
+
+
Devices
+
View which apps have access to each peripheral
+
+
+ + ${devices.map(device => html` +
+
+
+ +
+
+

${device.name}

+ ${device.apps.length} app${device.apps.length !== 1 ? 's' : ''} with access +
+
+
+ ${device.apps.map(appName => html` +
+ ${appName} +
+
+ `)} +
+
+ `)} + `; + } + + private renderPermissionsPanel(): TemplateResult { + const permissionTypes: { type: TPermissionType; icon: string; label: string; color: string; count: number }[] = [ + { type: 'print', icon: 'lucide:printer', label: 'Printing', color: 'blue', count: 2 }, + { type: 'scan', icon: 'lucide:scan', label: 'Scanning', color: 'purple', count: 1 }, + { type: 'camera', icon: 'lucide:camera', label: 'Camera', color: 'green', count: 1 }, + { type: 'audio', icon: 'lucide:mic', label: 'Microphone', color: 'red', count: 1 }, + { type: 'storage', icon: 'lucide:hardDrive', label: 'Network Storage', color: 'orange', count: 2 }, + { type: 'display', icon: 'lucide:monitor', label: 'Screen Sharing', color: 'gray', count: 2 }, + ]; + + return html` +
+
+
Permissions
+
Manage access by permission type
+
+
+ +
+ ${permissionTypes.map(perm => html` +
+
+
+ +
+
+

${perm.label}

+ ${perm.count} app${perm.count !== 1 ? 's' : ''} with access +
+
+
+ Manage + +
+
+ `)} +
+ `; + } + + private renderRequestsPanel(): TemplateResult { + const pendingRequests = this.accessRequests.filter(r => r.status === 'pending'); + + return html` +
+
+
Access Requests
+
Review and manage permission requests from SaaS applications
+
+
+ + ${pendingRequests.length > 0 ? html` +
+
+ Pending + ${pendingRequests.length} +
+ ${pendingRequests.map(request => html` +
+
+
+ ${request.appName} wants access to ${this.getPermissionLabel(request.permissionType)} +
+
+ ${request.deviceName ? `Device: ${request.deviceName} • ` : ''} + ${request.appDomain} • Requested ${this.formatTimeAgo(request.requestedAt)} +
+
+
+ + +
+
+ `)} +
+ ` : html` +
+
+ +

No pending requests

+

When SaaS apps request access to your peripherals, they'll appear here

+
+
+ `} + `; + } + + private renderActivityPanel(): TemplateResult { + return html` +
+
+
Activity Log
+
Recent peripheral access by SaaS applications
+
+
+ +
+
Recent Activity
+ ${this.activities.map(activity => html` +
+
+ +
+
+
+ ${activity.appName} ${activity.action} +
+
${this.formatTimeAgo(activity.timestamp)}
+
+
+ `)} +
+ `; + } + + private renderSecurityPanel(): TemplateResult { + return html` +
+
+
Security Settings
+
Configure how SaaS apps can access your peripherals
+
+
+ +
+
Access Control
+
+
+
+ +
+
+
Require Approval
+
Ask before granting new app access
+
+
+
this.requireApproval = !this.requireApproval} + >
+
+
+
+
+ +
+
+
Auto-revoke Inactive Apps
+
Remove access after 30 days of inactivity
+
+
+
this.autoRevokeInactive = !this.autoRevokeInactive} + >
+
+
+ +
+
Logging
+
+
+
+ +
+
+
Activity Logging
+
Record all peripheral access events
+
+
+
this.activityLogging = !this.activityLogging} + >
+
+
+ +
+
Danger Zone
+
+
+
+ +
+
+
Revoke All Access
+
Remove all SaaS app permissions
+
+
+ +
+
+ `; + } + + private renderPermissionBadges(permissions: ISaasPermission[]): TemplateResult[] { + const permissionIcons: Record = { + print: 'lucide:printer', + scan: 'lucide:scan', + storage: 'lucide:hardDrive', + camera: 'lucide:camera', + audio: 'lucide:mic', + display: 'lucide:monitor', + network: 'lucide:wifi', + }; + + return permissions.map(perm => html` +
+ +
+ `); + } + + private getPermissionLabel(type: TPermissionType): string { + const labels: Record = { + print: 'Printing', + scan: 'Scanning', + storage: 'Storage', + camera: 'Camera', + audio: 'Microphone', + display: 'Display', + network: 'Network', + }; + return labels[type]; + } + + private getPermissionIcon(type: TPermissionType): string { + const icons: Record = { + print: 'lucide:printer', + scan: 'lucide:scan', + storage: 'lucide:hardDrive', + camera: 'lucide:camera', + audio: 'lucide:mic', + display: 'lucide:monitor', + network: 'lucide:wifi', + }; + return icons[type]; + } + + private formatTimeAgo(date: Date): string { + const now = new Date(); + const diff = now.getTime() - date.getTime(); + const minutes = Math.floor(diff / 60000); + const hours = Math.floor(diff / 3600000); + const days = Math.floor(diff / 86400000); + + if (minutes < 1) return 'Just now'; + if (minutes < 60) return `${minutes}m ago`; + if (hours < 24) return `${hours}h ago`; + if (days < 7) return `${days}d ago`; + return date.toLocaleDateString(); + } + + private approveRequest(requestId: string): void { + this.accessRequests = this.accessRequests.map(r => + r.id === requestId ? { ...r, status: 'approved' as const } : r + ); + this.dispatchEvent(new CustomEvent('request-approved', { + detail: { requestId }, + bubbles: true, + composed: true, + })); + } + + private denyRequest(requestId: string): void { + this.accessRequests = this.accessRequests.map(r => + r.id === requestId ? { ...r, status: 'denied' as const } : r + ); + this.dispatchEvent(new CustomEvent('request-denied', { + detail: { requestId }, + bubbles: true, + composed: true, + })); + } +} diff --git a/ts_web/views/eco-view-saasshare/index.ts b/ts_web/views/eco-view-saasshare/index.ts new file mode 100644 index 0000000..421a95e --- /dev/null +++ b/ts_web/views/eco-view-saasshare/index.ts @@ -0,0 +1 @@ +export * from './eco-view-saasshare.js'; diff --git a/ts_web/views/eco-view-settings/eco-view-settings.demo.ts b/ts_web/views/eco-view-settings/eco-view-settings.demo.ts index 5acdb55..66f1a11 100644 --- a/ts_web/views/eco-view-settings/eco-view-settings.demo.ts +++ b/ts_web/views/eco-view-settings/eco-view-settings.demo.ts @@ -4,15 +4,15 @@ export const demo = () => html`
- + >
`; diff --git a/ts_web/views/eco-view-settings/eco-view-settings.ts b/ts_web/views/eco-view-settings/eco-view-settings.ts index 4cbf7ae..b55bf3f 100644 --- a/ts_web/views/eco-view-settings/eco-view-settings.ts +++ b/ts_web/views/eco-view-settings/eco-view-settings.ts @@ -9,8 +9,8 @@ import { state, } from '@design.estate/dees-element'; import { DeesAppuiSecondarymenu, DeesIcon } from '@design.estate/dees-catalog'; -import type { ISecondaryMenuGroup, ISecondaryMenuItem } from '../../interfaces/secondarymenu.js'; -import { demo } from './eco-settings.demo.js'; +import type { ISecondaryMenuGroup, ISecondaryMenuItem } from '../../elements/interfaces/secondarymenu.js'; +import { demo } from './eco-view-settings.demo.js'; // Ensure components are registered DeesAppuiSecondarymenu; @@ -18,7 +18,7 @@ DeesIcon; declare global { interface HTMLElementTagNameMap { - 'eco-settings': EcoSettings; + 'eco-view-settings': EcoViewSettings; } } @@ -35,10 +35,10 @@ export type TSettingsPanel = | 'updates' | 'about'; -@customElement('eco-settings') -export class EcoSettings extends DeesElement { +@customElement('eco-view-settings') +export class EcoViewSettings extends DeesElement { public static demo = demo; - public static demoGroup = 'App Launcher'; + public static demoGroup = 'Views'; public static styles = [ cssManager.defaultStyles, diff --git a/ts_web/views/eco-view-settings/index.ts b/ts_web/views/eco-view-settings/index.ts index 1b6599f..68b38de 100644 --- a/ts_web/views/eco-view-settings/index.ts +++ b/ts_web/views/eco-view-settings/index.ts @@ -1 +1 @@ -export * from './eco-settings.js'; +export * from './eco-view-settings.js'; diff --git a/ts_web/views/eco-view-system/eco-view-system.demo.ts b/ts_web/views/eco-view-system/eco-view-system.demo.ts new file mode 100644 index 0000000..f93ae36 --- /dev/null +++ b/ts_web/views/eco-view-system/eco-view-system.demo.ts @@ -0,0 +1,18 @@ +import { html } from '@design.estate/dees-element'; + +export const demo = () => html` + +
+ +
+`; diff --git a/ts_web/views/eco-view-system/eco-view-system.ts b/ts_web/views/eco-view-system/eco-view-system.ts new file mode 100644 index 0000000..ec826f0 --- /dev/null +++ b/ts_web/views/eco-view-system/eco-view-system.ts @@ -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` +
+ +
+ ${this.renderActivePanel()} +
+
+ `; + } + + 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` +
+
System Overview
+
Real-time system performance metrics
+
+ +
+ +
+ `; + } + + 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` +
+
CPU
+
Processor usage and performance
+
+ +
+ +
+ `; + } + + 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` +
+
Memory
+
RAM and swap usage details
+
+ +
+ +
+ `; + } + + 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` +
+
Storage
+
Disk usage and I/O performance
+
+ +
+ +
+ `; + } + + 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` +
+
Network
+
Network traffic and connectivity
+
+ +
+ +
+ `; + } + + 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` +
+
Processes
+
Running processes and resource usage
+
+ +
+ +
+ +
+
Top Processes by CPU
+
+
+ Process + PID + CPU % + Memory % +
+ ${topProcesses.map(proc => html` +
+ ${proc.name} + ${proc.pid} + ${proc.cpu}% + ${proc.memory}% +
+ `)} +
+
+ `; + } +} diff --git a/ts_web/views/eco-view-system/index.ts b/ts_web/views/eco-view-system/index.ts new file mode 100644 index 0000000..99a6c25 --- /dev/null +++ b/ts_web/views/eco-view-system/index.ts @@ -0,0 +1 @@ +export * from './eco-view-system.js'; diff --git a/ts_web/views/index.ts b/ts_web/views/index.ts new file mode 100644 index 0000000..ddbb266 --- /dev/null +++ b/ts_web/views/index.ts @@ -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';