Compare commits

..

29 Commits

Author SHA1 Message Date
c3fca1db36 1.8.12
Some checks failed
Default (tags) / security (push) Failing after 45s
Default (tags) / test (push) Failing after 18s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-06-19 12:10:18 +00:00
2a5e6ee37a feat: Register dees-button in dees-form-submit and import necessary components in dees-simple-login 2025-06-19 12:09:48 +00:00
41e2125dc7 1.8.11
Some checks failed
Default (tags) / security (push) Failing after 55s
Default (tags) / test (push) Failing after 37s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-06-19 11:55:09 +00:00
2a76b67e9a 1.8.10 2025-06-19 11:50:37 +00:00
d697958536 feat: Improve login event handling and form data validation in dees-simple-login component 2025-06-19 11:50:24 +00:00
1789807f90 1.8.9 2025-06-19 11:39:23 +00:00
03315db863 feat: Enhance demo components with new input types and layout options
- Added dropdown and radio input components to the demo for application settings.
- Introduced horizontal layout for display preferences and notification settings.
- Implemented checkbox demo with programmatic selection and clear functionality.
- Created file upload and quantity selector demos with various states and configurations.
- Added comprehensive radio input demo showcasing group behavior and various states.
- Developed text input demo with validation states and advanced features like password visibility.
- Introduced a new panel component for better content organization in demos.
2025-06-19 11:39:16 +00:00
79b1a4ea9f feat: Implement unified input component architecture with standardized margins and layout modes 2025-06-19 09:41:00 +00:00
8fb5e2e2a2 1.8.8 2025-06-17 11:51:47 +00:00
640a69f4cd feat: Integrate dees-statsgrid component into dashboard view with dynamic stats tiles 2025-06-17 11:51:34 +00:00
bdb666cbe2 feat: Enhance demo components with improved layout, styling, and functionality for login and dashboard views 2025-06-17 11:45:25 +00:00
8a1d830376 feat: Enhance context menu functionality with keyboard navigation and improved item handling 2025-06-17 11:39:16 +00:00
c1e8f8c2a6 feat: Enhance selection options with icons and dividers for improved UI 2025-06-17 10:00:50 +00:00
a8f0e5659e feat: Add profile dropdown component and integrate with appbar for user menu 2025-06-17 09:55:28 +00:00
cd3c7c8e63 feat: Refactor theming in app components to use dynamic CSS variables 2025-06-17 08:58:47 +00:00
5b4319432c feat: Enhance dees-appui components with dynamic tab and menu configurations
- Updated dees-appui-mainmenu to accept dynamic tabs with actions and icons.
- Modified dees-appui-mainselector to support dynamic selection options.
- Introduced dees-appui-tabs for improved tab navigation with customizable styles.
- Added dees-appui-view to manage views with tabs and content dynamically.
- Implemented event dispatching for tab and option selections.
- Created a comprehensive architecture documentation for dees-appui system.
- Added demo implementations for dees-appui-base and other components.
- Improved responsiveness and user interaction feedback across components.
2025-06-17 08:41:36 +00:00
e33f4e7a70 1.8.7 2025-06-16 23:48:47 +00:00
f101df9329 1.8.6 2025-06-16 23:48:37 +00:00
d926f5c5e4 1.8.5 2025-06-16 23:48:13 +00:00
8ad754c9bc feat(dees-appui-appbar): implement dynamic menu system with support for submenus, shortcuts, and user account features
feat(dees-contextmenu): adjust menu item positioning for improved alignment
fix(dees-appui-appbar.demo): add demo functionality for app bar with dynamic menu items and user interactions
feat(interfaces): create IAppBarMenuItem interface for enhanced menu item configurations
docs: add comprehensive improvement plan for dees-appui-appbar component
2025-06-16 23:16:25 +00:00
ed20e04e96 fix(dees-catalog): update @design.estate/dees-wcctools dependency to version 1.0.98 for compatibility and enhance demo functionality with real-time data updates 2025-06-16 22:23:22 +00:00
daef1aa841 fix(dees-catalog): update @design.estate/dees-wcctools dependency to version 1.0.97 for compatibility 2025-06-16 16:04:04 +00:00
339ea2d7d4 fix(dees-catalog): update @design.estate/dees-wcctools dependency to version 1.0.96 for compatibility and add demotools import in demo files 2025-06-16 15:11:52 +00:00
036bba44ae fix(dees-catalog): update @design.estate/dees-wcctools dependency to version 1.0.95 for compatibility
feat(dees-chart-area): refactor demo function for improved dataset handling and real-time updates
feat(dees-chart-log): enhance demo function with simulation controls for server log generation
2025-06-16 14:59:22 +00:00
48fbeb397d feat(dees-button-group): add new button group component with demo and styling
fix(dees-chart-area): improve real-time updates and chart element handling
fix(dees-chart-log): refactor demo to store log element reference
chore: update dependencies in package.json and pnpm-lock.yaml
2025-06-16 14:37:09 +00:00
346abfa685 1.8.4
Some checks failed
Default (tags) / security (push) Failing after 32s
Default (tags) / test (push) Failing after 18s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-06-12 11:11:36 +00:00
f1123f319f fix(dees-catalog): downgrade @webcontainer/api to version 1.2.0 for compatibility 2025-06-12 11:11:21 +00:00
ac15da9c82 1.8.3
Some checks failed
Default (tags) / security (push) Failing after 34s
Default (tags) / test (push) Failing after 19s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-06-12 11:09:27 +00:00
b9432c8489 feat(dees-chart-area): Enhance chart component with dynamic datasets, real-time updates, and improved demo features 2025-06-12 11:09:14 +00:00
59 changed files with 8047 additions and 1151 deletions

View File

@ -1,6 +1,6 @@
{
"name": "@design.estate/dees-catalog",
"version": "1.8.2",
"version": "1.8.12",
"private": false,
"description": "A comprehensive library that provides dynamic web components for building sophisticated and modern web applications using JavaScript and TypeScript.",
"main": "dist_ts_web/index.js",
@ -17,7 +17,7 @@
"dependencies": {
"@design.estate/dees-domtools": "^2.1.1",
"@design.estate/dees-element": "^2.0.42",
"@design.estate/dees-wcctools": "^1.0.90",
"@design.estate/dees-wcctools": "^1.0.98",
"@fortawesome/fontawesome-svg-core": "^6.7.2",
"@fortawesome/free-brands-svg-icons": "^6.7.2",
"@fortawesome/free-regular-svg-icons": "^6.7.2",
@ -26,11 +26,11 @@
"@push.rocks/smartpromise": "^4.2.0",
"@push.rocks/smartstring": "^4.0.15",
"@tsclass/tsclass": "^9.2.0",
"@webcontainer/api": "1.6.1",
"@webcontainer/api": "1.2.0",
"apexcharts": "^4.7.0",
"highlight.js": "11.11.1",
"ibantools": "^4.5.1",
"lucide": "^0.514.0",
"lucide": "^0.515.0",
"monaco-editor": "^0.52.2",
"pdfjs-dist": "^4.10.38",
"xterm": "^5.3.0",
@ -43,7 +43,7 @@
"@git.zone/tswatch": "^2.0.37",
"@push.rocks/projectinfo": "^5.0.2",
"@push.rocks/tapbundle": "^6.0.3",
"@types/node": "^24.0.0"
"@types/node": "^22.0.0"
},
"files": [
"ts/**/*",

112
pnpm-lock.yaml generated
View File

@ -15,8 +15,8 @@ importers:
specifier: ^2.0.42
version: 2.0.42
'@design.estate/dees-wcctools':
specifier: ^1.0.90
version: 1.0.90
specifier: ^1.0.98
version: 1.0.98
'@fortawesome/fontawesome-svg-core':
specifier: ^6.7.2
version: 6.7.2
@ -42,8 +42,8 @@ importers:
specifier: ^9.2.0
version: 9.2.0
'@webcontainer/api':
specifier: 1.6.1
version: 1.6.1
specifier: 1.2.0
version: 1.2.0
apexcharts:
specifier: ^4.7.0
version: 4.7.0
@ -54,8 +54,8 @@ importers:
specifier: ^4.5.1
version: 4.5.1
lucide:
specifier: ^0.514.0
version: 0.514.0
specifier: ^0.515.0
version: 0.515.0
monaco-editor:
specifier: ^0.52.2
version: 0.52.2
@ -88,8 +88,8 @@ importers:
specifier: ^6.0.3
version: 6.0.3(@aws-sdk/credential-providers@3.826.0)(socks@2.8.5)
'@types/node':
specifier: ^24.0.0
version: 24.0.0
specifier: ^22.0.0
version: 22.15.31
packages:
@ -305,8 +305,8 @@ packages:
'@design.estate/dees-element@2.0.42':
resolution: {integrity: sha512-1PzHP6q/PtSiu4P0nCxjSeHtRHn62zoSouMy8JFW2h29FT/CSDVaTUAUqYqnvwE/U98aLNivWTmerZitDF7kBQ==}
'@design.estate/dees-wcctools@1.0.90':
resolution: {integrity: sha512-EHYWHiOe+P261e9fBbOBmkD7lIsOpD+tu4VZQr20oc8vhsFjeUGJqYeBm/Ghwg+Gck/dto+K9zyJNIyQ642cEw==}
'@design.estate/dees-wcctools@1.0.98':
resolution: {integrity: sha512-6EolTGBiXgF1wgr+KOeSXAIKpXqU95FU4vOJYPPEvb+e3ebFXCuL/B4UTFZYG3e1KuTZgxiaJ04L8ejm5HfTZA==}
'@esbuild/aix-ppc64@0.24.2':
resolution: {integrity: sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA==}
@ -1609,8 +1609,8 @@ packages:
'@types/node-forge@1.3.11':
resolution: {integrity: sha512-FQx220y22OKNTqaByeBGqHWYz4cl94tpcxeFdvBo3wjG6XPBuZ0BNgNZRV5J5TFmmcsJ4IzsLkmGRiQbnYsBEQ==}
'@types/node@24.0.0':
resolution: {integrity: sha512-yZQa2zm87aRVcqDyH5+4Hv9KYgSdgwX1rFnGvpbzMaC7YAljmhBET93TPiTd3ObwTL+gSpIzPKg5BqVxdCvxKg==}
'@types/node@22.15.31':
resolution: {integrity: sha512-jnVe5ULKl6tijxUhvQeNbQG/84fHfg+yMak02cT8QVhBx/F05rAVxCGBYYTh2EKz22D6JF5ktXuNwdx7b9iEGw==}
'@types/parse5@6.0.3':
resolution: {integrity: sha512-SuT16Q1K51EAVPz1K29DJ/sXjhSQ0zjvsypYJ6tlwVsRV9jwW5Adq2ch8Dq8kDBCkYnELS7N7VNCSB5nC56t/g==}
@ -1740,8 +1740,8 @@ packages:
resolution: {integrity: sha512-84E1025aUSjvZU1j17eCTwV7m5Zg3cZHErV3+CaJM9JPCesZwLraIa0ONIQ9w4KLgcDgJFw9UnJ0LbFf42h6tg==}
engines: {node: '>=18.0.0'}
'@webcontainer/api@1.6.1':
resolution: {integrity: sha512-2RS2KiIw32BY1Icf6M1DvqSmcon9XICZCDgS29QJb2NmF12ZY2V5Ia+949hMKB3Wno+P/Y8W+sPP59PZeXSELg==}
'@webcontainer/api@1.2.0':
resolution: {integrity: sha512-tzoKBd4lLdhHy5GHFpUkl+ndoSba8JqmB7x0ZQFnWfjbcbQOvKQfxA8MEMUYhgqjWHnbrWdAfnBEHz5f5lYG5A==}
'@yr/monotone-cubic-spline@1.0.3':
resolution: {integrity: sha512-FQXkOta0XBSUPHndIKON2Y9JeQz5ZeMqLYZVVK93FliNBFm7LNMIZmY6FrMEB9XPcDbE2bekMbZD6kzDkxwYjA==}
@ -3153,8 +3153,8 @@ packages:
resolution: {integrity: sha512-MhWWlVnuab1RG5/zMRRcVGXZLCXrZTgfwMikgzCegsPnG62yDQo5JnqKkrK4jO5iKqDAZGItAqN5CtKBCBWRUA==}
engines: {node: '>=16.14'}
lucide@0.514.0:
resolution: {integrity: sha512-GQ3Rzj1qFANBvTmhe8RM3vR491RGnSjHsivA5wSuEj5taLwH1Sv4N7n6ae3ENGsYRzdrkVXZo4ieUbD1WOXmoA==}
lucide@0.515.0:
resolution: {integrity: sha512-n7tFK3R1HdsxwM5rMOa6twAi/4RGZCysc7dmk7Cr2GiOUcnHX0+MX9xw3xynpbekXCgS4+o9HeIWhBl3rZHIAA==}
make-dir@3.1.0:
resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==}
@ -4265,8 +4265,8 @@ packages:
resolution: {integrity: sha512-ZPtzy0hu4cZjv3z5NW9gfKnNLjoz4y6uv4HlelAjDK7sY/xOkKZv9xK/WQpcsBB3jEybChz9DPC2U/+cusjJVQ==}
engines: {node: '>=18'}
undici-types@7.8.0:
resolution: {integrity: sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==}
undici-types@6.21.0:
resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
unified@11.0.5:
resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==}
@ -5176,7 +5176,7 @@ snapshots:
- supports-color
- vue
'@design.estate/dees-wcctools@1.0.90':
'@design.estate/dees-wcctools@1.0.98':
dependencies:
'@design.estate/dees-domtools': 2.3.2
'@design.estate/dees-element': 2.0.42
@ -5504,7 +5504,7 @@ snapshots:
'@jest/schemas': 29.6.3
'@types/istanbul-lib-coverage': 2.0.6
'@types/istanbul-reports': 3.0.4
'@types/node': 24.0.0
'@types/node': 22.15.31
'@types/yargs': 17.0.33
chalk: 4.1.2
@ -6932,19 +6932,19 @@ snapshots:
'@types/accepts@1.3.7':
dependencies:
'@types/node': 24.0.0
'@types/node': 22.15.31
'@types/babel__code-frame@7.0.6': {}
'@types/body-parser@1.19.5':
dependencies:
'@types/connect': 3.4.38
'@types/node': 24.0.0
'@types/node': 22.15.31
'@types/body-parser@1.19.6':
dependencies:
'@types/connect': 3.4.38
'@types/node': 24.0.0
'@types/node': 22.15.31
'@types/buffer-json@2.0.3': {}
@ -6960,17 +6960,17 @@ snapshots:
'@types/clean-css@4.2.11':
dependencies:
'@types/node': 24.0.0
'@types/node': 22.15.31
source-map: 0.6.1
'@types/co-body@6.1.3':
dependencies:
'@types/node': 24.0.0
'@types/node': 22.15.31
'@types/qs': 6.14.0
'@types/connect@3.4.38':
dependencies:
'@types/node': 24.0.0
'@types/node': 22.15.31
'@types/content-disposition@0.5.9': {}
@ -6981,11 +6981,11 @@ snapshots:
'@types/connect': 3.4.38
'@types/express': 5.0.3
'@types/keygrip': 1.0.6
'@types/node': 24.0.0
'@types/node': 22.15.31
'@types/cors@2.8.17':
dependencies:
'@types/node': 24.0.0
'@types/node': 22.15.31
'@types/debounce@1.2.4': {}
@ -6999,7 +6999,7 @@ snapshots:
'@types/express-serve-static-core@5.0.6':
dependencies:
'@types/node': 24.0.0
'@types/node': 22.15.31
'@types/qs': 6.9.18
'@types/range-parser': 1.2.7
'@types/send': 0.17.4
@ -7022,30 +7022,30 @@ snapshots:
'@types/from2@2.3.5':
dependencies:
'@types/node': 24.0.0
'@types/node': 22.15.31
'@types/fs-extra@11.0.4':
dependencies:
'@types/jsonfile': 6.1.4
'@types/node': 24.0.0
'@types/node': 22.15.31
'@types/fs-extra@9.0.13':
dependencies:
'@types/node': 24.0.0
'@types/node': 22.15.31
'@types/glob@7.2.0':
dependencies:
'@types/minimatch': 5.1.2
'@types/node': 24.0.0
'@types/node': 22.15.31
'@types/glob@8.1.0':
dependencies:
'@types/minimatch': 5.1.2
'@types/node': 24.0.0
'@types/node': 22.15.31
'@types/gunzip-maybe@1.4.2':
dependencies:
'@types/node': 24.0.0
'@types/node': 22.15.31
'@types/hast@3.0.4':
dependencies:
@ -7081,7 +7081,7 @@ snapshots:
'@types/jsonfile@6.1.4':
dependencies:
'@types/node': 24.0.0
'@types/node': 22.15.31
'@types/keygrip@1.0.6': {}
@ -7098,7 +7098,7 @@ snapshots:
'@types/http-errors': 2.0.5
'@types/keygrip': 1.0.6
'@types/koa-compose': 3.2.8
'@types/node': 24.0.0
'@types/node': 22.15.31
'@types/mdast@4.0.4':
dependencies:
@ -7116,11 +7116,11 @@ snapshots:
'@types/node-forge@1.3.11':
dependencies:
'@types/node': 24.0.0
'@types/node': 22.15.31
'@types/node@24.0.0':
'@types/node@22.15.31':
dependencies:
undici-types: 7.8.0
undici-types: 6.21.0
'@types/parse5@6.0.3': {}
@ -7138,30 +7138,30 @@ snapshots:
'@types/s3rver@3.7.4':
dependencies:
'@types/node': 24.0.0
'@types/node': 22.15.31
'@types/semver@7.7.0': {}
'@types/send@0.17.4':
dependencies:
'@types/mime': 1.3.5
'@types/node': 24.0.0
'@types/node': 22.15.31
'@types/send@0.17.5':
dependencies:
'@types/mime': 1.3.5
'@types/node': 24.0.0
'@types/node': 22.15.31
'@types/serve-static@1.15.7':
dependencies:
'@types/http-errors': 2.0.4
'@types/node': 24.0.0
'@types/node': 22.15.31
'@types/send': 0.17.4
'@types/serve-static@1.15.8':
dependencies:
'@types/http-errors': 2.0.5
'@types/node': 24.0.0
'@types/node': 22.15.31
'@types/send': 0.17.5
'@types/sinon-chai@3.2.12':
@ -7181,11 +7181,11 @@ snapshots:
'@types/tar-stream@2.2.3':
dependencies:
'@types/node': 24.0.0
'@types/node': 22.15.31
'@types/through2@2.0.41':
dependencies:
'@types/node': 24.0.0
'@types/node': 22.15.31
'@types/triple-beam@1.3.5': {}
@ -7209,18 +7209,18 @@ snapshots:
'@types/whatwg-url@8.2.2':
dependencies:
'@types/node': 24.0.0
'@types/node': 22.15.31
'@types/webidl-conversions': 7.0.3
'@types/which@3.0.4': {}
'@types/ws@7.4.7':
dependencies:
'@types/node': 24.0.0
'@types/node': 22.15.31
'@types/ws@8.18.1':
dependencies:
'@types/node': 24.0.0
'@types/node': 22.15.31
'@types/yargs-parser@21.0.3': {}
@ -7230,7 +7230,7 @@ snapshots:
'@types/yauzl@2.10.3':
dependencies:
'@types/node': 24.0.0
'@types/node': 22.15.31
optional: true
'@ungap/structured-clone@1.3.0': {}
@ -7311,7 +7311,7 @@ snapshots:
- supports-color
- utf-8-validate
'@webcontainer/api@1.6.1': {}
'@webcontainer/api@1.2.0': {}
'@yr/monotone-cubic-spline@1.0.3': {}
@ -7857,7 +7857,7 @@ snapshots:
engine.io@6.6.4:
dependencies:
'@types/cors': 2.8.17
'@types/node': 24.0.0
'@types/node': 22.15.31
accepts: 1.3.8
base64id: 2.0.0
cookie: 0.7.2
@ -8643,7 +8643,7 @@ snapshots:
jest-util@29.7.0:
dependencies:
'@jest/types': 29.6.3
'@types/node': 24.0.0
'@types/node': 22.15.31
chalk: 4.1.2
ci-info: 3.9.0
graceful-fs: 4.2.11
@ -8846,7 +8846,7 @@ snapshots:
lru-cache@8.0.5: {}
lucide@0.514.0: {}
lucide@0.515.0: {}
make-dir@3.1.0:
dependencies:
@ -10217,7 +10217,7 @@ snapshots:
uint8array-extras@1.4.0: {}
undici-types@7.8.0: {}
undici-types@6.21.0: {}
unified@11.0.5:
dependencies:

View File

@ -0,0 +1,513 @@
# Building Applications with dees-appui Architecture
## Overview
The dees-appui system provides a comprehensive framework for building desktop-style web applications with a consistent layout, navigation, and view management system. This document outlines the architecture and best practices for building applications using these components.
## Core Architecture
### Component Hierarchy
```
dees-appui-base
├── dees-appui-appbar (top menu bar)
├── dees-appui-mainmenu (left sidebar - primary navigation)
├── dees-appui-mainselector (second sidebar - contextual navigation)
├── dees-appui-maincontent (main content area)
│ └── dees-appui-view (view container)
│ └── dees-appui-tabs (tab navigation within views)
└── dees-appui-activitylog (right sidebar - optional)
```
### View-Based Architecture
The system is built around the concept of **Views** - self-contained modules that represent different sections of your application. Each view can have:
- Its own tabs for sub-navigation
- Menu items for the selector (contextual navigation)
- Content areas with dynamic loading
- State management
- Event handling
## Implementation Plan
### Phase 1: Application Shell Setup
```typescript
// app-shell.ts
import { LitElement, html, css } from 'lit';
import { customElement, property } from 'lit/decorators.js';
import type { IAppView } from '@design.estate/dees-catalog';
@customElement('my-app-shell')
export class MyAppShell extends LitElement {
@property({ type: Array })
views: IAppView[] = [];
@property({ type: String })
activeViewId: string = '';
render() {
const activeView = this.views.find(v => v.id === this.activeViewId);
return html`
<dees-appui-base
.appbarMenuItems=${this.getAppBarMenuItems()}
.appbarBreadcrumbs=${this.getBreadcrumbs()}
.appbarTheme=${'dark'}
.appbarUser=${{ name: 'User', status: 'online' }}
.mainmenuTabs=${this.getMainMenuTabs()}
.mainselectorOptions=${activeView?.menuItems || []}
@mainmenu-tab-select=${this.handleMainMenuSelect}
@mainselector-option-select=${this.handleSelectorSelect}
>
<dees-appui-view
slot="maincontent"
.viewConfig=${activeView}
@view-tab-select=${this.handleViewTabSelect}
></dees-appui-view>
</dees-appui-base>
`;
}
}
```
### Phase 2: View Definition
```typescript
// views/dashboard-view.ts
export const dashboardView: IAppView = {
id: 'dashboard',
name: 'Dashboard',
description: 'System overview and metrics',
iconName: 'home',
tabs: [
{
key: 'overview',
iconName: 'chart-line',
action: () => console.log('Overview selected'),
content: () => html`
<dashboard-overview></dashboard-overview>
`
},
{
key: 'metrics',
iconName: 'tachometer-alt',
action: () => console.log('Metrics selected'),
content: () => html`
<dashboard-metrics></dashboard-metrics>
`
},
{
key: 'alerts',
iconName: 'bell',
action: () => console.log('Alerts selected'),
content: () => html`
<dashboard-alerts></dashboard-alerts>
`
}
],
menuItems: [
{ key: 'Time Range', action: () => showTimeRangeSelector() },
{ key: 'Refresh Rate', action: () => showRefreshSettings() },
{ key: 'Export Data', action: () => exportDashboardData() }
]
};
```
### Phase 3: View Management System
```typescript
// services/view-manager.ts
export class ViewManager {
private views: Map<string, IAppView> = new Map();
private activeView: IAppView | null = null;
private viewCache: Map<string, any> = new Map();
registerView(view: IAppView) {
this.views.set(view.id, view);
}
async activateView(viewId: string) {
const view = this.views.get(viewId);
if (!view) throw new Error(`View ${viewId} not found`);
// Deactivate current view
if (this.activeView) {
await this.deactivateView(this.activeView.id);
}
// Activate new view
this.activeView = view;
// Update navigation
this.updateMainSelector(view.menuItems);
this.updateBreadcrumbs(view);
// Load view data if needed
if (!this.viewCache.has(viewId)) {
await this.loadViewData(view);
}
return view;
}
private async loadViewData(view: IAppView) {
// Implement lazy loading of view data
const viewData = await import(`./views/${view.id}/data.js`);
this.viewCache.set(view.id, viewData);
}
}
```
### Phase 4: Navigation Integration
```typescript
// navigation/app-navigation.ts
export class AppNavigation {
constructor(
private viewManager: ViewManager,
private appShell: MyAppShell
) {}
setupMainMenu(): ITab[] {
return [
{
key: 'dashboard',
iconName: 'home',
action: () => this.navigateToView('dashboard')
},
{
key: 'projects',
iconName: 'folder',
action: () => this.navigateToView('projects')
},
{
key: 'analytics',
iconName: 'chart-bar',
action: () => this.navigateToView('analytics')
},
{
key: 'settings',
iconName: 'cog',
action: () => this.navigateToView('settings')
}
];
}
async navigateToView(viewId: string) {
const view = await this.viewManager.activateView(viewId);
this.appShell.activeViewId = viewId;
// Update URL
window.history.pushState(
{ viewId },
view.name,
`/${viewId}`
);
}
handleBrowserNavigation() {
window.addEventListener('popstate', (event) => {
if (event.state?.viewId) {
this.navigateToView(event.state.viewId);
}
});
}
}
```
### Phase 5: Dynamic View Loading
```typescript
// views/view-loader.ts
export class ViewLoader {
private loadedViews: Set<string> = new Set();
async loadView(viewId: string): Promise<IAppView> {
if (this.loadedViews.has(viewId)) {
return this.getViewConfig(viewId);
}
// Dynamic import
const viewModule = await import(`./views/${viewId}/index.js`);
const viewConfig = viewModule.default as IAppView;
// Register custom elements if needed
if (viewModule.registerElements) {
await viewModule.registerElements();
}
this.loadedViews.add(viewId);
return viewConfig;
}
async preloadViews(viewIds: string[]) {
const promises = viewIds.map(id => this.loadView(id));
await Promise.all(promises);
}
}
```
## Best Practices
### 1. View Organization
```
src/
├── views/
│ ├── dashboard/
│ │ ├── index.ts # View configuration
│ │ ├── data.ts # Data fetching/management
│ │ ├── components/ # View-specific components
│ │ │ ├── dashboard-overview.ts
│ │ │ ├── dashboard-metrics.ts
│ │ │ └── dashboard-alerts.ts
│ │ └── styles.ts # View-specific styles
│ ├── projects/
│ │ └── ...
│ └── settings/
│ └── ...
├── services/
│ ├── view-manager.ts
│ ├── navigation.ts
│ └── state-manager.ts
└── app-shell.ts
```
### 2. State Management
```typescript
// services/state-manager.ts
export class StateManager {
private viewStates: Map<string, any> = new Map();
saveViewState(viewId: string, state: any) {
this.viewStates.set(viewId, {
...this.getViewState(viewId),
...state,
lastUpdated: Date.now()
});
}
getViewState(viewId: string): any {
return this.viewStates.get(viewId) || {};
}
// Persist to localStorage
persistState() {
const serialized = JSON.stringify(
Array.from(this.viewStates.entries())
);
localStorage.setItem('app-state', serialized);
}
restoreState() {
const saved = localStorage.getItem('app-state');
if (saved) {
const entries = JSON.parse(saved);
this.viewStates = new Map(entries);
}
}
}
```
### 3. View Communication
```typescript
// events/view-events.ts
export class ViewEventBus {
private eventTarget = new EventTarget();
emit(eventName: string, detail: any) {
this.eventTarget.dispatchEvent(
new CustomEvent(eventName, { detail })
);
}
on(eventName: string, handler: (detail: any) => void) {
this.eventTarget.addEventListener(eventName, (e: CustomEvent) => {
handler(e.detail);
});
}
// Cross-view communication
sendMessage(fromView: string, toView: string, message: any) {
this.emit('view-message', {
from: fromView,
to: toView,
message
});
}
}
```
### 4. Responsive Design
```typescript
// views/responsive-view.ts
export const createResponsiveView = (config: IAppView): IAppView => {
return {
...config,
tabs: config.tabs.map(tab => ({
...tab,
content: () => html`
<div class="view-content ${getDeviceClass()}">
${tab.content()}
</div>
`
}))
};
};
function getDeviceClass(): string {
const width = window.innerWidth;
if (width < 768) return 'mobile';
if (width < 1024) return 'tablet';
return 'desktop';
}
```
### 5. Performance Optimization
```typescript
// optimization/lazy-components.ts
export const lazyComponent = (
importFn: () => Promise<any>,
componentName: string
) => {
let loaded = false;
return () => {
if (!loaded) {
importFn().then(() => {
loaded = true;
});
return html`<dees-spinner></dees-spinner>`;
}
return html`<${componentName}></${componentName}>`;
};
};
// Usage in view
tabs: [
{
key: 'heavy-component',
content: lazyComponent(
() => import('./components/heavy-component.js'),
'heavy-component'
)
}
]
```
## Advanced Features
### 1. View Permissions
```typescript
interface IAppViewWithPermissions extends IAppView {
requiredPermissions?: string[];
visibleTo?: (user: User) => boolean;
}
class PermissionManager {
canAccessView(view: IAppViewWithPermissions, user: User): boolean {
if (view.visibleTo) {
return view.visibleTo(user);
}
if (view.requiredPermissions) {
return view.requiredPermissions.every(
perm => user.permissions.includes(perm)
);
}
return true;
}
}
```
### 2. View Lifecycle Hooks
```typescript
interface IAppViewLifecycle extends IAppView {
onActivate?: () => Promise<void>;
onDeactivate?: () => Promise<void>;
onTabChange?: (oldTab: string, newTab: string) => void;
onDestroy?: () => void;
}
```
### 3. Dynamic Menu Generation
```typescript
class DynamicMenuBuilder {
buildMainMenu(views: IAppView[], user: User): ITab[] {
return views
.filter(view => this.canShowInMenu(view, user))
.map(view => ({
key: view.id,
iconName: view.iconName || 'file',
action: () => this.navigation.navigateToView(view.id)
}));
}
buildSelectorMenu(view: IAppView, context: any): ISelectionOption[] {
const baseItems = view.menuItems || [];
const contextItems = this.getContextualItems(view, context);
return [...baseItems, ...contextItems];
}
}
```
## Migration Strategy
For existing applications:
1. **Identify Views**: Map existing routes/pages to views
2. **Extract Components**: Move page-specific components into view folders
3. **Define View Configs**: Create IAppView configurations
4. **Update Navigation**: Replace existing routing with view navigation
5. **Migrate State**: Move page state to ViewManager
6. **Test & Optimize**: Ensure smooth transitions and performance
## Example Application Structure
```typescript
// main.ts
import { ViewManager } from './services/view-manager.js';
import { AppNavigation } from './services/navigation.js';
import { dashboardView } from './views/dashboard/index.js';
import { projectsView } from './views/projects/index.js';
import { settingsView } from './views/settings/index.js';
const app = new MyAppShell();
const viewManager = new ViewManager();
const navigation = new AppNavigation(viewManager, app);
// Register views
viewManager.registerView(dashboardView);
viewManager.registerView(projectsView);
viewManager.registerView(settingsView);
// Setup navigation
app.views = [dashboardView, projectsView, settingsView];
navigation.setupMainMenu();
navigation.handleBrowserNavigation();
// Initial navigation
navigation.navigateToView('dashboard');
document.body.appendChild(app);
```
This architecture provides:
- **Modularity**: Each view is self-contained
- **Scalability**: Easy to add new views
- **Performance**: Lazy loading and caching
- **Consistency**: Unified navigation and layout
- **Flexibility**: Customizable per view
- **Maintainability**: Clear separation of concerns

View File

@ -9,9 +9,23 @@
- Fully functional area chart component using ApexCharts
- Displays time-series data with gradient fills
- Responsive with ResizeObserver (debounced to prevent flicker)
- Demo shows CPU and memory usage metrics
- Fixed: Chart now properly respects container boundaries on initial render
- Overflow prevention with proper CSS containment
- Enhanced demo features:
- Multiple dataset examples (System Usage, Network Traffic, Sales Analytics)
- Real-time data simulation with automatic updates
- Dynamic dataset switching
- Customizable Y-axis formatters (percentages, currency, units)
- Data randomization for testing
- Manual data point addition
- Properties:
- `label`: Chart title
- `series`: ApexAxisChartSeries data
- `yAxisFormatter`: Custom Y-axis label formatter function
- Methods:
- `updateSeries()`: Update chart data
- `appendData()`: Add new data points to existing series
- Demo uses global reference to access chart element (window.__demoChartElement)
### dees-chart-log
- Server log viewer component (not a chart despite the name)
@ -28,3 +42,20 @@
- Demo includes realistic server log simulation
- Note: In demos, buttons use `@clicked` event (not `@click`)
- Demo uses global reference to access log element (window.__demoLogElement)
## UI Components
### dees-button-group
- Groups multiple buttons together with a unified background
- Properties:
- `label`: Optional label text displayed before the buttons
- `direction`: 'horizontal' | 'vertical' layout
- Features:
- Light/dark theme support
- Flexible layout with proper spacing
- Works with all button types (normal, highlighted, success, danger)
- Use cases:
- View mode selectors
- Action grouping
- Navigation options
- Filter controls

256
readme.md
View File

@ -306,17 +306,119 @@ Submit button component specifically designed for `DeesForm`.
### Layout Components
#### `DeesAppuiBase`
Base container component for application layout structure.
Base container component for application layout structure with integrated appbar, menu system, and content areas.
```typescript
<dees-appui-base>
<dees-appui-mainmenu></dees-appui-mainmenu>
<dees-appui-mainselector></dees-appui-mainselector>
<dees-appui-maincontent></dees-appui-maincontent>
<dees-appui-appbar></dees-appui-appbar>
<dees-appui-base
// Appbar configuration
.appbarMenuItems=${[
{
name: 'File',
action: async () => {},
submenu: [
{ name: 'New', shortcut: 'Cmd+N', iconName: 'file-plus', action: async () => {} },
{ name: 'Open', shortcut: 'Cmd+O', iconName: 'folder-open', action: async () => {} },
{ divider: true },
{ name: 'Save', shortcut: 'Cmd+S', iconName: 'save', action: async () => {} }
]
},
{
name: 'Edit',
action: async () => {},
submenu: [
{ name: 'Undo', shortcut: 'Cmd+Z', iconName: 'undo', action: async () => {} },
{ name: 'Redo', shortcut: 'Cmd+Shift+Z', iconName: 'redo', action: async () => {} }
]
}
]}
.appbarBreadcrumbs=${'Dashboard > Overview'}
.appbarTheme=${'dark'}
.appbarUser=${{
name: 'John Doe',
status: 'online'
}}
.appbarShowSearch=${true}
.appbarShowWindowControls=${true}
// Main menu configuration (left sidebar)
.mainmenuTabs=${[
{ key: 'dashboard', iconName: 'home', action: () => {} },
{ key: 'projects', iconName: 'folder', action: () => {} },
{ key: 'settings', iconName: 'cog', action: () => {} }
]}
.mainmenuSelectedTab=${selectedTab}
// Selector configuration (second sidebar)
.mainselectorOptions=${[
{ key: 'Overview', action: () => {} },
{ key: 'Components', action: () => {} },
{ key: 'Services', action: () => {} }
]}
.mainselectorSelectedOption=${selectedOption}
// Main content tabs
.maincontentTabs=${[
{ key: 'tab1', iconName: 'file', action: () => {} }
]}
// Event handlers
@appbar-menu-select=${(e) => handleMenuSelect(e.detail)}
@appbar-breadcrumb-navigate=${(e) => handleBreadcrumbNav(e.detail)}
@appbar-search-click=${() => handleSearch()}
@appbar-user-menu-open=${() => handleUserMenu()}
@mainmenu-tab-select=${(e) => handleTabSelect(e.detail)}
@mainselector-option-select=${(e) => handleOptionSelect(e.detail)}
>
<div slot="maincontent">
<!-- Your main application content goes here -->
</div>
</dees-appui-base>
```
Key Features:
- **Integrated Layout System**: Automatically arranges appbar, sidebars, and content area
- **Centralized Configuration**: Pass properties to all child components from one place
- **Event Propagation**: All child component events are re-emitted for easy handling
- **Responsive Grid**: Uses CSS Grid for flexible, responsive layout
- **Slot Support**: Main content area supports custom content via slots
Layout Structure:
```
┌─────────────────────────────────────────────────┐
│ AppBar │
├────┬──────────────┬─────────────────┬──────────┤
│ │ │ │ │
│ M │ Selector │ Main Content │ Activity │
│ e │ │ │ Log │
│ n │ │ │ │
│ u │ │ │ │
│ │ │ │ │
└────┴──────────────┴─────────────────┴──────────┘
```
Grid Configuration:
- Main Menu: 60px width
- Selector: 240px width
- Main Content: Flexible (1fr)
- Activity Log: 240px width
Child Component Access:
```typescript
// Access child components after firstUpdated
const base = document.querySelector('dees-appui-base');
base.appbar; // DeesAppuiAppbar instance
base.mainmenu; // DeesAppuiMainmenu instance
base.mainselector; // DeesAppuiMainselector instance
base.maincontent; // DeesAppuiMaincontent instance
base.activitylog; // DeesAppuiActivitylog instance
```
Best Practices:
1. **Configuration**: Set all properties on the base component for consistency
2. **Event Handling**: Listen to events on the base component rather than child components
3. **Content**: Use the `maincontent` slot for your application's primary interface
4. **State Management**: Manage selected tabs and options at the base component level
#### `DeesAppuiMainmenu`
Main navigation menu component for application-wide navigation.
@ -378,28 +480,148 @@ Main content area with tab management support.
```
#### `DeesAppuiAppbar`
Top application bar with actions and status information.
Professional application bar component with hierarchical menus, breadcrumb navigation, and user account management.
```typescript
<dees-appui-appbar
title="My Application"
.actions=${[
.menuItems=${[
{
icon: 'bell',
label: 'Notifications',
action: () => showNotifications()
name: 'File',
action: async () => {}, // No-op for parent menu items
submenu: [
{
name: 'New File',
shortcut: 'Cmd+N',
iconName: 'file-plus',
action: async () => handleNewFile()
},
{
icon: 'user',
label: 'Profile',
action: () => showProfile()
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
}
]
},
{
name: 'Edit',
action: async () => {},
submenu: [
{ name: 'Undo', shortcut: 'Cmd+Z', iconName: 'undo', action: async () => handleUndo() },
{ name: 'Redo', shortcut: 'Cmd+Shift+Z', iconName: 'redo', action: async () => handleRedo() }
]
}
]}
showSearch // Optional: display search bar
@search=${handleSearch}
.breadcrumbs=${'Project > src > components > AppBar.ts'}
.breadcrumbSeparator=${' > '}
.showWindowControls=${true}
.showSearch=${true}
.theme=${'dark'} // Options: 'light' | 'dark'
.user=${{
name: 'John Doe',
avatar: '/path/to/avatar.jpg', // Optional
status: 'online' // Options: 'online' | 'offline' | 'busy' | 'away'
}}
@menu-select=${(e) => handleMenuSelect(e.detail.item)}
@breadcrumb-navigate=${(e) => handleBreadcrumbClick(e.detail)}
@search-click=${() => handleSearchClick()}
@user-menu-open=${() => handleUserMenuOpen()}
></dees-appui-appbar>
```
Key Features:
- **Hierarchical Menu System**
- Top-level text-only menus (following desktop UI standards)
- Dropdown submenus with icons and keyboard shortcuts
- Support for nested submenus
- Menu dividers for visual grouping
- Disabled state support
- **Keyboard Navigation**
- Tab navigation between top-level items
- Arrow keys for dropdown navigation (Up/Down in dropdowns, Left/Right between top items)
- Enter to select items
- Escape to close dropdowns
- Home/End keys for first/last item
- **Breadcrumb Navigation**
- Customizable breadcrumb trail
- Configurable separator
- Click events for navigation
- **User Account Section**
- User avatar with fallback to initials
- Status indicator (online, offline, busy, away)
- Click handler for user menu
- **Visual Features**
- Light and dark theme support
- Smooth animations and transitions
- Window controls integration
- Search icon with click handler
- Responsive layout using CSS Grid
- **Accessibility**
- Full ARIA support (menubar, menuitem roles)
- Keyboard navigation
- Focus management
- Screen reader compatible
Menu Item Interface:
```typescript
// Regular menu item
interface IAppBarMenuItemRegular {
name: string; // Display text
action: () => Promise<any>; // Click handler
iconName?: string; // Optional icon (for dropdown items)
shortcut?: string; // Keyboard shortcut display
submenu?: IAppBarMenuItem[]; // Nested menu items
disabled?: boolean; // Disabled state
checked?: boolean; // For checkbox menu items
radioGroup?: string; // For radio button menu items
}
// Divider item
interface IAppBarMenuDivider {
divider: true;
}
// Combined type
type IAppBarMenuItem = IAppBarMenuItemRegular | IAppBarMenuDivider;
```
Best Practices:
1. **Menu Structure**
- Keep top-level menus text-only (no icons)
- Use icons in dropdown items for visual clarity
- Group related actions with dividers
- Provide keyboard shortcuts for common actions
2. **Navigation**
- Use breadcrumbs for deep navigation hierarchies
- Keep breadcrumb labels concise
- Provide meaningful navigation events
3. **User Experience**
- Show user status when relevant
- Provide clear visual feedback
- Ensure smooth transitions
- Handle edge cases (long menus, small screens)
4. **Accessibility**
- Always provide text labels
- Ensure keyboard navigation works
- Test with screen readers
- Maintain focus management
#### `DeesMobileNavigation`
Responsive navigation component for mobile devices.

213
readme.plan.md Normal file
View File

@ -0,0 +1,213 @@
# Input Component Unification Plan
Command to reread guidelines: `cat /home/philkunz/.claude/CLAUDE.md`
## Problem Summary
The dees-input components have inconsistent margin behavior causing vertical alignment issues in horizontal flexbox layouts:
- **dees-input-text**: 8px top, 24px bottom margin
- **dees-input-dropdown**: 0px top, 24px bottom margin
- **dees-input-checkbox/radio**: 20px top, 20px bottom margin
- Different components use different label implementations (some use dees-label, others have built-in labels)
## Proposed Solution
### 1. Standardize Margin System
Create a unified margin approach for all input components:
```css
/* Default vertical stacking mode (for forms) */
:host {
margin: 0;
margin-bottom: 16px; /* Reduced from 24px for better density */
}
/* Last child in container should have no bottom margin */
:host(:last-child) {
margin-bottom: 0;
}
/* Horizontal layout mode - activated by parent context or attribute */
:host([horizontal-layout]) {
margin: 0;
margin-right: 16px;
margin-bottom: 0;
}
:host([horizontal-layout]:last-child) {
margin-right: 0;
}
```
### 2. Unified Label Architecture
All input components should use the `dees-label` component for consistency:
- Move label rendering from built-in implementations to `dees-label` usage
- Add a `labelPosition` property to all inputs: `'top' | 'left' | 'right' | 'none'`
- Default to 'top' for text/dropdown, 'right' for checkbox/radio
### 3. Layout Mode Support
Add a `layoutMode` property to all input components:
```typescript
@property({ type: String })
public layoutMode: 'vertical' | 'horizontal' | 'auto' = 'auto';
```
- `vertical`: Traditional form layout (label on top)
- `horizontal`: Inline layout (label position configurable)
- `auto`: Detect from parent context
### 4. Implementation Steps
1. **Create base input class** (`DeesInputBase`):
- Common margin styles
- Layout mode detection
- Label position handling
- Shared properties (key, required, disabled, value)
2. **Update dees-input-text**:
- Extend from DeesInputBase
- Remove hardcoded margins
- Keep using dees-label component
3. **Update dees-input-dropdown**:
- Extend from DeesInputBase
- Remove hardcoded margins
- Switch from built-in label to dees-label
4. **Update dees-input-checkbox**:
- Extend from DeesInputBase
- Remove hardcoded margins
- Add support for label position (keep default as 'right')
- Switch to dees-label component
5. **Update dees-input-radio**:
- Same as checkbox
6. **Update dees-form**:
- Add property to control child input layout mode
- Ensure proper spacing context
### 5. CSS Variable System
Introduce CSS variables for consistent spacing:
```css
:host {
--dees-input-spacing-unit: 8px;
--dees-input-vertical-gap: calc(var(--dees-input-spacing-unit) * 2); /* 16px */
--dees-input-horizontal-gap: calc(var(--dees-input-spacing-unit) * 2); /* 16px */
--dees-input-label-gap: var(--dees-input-spacing-unit); /* 8px */
}
```
### 6. Backward Compatibility
- Keep existing properties and methods
- Add deprecation notices for properties that will be removed
- Provide migration guide in documentation
### 7. Testing Requirements
- Test all inputs in vertical form layouts
- Test all inputs in horizontal flexbox containers
- Test mixed input types in same container
- Test with and without labels
- Test theme switching (light/dark)
- Test responsive behavior
## Expected Outcome
- All input components will align properly in horizontal layouts
- Consistent spacing in vertical forms
- Unified label handling across all inputs
- Better developer experience with predictable behavior
- Maintained backward compatibility
## Timeline
1. Phase 1: Create DeesInputBase class and update dees-input-text ✅
2. Phase 2: Update remaining input components ✅
3. Phase 3: Update documentation and examples ✅
4. Phase 4: Testing and refinement ✅
## Implementation Status
### Completed:
1. **Created DeesInputBase class** (`dees-input-base.ts`):
- Generic base class with unified margin system
- Layout mode support (vertical/horizontal/auto)
- Label position control
- Common properties and methods
- CSS variables for consistent spacing
2. **Updated all input components**:
- `dees-input-text`: Now extends DeesInputBase, margins removed
- `dees-input-dropdown`: Now extends DeesInputBase, uses dees-label
- `dees-input-checkbox`: Now extends DeesInputBase, uses dees-label (default label position: right)
- `dees-input-radio`: Now extends DeesInputBase, uses dees-label (default label position: right)
- `dees-input-phone`: Now extends DeesInputBase with phone formatting functionality
- `dees-input-iban`: Now extends DeesInputBase with IBAN validation
- `dees-input-quantityselector`: Now extends DeesInputBase
- `dees-input-multitoggle`: Now extends DeesInputBase with value property for forms
- `dees-input-typelist`: Now extends DeesInputBase
- `dees-input-fileupload`: Now extends DeesInputBase, uses dees-label
3. **Updated dees-form**:
- Added `horizontal-layout` property
- Auto-detection of layout mode for child inputs
- Added dropdown to form input types
4. **Fixed TypeScript errors**:
- Added value property to dropdown for form compatibility
- Fixed changeSubject typing
- Updated form value type to include dropdown options
- Fixed firstUpdated method signatures (phone, iban, fileupload)
- Fixed CSS-in-JS errors in quantityselector (removed dynamic references)
- Added value property to multitoggle for form compatibility
- Removed duplicate properties in fileupload (label, key, disabled, required)
### Result:
All input components now have:
- Unified 16px bottom margin in vertical layouts
- 16px right margin in horizontal layouts
- No margin on last child
- Consistent label handling via dees-label
- Flexible layout modes
- Better alignment in flexbox containers
## Demo Improvements
### Created external demo files:
1. **dees-input-text.demo.ts**: Comprehensive demos showing:
- Basic text inputs with descriptions
- Horizontal layout examples
- Label position variations
- Validation states
- Password input features
2. **dees-input-checkbox.demo.ts**: Interactive demos featuring:
- Basic checkbox usage
- Horizontal layout groups
- Feature selection with batch operations
- Real-world examples
3. **dees-input-radio.demo.ts**: Radio button demos including:
- Radio groups with proper behavior
- Horizontal yes/no questions
- Survey-style layouts
- Settings examples
### Updated existing demos:
1. **dees-input-dropdown.demo.ts**: Enhanced with dees-demowrapper and comprehensive examples
2. **dees-form.demo.ts**: Added horizontal form layout examples and advanced form features
3. **dees-simple-appdash.demo.ts**: Enhanced settings view with horizontal forms and radio groups
All demos now use the `dees-demowrapper` component for consistency and include proper styling for light/dark themes.

View File

@ -22,15 +22,15 @@ export class DeesAppuiActivitylog extends DeesElement {
cssManager.defaultStyles,
css`
:host {
color: #fff;
color: ${cssManager.bdTheme('#333', '#fff')};
position: relative;
display: block;
width: 100%;
max-width: 300px;
height: 100%;
background: #111c28;
background: ${cssManager.bdTheme('#f8f8f8', '#111c28')};
font-family: 'Intel One Mono', sans-serif;
border-left: 1px solid #202020;
border-left: 1px solid ${cssManager.bdTheme('#e0e0e0', '#202020')};
cursor: default;
}
.maincontainer {
@ -47,7 +47,8 @@ export class DeesAppuiActivitylog extends DeesElement {
height: 32px;
width: 100%;
padding: 0px 12px 0px 12px;
background: #0e151f;
background: ${cssManager.bdTheme('#ffffff', '#0e151f')};
border-bottom: 1px solid ${cssManager.bdTheme('#e0e0e0', '#202020')};
}
.topbar .heading {
@ -57,6 +58,7 @@ export class DeesAppuiActivitylog extends DeesElement {
font-weight: 500;
font-size: 14px;
font-family: 'Geist Sans', sans-serif;
color: ${cssManager.bdTheme('#666', '#ccc')};
}
.activityContainer {
@ -73,7 +75,7 @@ export class DeesAppuiActivitylog extends DeesElement {
font-size: 12px;
text-align: center;
padding-top: 16px;
color: #888
color: ${cssManager.bdTheme('#666', '#888')}
}
.streamingIndicator.bottom {
@ -85,19 +87,19 @@ export class DeesAppuiActivitylog extends DeesElement {
min-height: 30px;
font-size: 12px;
padding: 8px 16px;
border-bottom: 1px dotted #ffffff20;
border-bottom: 1px dotted ${cssManager.bdTheme('#00000020', '#ffffff20')};
}
.activityentry:last-of-type {
border-bottom: 1px solid #ffffff00;
border-bottom: 1px solid transparent;
}
.activityentry:hover {
background: #00000080;
background: ${cssManager.bdTheme('#00000005', '#00000080')};
}
.timestamp {
color: #ff8787;
color: ${cssManager.bdTheme('#e57373', '#ff8787')};
}
.searchbox {
@ -105,10 +107,11 @@ export class DeesAppuiActivitylog extends DeesElement {
bottom: 0px;
width: 100%;
height: 40px;
background: #0e151f;
background: ${cssManager.bdTheme('#ffffff', '#0e151f')};
border-top: 1px solid ${cssManager.bdTheme('#e0e0e0', '#202020')};
}
.searchbox input {
color: #fff;
color: ${cssManager.bdTheme('#333', '#fff')};
background: none;
width: 100%;
height: 40px;
@ -127,7 +130,10 @@ export class DeesAppuiActivitylog extends DeesElement {
width: 100%;
height: 32px;
bottom: 40px;
background: linear-gradient(180deg, #111c2800 0%, #0e151f 100%);
background: ${cssManager.bdTheme(
'linear-gradient(180deg, #f8f8f800 0%, #ffffff 100%)',
'linear-gradient(180deg, #111c2800 0%, #0e151f 100%)'
)};
pointer-events: none;
}
.topShadow {
@ -135,7 +141,10 @@ export class DeesAppuiActivitylog extends DeesElement {
width: 100%;
height: 32px;
top: 32px;
background: linear-gradient(0deg, #111c2800 0%, #0e151f 100%);
background: ${cssManager.bdTheme(
'linear-gradient(0deg, #f8f8f800 0%, #ffffff 100%)',
'linear-gradient(0deg, #111c2800 0%, #0e151f 100%)'
)};
pointer-events: none;
}
`,

View File

@ -0,0 +1,211 @@
import { html, css } from '@design.estate/dees-element';
import type { DeesAppuiBar } from './dees-appui-appbar.js';
import type { IAppBarMenuItem } from './interfaces/appbarmenuitem.js';
import '@design.estate/dees-wcctools/demotools';
export const demoFunc = () => {
// Sample menu items with various configurations
// Note: Following standard desktop UI patterns, top-level menu items don't have icons
// Icons are only used in dropdown menu items for better visual hierarchy
const menuItems: IAppBarMenuItem[] = [
{
name: 'File',
action: async () => {}, // No-op action for menu with submenu
submenu: [
{ name: 'New File', shortcut: 'Cmd+N', iconName: 'file-plus', action: async () => console.log('New file') },
{ name: 'Open...', shortcut: 'Cmd+O', iconName: 'folder-open', action: async () => console.log('Open') },
{ name: 'Open Recent', action: async () => {}, submenu: [
{ name: 'project-alpha.ts', action: async () => console.log('Open recent 1') },
{ name: 'config.json', action: async () => console.log('Open recent 2') },
{ name: 'readme.md', action: async () => console.log('Open recent 3') },
]},
{ divider: true },
{ name: 'Save', shortcut: 'Cmd+S', iconName: 'save', action: async () => console.log('Save') },
{ name: 'Save As...', shortcut: 'Cmd+Shift+S', action: async () => console.log('Save as'), disabled: true },
{ divider: true },
{ name: 'Exit', shortcut: 'Cmd+Q', action: async () => console.log('Exit') },
]
},
{
name: 'Edit',
action: async () => {}, // No-op action for menu with submenu
submenu: [
{ name: 'Undo', shortcut: 'Cmd+Z', iconName: 'undo', action: async () => console.log('Undo') },
{ name: 'Redo', shortcut: 'Cmd+Shift+Z', iconName: 'redo', action: async () => console.log('Redo') },
{ divider: true },
{ name: 'Cut', shortcut: 'Cmd+X', iconName: 'scissors', action: async () => console.log('Cut') },
{ name: 'Copy', shortcut: 'Cmd+C', iconName: 'copy', action: async () => console.log('Copy') },
{ name: 'Paste', shortcut: 'Cmd+V', iconName: 'clipboard', action: async () => console.log('Paste') },
{ divider: true },
{ name: 'Find', shortcut: 'Cmd+F', iconName: 'search', action: async () => console.log('Find') },
{ name: 'Replace', shortcut: 'Cmd+H', action: async () => console.log('Replace') },
]
},
{
name: 'View',
action: async () => {}, // No-op action for menu with submenu
submenu: [
{ name: 'Toggle Fullscreen', shortcut: 'F11', iconName: 'expand', action: async () => console.log('Fullscreen') },
{ name: 'Zoom In', shortcut: 'Cmd++', iconName: 'zoom-in', action: async () => console.log('Zoom in') },
{ name: 'Zoom Out', shortcut: 'Cmd+-', iconName: 'zoom-out', action: async () => console.log('Zoom out') },
{ name: 'Reset Zoom', shortcut: 'Cmd+0', action: async () => console.log('Reset zoom') },
{ divider: true },
{ name: 'Toggle Sidebar', shortcut: 'Cmd+B', action: async () => console.log('Toggle sidebar') },
{ name: 'Toggle Terminal', shortcut: 'Cmd+J', iconName: 'terminal', action: async () => console.log('Toggle terminal') },
]
},
{
name: 'Help',
action: async () => {}, // No-op action for menu with submenu
submenu: [
{ name: 'Documentation', iconName: 'book', action: async () => console.log('Documentation') },
{ name: 'Release Notes', iconName: 'file-text', action: async () => console.log('Release notes') },
{ divider: true },
{ name: 'Report Issue', iconName: 'bug', action: async () => console.log('Report issue') },
{ name: 'About', iconName: 'info', action: async () => console.log('About') },
]
}
];
return html`
<dees-demowrapper .runAfterRender=${async (elementArg: HTMLElement) => {
const appbar = elementArg.querySelector('#appbar') as DeesAppuiBar;
// Set up status toggle
const statusButtons = elementArg.querySelectorAll('.status-toggle dees-button');
statusButtons[0].addEventListener('click', () => {
appbar.user = { ...appbar.user, status: 'online' };
});
statusButtons[1].addEventListener('click', () => {
appbar.user = { ...appbar.user, status: 'busy' };
});
statusButtons[2].addEventListener('click', () => {
appbar.user = { ...appbar.user, status: 'away' };
});
statusButtons[3].addEventListener('click', () => {
appbar.user = { ...appbar.user, status: 'offline' };
});
// Set up window controls toggle
const windowControlsButton = elementArg.querySelector('.window-controls-toggle dees-button');
windowControlsButton.addEventListener('click', () => {
appbar.showWindowControls = !appbar.showWindowControls;
});
// Set up breadcrumb buttons
const breadcrumbButtons = elementArg.querySelectorAll('.breadcrumb-toggle dees-button');
breadcrumbButtons[0].addEventListener('click', () => {
appbar.breadcrumbs = 'Home > Documents > Projects > MyApp > src > index.ts';
});
breadcrumbButtons[1].addEventListener('click', () => {
appbar.breadcrumbs = 'Dashboard';
});
}}>
<style>
${css`
.demo-container {
height: 600px;
width: 100%;
background: #1a1a1a;
display: flex;
flex-direction: column;
}
.content {
flex: 1;
padding: 20px;
color: #ccc;
}
.controls {
padding: 20px;
display: flex;
gap: 16px;
flex-wrap: wrap;
}
.control-group {
display: flex;
flex-direction: column;
gap: 8px;
}
.control-group label {
font-size: 12px;
color: #888;
}
`}
</style>
<div class="demo-container">
<dees-appui-appbar
id="appbar"
.menuItems=${menuItems}
.breadcrumbs=${'Project > src > components > AppBar.ts'}
.breadcrumbSeparator=${' > '}
.showWindowControls=${true}
.showSearch=${true}
.theme=${'dark'}
.user=${{
name: 'John Doe',
status: 'online' as 'online' | 'offline' | 'busy' | 'away'
}}
@menu-select=${(e: CustomEvent) => console.log('Menu selected:', e.detail.item)}
@breadcrumb-navigate=${(e: CustomEvent) => console.log('Breadcrumb clicked:', e.detail)}
@search-click=${() => console.log('Search clicked')}
@user-menu-open=${() => console.log('User menu clicked')}
></dees-appui-appbar>
<div class="content">
<h2>App Bar Demo</h2>
<p>This demo shows various features of the app bar component:</p>
<ul>
<li>Dynamic menu items with icons, shortcuts, and submenus</li>
<li>Breadcrumb navigation</li>
<li>User account section with status indicator</li>
<li>Search icon</li>
<li>Window controls (platform-specific)</li>
<li>Dark/light theme support</li>
<li>Keyboard navigation (Tab, Enter, Escape)</li>
<li>Custom events for all interactions</li>
</ul>
</div>
<div class="controls">
<div class="control-group">
<label>Theme</label>
<dees-button-group class="theme-toggle">
<dees-button>Dark</dees-button>
<dees-button>Light</dees-button>
</dees-button-group>
</div>
<div class="control-group">
<label>User Status</label>
<dees-button-group class="status-toggle">
<dees-button>Online</dees-button>
<dees-button>Busy</dees-button>
<dees-button>Away</dees-button>
<dees-button>Offline</dees-button>
</dees-button-group>
</div>
<div class="control-group">
<label>Window Controls</label>
<dees-button-group class="window-controls-toggle">
<dees-button>Toggle</dees-button>
</dees-button-group>
</div>
<div class="control-group">
<label>Breadcrumbs</label>
<dees-button-group class="breadcrumb-toggle">
<dees-button>Long Path</dees-button>
<dees-button>Short Path</dees-button>
</dees-button-group>
</div>
</div>
</div>
</dees-demowrapper>
`;
};

View File

@ -1,59 +1,310 @@
import {
DeesElement,
type TemplateResult,
property,
customElement,
property,
state,
html,
css,
cssManager,
} from '@design.estate/dees-element';
import * as domtools from '@design.estate/dees-domtools';
import * as interfaces from './interfaces/index.js';
import * as plugins from './00plugins.js';
import { demoFunc } from './dees-appui-appbar.demo.js';
// Import required components
import './dees-icon.js';
import './dees-windowcontrols.js';
import './dees-appui-profiledropdown.js';
declare global {
interface HTMLElementTagNameMap {
'dees-appui-appbar': DeesAppuiBar;
}
}
@customElement('dees-appui-appbar')
export class DeesAppuiBar extends DeesElement {
public static demo = () => html`<dees-appui-appbar></dees-appui-appbar>`;
public static demo = demoFunc;
// INSTANCE PROPERTIES
@property({ type: Array })
public menuItems: interfaces.IAppBarMenuItem[] = [];
@property({ type: String })
public breadcrumbs: string = '';
@property({ type: String })
public breadcrumbSeparator: string = ' > ';
@property({ type: Boolean })
public showWindowControls: boolean = true;
@property({ type: Object })
public user?: {
name: string;
email?: string;
avatar?: string;
status?: 'online' | 'offline' | 'busy' | 'away';
};
@property({ type: Array })
public profileMenuItems: (plugins.tsclass.website.IMenuItem & { shortcut?: string } | { divider: true })[] = [];
@property({ type: Boolean })
public showSearch: boolean = false;
// STATE
@state()
private activeMenu: string | null = null;
@state()
private openDropdowns: Set<string> = new Set();
@state()
private focusedItem: string | null = null;
@state()
private focusedDropdownItem: number = -1;
@state()
private isProfileDropdownOpen: boolean = false;
public static styles = [
cssManager.defaultStyles,
css`
:host {
/* CSS Variables for theming */
--appbar-height: 40px;
--appbar-font-size: 12px;
display: block;
position: relative;
height: 100%;
width: 100%;
height: 40px;
border-bottom: 1px solid #202020;
background: #000000;
color: #ffffff80;
font-size: 12px;
height: var(--appbar-height);
border-bottom: 1px solid ${cssManager.bdTheme('#e0e0e0', '#202020')};
background: ${cssManager.bdTheme('#ffffff', '#000000')};
color: ${cssManager.bdTheme('#00000080', '#ffffff80')};
font-size: var(--appbar-font-size);
display: grid;
grid-template-columns: ${cssManager.cssGridColumns(3, 20)};
-webkit-app-region: drag;
user-select: none;
}
.menus {
display: flex;
padding-left: 8px;
align-items: center;
gap: 4px;
padding: 0 8px;
cursor: default;
}
.menuItem {
position: relative;
line-height: 24px;
padding: 0px 8px;
padding: 0px 12px;
margin: 8px 0px;
border-radius: 4px;
-webkit-app-region: no-drag;
transition: all 0.2s ease;
cursor: default;
outline: none;
display: flex;
align-items: center;
gap: 4px;
}
/* Optional: Style for menu items with icons (not typically used for top-level items) */
.menuItem dees-icon {
font-size: 14px;
opacity: 0.8;
}
.menuItem:hover {
background: #ffffff20;
background: ${cssManager.bdTheme('#00000010', '#ffffff20')};
color: ${cssManager.bdTheme('#000000', '#ffffff')};
}
.menuItem.active {
background: ${cssManager.bdTheme('#00000020', '#ffffff30')};
color: ${cssManager.bdTheme('#000000', '#ffffff')};
}
.menuItem[disabled] {
opacity: 0.5;
cursor: not-allowed;
pointer-events: none;
}
.menuItem:focus-visible {
box-shadow: 0 0 0 2px ${cssManager.bdTheme('#00000080', '#ffffff80')};
}
/* Dropdown styles */
.dropdown {
position: absolute;
top: 100%;
left: 0;
min-width: 200px;
background: ${cssManager.bdTheme('#ffffff', '#000000')};
border: 1px solid ${cssManager.bdTheme('#e0e0e0', '#202020')};
border-radius: 4px;
box-shadow: ${cssManager.bdTheme('0 4px 12px rgba(0, 0, 0, 0.15)', '0 4px 12px rgba(0, 0, 0, 0.3)')};
margin-top: 4px;
z-index: 1000;
opacity: 0;
transform: translateY(-10px);
transition: opacity 0.2s, transform 0.2s;
pointer-events: none;
}
.dropdown.open {
opacity: 1;
transform: translateY(0);
pointer-events: auto;
}
.dropdown-item {
padding: 8px 16px;
cursor: default;
display: flex;
align-items: center;
gap: 8px;
transition: background 0.1s;
}
.dropdown-item:hover,
.dropdown-item.focused {
background: ${cssManager.bdTheme('#00000010', '#ffffff20')};
}
.dropdown-divider {
height: 1px;
background: ${cssManager.bdTheme('#e0e0e0', '#202020')};
margin: 4px 0;
}
.dropdown-item[disabled] {
opacity: 0.5;
cursor: not-allowed;
pointer-events: none;
}
.dropdown-item .shortcut {
margin-left: auto;
opacity: 0.6;
font-size: 11px;
}
/* Breadcrumbs */
.breadcrumbs {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
padding: 0 16px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.breadcrumb-item {
color: ${cssManager.bdTheme('#00000080', '#ffffff80')};
cursor: default;
transition: color 0.2s;
}
.breadcrumb-item:hover {
color: ${cssManager.bdTheme('#000000', '#ffffff')};
}
.breadcrumb-separator {
margin: 0 8px;
opacity: 0.5;
}
/* Account section */
.account {
display: flex;
align-items: center;
justify-content: flex-end;
padding: 0 16px;
gap: 12px;
}
.search-icon {
cursor: default;
opacity: 0.7;
transition: opacity 0.2s;
}
.search-icon:hover {
opacity: 1;
}
.user-info {
display: flex;
align-items: center;
gap: 8px;
cursor: default;
padding: 4px 8px;
border-radius: 4px;
transition: background 0.2s;
}
.user-info:hover {
background: ${cssManager.bdTheme('#00000010', '#ffffff20')};
}
.user-avatar {
position: relative;
width: 24px;
height: 24px;
line-height: 24px;
margin: 8px;
border-radius: 8px;
text-align: center;
border-radius: 50%;
background: ${cssManager.bdTheme('#00000020', '#ffffff30')};
display: flex;
align-items: center;
justify-content: center;
font-size: 10px;
font-weight: bold;
}
.user-avatar img {
width: 100%;
height: 100%;
border-radius: 50%;
object-fit: cover;
}
.user-status {
position: absolute;
bottom: -2px;
right: -2px;
width: 8px;
height: 8px;
border-radius: 50%;
border: 2px solid ${cssManager.bdTheme('#ffffff', '#000000')};
}
.user-status.online {
background: #4caf50;
}
.user-status.offline {
background: #757575;
}
.user-status.busy {
background: #f44336;
}
.user-status.away {
background: #ff9800;
}
`,
];
@ -62,16 +313,391 @@ export class DeesAppuiBar extends DeesElement {
public render(): TemplateResult {
return html`
<div class="menus">
<dees-windowcontrols></dees-windowcontrols>
<div class="menuItem">File</div>
<div class="menuItem">View</div>
<div class="menuItem">Help</div>
<div class="menuItem">Terminal</div>
${this.showWindowControls ? html`<dees-windowcontrols></dees-windowcontrols>` : ''}
${this.renderMenuItems()}
</div>
<div class="breadcrumbs">
tool:social.io > org:design.estate > prop:lossless.com
${this.renderBreadcrumbs()}
</div>
<div class="account">
${this.renderAccountSection()}
</div>
<div class="account"></div>
`;
}
private renderMenuItems(): TemplateResult {
return html`
${this.menuItems.map((item, index) => this.renderMenuItem(item, `menu-${index}`))}
`;
}
private renderMenuItem(item: interfaces.IAppBarMenuItem, itemId: string): TemplateResult {
if ('divider' in item && item.divider) {
return html`<div class="dropdown-divider"></div>`;
}
const menuItem = item as interfaces.IAppBarMenuItemRegular;
const isActive = this.activeMenu === itemId;
const hasSubmenu = menuItem.submenu && menuItem.submenu.length > 0;
return html`
<div
class="menuItem ${isActive ? 'active' : ''}"
?disabled=${menuItem.disabled}
tabindex="${menuItem.disabled ? -1 : 0}"
data-item-id="${itemId}"
@click=${() => this.handleMenuClick(menuItem, itemId)}
@keydown=${(e: KeyboardEvent) => this.handleMenuKeydown(e, menuItem, itemId)}
role="menuitem"
aria-haspopup="${hasSubmenu}"
aria-expanded="${isActive}"
>
${menuItem.iconName ? html`<dees-icon .icon="${`lucide:${menuItem.iconName}`}"></dees-icon>` : ''}
${menuItem.name}
${hasSubmenu ? this.renderDropdown(menuItem.submenu, itemId, isActive) : ''}
</div>
`;
}
private renderDropdown(items: interfaces.IAppBarMenuItem[], parentId: string, isOpen: boolean): TemplateResult {
return html`
<div
class="dropdown ${isOpen ? 'open' : ''}"
@click=${(e: Event) => e.stopPropagation()}
@keydown=${(e: KeyboardEvent) => this.handleDropdownKeydown(e, items, parentId)}
tabindex="${isOpen ? 0 : -1}"
role="menu"
>
${items.map((item, index) => this.renderDropdownItem(item, `${parentId}-${index}`))}
</div>
`;
}
private renderDropdownItem(item: interfaces.IAppBarMenuItem, itemId: string): TemplateResult {
if ('divider' in item && item.divider) {
return html`<div class="dropdown-divider"></div>`;
}
const menuItem = item as interfaces.IAppBarMenuItemRegular;
const itemIndex = parseInt(itemId.split('-').pop() || '0');
const isFocused = this.focusedDropdownItem === itemIndex;
return html`
<div
class="dropdown-item ${isFocused ? 'focused' : ''}"
?disabled=${menuItem.disabled}
@click=${() => this.handleDropdownItemClick(menuItem)}
@mouseenter=${() => this.focusedDropdownItem = itemIndex}
role="menuitem"
tabindex="${menuItem.disabled ? -1 : 0}"
>
${menuItem.iconName ? html`<dees-icon .icon="${`lucide:${menuItem.iconName}`}"></dees-icon>` : ''}
<span>${menuItem.name}</span>
${menuItem.shortcut ? html`<span class="shortcut">${menuItem.shortcut}</span>` : ''}
</div>
`;
}
private renderBreadcrumbs(): TemplateResult {
if (!this.breadcrumbs) {
return html``;
}
const parts = this.breadcrumbs.split(this.breadcrumbSeparator);
return html`
${parts.map((part, index) => html`
${index > 0 ? html`<span class="breadcrumb-separator">${this.breadcrumbSeparator}</span>` : ''}
<span
class="breadcrumb-item"
@click=${() => this.handleBreadcrumbClick(part, index)}
>
${part}
</span>
`)}
`;
}
private renderAccountSection(): TemplateResult {
return html`
${this.showSearch ? html`
<dees-icon
class="search-icon"
.icon=${'lucide:search'}
@click=${this.handleSearchClick}
></dees-icon>
` : ''}
${this.user ? html`
<div style="position: relative;">
<div class="user-info" @click=${this.handleUserClick}>
<div class="user-avatar">
${this.user.avatar ?
html`<img src="${this.user.avatar}" alt="${this.user.name}">` :
html`${this.user.name.charAt(0).toUpperCase()}`
}
${this.user.status ? html`
<div class="user-status ${this.user.status}"></div>
` : ''}
</div>
<span>${this.user.name}</span>
</div>
<dees-appui-profiledropdown
.user=${this.user}
.menuItems=${this.profileMenuItems}
.isOpen=${this.isProfileDropdownOpen}
.position=${'top-right'}
@menu-select=${(e: CustomEvent) => this.handleProfileMenuSelect(e)}
></dees-appui-profiledropdown>
</div>
` : ''}
`;
}
// Event handlers
private handleMenuClick(item: interfaces.IAppBarMenuItemRegular, itemId: string) {
if (item.disabled) return;
if (item.submenu && item.submenu.length > 0) {
// Toggle dropdown
if (this.activeMenu === itemId) {
this.activeMenu = null;
} else {
this.activeMenu = itemId;
}
} else {
// Execute action
this.activeMenu = null;
if (item.action) {
item.action();
}
this.dispatchEvent(new CustomEvent('menu-select', {
detail: { item },
bubbles: true,
composed: true
}));
}
}
private handleDropdownItemClick(item: interfaces.IAppBarMenuItemRegular) {
if (item.disabled) return;
this.activeMenu = null;
if (item.action) {
item.action();
}
this.dispatchEvent(new CustomEvent('menu-select', {
detail: { item },
bubbles: true,
composed: true
}));
}
private handleMenuKeydown(e: KeyboardEvent, item: interfaces.IAppBarMenuItemRegular, itemId: string) {
switch (e.key) {
case 'Enter':
case ' ':
e.preventDefault();
this.handleMenuClick(item, itemId);
break;
case 'ArrowDown':
if (item.submenu && this.activeMenu === itemId) {
e.preventDefault();
// Focus first non-disabled item in dropdown
this.focusedDropdownItem = 0;
const firstValidItem = this.findNextValidItem(item.submenu, -1, 1);
if (firstValidItem !== -1) {
this.focusedDropdownItem = firstValidItem;
// Focus the dropdown element
setTimeout(() => {
const dropdown = this.renderRoot.querySelector('.dropdown.open');
if (dropdown) {
(dropdown as HTMLElement).focus();
}
}, 0);
}
}
break;
case 'Escape':
this.activeMenu = null;
this.focusedDropdownItem = -1;
break;
case 'Tab':
// Let default tab navigation work but close dropdown
if (this.activeMenu === itemId) {
this.activeMenu = null;
this.focusedDropdownItem = -1;
}
break;
case 'ArrowRight':
e.preventDefault();
this.focusNextMenuItem(itemId, 1);
break;
case 'ArrowLeft':
e.preventDefault();
this.focusNextMenuItem(itemId, -1);
break;
}
}
private handleBreadcrumbClick(breadcrumb: string, index: number) {
this.dispatchEvent(new CustomEvent('breadcrumb-navigate', {
detail: { breadcrumb, index },
bubbles: true,
composed: true
}));
}
private handleSearchClick() {
this.dispatchEvent(new CustomEvent('search-click', {
bubbles: true,
composed: true
}));
}
private handleUserClick() {
this.isProfileDropdownOpen = !this.isProfileDropdownOpen;
// Also emit the event for backward compatibility
this.dispatchEvent(new CustomEvent('user-menu-open', {
bubbles: true,
composed: true
}));
}
private handleProfileMenuSelect(e: CustomEvent) {
this.isProfileDropdownOpen = false;
// Re-emit the event
this.dispatchEvent(new CustomEvent('profile-menu-select', {
detail: e.detail,
bubbles: true,
composed: true
}));
}
// Lifecycle
async connectedCallback() {
await super.connectedCallback();
// Add global click listener to close dropdowns
this.addEventListener('click', this.handleGlobalClick);
document.addEventListener('click', this.handleDocumentClick);
}
async disconnectedCallback() {
await super.disconnectedCallback();
document.removeEventListener('click', this.handleDocumentClick);
}
private handleGlobalClick = (e: Event) => {
// Prevent closing when clicking inside
e.stopPropagation();
}
private handleDocumentClick = () => {
// Close all dropdowns when clicking outside
this.activeMenu = null;
this.focusedDropdownItem = -1;
// Note: Profile dropdown handles its own outside clicks
}
private handleDropdownKeydown(e: KeyboardEvent, items: interfaces.IAppBarMenuItem[], _parentId: string) {
const validItems = items.filter(item => !('divider' in item && item.divider));
switch (e.key) {
case 'ArrowDown':
e.preventDefault();
const nextIndex = this.findNextValidItem(items, this.focusedDropdownItem, 1);
if (nextIndex !== -1) {
this.focusedDropdownItem = nextIndex;
}
break;
case 'ArrowUp':
e.preventDefault();
const prevIndex = this.findNextValidItem(items, this.focusedDropdownItem, -1);
if (prevIndex !== -1) {
this.focusedDropdownItem = prevIndex;
}
break;
case 'Enter':
e.preventDefault();
if (this.focusedDropdownItem !== -1) {
const focusedItem = validItems[this.focusedDropdownItem];
if (focusedItem && 'action' in focusedItem && !focusedItem.disabled) {
this.handleDropdownItemClick(focusedItem as interfaces.IAppBarMenuItemRegular);
}
}
break;
case 'Home':
e.preventDefault();
const firstIndex = this.findNextValidItem(items, -1, 1);
if (firstIndex !== -1) {
this.focusedDropdownItem = firstIndex;
}
break;
case 'End':
e.preventDefault();
const lastIndex = this.findNextValidItem(items, items.length, -1);
if (lastIndex !== -1) {
this.focusedDropdownItem = lastIndex;
}
break;
case 'Escape':
e.preventDefault();
this.activeMenu = null;
this.focusedDropdownItem = -1;
// Return focus to menu item
const menuItem = this.renderRoot.querySelector(`.menuItem.active`);
if (menuItem) {
(menuItem as HTMLElement).focus();
}
break;
}
}
private findNextValidItem(items: interfaces.IAppBarMenuItem[], currentIndex: number, direction: number): number {
let index = currentIndex + direction;
while (index >= 0 && index < items.length) {
const item = items[index];
// Skip dividers and disabled items
if (!('divider' in item && item.divider) && !('disabled' in item && item.disabled)) {
return index;
}
index += direction;
}
return -1;
}
private focusNextMenuItem(currentItemId: string, direction: number) {
const menuItems = Array.from(this.renderRoot.querySelectorAll('.menuItem'));
const currentIndex = menuItems.findIndex(item => item.getAttribute('data-item-id') === currentItemId);
if (currentIndex === -1) return;
let nextIndex = currentIndex + direction;
// Wrap around
if (nextIndex < 0) {
nextIndex = menuItems.length - 1;
} else if (nextIndex >= menuItems.length) {
nextIndex = 0;
}
// Find next non-disabled item
let attempts = 0;
while (attempts < menuItems.length) {
const nextItem = menuItems[nextIndex] as HTMLElement;
if (!nextItem.hasAttribute('disabled')) {
nextItem.focus();
// Close current dropdown if open
if (this.activeMenu) {
this.activeMenu = null;
this.focusedDropdownItem = -1;
}
break;
}
nextIndex = (nextIndex + direction + menuItems.length) % menuItems.length;
attempts++;
}
}
}

View File

@ -0,0 +1,157 @@
import { html, css } from '@design.estate/dees-element';
import type { DeesAppuiBase } from './dees-appui-base.js';
import type { IAppBarMenuItem } from './interfaces/appbarmenuitem.js';
import type { ITab } from './interfaces/tab.js';
import type { ISelectionOption } from './interfaces/selectionoption.js';
import * as plugins from './00plugins.js';
import '@design.estate/dees-wcctools/demotools';
export const demoFunc = () => {
// Menu items for the appbar
const menuItems: IAppBarMenuItem[] = [
{
name: 'File',
action: async () => {},
submenu: [
{ name: 'New Project', shortcut: 'Cmd+N', iconName: 'filePlus', action: async () => console.log('New project') },
{ name: 'Open Project...', shortcut: 'Cmd+O', iconName: 'folderOpen', action: async () => console.log('Open project') },
{ name: 'Recent Projects', action: async () => {}, submenu: [
{ name: 'my-app', action: async () => console.log('Open my-app') },
{ name: 'component-lib', action: async () => console.log('Open component-lib') },
{ name: 'api-server', action: async () => console.log('Open api-server') },
]},
{ divider: true },
{ name: 'Save All', shortcut: 'Cmd+Shift+S', iconName: 'save', action: async () => console.log('Save all') },
{ divider: true },
{ name: 'Close Project', action: async () => console.log('Close project') },
]
},
{
name: 'Edit',
action: async () => {},
submenu: [
{ name: 'Undo', shortcut: 'Cmd+Z', iconName: 'undo', action: async () => console.log('Undo') },
{ name: 'Redo', shortcut: 'Cmd+Shift+Z', iconName: 'redo', action: async () => console.log('Redo') },
{ divider: true },
{ name: 'Cut', shortcut: 'Cmd+X', iconName: 'scissors', action: async () => console.log('Cut') },
{ name: 'Copy', shortcut: 'Cmd+C', iconName: 'copy', action: async () => console.log('Copy') },
{ name: 'Paste', shortcut: 'Cmd+V', iconName: 'clipboard', action: async () => console.log('Paste') },
]
},
{
name: 'View',
action: async () => {},
submenu: [
{ name: 'Toggle Sidebar', shortcut: 'Cmd+B', action: async () => console.log('Toggle sidebar') },
{ name: 'Toggle Terminal', shortcut: 'Cmd+J', iconName: 'terminal', action: async () => console.log('Toggle terminal') },
{ divider: true },
{ name: 'Zoom In', shortcut: 'Cmd++', iconName: 'zoomIn', action: async () => console.log('Zoom in') },
{ name: 'Zoom Out', shortcut: 'Cmd+-', iconName: 'zoomOut', action: async () => console.log('Zoom out') },
{ name: 'Reset Zoom', shortcut: 'Cmd+0', action: async () => console.log('Reset zoom') },
]
},
{
name: 'Help',
action: async () => {},
submenu: [
{ name: 'Documentation', iconName: 'book', action: async () => console.log('Documentation') },
{ name: 'Release Notes', iconName: 'fileText', action: async () => console.log('Release notes') },
{ divider: true },
{ name: 'Report Issue', iconName: 'bug', action: async () => console.log('Report issue') },
{ name: 'About', iconName: 'info', action: async () => console.log('About') },
]
}
];
// Main menu tabs (left sidebar)
const mainMenuTabs: ITab[] = [
{ key: 'dashboard', iconName: 'home', action: () => console.log('Dashboard selected') },
{ key: 'projects', iconName: 'folder', action: () => console.log('Projects selected') },
{ key: 'analytics', iconName: 'lineChart', action: () => console.log('Analytics selected') },
{ key: 'settings', iconName: 'settings', action: () => console.log('Settings selected') },
];
// Selector options (second sidebar)
const selectorOptions: (ISelectionOption | { divider: true })[] = [
{ key: 'Overview', iconName: 'home', action: () => console.log('Overview selected') },
{ key: 'Components', iconName: 'package', action: () => console.log('Components selected') },
{ key: 'Services', iconName: 'server', action: () => console.log('Services selected') },
{ divider: true },
{ key: 'Database', iconName: 'database', action: () => console.log('Database selected') },
{ key: 'Settings', iconName: 'settings', action: () => console.log('Settings selected') },
];
// Main content tabs
const mainContentTabs: ITab[] = [
{ key: 'Details', iconName: 'file', action: () => console.log('Details tab') },
{ key: 'Logs', iconName: 'list', action: () => console.log('Logs tab') },
{ key: 'Metrics', iconName: 'lineChart', action: () => console.log('Metrics tab') },
];
// Profile menu items
const profileMenuItems: (plugins.tsclass.website.IMenuItem & { shortcut?: string } | { divider: true })[] = [
{ name: 'Profile Settings', iconName: 'user', action: async () => console.log('Profile settings') },
{ name: 'Account', iconName: 'settings', action: async () => console.log('Account settings') },
{ divider: true },
{ name: 'Help & Support', iconName: 'helpCircle', action: async () => console.log('Help') },
{ name: 'Keyboard Shortcuts', iconName: 'keyboard', shortcut: 'Cmd+K', action: async () => console.log('Shortcuts') },
{ divider: true },
{ name: 'Sign Out', iconName: 'logOut', action: async () => console.log('Sign out') }
];
return html`
<dees-demowrapper>
<style>
${css`
.demo-container {
position: absolute;
top: 0;
left: 0;
height: 100%;
width: 100%;
overflow: hidden;
}
`}
</style>
<div class="demo-container">
<dees-appui-base
.appbarMenuItems=${menuItems}
.appbarBreadcrumbs=${'Dashboard'}
.appbarUser=${{
name: 'Jane Smith',
email: 'jane.smith@example.com',
status: 'online' as 'online' | 'offline' | 'busy' | 'away'
}}
.appbarProfileMenuItems=${profileMenuItems}
.appbarShowWindowControls=${true}
.appbarShowSearch=${true}
.mainmenuTabs=${mainMenuTabs}
.mainselectorOptions=${selectorOptions}
.maincontentTabs=${mainContentTabs}
@appbar-menu-select=${(e: CustomEvent) => console.log('Menu selected:', e.detail)}
@appbar-breadcrumb-navigate=${(e: CustomEvent) => console.log('Breadcrumb:', e.detail)}
@appbar-search-click=${() => console.log('Search clicked')}
@appbar-user-menu-open=${() => console.log('User menu opened')}
@appbar-profile-menu-select=${(e: CustomEvent) => console.log('Profile menu selected:', e.detail)}
@mainmenu-tab-select=${(e: CustomEvent) => console.log('Tab selected:', e.detail)}
@mainselector-option-select=${(e: CustomEvent) => console.log('Option selected:', e.detail)}
>
<div slot="maincontent" style="padding: 40px; color: #ccc;">
<h1>Application Content</h1>
<p>This is the main content area where your application's primary interface would be displayed.</p>
<p>The layout includes:</p>
<ul>
<li>App bar with menus, breadcrumbs, and user account</li>
<li>Main menu (left sidebar) for primary navigation</li>
<li>Selector menu (second sidebar) for sub-navigation</li>
<li>Main content area (this section)</li>
<li>Activity log (right sidebar)</li>
</ul>
</div>
</dees-appui-base>
</div>
</dees-demowrapper>
`;
};

View File

@ -6,11 +6,89 @@ import {
html,
css,
cssManager,
state,
} from '@design.estate/dees-element';
import * as interfaces from './interfaces/index.js';
import * as plugins from './00plugins.js';
import type { DeesAppuiBar } from './dees-appui-appbar.js';
import type { DeesAppuiMainmenu } from './dees-appui-mainmenu.js';
import type { DeesAppuiMainselector } from './dees-appui-mainselector.js';
import type { DeesAppuiMaincontent } from './dees-appui-maincontent.js';
import type { DeesAppuiActivitylog } from './dees-appui-activitylog.js';
import { demoFunc } from './dees-appui-base.demo.js';
// Import child components
import './dees-appui-appbar.js';
import './dees-appui-mainmenu.js';
import './dees-appui-mainselector.js';
import './dees-appui-maincontent.js';
import './dees-appui-activitylog.js';
@customElement('dees-appui-base')
export class DeesAppuiBase extends DeesElement {
public static demo = () => html`<dees-appui-base></dees-appui-base>`;
public static demo = demoFunc;
// Properties for appbar
@property({ type: Array })
public appbarMenuItems: interfaces.IAppBarMenuItem[] = [];
@property({ type: String })
public appbarBreadcrumbs: string = '';
@property({ type: String })
public appbarBreadcrumbSeparator: string = ' > ';
@property({ type: Boolean })
public appbarShowWindowControls: boolean = true;
@property({ type: Object })
public appbarUser?: {
name: string;
email?: string;
avatar?: string;
status?: 'online' | 'offline' | 'busy' | 'away';
};
@property({ type: Array })
public appbarProfileMenuItems: (plugins.tsclass.website.IMenuItem & { shortcut?: string } | { divider: true })[] = [];
@property({ type: Boolean })
public appbarShowSearch: boolean = false;
// Properties for mainmenu
@property({ type: Array })
public mainmenuTabs: interfaces.ITab[] = [];
@property({ type: Object })
public mainmenuSelectedTab?: interfaces.ITab;
// Properties for mainselector
@property({ type: Array })
public mainselectorOptions: (interfaces.ISelectionOption | { divider: true })[] = [];
@property({ type: Object })
public mainselectorSelectedOption?: interfaces.ISelectionOption;
// Properties for maincontent
@property({ type: Array })
public maincontentTabs: interfaces.ITab[] = [];
// References to child components
@state()
public appbar?: DeesAppuiBar;
@state()
public mainmenu?: DeesAppuiMainmenu;
@state()
public mainselector?: DeesAppuiMainselector;
@state()
public maincontent?: DeesAppuiMaincontent;
@state()
public activitylog?: DeesAppuiActivitylog;
public static styles = [
cssManager.defaultStyles,
@ -19,6 +97,7 @@ export class DeesAppuiBase extends DeesElement {
position: absolute;
height: 100%;
width: 100%;
background: ${cssManager.bdTheme('#f0f0f0', '#1a1a1a')};
}
.maingrid {
position: absolute;
@ -26,7 +105,7 @@ export class DeesAppuiBase extends DeesElement {
height: calc(100% - 40px);
width: 100%;
display: grid;
grid-template-columns: 60px 240px auto 240px;
grid-template-columns: 60px 240px 1fr 240px;
}
`,
];
@ -35,13 +114,106 @@ export class DeesAppuiBase extends DeesElement {
public render(): TemplateResult {
return html`
<style></style>
<dees-appui-appbar></dees-appui-appbar>
<dees-appui-appbar
.menuItems=${this.appbarMenuItems}
.breadcrumbs=${this.appbarBreadcrumbs}
.breadcrumbSeparator=${this.appbarBreadcrumbSeparator}
.showWindowControls=${this.appbarShowWindowControls}
.user=${this.appbarUser}
.profileMenuItems=${this.appbarProfileMenuItems}
.showSearch=${this.appbarShowSearch}
@menu-select=${(e: CustomEvent) => this.handleAppbarMenuSelect(e)}
@breadcrumb-navigate=${(e: CustomEvent) => this.handleAppbarBreadcrumbNavigate(e)}
@search-click=${() => this.handleAppbarSearchClick()}
@user-menu-open=${() => this.handleAppbarUserMenuOpen()}
@profile-menu-select=${(e: CustomEvent) => this.handleAppbarProfileMenuSelect(e)}
></dees-appui-appbar>
<div class="maingrid">
<dees-appui-mainmenu></dees-appui-mainmenu>
<dees-appui-mainselector></dees-appui-mainselector>
<dees-appui-maincontent></dees-appui-maincontent>
<dees-appui-mainmenu
.tabs=${this.mainmenuTabs}
.selectedTab=${this.mainmenuSelectedTab}
@tab-select=${(e: CustomEvent) => this.handleMainmenuTabSelect(e)}
></dees-appui-mainmenu>
<dees-appui-mainselector
.selectionOptions=${this.mainselectorOptions}
.selectedOption=${this.mainselectorSelectedOption}
@option-select=${(e: CustomEvent) => this.handleMainselectorOptionSelect(e)}
></dees-appui-mainselector>
<dees-appui-maincontent
.tabs=${this.maincontentTabs}
>
<slot name="maincontent"></slot>
</dees-appui-maincontent>
<dees-appui-activitylog></dees-appui-activitylog>
</div>
`;
}
async firstUpdated() {
// Get references to child components
this.appbar = this.shadowRoot.querySelector('dees-appui-appbar');
this.mainmenu = this.shadowRoot.querySelector('dees-appui-mainmenu');
this.mainselector = this.shadowRoot.querySelector('dees-appui-mainselector');
this.maincontent = this.shadowRoot.querySelector('dees-appui-maincontent');
this.activitylog = this.shadowRoot.querySelector('dees-appui-activitylog');
}
// Event handlers for appbar
private handleAppbarMenuSelect(e: CustomEvent) {
this.dispatchEvent(new CustomEvent('appbar-menu-select', {
detail: e.detail,
bubbles: true,
composed: true
}));
}
private handleAppbarBreadcrumbNavigate(e: CustomEvent) {
this.dispatchEvent(new CustomEvent('appbar-breadcrumb-navigate', {
detail: e.detail,
bubbles: true,
composed: true
}));
}
private handleAppbarSearchClick() {
this.dispatchEvent(new CustomEvent('appbar-search-click', {
bubbles: true,
composed: true
}));
}
private handleAppbarUserMenuOpen() {
this.dispatchEvent(new CustomEvent('appbar-user-menu-open', {
bubbles: true,
composed: true
}));
}
private handleAppbarProfileMenuSelect(e: CustomEvent) {
this.dispatchEvent(new CustomEvent('appbar-profile-menu-select', {
detail: e.detail,
bubbles: true,
composed: true
}));
}
// Event handlers for mainmenu
private handleMainmenuTabSelect(e: CustomEvent) {
this.mainmenuSelectedTab = e.detail.tab;
this.dispatchEvent(new CustomEvent('mainmenu-tab-select', {
detail: e.detail,
bubbles: true,
composed: true
}));
}
// Event handlers for mainselector
private handleMainselectorOptionSelect(e: CustomEvent) {
this.mainselectorSelectedOption = e.detail.option;
this.dispatchEvent(new CustomEvent('mainselector-option-select', {
detail: e.detail,
bubbles: true,
composed: true
}));
}
}

View File

@ -11,35 +11,47 @@ import {
} from '@design.estate/dees-element';
import * as domtools from '@design.estate/dees-domtools';
import './dees-appui-tabs.js';
import type { DeesAppuiTabs } from './dees-appui-tabs.js';
@customElement('dees-appui-maincontent')
export class DeesAppuiMaincontent extends DeesElement {
public static demo = () => html`<dees-appui-maincontent></dees-appui-maincontent>`;
public static demo = () => html`
<dees-appui-maincontent
.tabs=${[
{ key: 'Overview', iconName: 'home', action: () => console.log('Overview') },
{ key: 'Details', iconName: 'file', action: () => console.log('Details') },
{ key: 'Settings', iconName: 'cog', action: () => console.log('Settings') },
]}
>
<div slot="content" style="padding: 40px; color: #ccc;">
<h1>Main Content Area</h1>
<p>This is where your application content goes.</p>
</div>
</dees-appui-maincontent>
`;
// INSTANCE
@property({
type: Array,
})
public tabs: interfaces.ITab[] = [
{ key: 'option 1', action: () => {} },
{ key: 'a very long option', action: () => {} },
{ key: 'reminder: set your tabs', action: () => {} },
{ key: 'option 4', action: () => {} },
{ key: '⚠️ Please set tabs', action: () => console.warn('No tabs configured for maincontent') },
];
@property()
public selectedTab = null;
@property({ type: Object })
public selectedTab: interfaces.ITab | null = null;
public static styles = [
cssManager.defaultStyles,
css`
:host {
color: #fff;
color: ${cssManager.bdTheme('#333', '#fff')};
display: block;
width: 100%;
height: 100%;
position: relative;
background: #161616;
background: ${cssManager.bdTheme('#ffffff', '#161616')};
}
.maincontainer {
position: absolute;
@ -52,110 +64,58 @@ export class DeesAppuiMaincontent extends DeesElement {
.topbar {
position: absolute;
width: 100%;
background: #000000;
user-select: none;
}
.topbar .tabsContainer {
padding-top: 20px;
padding-bottom: 0px;
position: relative;
z-index: 1;
display: grid;
margin-left: 24px;
font-size: 14px;
}
.topbar .tabsContainer .tab {
color: #a0a0a0;
white-space: nowrap;
margin-right: 30px;
padding-top: 4px;
padding-bottom: 12px;
transition: color 0.1s;
}
.topbar .tabsContainer .tab:hover {
color: #ffffff;
}
.topbar .tabsContainer .tab.selectedTab {
color: #e0e0e0;
}
.topbar .tabIndicator {
.content-area {
position: absolute;
z-index: 0;
left: 40px;
bottom: 0px;
height: 40px;
width: 40px;
background: #161616;
transition: all 0.1s;
border-top-left-radius: 8px;
border-top-right-radius: 8px;
border-top: 1px solid #444444;
}
.mainicon {
top: 60px;
left: 0;
right: 0;
bottom: 0;
overflow: auto;
}
`,
];
public render(): TemplateResult {
return html`
<style>
.topbar .tabsContainer {
grid-template-columns: repeat(${this.tabs.length}, min-content);
}
</style>
<div class="maincontainer">
<div class="topbar">
<div class="tabsContainer">
${this.tabs.map((tabArg) => {
return html`
<div
class="tab ${tabArg === this.selectedTab ? 'selectedTab' : null}"
@click="${() => {
this.selectedTab = tabArg;
this.updateTabIndicator();
tabArg.action();
}}"
>
${tabArg.key}
<dees-appui-tabs
.tabs=${this.tabs}
.selectedTab=${this.selectedTab}
.showTabIndicator=${true}
.tabStyle=${'horizontal'}
@tab-select=${(e: CustomEvent) => this.handleTabSelect(e)}
></dees-appui-tabs>
</div>
`;
})}
</div>
<div class="tabIndicator"></div>
<div class="content-area">
<slot></slot>
<slot name="content"></slot>
</div>
</div>
`;
}
/**
* updates the indicator
*/
private updateTabIndicator() {
let selectedTab = this.selectedTab;
const tabIndex = this.tabs.indexOf(selectedTab);
const selectedTabElement: HTMLElement = this.shadowRoot.querySelector(
`.tabsContainer .tab:nth-child(${tabIndex + 1})`
);
const tabsContainer: HTMLElement = this.shadowRoot.querySelector('.tabsContainer');
const marginLeft = parseInt(window.getComputedStyle(tabsContainer).getPropertyValue("margin-left"));
const tabIndicator: HTMLElement = this.shadowRoot.querySelector('.tabIndicator');
tabIndicator.style.width = selectedTabElement.clientWidth + 24 + 'px';
tabIndicator.style.left = selectedTabElement.offsetLeft + marginLeft - 12 + 'px';
private handleTabSelect(e: CustomEvent) {
this.selectedTab = e.detail.tab;
// Re-emit the event
this.dispatchEvent(new CustomEvent('tab-select', {
detail: e.detail,
bubbles: true,
composed: true
}));
}
private updateTab(tabArg: interfaces.ITab) {
this.selectedTab = tabArg;
this.updateTabIndicator();
this.selectedTab.action();
}
firstUpdated() {
this.updateTab(this.tabs[0]);
async firstUpdated(_changedProperties: Map<string | number | symbol, unknown>) {
await super.firstUpdated(_changedProperties);
// Tab selection is now handled by the dees-appui-tabs component
// But we need to ensure the tabs component is ready
const tabsComponent = this.shadowRoot.querySelector('dees-appui-tabs') as DeesAppuiTabs;
if (tabsComponent) {
await tabsComponent.updateComplete;
}
}
}

View File

@ -18,17 +18,23 @@ import { DeesContextmenu } from './dees-contextmenu.js';
*/
@customElement('dees-appui-mainmenu')
export class DeesAppuiMainmenu extends DeesElement {
public static demo = () => html`<dees-appui-mainmenu></dees-appui-mainmenu>`;
public static demo = () => html`
<dees-appui-mainmenu
.tabs=${[
{ key: 'Dashboard', iconName: 'home', action: () => console.log('Dashboard') },
{ key: 'Projects', iconName: 'folder', action: () => console.log('Projects') },
{ key: 'Analytics', iconName: 'lineChart', action: () => console.log('Analytics') },
{ key: 'Settings', iconName: 'settings', action: () => console.log('Settings') },
]}
></dees-appui-mainmenu>
`;
// INSTANCE
// INSTANCE
@property()
@property({ type: Array })
public tabs: interfaces.ITab[] = [
{ key: 'option 1', iconName: 'building', action: () => {} },
{ key: 'option 2', iconName: 'building', action: () => {} },
{ key: 'option 3', iconName: 'building', action: () => {} },
{ key: 'option 4', iconName: 'building', action: () => {} },
{ key: '⚠️ Please set tabs', iconName: 'alertTriangle', action: () => console.warn('No tabs configured for mainmenu') },
];
@property()
@ -39,16 +45,16 @@ export class DeesAppuiMainmenu extends DeesElement {
css`
.mainContainer {
--menuSize: 60px;
color: #ccc;
color: ${cssManager.bdTheme('#666', '#ccc')};
z-index: 10;
display: block;
position: relative;
width: var(--menuSize);
height: 100%;
background: #000000;
box-shadow: 0px 0px 5px rgba(0, 0, 0, 0.5);
background: ${cssManager.bdTheme('#f5f5f5', '#000000')};
box-shadow: ${cssManager.bdTheme('0px 0px 5px rgba(0, 0, 0, 0.1)', '0px 0px 5px rgba(0, 0, 0, 0.5)')};
user-select: none;
border-right: 1px solid #202020;
border-right: 1px solid ${cssManager.bdTheme('#e0e0e0', '#202020')};
}
.tabsContainer {
@ -64,17 +70,17 @@ export class DeesAppuiMainmenu extends DeesElement {
}
.tab:hover {
background: rgba(255, 255, 255, 0.15);
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.05)', 'rgba(255, 255, 255, 0.15)')};
}
.tab.selectedTab {
color: #fff;
background: rgba(255, 255, 255, 0.1);
color: ${cssManager.bdTheme('#000', '#fff')};
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.1)', 'rgba(255, 255, 255, 0.1)')};
}
.tabIndicator {
opacity: 0;
background: #4e729a;
background: ${cssManager.bdTheme('#2196f3', '#4e729a')};
position: absolute;
width: 5px;
height: calc((var(--menuSize) / 3) * 2);
@ -105,7 +111,7 @@ export class DeesAppuiMainmenu extends DeesElement {
this.updateTab(tabArg);
}}"
>
<dees-icon iconFA="${tabArg.iconName as any}"></dees-icon>
<dees-icon .icon="${tabArg.iconName ? `lucide:${tabArg.iconName}` : ''}"></dees-icon>
</div>
`;
})}
@ -115,7 +121,7 @@ export class DeesAppuiMainmenu extends DeesElement {
`;
}
private async updateTabIndicator() {
private updateTabIndicator() {
let selectedTab = this.selectedTab;
if (!selectedTab) {
selectedTab = this.tabs[0];
@ -124,7 +130,12 @@ export class DeesAppuiMainmenu extends DeesElement {
const selectedTabElement: HTMLElement = this.shadowRoot.querySelector(
`.tabsContainer .tab:nth-child(${tabIndex + 1})`
);
if (!selectedTabElement) return;
const tabIndicator: HTMLElement = this.shadowRoot.querySelector('.tabIndicator');
if (!tabIndicator) return;
const offsetTop = selectedTabElement.offsetTop;
tabIndicator.style.opacity = `1`;
tabIndicator.style.top = `calc(${offsetTop}px + (var(--menuSize) / 6))`;
@ -134,6 +145,13 @@ export class DeesAppuiMainmenu extends DeesElement {
this.selectedTab = tabArg;
this.updateTabIndicator();
this.selectedTab.action();
// Emit tab-select event
this.dispatchEvent(new CustomEvent('tab-select', {
detail: { tab: tabArg },
bubbles: true,
composed: true
}));
}
firstUpdated() {

View File

@ -2,6 +2,7 @@ import * as plugins from './00plugins.js';
import * as interfaces from './interfaces/index.js';
import { DeesContextmenu } from './dees-contextmenu.js';
import './dees-icon.js';
import {
DeesElement,
@ -19,22 +20,22 @@ import {
*/
@customElement('dees-appui-mainselector')
export class DeesAppuiMainselector extends DeesElement {
public static demo = () => html`<dees-appui-mainselector></dees-appui-mainselector>`;
public static demo = () => html`
<dees-appui-mainselector
.selectionOptions=${[
{ key: 'Overview', iconName: 'home', action: () => console.log('Overview') },
{ key: 'Components', iconName: 'package', action: () => console.log('Components') },
{ key: 'Services', iconName: 'server', action: () => console.log('Services') },
{ key: 'Database', iconName: 'database', action: () => console.log('Database') },
{ key: 'Settings', iconName: 'settings', action: () => console.log('Settings') },
]}
></dees-appui-mainselector>
`;
// INSTANCE
@property()
public selectionOptions: interfaces.ISelectionOption[] = [
{
key: 'Overview',
action: () => {},
},
{
key: 'option 1',
action: () => {},
},
{ key: 'option 2', action: () => {} },
{ key: 'option 3', action: () => {} },
{ key: 'option 4', action: () => {} },
@property({ type: Array })
public selectionOptions: (interfaces.ISelectionOption | { divider: true })[] = [
{ key: '⚠️ Please set selection options', action: () => console.warn('No selection options configured for mainselector') },
];
@property()
@ -44,14 +45,14 @@ export class DeesAppuiMainselector extends DeesElement {
cssManager.defaultStyles,
css`
:host {
color: #fff;
color: ${cssManager.bdTheme('#333', '#fff')};
position: relative;
display: block;
width: 100%;
max-width: 300px;
height: 100%;
background: #000000;
border-right: 1px solid #222222;
background: ${cssManager.bdTheme('#fafafa', '#000000')};
border-right: 1px solid ${cssManager.bdTheme('#e0e0e0', '#202020')};
}
.maincontainer {
position: absolute;
@ -63,52 +64,79 @@ export class DeesAppuiMainselector extends DeesElement {
.topbar {
position: absolute;
height: 32px;
height: 40px;
width: 100%;
display: flex;
align-items: center;
border-bottom: 1px solid ${cssManager.bdTheme('#e0e0e0', '#202020')};
}
.topbar .heading {
padding-left: 16px;
padding-top: 8px;
line-height: 24px;
padding-left: 12px;
font-family: 'Geist Sans', sans-serif;
font-weight: 600;
font-size: 14px;
font-size: 12px;
color: ${cssManager.bdTheme('#666', '#999')};
text-transform: uppercase;
letter-spacing: 0.5px;
}
.selectionOptions {
position: absolute;
top: 32px;
padding-top: 8px;
top: 40px;
left: 0px;
width: 100%;
right: 0px;
bottom: 0px;
overflow-y: auto;
font-family: 'Geist Sans', sans-serif;
font-size: 14px;
font-size: 12px;
padding: 4px 0;
}
.selectionOptions .selectionOption {
cursor: default;
margin-left: 16px;
margin-right: 16px;
padding-top: 8px;
padding-bottom: 8px;
border-top: 1px dotted #303030;
border-left: 0px solid rgba(0, 0, 0, 0);
transition: all 0.1s;
padding: 8px 12px;
margin: 0;
transition: background 0.1s;
display: flex;
align-items: center;
gap: 8px;
color: ${cssManager.bdTheme('#333', '#ccc')};
user-select: none;
}
.selectionOptions .selectionOption:hover {
border-left: 2px solid #26a69a50;
padding-left: 8px;
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.04)', 'rgba(255, 255, 255, 0.08)')};
}
.selectionOptions .selectionOption:first-child {
border-top: 1px solid rgba(0, 0, 0, 0);
.selectionOptions .selectionOption:active {
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.08)', 'rgba(255, 255, 255, 0.12)')};
}
.selectionOptions .selectionOption.selectedOption {
border-left: 4px solid #26a69a;
padding-left: 10px;
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.08)', 'rgba(255, 255, 255, 0.12)')};
color: ${cssManager.bdTheme('#000', '#fff')};
font-weight: 500;
}
.selectionOptions .selectionOption.selectedOption::before {
content: '';
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 3px;
background: ${cssManager.bdTheme('#26a69a', '#26a69a')};
}
.selectionOption {
position: relative;
}
.selection-divider {
height: 1px;
background: ${cssManager.bdTheme('#e0e0e0', '#202020')};
margin: 4px 0;
}
`,
];
@ -118,17 +146,22 @@ export class DeesAppuiMainselector extends DeesElement {
<style></style>
<div class="maincontainer">
<div class="topbar">
<div class="heading">Properties</div>
<div class="heading">Selector</div>
</div>
<div class="selectionOptions">
${this.selectionOptions.map((selectionOptionArg) => {
if ('divider' in selectionOptionArg && selectionOptionArg.divider) {
return html`<div class="selection-divider"></div>`;
}
const option = selectionOptionArg as interfaces.ISelectionOption;
return html`
<div
class="selectionOption ${this.selectedOption === selectionOptionArg
class="selectionOption ${this.selectedOption === option
? 'selectedOption'
: null}"
@click="${() => {
this.selectOption(selectionOptionArg);
this.selectOption(option);
}}"
@contextmenu="${(eventArg: MouseEvent) => {
DeesContextmenu.openContextMenuWithOptions(eventArg, [
@ -140,7 +173,10 @@ export class DeesAppuiMainselector extends DeesElement {
]);
}}"
>
${selectionOptionArg.key}
${option.iconName ? html`
<dees-icon .icon="${`lucide:${option.iconName}`}" style="font-size: 14px; opacity: 0.7;"></dees-icon>
` : ''}
<span style="flex: 1;">${option.key}</span>
</div>
`;
})}
@ -152,9 +188,24 @@ export class DeesAppuiMainselector extends DeesElement {
private selectOption(optionArg: interfaces.ISelectionOption) {
this.selectedOption = optionArg;
this.selectedOption.action();
// Emit option-select event
this.dispatchEvent(new CustomEvent('option-select', {
detail: { option: optionArg },
bubbles: true,
composed: true
}));
}
firstUpdated() {
this.selectOption(this.selectionOptions[0]);
async firstUpdated(_changedProperties: Map<string | number | symbol, unknown>) {
await super.firstUpdated(_changedProperties);
if (this.selectionOptions && this.selectionOptions.length > 0) {
await this.updateComplete;
// Find first non-divider option
const firstOption = this.selectionOptions.find(option => !('divider' in option)) as interfaces.ISelectionOption;
if (firstOption) {
this.selectOption(firstOption);
}
}
}
}

View File

@ -0,0 +1,401 @@
import * as plugins from './00plugins.js';
import {
DeesElement,
type TemplateResult,
property,
customElement,
html,
css,
cssManager,
state,
} from '@design.estate/dees-element';
@customElement('dees-appui-profiledropdown')
export class DeesAppuiProfileDropdown extends DeesElement {
public static demo = () => html`
<dees-appui-profiledropdown
.user=${{
name: 'John Doe',
email: 'john.doe@example.com',
avatar: 'https://randomuser.me/api/portraits/men/1.jpg',
status: 'online' as 'online'
}}
.menuItems=${[
{ name: 'Profile Settings', iconName: 'user', action: async () => console.log('Profile') },
{ name: 'Account', iconName: 'settings', action: async () => console.log('Account') },
{ divider: true },
{ name: 'Help & Support', iconName: 'helpCircle', action: async () => console.log('Help') },
{ name: 'Keyboard Shortcuts', iconName: 'keyboard', shortcut: 'Cmd+K', action: async () => console.log('Shortcuts') },
{ divider: true },
{ name: 'Sign Out', iconName: 'logOut', action: async () => console.log('Sign out') }
]}
.isOpen=${true}
></dees-appui-profiledropdown>
`;
@property({ type: Object })
public user?: {
name: string;
email?: string;
avatar?: string;
status?: 'online' | 'offline' | 'busy' | 'away';
};
@property({ type: Array })
public menuItems: (plugins.tsclass.website.IMenuItem & { shortcut?: string } | { divider: true })[] = [];
@property({ type: Boolean, reflect: true })
public isOpen: boolean = false;
@property({ type: String })
public position: 'top-right' | 'top-left' | 'bottom-right' | 'bottom-left' = 'top-right';
public static styles = [
cssManager.defaultStyles,
css`
:host {
display: block;
position: absolute;
top: 100%;
left: 0;
right: 0;
pointer-events: none;
}
.dropdown {
position: absolute;
min-width: 220px;
background: ${cssManager.bdTheme('#ffffff', '#000000')};
border: 1px solid ${cssManager.bdTheme('#e0e0e0', '#202020')};
border-radius: 4px;
box-shadow: ${cssManager.bdTheme(
'0 4px 12px rgba(0, 0, 0, 0.15)',
'0 4px 12px rgba(0, 0, 0, 0.3)'
)};
z-index: 1000;
opacity: 0;
transform: scale(0.95) translateY(-10px);
transition: opacity 0.2s, transform 0.2s;
pointer-events: none;
overflow: hidden;
font-size: 12px;
}
:host([isopen]) .dropdown {
opacity: 1;
transform: scale(1) translateY(0);
pointer-events: auto;
}
.backdrop {
display: none;
}
/* Position variants */
.dropdown.top-right {
top: 100%;
right: 0;
margin-top: 4px;
}
.dropdown.top-left {
top: 100%;
left: 0;
margin-top: 8px;
}
.dropdown.bottom-right {
bottom: 100%;
right: 0;
margin-bottom: 8px;
}
.dropdown.bottom-left {
bottom: 100%;
left: 0;
margin-bottom: 8px;
}
/* User section */
.user-section {
padding: 12px;
border-bottom: 1px solid ${cssManager.bdTheme('#e0e0e0', '#202020')};
}
.user-info {
display: flex;
align-items: center;
gap: 10px;
}
.user-avatar {
position: relative;
width: 36px;
height: 36px;
border-radius: 50%;
background: ${cssManager.bdTheme('#f0f0f0', '#1a1a1a')};
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
font-weight: 600;
color: ${cssManager.bdTheme('#666', '#999')};
overflow: hidden;
}
.user-avatar img {
width: 100%;
height: 100%;
object-fit: cover;
}
.user-status {
position: absolute;
bottom: 0;
right: 0;
width: 10px;
height: 10px;
border-radius: 50%;
border: 2px solid ${cssManager.bdTheme('#ffffff', '#000000')};
}
.user-status.online {
background: #4caf50;
}
.user-status.offline {
background: #757575;
}
.user-status.busy {
background: #f44336;
}
.user-status.away {
background: #ff9800;
}
.user-details {
flex: 1;
min-width: 0;
}
.user-name {
font-size: 13px;
font-weight: 600;
color: ${cssManager.bdTheme('#000', '#fff')};
line-height: 1.2;
margin: 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.user-email {
font-size: 11px;
color: ${cssManager.bdTheme('#666', '#999')};
margin-top: 2px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* Menu section */
.menu-section {
padding: 4px 0;
}
.menu-item {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
cursor: default;
transition: background 0.1s;
color: ${cssManager.bdTheme('#333', '#ccc')};
font-size: 12px;
line-height: 1;
user-select: none;
}
.menu-item:hover {
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.04)', 'rgba(255, 255, 255, 0.08)')};
}
.menu-item:active {
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.08)', 'rgba(255, 255, 255, 0.12)')};
}
.menu-item dees-icon {
font-size: 14px;
opacity: 0.7;
}
.menu-item-text {
flex: 1;
}
.menu-shortcut {
font-size: 11px;
color: ${cssManager.bdTheme('#999', '#666')};
margin-left: auto;
opacity: 0.7;
}
.menu-divider {
height: 1px;
background: ${cssManager.bdTheme('#e0e0e0', '#202020')};
margin: 4px 0;
}
/* Backdrop for mobile */
@media (max-width: 768px) {
.backdrop {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.3);
z-index: 999;
opacity: 0;
transition: opacity 0.2s;
display: none;
}
:host([isopen]) .backdrop {
display: block;
opacity: 1;
pointer-events: auto;
}
.dropdown {
position: fixed;
top: 50%;
left: 50%;
right: auto;
bottom: auto;
transform: translate(-50%, -50%) scale(0.95);
margin: 0;
max-width: calc(100vw - 32px);
max-height: calc(100vh - 32px);
overflow-y: auto;
}
:host([isopen]) .dropdown {
transform: translate(-50%, -50%) scale(1);
}
}
`,
];
public render(): TemplateResult {
return html`
<div class="backdrop" @click=${() => this.close()}></div>
<div class="dropdown ${this.position}">
${this.user ? html`
<div class="user-section">
<div class="user-info">
<div class="user-avatar">
${this.user.avatar
? html`<img src="${this.user.avatar}" alt="${this.user.name}">`
: this.getInitials(this.user.name)
}
${this.user.status ? html`
<div class="user-status ${this.user.status}"></div>
` : ''}
</div>
<div class="user-details">
<div class="user-name">${this.user.name}</div>
${this.user.email ? html`
<div class="user-email">${this.user.email}</div>
` : ''}
</div>
</div>
</div>
` : ''}
<div class="menu-section">
${this.menuItems.map(item => this.renderMenuItem(item))}
</div>
</div>
`;
}
private renderMenuItem(item: plugins.tsclass.website.IMenuItem & { shortcut?: string } | { divider: true }): TemplateResult {
if ('divider' in item && item.divider) {
return html`<div class="menu-divider"></div>`;
}
const menuItem = item as plugins.tsclass.website.IMenuItem & { iconName?: string; shortcut?: string };
return html`
<div class="menu-item" @click=${() => this.handleMenuClick(menuItem)}>
${menuItem.iconName ? html`
<dees-icon .icon="${`lucide:${menuItem.iconName}`}"></dees-icon>
` : ''}
<span class="menu-item-text">${menuItem.name}</span>
${menuItem.shortcut ? html`
<span class="menu-shortcut">${menuItem.shortcut}</span>
` : ''}
</div>
`;
}
private getInitials(name: string): string {
return name
.split(' ')
.map(part => part[0])
.join('')
.toUpperCase()
.slice(0, 2);
}
private async handleMenuClick(item: plugins.tsclass.website.IMenuItem & { iconName?: string; shortcut?: string }) {
await item.action();
this.close();
// Emit menu-select event
this.dispatchEvent(new CustomEvent('menu-select', {
detail: { item },
bubbles: true,
composed: true
}));
}
public open() {
this.isOpen = true;
}
public close() {
this.isOpen = false;
}
public toggle() {
this.isOpen = !this.isOpen;
}
// Handle clicks outside the dropdown
async connectedCallback() {
await super.connectedCallback();
this.handleOutsideClick = this.handleOutsideClick.bind(this);
document.addEventListener('click', this.handleOutsideClick);
}
async disconnectedCallback() {
await super.disconnectedCallback();
document.removeEventListener('click', this.handleOutsideClick);
}
private handleOutsideClick(event: MouseEvent) {
if (this.isOpen && !this.contains(event.target as Node)) {
// Check if the click is on the parent element (which contains the profile button)
const parentElement = this.parentElement;
if (parentElement && parentElement.contains(event.target as Node)) {
// Don't close if clicking within the parent element (e.g., on the profile button)
return;
}
this.close();
}
}
}

View File

@ -0,0 +1,247 @@
import * as interfaces from './interfaces/index.js';
import {
DeesElement,
type TemplateResult,
property,
customElement,
html,
css,
cssManager,
} from '@design.estate/dees-element';
import * as domtools from '@design.estate/dees-domtools';
@customElement('dees-appui-tabs')
export class DeesAppuiTabs extends DeesElement {
public static demo = () => html`
<dees-appui-tabs
.tabs=${[
{ key: 'Tab 1', action: () => console.log('Tab 1 clicked') },
{ key: 'Tab 2', action: () => console.log('Tab 2 clicked') },
{ key: 'Tab 3', action: () => console.log('Tab 3 clicked') },
]}
></dees-appui-tabs>
`;
// INSTANCE
@property({
type: Array,
})
public tabs: interfaces.ITab[] = [];
@property({ type: Object })
public selectedTab: interfaces.ITab | null = null;
@property({ type: Boolean })
public showTabIndicator: boolean = true;
@property({ type: String })
public tabStyle: 'horizontal' | 'vertical' = 'horizontal';
public static styles = [
cssManager.defaultStyles,
css`
:host {
display: block;
position: relative;
width: 100%;
}
.tabs-wrapper {
position: relative;
background: ${cssManager.bdTheme('#f5f5f5', '#000000')};
height: 52px;
}
.tabsContainer {
position: relative;
z-index: 1;
user-select: none;
}
.tabsContainer.horizontal {
display: grid;
padding-top: 20px;
padding-bottom: 0px;
margin-left: 24px;
font-size: 14px;
}
.tabsContainer.vertical {
display: flex;
flex-direction: column;
padding: 20px;
font-size: 14px;
}
.tab {
color: ${cssManager.bdTheme('#666', '#a0a0a0')};
white-space: nowrap;
cursor: default;
transition: color 0.1s;
}
.horizontal .tab {
margin-right: 30px;
padding-top: 4px;
padding-bottom: 12px;
}
.vertical .tab {
padding: 12px 16px;
margin-bottom: 4px;
border-radius: 4px;
width: 100%;
display: flex;
align-items: center;
gap: 8px;
}
.tab:hover {
color: ${cssManager.bdTheme('#000', '#ffffff')};
}
.vertical .tab:hover {
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.05)', 'rgba(255, 255, 255, 0.05)')};
}
.tab.selectedTab {
color: ${cssManager.bdTheme('#333', '#e0e0e0')};
}
.vertical .tab.selectedTab {
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.1)', 'rgba(255, 255, 255, 0.1)')};
color: ${cssManager.bdTheme('#000', '#ffffff')};
}
.tab dees-icon {
font-size: 16px;
}
.tabs-wrapper .tabIndicator {
position: absolute;
z-index: 0;
left: 40px;
bottom: 0px;
height: 40px;
width: 40px;
background: ${cssManager.bdTheme('#ffffff', '#161616')};
transition: all 0.1s;
border-top-left-radius: 8px;
border-top-right-radius: 8px;
border-top: 1px solid ${cssManager.bdTheme('#e0e0e0', '#444444')};
}
.vertical .tabIndicator {
display: none;
}
.content {
margin-top: 20px;
}
`,
];
public render(): TemplateResult {
return html`
${this.tabStyle === 'horizontal' ? html`
<style>
.tabsContainer.horizontal {
grid-template-columns: repeat(${this.tabs.length}, min-content);
}
</style>
<div class="tabs-wrapper">
<div class="tabsContainer horizontal">
${this.tabs.map((tabArg) => {
return html`
<div
class="tab ${tabArg === this.selectedTab ? 'selectedTab' : ''}"
@click="${() => this.selectTab(tabArg)}"
>
${tabArg.key}
</div>
`;
})}
</div>
${this.showTabIndicator ? html`
<div class="tabIndicator"></div>
` : ''}
</div>
` : html`
<div class="tabsContainer vertical">
${this.tabs.map((tabArg) => {
return html`
<div
class="tab ${tabArg === this.selectedTab ? 'selectedTab' : ''}"
@click="${() => this.selectTab(tabArg)}"
>
${tabArg.iconName ? html`<dees-icon .iconName=${tabArg.iconName}></dees-icon>` : ''}
${tabArg.key}
</div>
`;
})}
</div>
`}
<div class="content">
<slot></slot>
</div>
`;
}
private selectTab(tabArg: interfaces.ITab) {
this.selectedTab = tabArg;
this.updateTabIndicator();
tabArg.action();
// Emit tab-select event
this.dispatchEvent(new CustomEvent('tab-select', {
detail: { tab: tabArg },
bubbles: true,
composed: true
}));
}
/**
* updates the indicator position
*/
private updateTabIndicator() {
if (!this.showTabIndicator || this.tabStyle !== 'horizontal' || !this.selectedTab) {
return;
}
const tabIndex = this.tabs.indexOf(this.selectedTab);
const selectedTabElement: HTMLElement = this.shadowRoot.querySelector(
`.tabs-wrapper .tabsContainer .tab:nth-child(${tabIndex + 1})`
);
if (!selectedTabElement) return;
const tabsContainer: HTMLElement = this.shadowRoot.querySelector('.tabs-wrapper .tabsContainer');
const marginLeft = parseInt(window.getComputedStyle(tabsContainer).getPropertyValue("margin-left"));
const tabIndicator: HTMLElement = this.shadowRoot.querySelector('.tabs-wrapper .tabIndicator');
if (tabIndicator) {
tabIndicator.style.width = selectedTabElement.clientWidth + 24 + 'px';
tabIndicator.style.left = selectedTabElement.offsetLeft + marginLeft - 12 + 'px';
}
}
firstUpdated() {
if (this.tabs && this.tabs.length > 0) {
this.selectTab(this.tabs[0]);
}
}
async updated(changedProperties: Map<string, any>) {
super.updated(changedProperties);
if (changedProperties.has('tabs') && this.tabs && this.tabs.length > 0 && !this.selectedTab) {
this.selectTab(this.tabs[0]);
}
if (changedProperties.has('selectedTab') || changedProperties.has('tabs')) {
this.updateTabIndicator();
}
}
}

View File

@ -0,0 +1,192 @@
import * as interfaces from './interfaces/index.js';
import {
DeesElement,
type TemplateResult,
property,
customElement,
html,
css,
cssManager,
state,
} from '@design.estate/dees-element';
import './dees-appui-tabs.js';
import type { DeesAppuiTabs } from './dees-appui-tabs.js';
export interface IAppViewTab extends interfaces.ITab {
content?: TemplateResult | (() => TemplateResult);
}
export interface IAppView {
id: string;
name: string;
description?: string;
iconName?: string;
tabs: IAppViewTab[];
menuItems?: interfaces.ISelectionOption[];
}
@customElement('dees-appui-view')
export class DeesAppuiView extends DeesElement {
public static demo = () => html`
<dees-appui-view
.viewConfig=${{
id: 'demo-view',
name: 'Demo View',
description: 'A demonstration view',
iconName: 'home',
tabs: [
{
key: 'overview',
iconName: 'chart-line',
action: () => console.log('Overview tab'),
content: html`<div style="padding: 20px;">Overview Content</div>`
},
{
key: 'details',
iconName: 'file-alt',
action: () => console.log('Details tab'),
content: html`<div style="padding: 20px;">Details Content</div>`
}
],
menuItems: [
{ key: 'General', action: () => console.log('General') },
{ key: 'Advanced', action: () => console.log('Advanced') },
]
}}
></dees-appui-view>
`;
// INSTANCE
@property({ type: Object })
public viewConfig: IAppView;
@state()
private selectedTab: IAppViewTab | null = null;
@state()
private tabs: DeesAppuiTabs;
public static styles = [
cssManager.defaultStyles,
css`
:host {
display: block;
position: relative;
width: 100%;
height: 100%;
background: #161616;
}
.view-container {
position: relative;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
}
.view-header {
background: #000000;
border-bottom: 1px solid #333;
flex-shrink: 0;
}
.view-content {
flex: 1;
position: relative;
overflow: hidden;
}
.tab-content {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
overflow: auto;
opacity: 0;
transition: opacity 0.2s;
}
.tab-content.active {
opacity: 1;
}
dees-appui-tabs {
height: 60px;
}
`,
];
public render(): TemplateResult {
if (!this.viewConfig) {
return html`<div>No view configuration provided</div>`;
}
return html`
<div class="view-container">
<div class="view-header">
<dees-appui-tabs
.tabs=${this.viewConfig.tabs}
.selectedTab=${this.selectedTab}
@tab-select=${(e: CustomEvent) => this.handleTabSelect(e)}
></dees-appui-tabs>
</div>
<div class="view-content">
${this.viewConfig.tabs.map((tab) => {
const isActive = tab === this.selectedTab;
const content = typeof tab.content === 'function' ? tab.content() : tab.content;
return html`
<div class="tab-content ${isActive ? 'active' : ''}">
${content || html`<slot name="${tab.key}"></slot>`}
</div>
`;
})}
</div>
</div>
`;
}
async firstUpdated() {
this.tabs = this.shadowRoot.querySelector('dees-appui-tabs');
if (this.viewConfig?.tabs?.length > 0) {
this.selectedTab = this.viewConfig.tabs[0];
}
}
private handleTabSelect(e: CustomEvent) {
this.selectedTab = e.detail.tab;
// Re-emit the event with view context
this.dispatchEvent(new CustomEvent('view-tab-select', {
detail: {
view: this.viewConfig,
tab: e.detail.tab
},
bubbles: true,
composed: true
}));
}
// Public methods for external control
public selectTab(tabKey: string) {
const tab = this.viewConfig.tabs.find(t => t.key === tabKey);
if (tab) {
this.selectedTab = tab;
if (this.tabs) {
this.tabs.selectedTab = tab;
}
}
}
public getMenuItems(): interfaces.ISelectionOption[] {
return this.viewConfig?.menuItems || [];
}
public getTabs(): IAppViewTab[] {
return this.viewConfig?.tabs || [];
}
}

View File

@ -0,0 +1,114 @@
import { html, css } from '@design.estate/dees-element';
export const demoFunc = () => {
return html`
<style>
${css`
.demoBox {
background: #000000;
padding: 40px;
min-height: 100vh;
box-sizing: border-box;
}
.demo-section {
margin-bottom: 32px;
}
.demo-title {
color: #fff;
font-size: 20px;
font-weight: 600;
margin-bottom: 16px;
font-family: 'Geist Sans', sans-serif;
}
.demo-description {
color: #999;
font-size: 14px;
margin-bottom: 24px;
font-family: 'Geist Sans', sans-serif;
}
`}
</style>
<div class="demoBox">
<div class="demo-section">
<h2 class="demo-title">Basic Button Groups</h2>
<p class="demo-description">Button groups without labels for simple grouping</p>
<dees-button-group>
<dees-button>Option 1</dees-button>
<dees-button>Option 2</dees-button>
<dees-button>Option 3</dees-button>
</dees-button-group>
</div>
<div class="demo-section">
<h2 class="demo-title">Labeled Button Groups</h2>
<p class="demo-description">Button groups with descriptive labels</p>
<dees-button-group label="View Mode:">
<dees-button type="highlighted">Grid</dees-button>
<dees-button>List</dees-button>
<dees-button>Cards</dees-button>
</dees-button-group>
</div>
<div class="demo-section">
<h2 class="demo-title">Multiple Groups</h2>
<p class="demo-description">Multiple button groups used together</p>
<div style="display: flex; gap: 16px; flex-wrap: wrap;">
<dees-button-group label="Dataset:">
<dees-button type="highlighted">System</dees-button>
<dees-button>Network</dees-button>
<dees-button>Sales</dees-button>
</dees-button-group>
<dees-button-group label="Time Range:">
<dees-button>1H</dees-button>
<dees-button type="highlighted">24H</dees-button>
<dees-button>7D</dees-button>
<dees-button>30D</dees-button>
</dees-button-group>
<dees-button-group label="Actions:">
<dees-button>Refresh</dees-button>
<dees-button>Export</dees-button>
</dees-button-group>
</div>
</div>
<div class="demo-section">
<h2 class="demo-title">Vertical Button Groups</h2>
<p class="demo-description">Button groups with vertical layout</p>
<div style="display: flex; gap: 24px;">
<dees-button-group direction="vertical" label="Navigation:">
<dees-button>Dashboard</dees-button>
<dees-button type="highlighted">Analytics</dees-button>
<dees-button>Reports</dees-button>
<dees-button>Settings</dees-button>
</dees-button-group>
<dees-button-group direction="vertical">
<dees-button>Add Item</dees-button>
<dees-button>Edit Item</dees-button>
<dees-button>Delete Item</dees-button>
</dees-button-group>
</div>
</div>
<div class="demo-section">
<h2 class="demo-title">Mixed Button Types</h2>
<p class="demo-description">Different button types within groups</p>
<dees-button-group label="Status:">
<dees-button type="success">Active</dees-button>
<dees-button>Pending</dees-button>
<dees-button type="danger">Inactive</dees-button>
</dees-button-group>
</div>
</div>
`;
};

View File

@ -0,0 +1,83 @@
import {
DeesElement,
css,
cssManager,
customElement,
html,
property,
type TemplateResult,
} from '@design.estate/dees-element';
import * as domtools from '@design.estate/dees-domtools';
import { demoFunc } from './dees-button-group.demo.js';
declare global {
interface HTMLElementTagNameMap {
'dees-button-group': DeesButtonGroup;
}
}
@customElement('dees-button-group')
export class DeesButtonGroup extends DeesElement {
public static demo = demoFunc;
@property()
public label: string = '';
@property()
public direction: 'horizontal' | 'vertical' = 'horizontal';
constructor() {
super();
domtools.elementBasic.setup();
}
public static styles = [
cssManager.defaultStyles,
css`
:host {
display: inline-block;
}
.button-group {
display: flex;
gap: 8px;
align-items: center;
padding: 8px;
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.05)', 'rgba(255, 255, 255, 0.05)')};
border-radius: 6px;
}
.button-group.vertical {
flex-direction: column;
align-items: stretch;
}
.label {
color: ${cssManager.bdTheme('#666', '#999')};
font-size: 12px;
font-family: 'Geist Sans', sans-serif;
margin-right: 8px;
white-space: nowrap;
}
.button-group.vertical .label {
margin-right: 0;
margin-bottom: 8px;
}
::slotted(*) {
margin: 0 !important;
}
`,
];
public render(): TemplateResult {
return html`
<div class="button-group ${this.direction}">
${this.label ? html`<span class="label">${this.label}</span>` : ''}
<slot></slot>
</div>
`;
}
}

View File

@ -1,8 +1,405 @@
import { html } from '@design.estate/dees-element';
import { html, css } from '@design.estate/dees-element';
import type { DeesChartArea } from './dees-chart-area.js';
import '@design.estate/dees-wcctools/demotools';
export const demoFunc = () => {
// Initial dataset values
const initialDatasets = {
system: {
label: 'System Usage (%)',
series: [
{
name: 'CPU',
data: [
{ x: new Date(Date.now() - 300000).toISOString(), y: 25 },
{ x: new Date(Date.now() - 240000).toISOString(), y: 30 },
{ x: new Date(Date.now() - 180000).toISOString(), y: 28 },
{ x: new Date(Date.now() - 120000).toISOString(), y: 35 },
{ x: new Date(Date.now() - 60000).toISOString(), y: 32 },
{ x: new Date().toISOString(), y: 38 },
],
},
{
name: 'Memory',
data: [
{ x: new Date(Date.now() - 300000).toISOString(), y: 45 },
{ x: new Date(Date.now() - 240000).toISOString(), y: 48 },
{ x: new Date(Date.now() - 180000).toISOString(), y: 46 },
{ x: new Date(Date.now() - 120000).toISOString(), y: 52 },
{ x: new Date(Date.now() - 60000).toISOString(), y: 50 },
{ x: new Date().toISOString(), y: 55 },
],
},
],
},
};
const initialFormatters = {
system: (val: number) => `${val}%`,
};
return html`
<dees-demowrapper .runAfterRender=${async (elementArg: HTMLElement) => {
// Get the chart elements
const chartElement = elementArg.querySelector('#main-chart') as DeesChartArea;
const connectionsChartElement = elementArg.querySelector('#connections-chart') as DeesChartArea;
let intervalId: number;
let connectionsIntervalId: number;
let currentDataset = 'system';
// Y-axis formatters for different datasets
const formatters = {
system: (val: number) => `${val}%`,
network: (val: number) => `${val} Mbps`,
sales: (val: number) => `$${val.toLocaleString()}`,
};
// Time window configuration (in milliseconds)
const TIME_WINDOW = 2 * 60 * 1000; // 2 minutes
const UPDATE_INTERVAL = 1000; // 1 second
const DATA_POINT_INTERVAL = 5000; // Show data points every 5 seconds
// Store previous values for smooth transitions
let previousValues = {
cpu: 30,
memory: 50,
download: 150,
upload: 30,
connections: 150
};
// Generate initial data points for time window
const generateInitialData = (baseValue: number, variance: number, interval: number = DATA_POINT_INTERVAL) => {
const data = [];
const now = Date.now();
const pointCount = Math.floor(TIME_WINDOW / interval);
for (let i = pointCount; i >= 0; i--) {
const timestamp = new Date(now - (i * interval)).toISOString();
const value = baseValue + (Math.random() - 0.5) * variance;
data.push({ x: timestamp, y: Math.round(value) });
}
return data;
};
// Different datasets to showcase
const datasets = {
system: {
label: 'System Usage (%)',
series: [
{
name: 'CPU',
data: generateInitialData(previousValues.cpu, 10),
},
{
name: 'Memory',
data: generateInitialData(previousValues.memory, 8),
},
],
},
network: {
label: 'Network Traffic (Mbps)',
series: [
{
name: 'Download',
data: generateInitialData(previousValues.download, 30),
},
{
name: 'Upload',
data: generateInitialData(previousValues.upload, 10),
},
],
},
sales: {
label: 'Sales Analytics',
series: [
{
name: 'Revenue',
data: [
{ x: '2025-01-01', y: 45000 },
{ x: '2025-01-02', y: 52000 },
{ x: '2025-01-03', y: 48000 },
{ x: '2025-01-04', y: 61000 },
{ x: '2025-01-05', y: 58000 },
{ x: '2025-01-06', y: 65000 },
],
},
{
name: 'Profit',
data: [
{ x: '2025-01-01', y: 12000 },
{ x: '2025-01-02', y: 14000 },
{ x: '2025-01-03', y: 11000 },
{ x: '2025-01-04', y: 18000 },
{ x: '2025-01-05', y: 16000 },
{ x: '2025-01-06', y: 20000 },
],
},
],
},
};
// Generate smooth value transitions
const getNextValue = (current: number, min: number, max: number, maxChange: number = 5) => {
// Add some randomness but keep it close to current value
const change = (Math.random() - 0.5) * maxChange * 2;
let newValue = current + change;
// Apply some "pressure" to move towards center of range
const center = (min + max) / 2;
const pressure = (center - newValue) * 0.1;
newValue += pressure;
// Ensure within bounds
newValue = Math.max(min, Math.min(max, newValue));
return Math.round(newValue);
};
// Track time of last data point
let lastDataPointTime = Date.now();
let connectionsLastUpdate = Date.now();
// Add real-time data
const addRealtimeData = () => {
if (!chartElement) return;
const now = Date.now();
// Only add new data point every DATA_POINT_INTERVAL
const shouldAddPoint = (now - lastDataPointTime) >= DATA_POINT_INTERVAL;
if (shouldAddPoint) {
lastDataPointTime = now;
const newTimestamp = new Date(now).toISOString();
// Generate smooth transitions for new values
if (currentDataset === 'system') {
// Generate new values
previousValues.cpu = getNextValue(previousValues.cpu, 20, 50, 3);
previousValues.memory = getNextValue(previousValues.memory, 40, 70, 2);
// Get current data and add new points
const currentSeries = chartElement.chartSeries.map((series, index) => ({
name: series.name,
data: [
...(series.data as Array<{x: any; y: any}>),
index === 0
? { x: newTimestamp, y: previousValues.cpu }
: { x: newTimestamp, y: previousValues.memory }
]
}));
chartElement.updateSeries(currentSeries, false);
} else if (currentDataset === 'network') {
// Generate new values
previousValues.download = getNextValue(previousValues.download, 100, 200, 10);
previousValues.upload = getNextValue(previousValues.upload, 20, 50, 5);
// Get current data and add new points
const currentSeries = chartElement.chartSeries.map((series, index) => ({
name: series.name,
data: [
...(series.data as Array<{x: any; y: any}>),
index === 0
? { x: newTimestamp, y: previousValues.download }
: { x: newTimestamp, y: previousValues.upload }
]
}));
chartElement.updateSeries(currentSeries, false);
}
}
};
// Update connections chart data
const updateConnections = () => {
if (!connectionsChartElement) return;
const now = Date.now();
const newTimestamp = new Date(now).toISOString();
// Generate new connections value with discrete changes
const change = Math.floor(Math.random() * 21) - 10; // -10 to +10 connections
previousValues.connections = Math.max(50, Math.min(300, previousValues.connections + change));
// Get current data and add new point
const currentSeries = connectionsChartElement.chartSeries;
const newData = [{
name: currentSeries[0]?.name || 'Connections',
data: [
...(currentSeries[0]?.data as Array<{x: any; y: any}> || []),
{ x: newTimestamp, y: previousValues.connections }
]
}];
connectionsChartElement.updateSeries(newData, false);
};
// Switch dataset
const switchDataset = (name: string) => {
currentDataset = name;
const dataset = datasets[name];
chartElement.label = dataset.label;
chartElement.series = dataset.series;
chartElement.yAxisFormatter = formatters[name];
// Set appropriate y-axis scaling
if (name === 'system') {
chartElement.yAxisScaling = 'percentage';
chartElement.yAxisMax = 100;
} else if (name === 'network') {
chartElement.yAxisScaling = 'dynamic';
} else {
chartElement.yAxisScaling = 'dynamic';
}
// Reset last data point time to get fresh data immediately
lastDataPointTime = Date.now() - DATA_POINT_INTERVAL;
};
// Start/stop real-time updates
const startRealtime = () => {
if (!intervalId && (currentDataset === 'system' || currentDataset === 'network')) {
chartElement.realtimeMode = true;
// Only add data every 5 seconds, chart auto-scrolls independently
intervalId = window.setInterval(() => addRealtimeData(), DATA_POINT_INTERVAL);
}
// Start connections updates
if (!connectionsIntervalId) {
connectionsChartElement.realtimeMode = true;
// Update connections every second
connectionsIntervalId = window.setInterval(() => updateConnections(), UPDATE_INTERVAL);
}
};
const stopRealtime = () => {
if (intervalId) {
window.clearInterval(intervalId);
intervalId = null;
chartElement.realtimeMode = false;
}
// Stop connections updates
if (connectionsIntervalId) {
window.clearInterval(connectionsIntervalId);
connectionsIntervalId = null;
connectionsChartElement.realtimeMode = false;
}
};
// Randomize current data (spike/drop simulation)
const randomizeData = () => {
if (currentDataset === 'system') {
// Simulate CPU/Memory spike
previousValues.cpu = Math.random() > 0.5 ? 85 : 25;
previousValues.memory = Math.random() > 0.5 ? 80 : 45;
} else if (currentDataset === 'network') {
// Simulate network traffic spike
previousValues.download = Math.random() > 0.5 ? 250 : 100;
previousValues.upload = Math.random() > 0.5 ? 80 : 20;
}
// Also spike connections
previousValues.connections = Math.random() > 0.5 ? 280 : 80;
// Force immediate update by resetting timers
lastDataPointTime = 0;
connectionsLastUpdate = 0;
};
// Wire up button click handlers
const buttons = elementArg.querySelectorAll('dees-button');
buttons.forEach(button => {
const text = button.textContent?.trim();
if (text === 'System Usage') {
button.addEventListener('click', () => switchDataset('system'));
} else if (text === 'Network Traffic') {
button.addEventListener('click', () => switchDataset('network'));
} else if (text === 'Sales Data') {
button.addEventListener('click', () => switchDataset('sales'));
} else if (text === 'Start Live') {
button.addEventListener('click', () => startRealtime());
} else if (text === 'Stop Live') {
button.addEventListener('click', () => stopRealtime());
} else if (text === 'Spike Values') {
button.addEventListener('click', () => randomizeData());
}
});
// Update button states based on current dataset
const updateButtonStates = () => {
const buttons = elementArg.querySelectorAll('dees-button');
buttons.forEach(button => {
const text = button.textContent?.trim();
if (text === 'System Usage') {
button.type = currentDataset === 'system' ? 'highlighted' : 'normal';
} else if (text === 'Network Traffic') {
button.type = currentDataset === 'network' ? 'highlighted' : 'normal';
} else if (text === 'Sales Data') {
button.type = currentDataset === 'sales' ? 'highlighted' : 'normal';
}
});
};
// Configure main chart with rolling window
chartElement.rollingWindow = TIME_WINDOW;
chartElement.realtimeMode = false; // Will be enabled when starting live updates
chartElement.yAxisScaling = 'percentage'; // Initial system dataset uses percentage
chartElement.yAxisMax = 100;
chartElement.autoScrollInterval = 1000; // Auto-scroll every second
// Set initial time window
setTimeout(() => {
chartElement.updateTimeWindow();
}, 100);
// Update button states when dataset changes
const originalSwitchDataset = switchDataset;
const switchDatasetWithButtonUpdate = (name: string) => {
originalSwitchDataset(name);
updateButtonStates();
};
// Replace switchDataset with the one that updates buttons
buttons.forEach(button => {
const text = button.textContent?.trim();
if (text === 'System Usage') {
button.removeEventListener('click', () => switchDataset('system'));
button.addEventListener('click', () => switchDatasetWithButtonUpdate('system'));
} else if (text === 'Network Traffic') {
button.removeEventListener('click', () => switchDataset('network'));
button.addEventListener('click', () => switchDatasetWithButtonUpdate('network'));
} else if (text === 'Sales Data') {
button.removeEventListener('click', () => switchDataset('sales'));
button.addEventListener('click', () => switchDatasetWithButtonUpdate('sales'));
}
});
// Initialize connections chart with data
if (connectionsChartElement) {
const initialConnectionsData = generateInitialData(previousValues.connections, 30, UPDATE_INTERVAL);
connectionsChartElement.series = [{
name: 'Connections',
data: initialConnectionsData
}];
// Configure connections chart
connectionsChartElement.rollingWindow = TIME_WINDOW;
connectionsChartElement.realtimeMode = false; // Will be enabled when starting live updates
connectionsChartElement.yAxisScaling = 'fixed';
connectionsChartElement.yAxisMax = 350;
connectionsChartElement.autoScrollInterval = 1000; // Auto-scroll every second
// Set initial time window
setTimeout(() => {
connectionsChartElement.updateTimeWindow();
}, 100);
}
}}>
<style>
${css`
.demoBox {
position: relative;
background: #000000;
@ -10,12 +407,77 @@ export const demoFunc = () => {
width: 100%;
padding: 40px;
box-sizing: border-box;
display: flex;
flex-direction: column;
gap: 24px;
}
.controls {
display: flex;
flex-wrap: wrap;
gap: 12px;
margin-bottom: 8px;
}
.chart-container {
flex: 1;
min-height: 400px;
}
.info {
color: #666;
font-size: 11px;
font-family: 'Geist Sans', sans-serif;
text-align: center;
margin-top: 8px;
}
`}
</style>
<div class="demoBox">
<div class="controls">
<dees-button-group label="Dataset:">
<dees-button type="highlighted">System Usage</dees-button>
<dees-button>Network Traffic</dees-button>
<dees-button>Sales Data</dees-button>
</dees-button-group>
<dees-button-group label="Real-time:">
<dees-button>Start Live</dees-button>
<dees-button>Stop Live</dees-button>
</dees-button-group>
<dees-button-group label="Actions:">
<dees-button>Spike Values</dees-button>
</dees-button-group>
</div>
<div class="chart-container">
<dees-chart-area
.label=${'System Usage'}
id="main-chart"
.label=${initialDatasets.system.label}
.series=${initialDatasets.system.series}
.yAxisFormatter=${initialFormatters.system}
></dees-chart-area>
</div>
<div class="chart-container" style="margin-top: 20px;">
<dees-chart-area
id="connections-chart"
.label=${'Active Connections'}
.series=${[{
name: 'Connections',
data: [] as Array<{x: any; y: any}>
}]}
.yAxisFormatter=${(val: number) => `${val}`}
></dees-chart-area>
</div>
<div class="info">
Real-time monitoring with 2-minute rolling window •
Updates every second with smooth value transitions •
Click 'Spike Values' to simulate load spikes
</div>
</div>
</dees-demowrapper>
`;
};

View File

@ -6,7 +6,6 @@ import {
html,
property,
state,
type CSSResult,
type TemplateResult,
} from '@design.estate/dees-element';
@ -32,8 +31,36 @@ export class DeesChartArea extends DeesElement {
@property()
public label: string = 'Untitled Chart';
@property({ type: Array })
public series: ApexAxisChartSeries = [];
// Override getter to return internal chart data
get chartSeries(): ApexAxisChartSeries {
return this.internalChartData.length > 0 ? this.internalChartData : this.series;
}
@property({ attribute: false })
public yAxisFormatter: (value: number) => string = (val) => `${val} Mbps`;
@property({ type: Number })
public rollingWindow: number = 0; // 0 means no rolling window
@property({ type: Boolean })
public realtimeMode: boolean = false;
@property({ type: String })
public yAxisScaling: 'fixed' | 'dynamic' | 'percentage' = 'dynamic';
@property({ type: Number })
public yAxisMax: number = 100; // Used when yAxisScaling is 'fixed' or 'percentage'
@property({ type: Number })
public autoScrollInterval: number = 1000; // Auto-scroll interval in milliseconds (0 to disable)
private resizeObserver: ResizeObserver;
private resizeTimeout: number;
private internalChartData: ApexAxisChartSeries = [];
private autoScrollTimer: number | null = null;
constructor() {
super();
@ -68,6 +95,7 @@ export class DeesChartArea extends DeesElement {
clearTimeout(this.resizeTimeout);
}
this.resizeObserver.disconnect();
this.stopAutoScroll();
});
}
@ -144,8 +172,8 @@ export class DeesChartArea extends DeesElement {
const initialWidth = mainbox.clientWidth - paddingLeft - paddingRight;
const initialHeight = mainbox.offsetHeight - paddingTop - paddingBottom;
var options: ApexCharts.ApexOptions = {
series: [
// Use provided series data or default demo data
const chartSeries = this.series.length > 0 ? this.series : [
{
name: 'cpu',
data: [
@ -166,7 +194,13 @@ export class DeesChartArea extends DeesElement {
{ x: '2025-01-15T19:00:00', y: 40 },
],
},
],
];
// Store internal data
this.internalChartData = chartSeries;
var options: ApexCharts.ApexOptions = {
series: chartSeries,
chart: {
width: initialWidth || 100, // Use actual width or fallback
height: initialHeight || 100, // Use actual height or fallback
@ -175,12 +209,16 @@ export class DeesChartArea extends DeesElement {
show: false, // This line disables the toolbar
},
animations: {
enabled: true,
enabled: !this.realtimeMode, // Disable animations in realtime mode
speed: 400,
animateGradually: {
enabled: true,
delay: 150
enabled: false, // Disable gradual animation for cleaner updates
delay: 0
},
dynamicAnimation: {
enabled: !this.realtimeMode,
speed: 350
}
},
},
dataLabels: {
@ -193,10 +231,11 @@ export class DeesChartArea extends DeesElement {
xaxis: {
type: 'datetime', // Time-series data
labels: {
format: 'hh:mm A', // Time formatting
format: 'HH:mm:ss', // Time formatting with seconds
datetimeUTC: false,
style: {
colors: '#9e9e9e', // Label color
fontSize: '12px',
fontSize: '11px',
},
},
axisBorder: {
@ -208,10 +247,9 @@ export class DeesChartArea extends DeesElement {
},
yaxis: {
min: 0,
max: this.yAxisScaling === 'dynamic' ? undefined : this.yAxisMax,
labels: {
formatter: function (val: number) {
return `${val} Mbps`; // Format Y-axis labels
},
formatter: this.yAxisFormatter,
style: {
colors: '#9e9e9e', // Label color
fontSize: '12px',
@ -285,6 +323,162 @@ export class DeesChartArea extends DeesElement {
await this.resizeChart();
}
public async updated(changedProperties: Map<string, any>) {
super.updated(changedProperties);
// Update chart if series data changes
if (changedProperties.has('series') && this.chart && this.series.length > 0) {
await this.updateSeries(this.series);
}
// Update y-axis formatter if it changes
if (changedProperties.has('yAxisFormatter') && this.chart) {
await this.chart.updateOptions({
yaxis: {
labels: {
formatter: this.yAxisFormatter,
},
},
});
}
// Handle realtime mode changes
if (changedProperties.has('realtimeMode') && this.chart) {
await this.chart.updateOptions({
chart: {
animations: {
enabled: !this.realtimeMode,
speed: 400,
animateGradually: {
enabled: false,
delay: 0
},
dynamicAnimation: {
enabled: !this.realtimeMode,
speed: 350
}
}
}
});
// Start/stop auto-scroll based on realtime mode
if (this.realtimeMode && this.rollingWindow > 0 && this.autoScrollInterval > 0) {
this.startAutoScroll();
} else {
this.stopAutoScroll();
}
}
// Handle auto-scroll interval changes
if (changedProperties.has('autoScrollInterval') && this.chart) {
this.stopAutoScroll();
if (this.realtimeMode && this.rollingWindow > 0 && this.autoScrollInterval > 0) {
this.startAutoScroll();
}
}
// Handle y-axis scaling changes
if ((changedProperties.has('yAxisScaling') || changedProperties.has('yAxisMax')) && this.chart) {
await this.chart.updateOptions({
yaxis: {
min: 0,
max: this.yAxisScaling === 'dynamic' ? undefined : this.yAxisMax
}
});
}
}
public async updateSeries(newSeries: ApexAxisChartSeries, animate: boolean = true) {
if (!this.chart) {
return;
}
// Store the new data first
this.internalChartData = newSeries;
// Handle rolling window if enabled
if (this.rollingWindow > 0 && this.realtimeMode) {
const now = Date.now();
const cutoffTime = now - this.rollingWindow;
// Filter data to only include points within the rolling window
const filteredSeries = newSeries.map(series => ({
name: series.name,
data: (series.data as any[]).filter(point => {
if (typeof point === 'object' && point !== null && 'x' in point) {
return new Date(point.x).getTime() > cutoffTime;
}
return false;
})
}));
// Only update if we have data
if (filteredSeries.some(s => s.data.length > 0)) {
// Handle y-axis scaling first
if (this.yAxisScaling === 'dynamic') {
const allValues = filteredSeries.flatMap(s => (s.data as any[]).map(d => d.y));
if (allValues.length > 0) {
const maxValue = Math.max(...allValues);
const dynamicMax = Math.ceil(maxValue * 1.1);
await this.chart.updateOptions({
yaxis: {
min: 0,
max: dynamicMax
}
}, false, false);
}
}
this.chart.updateSeries(filteredSeries, false);
}
} else {
this.chart.updateSeries(newSeries, animate);
}
}
// New method to update just the x-axis for smooth scrolling
public async updateTimeWindow() {
if (!this.chart || this.rollingWindow <= 0) {
return;
}
const now = Date.now();
const cutoffTime = now - this.rollingWindow;
await this.chart.updateOptions({
xaxis: {
min: cutoffTime,
max: now,
labels: {
format: 'HH:mm:ss',
datetimeUTC: false,
style: {
colors: '#9e9e9e',
fontSize: '11px',
},
},
tickAmount: 6,
}
}, false, false);
}
public async appendData(newData: { data: any[] }[]) {
if (!this.chart) {
return;
}
// Use ApexCharts' appendData method for smoother real-time updates
this.chart.appendData(newData);
}
public async updateOptions(options: ApexCharts.ApexOptions, redrawPaths?: boolean, animate?: boolean) {
if (!this.chart) {
return;
}
return this.chart.updateOptions(options, redrawPaths, animate);
}
public async resizeChart() {
if (!this.chart) {
return;
@ -317,4 +511,21 @@ export class DeesChartArea extends DeesElement {
},
});
}
private startAutoScroll() {
if (this.autoScrollTimer) {
return; // Already running
}
this.autoScrollTimer = window.setInterval(() => {
this.updateTimeWindow();
}, this.autoScrollInterval);
}
private stopAutoScroll() {
if (this.autoScrollTimer) {
window.clearInterval(this.autoScrollTimer);
this.autoScrollTimer = null;
}
}
}

View File

@ -1,6 +1,12 @@
import { html } from '@design.estate/dees-element';
import type { DeesChartLog } from './dees-chart-log.js';
import '@design.estate/dees-wcctools/demotools';
export const demoFunc = () => {
return html`
<dees-demowrapper .runAfterRender=${async (elementArg: HTMLElement) => {
// Get the log element
const logElement = elementArg.querySelector('dees-chart-log') as DeesChartLog;
let intervalId: number;
const serverSources = ['Server', 'Database', 'API', 'Auth', 'Cache', 'Queue', 'WebSocket', 'Scheduler'];
@ -44,11 +50,6 @@ export const demoFunc = () => {
};
const generateRandomLog = () => {
const logElement = (window as any).__demoLogElement;
if (!logElement) {
console.warn('Log element not ready yet');
return;
}
const levels: Array<'debug' | 'info' | 'warn' | 'error' | 'success'> = ['debug', 'info', 'warn', 'error', 'success'];
const weights = [0.2, 0.5, 0.15, 0.1, 0.05]; // Weighted probability
@ -117,8 +118,19 @@ export const demoFunc = () => {
}
};
return html`
// Wire up button click handlers
const buttons = elementArg.querySelectorAll('dees-button');
buttons.forEach(button => {
const text = button.textContent?.trim();
if (text === 'Add Single Log') {
button.addEventListener('click', () => generateRandomLog());
} else if (text === 'Start Simulation') {
button.addEventListener('click', () => startSimulation());
} else if (text === 'Stop Simulation') {
button.addEventListener('click', () => stopSimulation());
}
});
}}>
<style>
.demoBox {
position: relative;
@ -144,14 +156,15 @@ export const demoFunc = () => {
</style>
<div class="demoBox">
<div class="controls">
<dees-button @clicked=${() => generateRandomLog()}>Add Single Log</dees-button>
<dees-button @clicked=${() => startSimulation()}>Start Simulation</dees-button>
<dees-button @clicked=${() => stopSimulation()}>Stop Simulation</dees-button>
<dees-button>Add Single Log</dees-button>
<dees-button>Start Simulation</dees-button>
<dees-button>Stop Simulation</dees-button>
</div>
<div class="info">Simulating realistic server logs with various levels and sources</div>
<dees-chart-log
.label=${'Production Server Logs'}
></dees-chart-log>
</div>
</dees-demowrapper>
`;
};

View File

@ -274,11 +274,6 @@ export class DeesChartLog extends DeesElement {
this.logEntries = demoLogs;
this.scrollToBottom();
// For demo purposes, store reference globally
if ((window as any).__demoLogElement === undefined) {
(window as any).__demoLogElement = this;
}
}
public async updateLog(entries?: ILogEntry[]) {

View File

@ -9,49 +9,143 @@ export const demoFunc = () => html`
display: block;
margin: 20px;
}
.demo-container {
display: flex;
flex-direction: column;
gap: 20px;
padding: 40px;
background: #f5f5f5;
min-height: 400px;
}
.demo-area {
background: white;
padding: 40px;
border-radius: 8px;
border: 1px solid #e0e0e0;
text-align: center;
cursor: context-menu;
}
</style>
<dees-button @contextmenu=${(eventArg) => {
<div class="demo-container">
<div class="demo-area" @contextmenu=${(eventArg: MouseEvent) => {
DeesContextmenu.openContextMenuWithOptions(eventArg, [
{
name: 'copy',
iconName: 'copySolid',
name: 'Cut',
iconName: 'scissors',
shortcut: 'Cmd+X',
action: async () => {
return null;
console.log('Cut action');
},
},
{
name: 'edit',
iconName: 'penToSquare',
name: 'Copy',
iconName: 'copy',
shortcut: 'Cmd+C',
action: async () => {
return null;
console.log('Copy action');
},
},{
name: 'paste',
iconName: 'pasteSolid',
},
{
name: 'Paste',
iconName: 'clipboard',
shortcut: 'Cmd+V',
action: async () => {
return null;
console.log('Paste action');
},
},
{ divider: true },
{
name: 'Delete',
iconName: 'trash2',
action: async () => {
console.log('Delete action');
},
},
{ divider: true },
{
name: 'Select All',
shortcut: 'Cmd+A',
action: async () => {
console.log('Select All action');
},
},
]);
}}>Right-Click for contextmenu</dees-button>
<dees-contextmenu class="withMargin"></dees-contextmenu>
}}>
<h3>Right-click anywhere in this area</h3>
<p>A context menu will appear with various options</p>
</div>
<dees-button @contextmenu=${(eventArg: MouseEvent) => {
DeesContextmenu.openContextMenuWithOptions(eventArg, [
{
name: 'Button Action 1',
iconName: 'play',
action: async () => {
console.log('Button action 1');
},
},
{
name: 'Button Action 2',
iconName: 'pause',
action: async () => {
console.log('Button action 2');
},
},
{
name: 'Disabled Action',
iconName: 'ban',
disabled: true,
action: async () => {
console.log('This should not run');
},
},
{ divider: true },
{
name: 'Settings',
iconName: 'settings',
action: async () => {
console.log('Settings');
},
},
]);
}}>Right-click on this button for a different menu</dees-button>
<div style="margin-top: 20px;">
<h4>Static Context Menu (always visible):</h4>
<dees-contextmenu
class="withMargin"
.menuItems=${[
{
name: 'copy',
iconName: 'copySolid',
action: async () => {},
name: 'New File',
iconName: 'filePlus',
shortcut: 'Cmd+N',
action: async () => console.log('New file'),
},
{
name: 'edit',
iconName: 'penToSquare',
action: async () => {},
},{
name: 'paste',
iconName: 'pasteSolid',
action: async () => {},
name: 'Open File',
iconName: 'folderOpen',
shortcut: 'Cmd+O',
action: async () => console.log('Open file'),
},
] as plugins.tsclass.website.IMenuItem[]}
{
name: 'Save',
iconName: 'save',
shortcut: 'Cmd+S',
action: async () => console.log('Save'),
},
{ divider: true },
{
name: 'Export',
iconName: 'download',
action: async () => console.log('Export'),
},
{
name: 'Import',
iconName: 'upload',
action: async () => console.log('Import'),
},
]}
></dees-contextmenu>
</div>
</div>
`;

View File

@ -1,4 +1,3 @@
import * as colors from './00colors.js';
import * as plugins from './00plugins.js';
import { demoFunc } from './dees-contextmenu.demo.js';
import {
@ -15,6 +14,7 @@ import {
import * as domtools from '@design.estate/dees-domtools';
import { DeesWindowLayer } from './dees-windowlayer.js';
import './dees-icon.js';
declare global {
interface HTMLElementTagNameMap {
@ -30,7 +30,7 @@ export class DeesContextmenu extends DeesElement {
// STATIC
// This will store all the accumulated menu items
public static contextMenuDeactivated = false;
public static accumulatedMenuItems: plugins.tsclass.website.IMenuItem[] = [];
public static accumulatedMenuItems: (plugins.tsclass.website.IMenuItem & { shortcut?: string; disabled?: boolean } | { divider: true })[] = [];
// Add a global event listener for the right-click context menu
public static initializeGlobalListener() {
@ -49,7 +49,13 @@ export class DeesContextmenu extends DeesElement {
// Traverse up the DOM tree to accumulate menu items
while (target) {
if ((target as any).getContextMenuItems) {
DeesContextmenu.accumulatedMenuItems.push(...(target as any).getContextMenuItems());
const items = (target as any).getContextMenuItems();
if (items && items.length > 0) {
if (DeesContextmenu.accumulatedMenuItems.length > 0) {
DeesContextmenu.accumulatedMenuItems.push({ divider: true });
}
DeesContextmenu.accumulatedMenuItems.push(...items);
}
}
target = (target as Node).parentNode;
}
@ -60,7 +66,7 @@ export class DeesContextmenu extends DeesElement {
}
// allows opening of a contextmenu with options
public static async openContextMenuWithOptions(eventArg: MouseEvent, menuItemsArg: plugins.tsclass.website.IMenuItem[]) {
public static async openContextMenuWithOptions(eventArg: MouseEvent, menuItemsArg: (plugins.tsclass.website.IMenuItem & { shortcut?: string; disabled?: boolean } | { divider: true })[]) {
if (this.contextMenuDeactivated) {
return;
}
@ -68,32 +74,60 @@ export class DeesContextmenu extends DeesElement {
eventArg.stopPropagation();
const contextMenu = new DeesContextmenu();
contextMenu.style.position = 'fixed';
contextMenu.style.zIndex = '2000';
contextMenu.style.top = `${eventArg.clientY.toString()}px`;
contextMenu.style.left = `${eventArg.clientX.toString()}px`;
contextMenu.style.zIndex = '10000';
contextMenu.style.opacity = '0';
contextMenu.style.transform = 'scale(0.95,0.95)';
contextMenu.style.transformOrigin = 'top left';
contextMenu.style.transform = 'scale(0.95) translateY(-10px)';
contextMenu.menuItems = menuItemsArg;
contextMenu.windowLayer = await DeesWindowLayer.createAndShow();
contextMenu.windowLayer.addEventListener('click', async () => {
await contextMenu.destroy();
})
document.body.append(contextMenu);
// Get dimensions after adding to DOM
await domtools.plugins.smartdelay.delayFor(0);
const rect = contextMenu.getBoundingClientRect();
const windowWidth = window.innerWidth;
const windowHeight = window.innerHeight;
// Calculate position
let top = eventArg.clientY;
let left = eventArg.clientX;
// Adjust if menu would go off right edge
if (left + rect.width > windowWidth) {
left = windowWidth - rect.width - 10;
}
// Adjust if menu would go off bottom edge
if (top + rect.height > windowHeight) {
top = windowHeight - rect.height - 10;
}
// Ensure menu doesn't go off left or top edge
if (left < 10) left = 10;
if (top < 10) top = 10;
contextMenu.style.top = `${top}px`;
contextMenu.style.left = `${left}px`;
contextMenu.style.transformOrigin = 'top left';
// Animate in
await domtools.plugins.smartdelay.delayFor(0);
contextMenu.style.opacity = '1';
contextMenu.style.transform = 'scale(1,1)';
contextMenu.style.transform = 'scale(1) translateY(0)';
}
// INSTANCE
@property({
type: Array,
})
public menuItems: plugins.tsclass.website.IMenuItem[] = [];
public menuItems: (plugins.tsclass.website.IMenuItem & { shortcut?: string; disabled?: boolean; divider?: never } | { divider: true })[] = [];
windowLayer: DeesWindowLayer;
constructor() {
super();
this.tabIndex = 0;
}
/**
@ -104,40 +138,70 @@ export class DeesContextmenu extends DeesElement {
css`
:host {
display: block;
transition: all 0.1s;
transition: opacity 0.2s, transform 0.2s;
outline: none;
}
.mainbox {
color: ${cssManager.bdTheme('#222', '#ccc')};
font-size: 14px;
width: 200px;
border: 1px solid ${cssManager.bdTheme('#fff', '#ffffff10')};
min-height: 34px;
border-radius: 3px;
background: ${cssManager.bdTheme('#fff', '#222')};
box-shadow: 0px 1px 4px ${cssManager.bdTheme('#00000020', '#000000')};
min-width: 200px;
max-width: 280px;
background: ${cssManager.bdTheme('#ffffff', '#000000')};
border: 1px solid ${cssManager.bdTheme('#e0e0e0', '#202020')};
border-radius: 4px;
box-shadow: ${cssManager.bdTheme(
'0 4px 12px rgba(0, 0, 0, 0.15)',
'0 4px 12px rgba(0, 0, 0, 0.3)'
)};
user-select: none;
padding: 4px;
padding: 4px 0;
font-size: 12px;
color: ${cssManager.bdTheme('#333', '#ccc')};
}
.mainbox .menuitem {
padding: 4px 8px;
border-radius: 3px;
.menuitem {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
cursor: default;
transition: background 0.1s;
line-height: 1;
}
.mainbox .menuitem dees-icon {
display: inline-block;
margin-right: 8px;
width: 14px;
transform: translateY(2px);
.menuitem:hover {
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.04)', 'rgba(255, 255, 255, 0.08)')};
}
.mainbox .menuitem:hover {
background: ${cssManager.bdTheme(colors.bright.blue, colors.dark.blue)};
.menuitem:active {
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.08)', 'rgba(255, 255, 255, 0.12)')};
}
.mainbox .menuitem:active {
background: #ffffff05;
.menuitem.disabled {
opacity: 0.5;
cursor: not-allowed;
pointer-events: none;
}
.menuitem dees-icon {
font-size: 14px;
opacity: 0.7;
}
.menuitem-text {
flex: 1;
}
.menuitem-shortcut {
font-size: 11px;
color: ${cssManager.bdTheme('#999', '#666')};
margin-left: auto;
opacity: 0.7;
}
.menu-divider {
height: 1px;
background: ${cssManager.bdTheme('#e0e0e0', '#202020')};
margin: 4px 0;
}
`,
];
@ -146,10 +210,20 @@ export class DeesContextmenu extends DeesElement {
return html`
<div class="mainbox">
${this.menuItems.map((menuItemArg) => {
if ('divider' in menuItemArg && menuItemArg.divider) {
return html`<div class="menu-divider"></div>`;
}
const menuItem = menuItemArg as plugins.tsclass.website.IMenuItem & { shortcut?: string; disabled?: boolean };
return html`
<div class="menuitem" @click=${() => this.handleClick(menuItemArg)}>
<dees-icon .iconFA=${(menuItemArg.iconName as any) || 'minus'}></dees-icon
>${menuItemArg.name}
<div class="menuitem ${menuItem.disabled ? 'disabled' : ''}" @click=${() => !menuItem.disabled && this.handleClick(menuItem)}>
${menuItem.iconName ? html`
<dees-icon .icon="${`lucide:${menuItem.iconName}`}"></dees-icon>
` : ''}
<span class="menuitem-text">${menuItem.name}</span>
${menuItem.shortcut ? html`
<span class="menuitem-shortcut">${menuItem.shortcut}</span>
` : ''}
</div>
`;
})}
@ -158,8 +232,8 @@ export class DeesContextmenu extends DeesElement {
DeesContextmenu.contextMenuDeactivated = true;
this.destroy();
}}>
<dees-icon .iconFA=${'xmark'}></dees-icon
>allow native context
<dees-icon .icon="lucide:x"></dees-icon>
<span class="menuitem-text">Allow native context</span>
</div>
` : html``}
</div>
@ -167,10 +241,45 @@ export class DeesContextmenu extends DeesElement {
}
public async firstUpdated() {
// Focus on the menu for keyboard navigation
this.focus();
// Add keyboard event listeners
this.addEventListener('keydown', this.handleKeydown);
}
public async handleClick(menuItem: plugins.tsclass.website.IMenuItem) {
private handleKeydown = (event: KeyboardEvent) => {
const menuItems = Array.from(this.shadowRoot.querySelectorAll('.menuitem:not(.disabled)'));
const currentIndex = menuItems.findIndex(item => item.matches(':hover'));
switch (event.key) {
case 'ArrowDown':
event.preventDefault();
const nextIndex = currentIndex + 1 < menuItems.length ? currentIndex + 1 : 0;
(menuItems[nextIndex] as HTMLElement).dispatchEvent(new MouseEvent('mouseenter'));
break;
case 'ArrowUp':
event.preventDefault();
const prevIndex = currentIndex - 1 >= 0 ? currentIndex - 1 : menuItems.length - 1;
(menuItems[prevIndex] as HTMLElement).dispatchEvent(new MouseEvent('mouseenter'));
break;
case 'Enter':
event.preventDefault();
if (currentIndex >= 0) {
(menuItems[currentIndex] as HTMLElement).click();
}
break;
case 'Escape':
event.preventDefault();
this.destroy();
break;
}
}
public async handleClick(menuItem: plugins.tsclass.website.IMenuItem & { shortcut?: string; disabled?: boolean }) {
menuItem.action();
await this.destroy();
}
@ -180,7 +289,7 @@ export class DeesContextmenu extends DeesElement {
this.windowLayer.destroy();
}
this.style.opacity = '0';
this.style.transform = 'scale(0.95,0,95)';
this.style.transform = 'scale(0.95) translateY(-10px)';
await domtools.plugins.smartdelay.delayFor(100);
this.parentElement.removeChild(this);
}

View File

@ -8,6 +8,7 @@ import {
type CSSResult,
} from '@design.estate/dees-element';
import { DeesForm } from './dees-form.js';
import './dees-button.js'; // Import to ensure dees-button is registered
declare global {
interface HTMLElementTagNameMap {

View File

@ -1,69 +1,248 @@
import { html, domtools, cssManager } from '@design.estate/dees-element';
import { html, css, domtools, cssManager } from '@design.estate/dees-element';
import type { DeesForm } from './dees-form.js';
import '@design.estate/dees-wcctools/demotools';
export const demoFunc = () => html`
<dees-demowrapper>
<style>
.demoContainer {
max-width: 400px;
margin: 24px auto;
padding: 16px;
background: ${cssManager.bdTheme('#eeeeeb', '#111')};
box-shadow: 0px 1px 3px #00000030;
${css`
.demo-container {
display: flex;
flex-direction: column;
gap: 24px;
padding: 24px;
max-width: 1200px;
margin: 0 auto;
}
.demo-section {
background: #f8f9fa;
border-radius: 8px;
padding: 24px;
}
@media (prefers-color-scheme: dark) {
.demo-section {
background: #1a1a1a;
}
}
.demo-section h3 {
margin-top: 0;
margin-bottom: 16px;
color: #0069f2;
font-size: 18px;
}
.demo-section p {
margin-top: 0;
margin-bottom: 16px;
color: #666;
font-size: 14px;
}
@media (prefers-color-scheme: dark) {
.demo-section p {
color: #999;
}
}
.form-container {
background: #fff;
border: 1px solid #e0e0e0;
border-radius: 8px;
padding: 24px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
@media (prefers-color-scheme: dark) {
.form-container {
background: #222;
border-color: #333;
}
}
.horizontal-form {
display: flex;
align-items: flex-start;
gap: 16px;
flex-wrap: wrap;
}
`}
</style>
<div class="demoContainer">
<div class="demo-container">
<div class="demo-section">
<h3>Complete Form Example</h3>
<p>A comprehensive form with various input types, validation, and form submission handling</p>
<div class="form-container">
<dees-form
style="display: block; margin:auto; max-width: 500px; padding: 20px"
@formData=${async (eventArg) => {
const form: DeesForm = eventArg.currentTarget;
form.setStatus('pending', 'authenticating...');
await domtools.plugins.smartdelay.delayFor(1000);
form.setStatus('success', 'authenticated!');
form.setStatus('pending', 'Processing...');
await domtools.plugins.smartdelay.delayFor(2000);
form.setStatus('success', 'Form submitted successfully!');
await domtools.plugins.smartdelay.delayFor(2000);
form.reset();
}}
>
<dees-input-text
.required=${true}
key="firstName"
label="First Name"
.description=${'Your given name'}
></dees-input-text>
<dees-input-text
.required=${true}
key="lastName"
label="Last Name"
></dees-input-text>
<dees-input-text
.required=${true}
key="email"
label="Email Address"
.description=${'We will use this to contact you'}
></dees-input-text>
<dees-input-dropdown
.label=${'title'}
.required=${true}
key="country"
.label=${'Country'}
.options=${[
{ option: 'option 1', key: 'option1' },
{ option: 'option 2', key: 'option2' },
{ option: 'option 3', key: 'option3' },
{ option: 'United States', key: 'us' },
{ option: 'Canada', key: 'ca' },
{ option: 'Germany', key: 'de' },
{ option: 'France', key: 'fr' },
{ option: 'United Kingdom', key: 'uk' },
]}
></dees-input-dropdown>
<dees-input-multiselect
.label=${'title'}
.options=${[
{ option: 'option 1', key: 'option1' },
{ option: 'option 2', key: 'option2' },
{ option: 'option 3', key: 'option3' },
]}></dees-input-multiselect>
<dees-input-typelist
.label=${'a type list'}
></dees-input-typelist>
<dees-input-text .required="${true}" key="hello1" label="a text" .description=${`
This is an awesome description.
`}></dees-input-text>
<dees-input-text .required="${true}" key="hello2" label="also a text"></dees-input-text>
<dees-input-text
.required="${true}"
key="hello3"
label="a password"
.required=${true}
key="password"
label="Password"
isPasswordBool
.description=${'Minimum 8 characters'}
></dees-input-text>
<dees-input-checkbox
.required="${true}"
key="hello3"
label="another text"
.required=${true}
key="terms"
label="I agree to the Terms and Conditions"
></dees-input-checkbox>
<dees-input-iban></dees-input-iban>
<dees-input-multitoggle
.label=${'multi select'}
.options=${['option 1', 'option 2', 'option 3']}
.selectedOption=${'option 1'}
></dees-input-multitoggle>
<dees-input-fileupload
.label=${'attachments'}
></dees-input-fileupload>
<dees-form-submit>Submit</dees-form-submit>
<dees-input-checkbox
key="newsletter"
label="Send me promotional emails"
.value=${true}
></dees-input-checkbox>
<dees-form-submit>Create Account</dees-form-submit>
</dees-form>
</div>
</div>
<div class="demo-section">
<h3>Horizontal Form Layout</h3>
<p>Compact form with inputs arranged horizontally - perfect for filters and quick forms</p>
<div class="form-container">
<dees-form horizontal-layout>
<dees-input-text
key="search"
label="Search"
></dees-input-text>
<dees-input-dropdown
key="category"
.label=${'Category'}
.enableSearch=${false}
.options=${[
{ option: 'All', key: 'all' },
{ option: 'Products', key: 'products' },
{ option: 'Services', key: 'services' },
{ option: 'Support', key: 'support' },
]}
></dees-input-dropdown>
<dees-input-dropdown
key="sort"
.label=${'Sort By'}
.enableSearch=${false}
.options=${[
{ option: 'Newest', key: 'newest' },
{ option: 'Popular', key: 'popular' },
{ option: 'Price: Low to High', key: 'price_asc' },
{ option: 'Price: High to Low', key: 'price_desc' },
]}
></dees-input-dropdown>
<dees-input-checkbox
key="inStock"
label="In Stock Only"
.value=${true}
></dees-input-checkbox>
</dees-form>
</div>
</div>
<div class="demo-section">
<h3>Advanced Form Features</h3>
<p>Form with specialized input types and complex validation</p>
<div class="form-container">
<dees-form
@formData=${async (eventArg) => {
const form: DeesForm = eventArg.currentTarget;
const data = eventArg.detail.data;
console.log('Form data:', data);
form.setStatus('success', 'Data logged to console!');
}}
>
<dees-input-iban
key="iban"
label="IBAN"
.required=${true}
></dees-input-iban>
<dees-input-phone
key="phone"
label="Phone Number"
.required=${true}
></dees-input-phone>
<dees-input-multitoggle
key="preferences"
.label=${'Notification Preferences'}
.options=${['Email', 'SMS', 'Push', 'In-App']}
.selectedOption=${'Email'}
></dees-input-multitoggle>
<dees-input-multiselect
key="interests"
.label=${'Areas of Interest'}
.options=${[
{ option: 'Technology', key: 'tech' },
{ option: 'Design', key: 'design' },
{ option: 'Business', key: 'business' },
{ option: 'Marketing', key: 'marketing' },
{ option: 'Sales', key: 'sales' },
]}
></dees-input-multiselect>
<dees-input-fileupload
key="documents"
.label=${'Upload Documents'}
.description=${'PDF, DOC, or DOCX files up to 10MB'}
></dees-input-fileupload>
<dees-form-submit>Submit Application</dees-form-submit>
</dees-form>
</div>
</div>
</div>
</dees-demowrapper>
`;

View File

@ -4,6 +4,7 @@ import {
type TemplateResult,
DeesElement,
type CSSResult,
property,
} from '@design.estate/dees-element';
import * as domtools from '@design.estate/dees-domtools';
@ -11,27 +12,42 @@ import { DeesInputCheckbox } from './dees-input-checkbox.js';
import { DeesInputText } from './dees-input-text.js';
import { DeesInputQuantitySelector } from './dees-input-quantityselector.js';
import { DeesInputRadio } from './dees-input-radio.js';
import { DeesInputDropdown } from './dees-input-dropdown.js';
import { DeesInputFileupload } from './dees-input-fileupload.js';
import { DeesInputIban } from './dees-input-iban.js';
import { DeesInputMultitoggle } from './dees-input-multitoggle.js';
import { DeesInputPhone } from './dees-input-phone.js';
import { DeesInputTypelist } from './dees-input-typelist.js';
import { DeesFormSubmit } from './dees-form-submit.js';
import { DeesTable } from './dees-table.js';
import { demoFunc } from './dees-form.demo.js';
import { DeesInputIban } from './dees-input-iban.js';
// Unified set for form input types
const FORM_INPUT_TYPES = [
DeesInputCheckbox,
DeesInputDropdown,
DeesInputFileupload,
DeesInputIban,
DeesInputText,
DeesInputMultitoggle,
DeesInputPhone,
DeesInputQuantitySelector,
DeesInputRadio,
DeesInputText,
DeesInputTypelist,
DeesTable,
];
export type TFormInputElement =
| DeesInputCheckbox
| DeesInputDropdown
| DeesInputFileupload
| DeesInputIban
| DeesInputText
| DeesInputMultitoggle
| DeesInputPhone
| DeesInputQuantitySelector
| DeesInputRadio
| DeesInputText
| DeesInputTypelist
| DeesTable<any>;
declare global {
@ -48,6 +64,13 @@ export class DeesForm extends DeesElement {
public changeSubject = new domtools.plugins.smartrx.rxjs.Subject();
public readyDeferred = domtools.plugins.smartpromise.defer();
/**
* Controls the layout mode of child input components
* When true, sets all child inputs to horizontal layout
*/
@property({ type: Boolean, reflect: true, attribute: 'horizontal-layout' })
public horizontalLayout: boolean = false;
public render(): TemplateResult {
return html`
<style>
@ -62,6 +85,7 @@ export class DeesForm extends DeesElement {
public async firstUpdated() {
const formChildren = this.getFormElements();
this.updateRequiredStatus();
this.updateChildrenLayoutMode();
for (const child of formChildren) {
child.changeSubject.subscribe(async () => {
@ -107,7 +131,7 @@ export class DeesForm extends DeesElement {
*/
public async collectFormData() {
const children = this.getFormElements();
const valueObject: { [key: string]: string | number | boolean | any[] } = {};
const valueObject: { [key: string]: string | number | boolean | any[] | File[] | { option: string; key: string; payload?: any } } = {};
for (const child of children) {
if (!child.key) {
console.log(`form element with label "${child.label}" has no key. skipping.`);
@ -202,4 +226,28 @@ export class DeesForm extends DeesElement {
}
});
}
/**
* Updates the layout mode of child input components based on form's horizontalLayout property
*/
private updateChildrenLayoutMode() {
const formChildren = this.getFormElements();
for (const child of formChildren) {
if ('layoutMode' in child) {
// The child's auto mode will detect this form's horizontal-layout attribute
(child as any).layoutMode = 'auto';
}
}
}
/**
* Called when properties change
*/
updated(changedProperties: Map<string, any>) {
super.updated(changedProperties);
if (changedProperties.has('horizontalLayout')) {
this.updateChildrenLayoutMode();
}
}
}

View File

@ -0,0 +1,184 @@
import {
DeesElement,
property,
css,
type CSSResult,
cssManager,
} from '@design.estate/dees-element';
import * as domtools from '@design.estate/dees-domtools';
/**
* Base class for all dees-input components
* Provides unified margin system and layout mode support
*/
export abstract class DeesInputBase<T = any> extends DeesElement {
/**
* Layout mode for the input component
* - vertical: Traditional form layout (label on top)
* - horizontal: Inline layout (label position configurable)
* - auto: Detect from parent context
*/
@property({ type: String })
public layoutMode: 'vertical' | 'horizontal' | 'auto' = 'auto';
/**
* Position of the label relative to the input
*/
@property({ type: String })
public labelPosition: 'top' | 'left' | 'right' | 'none' = 'top';
/**
* Common properties for all inputs
*/
@property({ type: String })
public key: string;
@property({ type: String })
public label: string;
@property({ type: Boolean })
public required: boolean = false;
@property({ type: Boolean })
public disabled: boolean = false;
@property({ type: String })
public description: string;
/**
* Common styles for all input components
*/
public static get baseStyles(): CSSResult[] {
return [
css`
/* CSS Variables for consistent spacing */
:host {
--dees-input-spacing-unit: 8px;
--dees-input-vertical-gap: calc(var(--dees-input-spacing-unit) * 2); /* 16px */
--dees-input-horizontal-gap: calc(var(--dees-input-spacing-unit) * 2); /* 16px */
--dees-input-label-gap: var(--dees-input-spacing-unit); /* 8px */
}
/* Default vertical stacking mode (for forms) */
:host {
display: block;
margin: 0;
margin-bottom: var(--dees-input-vertical-gap);
}
/* Last child in container should have no bottom margin */
:host(:last-child) {
margin-bottom: 0;
}
/* Horizontal layout mode - activated by attribute */
:host([layout-mode="horizontal"]) {
display: inline-block;
margin: 0;
margin-right: var(--dees-input-horizontal-gap);
margin-bottom: 0;
}
:host([layout-mode="horizontal"]:last-child) {
margin-right: 0;
}
/* Auto mode - inherit from parent dees-form if present */
/* Label position variations */
:host([label-position="left"]) .input-wrapper {
display: grid;
grid-template-columns: auto 1fr;
gap: var(--dees-input-label-gap);
align-items: center;
}
:host([label-position="right"]) .input-wrapper {
display: grid;
grid-template-columns: 1fr auto;
gap: var(--dees-input-label-gap);
align-items: center;
}
:host([label-position="top"]) .input-wrapper {
display: block;
}
:host([label-position="none"]) dees-label {
display: none;
}
`,
];
}
/**
* Subject for value changes that all inputs should implement
*/
public changeSubject = new domtools.plugins.smartrx.rxjs.Subject<T>();
/**
* Called when the element is connected to the DOM
* Sets up layout mode detection
*/
async connectedCallback() {
await super.connectedCallback();
this.detectLayoutMode();
}
/**
* Detects the appropriate layout mode based on parent context
*/
private detectLayoutMode() {
if (this.layoutMode !== 'auto') {
this.setAttribute('layout-mode', this.layoutMode);
return;
}
// Check if parent is a form with horizontal layout
const parentForm = this.closest('dees-form');
if (parentForm && parentForm.hasAttribute('horizontal-layout')) {
this.setAttribute('layout-mode', 'horizontal');
} else {
this.setAttribute('layout-mode', 'vertical');
}
}
/**
* Updates the layout mode attribute when property changes
*/
updated(changedProperties: Map<string, any>) {
super.updated(changedProperties);
if (changedProperties.has('layoutMode')) {
this.detectLayoutMode();
}
if (changedProperties.has('labelPosition')) {
this.setAttribute('label-position', this.labelPosition);
}
}
/**
* Standard method for freezing input (disabling)
*/
public async freeze() {
this.disabled = true;
}
/**
* Standard method for unfreezing input (enabling)
*/
public async unfreeze() {
this.disabled = false;
}
/**
* Abstract method that child classes must implement to get their value
*/
public abstract getValue(): any;
/**
* Abstract method that child classes must implement to set their value
*/
public abstract setValue(value: any): void;
}

View File

@ -0,0 +1,267 @@
import { html, css } from '@design.estate/dees-element';
import '@design.estate/dees-wcctools/demotools';
import type { DeesInputCheckbox } from './dees-input-checkbox.js';
import './dees-button.js';
export const demoFunc = () => html`
<dees-demowrapper .runAfterRender=${async (elementArg: HTMLElement) => {
// Get all checkboxes for demo interactions
const checkboxes = elementArg.querySelectorAll('dees-input-checkbox');
// Example of programmatic interaction
const selectAllBtn = elementArg.querySelector('#select-all-btn');
const clearAllBtn = elementArg.querySelector('#clear-all-btn');
if (selectAllBtn && clearAllBtn) {
selectAllBtn.addEventListener('click', () => {
checkboxes.forEach((checkbox: DeesInputCheckbox) => {
if (!checkbox.disabled && checkbox.key?.startsWith('feature')) {
checkbox.value = true;
}
});
});
clearAllBtn.addEventListener('click', () => {
checkboxes.forEach((checkbox: DeesInputCheckbox) => {
if (!checkbox.disabled && checkbox.key?.startsWith('feature')) {
checkbox.value = false;
}
});
});
}
}}>
<style>
${css`
.demo-container {
display: flex;
flex-direction: column;
gap: 24px;
padding: 24px;
max-width: 1200px;
margin: 0 auto;
}
.demo-section {
background: #f8f9fa;
border-radius: 8px;
padding: 24px;
}
@media (prefers-color-scheme: dark) {
.demo-section {
background: #1a1a1a;
}
}
.demo-section h3 {
margin-top: 0;
margin-bottom: 16px;
color: #0069f2;
font-size: 18px;
}
.demo-section p {
margin-top: 0;
margin-bottom: 16px;
color: #666;
font-size: 14px;
}
@media (prefers-color-scheme: dark) {
.demo-section p {
color: #999;
}
}
.horizontal-group {
display: flex;
align-items: center;
gap: 16px;
flex-wrap: wrap;
}
.checkbox-group {
display: flex;
flex-direction: column;
gap: 8px;
}
.feature-list {
background: #f0f0f0;
border-radius: 4px;
padding: 16px;
margin-bottom: 16px;
}
@media (prefers-color-scheme: dark) {
.feature-list {
background: #0a0a0a;
}
}
.button-group {
display: flex;
gap: 8px;
margin-bottom: 16px;
}
`}
</style>
<div class="demo-container">
<div class="demo-section">
<h3>Basic Checkboxes</h3>
<p>Standard checkbox inputs for boolean selections</p>
<dees-input-checkbox
.label=${'I agree to the Terms and Conditions'}
.value=${true}
.key=${'terms'}
></dees-input-checkbox>
<dees-input-checkbox
.label=${'Subscribe to newsletter'}
.value=${false}
.key=${'newsletter'}
></dees-input-checkbox>
<dees-input-checkbox
.label=${'Enable notifications'}
.required=${true}
.key=${'notifications'}
></dees-input-checkbox>
</div>
<div class="demo-section">
<h3>Horizontal Layout</h3>
<p>Checkboxes arranged horizontally for compact forms</p>
<div class="horizontal-group">
<dees-input-checkbox
.label=${'Option A'}
.layoutMode=${'horizontal'}
.key=${'optionA'}
></dees-input-checkbox>
<dees-input-checkbox
.label=${'Option B'}
.layoutMode=${'horizontal'}
.value=${true}
.key=${'optionB'}
></dees-input-checkbox>
<dees-input-checkbox
.label=${'Option C'}
.layoutMode=${'horizontal'}
.key=${'optionC'}
></dees-input-checkbox>
<dees-input-checkbox
.label=${'Option D'}
.layoutMode=${'horizontal'}
.value=${true}
.key=${'optionD'}
></dees-input-checkbox>
</div>
</div>
<div class="demo-section">
<h3>Feature Selection Example</h3>
<p>Common use case for feature toggles with batch operations</p>
<div class="button-group">
<dees-button id="select-all-btn" type="secondary">Select All</dees-button>
<dees-button id="clear-all-btn" type="secondary">Clear All</dees-button>
</div>
<div class="feature-list">
<div class="checkbox-group">
<dees-input-checkbox
.label=${'Dark Mode Support'}
.value=${true}
.key=${'feature1'}
></dees-input-checkbox>
<dees-input-checkbox
.label=${'Email Notifications'}
.value=${true}
.key=${'feature2'}
></dees-input-checkbox>
<dees-input-checkbox
.label=${'Two-Factor Authentication'}
.value=${false}
.key=${'feature3'}
></dees-input-checkbox>
<dees-input-checkbox
.label=${'API Access'}
.value=${true}
.key=${'feature4'}
></dees-input-checkbox>
<dees-input-checkbox
.label=${'Advanced Analytics'}
.value=${false}
.key=${'feature5'}
></dees-input-checkbox>
</div>
</div>
</div>
<div class="demo-section">
<h3>States</h3>
<p>Different checkbox states and configurations</p>
<dees-input-checkbox
.label=${'Disabled Unchecked'}
.disabled=${true}
.key=${'disabled1'}
></dees-input-checkbox>
<dees-input-checkbox
.label=${'Disabled Checked'}
.disabled=${true}
.value=${true}
.key=${'disabled2'}
></dees-input-checkbox>
<dees-input-checkbox
.label=${'Required Checkbox'}
.required=${true}
.key=${'required'}
></dees-input-checkbox>
</div>
<div class="demo-section">
<h3>Real-world Examples</h3>
<p>Common checkbox patterns in applications</p>
<div class="checkbox-group">
<dees-input-checkbox
.label=${'Remember me on this device'}
.value=${true}
.key=${'rememberMe'}
></dees-input-checkbox>
<dees-input-checkbox
.label=${'Make my profile public'}
.value=${false}
.key=${'publicProfile'}
></dees-input-checkbox>
<dees-input-checkbox
.label=${'Allow others to find me by email'}
.value=${false}
.key=${'findByEmail'}
></dees-input-checkbox>
<dees-input-checkbox
.label=${'Send me product updates and announcements'}
.value=${true}
.key=${'productUpdates'}
></dees-input-checkbox>
</div>
</div>
</div>
</dees-demowrapper>
`;

View File

@ -1,14 +1,13 @@
import {
customElement,
DeesElement,
type TemplateResult,
property,
html,
css,
cssManager,
type CSSResult,
} from '@design.estate/dees-element';
import * as domtools from '@design.estate/dees-domtools';
import { DeesInputBase } from './dees-input-base.js';
import { demoFunc } from './dees-input-checkbox.demo.js';
declare global {
interface HTMLElementTagNameMap {
@ -17,51 +16,33 @@ declare global {
}
@customElement('dees-input-checkbox')
export class DeesInputCheckbox extends DeesElement {
export class DeesInputCheckbox extends DeesInputBase<DeesInputCheckbox> {
// STATIC
public static demo = () => html`<dees-input-checkbox></dees-input-checkbox>`;
public static demo = demoFunc;
// INSTANCE
public changeSubject = new domtools.plugins.smartrx.rxjs.Subject();
@property({
type: String,
reflect: true,
})
public key: string;
@property({
type: String,
})
public label: string = 'Label';
@property({
type: Boolean,
})
public value: boolean = false;
@property({
type: Boolean,
})
public required: boolean = false;
@property({
type: Boolean
})
public disabled: boolean = false;
constructor() {
super();
this.labelPosition = 'right'; // Checkboxes default to label on the right
}
public render(): TemplateResult {
return html`
${domtools.elementBasic.styles}
<style>
public static styles = [
...DeesInputBase.baseStyles,
cssManager.defaultStyles,
css`
* {
box-sizing: border-box;
}
:host {
display: block;
position: relative;
margin: 20px 0px;
cursor: default;
}
:host(:hover) {
@ -69,21 +50,12 @@ export class DeesInputCheckbox extends DeesElement {
}
.maincontainer {
display: grid;
grid-template-columns: 25px auto;
padding: 5px 0px;
color: ${this.goBright ? '#333' : '#ccc'};
color: ${cssManager.bdTheme('#333', '#ccc')};
}
.maincontainer:hover {
${this.goBright ? '#000' : '#ccc'};
}
.label {
margin-left: 15px;
line-height: 25px;
font-size: 14px;
font-weight: normal;
color: ${cssManager.bdTheme('#000', '#fff')};
}
input:focus {
@ -94,12 +66,12 @@ export class DeesInputCheckbox extends DeesElement {
.checkbox {
transition: all 0.1s;
box-sizing: border-box;
border: 1px solid ${this.goBright ? '#CCC' : '#999'};
border: 1px solid ${cssManager.bdTheme('#CCC', '#999')};
border-radius: 2px;
height: 24px;
width: 24px;
display: inline-block;
background: ${this.goBright ? '#fafafa' : '#222'};
background: ${cssManager.bdTheme('#fafafa', '#222')};
}
.checkbox.selected {
@ -146,7 +118,12 @@ export class DeesInputCheckbox extends DeesElement {
img {
padding: 4px;
}
</style>
`,
];
public render(): TemplateResult {
return html`
<div class="input-wrapper">
<div class="maincontainer" @click="${this.toggleSelected}">
<div class="checkbox ${this.value ? 'selected' : ''} ${this.disabled ? 'disabled' : ''}" tabindex="0">
${this.value
@ -158,7 +135,8 @@ export class DeesInputCheckbox extends DeesElement {
`
: html``}
</div>
<div class="label">${this.label}</div>
</div>
<dees-label .label=${this.label}></dees-label>
</div>
`;
}
@ -177,6 +155,14 @@ export class DeesInputCheckbox extends DeesElement {
this.changeSubject.next(this);
}
public getValue(): boolean {
return this.value;
}
public setValue(value: boolean): void {
this.value = value;
}
public focus(): void {
const checkboxDiv = this.shadowRoot.querySelector('.checkbox');
if (checkboxDiv) {

View File

@ -1,27 +1,200 @@
import { html } from '@design.estate/dees-element';
import { html, css } from '@design.estate/dees-element';
import '@design.estate/dees-wcctools/demotools';
export const demoFunc = () => html`
<dees-demowrapper>
<style>
${css`
.demo-container {
display: flex;
flex-direction: column;
gap: 24px;
padding: 24px;
max-width: 1200px;
margin: 0 auto;
}
.demo-section {
background: #f8f9fa;
border-radius: 8px;
padding: 24px;
position: relative;
}
@media (prefers-color-scheme: dark) {
.demo-section {
background: #1a1a1a;
}
}
.demo-section h3 {
margin-top: 0;
margin-bottom: 16px;
color: #0069f2;
font-size: 18px;
}
.demo-section p {
margin-top: 0;
margin-bottom: 16px;
color: #666;
font-size: 14px;
}
@media (prefers-color-scheme: dark) {
.demo-section p {
color: #999;
}
}
.horizontal-group {
display: flex;
align-items: center;
gap: 16px;
flex-wrap: wrap;
}
.spacer {
height: 200px;
display: flex;
align-items: center;
justify-content: center;
color: #999;
font-size: 14px;
}
`}
</style>
<div class="demo-container">
<div class="demo-section">
<h3>Basic Dropdowns</h3>
<p>Standard dropdown with search functionality and various options</p>
<dees-input-dropdown
.label=${'Select Country'}
.options=${[
{option: 'option 1', key: 'option1'},
{option: 'option 2', key: 'option2'},
{option: 'option 3', key: 'option3'}
{ option: 'United States', key: 'us' },
{ option: 'Canada', key: 'ca' },
{ option: 'Germany', key: 'de' },
{ option: 'France', key: 'fr' },
{ option: 'United Kingdom', key: 'uk' },
{ option: 'Australia', key: 'au' },
{ option: 'Japan', key: 'jp' },
{ option: 'Brazil', key: 'br' }
]}
.selectedOption=${{ option: 'United States', key: 'us' }}
></dees-input-dropdown>
<dees-input-dropdown
.label=${'Select Role'}
.options=${[
{ option: 'Administrator', key: 'admin' },
{ option: 'Editor', key: 'editor' },
{ option: 'Viewer', key: 'viewer' },
{ option: 'Guest', key: 'guest' }
]}
></dees-input-dropdown>
</div>
<div class="demo-section">
<h3>Without Search</h3>
<p>Dropdown with search functionality disabled for simpler selection</p>
<dees-input-dropdown
.label=${'Priority Level'}
.enableSearch=${false}
.options=${[
{option: 'option 1', key: 'option1'},
{option: 'option 2', key: 'option2'},
{option: 'option 3', key: 'option3'}
{ option: 'High', key: 'high' },
{ option: 'Medium', key: 'medium' },
{ option: 'Low', key: 'low' }
]}
.selectedOption=${{ option: 'Medium', key: 'medium' }}
></dees-input-dropdown>
<div style="height: 300px"></div>
</div>
<div class="demo-section">
<h3>Horizontal Layout</h3>
<p>Multiple dropdowns in a horizontal layout for compact forms</p>
<div class="horizontal-group">
<dees-input-dropdown
.label=${'Department'}
.layoutMode=${'horizontal'}
.options=${[
{option: 'option 1', key: 'option1'},
{option: 'option 2', key: 'option2'},
{option: 'option 3', key: 'option3'}
{ option: 'Engineering', key: 'eng' },
{ option: 'Design', key: 'design' },
{ option: 'Marketing', key: 'marketing' },
{ option: 'Sales', key: 'sales' }
]}
></dees-input-dropdown>
<dees-input-dropdown
.label=${'Team Size'}
.layoutMode=${'horizontal'}
.enableSearch=${false}
.options=${[
{ option: '1-5', key: 'small' },
{ option: '6-20', key: 'medium' },
{ option: '21-50', key: 'large' },
{ option: '50+', key: 'xlarge' }
]}
></dees-input-dropdown>
<dees-input-dropdown
.label=${'Location'}
.layoutMode=${'horizontal'}
.options=${[
{ option: 'Remote', key: 'remote' },
{ option: 'On-site', key: 'onsite' },
{ option: 'Hybrid', key: 'hybrid' }
]}
></dees-input-dropdown>
</div>
</div>
<div class="demo-section">
<h3>States</h3>
<p>Different states and configurations</p>
<dees-input-dropdown
.label=${'Required Field'}
.required=${true}
.options=${[
{ option: 'Option A', key: 'a' },
{ option: 'Option B', key: 'b' },
{ option: 'Option C', key: 'c' }
]}
></dees-input-dropdown>
<dees-input-dropdown
.label=${'Disabled Dropdown'}
.disabled=${true}
.options=${[
{ option: 'Cannot Select', key: 'disabled' }
]}
.selectedOption=${{ option: 'Cannot Select', key: 'disabled' }}
></dees-input-dropdown>
</div>
<div class="spacer">
(Spacer to test dropdown positioning)
</div>
<div class="demo-section">
<h3>Bottom Positioning</h3>
<p>Dropdown that opens upward when near bottom of viewport</p>
<dees-input-dropdown
.label=${'Opens Upward'}
.options=${[
{ option: 'First Option', key: 'first' },
{ option: 'Second Option', key: 'second' },
{ option: 'Third Option', key: 'third' },
{ option: 'Fourth Option', key: 'fourth' },
{ option: 'Fifth Option', key: 'fifth' }
]}
></dees-input-dropdown>
</div>
</div>
</dees-demowrapper>
`

View File

@ -1,17 +1,16 @@
import {
customElement,
DeesElement,
type TemplateResult,
property,
state,
html,
css,
cssManager,
type CSSResult,
} from '@design.estate/dees-element';
import * as domtools from '@design.estate/dees-domtools';
import { demoFunc } from './dees-input-dropdown.demo.js';
import { DeesWindowLayer } from './dees-windowlayer.js';
import { DeesInputBase } from './dees-input-base.js';
declare global {
interface HTMLElementTagNameMap {
@ -20,20 +19,10 @@ declare global {
}
@customElement('dees-input-dropdown')
export class DeesInputDropdown extends DeesElement {
export class DeesInputDropdown extends DeesInputBase<DeesInputDropdown> {
public static demo = demoFunc;
// INSTANCE
public changeSubject = new domtools.plugins.smartrx.rxjs.Subject();
@property({
type: String,
reflect: true,
})
public label: string = 'Label';
@property()
public key: string;
@property()
public options: { option: string; key: string; payload?: any }[] = [];
@ -41,20 +30,21 @@ export class DeesInputDropdown extends DeesElement {
@property()
public selectedOption: { option: string; key: string; payload?: any } = null;
@property({
type: Boolean,
})
public required: boolean = false;
// Add value property for form compatibility
public get value() {
return this.selectedOption;
}
public set value(val: { option: string; key: string; payload?: any }) {
this.selectedOption = val;
}
@property({
type: Boolean,
})
public enableSearch: boolean = true;
@property({
type: Boolean,
})
public disabled: boolean = false;
@state()
public opensToTop: boolean = false;
@ -69,6 +59,7 @@ export class DeesInputDropdown extends DeesElement {
public isOpened = false;
public static styles = [
...DeesInputBase.baseStyles,
cssManager.defaultStyles,
css`
* {
@ -78,19 +69,13 @@ export class DeesInputDropdown extends DeesElement {
:host {
font-family: Roboto;
position: relative;
display: block;
color: ${cssManager.bdTheme('#222', '#fff')};
margin-bottom: 24px;
}
.maincontainer {
display: block;
}
.label {
font-size: 14px;
margin-bottom: 8px;
}
.selectedBox {
user-select: none;
@ -205,8 +190,9 @@ export class DeesInputDropdown extends DeesElement {
public render(): TemplateResult {
return html`
<div class="input-wrapper">
<dees-label .label=${this.label}></dees-label>
<div class="maincontainer" @keydown="${this.isOpened ? this.handleKeyDown : undefined}">
${this.label ? html`<div class="label">${this.label}</div>` : html``}
<div class="selectionBox">
${this.enableSearch && !this.opensToTop
? html`
@ -249,6 +235,7 @@ export class DeesInputDropdown extends DeesElement {
${this.selectedOption?.option || 'Select...'}
</div>
</div>
</div>
`;
}
@ -372,4 +359,12 @@ export class DeesInputDropdown extends DeesElement {
event.preventDefault();
}
}
public getValue(): { option: string; key: string; payload?: any } {
return this.selectedOption;
}
public setValue(value: { option: string; key: string; payload?: any }): void {
this.selectedOption = value;
}
}

View File

@ -0,0 +1,138 @@
import { html, css, cssManager } from '@design.estate/dees-element';
export const demoFunc = () => html`
<dees-demowrapper>
<style>
${css`
.demo-container {
display: flex;
flex-direction: column;
gap: 24px;
padding: 24px;
max-width: 1200px;
margin: 0 auto;
}
.upload-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 24px;
}
@media (max-width: 768px) {
.upload-grid {
grid-template-columns: 1fr;
}
}
.upload-box {
padding: 16px;
background: ${cssManager.bdTheme('#fff', '#2a2a2a')};
border-radius: 4px;
border: 1px solid ${cssManager.bdTheme('#e0e0e0', '#444')};
}
.upload-box h4 {
margin-top: 0;
margin-bottom: 16px;
color: ${cssManager.bdTheme('#333', '#fff')};
font-size: 16px;
}
.info-section {
margin-top: 32px;
padding: 16px;
background: ${cssManager.bdTheme('#fff3cd', '#332701')};
border: 1px solid ${cssManager.bdTheme('#ffeaa7', '#664400')};
border-radius: 4px;
color: ${cssManager.bdTheme('#856404', '#ffecb5')};
}
`}
</style>
<div class="demo-container">
<dees-panel .title=${'Basic File Upload'} .subtitle=${'Simple file upload with drag and drop support'}>
<dees-input-fileupload
.label=${'Attachments'}
.description=${'Upload files by clicking or dragging'}
></dees-input-fileupload>
<dees-input-fileupload
.label=${'Resume'}
.description=${'Upload your CV in PDF format'}
.buttonText=${'Choose Resume...'}
></dees-input-fileupload>
</dees-panel>
<dees-panel .title=${'Multiple Upload Areas'} .subtitle=${'Different upload zones for various file types'}>
<div class="upload-grid">
<div class="upload-box">
<h4>Profile Picture</h4>
<dees-input-fileupload
.label=${'Avatar'}
.description=${'JPG, PNG or GIF'}
.buttonText=${'Select Image...'}
></dees-input-fileupload>
</div>
<div class="upload-box">
<h4>Cover Image</h4>
<dees-input-fileupload
.label=${'Banner'}
.description=${'Recommended: 1200x400px'}
.buttonText=${'Select Banner...'}
></dees-input-fileupload>
</div>
</div>
</dees-panel>
<dees-panel .title=${'Required & Disabled States'} .subtitle=${'Different upload states for validation'}>
<dees-input-fileupload
.label=${'Identity Document'}
.description=${'Required for verification'}
.required=${true}
.buttonText=${'Upload Document...'}
></dees-input-fileupload>
<dees-input-fileupload
.label=${'System Files'}
.description=${'File upload is disabled'}
.disabled=${true}
.value=${[]}
></dees-input-fileupload>
</dees-panel>
<dees-panel .title=${'Application Form'} .subtitle=${'Complete form with file upload integration'}>
<dees-form>
<dees-input-text .label=${'Full Name'} .required=${true}></dees-input-text>
<dees-input-text .label=${'Email'} .inputType=${'email'} .required=${true}></dees-input-text>
<dees-input-fileupload
.label=${'Resume'}
.description=${'Upload your CV (PDF preferred)'}
.required=${true}
></dees-input-fileupload>
<dees-input-fileupload
.label=${'Portfolio'}
.description=${'Optional: Upload work samples'}
></dees-input-fileupload>
<dees-input-text
.label=${'Cover Letter'}
.inputType=${'textarea'}
.description=${'Tell us why you would be a great fit'}
></dees-input-text>
</dees-form>
<div class="info-section">
<h4>Features:</h4>
<ul>
<li>Click to select files or drag & drop</li>
<li>Multiple file selection support</li>
<li>Visual feedback for drag operations</li>
<li>Right-click files to remove them</li>
<li>Integrates seamlessly with forms</li>
</ul>
</div>
</dees-panel>
</div>
</dees-demowrapper>
`;

View File

@ -2,6 +2,8 @@ import * as colors from './00colors.js';
import * as plugins from './00plugins.js';
import { DeesContextmenu } from './dees-contextmenu.js';
import { DeesInputBase } from './dees-input-base.js';
import { demoFunc } from './dees-input-fileupload.demo.js';
import {
customElement,
@ -23,23 +25,9 @@ declare global {
}
@customElement('dees-input-fileupload')
export class DeesInputFileupload extends DeesElement {
public static demo = () =>
html`<dees-input-fileupload .label=${'Attachments'}></dees-input-fileupload>`;
export class DeesInputFileupload extends DeesInputBase<DeesInputFileupload> {
public static demo = demoFunc;
// INSTANCE
public changeSubject = new domtools.plugins.smartrx.rxjs.Subject();
@property({
type: String,
})
public label: string = null;
@property({
type: String,
reflect: true,
})
public key: string;
@property({
attribute: false,
@ -49,16 +37,6 @@ export class DeesInputFileupload extends DeesElement {
@property()
public state: 'idle' | 'dragOver' | 'dropped' | 'uploading' | 'completed' = 'idle';
@property({
type: Boolean,
})
public required: boolean = false;
@property({
type: Boolean,
})
public disabled: boolean = false;
@property({
type: String,
})
@ -69,13 +47,12 @@ export class DeesInputFileupload extends DeesElement {
}
public static styles = [
...DeesInputBase.baseStyles,
cssManager.defaultStyles,
css`
:host {
position: relative;
display: grid;
margin: 10px 0px;
margin-bottom: 24px;
color: ${cssManager.bdTheme('#333', '#ccc')};
}
@ -112,11 +89,6 @@ export class DeesInputFileupload extends DeesElement {
background: #00000080;
}
.label {
font-size: 14px;
margin-bottom: 8px;
}
.uploadButton {
position: relative;
padding: 8px;
@ -173,10 +145,11 @@ export class DeesInputFileupload extends DeesElement {
public render(): TemplateResult {
return html`
<div class="input-wrapper">
<dees-label .label=${this.label} .description=${this.description}></dees-label>
<div class="hidden">
<input type="file"></div>
<input type="file">
</div>
${this.label ? html`<div class="label">${this.label}</div>` : null}
<div class="maincontainer ${this.state === 'dragOver' ? 'dragOver' : ''}">
${this.value.map(
(fileArg) => html`
@ -205,6 +178,7 @@ export class DeesInputFileupload extends DeesElement {
${this.buttonText}
</div>
</div>
</div>
`;
}
@ -221,7 +195,8 @@ export class DeesInputFileupload extends DeesElement {
this.changeSubject.next(this);
}
public firstUpdated() {
public firstUpdated(_changedProperties: Map<string | number | symbol, unknown>) {
super.firstUpdated(_changedProperties);
const inputFile: HTMLInputElement = this.shadowRoot.querySelector('input[type="file"]');
inputFile.addEventListener('change', (event: Event) => {
const target = event.target as HTMLInputElement;
@ -263,4 +238,12 @@ export class DeesInputFileupload extends DeesElement {
dropArea.addEventListener('dragover', handlerFunction, false);
dropArea.addEventListener('drop', handlerFunction, false);
}
public getValue(): File[] {
return this.value;
}
public setValue(value: File[]): void {
this.value = value;
}
}

View File

@ -1,3 +1,80 @@
import { html } from '@design.estate/dees-element';
import { html, css } from '@design.estate/dees-element';
export const demoFunc = () => html`<dees-input-iban .label=${'IBAN'}></dees-input-iban>`;
export const demoFunc = () => html`
<dees-demowrapper>
<style>
${css`
.demo-container {
display: flex;
flex-direction: column;
gap: 24px;
padding: 24px;
max-width: 1200px;
margin: 0 auto;
}
.payment-group {
display: flex;
align-items: center;
gap: 16px;
flex-wrap: wrap;
}
`}
</style>
<div class="demo-container">
<dees-panel .title=${'Basic IBAN Input'} .subtitle=${'International Bank Account Number with automatic formatting'}>
<dees-input-iban
.label=${'Bank Account IBAN'}
.description=${'Enter your International Bank Account Number'}
></dees-input-iban>
<dees-input-iban
.label=${'Verified IBAN'}
.description=${'This IBAN has been verified'}
.value=${'DE89370400440532013000'}
></dees-input-iban>
</dees-panel>
<dees-panel .title=${'Payment Information'} .subtitle=${'IBAN input with horizontal layout for payment forms'}>
<div class="payment-group">
<dees-input-text
.label=${'Account Holder'}
.layoutMode=${'horizontal'}
.value=${'John Doe'}
></dees-input-text>
<dees-input-iban
.label=${'IBAN'}
.layoutMode=${'horizontal'}
.value=${'GB82WEST12345698765432'}
></dees-input-iban>
</div>
</dees-panel>
<dees-panel .title=${'Validation & States'} .subtitle=${'Required fields and disabled states'}>
<dees-input-iban
.label=${'Payment Account'}
.description=${'Required for processing payments'}
.required=${true}
></dees-input-iban>
<dees-input-iban
.label=${'Locked IBAN'}
.description=${'This IBAN cannot be changed'}
.value=${'FR1420041010050500013M02606'}
.disabled=${true}
></dees-input-iban>
</dees-panel>
<dees-panel .title=${'Bank Transfer Form'} .subtitle=${'Complete form example with IBAN validation'}>
<dees-form>
<dees-input-text .label=${'Recipient Name'} .required=${true}></dees-input-text>
<dees-input-iban .label=${'Recipient IBAN'} .required=${true}></dees-input-iban>
<dees-input-text .label=${'Transfer Reference'} .description=${'Optional reference for the transfer'}></dees-input-text>
<dees-input-text .label=${'Amount'} .inputType=${'number'} .required=${true}></dees-input-text>
</dees-form>
</dees-panel>
</div>
</dees-demowrapper>
`;

View File

@ -1,18 +1,19 @@
import {
customElement,
DeesElement,
type TemplateResult,
state,
html,
domtools,
property,
css,
cssManager,
} from '@design.estate/dees-element';
import * as domtools from '@design.estate/dees-domtools';
import { DeesInputBase } from './dees-input-base.js';
import * as ibantools from 'ibantools';
import { demoFunc } from './dees-input-iban.demo.js';
@customElement('dees-input-iban')
export class DeesInputIban extends DeesElement {
export class DeesInputIban extends DeesInputBase<DeesInputIban> {
// STATIC
public static demo = demoFunc;
@ -23,60 +24,44 @@ export class DeesInputIban extends DeesElement {
@state()
enteredIbanIsValid: boolean = false;
@property({
type: Boolean,
})
public disabled = false;
@property({
type: Boolean,
})
public required = false;
@property({
type: String,
})
public label = '';
@property({
type: String,
})
public key = '';
@property({
type: String,
})
public value = '';
public changeSubject = new domtools.plugins.smartrx.rxjs.Subject<DeesInputIban>();
public static styles = [
...DeesInputBase.baseStyles,
cssManager.defaultStyles,
css`
/* IBAN input specific styles can go here */
`,
];
public render(): TemplateResult {
return html`
<style>
input[type='text'] {
line-height: 20px;
padding: 5px;
width: 250px;
}
</style>
<div class="input-wrapper">
<dees-label .label=${this.label || 'IBAN'} .description=${this.description}></dees-label>
<dees-input-text
.label=${'IBAN'}
.value=${this.value}
.disabled=${this.disabled}
.required=${this.required}
.placeholder=${'DE89 3704 0044 0532 0130 00'}
@input=${(eventArg: InputEvent) => {
this.validateIban(eventArg);
}}
></dees-input-text>
</div>
`;
}
public async firstUpdated() {
const deesInputText = this.shadowRoot.querySelector('dees-input-text');
deesInputText.disabled = this.disabled;
deesInputText.required = this.required;
deesInputText.changeSubject.subscribe(valueArg => {
this.value = valueArg.value;
public firstUpdated(_changedProperties: Map<string | number | symbol, unknown>) {
super.firstUpdated(_changedProperties);
const deesInputText = this.shadowRoot.querySelector('dees-input-text') as any;
if (deesInputText && deesInputText.changeSubject) {
deesInputText.changeSubject.subscribe(() => {
this.changeSubject.next(this);
})
});
}
}
public async validateIban(eventArg: InputEvent): Promise<void> {
@ -95,4 +80,13 @@ export class DeesInputIban extends DeesElement {
const deesInputText = this.shadowRoot.querySelector('dees-input-text');
deesInputText.validationText = `IBAN is valid: ${this.enteredIbanIsValid}`;
}
public getValue(): string {
return this.value;
}
public setValue(value: string): void {
this.value = value;
this.enteredString = ibantools.friendlyFormatIBAN(value) || '';
}
}

View File

@ -1,14 +1,128 @@
import { html } from '@design.estate/dees-element';
import { html, css } from '@design.estate/dees-element';
export const demoFunc = () => html`
<dees-demowrapper>
<style>
${css`
.demo-container {
display: flex;
flex-direction: column;
gap: 24px;
padding: 24px;
max-width: 1200px;
margin: 0 auto;
}
.settings-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 24px;
}
@media (max-width: 768px) {
.settings-grid {
grid-template-columns: 1fr;
}
}
`}
</style>
<div class="demo-container">
<dees-panel .title=${'Multi-Option Toggle'} .subtitle=${'Select from multiple options with a sliding indicator'}>
<dees-input-multitoggle
.options=${['option 1', 'option 2', 'a longer option with multiple words']}
.selectedOption=${'option 2'}
.label=${'Display Mode'}
.description=${'Choose how content is displayed'}
.options=${['List View', 'Grid View', 'Compact']}
.selectedOption=${'Grid View'}
></dees-input-multitoggle>
<dees-input-multitoggle
.label=${'T-Shirt Size'}
.description=${'Select your preferred size'}
.options=${['XS', 'S', 'M', 'L', 'XL', 'XXL']}
.selectedOption=${'M'}
></dees-input-multitoggle>
</dees-panel>
<dees-panel .title=${'Boolean Toggle'} .subtitle=${'Simple on/off switches with custom labels'}>
<dees-input-multitoggle
.label=${'Notifications'}
.description=${'Enable or disable push notifications'}
.type=${'boolean'}
.booleanTrueName=${'enabled'}
.booleanFalseName=${'disabled'}
.selectedOption=${'true'}
></dees-input-multitoggle>
<dees-input-multitoggle
.label=${'Theme Mode'}
.description=${'Switch between light and dark theme'}
.type=${'boolean'}
.booleanTrueName=${'Dark'}
.booleanFalseName=${'Light'}
.selectedOption=${'Dark'}
></dees-input-multitoggle>
</dees-panel>
<dees-panel .title=${'Settings Panel'} .subtitle=${'Configuration options in a horizontal layout'}>
<div class="settings-grid">
<dees-input-multitoggle
.label=${'Auto-Save'}
.layoutMode=${'horizontal'}
.type=${'boolean'}
.booleanTrueName=${'Enabled'}
.booleanFalseName=${'Disabled'}
.selectedOption=${'Enabled'}
></dees-input-multitoggle>
<dees-input-multitoggle
.label=${'Language'}
.layoutMode=${'horizontal'}
.options=${['English', 'German', 'French', 'Spanish']}
.selectedOption=${'English'}
></dees-input-multitoggle>
<dees-input-multitoggle
.label=${'Quality'}
.layoutMode=${'horizontal'}
.options=${['Low', 'Medium', 'High', 'Ultra']}
.selectedOption=${'High'}
></dees-input-multitoggle>
<dees-input-multitoggle
.label=${'Privacy'}
.layoutMode=${'horizontal'}
.type=${'boolean'}
.booleanTrueName=${'Private'}
.booleanFalseName=${'Public'}
.selectedOption=${'Private'}
></dees-input-multitoggle>
</div>
</dees-panel>
<dees-panel .title=${'States & Form Integration'} .subtitle=${'Disabled states and form usage'}>
<dees-input-multitoggle
.label=${'Account Type'}
.description=${'This setting is locked'}
.options=${['Free', 'Pro', 'Enterprise']}
.selectedOption=${'Enterprise'}
.disabled=${true}
></dees-input-multitoggle>
<dees-form>
<dees-input-text .label=${'Project Name'} .required=${true}></dees-input-text>
<dees-input-multitoggle
.label=${'Visibility'}
.type=${'boolean'}
.booleanTrueName=${'Public'}
.booleanFalseName=${'Private'}
.selectedOption=${'Private'}
></dees-input-multitoggle>
<dees-input-multitoggle
.label=${'License'}
.options=${['MIT', 'Apache 2.0', 'GPL v3', 'Proprietary']}
.selectedOption=${'MIT'}
></dees-input-multitoggle>
</dees-form>
</dees-panel>
</div>
</dees-demowrapper>
`;

View File

@ -1,14 +1,14 @@
import {
customElement,
DeesElement,
type TemplateResult,
state,
html,
domtools,
property,
css,
cssManager,
} from '@design.estate/dees-element';
import { DeesInputBase } from './dees-input-base.js';
import * as colors from './00colors.js'
const { demoFunc } = await import('./dees-input-multitoggle.demo.js');
@ -19,18 +19,9 @@ declare global {
}
@customElement('dees-input-multitoggle')
export class DeesInputMultitoggle extends DeesElement {
export class DeesInputMultitoggle extends DeesInputBase<DeesInputMultitoggle> {
public static demo = demoFunc;
@property({
type: String,
})
public label: string;
@property({
type: String,
})
public description: string;
@property()
type: 'boolean' | 'multi' | 'single' = 'multi';
@ -49,23 +40,38 @@ export class DeesInputMultitoggle extends DeesElement {
@property()
selectedOption: string = '';
@property()
@property({ type: Boolean })
boolValue: boolean = false;
// Add value property for form compatibility
public get value(): string | boolean {
if (this.type === 'boolean') {
return this.selectedOption === this.booleanTrueName;
}
return this.selectedOption;
}
public set value(val: string | boolean) {
if (this.type === 'boolean' && typeof val === 'boolean') {
this.selectedOption = val ? this.booleanTrueName : this.booleanFalseName;
} else {
this.selectedOption = val as string;
}
// Defer indicator update to next frame if component not yet updated
if (this.hasUpdated) {
this.setIndicator();
}
}
public static styles = [
...DeesInputBase.baseStyles,
cssManager.defaultStyles,
css`
:host {
display: block;
color: ${cssManager.bdTheme('#333', '#ccc')};
user-select: none;
margin: 8px 0px 24px 0px;
}
.label {
font-size: 14px;
margin-bottom: 8px;
}
.selections {
position: relative;
@ -76,11 +82,11 @@ export class DeesInputMultitoggle extends DeesElement {
width: min-content;
border-radius: 20px;
height: 32px;
border-top: 1px solid #ffffff10;
border-top: 1px solid ${cssManager.bdTheme('rgba(0,0,0,0.1)', 'rgba(255,255,255,0.1)')};
}
.option {
color: #ccc;
color: ${cssManager.bdTheme('#666', '#999')};
position: relative;
padding: 0px 16px;
line-height: 32px;
@ -93,11 +99,11 @@ export class DeesInputMultitoggle extends DeesElement {
}
.option:hover {
color: #fff;
color: ${cssManager.bdTheme('#333', '#fff')};
}
.option.selected {
color: #fff;
color: ${cssManager.bdTheme('#fff', '#fff')};
}
.indicator {
@ -107,14 +113,20 @@ export class DeesInputMultitoggle extends DeesElement {
left: 4px;
top: 3px;
border-radius: 16px;
background: #0050b9;
min-width: 36px;
background: ${cssManager.bdTheme(colors.bright.blueActive, colors.dark.blueActive)};
min-width: 24px;
transition: all 0.1s ease-in-out;
}
.indicator.no-transition {
transition: none;
}
`,
];
public render(): TemplateResult {
return html`
<div class="input-wrapper">
<dees-label .label=${this.label} .description=${this.description}></dees-label>
<div class="mainbox">
<div class="selections">
@ -127,14 +139,29 @@ export class DeesInputMultitoggle extends DeesElement {
)}
</div>
</div>
</div>
`;
}
public async firstUpdated() {
public async connectedCallback() {
await super.connectedCallback();
// Initialize boolean options early
if (this.type === 'boolean' && this.options.length === 0) {
this.options = [this.booleanTrueName || 'true', this.booleanFalseName || 'false'];
}
}
public async firstUpdated(_changedProperties: Map<string | number | symbol, unknown>) {
super.firstUpdated(_changedProperties);
// Update boolean options if they changed
if (this.type === 'boolean') {
this.options = [this.booleanTrueName || 'true', this.booleanFalseName || 'false'];
}
// Wait for the next frame to ensure DOM is fully rendered
await this.updateComplete;
requestAnimationFrame(() => {
this.setIndicator();
});
}
public async handleSelection(optionArg: string) {
@ -142,18 +169,57 @@ export class DeesInputMultitoggle extends DeesElement {
this.setIndicator();
}
private indicatorInitialized = false;
public async setIndicator() {
const indicator: HTMLDivElement = this.shadowRoot.querySelector('.indicator');
const selectedIndex = this.options.indexOf(this.selectedOption);
// If no valid selection, hide indicator
if (selectedIndex === -1 || !indicator) {
if (indicator) {
indicator.style.opacity = '0';
}
return;
}
const option: HTMLDivElement = this.shadowRoot.querySelector(
`.option:nth-child(${this.options.indexOf(this.selectedOption) + 2})`
`.option:nth-child(${selectedIndex + 2})`
);
if (indicator && option) {
// Only disable transition for the very first positioning
if (!this.indicatorInitialized) {
indicator.classList.add('no-transition');
this.indicatorInitialized = true;
// Remove the no-transition class after a brief delay
setTimeout(() => {
indicator.classList.remove('no-transition');
}, 50);
}
indicator.style.width = `${option.clientWidth - 8}px`;
indicator.style.left = `${option.offsetLeft + 4}px`;
indicator.style.opacity = '1';
}
setTimeout(() => {
indicator.style.transition = 'all 0.1s';
}, 100);
}
public getValue(): string | boolean {
if (this.type === 'boolean') {
return this.selectedOption === this.booleanTrueName;
}
return this.selectedOption;
}
public setValue(value: string | boolean): void {
if (this.type === 'boolean' && typeof value === 'boolean') {
this.selectedOption = value ? (this.booleanTrueName || 'true') : (this.booleanFalseName || 'false');
} else {
this.selectedOption = value as string;
}
if (this.hasUpdated) {
this.setIndicator();
}
}
}

View File

@ -1,3 +1,80 @@
import { html } from '@design.estate/dees-element';
import { html, css } from '@design.estate/dees-element';
export const demoFunc = () => html`<dees-input-phone .label=${'Phone Number'}></dees-input-phone>`;
export const demoFunc = () => html`
<dees-demowrapper>
<style>
${css`
.demo-container {
display: flex;
flex-direction: column;
gap: 24px;
padding: 24px;
max-width: 1200px;
margin: 0 auto;
}
.horizontal-group {
display: flex;
align-items: center;
gap: 16px;
flex-wrap: wrap;
}
`}
</style>
<div class="demo-container">
<dees-panel .title=${'Basic Phone Input'} .subtitle=${'Automatic formatting for phone numbers'}>
<dees-input-phone
.label=${'Phone Number'}
.description=${'Enter your phone number with country code'}
.value=${'5551234567'}
></dees-input-phone>
<dees-input-phone
.label=${'Contact Phone'}
.description=${'Required for account verification'}
.required=${true}
.placeholder=${'+1 (555) 000-0000'}
></dees-input-phone>
</dees-panel>
<dees-panel .title=${'Horizontal Layout'} .subtitle=${'Phone inputs arranged horizontally'}>
<div class="horizontal-group">
<dees-input-phone
.label=${'Mobile'}
.layoutMode=${'horizontal'}
.value=${'4155551234'}
></dees-input-phone>
<dees-input-phone
.label=${'Office'}
.layoutMode=${'horizontal'}
.placeholder=${'+1 (800) 555-0000'}
></dees-input-phone>
</div>
</dees-panel>
<dees-panel .title=${'International Numbers'} .subtitle=${'Supports formatting for numbers with country codes'}>
<dees-input-phone
.label=${'International Contact'}
.description=${'Automatically formats international numbers'}
.value=${'441234567890'}
></dees-input-phone>
<dees-input-phone
.label=${'Emergency Contact'}
.value=${'911'}
.disabled=${true}
></dees-input-phone>
</dees-panel>
<dees-panel .title=${'Form Integration'} .subtitle=${'Phone input as part of a contact form'}>
<dees-form>
<dees-input-text .label=${'Full Name'} .required=${true}></dees-input-text>
<dees-input-phone .label=${'Phone Number'} .required=${true}></dees-input-phone>
<dees-input-text .label=${'Email'} .inputType=${'email'}></dees-input-text>
</dees-form>
</dees-panel>
</div>
</dees-demowrapper>
`;

View File

@ -1,14 +1,14 @@
import {
customElement,
DeesElement,
type TemplateResult,
property,
state,
html,
css,
unsafeCSS,
cssManager,
type CSSResult,
} from '@design.estate/dees-element';
import * as domtools from '@design.estate/dees-domtools';
import { DeesInputBase } from './dees-input-base.js';
import { demoFunc } from './dees-input-phone.demo.js';
declare global {
@ -18,12 +18,116 @@ declare global {
}
@customElement('dees-input-phone')
export class DeesInputPhone extends DeesElement {
export class DeesInputPhone extends DeesInputBase<DeesInputPhone> {
// STATIC
public static demo = demoFunc;
// INSTANCE
public render() {
return html`<div>Phone Input</div>`;
@state()
protected formattedPhone: string = '';
@property({ type: String })
public value: string = '';
@property({ type: String })
public placeholder: string = '+1 (555) 123-4567';
public static styles = [
...DeesInputBase.baseStyles,
cssManager.defaultStyles,
css`
/* Phone input specific styles can go here */
`,
];
public render(): TemplateResult {
return html`
<div class="input-wrapper">
<dees-label .label=${this.label} .description=${this.description}></dees-label>
<dees-input-text
.value=${this.formattedPhone}
.disabled=${this.disabled}
.required=${this.required}
.placeholder=${this.placeholder}
@input=${(event: InputEvent) => this.handlePhoneInput(event)}
></dees-input-text>
</div>
`;
}
public firstUpdated(_changedProperties: Map<string | number | symbol, unknown>) {
super.firstUpdated(_changedProperties);
// Initialize formatted phone from value
if (this.value) {
this.formattedPhone = this.formatPhoneNumber(this.value);
}
// Subscribe to the inner input's changes
const innerInput = this.shadowRoot.querySelector('dees-input-text') as any;
if (innerInput && innerInput.changeSubject) {
innerInput.changeSubject.subscribe(() => {
this.changeSubject.next(this);
});
}
}
private handlePhoneInput(event: InputEvent) {
const input = event.target as HTMLInputElement;
const cleanedValue = this.cleanPhoneNumber(input.value);
const formatted = this.formatPhoneNumber(cleanedValue);
// Update the input with formatted value
if (input.value !== formatted) {
const cursorPosition = input.selectionStart || 0;
input.value = formatted;
// Try to maintain cursor position intelligently
const newCursorPos = this.calculateCursorPosition(cleanedValue, formatted, cursorPosition);
input.setSelectionRange(newCursorPos, newCursorPos);
}
this.formattedPhone = formatted;
this.value = cleanedValue;
this.changeSubject.next(this);
}
private cleanPhoneNumber(value: string): string {
// Remove all non-numeric characters
return value.replace(/\D/g, '');
}
private formatPhoneNumber(value: string): string {
// Basic US phone number formatting
// This can be enhanced to support international formats
const cleaned = this.cleanPhoneNumber(value);
if (cleaned.length === 0) return '';
if (cleaned.length <= 3) return cleaned;
if (cleaned.length <= 6) return `(${cleaned.slice(0, 3)}) ${cleaned.slice(3)}`;
if (cleaned.length <= 10) return `(${cleaned.slice(0, 3)}) ${cleaned.slice(3, 6)}-${cleaned.slice(6)}`;
// For numbers longer than 10 digits, format as international
return `+${cleaned.slice(0, cleaned.length - 10)} (${cleaned.slice(-10, -7)}) ${cleaned.slice(-7, -4)}-${cleaned.slice(-4)}`;
}
private calculateCursorPosition(cleaned: string, formatted: string, oldPos: number): number {
// Simple cursor position calculation
// Count how many formatting characters are before the cursor
let formattingChars = 0;
for (let i = 0; i < oldPos && i < formatted.length; i++) {
if (!/\d/.test(formatted[i])) {
formattingChars++;
}
}
return Math.min(oldPos + formattingChars, formatted.length);
}
public getValue(): string {
return this.value;
}
public setValue(value: string): void {
this.value = value;
this.formattedPhone = this.formatPhoneNumber(value);
}
}

View File

@ -0,0 +1,127 @@
import { html, css, cssManager } from '@design.estate/dees-element';
export const demoFunc = () => html`
<dees-demowrapper>
<style>
${css`
.demo-container {
display: flex;
flex-direction: column;
gap: 24px;
padding: 24px;
max-width: 1200px;
margin: 0 auto;
}
.shopping-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 16px;
}
.product-card {
padding: 16px;
background: ${cssManager.bdTheme('#fff', '#2a2a2a')};
border-radius: 4px;
box-shadow: 0 2px 4px ${cssManager.bdTheme('rgba(0,0,0,0.1)', 'rgba(0,0,0,0.3)')};
}
.product-name {
font-weight: 600;
margin-bottom: 8px;
}
.product-price {
color: #1976d2;
margin-bottom: 16px;
}
`}
</style>
<div class="demo-container">
<dees-panel .title=${'Basic Quantity Selector'} .subtitle=${'Simple quantity input with increment/decrement buttons'}>
<dees-input-quantityselector
.label=${'Quantity'}
.description=${'Select the desired quantity'}
.value=${1}
></dees-input-quantityselector>
<dees-input-quantityselector
.label=${'Items in Cart'}
.description=${'Adjust the quantity of items'}
.value=${3}
></dees-input-quantityselector>
</dees-panel>
<dees-panel .title=${'Shopping Cart'} .subtitle=${'Product cards with quantity selectors'}>
<div class="shopping-grid">
<div class="product-card">
<div class="product-name">Premium Headphones</div>
<div class="product-price">$199.99</div>
<dees-input-quantityselector
.label=${'Quantity'}
.layoutMode=${'horizontal'}
.value=${1}
></dees-input-quantityselector>
</div>
<div class="product-card">
<div class="product-name">Wireless Mouse</div>
<div class="product-price">$49.99</div>
<dees-input-quantityselector
.label=${'Quantity'}
.layoutMode=${'horizontal'}
.value=${2}
></dees-input-quantityselector>
</div>
<div class="product-card">
<div class="product-name">USB-C Cable</div>
<div class="product-price">$19.99</div>
<dees-input-quantityselector
.label=${'Quantity'}
.layoutMode=${'horizontal'}
.value=${1}
></dees-input-quantityselector>
</div>
</div>
</dees-panel>
<dees-panel .title=${'Required & Disabled States'} .subtitle=${'Different states for validation and restrictions'}>
<dees-input-quantityselector
.label=${'Number of Licenses'}
.description=${'Select how many licenses you need'}
.required=${true}
.value=${1}
></dees-input-quantityselector>
<dees-input-quantityselector
.label=${'Fixed Quantity'}
.description=${'This quantity cannot be changed'}
.disabled=${true}
.value=${5}
></dees-input-quantityselector>
</dees-panel>
<dees-panel .title=${'Order Form'} .subtitle=${'Complete order form with quantity selection'}>
<dees-form>
<dees-input-text .label=${'Customer Name'} .required=${true}></dees-input-text>
<dees-input-dropdown
.label=${'Product'}
.options=${['Basic Plan', 'Pro Plan', 'Enterprise Plan']}
.required=${true}
></dees-input-dropdown>
<dees-input-quantityselector
.label=${'Quantity'}
.description=${'Number of licenses'}
.value=${1}
></dees-input-quantityselector>
<dees-input-text
.label=${'Special Instructions'}
.inputType=${'textarea'}
></dees-input-text>
</dees-form>
</dees-panel>
</div>
</dees-demowrapper>
`;

View File

@ -1,5 +1,7 @@
import { customElement, property, html, type TemplateResult, DeesElement, type CSSResult, } from '@design.estate/dees-element';
import { customElement, property, html, type TemplateResult, css, cssManager } from '@design.estate/dees-element';
import * as domtools from '@design.estate/dees-domtools';
import { DeesInputBase } from './dees-input-base.js';
import { demoFunc } from './dees-input-quantityselector.demo.js';
declare global {
interface HTMLElementTagNameMap {
@ -8,67 +10,50 @@ declare global {
}
@customElement('dees-input-quantityselector')
export class DeesInputQuantitySelector extends DeesElement {
public static demo = () => html`<dees-input-quantityselector></dees-input-quantityselector>`;
export class DeesInputQuantitySelector extends DeesInputBase<DeesInputQuantitySelector> {
public static demo = demoFunc;
// INSTANCE
public changeSubject = new domtools.plugins.smartrx.rxjs.Subject();
@property()
public label: string = 'Label';
@property({
type: String,
reflect: true,
})
public key: string;
@property({
type: Number
})
public value: number = 1;
@property({
type: Boolean,
})
public required: boolean = false;
@property({
type: Boolean
})
public disabled: boolean = false;
constructor() {
super();
}
public render(): TemplateResult {
return html`
${domtools.elementBasic.styles}
<style>
public static styles = [
...DeesInputBase.baseStyles,
cssManager.defaultStyles,
css`
:host {
display: block;
width: 110px;
width: auto;
user-select: none;
}
.maincontainer {
.quantity-container {
transition: all 0.1s;
font-size: 14px;
display: grid;
grid-template-columns: 33% 34% 33%;
text-align: center;
background:none;
background: ${cssManager.bdTheme('#fafafa', '#222222')};
line-height: 40px;
padding: 0px;
min-width: 100px;
color: ${this.goBright ? '#666' : '#CCC'};
border: ${this.goBright ? '1px solid #333' : '1px solid #CCC'};
min-width: 110px;
color: ${cssManager.bdTheme('#666', '#CCC')};
border: 1px solid ${cssManager.bdTheme('#CCC', '#444')};
border-radius: 4px;
}
.mainContainer:hover {
color: ${this.goBright ? '#333' : '#fff'};
border: ${this.goBright ? '1px solid #333' : '1px solid #fff'};
.quantity-container.disabled {
opacity: 0.5;
pointer-events: none;
}
.quantity-container:hover {
color: ${cssManager.bdTheme('#333', '#fff')};
border-color: ${cssManager.bdTheme('#999', '#666')};
}
.minus {
@ -91,28 +76,41 @@ export class DeesInputQuantitySelector extends DeesElement {
text-align: center;
}
`,
];
</style>
<div class="maincontainer">
public render(): TemplateResult {
return html`
<div class="input-wrapper">
<dees-label .label=${this.label}></dees-label>
<div class="quantity-container ${this.disabled ? 'disabled' : ''}">
<div class="selector minus" @click="${() => {this.decrease();}}">-</div>
<div class="quantity">${this.value}</div>
<div class="selector plus" @click="${() => {this.increase();}}">+</div>
</div>
</div>
`;
}
public increase() {
if (!this.disabled) {
this.value++;
this.changeSubject.next(this);
}
}
public decrease() {
if (this.value > 0) {
if (!this.disabled && this.value > 0) {
this.value--;
} else {
// nothing to do here
}
this.changeSubject.next(this);
}
}
public getValue(): number {
return this.value;
}
public setValue(value: number): void {
this.value = value;
}
}

View File

@ -0,0 +1,277 @@
import { html, css } from '@design.estate/dees-element';
import '@design.estate/dees-wcctools/demotools';
import type { DeesInputRadio } from './dees-input-radio.js';
export const demoFunc = () => html`
<dees-demowrapper .runAfterRender=${async (elementArg: HTMLElement) => {
// Implement radio group behavior
const radioGroups = new Map<string, DeesInputRadio[]>();
// Group radios by their container
const radioContainers = elementArg.querySelectorAll('.radio-group');
radioContainers.forEach((container) => {
const radios = Array.from(container.querySelectorAll('dees-input-radio')) as DeesInputRadio[];
const groupName = container.getAttribute('data-group') || 'default';
radioGroups.set(groupName, radios);
// Add click handlers for radio group behavior
radios.forEach((radio) => {
radio.addEventListener('click', () => {
if (!radio.disabled && !radio.value) {
// Uncheck all other radios in the group
radios.forEach((r) => {
if (r !== radio) {
r.value = false;
}
});
radio.value = true;
}
});
});
});
}}>
<style>
${css`
.demo-container {
display: flex;
flex-direction: column;
gap: 24px;
padding: 24px;
max-width: 1200px;
margin: 0 auto;
}
.demo-section {
background: #f8f9fa;
border-radius: 8px;
padding: 24px;
}
@media (prefers-color-scheme: dark) {
.demo-section {
background: #1a1a1a;
}
}
.demo-section h3 {
margin-top: 0;
margin-bottom: 16px;
color: #0069f2;
font-size: 18px;
}
.demo-section p {
margin-top: 0;
margin-bottom: 16px;
color: #666;
font-size: 14px;
}
@media (prefers-color-scheme: dark) {
.demo-section p {
color: #999;
}
}
.horizontal-group {
display: flex;
align-items: center;
gap: 16px;
flex-wrap: wrap;
}
.radio-group {
display: flex;
flex-direction: column;
gap: 8px;
padding: 16px;
background: #f0f0f0;
border-radius: 4px;
margin-bottom: 16px;
}
@media (prefers-color-scheme: dark) {
.radio-group {
background: #0a0a0a;
}
}
.radio-group-title {
font-weight: 500;
margin-bottom: 8px;
color: #333;
}
@media (prefers-color-scheme: dark) {
.radio-group-title {
color: #ccc;
}
}
.grid-layout {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 24px;
}
`}
</style>
<div class="demo-container">
<div class="demo-section">
<h3>Basic Radio Groups</h3>
<p>Radio buttons for single-choice selections</p>
<div class="radio-group" data-group="plan">
<div class="radio-group-title">Select your subscription plan:</div>
<dees-input-radio
.label=${'Basic Plan - $9/month'}
.value=${true}
.key=${'plan-basic'}
></dees-input-radio>
<dees-input-radio
.label=${'Pro Plan - $29/month'}
.key=${'plan-pro'}
></dees-input-radio>
<dees-input-radio
.label=${'Enterprise Plan - $99/month'}
.key=${'plan-enterprise'}
></dees-input-radio>
</div>
<div class="radio-group" data-group="priority">
<div class="radio-group-title">Task Priority:</div>
<dees-input-radio
.label=${'High Priority'}
.key=${'priority-high'}
></dees-input-radio>
<dees-input-radio
.label=${'Medium Priority'}
.value=${true}
.key=${'priority-medium'}
></dees-input-radio>
<dees-input-radio
.label=${'Low Priority'}
.key=${'priority-low'}
></dees-input-radio>
</div>
</div>
<div class="demo-section">
<h3>Horizontal Layout</h3>
<p>Radio buttons arranged horizontally for yes/no questions</p>
<div class="radio-group" data-group="agreement" style="flex-direction: row;">
<div style="margin-right: 16px;">Do you agree?</div>
<dees-input-radio
.label=${'Yes'}
.layoutMode=${'horizontal'}
.value=${true}
.key=${'agree-yes'}
></dees-input-radio>
<dees-input-radio
.label=${'No'}
.layoutMode=${'horizontal'}
.key=${'agree-no'}
></dees-input-radio>
<dees-input-radio
.label=${'Maybe'}
.layoutMode=${'horizontal'}
.key=${'agree-maybe'}
></dees-input-radio>
</div>
<div class="radio-group" data-group="experience" style="flex-direction: row;">
<div style="margin-right: 16px;">Experience Level:</div>
<dees-input-radio
.label=${'Beginner'}
.layoutMode=${'horizontal'}
.key=${'exp-beginner'}
></dees-input-radio>
<dees-input-radio
.label=${'Intermediate'}
.layoutMode=${'horizontal'}
.value=${true}
.key=${'exp-intermediate'}
></dees-input-radio>
<dees-input-radio
.label=${'Expert'}
.layoutMode=${'horizontal'}
.key=${'exp-expert'}
></dees-input-radio>
</div>
</div>
<div class="demo-section">
<h3>Survey Example</h3>
<p>Multiple radio groups in a survey format</p>
<div class="grid-layout">
<div class="radio-group" data-group="satisfaction">
<div class="radio-group-title">How satisfied are you?</div>
<dees-input-radio .label=${'Very Satisfied'} .key=${'sat-very'}></dees-input-radio>
<dees-input-radio .label=${'Satisfied'} .value=${true} .key=${'sat-normal'}></dees-input-radio>
<dees-input-radio .label=${'Neutral'} .key=${'sat-neutral'}></dees-input-radio>
<dees-input-radio .label=${'Dissatisfied'} .key=${'sat-dis'}></dees-input-radio>
<dees-input-radio .label=${'Very Dissatisfied'} .key=${'sat-verydis'}></dees-input-radio>
</div>
<div class="radio-group" data-group="recommend">
<div class="radio-group-title">Would you recommend us?</div>
<dees-input-radio .label=${'Definitely'} .key=${'rec-def'}></dees-input-radio>
<dees-input-radio .label=${'Probably'} .value=${true} .key=${'rec-prob'}></dees-input-radio>
<dees-input-radio .label=${'Not Sure'} .key=${'rec-unsure'}></dees-input-radio>
<dees-input-radio .label=${'Probably Not'} .key=${'rec-probnot'}></dees-input-radio>
<dees-input-radio .label=${'Definitely Not'} .key=${'rec-defnot'}></dees-input-radio>
</div>
</div>
</div>
<div class="demo-section">
<h3>States</h3>
<p>Different radio button states</p>
<div class="radio-group" data-group="states">
<dees-input-radio
.label=${'Normal Radio'}
.key=${'state-normal'}
></dees-input-radio>
<dees-input-radio
.label=${'Selected Radio'}
.value=${true}
.key=${'state-selected'}
></dees-input-radio>
<dees-input-radio
.label=${'Disabled Unchecked'}
.disabled=${true}
.key=${'state-disabled1'}
></dees-input-radio>
<dees-input-radio
.label=${'Disabled Checked'}
.disabled=${true}
.value=${true}
.key=${'state-disabled2'}
></dees-input-radio>
</div>
</div>
<div class="demo-section">
<h3>Settings Example</h3>
<p>Common radio button patterns in settings</p>
<div class="radio-group" data-group="theme">
<div class="radio-group-title">Theme Preference:</div>
<dees-input-radio .label=${'Light Theme'} .key=${'theme-light'}></dees-input-radio>
<dees-input-radio .label=${'Dark Theme'} .value=${true} .key=${'theme-dark'}></dees-input-radio>
<dees-input-radio .label=${'System Default'} .key=${'theme-system'}></dees-input-radio>
</div>
<div class="radio-group" data-group="notifications">
<div class="radio-group-title">Notification Frequency:</div>
<dees-input-radio .label=${'All Notifications'} .key=${'notif-all'}></dees-input-radio>
<dees-input-radio .label=${'Important Only'} .value=${true} .key=${'notif-important'}></dees-input-radio>
<dees-input-radio .label=${'None'} .key=${'notif-none'}></dees-input-radio>
</div>
</div>
</div>
</dees-demowrapper>
`;

View File

@ -1,5 +1,6 @@
import {customElement, DeesElement, type TemplateResult, property, html, type CSSResult,} from '@design.estate/dees-element';
import * as domtools from '@design.estate/dees-domtools';
import {customElement, type TemplateResult, property, html, css, cssManager} from '@design.estate/dees-element';
import { DeesInputBase } from './dees-input-base.js';
import { demoFunc } from './dees-input-radio.demo.js';
declare global {
interface HTMLElementTagNameMap {
@ -8,55 +9,34 @@ declare global {
}
@customElement('dees-input-radio')
export class DeesInputRadio extends DeesElement {
public static demo = () => html`<dees-input-radio></dees-input-radio>`;
export class DeesInputRadio extends DeesInputBase<DeesInputRadio> {
public static demo = demoFunc;
// INSTANCE
public changeSubject = new domtools.plugins.smartrx.rxjs.Subject();
@property({
type: String,
reflect: true,
})
public key: string;
@property()
public label: string = 'Label';
@property()
public value: boolean = false;
@property({
type: Boolean,
})
public required: boolean = false;
@property({
type: Boolean
})
public disabled: boolean = false;
constructor() {
super();
this.labelPosition = 'right'; // Radio buttons default to label on the right
}
public render(): TemplateResult {
return html `
<style>
public static styles = [
...DeesInputBase.baseStyles,
cssManager.defaultStyles,
css`
* {
box-sizing: border-box;
}
:host {
display: block;
position: relative;
margin: 20px 0px;
}
.maincontainer {
transition: all 0.3s;
display: grid;
grid-template-columns: 25px auto;
padding: 5px 0px;
color: #ccc;
}
@ -65,14 +45,6 @@ export class DeesInputRadio extends DeesElement {
color: #fff;
}
.label {
margin-left: 15px;
line-height: 25px;
font-size: 14px;
font-weight: normal;
}
input:focus {
outline: none;
border-bottom: 1px solid #e4002b;
@ -106,12 +78,18 @@ export class DeesInputRadio extends DeesElement {
height: 10px;
border-radius: 10px;
}
</style>
`,
];
public render(): TemplateResult {
return html`
<div class="input-wrapper">
<div class="maincontainer" @click="${this.toggleSelected}">
<div class="checkbox ${this.value ? 'selected' : ''}">
${this.value ? html`<div class="innercircle"></div>`: html``}
</div>
<div class="label">${this.label}</div>
</div>
<dees-label .label=${this.label}></dees-label>
</div>
`;
}
@ -124,4 +102,12 @@ export class DeesInputRadio extends DeesElement {
}));
this.changeSubject.next(this);
}
public getValue(): boolean {
return this.value;
}
public setValue(value: boolean): void {
this.value = value;
}
}

View File

@ -0,0 +1,199 @@
import { html, css } from '@design.estate/dees-element';
import '@design.estate/dees-wcctools/demotools';
export const demoFunc = () => html`
<dees-demowrapper>
<style>
${css`
.demo-container {
display: flex;
flex-direction: column;
gap: 24px;
padding: 24px;
max-width: 1200px;
margin: 0 auto;
}
.demo-section {
background: #f8f9fa;
border-radius: 8px;
padding: 24px;
}
@media (prefers-color-scheme: dark) {
.demo-section {
background: #1a1a1a;
}
}
.demo-section h3 {
margin-top: 0;
margin-bottom: 16px;
color: #0069f2;
font-size: 18px;
}
.demo-section p {
margin-top: 0;
margin-bottom: 16px;
color: #666;
font-size: 14px;
}
@media (prefers-color-scheme: dark) {
.demo-section p {
color: #999;
}
}
.horizontal-group {
display: flex;
align-items: center;
gap: 16px;
flex-wrap: wrap;
}
.grid-layout {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
}
@media (max-width: 768px) {
.grid-layout {
grid-template-columns: 1fr;
}
}
`}
</style>
<div class="demo-container">
<div class="demo-section">
<h3>Basic Text Inputs</h3>
<p>Standard text inputs with labels and descriptions</p>
<dees-input-text
.label=${'Username'}
.value=${'johndoe'}
.key=${'username'}
></dees-input-text>
<dees-input-text
.label=${'Email Address'}
.value=${'john@example.com'}
.description=${'We will never share your email with anyone'}
.key=${'email'}
></dees-input-text>
<dees-input-text
.label=${'Password'}
.isPasswordBool=${true}
.value=${'secret123'}
.key=${'password'}
></dees-input-text>
</div>
<div class="demo-section">
<h3>Horizontal Layout</h3>
<p>Multiple inputs arranged horizontally for compact forms</p>
<div class="horizontal-group">
<dees-input-text
.label=${'First Name'}
.value=${'John'}
.layoutMode=${'horizontal'}
.key=${'firstName'}
></dees-input-text>
<dees-input-text
.label=${'Last Name'}
.value=${'Doe'}
.layoutMode=${'horizontal'}
.key=${'lastName'}
></dees-input-text>
<dees-input-text
.label=${'Age'}
.value=${'28'}
.layoutMode=${'horizontal'}
.key=${'age'}
></dees-input-text>
</div>
</div>
<div class="demo-section">
<h3>Label Positions</h3>
<p>Different label positioning options for various layouts</p>
<dees-input-text
.label=${'Label on Top (Default)'}
.value=${'Standard layout'}
.labelPosition=${'top'}
></dees-input-text>
<dees-input-text
.label=${'Label on Left'}
.value=${'Inline label'}
.labelPosition=${'left'}
></dees-input-text>
<div class="grid-layout">
<dees-input-text
.label=${'City'}
.value=${'New York'}
.labelPosition=${'left'}
></dees-input-text>
<dees-input-text
.label=${'ZIP Code'}
.value=${'10001'}
.labelPosition=${'left'}
></dees-input-text>
</div>
</div>
<div class="demo-section">
<h3>Validation & States</h3>
<p>Different validation states and input configurations</p>
<dees-input-text
.label=${'Required Field'}
.required=${true}
.key=${'requiredField'}
></dees-input-text>
<dees-input-text
.label=${'Disabled Field'}
.value=${'Cannot edit this'}
.disabled=${true}
></dees-input-text>
<dees-input-text
.label=${'Field with Error'}
.value=${'invalid@'}
.validationText=${'Please enter a valid email address'}
.validationState=${'invalid'}
></dees-input-text>
</div>
<div class="demo-section">
<h3>Advanced Features</h3>
<p>Password visibility toggle and other advanced features</p>
<dees-input-text
.label=${'Password with Toggle'}
.isPasswordBool=${true}
.value=${'mySecurePassword123'}
.description=${'Click the eye icon to show/hide password'}
></dees-input-text>
<dees-input-text
.label=${'API Key'}
.isPasswordBool=${true}
.value=${'sk-1234567890abcdef'}
.description=${'Keep this key secure and never share it'}
></dees-input-text>
</div>
</div>
</dees-demowrapper>
`;

View File

@ -1,16 +1,15 @@
import * as colors from './00colors.js';
import { DeesInputBase } from './dees-input-base.js';
import { demoFunc } from './dees-input-text.demo.js';
import {
customElement,
DeesElement,
type TemplateResult,
property,
html,
cssManager,
css,
type CSSResult,
} from '@design.estate/dees-element';
import * as domtools from '@design.estate/dees-domtools';
declare global {
interface HTMLElementTagNameMap {
@ -19,47 +18,16 @@ declare global {
}
@customElement('dees-input-text')
export class DeesInputText extends DeesElement {
public static demo = () => html`
<dees-input-text .label=${'this is a label'} .value=${'test'}></dees-input-text>
<dees-input-text .isPasswordBool=${true}></dees-input-text>
`;
export class DeesInputText extends DeesInputBase {
public static demo = demoFunc;
// INSTANCE
public changeSubject = new domtools.plugins.smartrx.rxjs.Subject<DeesInputText>();
@property({
type: String,
})
public label: string;
@property({
type: String,
})
public description: string;
@property({
type: String,
reflect: true,
})
public key: string;
@property({
type: String,
reflect: true,
})
public value: string = '';
@property({
type: Boolean,
})
public required: boolean = false;
@property({
type: Boolean,
})
public disabled: boolean = false;
@property({
type: Boolean,
reflect: true,
@ -87,6 +55,7 @@ export class DeesInputText extends DeesElement {
validationFunction: (value: string) => boolean;
public static styles = [
...DeesInputBase.baseStyles,
cssManager.defaultStyles,
css`
* {
@ -95,9 +64,6 @@ export class DeesInputText extends DeesElement {
:host {
position: relative;
display: grid;
margin: 8px 0px;
margin-bottom: 24px;
z-index: auto;
}
@ -193,8 +159,9 @@ export class DeesInputText extends DeesElement {
}
`}
</style>
<div class="maincontainer">
<div class="input-wrapper">
<dees-label .label=${this.label} .description=${this.description}></dees-label>
<div class="maincontainer">
<input
type="${this.isPasswordBool && !this.showPasswordBool ? 'password' : 'text'}"
.value=${this.value}
@ -212,14 +179,12 @@ export class DeesInputText extends DeesElement {
`
: html``}
</div>
</div>
`;
}
firstUpdated() {
const input = this.shadowRoot.querySelector('input');
input.addEventListener('input', (eventArg: InputEvent) => {
});
// Input event handling is already done in updateValue method
}
public async updateValue(eventArg: Event) {
@ -228,16 +193,15 @@ export class DeesInputText extends DeesElement {
this.changeSubject.next(this);
}
public async freeze() {
this.disabled = true;
public getValue(): string {
return this.value;
}
public async unfreeze() {
this.disabled = false;
public setValue(value: string): void {
this.value = value;
}
public async togglePasswordView() {
const domtools = await this.domtoolsPromise;
this.showPasswordBool = !this.showPasswordBool;
console.log(`this.showPasswordBool is: ${this.showPasswordBool}`);
}

View File

@ -1,15 +1,118 @@
import { html } from '@design.estate/dees-element';
import { html, css } from '@design.estate/dees-element';
export const demoFunc = () => html`
<dees-demowrapper>
<style>
.demoContainer {
max-width: 600px;
margin: auto;
padding: 40px;
background: #000;
${css`
.demo-container {
display: flex;
flex-direction: column;
gap: 24px;
padding: 24px;
max-width: 1200px;
margin: 0 auto;
}
.horizontal-group {
display: flex;
gap: 24px;
align-items: flex-start;
}
.info-box {
margin-top: 16px;
padding: 12px;
background: #e3f2fd;
border-radius: 4px;
font-size: 14px;
color: #1976d2;
}
@media (prefers-color-scheme: dark) {
.info-box {
background: #1e3a5f;
color: #90caf9;
}
}
`}
</style>
<div class="demoContainer">
<dees-input-typelist></dees-input-typelist>
<div class="demo-container">
<dees-panel .title=${'Basic Type List'} .subtitle=${'Add and remove items from a list'}>
<dees-input-typelist
.label=${'Tags'}
.description=${'Add tags by typing and pressing Enter'}
.value=${['javascript', 'typescript', 'web-components']}
></dees-input-typelist>
<dees-input-typelist
.label=${'Team Members'}
.description=${'Add email addresses of team members'}
.value=${['alice@example.com', 'bob@example.com']}
></dees-input-typelist>
</dees-panel>
<dees-panel .title=${'Skills & Keywords'} .subtitle=${'Manage lists of skills and keywords'}>
<dees-input-typelist
.label=${'Your Skills'}
.description=${'List your professional skills'}
.value=${['HTML', 'CSS', 'JavaScript', 'Node.js', 'React']}
></dees-input-typelist>
<div class="horizontal-group">
<dees-input-typelist
.label=${'Categories'}
.layoutMode=${'horizontal'}
.value=${['Technology', 'Design', 'Business']}
></dees-input-typelist>
<dees-input-typelist
.label=${'Keywords'}
.layoutMode=${'horizontal'}
.value=${['innovation', 'startup', 'growth']}
></dees-input-typelist>
</div>
</dees-panel>
<dees-panel .title=${'Required & Disabled States'} .subtitle=${'Different input states for validation'}>
<dees-input-typelist
.label=${'Project Dependencies'}
.description=${'List all required npm packages'}
.required=${true}
.value=${['@design.estate/dees-element', '@design.estate/dees-domtools']}
></dees-input-typelist>
<dees-input-typelist
.label=${'System Tags'}
.description=${'These tags are managed by the system'}
.disabled=${true}
.value=${['system', 'protected', 'readonly']}
></dees-input-typelist>
</dees-panel>
<dees-panel .title=${'Article Publishing Form'} .subtitle=${'Complete form with tag management'}>
<dees-form>
<dees-input-text .label=${'Article Title'} .required=${true}></dees-input-text>
<dees-input-text
.label=${'Summary'}
.inputType=${'textarea'}
.description=${'Brief description of the article'}
></dees-input-text>
<dees-input-typelist
.label=${'Tags'}
.description=${'Add relevant tags for better discoverability'}
.value=${['tutorial', 'web-development']}
></dees-input-typelist>
<dees-input-typelist
.label=${'Co-Authors'}
.description=${'Add email addresses of co-authors'}
></dees-input-typelist>
</dees-form>
<div class="info-box">
<strong>Tip:</strong> Type a value and press Enter to add it to the list. Click on any item to remove it.
</div>
</dees-panel>
</div>
</dees-demowrapper>
`;

View File

@ -1,44 +1,37 @@
import {
customElement,
DeesElement,
type TemplateResult,
state,
html,
domtools,
property,
css,
cssManager,
} from '@design.estate/dees-element';
import * as domtools from '@design.estate/dees-domtools';
import { DeesInputBase } from './dees-input-base.js';
const { demoFunc } = await import('./dees-input-typelist.demo.js');
@customElement('dees-input-typelist')
export class DeesInputTypelist extends DeesElement {
export class DeesInputTypelist extends DeesInputBase<DeesInputTypelist> {
public static demo = demoFunc;
// INSTANCE
@property({
type: String,
})
public label: string;
@property({ type: Array })
public value: string[] = [];
@state()
private inputValue: string = '';
constructor() {
super();
}
public static styles = [
...DeesInputBase.baseStyles,
cssManager.defaultStyles,
css`
:host {
display: block;
color: ${cssManager.bdTheme('#333', '#fff')};
margin: 8px 0px 24px 0px;
}
.label {
font-size: 14px;
margin-bottom: 8px;
}
.mainbox {
border-radius: 3px;
@ -79,20 +72,89 @@ export class DeesInputTypelist extends DeesElement {
input:focus {
height: 32px;
}
.tag {
display: inline-block;
background: ${cssManager.bdTheme('#e0e0e0', '#444')};
color: ${cssManager.bdTheme('#333', '#fff')};
padding: 4px 8px;
border-radius: 3px;
margin: 2px;
font-size: 12px;
}
.tag .remove {
margin-left: 6px;
cursor: pointer;
opacity: 0.6;
}
.tag .remove:hover {
opacity: 1;
}
`,
];
public render(): TemplateResult {
return html`
<div class="label">${this.label}</div>
<div class="input-wrapper">
<dees-label .label=${this.label} .description=${this.description}></dees-label>
<div class="mainbox">
<div class="tags" @click=${() => {
this.shadowRoot.querySelector('input').focus();
}}>
<div class="notags">No tags yet</div>
${this.value.length === 0
? html`<div class="notags">No tags yet</div>`
: this.value.map(
(tag) => html`
<span class="tag">
${tag}
<span class="remove" @click=${(e: Event) => {
e.stopPropagation();
this.removeTag(tag);
}}>×</span>
</span>
`
)}
</div>
<input
type="text"
placeholder="Type, press Enter to add it..."
.value=${this.inputValue}
@input=${(e: InputEvent) => {
this.inputValue = (e.target as HTMLInputElement).value;
}}
@keydown=${(e: KeyboardEvent) => {
if (e.key === 'Enter' && this.inputValue.trim()) {
e.preventDefault();
this.addTag(this.inputValue.trim());
}
}}
.disabled=${this.disabled}
/>
</div>
<input type="text" placeholder="Type, press Enter to add it..." />
</div>
`;
}
private addTag(tag: string) {
if (!this.value.includes(tag)) {
this.value = [...this.value, tag];
this.inputValue = '';
this.changeSubject.next(this);
}
}
private removeTag(tag: string) {
this.value = this.value.filter((t) => t !== tag);
this.changeSubject.next(this);
}
public getValue(): string[] {
return this.value;
}
public setValue(value: string[]): void {
this.value = value;
}
}

View File

@ -0,0 +1,81 @@
import { html, css, cssManager } from '@design.estate/dees-element';
export const demoFunc = () => html`
<style>
${css`
.demo-background {
padding: 24px;
background: ${cssManager.bdTheme('#f0f0f0', '#0a0a0a')};
min-height: 100vh;
}
.demo-container {
max-width: 1200px;
margin: 0 auto;
display: flex;
flex-direction: column;
gap: 24px;
}
.grid-layout {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 24px;
}
@media (max-width: 768px) {
.grid-layout {
grid-template-columns: 1fr;
}
}
`}
</style>
<div class="demo-background">
<div class="demo-container">
<dees-panel .title=${'Panel Component'}>
<p>The panel component automatically follows the theme and provides consistent styling for grouped content.</p>
<p>It's perfect for creating sections in your application with proper spacing and borders.</p>
</dees-panel>
<dees-panel .title=${'Panel with Subtitle'} .subtitle=${'Additional context information'}>
<p>Panels can have both a title and subtitle to provide more context.</p>
<p>The subtitle appears in a smaller, muted text below the title.</p>
</dees-panel>
<div class="grid-layout">
<dees-panel .title=${'Feature 1'}>
<p>Grid layouts work great with panels for creating dashboards and feature sections.</p>
<dees-button>Action</dees-button>
</dees-panel>
<dees-panel .title=${'Feature 2'}>
<p>Each panel maintains consistent spacing and styling.</p>
<dees-button>Another Action</dees-button>
</dees-panel>
</div>
<dees-panel .title=${'Complex Content'}>
<h4>Nested Elements</h4>
<p>Panels can contain any type of content:</p>
<ul>
<li>Text and paragraphs</li>
<li>Lists and tables</li>
<li>Form inputs</li>
<li>Other components</li>
</ul>
<dees-input-text .label=${'Example Input'} .description=${'Input inside a panel'}></dees-input-text>
<div style="margin-top: 16px;">
<dees-button>Submit</dees-button>
</div>
</dees-panel>
<dees-panel>
<p>Panels work great even without a title for simple content grouping.</p>
<p>They provide visual separation and consistent padding.</p>
</dees-panel>
</div>
</div>
`;

View File

@ -0,0 +1,77 @@
import {
customElement,
DeesElement,
html,
css,
cssManager,
property,
type TemplateResult,
} from '@design.estate/dees-element';
import { demoFunc } from './dees-panel.demo.js';
declare global {
interface HTMLElementTagNameMap {
'dees-panel': DeesPanel;
}
}
@customElement('dees-panel')
export class DeesPanel extends DeesElement {
public static demo = demoFunc;
@property({ type: String })
public title: string = '';
@property({ type: String })
public subtitle: string = '';
public static styles = [
cssManager.defaultStyles,
css`
:host {
display: block;
background: ${cssManager.bdTheme('#ffffff', '#1a1a1a')};
border-radius: 8px;
padding: 24px;
box-shadow: 0 2px 4px ${cssManager.bdTheme('rgba(0,0,0,0.1)', 'rgba(0,0,0,0.3)')};
border: 1px solid ${cssManager.bdTheme('rgba(0,0,0,0.1)', 'rgba(255,255,255,0.1)')};
}
.title {
margin: 0 0 16px 0;
font-size: 18px;
font-weight: 500;
color: ${cssManager.bdTheme('#0069f2', '#0099ff')};
}
.subtitle {
margin: -12px 0 16px 0;
font-size: 14px;
color: ${cssManager.bdTheme('#666', '#999')};
}
.content {
color: ${cssManager.bdTheme('#333', '#ccc')};
}
/* Remove margins from first and last children */
.content ::slotted(*:first-child) {
margin-top: 0;
}
.content ::slotted(*:last-child) {
margin-bottom: 0;
}
`,
];
public render(): TemplateResult {
return html`
${this.title ? html`<h3 class="title">${this.title}</h3>` : ''}
${this.subtitle ? html`<p class="subtitle">${this.subtitle}</p>` : ''}
<div class="content">
<slot></slot>
</div>
`;
}
}

View File

@ -1,21 +1,293 @@
import { html } from '@design.estate/dees-element';
import { html, DeesElement, customElement, css, cssManager } from '@design.estate/dees-element';
import type { IView } from './dees-simple-appdash.js';
import './dees-form.js';
import './dees-input-text.js';
import './dees-input-checkbox.js';
import './dees-input-dropdown.js';
import './dees-input-radio.js';
import './dees-form-submit.js';
import './dees-statsgrid.js';
import type { IStatsTile } from './dees-statsgrid.js';
// Create demo view components
@customElement('demo-view-dashboard')
class DemoViewDashboard extends DeesElement {
static styles = [
cssManager.defaultStyles,
css`
:host {
display: block;
padding: 40px;
}
h1 {
margin: 0 0 20px 0;
color: ${cssManager.bdTheme('#000', '#fff')};
}
dees-statsgrid {
margin-top: 20px;
}
`
];
private statsTiles: IStatsTile[] = [
{
id: 'users',
title: 'Active Users',
value: 1234,
type: 'number',
icon: 'faUsers',
description: '+15% from last week',
color: '#22c55e'
},
{
id: 'pageviews',
title: 'Page Views',
value: 56700,
type: 'number',
icon: 'faEye',
description: '56.7k total views',
color: '#3b82f6'
},
{
id: 'uptime',
title: 'System Uptime',
value: 89,
unit: '%',
type: 'gauge',
icon: 'faServer',
description: 'Last 30 days',
color: '#10b981',
gaugeOptions: {
min: 0,
max: 100,
thresholds: [
{ value: 80, color: '#ef4444' },
{ value: 90, color: '#f59e0b' },
{ value: 100, color: '#10b981' }
]
}
},
{
id: 'response',
title: 'Avg Response Time',
value: 3.2,
unit: 's',
type: 'number',
icon: 'faClock',
description: '-0.5s improvement',
color: '#f59e0b'
},
{
id: 'revenue',
title: 'Monthly Revenue',
value: 48520,
unit: '$',
type: 'trend',
icon: 'faDollarSign',
description: '+8.2% growth',
color: '#22c55e',
trendData: [35000, 38000, 37500, 41000, 39800, 42000, 44100, 43200, 45600, 47100, 46800, 48520]
},
{
id: 'traffic',
title: 'Traffic Trend',
value: 1680,
type: 'trend',
icon: 'faChartLine',
description: 'Last 7 days',
color: '#3b82f6',
trendData: [1200, 1350, 1100, 1450, 1600, 1550, 1680]
}
];
render() {
return html`
<h1>Dashboard</h1>
<p>Welcome to your application dashboard. Here's an overview of your metrics:</p>
<dees-statsgrid
.tiles=${this.statsTiles}
@tile-action=${(e: CustomEvent) => {
console.log('Tile action:', e.detail);
}}
></dees-statsgrid>
`;
}
}
@customElement('demo-view-analytics')
class DemoViewAnalytics extends DeesElement {
static styles = [
cssManager.defaultStyles,
css`
:host {
display: block;
padding: 40px;
}
h1 {
margin: 0 0 20px 0;
color: ${cssManager.bdTheme('#000', '#fff')};
}
`
];
render() {
return html`
<h1>Analytics</h1>
<p>This is the analytics view. You can add charts and metrics here.</p>
`;
}
}
@customElement('demo-view-settings')
class DemoViewSettings extends DeesElement {
static styles = [
cssManager.defaultStyles,
css`
:host {
display: block;
padding: 40px;
}
h1 {
margin: 0 0 20px 0;
color: ${cssManager.bdTheme('#000', '#fff')};
}
.settings-section {
margin-top: 30px;
}
.settings-section h2 {
font-size: 18px;
margin: 0 0 15px 0;
color: ${cssManager.bdTheme('#333', '#ccc')};
}
.horizontal-form-section {
background: ${cssManager.bdTheme('#f5f5f5', '#1a1a1a')};
padding: 20px;
border-radius: 8px;
margin: 15px 0;
}
`
];
render() {
return html`
<h1>Settings</h1>
<p>Configure your application settings below:</p>
<div class="settings-section">
<h2>General Settings</h2>
<dees-form>
<dees-input-text key="appName" label="Application Name" value="My App"></dees-input-text>
<dees-input-text key="apiEndpoint" label="API Endpoint" value="https://api.example.com"></dees-input-text>
<dees-input-dropdown
key="environment"
label="Environment"
.options=${[
{ option: 'Development', key: 'dev' },
{ option: 'Staging', key: 'staging' },
{ option: 'Production', key: 'prod' }
]}
.selectedOption=${{ option: 'Production', key: 'prod' }}
></dees-input-dropdown>
<dees-input-checkbox key="enableNotifications" label="Enable Notifications" value="true"></dees-input-checkbox>
<dees-input-checkbox key="enableAnalytics" label="Enable Analytics" value="false"></dees-input-checkbox>
<dees-form-submit>Save General Settings</dees-form-submit>
</dees-form>
</div>
<div class="settings-section">
<h2>Display Preferences</h2>
<div class="horizontal-form-section">
<p style="margin-top: 0; margin-bottom: 16px;">Quick display settings using horizontal layout:</p>
<dees-form horizontal-layout>
<dees-input-dropdown
key="theme"
label="Theme"
.enableSearch=${false}
.options=${[
{ option: 'Light', key: 'light' },
{ option: 'Dark', key: 'dark' },
{ option: 'Auto', key: 'auto' }
]}
.selectedOption=${{ option: 'Dark', key: 'dark' }}
></dees-input-dropdown>
<dees-input-dropdown
key="language"
label="Language"
.enableSearch=${false}
.options=${[
{ option: 'English', key: 'en' },
{ option: 'German', key: 'de' },
{ option: 'Spanish', key: 'es' },
{ option: 'French', key: 'fr' }
]}
.selectedOption=${{ option: 'English', key: 'en' }}
></dees-input-dropdown>
<dees-input-checkbox key="compactMode" label="Compact Mode"></dees-input-checkbox>
</dees-form>
</div>
</div>
<div class="settings-section">
<h2>Notification Settings</h2>
<dees-form>
<div style="margin-bottom: 16px;">
<div style="font-weight: 500; margin-bottom: 8px;">Email Frequency:</div>
<dees-input-radio label="Real-time" value="true" key="email-realtime"></dees-input-radio>
<dees-input-radio label="Daily Digest" key="email-daily"></dees-input-radio>
<dees-input-radio label="Weekly Summary" key="email-weekly"></dees-input-radio>
<dees-input-radio label="Never" key="email-never"></dees-input-radio>
</div>
<dees-input-checkbox key="pushNotifications" label="Enable Push Notifications" value="true"></dees-input-checkbox>
<dees-input-checkbox key="soundAlerts" label="Play Sound for Alerts" value="true"></dees-input-checkbox>
<dees-form-submit>Update Notifications</dees-form-submit>
</dees-form>
</div>
`;
}
}
export const demoFunc = () => html`
<style>
body {
margin: 0;
padding: 0;
}
.demo-container {
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
}
</style>
<div class="demo-container">
<dees-simple-appdash
name="My Application"
terminalSetupCommand="echo 'Welcome to the terminal!'"
.viewTabs=${[
{
name: 'View 1',
element: null,
name: 'Dashboard',
iconName: 'home',
element: DemoViewDashboard,
},
{
name: 'View 2',
element: null,
name: 'Analytics',
iconName: 'lineChart',
element: DemoViewAnalytics,
},
{
name: 'View 3',
element: null,
name: 'Settings',
iconName: 'settings',
element: DemoViewSettings,
}
] as IView[]}
>Hello there</dees-simple-appdash>
@logout=${() => {
console.log('Logout event triggered');
alert('Logout clicked!');
}}
@view-select=${(e: CustomEvent) => {
console.log('View selected:', e.detail.view.name);
}}
></dees-simple-appdash>
</div>
`;

View File

@ -1,5 +1,4 @@
import { demoFunc } from './dees-simple-appdash.demo.js';
import * as colors from './00colors.js';
import {
customElement,
@ -14,7 +13,8 @@ import {
state,
domtools,
} from '@design.estate/dees-element';
import { DeesTerminal } from './dees-terminal.js';
import './dees-icon.js';
import type { DeesTerminal } from './dees-terminal.js';
declare global {
interface HTMLElementTagNameMap {
@ -24,6 +24,7 @@ declare global {
export interface IView {
name: string;
iconName?: string;
element: DeesElement['constructor']['prototype'];
}
@ -34,13 +35,17 @@ export class DeesSimpleAppDash extends DeesElement {
// INSTANCE
@property()
public name = 'Dees Simple Login';
public name: string = 'Application Dashboard';
@property()
@property({ type: Array })
public viewTabs: IView[] = [];
@property()
public terminalSetupCommand: string = `pnpm install @serve.zone/cli && clear && servezone info`;
@property({ type: String })
public terminalSetupCommand: string = `echo "Terminal ready"`;
@state()
private selectedView: IView;
public static styles = [
cssManager.defaultStyles,
@ -69,54 +74,110 @@ export class DeesSimpleAppDash extends DeesElement {
top: 0px;
left: 0px;
height: calc(100% - 24px);
width: 200px;
background: ${cssManager.bdTheme('#eeeeeb', '#000')};
border-right: 1px solid ${cssManager.bdTheme('#ccc', '#ffffff20')};
font-size: 14px;
line-height: 32px;
width: 240px;
background: ${cssManager.bdTheme('#fafafa', '#000')};
border-right: 1px solid ${cssManager.bdTheme('#e0e0e0', '#202020')};
font-size: 12px;
font-family: 'Geist Sans', sans-serif;
padding: 0px 16px;
z-index: 2;
box-shadow: 0px 4px 10px 0px rgba(0, 0, 0, 0.8);
display: grid;
grid-template-rows: min-content auto min-content;
grid-template-rows: auto min-content;
overflow: hidden;
}
.appbar .viewTabs {
margin-left: -8px;
margin-right: -8px;
.sidebar-header {
padding: 16px 12px;
border-bottom: 1px solid ${cssManager.bdTheme('#e0e0e0', '#202020')};
display: flex;
flex-direction: column;
align-items: top;
}
.viewTab {
padding: 0px 8px;
}
.viewTab:hover {
background: ${cssManager.bdTheme('#ccc', '#ffffff10')};
color: ${cssManager.bdTheme('#000', '#fff')};
}
.viewTab:active {
background: ${cssManager.bdTheme('#aaa', '#ffffff20')};
color: ${cssManager.bdTheme('#000', '#fff')};
align-items: center;
gap: 8px;
}
.appName {
font-size: 14px;
font-weight: 600;
color: ${cssManager.bdTheme('#000', '#fff')};
white-space: nowrap;
color: ${cssManager.bdTheme('#666', '#999')};
overflow: hidden;
text-overflow: ellipsis;
}
.viewTabs-container {
flex: 1;
overflow-y: auto;
padding: 4px 0;
}
.viewTabs {
display: flex;
flex-direction: column;
}
.viewTab {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
cursor: default;
transition: background 0.1s;
color: ${cssManager.bdTheme('#333', '#ccc')};
user-select: none;
position: relative;
}
.viewTab:hover {
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.04)', 'rgba(255, 255, 255, 0.08)')};
}
.viewTab:active {
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.08)', 'rgba(255, 255, 255, 0.12)')};
}
.viewTab.selected {
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.08)', 'rgba(255, 255, 255, 0.12)')};
color: ${cssManager.bdTheme('#000', '#fff')};
font-weight: 500;
}
.viewTab.selected::before {
content: '';
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 3px;
background: ${cssManager.bdTheme('#26a69a', '#26a69a')};
}
.viewTab dees-icon {
font-size: 14px;
opacity: 0.7;
}
.appActions {
display: flex;
padding: 12px;
border-top: 1px solid ${cssManager.bdTheme('#e0e0e0', '#202020')};
}
.appActions .action {
.action {
display: flex;
align-items: center;
gap: 8px;
padding: 8px;
border-radius: 4px;
cursor: default;
transition: background 0.1s;
color: ${cssManager.bdTheme('#333', '#ccc')};
}
.appActions .action:hover {
color: ${cssManager.bdTheme('#000', '#fff')};
.action:hover {
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.04)', 'rgba(255, 255, 255, 0.08)')};
}
.action dees-icon {
font-size: 14px;
opacity: 0.7;
}
.appcontent {
@ -124,11 +185,11 @@ export class DeesSimpleAppDash extends DeesElement {
position: absolute;
top: 0px;
right: 0px;
height: calc(100vh - 24px);
height: calc(100% - 24px);
bottom: 24px;
width: calc(100vw - 200px);
width: calc(100% - 240px);
overflow: auto;
background: ${cssManager.bdTheme('#eeeeeb', '#000')};
background: ${cssManager.bdTheme('#f5f5f5', '#000')};
overscroll-behavior: contain;
}
@ -138,23 +199,36 @@ export class DeesSimpleAppDash extends DeesElement {
bottom: 0px;
left: 0px;
width: 100%;
border-top: 1px solid #44444480;
border-top: 1px solid ${cssManager.bdTheme('#00000020', '#ffffff20')};
height: 24px;
background: ${cssManager.bdTheme(colors.bright.blueMuted, colors.dark.blueMuted)};
background: ${cssManager.bdTheme('#2196f3', '#1565c0')};
z-index: 2;
display: flex;
justify-content: flex-end;
align-items: center;
flex-direction: row;
box-shadow: 0px 0px 10px 0px rgba(0, 0, 0, 0.8);
font-size: 12px;
}
.control {
width: min-content;
display: flex;
align-items: center;
gap: 4px;
margin-right: 16px;
font-size: 12px;
white-space: nowrap;
cursor: default;
opacity: 0.8;
transition: opacity 0.2s;
}
.control:hover {
opacity: 1;
}
.control dees-icon {
font-size: 14px;
}
`,
];
@ -162,31 +236,49 @@ export class DeesSimpleAppDash extends DeesElement {
return html`
<div class="maincontainer">
<div class="appbar">
<div>
<div class="sidebar-header">
<dees-icon .icon="lucide:grid3x3" style="font-size: 18px;"></dees-icon>
<div class="appName">${this.name}</div>
</div>
<div class="viewTabs-container">
<div class="viewTabs">
${this.viewTabs.map(
(view) => html`
<div class="viewTab" @click=${() => {
this.loadView(view);
}}>${view.name}</div>
<div
class="viewTab ${this.selectedView === view ? 'selected' : ''}"
@click=${() => this.loadView(view)}
>
${view.iconName ? html`
<dees-icon .icon="${`lucide:${view.iconName}`}"></dees-icon>
` : ''}
<span style="flex: 1;">${view.name}</span>
</div>
`
)}
</div>
</div>
</div>
<div class="appActions">
<div class="action" @click=${() => {
this.dispatchEvent(new CustomEvent('logout'));
}}>Logout</div>
this.dispatchEvent(new CustomEvent('logout', { bubbles: true, composed: true }));
}}>
<dees-icon .icon="lucide:logOut"></dees-icon>
<span>Logout</span>
</div>
</div>
</div>
<div class="appcontent">
<!-- Content goes here -->
</div>
<div class="controlbar">
<div class="control">
<dees-icon .iconFA=${'networkWired'}></dees-icon>
<dees-icon .icon="lucide:wifi"></dees-icon>
<span>Connected</span>
</div>
<div class="control" @click=${this.launchTerminal}>
<dees-icon .iconFA=${'terminal'}></dees-icon>
<dees-icon .icon="lucide:terminal"></dees-icon>
<span>Terminal</span>
</div>
</div>
</div>
@ -196,31 +288,59 @@ export class DeesSimpleAppDash extends DeesElement {
public async firstUpdated(_changedProperties): Promise<void> {
const domtools = await this.domtoolsPromise;
super.firstUpdated(_changedProperties);
if (this.viewTabs && this.viewTabs.length > 0) {
await this.loadView(this.viewTabs[0]);
}
}
public currentTerminal: DeesTerminal;
public async launchTerminal() {
const domtools = await this.domtoolsPromise;
if (this.currentTerminal) {
// If terminal already exists, remove it
await this.closeTerminal();
return;
}
const maincontainer = this.shadowRoot.querySelector('.maincontainer');
const { DeesTerminal } = await import('./dees-terminal.js');
const terminal = new DeesTerminal();
terminal.setupCommand = this.terminalSetupCommand;
this.currentTerminal = terminal;
maincontainer.appendChild(terminal);
terminal.style.position = 'absolute';
terminal.style.zIndex = '1';
terminal.style.zIndex = '10';
terminal.style.top = '0px';
terminal.style.left = '200px';
terminal.style.left = '240px';
terminal.style.right = '0px';
terminal.style.bottom = '24px';
terminal.style.opacity = '0';
terminal.style.transform = 'translateY(20px)';
terminal.style.transition = 'all 0.2s';
await domtools.plugins.smartdelay.delayFor(0);
terminal.style.background = '#000';
terminal.style.boxShadow = '0 4px 20px rgba(0, 0, 0, 0.3)';
// Add close button to terminal
terminal.addEventListener('close', () => this.closeTerminal());
await domtools.convenience.smartdelay.delayFor(0);
terminal.style.opacity = '1';
terminal.style.transform = 'translateY(0px)';
return terminal;
}
private async closeTerminal() {
const domtools = await this.domtoolsPromise;
if (this.currentTerminal) {
this.currentTerminal.style.opacity = '0';
this.currentTerminal.style.transform = 'translateY(20px)';
await domtools.convenience.smartdelay.delayFor(200);
this.currentTerminal.remove();
this.currentTerminal = null;
}
}
private currentView: DeesElement;
public async loadView(viewArg: IView) {
const appcontent = this.shadowRoot.querySelector('.appcontent');
@ -230,5 +350,13 @@ export class DeesSimpleAppDash extends DeesElement {
}
appcontent.appendChild(view);
this.currentView = view;
this.selectedView = viewArg;
// Emit view-select event
this.dispatchEvent(new CustomEvent('view-select', {
detail: { view: viewArg },
bubbles: true,
composed: true
}));
}
}

View File

@ -1,3 +1,37 @@
import { html } from '@design.estate/dees-element';
export const demoFunc = () => html` <dees-simple-login name="someapp"> Hello there </dees-simple-login> `;
export const demoFunc = () => html`
<style>
body {
margin: 0;
padding: 0;
}
.demo-container {
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
}
</style>
<div class="demo-container">
<dees-simple-login
name="My Application"
@login=${(e: CustomEvent) => {
console.log('Login event received:', e.detail);
const loginData = e.detail?.data || e.detail;
if (loginData?.username && loginData?.password) {
alert(`Login attempted with:\nUsername: ${loginData.username}\nPassword: ${loginData.password}`);
// Here you would typically validate credentials and show the slotted content
} else {
console.error('Invalid login data structure:', e.detail);
}
}}
>
<div style="padding: 40px; text-align: center;">
<h1>Welcome!</h1>
<p>This is the slotted content that appears after login.</p>
</div>
</dees-simple-login>
</div>
`;

View File

@ -13,6 +13,11 @@ import {
state,
} from '@design.estate/dees-element';
// Import components used in template
import './dees-form.js';
import './dees-input-text.js';
import './dees-form-submit.js';
declare global {
interface HTMLElementTagNameMap {
'dees-simple-login': DeesSimpleLogin;
@ -26,51 +31,77 @@ export class DeesSimpleLogin extends DeesElement {
// INSTANCE
@property()
public name = 'Dees Simple Login';
public name: string = 'Application';
public static styles = [
cssManager.defaultStyles,
css`
:host {
color: ${cssManager.bdTheme('#333', '#fff')};
color: ${cssManager.bdTheme('#333', '#ccc')};
user-select: none;
display: block;
width: 100%;
height: 100%;
font-family: 'Geist Sans', sans-serif;
}
.loginContainer {
position: absolute;
display: flex;
justify-content: center; /* aligns horizontally */
align-items: center; /* aligns vertically */
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
top: 0px;
left: 0px;
top: 0;
left: 0;
background: ${cssManager.bdTheme('#f5f5f5', '#000')};
}
.slotContainer {
position: absolute;
width: 100%;
height: 100%;
top: 0px;
left: 0px;
top: 0;
left: 0;
opacity: 0;
transition: opacity 0.3s, transform 0.3s;
pointer-events: none;
}
.login {
min-width: 320px;
min-height: 100px;
background: ${cssManager.bdTheme('#eeeeeb', '#111')};
box-shadow: ${cssManager.bdTheme('0px 1px 4px rgba(0,0,0,0.3)', 'none')};
background: ${cssManager.bdTheme('#ffffff', '#111')};
box-shadow: ${cssManager.bdTheme(
'0 4px 12px rgba(0, 0, 0, 0.15)',
'0 4px 12px rgba(0, 0, 0, 0.3)'
)};
border-radius: 8px;
border: 1px solid ${cssManager.bdTheme('#e0e0e0', '#202020')};
padding: 24px;
transition: opacity 0.3s, transform 0.3s;
}
.header {
text-align: center;
font-size: 16px;
font-weight: 600;
margin-bottom: 20px;
color: ${cssManager.bdTheme('#000', '#fff')};
}
.slotContainer {
opacity:0;
transition: opacity 0.3s, transform 0.3s;
pointer-events: none;
.login dees-form {
display: flex;
flex-direction: column;
gap: 12px;
}
.login dees-input-text {
width: 100%;
}
.login dees-form-submit {
margin-top: 4px;
width: 100%;
}
`,
];
@ -79,11 +110,11 @@ export class DeesSimpleLogin extends DeesElement {
return html`
<div class="loginContainer">
<div class="login">
<dees-form>
<div class="header">Login to ${this.name}</div>
<dees-input-text key="username" label="username" required></dees-input-text>
<dees-input-text key="password" label="password" isPasswordBool required></dees-input-text>
<dees-form-submit disabled>login</dees-form-submit>
<dees-form>
<dees-input-text key="username" label="Username" required></dees-input-text>
<dees-input-text key="password" label="Password" isPasswordBool required></dees-input-text>
<dees-form-submit>Login</dees-form-submit>
</dees-form>
</div>
</div>
@ -93,19 +124,48 @@ export class DeesSimpleLogin extends DeesElement {
`;
}
public async firstUpdated(_changedProperties): Promise<void> {
const domtools = await this.domtoolsPromise;
public async firstUpdated(_changedProperties: Map<string | number | symbol, unknown>): Promise<void> {
super.firstUpdated(_changedProperties);
const form = this.shadowRoot.querySelector('dees-form');
const domtools = await this.domtoolsPromise;
// Wait a tick to ensure child elements are rendered
await this.updateComplete;
const form = this.shadowRoot.querySelector('dees-form') as any;
if (!form) {
console.error('dees-form element not found in dees-simple-login');
return;
}
// Check if the form has the readyDeferred property and wait for it
if (form.readyDeferred?.promise) {
try {
await form.readyDeferred.promise;
const username = this.shadowRoot.querySelector('dees-input-text[label="username"]');
const password = this.shadowRoot.querySelector('dees-input-text[label="password"]');
} catch (error) {
console.error('Error waiting for form ready:', error);
}
}
const username = this.shadowRoot.querySelector('dees-input-text[key="username"]');
const password = this.shadowRoot.querySelector('dees-input-text[key="password"]');
const submit = this.shadowRoot.querySelector('dees-form-submit');
// Add form data listener
form.addEventListener('formData', (event: CustomEvent) => {
this.dispatchEvent(new CustomEvent('login', { detail: event.detail }));
this.dispatchEvent(new CustomEvent('login', {
detail: event.detail,
bubbles: true,
composed: true
}));
});
}
/**
* Dispatches a 'login' event when the form is submitted.
* Event detail structure: { data: { username: string, password: string } }
*/
/**
* allows switching to slotted content
*/
@ -123,8 +183,5 @@ export class DeesSimpleLogin extends DeesElement {
slotContainerDiv.style.transform = 'translateY(0px)';
await domtools.convenience.smartdelay.delayFor(300);
slotContainerDiv.style.pointerEvents = 'all';
}
}

View File

@ -4,8 +4,12 @@ export * from './dees-appui-base.js';
export * from './dees-appui-maincontent.js';
export * from './dees-appui-mainmenu.js';
export * from './dees-appui-mainselector.js';
export * from './dees-appui-profiledropdown.js';
export * from './dees-appui-tabs.js';
export * from './dees-appui-view.js';
export * from './dees-badge.js';
export * from './dees-button-exit.js';
export * from './dees-button-group.js';
export * from './dees-button.js';
export * from './dees-chart-area.js';
export * from './dees-chart-log.js';
@ -35,6 +39,7 @@ export * from './dees-label.js';
export * from './dees-mobilenavigation.js';
export * from './dees-modal.js';
export * from './dees-input-multitoggle.js';
export * from './dees-panel.js';
export * from './dees-pdf.js';
export * from './dees-searchbar.js';
export * from './dees-simple-appdash.js';

View File

@ -0,0 +1,34 @@
import * as plugins from '../00plugins.js';
/**
* Divider menu item
*/
export interface IAppBarMenuDivider {
divider: true;
}
/**
* Regular menu item
*/
export interface IAppBarMenuItemRegular extends plugins.tsclass.website.IMenuItem {
id?: string;
shortcut?: string; // e.g., "Cmd+S" or "Ctrl+S"
submenu?: IAppBarMenuItem[];
disabled?: boolean;
checked?: boolean; // For checkbox menu items
radioGroup?: string; // For radio button menu items
}
/**
* Extended menu item interface for app bar menus
* Can be either a regular menu item or a divider
*/
export type IAppBarMenuItem = IAppBarMenuItemRegular | IAppBarMenuDivider;
/**
* Interface for the menu bar configuration
*/
export interface IMenuBar {
menuItems: IAppBarMenuItem[];
onMenuSelect?: (item: IAppBarMenuItem) => void;
}

View File

@ -1,2 +1,3 @@
export * from './tab.js';
export * from './selectionoption.js';
export * from './appbarmenuitem.js';

View File

@ -1,4 +1,5 @@
export interface ISelectionOption {
key: string;
iconName?: string;
action: () => void;
}