Compare commits

..

64 Commits

Author SHA1 Message Date
2f17dea480 feat(playbook): add PlayBook 2025-07-04 18:42:53 +00:00
ce33aff843 1.10.10
Some checks failed
Default (tags) / security (push) Failing after 13s
Default (tags) / test (push) Failing after 57s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-06-30 13:07:16 +00:00
09eea844d7 feat(dees-mobilenavigation): update to use zindex registry and shadcn-like design
- Replace old zIndexLayers with new zIndexRegistry system
- Update design to match shadcn aesthetic with clean borders and shadows
- Add support for icons in menu items using Lucide icons
- Improve animations with staggered item appearance
- Better typography using Geist font family
- Add divider support for menu item grouping
- Improve hover and active states
- Add custom scrollbar styling
- Create comprehensive demo showcasing all features
- Ensure proper cleanup in disconnectedCallback
2025-06-30 13:04:19 +00:00
956edf0d63 fix(icons): update icon usage across components
- Replace .iconName property with .icon for dees-icon component
- Fix incorrect lucide icon names to use proper prefix and kebab-case
- Replace deprecated .iconFA property with .icon
- Add loading animation to dees-input-fileupload button
- Maintain compatibility with external interfaces expecting iconName
2025-06-30 12:57:13 +00:00
1db74177b3 update 2025-06-30 12:02:02 +00:00
1c25554c38 update 2025-06-30 11:35:38 +00:00
7d1e06701b update 2025-06-30 11:24:38 +00:00
aae4427281 update 2025-06-30 11:18:30 +00:00
911c51d078 update 2025-06-30 11:08:14 +00:00
2c12c22666 update 2025-06-30 10:58:31 +00:00
60a811fd18 update 2025-06-30 10:53:22 +00:00
9a9aea56da add datepicker 2025-06-30 10:40:23 +00:00
49ad998b2c update 2025-06-29 14:00:55 +00:00
5066681b3a update 2025-06-28 12:34:35 +00:00
ee22879c00 update 2025-06-28 12:27:35 +00:00
9b0ff2d856 1.10.9
Some checks failed
Default (tags) / security (push) Failing after 59s
Default (tags) / test (push) Failing after 19s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-06-28 10:05:09 +00:00
7e14645ed7 update 2025-06-27 23:48:39 +00:00
811737adcd update 2025-06-27 22:55:20 +00:00
7b6c135cd3 update 2025-06-27 22:47:24 +00:00
46065b2424 1.10.8
Some checks failed
Default (tags) / security (push) Failing after 55s
Default (tags) / test (push) Failing after 17s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-06-27 21:19:43 +00:00
e76a6c3632 update 2025-06-27 21:19:14 +00:00
896bc2bbb1 update 2025-06-27 21:16:52 +00:00
296d254ba2 update 2025-06-27 21:07:47 +00:00
ecad05098f update 2025-06-27 21:05:28 +00:00
956964f5b9 update dees-chips 2025-06-27 21:01:12 +00:00
ed73e16bbb update dees-modal 2025-06-27 19:48:32 +00:00
7817b4a440 1.10.7
Some checks failed
Default (tags) / security (push) Failing after 54s
Default (tags) / test (push) Failing after 12s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-06-27 19:33:37 +00:00
03f25b7f10 update wysiwyg 2025-06-27 19:29:26 +00:00
24957f02d4 update wysiwyg 2025-06-27 19:25:34 +00:00
fe3cd0591f update 2025-06-27 18:38:39 +00:00
56f5f5887b update 2025-06-27 18:03:42 +00:00
2e0bf26301 update table 2025-06-27 17:50:54 +00:00
3d7f5253e8 update fonts 2025-06-27 17:32:01 +00:00
669f12e822 1.10.6
Some checks failed
Default (tags) / security (push) Failing after 52s
Default (tags) / test (push) Failing after 19s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-06-27 17:14:49 +00:00
8b870a8e46 update 2025-06-27 17:14:46 +00:00
9148f0595a update 2025-06-27 17:14:26 +00:00
ea7da1c9b9 update 2025-06-27 16:39:17 +00:00
3e81f54e99 update 2025-06-27 16:29:19 +00:00
65aa9f3c06 update 2025-06-27 16:20:06 +00:00
82ebd9c556 update 2025-06-27 15:58:26 +00:00
50aa071e2e update dees-chart 2025-06-27 15:43:26 +00:00
807e1ff733 1.10.5
Some checks failed
Default (tags) / security (push) Failing after 52s
Default (tags) / test (push) Failing after 19s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-06-27 13:44:52 +00:00
4146a348ab update statsgrid 2025-06-27 13:44:36 +00:00
bd10b4e64d 1.10.4
Some checks failed
Default (tags) / security (push) Failing after 40s
Default (tags) / test (push) Failing after 18s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-06-27 11:50:26 +00:00
243ecddd42 update button and statsgrid with better styles. 2025-06-27 11:50:07 +00:00
d7b690621e 1.10.3
Some checks failed
Default (tags) / security (push) Failing after 42s
Default (tags) / test (push) Failing after 32s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-06-27 00:45:53 +00:00
60951330d1 fix(typelist): update styling 2025-06-27 00:45:11 +00:00
7095197d07 1.10.2
Some checks failed
Default (tags) / security (push) Failing after 14s
Default (tags) / test (push) Failing after 40s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-06-27 00:35:21 +00:00
3ee48e80ad fix(wysiwig): zindexregistry for menus 2025-06-27 00:35:06 +00:00
00ad2b0563 1.10.1
Some checks failed
Default (tags) / security (push) Failing after 43s
Default (tags) / test (push) Failing after 58s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-06-27 00:19:03 +00:00
a57005a49b fix(modal): scroll behaviour contain 2025-06-27 00:18:36 +00:00
d67a66662d 1.10.0
Some checks failed
Default (tags) / security (push) Failing after 23s
Default (tags) / test (push) Failing after 12s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-06-27 00:00:00 +00:00
c75c5bcd3b feat(dees-modal): Add mobileFullscreen option to modals for full-screen mobile support 2025-06-27 00:00:00 +00:00
ad0864cddf 1.9.9
Some checks failed
Default (tags) / security (push) Failing after 22s
Default (tags) / test (push) Failing after 12s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-06-26 23:34:11 +00:00
9985c29a84 fix(dees-input-multitoggle, dees-input-typelist): Replace dynamic import with static import for demo functions in dees-input-multitoggle and dees-input-typelist 2025-06-26 23:34:11 +00:00
1dcaccdb6d 1.9.8
Some checks failed
Default (tags) / security (push) Failing after 33s
Default (tags) / test (push) Failing after 27s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-06-26 23:19:44 +00:00
35eb410051 fix(deps, windowlayer): Update dependency versions and adjust dees-windowlayer CSS to add pointer-events fix 2025-06-26 23:19:43 +00:00
10c43ecd59 update 2025-06-26 20:20:34 +00:00
9df4a09414 1.9.7
Some checks failed
Default (tags) / security (push) Failing after 35s
Default (tags) / test (push) Failing after 29s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-06-26 19:19:19 +00:00
7cbc941407 update readme 2025-06-26 19:18:58 +00:00
b31f306106 1.9.6
Some checks failed
Default (tags) / security (push) Failing after 36s
Default (tags) / test (push) Failing after 29s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-06-26 19:00:57 +00:00
1dbbac450c update dees-tags 2025-06-26 19:00:15 +00:00
b5a2bd7436 fix zindex 2025-06-26 18:37:49 +00:00
931a760ee1 update z-index showcase 2025-06-26 18:13:08 +00:00
101 changed files with 17553 additions and 4086 deletions

174
CLAUDE.md Normal file
View File

@ -0,0 +1,174 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
@design.estate/dees-catalog is a comprehensive web components library built with TypeScript and LitElement. It provides a large collection of UI components for building modern web applications with consistent design and behavior.
## Build and Development Commands
```bash
# Install dependencies
pnpm install
# Build the project
pnpm run build
# This runs: tsbuild tsfolders --allowimplicitany && tsbundle element --production --bundler esbuild
# Run development watch mode
pnpm run watch
# This runs: tswatch element
# Run tests (browser tests)
pnpm test
# This runs: tstest test/ --web --verbose --timeout 30 --logfile
# Run a specific test file
tsx test/test.wysiwyg-basic.browser.ts --verbose
# Build documentation
pnpm run buildDocs
```
### Testing Notes
- Test files follow the pattern: `test.*.browser.ts`, `test.*.node.ts`, or `test.*.both.ts`
- Browser tests run in a headless browser environment
- Use `--logfile` option to store logs in `.nogit/testlogs/`
- For debugging, create files in `.nogit/debug/` and run with `tsx`
## Architecture Overview
### Component Structure
The library is organized into several categories:
1. **Core UI Components** (`dees-button`, `dees-badge`, `dees-icon`, etc.)
- Basic building blocks with consistent theming
- All support light/dark themes via `cssManager.bdTheme()`
2. **Form Components** (`dees-form`, `dees-input-*`)
- Complete form system with validation
- Base class `DeesInputBase` provides common functionality
- Form data collection via `DeesForm` container
3. **Layout Components** (`dees-appui-*`)
- Application shell components
- `DeesAppuiBase` orchestrates the entire layout
- Grid-based responsive design
4. **Data Display** (`dees-table`, `dees-dataview-*`, `dees-statsgrid`)
- Complex data visualization components
- Interactive tables with sorting/filtering
- Chart components using ApexCharts
5. **Overlays** (`dees-modal`, `dees-contextmenu`, `dees-toast`)
- Managed by central z-index registry
- Window layer system for proper stacking
### Key Architectural Patterns
#### Z-Index Management
All overlay components use a centralized z-index registry system:
- Definition in `ts_web/elements/00zindex.ts`
- Dynamic z-index assignment via `ZIndexRegistry` class
- Components get z-index from registry when showing
- Ensures proper stacking order (dropdowns above modals, etc.)
#### Theme System
- All components support light/dark themes
- Use `cssManager.bdTheme(lightValue, darkValue)` for theme-aware colors
- Consistent color palette defined in `00colors.ts`
#### Component Demo System
- Each component has a static `demo` property
- Demo functions in separate `.demo.ts` files
- Showcase pages aggregate demos (e.g., `input-showcase.ts`)
#### WYSIWYG Editor Architecture
The WYSIWYG editor uses a sophisticated architecture with separated concerns:
- **Main Component**: `dees-input-wysiwyg.ts` - Orchestrates the editor
- **Handler Classes**:
- `WysiwygInputHandler` - Handles text input and block transformations
- `WysiwygKeyboardHandler` - Manages keyboard shortcuts and navigation
- `WysiwygDragDropHandler` - Manages block reordering
- `WysiwygModalManager` - Shows configuration modals
- `WysiwygBlockOperations` - Core block manipulation logic
- **Global Menus**:
- `DeesSlashMenu` and `DeesFormattingMenu` render globally to avoid focus issues
- Singleton pattern ensures single instance
- **Programmatic Rendering**: Uses manual DOM manipulation to prevent focus loss
### Component Communication
- Custom events for parent-child communication
- Form components emit standardized events (`change`, `blur`, etc.)
- Complex components like `DeesAppuiBase` re-emit child events
### Build System
- TypeScript compilation with decorators support
- Web component bundling with esbuild
- Element exports in `ts_web/elements/index.ts`
- Distribution builds in `dist_ts_web/`
## Important Implementation Details
### When Creating New Components
1. Extend `DeesElement` from `@design.estate/dees-element`
2. Use `@customElement('dees-componentname')` decorator
3. Implement theme support with `cssManager.bdTheme()`
4. Create a demo function in a separate `.demo.ts` file
5. Export from `elements/index.ts`
### Form Input Components
1. Extend `DeesInputBase` for form inputs
2. Implement `getValue()` and `setValue()` methods
3. Use `changeSubject.next(this)` to emit changes
4. Support `disabled` and `required` properties
### Overlay Components
1. Import z-index from `00zindex.ts`
2. Get z-index from registry when showing: `zIndexRegistry.getNextZIndex()`
3. Register/unregister with the registry
4. Use `DeesWindowLayer` for backdrop if needed
### Testing Components
1. Create test files in `test/` directory
2. Use `@git.zone/tstest` with tap-bundle
3. Test in browser environment for web components
4. Use proper async/await for component lifecycle
## Common Patterns and Pitfalls
### Focus Management
- WYSIWYG editor uses programmatic rendering to prevent focus loss
- Use `requestAnimationFrame` for timing-sensitive focus operations
- Avoid reactive re-renders during user input
### Event Handling
- Prevent event bubbling in nested interactive components
- Use `pointer-events: none/auto` for click-through behavior
- Handle both mouse and keyboard events for accessibility
### Performance Considerations
- Large components (editor, terminal) use lazy loading
- Charts use debounced resize observers
- Tables implement virtual scrolling for large datasets
## File Organization
```
ts_web/
├── elements/ # All component files
│ ├── 00*.ts # Shared utilities (colors, z-index, plugins)
│ ├── dees-*.ts # Component implementations
│ ├── dees-*.demo.ts # Component demos
│ ├── interfaces/ # Shared TypeScript interfaces
│ ├── helperclasses/ # Utility classes (FormController)
│ └── wysiwyg/ # WYSIWYG editor subsystem
├── pages/ # Demo showcase pages
└── index.ts # Main export file
```
## Recent Major Changes
- Z-Index Registry System (2025-12-24): Dynamic stacking order management
- WYSIWYG Refactoring (2025-06-24): Complete architecture overhaul with separated concerns
- Form System Enhancement: Unified validation and data collection
- Theme System: Consistent light/dark theme support across all components

View File

@ -1,5 +1,108 @@
# Changelog # Changelog
## 2025-06-29 - 1.10.10 - improve(dees-dashboardgrid, dees-input-wysiwyg)
Enhanced dashboard grid component with advanced spacing and layout features inspired by gridstack.js
Dashboard Grid improvements:
- Improved margin system supporting uniform or individual margins (top, right, bottom, left)
- Added collision detection to prevent widget overlap during drag operations
- Implemented auto-positioning for new widgets to find first available space
- Added compact() method to eliminate gaps and compress layout vertically or horizontally
- Enhanced resize constraints with minW, maxW, minH, maxH support
- Added optional grid lines visualization for better layout understanding
- Improved resize handles with better visibility and hover states
- Added RTL (right-to-left) layout support
- Implemented cellHeightUnit option supporting 'px', 'em', 'rem', or 'auto' (square cells)
- Added configurable animation with enableAnimation property
- Enhanced demo with interactive controls for testing all features
- Better calculation of widget positions accounting for margins between cells
- Added findAvailablePosition() for intelligent widget placement
- Improved drag and resize calculations for pixel-perfect positioning
WYSIWYG editor drag and drop fixes:
- Fixed drop indicator positioning to properly account for block margins
- Added defensive checks in drag event handlers to prevent potential crashes
- Improved updateBlockPositions with null checks and error handling
- Updated drop indicator calculation to use simplified margin approach
- Fixed drop indicator height to match the exact space occupied by dragged blocks
- Improved drop indicator positioning algorithm to accurately show where blocks will land
- Simplified visual block position calculations accounting for CSS transforms
- Enhanced margin calculation to use correct values based on block type (16px for paragraphs, 24px for headings, 20px for code/quotes)
- Fixed index calculation issue when dragging blocks downward by adjusting target index for excluded dragged block
## 2025-06-28 - 1.10.9 - feat(dees-dashboardgrid)
Add new dashboard grid component with drag-and-drop and resize capabilities
- Created dees-dashboardgrid component for building flexible dashboard layouts
- Features drag-and-drop functionality for rearranging widgets
- Includes resize handles for adjusting widget dimensions
- Supports configurable grid properties (columns, cell height, gap)
- Provides widget locking and editable mode controls
- Styled with shadcn design principles
- No external dependencies - built with native browser APIs
- Emits events for widget movements and resizes
- Includes comprehensive demo with sample dashboard widgets
## 2025-06-27 - 1.10.8 - feat(ui-components)
Update multiple components with shadcn-aligned styling and improved animations
- Updated dees-modal with shadcn colors, borders, and subtle shadows
- Updated dees-chips with shadcn styling and fixed selection logic bug
- Updated dees-dataview-codebox with shadcn syntax highlighting colors and responsive label layout
- Updated dees-input-multitoggle with transparent blue indicator and smooth animations
- Updated dees-appui-tabs with animated sliding indicator for both horizontal and vertical layouts
- Fixed indicator positioning to be perfectly centered on tab content
- Indicator width is content width + 8px for minimal visual padding
- Fixed tab content centering by using consistent padding (12px → 16px on all sides)
- Fixed icon rendering by correcting property name from .iconName to .icon
- Added visual separators between tabs for better distinction
- Added subtle hover backgrounds for improved interactivity
- Refactored tabs component code for better maintainability and elegance
- Updated dees-appui-activitylog with shadcn-aligned styling:
- Updated background and text colors to match shadcn palette
- Enhanced topbar with better spacing and typography
- Improved activity entries with subtle hover states and better spacing
- Added activity type icons with color-coded backgrounds (login, logout, view, create, update)
- Added date separators ("Today", "Yesterday") for better temporal organization
- Enhanced streaming indicators with animated pulse effect
- Redesigned searchbox with modern input styling, search icon, and focus states
- Added custom scrollbar styling for consistency
- Updated timestamps to be more subtle with tabular number formatting
- Refined shadow effects for better visual hierarchy
- Added subtle box shadow to component for depth
- Added fade-in animation for new activity entries
- Improved user name highlighting with better typography
- Updated context menu with more relevant actions
- Improved overall spacing and visual consistency across components
## 2025-06-27 - 1.10.1 - fix(modal)
Improve modal overscroll behavior by adding 'overscroll-behavior: contain' to content container
- Added 'overscroll-behavior: contain' to .modal .content to ensure proper scroll containment
- Applied overscroll-behavior in modal container for enhanced responsiveness on mobile and desktop
## 2025-06-26 - 1.10.0 - feat(dees-modal)
Add mobileFullscreen option to modals for full-screen mobile support
- Introduced a new boolean property 'mobileFullscreen' in ts_web/elements/dees-modal.ts
- Updated modal CSS under the media query to apply 'mobile-fullscreen' class, allowing full viewport modals on mobile devices
- Extended modal style rules to include adjustments for margin, border-radius, and maximum heights on smaller screens
## 2025-06-26 - 1.9.9 - fix(dees-input-multitoggle, dees-input-typelist)
Replace dynamic import with static import for demo functions in dees-input-multitoggle and dees-input-typelist
- Converted `await import('./dees-input-multitoggle.demo.js')` to a direct static import.
- Converted `await import('./dees-input-typelist.demo.js')` to a direct static import to improve build performance and clarity.
## 2025-06-26 - 1.9.8 - fix(deps, windowlayer)
Update dependency versions and adjust dees-windowlayer CSS to add pointer-events fix
- Bump @design.estate/dees-wcctools from ^1.0.98 to ^1.0.101
- Bump @tiptap packages from 2.22.3 to 2.23.0
- Bump lucide from ^0.522.0 to ^0.523.0
- Bump @git.zone/tsbundle from ^2.4.0 to ^2.5.1 and tswatch from ^2.0.37 to ^2.1.2
- Add 'pointer-events: none' to dees-windowlayer CSS to improve overlay behavior
## 2025-06-22 - 1.9.0 - feat(form-inputs) ## 2025-06-22 - 1.9.0 - feat(form-inputs)
Improve form input consistency and auto spacing across inputs and buttons Improve form input consistency and auto spacing across inputs and buttons

View File

@ -1,14 +1,14 @@
{ {
"name": "@design.estate/dees-catalog", "name": "@design.estate/dees-catalog",
"version": "1.9.5", "version": "1.10.10",
"private": false, "private": false,
"description": "A comprehensive library that provides dynamic web components for building sophisticated and modern web applications using JavaScript and TypeScript.", "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", "main": "dist_ts_web/index.js",
"typings": "dist_ts_web/index.d.ts", "typings": "dist_ts_web/index.d.ts",
"type": "module", "type": "module",
"scripts": { "scripts": {
"test": "tstest test/ --web --verbose --timeout 30", "test": "tstest test/ --web --verbose --timeout 30 --logfile",
"build": "tsbuild tsfolders --allowimplicitany && tsbundle element --production", "build": "tsbuild tsfolders --allowimplicitany && tsbundle element --production --bundler esbuild",
"watch": "tswatch element", "watch": "tswatch element",
"buildDocs": "tsdoc" "buildDocs": "tsdoc"
}, },
@ -17,7 +17,7 @@
"dependencies": { "dependencies": {
"@design.estate/dees-domtools": "^2.3.3", "@design.estate/dees-domtools": "^2.3.3",
"@design.estate/dees-element": "^2.0.45", "@design.estate/dees-element": "^2.0.45",
"@design.estate/dees-wcctools": "^1.0.98", "@design.estate/dees-wcctools": "^1.1.0",
"@fortawesome/fontawesome-svg-core": "^6.7.2", "@fortawesome/fontawesome-svg-core": "^6.7.2",
"@fortawesome/free-brands-svg-icons": "^6.7.2", "@fortawesome/free-brands-svg-icons": "^6.7.2",
"@fortawesome/free-regular-svg-icons": "^6.7.2", "@fortawesome/free-regular-svg-icons": "^6.7.2",
@ -25,18 +25,18 @@
"@push.rocks/smarti18n": "^1.0.4", "@push.rocks/smarti18n": "^1.0.4",
"@push.rocks/smartpromise": "^4.2.0", "@push.rocks/smartpromise": "^4.2.0",
"@push.rocks/smartstring": "^4.0.15", "@push.rocks/smartstring": "^4.0.15",
"@tiptap/core": "^2.22.3", "@tiptap/core": "^2.23.0",
"@tiptap/extension-link": "^2.22.3", "@tiptap/extension-link": "^2.23.0",
"@tiptap/extension-text-align": "^2.22.3", "@tiptap/extension-text-align": "^2.23.0",
"@tiptap/extension-typography": "^2.22.3", "@tiptap/extension-typography": "^2.23.0",
"@tiptap/extension-underline": "^2.22.3", "@tiptap/extension-underline": "^2.23.0",
"@tiptap/starter-kit": "^2.22.3", "@tiptap/starter-kit": "^2.23.0",
"@tsclass/tsclass": "^9.2.0", "@tsclass/tsclass": "^9.2.0",
"@webcontainer/api": "1.2.0", "@webcontainer/api": "1.2.0",
"apexcharts": "^4.7.0", "apexcharts": "^4.7.0",
"highlight.js": "11.11.1", "highlight.js": "11.11.1",
"ibantools": "^4.5.1", "ibantools": "^4.5.1",
"lucide": "^0.522.0", "lucide": "^0.525.0",
"monaco-editor": "^0.52.2", "monaco-editor": "^0.52.2",
"pdfjs-dist": "^4.10.38", "pdfjs-dist": "^4.10.38",
"xterm": "^5.3.0", "xterm": "^5.3.0",
@ -44,9 +44,9 @@
}, },
"devDependencies": { "devDependencies": {
"@git.zone/tsbuild": "^2.6.4", "@git.zone/tsbuild": "^2.6.4",
"@git.zone/tsbundle": "^2.4.0", "@git.zone/tsbundle": "^2.5.1",
"@git.zone/tstest": "^2.3.1", "@git.zone/tstest": "^2.3.1",
"@git.zone/tswatch": "^2.0.37", "@git.zone/tswatch": "^2.1.2",
"@push.rocks/projectinfo": "^5.0.2", "@push.rocks/projectinfo": "^5.0.2",
"@push.rocks/tapbundle": "^6.0.3", "@push.rocks/tapbundle": "^6.0.3",
"@types/node": "^22.0.0" "@types/node": "^22.0.0"

709
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -1,5 +1,5 @@
!!! Please pay attention to the following points when writing the readme: !!! !!! Please pay attention to the following points when writing the readme: !!!
* Give a short rundown of components and a few points abputspecific features on each. * Give a short rundown of components and a few points abput specific features on each.
* Try to list all components in a summary. * Try to list all components in a summary.
* Then list all components with a short description. * Then list all components with a short description.

748
readme.md
View File

@ -1,5 +1,8 @@
# @design.estate/dees-catalog # @design.estate/dees-catalog
An extensive library for building modern web applications with dynamic components using Web Components, JavaScript, and TypeScript. A comprehensive web components library built with TypeScript and LitElement, providing 75+ UI components for building modern web applications with consistent design and behavior.
## Development Guide
For developers working on this library, please refer to the [UI Components Playbook](readme.playbook.md) for comprehensive patterns, best practices, and architectural guidelines.
## Install ## Install
To install the `@design.estate/dees-catalog` library, you can use npm or any other compatible JavaScript package manager: To install the `@design.estate/dees-catalog` library, you can use npm or any other compatible JavaScript package manager:
@ -12,14 +15,16 @@ npm install @design.estate/dees-catalog
| Category | Components | | Category | Components |
|----------|------------| |----------|------------|
| Core UI | `DeesButton`, `DeesBadge`, `DeesChips`, `DeesIcon`, `DeesLabel`, `DeesSpinner`, `DeesToast` | | Core UI | [`DeesButton`](#deesbutton), [`DeesButtonExit`](#deesbuttonexit), [`DeesButtonGroup`](#deesbuttongroup), [`DeesBadge`](#deesbadge), [`DeesChips`](#deeschips), [`DeesHeading`](#deesheading), [`DeesHint`](#deeshint), [`DeesIcon`](#deesicon), [`DeesLabel`](#deeslabel), [`DeesPanel`](#deespanel), [`DeesSearchbar`](#deessearchbar), [`DeesSpinner`](#deesspinner), [`DeesToast`](#deestoast), [`DeesWindowcontrols`](#deeswindowcontrols) |
| Forms | `DeesForm`, `DeesInputText`, `DeesInputCheckbox`, `DeesInputDropdown`, `DeesInputRadio`, `DeesInputFileupload`, `DeesInputIban`, `DeesInputPhone`, `DeesInputQuantitySelector`, `DeesInputMultitoggle`, `DeesFormSubmit` | | Forms | [`DeesForm`](#deesform), [`DeesInputText`](#deesinputtext), [`DeesInputCheckbox`](#deesinputcheckbox), [`DeesInputDropdown`](#deesinputdropdown), [`DeesInputRadiogroup`](#deesinputradiogroup), [`DeesInputFileupload`](#deesinputfileupload), [`DeesInputIban`](#deesinputiban), [`DeesInputPhone`](#deesinputphone), [`DeesInputQuantitySelector`](#deesinputquantityselector), [`DeesInputMultitoggle`](#deesinputmultitoggle), [`DeesInputTags`](#deesinputtags), [`DeesInputTypelist`](#deesinputtypelist), [`DeesInputRichtext`](#deesinputrichtext), [`DeesInputWysiwyg`](#deesinputwysiwyg), [`DeesInputDatepicker`](#deesinputdatepicker), [`DeesInputSearchselect`](#deesinputsearchselect), [`DeesFormSubmit`](#deesformsubmit) |
| Layout | `DeesAppuiBase`, `DeesAppuiMainmenu`, `DeesAppuiMainselector`, `DeesAppuiMaincontent`, `DeesAppuiAppbar`, `DeesMobileNavigation` | | Layout | [`DeesAppuiBase`](#deesappuibase), [`DeesAppuiMainmenu`](#deesappuimainmenu), [`DeesAppuiMainselector`](#deesappuimainselector), [`DeesAppuiMaincontent`](#deesappuimaincontent), [`DeesAppuiAppbar`](#deesappuiappbar), [`DeesAppuiActivitylog`](#deesappuiactivitylog), [`DeesAppuiProfiledropdown`](#deesappuiprofiledropdown), [`DeesAppuiTabs`](#deesappuitabs), [`DeesAppuiView`](#deesappuiview), [`DeesMobileNavigation`](#deesmobilenavigation), [`DeesDashboardGrid`](#deesdashboardgrid) |
| Data Display | `DeesTable`, `DeesDataviewCodebox`, `DeesDataviewStatusobject`, `DeesPdf`, `DeesStatsGrid` | | Data Display | [`DeesTable`](#deestable), [`DeesDataviewCodebox`](#deesdataviewcodebox), [`DeesDataviewStatusobject`](#deesdataviewstatusobject), [`DeesPdf`](#deespdf), [`DeesStatsGrid`](#deesstatsgrid), [`DeesPagination`](#deespagination) |
| Visualization | `DeesChartArea`, `DeesChartLog` | | Visualization | [`DeesChartArea`](#deeschartarea), [`DeesChartLog`](#deeschartlog) |
| Dialogs & Overlays | `DeesModal`, `DeesContextmenu`, `DeesSpeechbubble`, `DeesWindowlayer` | | Dialogs & Overlays | [`DeesModal`](#deesmodal), [`DeesContextmenu`](#deescontextmenu), [`DeesSpeechbubble`](#deesspeechbubble), [`DeesWindowlayer`](#deeswindowlayer) |
| Navigation | `DeesStepper`, `DeesProgressbar` | | Navigation | [`DeesStepper`](#deesstepper), [`DeesProgressbar`](#deesprogressbar) |
| Development | `DeesEditor`, `DeesEditorMarkdown`, `DeesTerminal`, `DeesUpdater` | | Development | [`DeesEditor`](#deeseditor), [`DeesEditorMarkdown`](#deeseditormarkdown), [`DeesEditorMarkdownoutlet`](#deeseditormarkdownoutlet), [`DeesTerminal`](#deesterminal), [`DeesUpdater`](#deesupdater) |
| Auth & Utilities | [`DeesSimpleAppdash`](#deessimpleappdash), [`DeesSimpleLogin`](#deessimplelogin) |
| Shopping | [`DeesShoppingProductcard`](#deesshoppingproductcard) |
## Detailed Component Documentation ## Detailed Component Documentation
@ -69,14 +74,36 @@ Interactive chips/tags with selection capabilities.
``` ```
#### `DeesIcon` #### `DeesIcon`
Display icons from various icon sets including FontAwesome. Display icons from FontAwesome and Lucide icon libraries with library prefixes.
```typescript ```typescript
// FontAwesome icons - use 'fa:' prefix
<dees-icon <dees-icon
icon="home" // FontAwesome icon name icon="fa:check" // FontAwesome icon with fa: prefix
type="solid" // Options: solid, regular, brands iconSize="24" // Size in pixels
size="1.5rem" // Optional: custom size color="#22c55e" // Optional: custom color
></dees-icon>
// Lucide icons - use 'lucide:' prefix
<dees-icon
icon="lucide:menu" // Lucide icon with lucide: prefix
iconSize="24" // Size in pixels
color="#007bff" // Optional: custom color color="#007bff" // Optional: custom color
strokeWidth="2" // Optional: stroke width for Lucide icons
></dees-icon>
// Available FontAwesome icons include:
// fa:check, fa:bell, fa:gear, fa:trash, fa:copy, fa:paste, fa:eye, fa:eyeSlash,
// fa:plus, fa:minus, fa:circleInfo, fa:circleCheck, fa:circleXmark, fa:message,
// fa:arrowRight, fa:facebook, fa:twitter, fa:linkedin, fa:instagram, etc.
// Available Lucide icons include:
// lucide:menu, lucide:settings, lucide:home, lucide:file, lucide:folder,
// lucide:search, lucide:user, lucide:heart, lucide:star, lucide:download, etc.
// Legacy API (deprecated but still supported)
<dees-icon
iconFA="check" // Without prefix - assumes FontAwesome
></dees-icon> ></dees-icon>
``` ```
@ -149,6 +176,93 @@ Key Features:
- Theme-aware styling - Theme-aware styling
- Programmatic control - Programmatic control
#### `DeesButtonExit`
Exit/close button component with consistent styling.
```typescript
<dees-button-exit
@click=${handleClose}
></dees-button-exit>
```
#### `DeesButtonGroup`
Container for grouping related buttons together.
```typescript
<dees-button-group
.buttons=${[
{ text: 'Save', type: 'highlighted', action: handleSave },
{ text: 'Cancel', type: 'normal', action: handleCancel }
]}
spacing="medium" // Options: small, medium, large
></dees-button-group>
```
#### `DeesHeading`
Consistent heading component with level and styling options.
```typescript
<dees-heading
level={1} // 1-6 for H1-H6
text="Page Title"
.subheading=${'Optional subtitle'}
centered // Optional: center alignment
></dees-heading>
```
#### `DeesHint`
Hint/tooltip component for providing contextual help.
```typescript
<dees-hint
text="This field is required"
type="info" // Options: info, warning, error, success
position="top" // Options: top, bottom, left, right
></dees-hint>
```
#### `DeesPanel`
Container component for grouping related content with optional title and actions.
```typescript
<dees-panel
.title=${'Panel Title'}
.subtitle=${'Optional subtitle'}
collapsible // Optional: allow collapse/expand
collapsed={false} // Initial collapsed state
.actions=${[
{ icon: 'settings', action: handleSettings }
]}
>
<!-- Panel content -->
</dees-panel>
```
#### `DeesSearchbar`
Search input component with suggestions and search handling.
```typescript
<dees-searchbar
placeholder="Search..."
.suggestions=${['item1', 'item2', 'item3']}
showClearButton // Show clear button when has value
@search=${handleSearch}
@suggestion-select=${handleSuggestionSelect}
></dees-searchbar>
```
#### `DeesWindowcontrols`
Window control buttons (minimize, maximize, close) for desktop-like applications.
```typescript
<dees-windowcontrols
.controls=${['minimize', 'maximize', 'close']}
@minimize=${handleMinimize}
@maximize=${handleMaximize}
@close=${handleClose}
></dees-windowcontrols>
```
### Form Components ### Form Components
#### `DeesForm` #### `DeesForm`
@ -207,22 +321,6 @@ Dropdown selection component with search and filtering capabilities.
></dees-input-dropdown> ></dees-input-dropdown>
``` ```
#### `DeesInputRadio`
Radio button group for single-choice selections.
```typescript
<dees-input-radio
key="gender"
label="Gender"
.options=${[
{ key: 'male', option: 'Male' },
{ key: 'female', option: 'Female' },
{ key: 'other', option: 'Other' }
]}
required
></dees-input-radio>
```
#### `DeesInputFileupload` #### `DeesInputFileupload`
File upload component with drag-and-drop support. File upload component with drag-and-drop support.
@ -293,6 +391,193 @@ Multi-state toggle button group.
></dees-input-multitoggle> ></dees-input-multitoggle>
``` ```
#### `DeesInputRadiogroup`
Radio button group for single-choice selections with internal state management.
```typescript
<dees-input-radiogroup
key="plan"
label="Select Plan"
.options=${['Free', 'Pro', 'Enterprise']}
selectedOption="Pro"
required
@change=${handlePlanChange}
></dees-input-radiogroup>
// With custom option objects
<dees-input-radiogroup
key="priority"
label="Priority Level"
.options=${[
{ key: 'low', label: 'Low Priority' },
{ key: 'medium', label: 'Medium Priority' },
{ key: 'high', label: 'High Priority' }
]}
selectedOption="medium"
></dees-input-radiogroup>
```
#### `DeesInputTags`
Tag input component for managing lists of tags with auto-complete and validation.
```typescript
<dees-input-tags
key="skills"
label="Skills"
.value=${['JavaScript', 'TypeScript', 'CSS']}
placeholder="Add a skill..."
.suggestions=${[
'JavaScript', 'TypeScript', 'Python', 'Go', 'Rust',
'React', 'Vue', 'Angular', 'Node.js', 'Docker'
]}
maxTags={10} // Optional: limit number of tags
required
@change=${handleTagsChange}
></dees-input-tags>
```
Key Features:
- Add tags by pressing Enter or typing comma/semicolon
- Remove tags with click or backspace
- Auto-complete suggestions with keyboard navigation
- Maximum tag limit support
- Full theme support
- Form validation integration
#### `DeesInputTypelist`
Dynamic list input for managing arrays of typed values.
```typescript
<dees-input-typelist
key="features"
label="Product Features"
placeholder="Add a feature..."
.value=${['Feature 1', 'Feature 2']}
@change=${handleFeaturesChange}
></dees-input-typelist>
```
#### `DeesInputDatepicker`
Date and time picker component with calendar interface and manual typing support.
```typescript
<dees-input-datepicker
key="eventDate"
label="Event Date"
placeholder="YYYY-MM-DD"
value="2025-01-15T14:30:00Z" // ISO string format
dateFormat="YYYY-MM-DD" // Display format (default: YYYY-MM-DD)
enableTime={true} // Enable time selection
timeFormat="24h" // Options: 24h, 12h
minuteIncrement={15} // Time step in minutes
minDate="2025-01-01" // Minimum selectable date
maxDate="2025-12-31" // Maximum selectable date
.disabledDates=${[ // Array of disabled dates
'2025-01-10',
'2025-01-11'
]}
weekStartsOn={1} // 0 = Sunday, 1 = Monday
required
@change=${handleDateChange}
></dees-input-datepicker>
```
Key Features:
- Interactive calendar popup
- Manual date typing with multiple formats
- Optional time selection
- Configurable date format
- Min/max date constraints
- Disable specific dates
- Keyboard navigation
- Today button
- Clear functionality
- 12/24 hour time formats
- Theme-aware styling
- Live parsing and validation
Manual Input Formats:
```typescript
// Date formats supported
"2023-12-20" // ISO format (YYYY-MM-DD)
"20.12.2023" // European format (DD.MM.YYYY)
"12/20/2023" // US format (MM/DD/YYYY)
// Date with time (add space and time after any date format)
"2023-12-20 14:30"
"20.12.2023 9:45"
"12/20/2023 16:00"
```
The component automatically parses and validates input as you type, updating the internal date value when a valid date is recognized.
#### `DeesInputSearchselect`
Search-enabled dropdown selection component.
```typescript
<dees-input-searchselect
key="category"
label="Select Category"
placeholder="Search categories..."
.options=${[
{ key: 'tech', label: 'Technology' },
{ key: 'health', label: 'Healthcare' },
{ key: 'finance', label: 'Finance' }
]}
required
@change=${handleCategoryChange}
></dees-input-searchselect>
```
#### `DeesInputRichtext`
Rich text editor with formatting toolbar powered by TipTap.
```typescript
<dees-input-richtext
key="content"
label="Article Content"
.value=${htmlContent}
placeholder="Start writing..."
minHeight={300} // Minimum editor height
showWordCount={true} // Show word/character count
@change=${handleContentChange}
></dees-input-richtext>
```
Key Features:
- Full formatting toolbar (bold, italic, underline, strike, etc.)
- Heading levels (H1-H6)
- Lists (bullet, ordered)
- Links with URL editing
- Code blocks and inline code
- Blockquotes
- Horizontal rules
- Undo/redo support
- Word and character count
- HTML output
#### `DeesInputWysiwyg`
Advanced block-based editor with slash commands and rich content blocks.
```typescript
<dees-input-wysiwyg
key="document"
label="Document Editor"
.value=${documentContent}
outputFormat="html" // Options: html, markdown, json
@change=${handleDocumentChange}
></dees-input-wysiwyg>
```
Key Features:
- Slash commands for quick formatting
- Block-based editing (paragraphs, headings, lists, etc.)
- Drag and drop block reordering
- Multiple output formats
- Keyboard shortcuts
- Collaborative editing ready
- Extensible block types
#### `DeesFormSubmit` #### `DeesFormSubmit`
Submit button component specifically designed for `DeesForm`. Submit button component specifically designed for `DeesForm`.
@ -342,9 +627,9 @@ Base container component for application layout structure with integrated appbar
// Main menu configuration (left sidebar) // Main menu configuration (left sidebar)
.mainmenuTabs=${[ .mainmenuTabs=${[
{ key: 'dashboard', iconName: 'home', action: () => {} }, { key: 'dashboard', iconName: 'lucide:home', action: () => {} },
{ key: 'projects', iconName: 'folder', action: () => {} }, { key: 'projects', iconName: 'lucide:folder', action: () => {} },
{ key: 'settings', iconName: 'cog', action: () => {} } { key: 'settings', iconName: 'lucide:settings', action: () => {} }
]} ]}
.mainmenuSelectedTab=${selectedTab} .mainmenuSelectedTab=${selectedTab}
@ -358,7 +643,7 @@ Base container component for application layout structure with integrated appbar
// Main content tabs // Main content tabs
.maincontentTabs=${[ .maincontentTabs=${[
{ key: 'tab1', iconName: 'file', action: () => {} } { key: 'tab1', iconName: 'lucide:file', action: () => {} }
]} ]}
// Event handlers // Event handlers
@ -622,6 +907,91 @@ Best Practices:
- Test with screen readers - Test with screen readers
- Maintain focus management - Maintain focus management
#### `DeesAppuiActivitylog`
Activity log component for displaying system events and user actions.
```typescript
<dees-appui-activitylog
.entries=${[
{
timestamp: new Date(),
type: 'info',
message: 'User logged in',
details: { userId: '123' }
},
{
timestamp: new Date(),
type: 'error',
message: 'Failed to save document',
details: { error: 'Network error' }
}
]}
maxEntries={100} // Maximum entries to display
@entry-click=${handleEntryClick}
></dees-appui-activitylog>
```
#### `DeesAppuiProfiledropdown`
User profile dropdown component with status and menu options.
```typescript
<dees-appui-profiledropdown
.user=${{
name: 'John Doe',
email: 'john@example.com',
avatar: '/path/to/avatar.jpg',
status: 'online' // Options: online, offline, busy, away
}}
.menuItems=${[
{ name: 'Profile', iconName: 'user', action: async () => {} },
{ name: 'Settings', iconName: 'settings', action: async () => {} },
{ divider: true },
{ name: 'Logout', iconName: 'logOut', action: async () => {} }
]}
@status-change=${handleStatusChange}
></dees-appui-profiledropdown>
```
#### `DeesAppuiTabs`
Tab navigation component for organizing content sections.
```typescript
<dees-appui-tabs
.tabs=${[
{
key: 'overview',
label: 'Overview',
icon: 'home',
content: html`<div>Overview content</div>`
},
{
key: 'details',
label: 'Details',
icon: 'info',
content: html`<div>Details content</div>`
}
]}
selectedTab="overview"
@tab-change=${handleTabChange}
></dees-appui-tabs>
```
#### `DeesAppuiView`
View container component for consistent page layouts.
```typescript
<dees-appui-view
viewTitle="Dashboard"
viewSubtitle="System Overview"
.headerActions=${[
{ icon: 'refresh', action: handleRefresh },
{ icon: 'settings', action: handleSettings }
]}
>
<!-- View content -->
</dees-appui-view>
```
#### `DeesMobileNavigation` #### `DeesMobileNavigation`
Responsive navigation component for mobile devices. Responsive navigation component for mobile devices.
@ -647,6 +1017,100 @@ Responsive navigation component for mobile devices.
></dees-mobile-navigation> ></dees-mobile-navigation>
``` ```
#### `DeesDashboardGrid`
Drag-and-drop grid layout system for creating customizable dashboards.
```typescript
<dees-dashboardgrid
.widgets=${[
{
id: 'widget1',
x: 0, // Grid column position
y: 0, // Grid row position
w: 4, // Width in grid units
h: 3, // Height in grid units
minW: 2, // Minimum width
minH: 2, // Minimum height
maxW: 6, // Maximum width
title: 'Sales Overview',
icon: 'fa:chart-line',
content: html`<div>Widget content here</div>`,
noMove: false, // Allow moving
noResize: false // Allow resizing
},
{
id: 'widget2',
x: 4,
y: 0,
w: 4,
h: 3,
title: 'Recent Activity',
content: html`<dees-table .data=${activityData}></dees-table>`,
autoPosition: true // Auto-find position
}
]}
columns={12} // Number of grid columns
cellHeight={80} // Height of each grid cell in pixels
cellHeightUnit="px" // Options: px, em, rem, auto
margin={10} // Gap between widgets
editable={true} // Enable drag and resize
showGridLines={false} // Show grid guidelines
enableAnimation={true} // Smooth transitions
rtl={false} // Right-to-left support
@widget-move=${handleWidgetMove}
@widget-resize=${handleWidgetResize}
></dees-dashboardgrid>
// Programmatic methods
const grid = document.querySelector('dees-dashboardgrid');
// Add a new widget
grid.addWidget({
id: 'newWidget',
x: 0,
y: 0,
w: 3,
h: 2,
content: html`<div>New widget</div>`
}, true); // true = auto-position
// Remove widget
grid.removeWidget('widget1');
// Update widget
grid.updateWidget('widget2', {
title: 'Updated Title',
w: 6
});
// Get/set layout
const layout = grid.getLayout(); // Returns position data
grid.setLayout(savedLayout); // Restore positions
// Compact widgets
grid.compact('vertical'); // Or 'horizontal'
// Lock/unlock editing
grid.lockGrid();
grid.unlockGrid();
```
Key Features:
- Drag-and-drop widget repositioning
- Resize handles on edges and corners
- Grid-based layout system
- Collision detection
- Auto-positioning for new widgets
- Configurable constraints (min/max dimensions)
- Lock individual widgets or entire grid
- Compact layout algorithm
- Save/restore layout positions
- RTL layout support
- Optional grid lines for alignment
- Smooth animations
- Responsive sizing
- Empty state display
### Data Display Components ### Data Display Components
#### `DeesTable` #### `DeesTable`
@ -982,6 +1446,27 @@ setInterval(() => {
}, 3000); }, 3000);
``` ```
#### `DeesPagination`
Pagination component for navigating through large datasets.
```typescript
<dees-pagination
totalItems={500}
itemsPerPage={20}
currentPage={1}
maxVisiblePages={7} // Maximum page numbers to display
@page-change=${handlePageChange}
></dees-pagination>
```
Key Features:
- Page number navigation
- Previous/next buttons
- Jump to first/last page
- Configurable items per page
- Responsive design
- Keyboard navigation support
### Visualization Components ### Visualization Components
#### `DeesChartArea` #### `DeesChartArea`
@ -1306,52 +1791,6 @@ Key Features:
- Animation support - Animation support
- Accessibility features - Accessibility features
#### `DeesMobileNavigation`
Mobile-optimized navigation component with touch support.
```typescript
// Programmatic usage
DeesMobilenavigation.createAndShow([
{
name: 'Home',
action: async (nav) => {
// Handle navigation
return null;
}
},
{
name: 'Settings',
action: async (nav) => {
// Handle navigation
return null;
}
}
]);
// Component usage
<dees-mobilenavigation
heading="MENU"
.menuItems=${[
{
name: 'Profile',
action: (nav) => handleNavigation('profile')
},
{
name: 'Settings',
action: (nav) => handleNavigation('settings')
}
]}
></dees-mobilenavigation>
```
Key Features:
- Touch-friendly interface
- Slide-in animation
- Backdrop overlay
- Single instance management
- Custom menu items
- Responsive design
Best Practices: Best Practices:
1. Stepper Implementation 1. Stepper Implementation
@ -1368,13 +1807,6 @@ Best Practices:
- Performance monitoring - Performance monitoring
- Error state handling - Error state handling
3. Mobile Navigation
- Touch-optimized targets
- Clear visual hierarchy
- Smooth animations
- Gesture support
- Responsive behavior
Common Use Cases: Common Use Cases:
1. Stepper 1. Stepper
@ -1391,13 +1823,6 @@ Common Use Cases:
- Task completion - Task completion
- Step progression - Step progression
3. Mobile Navigation
- Responsive menus
- App navigation
- Settings access
- User actions
- Context switching
Accessibility Considerations: Accessibility Considerations:
- Keyboard navigation support - Keyboard navigation support
- ARIA labels and roles - ARIA labels and roles
@ -1461,6 +1886,26 @@ Key Features:
- Spellcheck integration - Spellcheck integration
- Auto-save functionality - Auto-save functionality
#### `DeesEditorMarkdownoutlet`
Markdown preview component for rendering markdown content.
```typescript
<dees-editor-markdownoutlet
.markdown=${markdownContent}
.theme=${'github'} // Options: github, dark, custom
.plugins=${['mermaid', 'highlight']} // Optional plugins
allowHtml={false} // Security: disable raw HTML
></dees-editor-markdownoutlet>
```
Key Features:
- Safe markdown rendering
- Multiple themes
- Plugin support (mermaid diagrams, syntax highlighting)
- XSS protection
- Custom CSS injection
- Responsive images
#### `DeesTerminal` #### `DeesTerminal`
Terminal emulator component for command-line interface. Terminal emulator component for command-line interface.
@ -1606,6 +2051,123 @@ Accessibility Features:
- Focus management - Focus management
- ARIA attributes - ARIA attributes
### Auth & Utilities Components
#### `DeesSimpleAppdash`
Simple application dashboard component for quick prototyping.
```typescript
<dees-simple-appdash
.appTitle=${'My Application'}
.menuItems=${[
{ name: 'Dashboard', icon: 'home', route: '/dashboard' },
{ name: 'Settings', icon: 'settings', route: '/settings' }
]}
.user=${{
name: 'John Doe',
role: 'Administrator'
}}
@menu-select=${handleMenuSelect}
>
<!-- Dashboard content -->
</dees-simple-appdash>
```
Key Features:
- Quick setup dashboard layout
- Built-in navigation
- User profile section
- Responsive design
- Minimal configuration
#### `DeesSimpleLogin`
Simple login form component with validation and customization.
```typescript
<dees-simple-login
.appName=${'My Application'}
.logo=${'./assets/logo.png'}
.backgroundImage=${'./assets/background.jpg'}
.fields=${['username', 'password']} // Options: username, email, password
showForgotPassword
showRememberMe
@login=${handleLogin}
@forgot-password=${handleForgotPassword}
></dees-simple-login>
```
Key Features:
- Customizable fields
- Built-in validation
- Remember me option
- Forgot password link
- Custom branding
- Responsive layout
- Loading states
### Shopping Components
#### `DeesShoppingProductcard`
Product card component for e-commerce applications.
```typescript
<dees-shopping-productcard
.productData=${{
name: 'Premium Headphones',
category: 'Electronics',
description: 'High-quality wireless headphones with noise cancellation',
price: 199.99,
originalPrice: 249.99, // Shows strikethrough price
currency: '$',
inStock: true,
stockText: 'In Stock', // Custom stock text
imageUrl: '/images/headphones.jpg',
iconName: 'lucide:headphones' // Fallback icon if no image
}}
quantity={1} // Current quantity
showQuantitySelector={true} // Show quantity selector
selectable={false} // Enable selection mode
selected={false} // Selection state
@quantityChange=${(e) => handleQuantityChange(e.detail)}
@selectionChange=${(e) => handleSelectionChange(e.detail)}
></dees-shopping-productcard>
```
Key Features:
- Product image with fallback icon
- Category label
- Product name and description
- Price display with original price strikethrough
- Stock status indicator
- Built-in quantity selector
- Selection mode for bulk operations
- Hover effects
- Responsive design
- Theme-aware styling
Product Data Interface:
```typescript
interface IProductData {
name: string;
category?: string;
description?: string;
price: number;
originalPrice?: number;
currency?: string;
inStock?: boolean;
stockText?: string;
imageUrl?: string;
iconName?: string;
}
```
Common Use Cases:
- Product listings
- Shopping carts
- Order summaries
- Product comparisons
- Wishlist displays
## License and Legal Information ## License and Legal Information
This repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the license file within this repository. This repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the license file within this repository.

784
readme.playbook.md Normal file
View File

@ -0,0 +1,784 @@
# UI Components Playbook
This playbook provides comprehensive guidance for creating and maintaining UI components in the @design.estate/dees-catalog library. Follow these patterns and best practices to ensure consistency, maintainability, and quality.
## Table of Contents
1. [Component Creation Checklist](#component-creation-checklist)
2. [Architectural Patterns](#architectural-patterns)
3. [Component Types and Base Classes](#component-types-and-base-classes)
4. [Theming System](#theming-system)
5. [Event Handling](#event-handling)
6. [State Management](#state-management)
7. [Form Components](#form-components)
8. [Overlay Components](#overlay-components)
9. [Complex Components](#complex-components)
10. [Performance Optimization](#performance-optimization)
11. [Focus Management](#focus-management)
12. [Demo System](#demo-system)
13. [Common Pitfalls and Anti-patterns](#common-pitfalls-and-anti-patterns)
14. [Code Examples](#code-examples)
## Component Creation Checklist
When creating a new component, follow this checklist:
- [ ] Choose the appropriate base class (`DeesElement` or `DeesInputBase`)
- [ ] Use `@customElement('dees-componentname')` decorator
- [ ] Implement consistent theming with `cssManager.bdTheme()`
- [ ] Create demo function in separate `.demo.ts` file
- [ ] Export component from `ts_web/elements/index.ts`
- [ ] Use proper TypeScript types and interfaces (prefix with `I` for interfaces, `T` for types)
- [ ] Implement proper event handling with bubbling and composition
- [ ] Consider mobile responsiveness
- [ ] Add focus states for accessibility
- [ ] Clean up resources in `destroy()` method
- [ ] Follow lowercase naming convention for files
- [ ] Add z-index registry support if it's an overlay component
## Architectural Patterns
### Base Component Structure
```typescript
import { customElement, property, state, css, TemplateResult, html } from '@design.estate/dees-element';
import { DeesElement } from '@design.estate/dees-element';
import * as cssManager from './00colors.js';
import * as demoFunc from './dees-componentname.demo.js';
@customElement('dees-componentname')
export class DeesComponentName extends DeesElement {
// Static demo reference
public static demo = demoFunc.demoFunc;
// Public properties (reactive, can be set via attributes)
@property({ type: String })
public label: string = '';
@property({ type: Boolean, reflect: true })
public disabled: boolean = false;
// Internal state (reactive, but not exposed as attributes)
@state()
private internalState: string = '';
// Static styles with theme support
public static styles = [
cssManager.defaultStyles,
css`
:host {
display: block;
background: ${cssManager.bdTheme('#ffffff', '#09090b')};
}
`
];
// Render method
public render(): TemplateResult {
return html`
<div class="main-container">
<!-- Component content -->
</div>
`;
}
// Lifecycle methods
public connectedCallback() {
super.connectedCallback();
// Setup that needs DOM access
}
public async firstUpdated() {
// One-time initialization after first render
}
// Cleanup
public destroy() {
// Clean up listeners, observers, registrations
super.destroy();
}
}
```
### Advanced Patterns
#### 1. Separation of Concerns (Complex Components)
For complex components like WYSIWYG editors, separate concerns into handler classes:
```typescript
export class DeesComplexComponent extends DeesElement {
// Orchestrator pattern - main component coordinates handlers
private inputHandler: InputHandler;
private stateHandler: StateHandler;
private renderHandler: RenderHandler;
constructor() {
super();
this.inputHandler = new InputHandler(this);
this.stateHandler = new StateHandler(this);
this.renderHandler = new RenderHandler(this);
}
}
```
#### 2. Singleton Pattern (Global Components)
For global UI elements like menus:
```typescript
export class DeesGlobalMenu extends DeesElement {
private static instance: DeesGlobalMenu;
public static getInstance(): DeesGlobalMenu {
if (!DeesGlobalMenu.instance) {
DeesGlobalMenu.instance = new DeesGlobalMenu();
document.body.appendChild(DeesGlobalMenu.instance);
}
return DeesGlobalMenu.instance;
}
}
```
#### 3. Registry Pattern (Z-Index Management)
Use centralized registries for global state:
```typescript
class ComponentRegistry {
private static instance: ComponentRegistry;
private registry = new WeakMap<HTMLElement, number>();
public register(element: HTMLElement, value: number) {
this.registry.set(element, value);
}
public unregister(element: HTMLElement) {
this.registry.delete(element);
}
}
```
## Component Types and Base Classes
### Standard Component (extends DeesElement)
Use for most UI components:
- Buttons, badges, icons
- Layout components
- Data display components
- Overlay components
### Form Input Component (extends DeesInputBase)
Use for all form inputs:
- Text inputs, dropdowns, checkboxes
- Date pickers, file uploads
- Rich text editors
**Required implementations:**
```typescript
export class DeesInputCustom extends DeesInputBase<ValueType> {
// Required: Get current value
public getValue(): ValueType {
return this.value;
}
// Required: Set value programmatically
public setValue(value: ValueType): void {
this.value = value;
this.changeSubject.next(this); // Notify form
}
// Optional: Custom validation
public async validate(): Promise<boolean> {
// Custom validation logic
return true;
}
}
```
## Theming System
### DO: Use Theme Functions
Always use `cssManager.bdTheme()` for colors that change between themes:
```typescript
// ✅ CORRECT
background: ${cssManager.bdTheme('#ffffff', '#09090b')};
color: ${cssManager.bdTheme('#000000', '#ffffff')};
border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#333333')};
// ❌ INCORRECT
background: #ffffff; // Hard-coded color
color: var(--custom-color); // Custom CSS variable
```
### DO: Use Consistent Color Values
Reference shared color constants when possible:
```typescript
// From 00colors.ts
background: ${cssManager.bdTheme(colors.bright.background, colors.dark.background)};
```
## Event Handling
### DO: Dispatch Custom Events Properly
```typescript
// ✅ CORRECT - Events bubble and cross shadow DOM
this.dispatchEvent(new CustomEvent('dees-componentname-change', {
detail: { value: this.value },
bubbles: true,
composed: true
}));
// ❌ INCORRECT - Event won't propagate properly
this.dispatchEvent(new CustomEvent('change', {
detail: { value: this.value }
// Missing bubbles and composed
}));
```
### DO: Use Event Delegation
For dynamic content, use event delegation:
```typescript
// ✅ CORRECT - Single listener for all items
this.addEventListener('click', (e: MouseEvent) => {
const item = (e.target as HTMLElement).closest('.item');
if (item) {
this.handleItemClick(item);
}
});
// ❌ INCORRECT - Multiple listeners
this.items.forEach(item => {
item.addEventListener('click', () => this.handleItemClick(item));
});
```
## State Management
### DO: Use Appropriate Property Decorators
```typescript
// Public API - use @property
@property({ type: String })
public label: string;
// Internal state - use @state
@state()
private isLoading: boolean = false;
// Reflect to attribute when needed
@property({ type: Boolean, reflect: true })
public disabled: boolean = false;
```
### DON'T: Manipulate State in Render
```typescript
// ❌ INCORRECT - Side effects in render
public render() {
this.counter++; // Don't modify state
return html`<div>${this.counter}</div>`;
}
// ✅ CORRECT - Pure render function
public render() {
return html`<div>${this.counter}</div>`;
}
```
## Form Components
### DO: Extend DeesInputBase
All form inputs must extend the base class:
```typescript
export class DeesInputNew extends DeesInputBase<string> {
// Inherits: key, label, value, required, disabled, validationState
}
```
### DO: Emit Changes Consistently
```typescript
private handleInput(e: Event) {
this.value = (e.target as HTMLInputElement).value;
this.changeSubject.next(this); // Notify form system
}
```
### DO: Support Standard Form Properties
```typescript
// All form inputs should support:
@property() public key: string;
@property() public label: string;
@property() public required: boolean = false;
@property() public disabled: boolean = false;
@property() public validationState: 'valid' | 'warn' | 'invalid';
```
## Overlay Components
### DO: Use Z-Index Registry
Never hardcode z-index values:
```typescript
// ✅ CORRECT
import { zIndexRegistry } from './00zindex.js';
public async show() {
this.modalZIndex = zIndexRegistry.getNextZIndex();
zIndexRegistry.register(this, this.modalZIndex);
this.style.zIndex = `${this.modalZIndex}`;
}
public async hide() {
zIndexRegistry.unregister(this);
}
// ❌ INCORRECT
public async show() {
this.style.zIndex = '9999'; // Hardcoded z-index
}
```
### DO: Use Window Layers
For modal backdrops:
```typescript
import { DeesWindowLayer } from './dees-windowlayer.js';
private windowLayer: DeesWindowLayer;
public async show() {
this.windowLayer = new DeesWindowLayer();
this.windowLayer.zIndex = zIndexRegistry.getNextZIndex();
document.body.append(this.windowLayer);
}
```
## Complex Components
### DO: Use Handler Classes
For complex logic, separate into specialized handlers:
```typescript
// wysiwyg/handlers/input.handler.ts
export class InputHandler {
constructor(private component: DeesInputWysiwyg) {}
public handleInput(event: InputEvent) {
// Specialized input handling
}
}
// Main component orchestrates
export class DeesInputWysiwyg extends DeesInputBase {
private inputHandler = new InputHandler(this);
}
```
### DO: Use Programmatic Rendering
For performance-critical updates that shouldn't trigger re-renders:
```typescript
// ✅ CORRECT - Direct DOM manipulation when needed
private updateBlockContent(blockId: string, content: string) {
const blockElement = this.shadowRoot.querySelector(`#${blockId}`);
if (blockElement) {
blockElement.textContent = content; // Direct update
}
}
// ❌ INCORRECT - Triggering full re-render
private updateBlockContent(blockId: string, content: string) {
this.blocks.find(b => b.id === blockId).content = content;
this.requestUpdate(); // Unnecessary re-render
}
```
## Performance Optimization
### DO: Debounce Expensive Operations
```typescript
private resizeTimeout: number;
private handleResize = () => {
clearTimeout(this.resizeTimeout);
this.resizeTimeout = window.setTimeout(() => {
this.updateLayout();
}, 250);
};
```
### DO: Use Observers Efficiently
```typescript
// Clean up observers
public disconnectedCallback() {
super.disconnectedCallback();
this.resizeObserver?.disconnect();
this.mutationObserver?.disconnect();
}
```
### DO: Implement Virtual Scrolling
For large lists:
```typescript
// Only render visible items
private getVisibleItems() {
const scrollTop = this.scrollContainer.scrollTop;
const containerHeight = this.scrollContainer.clientHeight;
const itemHeight = 50;
const startIndex = Math.floor(scrollTop / itemHeight);
const endIndex = Math.ceil((scrollTop + containerHeight) / itemHeight);
return this.items.slice(startIndex, endIndex);
}
```
## Focus Management
### DO: Handle Focus Timing
```typescript
// ✅ CORRECT - Wait for render
async focusInput() {
await this.updateComplete;
await new Promise(resolve => requestAnimationFrame(resolve));
this.inputElement?.focus();
}
// ❌ INCORRECT - Focus too early
focusInput() {
this.inputElement?.focus(); // Element might not exist
}
```
### DO: Prevent Focus Loss
```typescript
// For global menus
constructor() {
super();
// Prevent focus loss when clicking menu
this.addEventListener('mousedown', (e) => {
e.preventDefault();
});
}
```
### DO: Implement Blur Debouncing
```typescript
private blurTimeout: number;
private handleBlur = () => {
clearTimeout(this.blurTimeout);
this.blurTimeout = window.setTimeout(() => {
// Check if truly blurred
if (!this.contains(document.activeElement)) {
this.handleTrueBlur();
}
}, 100);
};
```
## Demo System
### DO: Create Comprehensive Demos
Every component needs a demo:
```typescript
// dees-button.demo.ts
import { html } from '@design.estate/dees-element';
export const demoFunc = () => html`
<dees-button>Default Button</dees-button>
<dees-button type="primary">Primary Button</dees-button>
<dees-button type="danger" disabled>Disabled Danger</dees-button>
`;
// In component file
import * as demoFunc from './dees-button.demo.js';
export class DeesButton extends DeesElement {
public static demo = demoFunc.demoFunc;
}
```
### DO: Include All Variants
Show all component states and variations in demos:
- Default state
- Different types/variants
- Disabled state
- Loading state
- Error states
- Edge cases (long text, empty content)
## Common Pitfalls and Anti-patterns
### ❌ DON'T: Hardcode Z-Index Values
```typescript
// ❌ WRONG
this.style.zIndex = '9999';
// ✅ CORRECT
this.style.zIndex = `${zIndexRegistry.getNextZIndex()}`;
```
### ❌ DON'T: Skip Base Classes
```typescript
// ❌ WRONG - Form input without base class
export class DeesInputCustom extends DeesElement {
// Missing standard form functionality
}
// ✅ CORRECT
export class DeesInputCustom extends DeesInputBase<string> {
// Inherits all form functionality
}
```
### ❌ DON'T: Forget Theme Support
```typescript
// ❌ WRONG
background-color: #ffffff;
color: #000000;
// ✅ CORRECT
background-color: ${cssManager.bdTheme('#ffffff', '#09090b')};
color: ${cssManager.bdTheme('#000000', '#ffffff')};
```
### ❌ DON'T: Create Components Without Demos
```typescript
// ❌ WRONG
export class DeesComponent extends DeesElement {
// No demo property
}
// ✅ CORRECT
export class DeesComponent extends DeesElement {
public static demo = demoFunc.demoFunc;
}
```
### ❌ DON'T: Emit Non-Bubbling Events
```typescript
// ❌ WRONG
this.dispatchEvent(new CustomEvent('change', {
detail: this.value
}));
// ✅ CORRECT
this.dispatchEvent(new CustomEvent('change', {
detail: this.value,
bubbles: true,
composed: true
}));
```
### ❌ DON'T: Skip Cleanup
```typescript
// ❌ WRONG
public connectedCallback() {
window.addEventListener('resize', this.handleResize);
}
// ✅ CORRECT
public connectedCallback() {
super.connectedCallback();
window.addEventListener('resize', this.handleResize);
}
public disconnectedCallback() {
super.disconnectedCallback();
window.removeEventListener('resize', this.handleResize);
}
```
### ❌ DON'T: Use Inline Styles for Theming
```typescript
// ❌ WRONG
<div style="background-color: ${this.darkMode ? '#000' : '#fff'}">
// ✅ CORRECT
<div class="themed-container">
// In styles:
.themed-container {
background-color: ${cssManager.bdTheme('#ffffff', '#000000')};
}
```
### ❌ DON'T: Forget Mobile Responsiveness
```typescript
// ❌ WRONG
:host {
width: 800px; // Fixed width
}
// ✅ CORRECT
:host {
width: 100%;
max-width: 800px;
}
@media (max-width: 768px) {
:host {
/* Mobile adjustments */
}
}
```
## Code Examples
### Example: Creating a New Button Variant
```typescript
// dees-special-button.ts
import { customElement, property, css, html } from '@design.estate/dees-element';
import { DeesElement } from '@design.estate/dees-element';
import * as cssManager from './00colors.js';
import * as demoFunc from './dees-special-button.demo.js';
@customElement('dees-special-button')
export class DeesSpecialButton extends DeesElement {
public static demo = demoFunc.demoFunc;
@property({ type: String })
public text: string = 'Click me';
@property({ type: Boolean, reflect: true })
public loading: boolean = false;
public static styles = [
cssManager.defaultStyles,
css`
:host {
display: inline-block;
}
.button {
padding: 8px 16px;
background: ${cssManager.bdTheme('#0066ff', '#0044cc')};
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
transition: all 0.2s;
}
.button:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px ${cssManager.bdTheme('rgba(0,0,0,0.1)', 'rgba(0,0,0,0.3)')};
}
:host([loading]) .button {
opacity: 0.7;
cursor: not-allowed;
}
`
];
public render() {
return html`
<button class="button" ?disabled=${this.loading} @click=${this.handleClick}>
${this.loading ? html`<dees-spinner size="small"></dees-spinner>` : this.text}
</button>
`;
}
private handleClick() {
this.dispatchEvent(new CustomEvent('special-click', {
bubbles: true,
composed: true
}));
}
}
```
### Example: Creating a Form Input
```typescript
// dees-input-special.ts
export class DeesInputSpecial extends DeesInputBase<string> {
public static demo = demoFunc.demoFunc;
public render() {
return html`
<dees-label .label=${this.label} .required=${this.required}>
<input
type="text"
.value=${this.value || ''}
?disabled=${this.disabled}
@input=${this.handleInput}
@blur=${this.handleBlur}
/>
</dees-label>
`;
}
private handleInput(e: Event) {
this.value = (e.target as HTMLInputElement).value;
this.changeSubject.next(this);
}
private handleBlur() {
this.dispatchEvent(new CustomEvent('blur', {
bubbles: true,
composed: true
}));
}
public getValue(): string {
return this.value;
}
public setValue(value: string): void {
this.value = value;
this.changeSubject.next(this);
}
}
```
## Summary
This playbook represents the collective wisdom and patterns found in the @design.estate/dees-catalog component library. Following these guidelines will help you create components that are:
- **Consistent**: Following established patterns
- **Maintainable**: Easy to understand and modify
- **Performant**: Optimized for real-world use
- **Accessible**: Usable by everyone
- **Theme-aware**: Supporting light and dark modes
- **Well-integrated**: Working seamlessly with the component ecosystem
Remember: When in doubt, look at existing components for examples. The codebase itself is the best documentation of these patterns in action.

View File

@ -1,138 +0,0 @@
# WYSIWYG Editor Refactoring Progress Summary
## Latest Updates
### Selection Highlighting Fix ✅
- **Issue**: "Paragraphs are not highlighted consistently, headings are always highlighted"
- **Root Cause**: The `shouldUpdate` method in `dees-wysiwyg-block.ts` was using a generic `.block` selector that would match the first element with that class, not necessarily the correct block element
- **Solution**: Changed the selector to be more specific: `.block.${blockType}` which ensures the correct element is found for each block type
- **Result**: All block types now highlight consistently when selected
### Enter Key Block Creation Fix ✅
- **Issue**: "When pressing enter and jumping to new block then typing something: The cursor is not at the beginning of the new block and there is content"
- **Root Cause**: Block handlers were rendering content with template syntax `${block.content || ''}` in their render methods, which violates the static HTML principle
- **Solution**:
- Removed all `${block.content}` from render methods in paragraph, heading, quote, and code block handlers
- Content is now set programmatically in the setup() method only when needed
- Fixed `setCursorToStart` and `setCursorToEnd` to always find elements fresh instead of relying on cached `blockElement`
- **Result**: New empty blocks remain truly empty, cursor positioning works correctly
### Backspace Key Deletion Fix ✅
- **Issue**: "After typing in a new block, pressing backspace deletes the whole block instead of just the last character"
- **Root Cause**:
1. `getCursorPositionInElement` was using `element.contains()` which doesn't work across Shadow DOM boundaries
2. The backspace handler was checking `block.content === ''` which only contains the stored content, not the actual DOM content
- **Solution**:
1. Fixed `getCursorPositionInElement` to use `containsAcrossShadowDOM` for proper Shadow DOM support
2. Updated backspace handler to get actual content from DOM using `blockComponent.getContent()` instead of relying on stored `block.content`
3. Added debug logging to track cursor position and content state
- **Result**: Backspace now correctly deletes individual characters instead of the whole block
### Arrow Left Navigation Fix ✅
- **Issue**: "When jumping to the previous block from the beginning of a block with arrow left, the cursor should be at the end of the previous block, not at the start"
- **Root Cause**: Browser's default focus behavior places cursor at the beginning of contenteditable elements, overriding our cursor positioning
- **Solution**: For 'end' position, set up the selection range BEFORE focusing the element:
1. Create a range pointing to the end of content
2. Apply the selection
3. Then focus the element (which preserves the existing selection)
4. Only use setCursorToEnd for empty blocks
- **Result**: Arrow left navigation now correctly places cursor at the end of the previous block
## Completed Phases
### Phase 1: Infrastructure ✅
- Created modular block handler architecture
- Implemented `IBlockHandler` interface and `BaseBlockHandler` class
- Created `BlockRegistry` for dynamic block type registration
- Set up proper file structure under `blocks/` directory
### Phase 2: Proof of Concept ✅
- Successfully migrated divider block as the simplest example
- Validated the architecture works correctly
- Established patterns for block migration
### Phase 3: Text Blocks ✅
- **Paragraph Block**: Full editing support with text splitting, selection handling, and cursor tracking
- **Heading Blocks**: All three heading levels (h1, h2, h3) with unified handler
- **Quote Block**: Italic styling with border, full editing capabilities
- **Code Block**: Monospace font, tab handling, plain text paste support
- **List Block**: Bullet/numbered lists with proper list item management
## Key Achievements
### 1. Preserved Critical Knowledge
- **Static Rendering**: Blocks use `innerHTML` in `firstUpdated` to prevent focus loss during typing
- **Shadow DOM Selection**: Implemented `containsAcrossShadowDOM` utility for proper selection detection
- **Cursor Position Tracking**: All editable blocks track cursor position across multiple events
- **Content Splitting**: HTML-aware splitting using Range API preserves formatting
- **Focus Management**: Microtask-based focus restoration ensures reliable cursor placement
### 2. Enhanced Architecture
- Each block type is now self-contained in its own file
- Block handlers are dynamically registered and loaded
- Common functionality is shared through base classes
- Styles are co-located with their block handlers
### 3. Maintained Functionality
- All keyboard navigation works (arrows, backspace, delete, enter)
- Text selection across Shadow DOM boundaries functions correctly
- Block merging and splitting behave as before
- IME (Input Method Editor) support is preserved
- Formatting shortcuts (Cmd/Ctrl+B/I/U/K) continue to work
## Code Organization
```
ts_web/elements/wysiwyg/
├── dees-wysiwyg-block.ts (simplified main component)
├── wysiwyg.selection.ts (Shadow DOM selection utilities)
├── wysiwyg.blockregistration.ts (handler registration)
└── blocks/
├── index.ts (exports and registry)
├── block.base.ts (base handler interface)
├── decorative/
│ └── divider.block.ts
└── text/
├── paragraph.block.ts
├── heading.block.ts
├── quote.block.ts
├── code.block.ts
└── list.block.ts
```
## Next Steps
### Phase 4: Media Blocks (In Progress)
- Image block with upload/drag-drop support
- YouTube block with video embedding
- Attachment block for file uploads
### Phase 5: Content Blocks
- Markdown block with preview toggle
- HTML block with raw HTML editing
### Phase 6: Cleanup
- Remove old code from main component
- Optimize bundle size
- Update documentation
## Technical Improvements
1. **Modularity**: Each block type is now completely self-contained
2. **Extensibility**: New blocks can be added by creating a handler and registering it
3. **Maintainability**: Files are smaller and focused on single responsibilities
4. **Type Safety**: Strong TypeScript interfaces ensure consistent implementation
5. **Performance**: No degradation in performance; potential for lazy loading in future
## Migration Pattern
For future block migrations, follow this pattern:
1. Create block handler extending `BaseBlockHandler`
2. Implement required methods: `render()`, `setup()`, `getStyles()`
3. Add helper methods for cursor/content management
4. Handle Shadow DOM selection properly using utilities
5. Register handler in `wysiwyg.blockregistration.ts`
6. Test all interactions (typing, selection, navigation)
The refactoring has been successful in making the codebase more maintainable while preserving all the hard-won functionality and edge case handling from the original implementation.

View File

@ -1,82 +0,0 @@
# WYSIWYG Editor Refactoring
## Summary of Changes
This refactoring cleaned up the wysiwyg editor implementation to fix focus, cursor position, and selection issues.
### Phase 1: Code Organization
#### 1. Removed Duplicate Code
- Removed duplicate `handleBlockInput` method from main component (was already in inputHandler)
- Removed duplicate `handleBlockKeyDown` method from main component (was already in keyboardHandler)
- Consolidated all input handling in the respective handler classes
#### 2. Simplified Focus Management
- Removed complex `updated` lifecycle method that was trying to maintain focus
- Simplified `handleBlockBlur` to not immediately close menus
- Added `requestAnimationFrame` to focus operations for better timing
- Removed `slashMenuOpenTime` tracking which was no longer needed
#### 3. Fixed Slash Menu Behavior
- Changed from `@mousedown` to `@click` events for better UX
- Added proper event prevention to avoid focus loss
- Menu now closes when clicking outside
- Simplified the insertBlock method to close menu first
### Phase 2: Cursor & Selection Fixes
#### 4. Enhanced Cursor Position Management
- Added `focusWithCursor()` method to block component for precise cursor positioning
- Improved `handleSlashCommand` to preserve cursor position when menu opens
- Added `getCaretCoordinates()` for accurate menu positioning based on cursor location
- Updated `focusBlock()` to support numeric cursor positions
#### 5. Fixed Selection Across Shadow DOM
- Added custom `block-text-selected` event to communicate selections across shadow boundaries
- Implemented `handleMouseUp()` in block component to detect selections
- Updated main component to listen for selection events from blocks
- Selection now works properly even with nested shadow DOMs
#### 6. Improved Slash Menu Close Behavior
- Added optional `clearSlash` parameter to `closeSlashMenu()`
- Escape key now properly clears the slash command
- Clicking outside clears the slash if menu is open
- Selecting an item preserves content and just transforms the block
### Technical Improvements
#### Block Component (`dees-wysiwyg-block`)
- Better focus management with immediate focus (removed unnecessary requestAnimationFrame)
- Added cursor position control methods
- Custom event dispatching for cross-shadow-DOM communication
- Improved content handling for different block types
#### Input Handler
- Preserves cursor position when showing slash menu
- Better caret coordinate calculation for menu positioning
- Ensures focus stays in the block when menu appears
#### Block Operations
- Enhanced `focusBlock()` to support start/end/numeric positions
- Better timing with requestAnimationFrame for focus operations
### Key Benefits
- Slash menu no longer causes focus or cursor position loss
- Text selection works properly across shadow DOM boundaries
- Cursor position is preserved when interacting with menus
- Cleaner, more maintainable code structure
- Better separation of concerns
## Testing
Use the test files in `.nogit/debug/`:
- `test-slash-menu.html` - Tests slash menu focus behavior
- `test-wysiwyg-formatting.html` - Tests text formatting
## Known Issues Fixed
- Slash menu disappearing immediately on first "/"
- Focus lost when slash menu opens
- Cursor position lost when typing "/"
- Text selection not working properly
- Selection events not propagating across shadow DOM
- Duplicate event handling causing conflicts

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,172 @@
import { expect, tap } from '@git.zone/tstest/tapbundle';
import { DeesInputWysiwyg } from '../ts_web/elements/wysiwyg/dees-input-wysiwyg.js';
// Initialize the element
DeesInputWysiwyg;
tap.test('wysiwyg drag and drop should work correctly', async () => {
const element = document.createElement('dees-input-wysiwyg');
document.body.appendChild(element);
// Wait for element to be ready
await element.updateComplete;
// Set initial content with multiple blocks
element.blocks = [
{ id: 'block1', type: 'paragraph', content: 'First paragraph' },
{ id: 'block2', type: 'heading-2', content: 'Test Heading' },
{ id: 'block3', type: 'paragraph', content: 'Second paragraph' },
];
element.renderBlocksProgrammatically();
await element.updateComplete;
// Check that blocks are rendered
const editorContent = element.shadowRoot!.querySelector('.editor-content') as HTMLDivElement;
expect(editorContent).toBeTruthy();
const blockWrappers = editorContent.querySelectorAll('.block-wrapper');
expect(blockWrappers.length).toEqual(3);
// Test drag handles exist for non-divider blocks
const dragHandles = editorContent.querySelectorAll('.drag-handle');
expect(dragHandles.length).toEqual(3);
// Get references to specific blocks
const firstBlock = editorContent.querySelector('[data-block-id="block1"]') as HTMLElement;
const secondBlock = editorContent.querySelector('[data-block-id="block2"]') as HTMLElement;
const firstDragHandle = firstBlock.querySelector('.drag-handle') as HTMLElement;
expect(firstBlock).toBeTruthy();
expect(secondBlock).toBeTruthy();
expect(firstDragHandle).toBeTruthy();
// Test drag initialization
console.log('Testing drag initialization...');
// Create drag event
const dragStartEvent = new DragEvent('dragstart', {
dataTransfer: new DataTransfer(),
clientY: 100,
bubbles: true
});
// Simulate drag start
firstDragHandle.dispatchEvent(dragStartEvent);
// Check that drag state is initialized
expect(element.dragDropHandler.dragState.draggedBlockId).toEqual('block1');
// Check that dragging class is applied
await new Promise(resolve => setTimeout(resolve, 20)); // Wait for setTimeout in drag start
expect(firstBlock.classList.contains('dragging')).toBeTrue();
expect(editorContent.classList.contains('dragging')).toBeTrue();
// Test drop indicator creation
const dropIndicator = editorContent.querySelector('.drop-indicator');
expect(dropIndicator).toBeTruthy();
// Simulate drag over
const dragOverEvent = new DragEvent('dragover', {
dataTransfer: new DataTransfer(),
clientY: 200,
bubbles: true,
cancelable: true
});
document.dispatchEvent(dragOverEvent);
// Check that blocks move out of the way
console.log('Checking block movements...');
const blocks = Array.from(editorContent.querySelectorAll('.block-wrapper'));
const hasMovedBlocks = blocks.some(block =>
block.classList.contains('move-up') || block.classList.contains('move-down')
);
console.log('Blocks with move classes:', blocks.filter(block =>
block.classList.contains('move-up') || block.classList.contains('move-down')
).length);
// Test drag end
const dragEndEvent = new DragEvent('dragend', {
bubbles: true
});
document.dispatchEvent(dragEndEvent);
// Wait for cleanup
await new Promise(resolve => setTimeout(resolve, 150));
// Check that drag state is cleaned up
expect(element.dragDropHandler.dragState.draggedBlockId).toBeNull();
expect(firstBlock.classList.contains('dragging')).toBeFalse();
expect(editorContent.classList.contains('dragging')).toBeFalse();
// Check that drop indicator is removed
const dropIndicatorAfter = editorContent.querySelector('.drop-indicator');
expect(dropIndicatorAfter).toBeFalsy();
// Clean up
document.body.removeChild(element);
});
tap.test('wysiwyg drag and drop visual feedback', async () => {
const element = document.createElement('dees-input-wysiwyg');
document.body.appendChild(element);
await element.updateComplete;
// Set initial content
element.blocks = [
{ id: 'block1', type: 'paragraph', content: 'Block 1' },
{ id: 'block2', type: 'paragraph', content: 'Block 2' },
{ id: 'block3', type: 'paragraph', content: 'Block 3' },
];
element.renderBlocksProgrammatically();
await element.updateComplete;
const editorContent = element.shadowRoot!.querySelector('.editor-content') as HTMLDivElement;
const block1 = editorContent.querySelector('[data-block-id="block1"]') as HTMLElement;
const dragHandle1 = block1.querySelector('.drag-handle') as HTMLElement;
// Start dragging block 1
const dragStartEvent = new DragEvent('dragstart', {
dataTransfer: new DataTransfer(),
clientY: 50,
bubbles: true
});
dragHandle1.dispatchEvent(dragStartEvent);
// Wait for dragging class
await new Promise(resolve => setTimeout(resolve, 20));
// Simulate dragging down
const dragOverEvent = new DragEvent('dragover', {
dataTransfer: new DataTransfer(),
clientY: 150, // Move down past block 2
bubbles: true,
cancelable: true
});
// Trigger the global drag over handler
element.dragDropHandler['handleGlobalDragOver'](dragOverEvent);
// Check that transform is applied to dragged block
const transform = block1.style.transform;
console.log('Dragged block transform:', transform);
expect(transform).toContain('translateY');
// Check drop indicator position
const dropIndicator = editorContent.querySelector('.drop-indicator') as HTMLElement;
if (dropIndicator) {
const indicatorStyle = dropIndicator.style;
console.log('Drop indicator position:', indicatorStyle.top, 'display:', indicatorStyle.display);
}
// Clean up
document.body.removeChild(element);
});
tap.start();

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,53 @@
import { unsafeCSS } from '@design.estate/dees-element';
/**
* Geist Sans font family - Main font for the design system
* Already available in the environment, no need to load
*/
export const geistSansFont = 'Geist Sans';
/**
* Intel One Mono font family - Monospace font for code and technical content
* Already available in the environment, no need to load
*/
export const intelOneMonoFont = 'Intel One Mono';
/**
* Complete font family stacks with fallbacks
*/
export const geistFontFamily = `'${geistSansFont}', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif`;
export const monoFontFamily = `'${intelOneMonoFont}', 'SF Mono', 'Monaco', 'Inconsolata', 'Fira Code', 'Fira Mono', 'Droid Sans Mono', 'Courier New', monospace`;
/**
* CSS-ready font family values using unsafeCSS
* Use these in component styles
*/
export const cssGeistFontFamily = unsafeCSS(geistFontFamily);
export const cssMonoFontFamily = unsafeCSS(monoFontFamily);
/**
* Cal Sans font for headings - Display font
* May need to be loaded separately
*/
export const calSansFont = 'Cal Sans';
export const calSansFontFamily = `'${calSansFont}', ${geistFontFamily}`;
export const cssCalSansFontFamily = unsafeCSS(calSansFontFamily);
/**
* Roboto Slab font for special content - Serif font
* May need to be loaded separately
*/
export const robotoSlabFont = 'Roboto Slab';
export const robotoSlabFontFamily = `'${robotoSlabFont}', Georgia, serif`;
export const cssRobotoSlabFontFamily = unsafeCSS(robotoSlabFontFamily);
/**
* Base font styles that can be applied to components
*/
export const baseFontStyles = unsafeCSS(`
font-family: ${geistFontFamily};
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
font-feature-settings: 'cv11', 'tnum', 'cv05' 1;
`);

View File

@ -56,4 +56,106 @@ export const componentZIndex = {
'dees-mobilenavigation': zIndexLayers.fixed.mobileNav, 'dees-mobilenavigation': zIndexLayers.fixed.mobileNav,
'dees-slash-menu': zIndexLayers.wysiwygMenus, 'dees-slash-menu': zIndexLayers.wysiwygMenus,
'dees-formatting-menu': zIndexLayers.wysiwygMenus, 'dees-formatting-menu': zIndexLayers.wysiwygMenus,
} as const; } as const;
/**
* Z-Index Registry for managing stacked elements
* Simple incremental z-index assignment based on creation order
*/
export class ZIndexRegistry {
private static instance: ZIndexRegistry;
private activeElements = new Set<HTMLElement>();
private elementZIndexMap = new WeakMap<HTMLElement, number>();
private currentZIndex = 1000; // Starting z-index
private constructor() {}
public static getInstance(): ZIndexRegistry {
if (!ZIndexRegistry.instance) {
ZIndexRegistry.instance = new ZIndexRegistry();
}
return ZIndexRegistry.instance;
}
/**
* Get the next available z-index
* @returns The next available z-index
*/
public getNextZIndex(): number {
this.currentZIndex += 10;
return this.currentZIndex;
}
/**
* Register an element with the z-index registry
* @param element - The HTML element to register
* @param zIndex - The z-index assigned to this element
*/
public register(element: HTMLElement, zIndex: number): void {
this.activeElements.add(element);
this.elementZIndexMap.set(element, zIndex);
}
/**
* Unregister an element from the z-index registry
* @param element - The HTML element to unregister
*/
public unregister(element: HTMLElement): void {
this.activeElements.delete(element);
this.elementZIndexMap.delete(element);
// If no more active elements, reset counter to base
if (this.activeElements.size === 0) {
this.currentZIndex = 1000;
}
}
/**
* Get the z-index for a specific element
* @param element - The HTML element
* @returns The z-index or undefined if not registered
*/
public getElementZIndex(element: HTMLElement): number | undefined {
return this.elementZIndexMap.get(element);
}
/**
* Get count of active elements
* @returns Number of active elements
*/
public getActiveCount(): number {
return this.activeElements.size;
}
/**
* Get the current highest z-index
* @returns The current z-index value
*/
public getCurrentZIndex(): number {
return this.currentZIndex;
}
/**
* Clear all registrations (useful for testing)
*/
public clear(): void {
this.activeElements.clear();
this.elementZIndexMap = new WeakMap();
this.currentZIndex = 1000;
}
/**
* Get all active elements in z-index order
* @returns Array of elements sorted by z-index
*/
public getActiveElementsInOrder(): HTMLElement[] {
return Array.from(this.activeElements).sort((a, b) => {
const aZ = this.elementZIndexMap.get(a) || 0;
const bZ = this.elementZIndexMap.get(b) || 0;
return aZ - bZ;
});
}
}
// Export singleton instance for convenience
export const zIndexRegistry = ZIndexRegistry.getInstance();

View File

@ -11,27 +11,46 @@ import {
import * as domtools from '@design.estate/dees-domtools'; import * as domtools from '@design.estate/dees-domtools';
import { DeesContextmenu } from './dees-contextmenu.js'; import { DeesContextmenu } from './dees-contextmenu.js';
import './dees-icon.js';
@customElement('dees-appui-activitylog') @customElement('dees-appui-activitylog')
export class DeesAppuiActivitylog extends DeesElement { export class DeesAppuiActivitylog extends DeesElement {
// STATIC // STATIC
public static demo = () => html`<dees-appui-activitylog></dees-appui-activitylog>`; public static demo = () => html`
<style>
.demo-container {
display: flex;
justify-content: center;
align-items: center;
height: 600px;
background: ${cssManager.bdTheme('#f4f4f5', '#09090b')};
padding: 32px;
}
</style>
<div class="demo-container">
<dees-appui-activitylog></dees-appui-activitylog>
</div>
`;
// INSTANCE // INSTANCE
public static styles = [ public static styles = [
cssManager.defaultStyles, cssManager.defaultStyles,
css` css`
:host { :host {
color: ${cssManager.bdTheme('#333', '#fff')}; color: ${cssManager.bdTheme('#09090b', '#fafafa')};
position: relative; position: relative;
display: block; display: block;
width: 100%; width: 100%;
max-width: 300px; max-width: 320px;
height: 100%; height: 100%;
background: ${cssManager.bdTheme('#f8f8f8', '#111c28')}; background: ${cssManager.bdTheme('#fafafa', '#0a0a0a')};
font-family: 'Intel One Mono', sans-serif; font-family: 'Geist Mono', monospace;
border-left: 1px solid ${cssManager.bdTheme('#e0e0e0', '#202020')}; border-left: 1px solid ${cssManager.bdTheme('#e5e7eb', '#27272a')};
cursor: default; cursor: default;
box-shadow: ${cssManager.bdTheme(
'-4px 0 12px rgba(0, 0, 0, 0.02)',
'-4px 0 12px rgba(0, 0, 0, 0.2)'
)};
} }
.maincontainer { .maincontainer {
position: absolute; position: absolute;
@ -44,108 +63,265 @@ export class DeesAppuiActivitylog extends DeesElement {
.topbar { .topbar {
position: absolute; position: absolute;
top: 0px; top: 0px;
height: 32px; height: 40px;
width: 100%; width: 100%;
padding: 0px 12px 0px 12px; padding: 0px 16px;
background: ${cssManager.bdTheme('#ffffff', '#0e151f')}; background: ${cssManager.bdTheme('#ffffff', '#09090b')};
border-bottom: 1px solid ${cssManager.bdTheme('#e0e0e0', '#202020')}; border-bottom: 1px solid ${cssManager.bdTheme('#e5e7eb', '#27272a')};
display: flex;
align-items: center;
} }
.topbar .heading { .topbar .heading {
text-align: left; font-weight: 600;
line-height: 24px;
padding-top: 8px;
font-weight: 500;
font-size: 14px; font-size: 14px;
font-family: 'Geist Sans', sans-serif; font-family: 'Geist Sans', sans-serif;
color: ${cssManager.bdTheme('#666', '#ccc')}; color: ${cssManager.bdTheme('#09090b', '#fafafa')};
} }
.activityContainer { .activityContainer {
position: absolute; position: absolute;
top: 32px; top: 40px;
bottom: 40px; bottom: 48px;
width: 100%; width: 100%;
padding: 8px 0px; padding: 12px 0px;
overflow-y: scroll; overflow-y: auto;
scrollbar-width: thin;
scrollbar-color: ${cssManager.bdTheme('#e5e7eb', '#27272a')} transparent;
}
.activityContainer::-webkit-scrollbar {
width: 6px;
}
.activityContainer::-webkit-scrollbar-track {
background: transparent;
}
.activityContainer::-webkit-scrollbar-thumb {
background: ${cssManager.bdTheme('#e5e7eb', '#27272a')};
border-radius: 3px;
}
.activityContainer::-webkit-scrollbar-thumb:hover {
background: ${cssManager.bdTheme('#d4d4d8', '#3f3f46')};
} }
.streamingIndicator { .streamingIndicator {
font-size: 12px; font-size: 11px;
text-align: center; text-align: center;
padding-top: 16px; padding: 16px;
color: ${cssManager.bdTheme('#666', '#888')} color: ${cssManager.bdTheme('#71717a', '#71717a')};
font-family: 'Geist Sans', sans-serif;
text-transform: uppercase;
letter-spacing: 0.05em;
font-weight: 500;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
.streamingIndicator::before {
content: '';
width: 6px;
height: 6px;
background: ${cssManager.bdTheme('#3b82f6', '#3b82f6')};
border-radius: 50%;
animation: pulse 2s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% { opacity: 0.4; transform: scale(0.8); }
50% { opacity: 1; transform: scale(1.2); }
} }
.streamingIndicator.bottom { .streamingIndicator.bottom {
padding-top: 0px; padding-top: 8px;
padding-bottom: 16px; padding-bottom: 16px;
} }
.activityentry { .activityentry {
min-height: 30px; min-height: 36px;
font-size: 12px; font-size: 13px;
padding: 8px 16px; padding: 10px 16px;
border-bottom: 1px dotted ${cssManager.bdTheme('#00000020', '#ffffff20')}; border-bottom: 1px solid ${cssManager.bdTheme('#f4f4f5', '#18181b')};
transition: all 0.15s ease;
display: flex;
align-items: center;
gap: 8px;
line-height: 1.4;
animation: fadeIn 0.3s ease-out;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(-4px);
}
to {
opacity: 1;
transform: translateY(0);
}
} }
.activityentry:last-of-type { .activityentry:last-of-type {
border-bottom: 1px solid transparent; border-bottom: none;
} }
.activityentry:hover { .activityentry:hover {
background: ${cssManager.bdTheme('#00000005', '#00000080')}; background: ${cssManager.bdTheme('#f4f4f5', '#18181b')};
} }
.timestamp { .timestamp {
color: ${cssManager.bdTheme('#e57373', '#ff8787')}; color: ${cssManager.bdTheme('#71717a', '#71717a')};
font-weight: 500;
font-size: 12px;
font-variant-numeric: tabular-nums;
flex-shrink: 0;
min-width: 45px;
}
.activity-icon {
width: 28px;
height: 28px;
border-radius: 6px;
background: ${cssManager.bdTheme('#f4f4f5', '#18181b')};
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
font-size: 14px;
}
.activity-icon.login {
background: ${cssManager.bdTheme('rgba(34, 197, 94, 0.1)', 'rgba(34, 197, 94, 0.1)')};
color: ${cssManager.bdTheme('#16a34a', '#22c55e')};
}
.activity-icon.logout {
background: ${cssManager.bdTheme('rgba(239, 68, 68, 0.1)', 'rgba(239, 68, 68, 0.1)')};
color: ${cssManager.bdTheme('#dc2626', '#ef4444')};
}
.activity-icon.view {
background: ${cssManager.bdTheme('rgba(59, 130, 246, 0.1)', 'rgba(59, 130, 246, 0.1)')};
color: ${cssManager.bdTheme('#2563eb', '#3b82f6')};
}
.activity-icon.create {
background: ${cssManager.bdTheme('rgba(168, 85, 247, 0.1)', 'rgba(168, 85, 247, 0.1)')};
color: ${cssManager.bdTheme('#9333ea', '#a855f7')};
}
.activity-icon.update {
background: ${cssManager.bdTheme('rgba(251, 146, 60, 0.1)', 'rgba(251, 146, 60, 0.1)')};
color: ${cssManager.bdTheme('#ea580c', '#fb923c')};
}
.activity-text {
flex: 1;
color: ${cssManager.bdTheme('#18181b', '#e4e4e7')};
}
.activity-user {
font-weight: 600;
color: ${cssManager.bdTheme('#09090b', '#fafafa')};
}
.date-separator {
padding: 12px 16px 8px;
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
color: ${cssManager.bdTheme('#71717a', '#71717a')};
background: ${cssManager.bdTheme('#f9fafb', '#09090b')};
border-bottom: 1px solid ${cssManager.bdTheme('#f4f4f5', '#18181b')};
position: sticky;
top: 0;
z-index: 1;
} }
.searchbox { .searchbox {
position: absolute; position: absolute;
bottom: 0px; bottom: 0px;
width: 100%; width: 100%;
height: 40px; height: 48px;
background: ${cssManager.bdTheme('#ffffff', '#0e151f')}; background: ${cssManager.bdTheme('#ffffff', '#09090b')};
border-top: 1px solid ${cssManager.bdTheme('#e0e0e0', '#202020')}; border-top: 1px solid ${cssManager.bdTheme('#e5e7eb', '#27272a')};
padding: 8px;
} }
.searchbox input {
color: ${cssManager.bdTheme('#333', '#fff')}; .search-wrapper {
background: none; position: relative;
width: 100%; width: 100%;
height: 40px; height: 32px;
line-height: 32px; }
border: none;
padding: 4px 12px; .search-icon {
font-family: 'Intel One Mono', sans-serif; position: absolute;
left: 10px;
top: 50%;
transform: translateY(-50%);
color: ${cssManager.bdTheme('#71717a', '#71717a')};
font-size: 14px;
pointer-events: none;
transition: color 0.15s ease;
}
.searchbox input {
color: ${cssManager.bdTheme('#09090b', '#fafafa')};
background: ${cssManager.bdTheme('#f4f4f5', '#18181b')};
width: 100%;
height: 100%;
border: 1px solid ${cssManager.bdTheme('#e5e7eb', '#27272a')};
border-radius: 6px;
padding: 0 12px 0 36px;
font-family: 'Geist Sans', sans-serif;
font-size: 13px;
transition: all 0.15s ease;
}
.searchbox input::placeholder {
color: ${cssManager.bdTheme('#71717a', '#71717a')};
} }
.searchbox input:focus { .searchbox input:focus {
outline: none; outline: none;
border-color: ${cssManager.bdTheme('#3b82f6', '#3b82f6')};
box-shadow: 0 0 0 3px ${cssManager.bdTheme('rgba(59, 130, 246, 0.1)', 'rgba(59, 130, 246, 0.1)')};
}
.searchbox input:focus ~ .search-icon,
.search-wrapper:has(input:focus) .search-icon {
color: ${cssManager.bdTheme('#3b82f6', '#3b82f6')};
} }
.bottomShadow { .bottomShadow {
position: absolute; position: absolute;
width: 100%; width: 100%;
height: 32px; height: 24px;
bottom: 40px; bottom: 48px;
background: ${cssManager.bdTheme( background: ${cssManager.bdTheme(
'linear-gradient(180deg, #f8f8f800 0%, #ffffff 100%)', 'linear-gradient(180deg, transparent 0%, #fafafa 100%)',
'linear-gradient(180deg, #111c2800 0%, #0e151f 100%)' 'linear-gradient(180deg, transparent 0%, #0a0a0a 100%)'
)}; )};
pointer-events: none; pointer-events: none;
opacity: 0.8;
} }
.topShadow { .topShadow {
position: absolute; position: absolute;
width: 100%; width: 100%;
height: 32px; height: 24px;
top: 32px; top: 40px;
background: ${cssManager.bdTheme( background: ${cssManager.bdTheme(
'linear-gradient(0deg, #f8f8f800 0%, #ffffff 100%)', 'linear-gradient(0deg, transparent 0%, #fafafa 100%)',
'linear-gradient(0deg, #111c2800 0%, #0e151f 100%)' 'linear-gradient(0deg, transparent 0%, #0a0a0a 100%)'
)}; )};
pointer-events: none; pointer-events: none;
opacity: 0.8;
} }
`, `,
]; ];
@ -159,86 +335,174 @@ export class DeesAppuiActivitylog extends DeesElement {
<div class="heading">Activity Log</div> <div class="heading">Activity Log</div>
</div> </div>
<div class="activityContainer"> <div class="activityContainer">
<div class="streamingIndicator">streaming...</div> <div class="streamingIndicator">Live Updates</div>
<div class="date-separator">Today</div>
<div class="activityentry" @contextmenu=${async eventArg => { <div class="activityentry" @contextmenu=${async eventArg => {
DeesContextmenu.openContextMenuWithOptions(eventArg, [ DeesContextmenu.openContextMenuWithOptions(eventArg, [
{ {
name: 'app settings', name: 'Copy activity',
action: async () => {}, action: async () => {},
}, },
{ {
name: 'account settings', name: 'View details',
action: async () => {}, action: async () => {},
}, },
{ {
name: 'logout', name: 'Filter by user',
action: async () => {}, action: async () => {},
}, },
]); ]);
}}> }}>
<span class="timestamp">22:01:</span> Max Mustermann logged in <span class="timestamp">22:20</span>
<div class="activity-icon logout">
<dees-icon .icon=${'lucide:logOut'}></dees-icon>
</div>
<div class="activity-text">
<span class="activity-user">Max Mustermann</span> logged out
</div>
</div> </div>
<div class="activityentry"> <div class="activityentry">
<span class="timestamp">22:02:</span> Max Mustermann viewed an invoice <span class="timestamp">22:19</span>
<div class="activity-icon update">
<dees-icon .icon=${'lucide:checkCircle'}></dees-icon>
</div>
<div class="activity-text">
<span class="activity-user">Max Mustermann</span> approved a payment
</div>
</div> </div>
<div class="activityentry"> <div class="activityentry">
<span class="timestamp">22:03:</span> Max Mustermann added a new contact <span class="timestamp">22:18</span>
<div class="activity-icon view">
<dees-icon .icon=${'lucide:archive'}></dees-icon>
</div>
<div class="activity-text">
<span class="activity-user">Max Mustermann</span> archived an invoice
</div>
</div> </div>
<div class="activityentry"> <div class="activityentry">
<span class="timestamp">22:04:</span> Max Mustermann updated account settings <span class="timestamp">22:17</span>
<div class="activity-icon login">
<dees-icon .icon=${'lucide:logIn'}></dees-icon>
</div>
<div class="activity-text">
<span class="activity-user">Max Mustermann</span> logged in
</div>
</div> </div>
<div class="activityentry"> <div class="activityentry">
<span class="timestamp">22:05:</span> Max Mustermann logged out <span class="timestamp">22:16</span>
<div class="activity-icon logout">
<dees-icon .icon=${'lucide:logOut'}></dees-icon>
</div>
<div class="activity-text">
<span class="activity-user">Max Mustermann</span> logged out
</div>
</div> </div>
<div class="activityentry"> <div class="activityentry">
<span class="timestamp">22:06:</span> Max Mustermann logged in <span class="timestamp">22:15</span>
<div class="activity-icon update">
<dees-icon .icon=${'lucide:key'}></dees-icon>
</div>
<div class="activity-text">
<span class="activity-user">Max Mustermann</span> changed password
</div>
</div> </div>
<div class="activityentry"> <div class="activityentry">
<span class="timestamp">22:07:</span> Max Mustermann created a new invoice <span class="timestamp">22:14</span>
<div class="activity-icon create">
<dees-icon .icon=${'lucide:userPlus'}></dees-icon>
</div>
<div class="activity-text">
<span class="activity-user">Max Mustermann</span> added a new user
</div>
</div> </div>
<div class="activityentry"> <div class="activityentry">
<span class="timestamp">22:08:</span> Max Mustermann sent an invoice <span class="timestamp">22:13</span>
<div class="activity-icon view">
<dees-icon .icon=${'lucide:messageCircle'}></dees-icon>
</div>
<div class="activity-text">
<span class="activity-user">Max Mustermann</span> contacted support
</div>
</div> </div>
<div class="date-separator">Yesterday</div>
<div class="activityentry"> <div class="activityentry">
<span class="timestamp">22:09:</span> Max Mustermann viewed reports <span class="timestamp">18:45</span>
<div class="activity-icon update">
<dees-icon .icon=${'lucide:trash2'}></dees-icon>
</div>
<div class="activity-text">
<span class="activity-user">Max Mustermann</span> deleted an invoice
</div>
</div> </div>
<div class="activityentry"> <div class="activityentry">
<span class="timestamp">22:10:</span> Max Mustermann logged out <span class="timestamp">17:30</span>
<div class="activity-icon login">
<dees-icon .icon=${'lucide:logIn'}></dees-icon>
</div>
<div class="activity-text">
<span class="activity-user">Max Mustermann</span> logged in
</div>
</div> </div>
<div class="activityentry"> <div class="activityentry">
<span class="timestamp">22:11:</span> Max Mustermann logged in <span class="timestamp">16:15</span>
<div class="activity-icon logout">
<dees-icon .icon=${'lucide:logOut'}></dees-icon>
</div>
<div class="activity-text">
<span class="activity-user">Max Mustermann</span> logged out
</div>
</div> </div>
<div class="activityentry"> <div class="activityentry">
<span class="timestamp">22:12:</span> Max Mustermann deleted an invoice <span class="timestamp">14:20</span>
<div class="activity-icon view">
<dees-icon .icon=${'lucide:barChart'}></dees-icon>
</div>
<div class="activity-text">
<span class="activity-user">Max Mustermann</span> viewed reports
</div>
</div> </div>
<div class="activityentry"> <div class="activityentry">
<span class="timestamp">22:13:</span> Max Mustermann contacted support <span class="timestamp">13:45</span>
<div class="activity-icon create">
<dees-icon .icon=${'lucide:send'}></dees-icon>
</div>
<div class="activity-text">
<span class="activity-user">Max Mustermann</span> sent an invoice
</div>
</div> </div>
<div class="activityentry"> <div class="activityentry">
<span class="timestamp">22:14:</span> Max Mustermann added a new user <span class="timestamp">13:30</span>
<div class="activity-icon create">
<dees-icon .icon=${'lucide:filePlus'}></dees-icon>
</div>
<div class="activity-text">
<span class="activity-user">Max Mustermann</span> created a new invoice
</div>
</div> </div>
<div class="activityentry">
<span class="timestamp">22:15:</span> Max Mustermann changed password <div class="streamingIndicator bottom">Loading History</div>
</div>
<div class="activityentry">
<span class="timestamp">22:16:</span> Max Mustermann logged out
</div>
<div class="activityentry">
<span class="timestamp">22:17:</span> Max Mustermann logged in
</div>
<div class="activityentry">
<span class="timestamp">22:18:</span> Max Mustermann archived an invoice
</div>
<div class="activityentry">
<span class="timestamp">22:19:</span> Max Mustermann approved a payment
</div>
<div class="activityentry">
<span class="timestamp">22:20:</span> Max Mustermann logged out
</div>
<div class="streamingIndicator bottom">loading more...</div>
</div> </div>
<div class="searchbox"> <div class="searchbox">
<input type="text" placeholder="Search" /> <div class="search-wrapper">
<dees-icon class="search-icon" .icon=${'lucide:search'}></dees-icon>
<input type="text" placeholder="Search activities, users..." />
</div>
</div> </div>
<div class="topShadow"></div> <div class="topShadow"></div>
<div class="bottomShadow"></div> <div class="bottomShadow"></div>

View File

@ -65,10 +65,10 @@ export const demoFunc = () => {
// Main menu tabs (left sidebar) // Main menu tabs (left sidebar)
const mainMenuTabs: ITab[] = [ const mainMenuTabs: ITab[] = [
{ key: 'dashboard', iconName: 'home', action: () => console.log('Dashboard selected') }, { key: 'dashboard', iconName: 'lucide:home', action: () => console.log('Dashboard selected') },
{ key: 'projects', iconName: 'folder', action: () => console.log('Projects selected') }, { key: 'projects', iconName: 'lucide:folder', action: () => console.log('Projects selected') },
{ key: 'analytics', iconName: 'lineChart', action: () => console.log('Analytics selected') }, { key: 'analytics', iconName: 'lucide:lineChart', action: () => console.log('Analytics selected') },
{ key: 'settings', iconName: 'settings', action: () => console.log('Settings selected') }, { key: 'settings', iconName: 'lucide:settings', action: () => console.log('Settings selected') },
]; ];
// Selector options (second sidebar) // Selector options (second sidebar)
@ -83,9 +83,9 @@ export const demoFunc = () => {
// Main content tabs // Main content tabs
const mainContentTabs: ITab[] = [ const mainContentTabs: ITab[] = [
{ key: 'Details', iconName: 'file', action: () => console.log('Details tab') }, { key: 'Details', iconName: 'lucide:file', action: () => console.log('Details tab') },
{ key: 'Logs', iconName: 'list', action: () => console.log('Logs tab') }, { key: 'Logs', iconName: 'lucide:list', action: () => console.log('Logs tab') },
{ key: 'Metrics', iconName: 'lineChart', action: () => console.log('Metrics tab') }, { key: 'Metrics', iconName: 'lucide:lineChart', action: () => console.log('Metrics tab') },
]; ];
// Profile menu items // Profile menu items

View File

@ -19,9 +19,9 @@ export class DeesAppuiMaincontent extends DeesElement {
public static demo = () => html` public static demo = () => html`
<dees-appui-maincontent <dees-appui-maincontent
.tabs=${[ .tabs=${[
{ key: 'Overview', iconName: 'home', action: () => console.log('Overview') }, { key: 'Overview', iconName: 'lucide:home', action: () => console.log('Overview') },
{ key: 'Details', iconName: 'file', action: () => console.log('Details') }, { key: 'Details', iconName: 'lucide:file', action: () => console.log('Details') },
{ key: 'Settings', iconName: 'cog', action: () => console.log('Settings') }, { key: 'Settings', iconName: 'lucide:settings', action: () => console.log('Settings') },
]} ]}
> >
<div slot="content" style="padding: 40px; color: #ccc;"> <div slot="content" style="padding: 40px; color: #ccc;">

View File

@ -22,10 +22,10 @@ export class DeesAppuiMainmenu extends DeesElement {
public static demo = () => html` public static demo = () => html`
<dees-appui-mainmenu <dees-appui-mainmenu
.tabs=${[ .tabs=${[
{ key: 'Dashboard', iconName: 'home', action: () => console.log('Dashboard') }, { key: 'Dashboard', iconName: 'lucide:home', action: () => console.log('Dashboard') },
{ key: 'Projects', iconName: 'folder', action: () => console.log('Projects') }, { key: 'Projects', iconName: 'lucide:folder', action: () => console.log('Projects') },
{ key: 'Analytics', iconName: 'lineChart', action: () => console.log('Analytics') }, { key: 'Analytics', iconName: 'lucide:lineChart', action: () => console.log('Analytics') },
{ key: 'Settings', iconName: 'settings', action: () => console.log('Settings') }, { key: 'Settings', iconName: 'lucide:settings', action: () => console.log('Settings') },
]} ]}
></dees-appui-mainmenu> ></dees-appui-mainmenu>
`; `;
@ -35,7 +35,7 @@ export class DeesAppuiMainmenu extends DeesElement {
// INSTANCE // INSTANCE
@property({ type: Array }) @property({ type: Array })
public tabs: interfaces.ITab[] = [ public tabs: interfaces.ITab[] = [
{ key: '⚠️ Please set tabs', iconName: 'alertTriangle', action: () => console.warn('No tabs configured for mainmenu') }, { key: '⚠️ Please set tabs', iconName: 'lucide:alertTriangle', action: () => console.warn('No tabs configured for mainmenu') },
]; ];
@property() @property()
@ -112,7 +112,7 @@ export class DeesAppuiMainmenu extends DeesElement {
this.updateTab(tabArg); this.updateTab(tabArg);
}}" }}"
> >
<dees-icon .icon="${tabArg.iconName ? `lucide:${tabArg.iconName}` : ''}"></dees-icon> <dees-icon .icon="${tabArg.iconName || ''}"></dees-icon>
</div> </div>
`; `;
})} })}

View File

@ -14,15 +14,94 @@ import * as domtools from '@design.estate/dees-domtools';
@customElement('dees-appui-tabs') @customElement('dees-appui-tabs')
export class DeesAppuiTabs extends DeesElement { export class DeesAppuiTabs extends DeesElement {
public static demo = () => html` public static demo = () => {
<dees-appui-tabs const horizontalTabs: interfaces.ITab[] = [
.tabs=${[ { key: 'Home', iconName: 'lucide:home', action: () => console.log('Home clicked') },
{ key: 'Tab 1', action: () => console.log('Tab 1 clicked') }, { key: 'Analytics Dashboard', iconName: 'lucide:lineChart', action: () => console.log('Analytics clicked') },
{ key: 'Tab 2', action: () => console.log('Tab 2 clicked') }, { key: 'Reports', iconName: 'lucide:fileText', action: () => console.log('Reports clicked') },
{ key: 'Tab 3', action: () => console.log('Tab 3 clicked') }, { key: 'User Settings', iconName: 'lucide:settings', action: () => console.log('Settings clicked') },
]} { key: 'Help', iconName: 'lucide:helpCircle', action: () => console.log('Help clicked') },
></dees-appui-tabs> ];
`;
const verticalTabs: interfaces.ITab[] = [
{ key: 'Profile', iconName: 'lucide:user', action: () => console.log('Profile clicked') },
{ key: 'Security', iconName: 'lucide:shield', action: () => console.log('Security clicked') },
{ key: 'Notifications', iconName: 'lucide:bell', action: () => console.log('Notifications clicked') },
{ key: 'Integrations', iconName: 'lucide:link', action: () => console.log('Integrations clicked') },
{ key: 'Advanced', iconName: 'lucide:code', action: () => console.log('Advanced clicked') },
];
const noIndicatorTabs: interfaces.ITab[] = [
{ key: 'All', action: () => console.log('All clicked') },
{ key: 'Active', action: () => console.log('Active clicked') },
{ key: 'Completed', action: () => console.log('Completed clicked') },
{ key: 'Archived', action: () => console.log('Archived clicked') },
];
const demoContent = (text: string) => html`
<div style="padding: 24px; color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};">
${text}
</div>
`;
return html`
<style>
.demo-container {
display: flex;
flex-direction: column;
gap: 32px;
padding: 48px;
background: ${cssManager.bdTheme('#f8f9fa', '#0a0a0a')};
min-height: 100vh;
}
.section {
background: ${cssManager.bdTheme('#ffffff', '#18181b')};
border: 1px solid ${cssManager.bdTheme('#e5e7eb', '#27272a')};
border-radius: 8px;
padding: 24px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.section-title {
font-size: 18px;
font-weight: 600;
margin-bottom: 16px;
color: ${cssManager.bdTheme('#09090b', '#fafafa')};
}
.two-column {
display: grid;
grid-template-columns: 200px 1fr;
gap: 24px;
align-items: start;
}
</style>
<div class="demo-container">
<div class="section">
<div class="section-title">Horizontal Tabs with Animated Indicator</div>
<dees-appui-tabs .tabs=${horizontalTabs}>
${demoContent('Select a tab to see the smooth sliding animation of the indicator. The indicator automatically adjusts its width to match the tab content with minimal padding.')}
</dees-appui-tabs>
</div>
<div class="section">
<div class="section-title">Vertical Tabs Layout</div>
<div class="two-column">
<dees-appui-tabs .tabStyle=${'vertical'} .tabs=${verticalTabs}></dees-appui-tabs>
${demoContent('Vertical tabs work great for settings pages and navigation menus. The animated indicator smoothly transitions between selections.')}
</div>
</div>
<div class="section">
<div class="section-title">Without Indicator</div>
<dees-appui-tabs .showTabIndicator=${false} .tabs=${noIndicatorTabs}>
${demoContent('Tabs can also be used without the animated indicator by setting showTabIndicator to false.')}
</dees-appui-tabs>
</div>
</div>
`;
};
// INSTANCE // INSTANCE
@property({ @property({
@ -50,148 +129,217 @@ export class DeesAppuiTabs extends DeesElement {
.tabs-wrapper { .tabs-wrapper {
position: relative; position: relative;
background: ${cssManager.bdTheme('#f5f5f5', '#000000')}; }
height: 52px;
.tabs-wrapper.horizontal-wrapper {
border-bottom: 1px solid ${cssManager.bdTheme('#e5e7eb', '#27272a')};
} }
.tabsContainer { .tabsContainer {
position: relative; position: relative;
z-index: 1;
user-select: none; user-select: none;
} }
.tabsContainer.horizontal { .tabsContainer.horizontal {
display: grid; display: flex;
padding-top: 20px; align-items: center;
padding-bottom: 0px;
margin-left: 24px;
font-size: 14px; font-size: 14px;
overflow-x: auto;
scrollbar-width: none;
height: 48px;
padding: 0 16px;
gap: 4px;
}
.tabsContainer.horizontal::-webkit-scrollbar {
display: none;
} }
.tabsContainer.vertical { .tabsContainer.vertical {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
padding: 20px; padding: 8px;
font-size: 14px; font-size: 14px;
gap: 2px;
position: relative;
background: ${cssManager.bdTheme('#f9fafb', '#18181b')};
border-radius: 8px;
} }
.tab { .tab {
color: ${cssManager.bdTheme('#666', '#a0a0a0')}; color: ${cssManager.bdTheme('#71717a', '#71717a')};
white-space: nowrap; white-space: nowrap;
cursor: default; cursor: pointer;
transition: color 0.1s; transition: color 0.15s ease;
font-weight: 500;
position: relative;
z-index: 2;
} }
.horizontal .tab { .horizontal .tab {
margin-right: 30px; padding: 0 16px;
padding-top: 4px; height: 100%;
padding-bottom: 12px; display: inline-flex;
align-items: center;
gap: 8px;
position: relative;
border-radius: 6px 6px 0 0;
transition: background-color 0.15s ease;
} }
.vertical .tab { .horizontal .tab:not(:last-child)::after {
padding: 12px 16px; content: '';
margin-bottom: 4px; position: absolute;
border-radius: 4px; right: -2px;
width: 100%; top: 50%;
display: flex; transform: translateY(-50%);
height: 20px;
width: 1px;
background: ${cssManager.bdTheme('#e5e7eb', '#27272a')};
opacity: 0.5;
}
.horizontal .tab .tab-content {
display: inline-flex;
align-items: center; align-items: center;
gap: 8px; gap: 8px;
} }
.vertical .tab {
padding: 10px 16px;
border-radius: 6px;
width: 100%;
display: flex;
align-items: center;
gap: 8px;
transition: all 0.15s ease;
}
.tab:hover { .tab:hover {
color: ${cssManager.bdTheme('#000', '#ffffff')}; color: ${cssManager.bdTheme('#09090b', '#fafafa')};
}
.horizontal .tab:hover {
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.03)', 'rgba(255, 255, 255, 0.03)')};
}
.horizontal .tab:hover::after,
.horizontal .tab:hover + .tab::after {
opacity: 0;
} }
.vertical .tab:hover { .vertical .tab:hover {
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.05)', 'rgba(255, 255, 255, 0.05)')}; background: ${cssManager.bdTheme('rgba(244, 244, 245, 0.5)', 'rgba(39, 39, 42, 0.5)')};
} }
.tab.selectedTab { .horizontal .tab.selectedTab {
color: ${cssManager.bdTheme('#333', '#e0e0e0')}; color: ${cssManager.bdTheme('#09090b', '#fafafa')};
}
.horizontal .tab.selectedTab::after,
.horizontal .tab.selectedTab + .tab::after {
opacity: 0;
} }
.vertical .tab.selectedTab { .vertical .tab.selectedTab {
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.1)', 'rgba(255, 255, 255, 0.1)')}; color: ${cssManager.bdTheme('#09090b', '#fafafa')};
color: ${cssManager.bdTheme('#000', '#ffffff')};
} }
.tab dees-icon { .tab dees-icon {
font-size: 16px; font-size: 16px;
} }
.tabs-wrapper .tabIndicator { .tabIndicator {
position: absolute; position: absolute;
z-index: 0; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
left: 40px; opacity: 0;
bottom: 0px; }
height: 40px;
width: 40px; .tabIndicator.no-transition {
background: ${cssManager.bdTheme('#ffffff', '#161616')}; transition: none;
transition: all 0.1s;
border-top-left-radius: 8px;
border-top-right-radius: 8px;
border-top: 1px solid ${cssManager.bdTheme('#e0e0e0', '#444444')};
} }
.vertical .tabIndicator { .tabs-wrapper .tabIndicator {
display: none; height: 3px;
bottom: 0;
background: ${cssManager.bdTheme('#3b82f6', '#3b82f6')};
border-radius: 3px 3px 0 0;
z-index: 3;
}
.vertical-wrapper {
position: relative;
}
.vertical-wrapper .tabIndicator {
left: 8px;
right: 8px;
border-radius: 6px;
background: ${cssManager.bdTheme('#ffffff', '#27272a')};
z-index: 1;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
} }
.content { .content {
margin-top: 20px; padding: 32px 24px;
} }
`, `,
]; ];
public render(): TemplateResult { public render(): TemplateResult {
return html` return html`
${this.tabStyle === 'horizontal' ? html` ${this.renderTabsWrapper()}
<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"> <div class="content">
<slot></slot> <slot></slot>
</div> </div>
`; `;
} }
private renderTabsWrapper(): TemplateResult {
const isHorizontal = this.tabStyle === 'horizontal';
const wrapperClass = isHorizontal ? 'tabs-wrapper horizontal-wrapper' : 'vertical-wrapper';
const containerClass = `tabsContainer ${this.tabStyle}`;
return html`
<div class="${wrapperClass}">
<div class="${containerClass}">
${this.tabs.map(tab => this.renderTab(tab, isHorizontal))}
</div>
${this.showTabIndicator ? html`<div class="tabIndicator"></div>` : ''}
</div>
`;
}
private renderTab(tab: interfaces.ITab, isHorizontal: boolean): TemplateResult {
const isSelected = tab === this.selectedTab;
const classes = `tab ${isSelected ? 'selectedTab' : ''}`;
const content = isHorizontal ? html`
<span class="tab-content">
${this.renderTabIcon(tab)}
${tab.key}
</span>
` : html`
${this.renderTabIcon(tab)}
${tab.key}
`;
return html`
<div
class="${classes}"
@click="${() => this.selectTab(tab)}"
>
${content}
</div>
`;
}
private renderTabIcon(tab: interfaces.ITab): TemplateResult | '' {
return tab.iconName ? html`<dees-icon .icon=${tab.iconName}></dees-icon>` : '';
}
private selectTab(tabArg: interfaces.ITab) { private selectTab(tabArg: interfaces.ITab) {
this.selectedTab = tabArg; this.selectedTab = tabArg;
this.updateTabIndicator();
tabArg.action(); tabArg.action();
// Emit tab-select event // Emit tab-select event
@ -202,31 +350,6 @@ export class DeesAppuiTabs extends DeesElement {
})); }));
} }
/**
* 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() { firstUpdated() {
if (this.tabs && this.tabs.length > 0) { if (this.tabs && this.tabs.length > 0) {
this.selectTab(this.tabs[0]); this.selectTab(this.tabs[0]);
@ -241,7 +364,88 @@ export class DeesAppuiTabs extends DeesElement {
} }
if (changedProperties.has('selectedTab') || changedProperties.has('tabs')) { if (changedProperties.has('selectedTab') || changedProperties.has('tabs')) {
this.updateTabIndicator(); await this.updateComplete;
// Wait for fonts to load on first update
if (!this.indicatorInitialized && document.fonts) {
await document.fonts.ready;
}
requestAnimationFrame(() => {
this.updateTabIndicator();
});
} }
} }
private indicatorInitialized = false;
private updateTabIndicator() {
if (!this.shouldShowIndicator()) return;
const selectedTabElement = this.getSelectedTabElement();
if (!selectedTabElement) return;
const indicator = this.getIndicatorElement();
if (!indicator) return;
this.handleInitialTransition(indicator);
if (this.tabStyle === 'horizontal') {
this.updateHorizontalIndicator(indicator, selectedTabElement);
} else {
this.updateVerticalIndicator(indicator, selectedTabElement);
}
indicator.style.opacity = '1';
}
private shouldShowIndicator(): boolean {
return this.selectedTab && this.showTabIndicator && this.tabs.includes(this.selectedTab);
}
private getSelectedTabElement(): HTMLElement | null {
const selectedIndex = this.tabs.indexOf(this.selectedTab);
const isHorizontal = this.tabStyle === 'horizontal';
const selector = isHorizontal
? `.tabs-wrapper .tabsContainer .tab:nth-child(${selectedIndex + 1})`
: `.vertical-wrapper .tabsContainer .tab:nth-child(${selectedIndex + 1})`;
return this.shadowRoot.querySelector(selector);
}
private getIndicatorElement(): HTMLElement | null {
return this.shadowRoot.querySelector('.tabIndicator');
}
private handleInitialTransition(indicator: HTMLElement): void {
if (!this.indicatorInitialized) {
indicator.classList.add('no-transition');
this.indicatorInitialized = true;
setTimeout(() => {
indicator.classList.remove('no-transition');
}, 50);
}
}
private updateHorizontalIndicator(indicator: HTMLElement, tabElement: HTMLElement): void {
const tabContent = tabElement.querySelector('.tab-content') as HTMLElement;
if (!tabContent) return;
const wrapperRect = indicator.parentElement.getBoundingClientRect();
const contentRect = tabContent.getBoundingClientRect();
const contentLeft = contentRect.left - wrapperRect.left;
const indicatorWidth = contentRect.width + 8;
const indicatorLeft = contentLeft - 4;
indicator.style.width = `${indicatorWidth}px`;
indicator.style.left = `${indicatorLeft}px`;
}
private updateVerticalIndicator(indicator: HTMLElement, tabElement: HTMLElement): void {
const tabsContainer = this.shadowRoot.querySelector('.vertical-wrapper .tabsContainer') as HTMLElement;
if (!tabsContainer) return;
indicator.style.top = `${tabElement.offsetTop + tabsContainer.offsetTop}px`;
indicator.style.height = `${tabElement.clientHeight}px`;
}
} }

View File

@ -35,17 +35,17 @@ export class DeesAppuiView extends DeesElement {
id: 'demo-view', id: 'demo-view',
name: 'Demo View', name: 'Demo View',
description: 'A demonstration view', description: 'A demonstration view',
iconName: 'home', iconName: 'lucide:home',
tabs: [ tabs: [
{ {
key: 'overview', key: 'overview',
iconName: 'chart-line', iconName: 'lucide:lineChart',
action: () => console.log('Overview tab'), action: () => console.log('Overview tab'),
content: html`<div style="padding: 20px;">Overview Content</div>` content: html`<div style="padding: 20px;">Overview Content</div>`
}, },
{ {
key: 'details', key: 'details',
iconName: 'file-alt', iconName: 'lucide:fileText',
action: () => console.log('Details tab'), action: () => console.log('Details tab'),
content: html`<div style="padding: 20px;">Details Content</div>` content: html`<div style="padding: 20px;">Details Content</div>`
} }

View File

@ -1,77 +1,421 @@
import { html, css } from '@design.estate/dees-element'; import { html, css, cssManager, domtools } from '@design.estate/dees-element';
import '@design.estate/dees-wcctools/demotools';
import './dees-panel.js';
import './dees-form.js';
import './dees-form-submit.js';
import './dees-input-text.js';
import './dees-icon.js';
import type { DeesButton } from './dees-button.js';
export const demoFunc = () => html` export const demoFunc = () => html`
<style> <style>
${css` ${css`
h3 { .demo-container {
margin-top: 32px; display: flex;
margin-bottom: 16px; flex-direction: column;
color: #333; gap: 24px;
padding: 24px;
max-width: 1200px;
margin: 0 auto;
} }
@media (prefers-color-scheme: dark) { dees-panel {
h3 { margin-bottom: 24px;
color: #ccc;
}
} }
.form-demo { dees-panel:last-child {
background: #f5f5f5; margin-bottom: 0;
padding: 20px;
border-radius: 8px;
margin: 20px 0;
}
@media (prefers-color-scheme: dark) {
.form-demo {
background: #1a1a1a;
}
} }
.button-group { .button-group {
display: flex; display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
}
.vertical-group {
display: flex;
flex-direction: column;
gap: 8px;
max-width: 300px;
}
.horizontal-group {
display: flex;
align-items: center;
gap: 16px; gap: 16px;
margin: 20px 0; flex-wrap: wrap;
}
.demo-output {
margin-top: 16px;
padding: 12px;
background: ${cssManager.bdTheme('hsl(210 40% 96.1%)', 'hsl(215 20.2% 16.8%)')};
border-radius: 6px;
font-size: 14px;
font-family: monospace;
color: ${cssManager.bdTheme('hsl(215.3 25% 8.8%)', 'hsl(210 40% 98%)')};
}
.icon-row {
display: flex;
align-items: center;
gap: 12px;
margin: 8px 0;
}
.code-snippet {
background: ${cssManager.bdTheme('hsl(210 40% 96.1%)', 'hsl(215 20.2% 11.8%)')};
padding: 8px 12px;
border-radius: 4px;
font-family: monospace;
font-size: 13px;
display: inline-block;
margin: 4px 0;
} }
`} `}
</style> </style>
<h3>Button Types</h3> <div class="demo-container">
<dees-button>This is a slotted Text</dees-button> <dees-demowrapper .runAfterRender=${async (elementArg: HTMLElement) => {
<p> // Log button clicks for demo purposes
<dees-button text="Highlighted: This text shows" type="highlighted">Highlighted</dees-button> const buttons = elementArg.querySelectorAll('dees-button');
</p> buttons.forEach((button) => {
<p><dees-button type="discreet">This is discreete button</dees-button></p> button.addEventListener('clicked', () => {
<p><dees-button disabled>This is a disabled button</dees-button></p> const type = button.getAttribute('type') || 'default';
<p><dees-button type="big">This is a slotted Text</dees-button></p> console.log(`Button variant clicked: ${type}`);
});
<h3>Button States</h3> });
<p><dees-button status="normal">Normal Status</dees-button></p> }}>
<p><dees-button disabled status="pending">Pending Status</dees-button></p> <dees-panel .title=${'1. Button Variants'} .subtitle=${'Different visual styles for various use cases'}>
<p><dees-button disabled status="success">Success Status</dees-button></p> <div class="button-group">
<p><dees-button disabled status="error">Error Status</dees-button></p> <dees-button type="default">Default</dees-button>
<dees-button type="secondary">Secondary</dees-button>
<h3>Buttons in Forms (Auto-spacing)</h3> <dees-button type="destructive">Destructive</dees-button>
<div class="form-demo"> <dees-button type="outline">Outline</dees-button>
<dees-form> <dees-button type="ghost">Ghost</dees-button>
<dees-input-text label="Name" key="name"></dees-input-text> <dees-button type="link">Link Button</dees-button>
<dees-input-text label="Email" key="email"></dees-input-text> </div>
<dees-button>Save Draft</dees-button> </dees-panel>
<dees-button type="highlighted">Save and Continue</dees-button> </dees-demowrapper>
<dees-form-submit>Submit Form</dees-form-submit>
</dees-form> <dees-demowrapper .runAfterRender=${async (elementArg: HTMLElement) => {
// Demonstrate size differences programmatically
const buttons = elementArg.querySelectorAll('dees-button');
buttons.forEach((button) => {
button.addEventListener('clicked', () => {
const size = button.getAttribute('size') || 'default';
console.log(`Button size: ${size}`);
});
});
}}>
<dees-panel .title=${'2. Button Sizes'} .subtitle=${'Multiple sizes for different contexts and use cases'}>
<div class="button-group">
<dees-button size="sm">Small Button</dees-button>
<dees-button size="default">Default Size</dees-button>
<dees-button size="lg">Large Button</dees-button>
<dees-button size="icon" type="outline" .text=${'🚀'}></dees-button>
</div>
<div class="button-group" style="margin-top: 16px;">
<dees-button size="sm" type="secondary">Small Secondary</dees-button>
<dees-button size="default" type="destructive">Default Destructive</dees-button>
<dees-button size="lg" type="outline">Large Outline</dees-button>
</div>
</dees-panel>
</dees-demowrapper>
<dees-demowrapper .runAfterRender=${async (elementArg: HTMLElement) => {
// Track icon button clicks
const iconButtons = elementArg.querySelectorAll('dees-button');
iconButtons.forEach((button) => {
button.addEventListener('clicked', () => {
const hasIcon = button.querySelector('dees-icon');
if (hasIcon) {
const iconName = hasIcon.getAttribute('iconFA') || 'unknown';
console.log(`Icon button clicked: ${iconName}`);
}
});
});
}}>
<dees-panel .title=${'3. Buttons with Icons'} .subtitle=${'Combining icons with text for enhanced visual communication'}>
<div class="icon-row">
<dees-button>
<dees-icon iconFA="faPlus"></dees-icon>
Add Item
</dees-button>
<dees-button type="destructive">
<dees-icon iconFA="faTrash"></dees-icon>
Delete
</dees-button>
<dees-button type="outline">
<dees-icon iconFA="faDownload"></dees-icon>
Download
</dees-button>
</div>
<div class="icon-row">
<dees-button type="secondary" size="sm">
<dees-icon iconFA="faCog"></dees-icon>
Settings
</dees-button>
<dees-button type="ghost">
<dees-icon iconFA="faChevronLeft"></dees-icon>
Back
</dees-button>
<dees-button type="ghost">
Next
<dees-icon iconFA="faChevronRight"></dees-icon>
</dees-button>
</div>
<div class="icon-row">
<dees-button size="icon" type="default">
<dees-icon iconFA="faPlus"></dees-icon>
</dees-button>
<dees-button size="icon" type="secondary">
<dees-icon iconFA="faCog"></dees-icon>
</dees-button>
<dees-button size="icon" type="outline">
<dees-icon iconFA="faSearch"></dees-icon>
</dees-button>
<dees-button size="icon" type="ghost">
<dees-icon iconFA="faEllipsisV"></dees-icon>
</dees-button>
<dees-button size="icon" type="destructive">
<dees-icon iconFA="faTrash"></dees-icon>
</dees-button>
</div>
</dees-panel>
</dees-demowrapper>
<dees-demowrapper .runAfterRender=${async (elementArg: HTMLElement) => {
// Demonstrate status changes
const pendingButton = elementArg.querySelector('dees-button[status="pending"]');
const successButton = elementArg.querySelector('dees-button[status="success"]');
const errorButton = elementArg.querySelector('dees-button[status="error"]');
// Simulate status changes
if (pendingButton) {
setTimeout(() => {
console.log('Pending button is showing loading state');
}, 1000);
}
if (successButton) {
successButton.addEventListener('clicked', () => {
console.log('Success state button clicked');
});
}
if (errorButton) {
errorButton.addEventListener('clicked', () => {
console.log('Error state button clicked');
});
}
}}>
<dees-panel .title=${'4. Button States'} .subtitle=${'Different states to indicate button status and loading conditions'}>
<div class="button-group">
<dees-button status="normal">Normal</dees-button>
<dees-button status="pending">Processing...</dees-button>
<dees-button status="success">Success!</dees-button>
<dees-button status="error">Error!</dees-button>
<dees-button disabled>Disabled</dees-button>
</div>
<div class="button-group" style="margin-top: 16px;">
<dees-button type="secondary" status="pending" size="sm">Small Loading</dees-button>
<dees-button type="outline" status="pending">Default Loading</dees-button>
<dees-button type="destructive" status="pending" size="lg">Large Loading</dees-button>
</div>
</dees-panel>
</dees-demowrapper>
<dees-demowrapper .runAfterRender=${async (elementArg: HTMLElement) => {
// Set up click handlers with the output element
const output = elementArg.querySelector('#click-output');
const clickMeBtn = elementArg.querySelector('dees-button:first-of-type');
const dataBtn = elementArg.querySelector('dees-button[type="secondary"]');
const asyncBtn = elementArg.querySelector('dees-button[type="destructive"]');
if (clickMeBtn && output) {
clickMeBtn.addEventListener('clicked', () => {
output.textContent = `Clicked: Default button at ${new Date().toLocaleTimeString()}`;
});
}
if (dataBtn && output) {
dataBtn.addEventListener('clicked', (e: CustomEvent) => {
output.textContent = `Clicked: Secondary button with data: ${e.detail.data}`;
});
}
if (asyncBtn && output) {
asyncBtn.addEventListener('clicked', async () => {
output.textContent = 'Processing...';
await domtools.plugins.smartdelay.delayFor(2000);
output.textContent = 'Action completed!';
});
}
}}>
<dees-panel .title=${'5. Event Handling'} .subtitle=${'Interactive examples with click event handling'}>
<div class="button-group">
<dees-button>Click Me</dees-button>
<dees-button type="secondary" .eventDetailData=${'custom-data-123'}>
Click with Data
</dees-button>
<dees-button type="destructive">Async Action</dees-button>
</div>
<div id="click-output" class="demo-output">
<em>Click a button to see the result...</em>
</div>
</dees-panel>
</dees-demowrapper>
<dees-demowrapper .runAfterRender=${async (elementArg: HTMLElement) => {
// Set up form submission handling
const form = elementArg.querySelector('dees-form');
const output = elementArg.querySelector('#form-output');
if (form && output) {
form.addEventListener('formData', (e: CustomEvent) => {
output.innerHTML = '<strong>Form submitted with data:</strong><br>' +
JSON.stringify(e.detail.data, null, 2);
});
}
// Track non-submit button clicks
const draftBtn = elementArg.querySelector('dees-button[type="secondary"]');
const cancelBtn = elementArg.querySelector('dees-button[type="ghost"]');
if (draftBtn) {
draftBtn.addEventListener('clicked', () => {
console.log('Save Draft clicked');
});
}
if (cancelBtn) {
cancelBtn.addEventListener('clicked', () => {
console.log('Cancel clicked');
});
}
}}>
<dees-panel .title=${'6. Form Integration'} .subtitle=${'Buttons working within forms with automatic spacing'}>
<dees-form>
<dees-input-text label="Name" key="name" required></dees-input-text>
<dees-input-text label="Email" key="email" type="email" required></dees-input-text>
<dees-input-text label="Message" key="message" isMultiline></dees-input-text>
<dees-button type="secondary">Save Draft</dees-button>
<dees-button type="ghost">Cancel</dees-button>
<dees-form-submit>Submit Form</dees-form-submit>
</dees-form>
<div id="form-output" class="demo-output" style="white-space: pre-wrap;">
<em>Submit the form to see the data...</em>
</div>
</dees-panel>
</dees-demowrapper>
<dees-demowrapper .runAfterRender=${async (elementArg: HTMLElement) => {
// Log legacy type mappings
const buttons = elementArg.querySelectorAll('dees-button');
buttons.forEach((button) => {
const type = button.getAttribute('type');
if (type) {
console.log(`Legacy type "${type}" is supported for backward compatibility`);
}
});
}}>
<dees-panel .title=${'7. Backward Compatibility'} .subtitle=${'Old button types are automatically mapped to new variants'}>
<div class="button-group">
<dees-button type="normal">Normal → Default</dees-button>
<dees-button type="highlighted">Highlighted → Destructive</dees-button>
<dees-button type="discreet">Discreet → Outline</dees-button>
<dees-button type="big">Big → Large Size</dees-button>
</div>
<p style="margin-top: 16px; font-size: 14px; color: ${cssManager.bdTheme('hsl(215.4 16.3% 56.9%)', 'hsl(215 20.2% 55.1%)')};">
These legacy type values are maintained for backward compatibility but we recommend using the new variant system.
</p>
</dees-panel>
</dees-demowrapper>
<dees-demowrapper .runAfterRender=${async (elementArg: HTMLElement) => {
// Track action group clicks
const actionGroup = elementArg.querySelectorAll('.vertical-group')[0];
const dangerGroup = elementArg.querySelectorAll('.vertical-group')[1];
if (actionGroup) {
const buttons = actionGroup.querySelectorAll('dees-button');
buttons.forEach((button, index) => {
button.addEventListener('clicked', () => {
const action = ['Save Changes', 'Discard', 'Help'][index];
console.log(`Action group: ${action} clicked`);
});
});
}
if (dangerGroup) {
const buttons = dangerGroup.querySelectorAll('dees-button');
buttons.forEach((button, index) => {
button.addEventListener('clicked', () => {
const action = ['Delete Account', 'Archive Data', 'Not Available'][index];
if (index !== 2) { // Skip disabled button
console.log(`Danger zone: ${action} clicked`);
}
});
});
}
}}>
<dees-panel .title=${'8. Advanced Examples'} .subtitle=${'Complex button configurations and real-world use cases'}>
<div class="horizontal-group">
<div class="vertical-group">
<h4 style="margin: 0 0 8px 0; font-size: 14px; font-weight: 500;">Action Group</h4>
<dees-button type="default" size="sm">
<dees-icon iconFA="faSave"></dees-icon>
Save Changes
</dees-button>
<dees-button type="secondary" size="sm">
<dees-icon iconFA="faUndo"></dees-icon>
Discard
</dees-button>
<dees-button type="ghost" size="sm">
<dees-icon iconFA="faQuestionCircle"></dees-icon>
Help
</dees-button>
</div>
<div class="vertical-group">
<h4 style="margin: 0 0 8px 0; font-size: 14px; font-weight: 500;">Danger Zone</h4>
<dees-button type="destructive" size="sm">
<dees-icon iconFA="faTrash"></dees-icon>
Delete Account
</dees-button>
<dees-button type="outline" size="sm">
<dees-icon iconFA="faArchive"></dees-icon>
Archive Data
</dees-button>
<dees-button type="ghost" size="sm" disabled>
<dees-icon iconFA="faBan"></dees-icon>
Not Available
</dees-button>
</div>
</div>
<div style="margin-top: 24px;">
<h4 style="margin: 0 0 8px 0; font-size: 14px; font-weight: 500;">Code Example:</h4>
<div class="code-snippet">
&lt;dees-button type="default" size="sm" @clicked="\${handleClick}"&gt;<br>
&nbsp;&nbsp;&lt;dees-icon iconFA="faSave"&gt;&lt;/dees-icon&gt;<br>
&nbsp;&nbsp;Save Changes<br>
&lt;/dees-button&gt;
</div>
</div>
</dees-panel>
</dees-demowrapper>
</div> </div>
`;
<h3>Buttons Outside Forms (No auto-spacing)</h3>
<div class="button-group">
<dees-button>Button 1</dees-button>
<dees-button>Button 2</dees-button>
<dees-button>Button 3</dees-button>
</div>
<h3>Manual Form Spacing</h3>
<div>
<dees-button inside-form="true">Manually spaced button 1</dees-button>
<dees-button inside-form="true">Manually spaced button 2</dees-button>
</div>
`;

View File

@ -1,4 +1,3 @@
import { demoFunc } from './dees-button.demo.js';
import { import {
customElement, customElement,
html, html,
@ -12,6 +11,7 @@ import {
} from '@design.estate/dees-element'; } from '@design.estate/dees-element';
import * as domtools from '@design.estate/dees-domtools'; import * as domtools from '@design.estate/dees-domtools';
import { demoFunc } from './dees-button.demo.js';
declare global { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {
@ -48,7 +48,12 @@ export class DeesButton extends DeesElement {
@property({ @property({
type: String type: String
}) })
public type: 'normal' | 'highlighted' | 'discreet' | 'big' = 'normal'; public type: 'default' | 'destructive' | 'outline' | 'secondary' | 'ghost' | 'link' | 'normal' | 'highlighted' | 'discreet' | 'big' = 'default';
@property({
type: String
})
public size: 'default' | 'sm' | 'lg' | 'icon' = 'default';
@property({ @property({
type: String type: String
@ -77,25 +82,23 @@ export class DeesButton extends DeesElement {
cssManager.defaultStyles, cssManager.defaultStyles,
css` css`
:host { :host {
display: block; display: inline-block;
box-sizing: border-box; box-sizing: border-box;
font-family: 'Geist Sans', 'monospace'; font-family: inherit;
} }
:host([hidden]) { :host([hidden]) {
display: none; display: none;
} }
/* Form spacing styles */ /* Form spacing styles */
/* Default vertical form layout */
:host([inside-form]) { :host([inside-form]) {
margin-bottom: 16px; /* Using standard 16px like inputs */ margin-bottom: 16px;
} }
:host([inside-form]:last-child) { :host([inside-form]:last-child) {
margin-bottom: 0; margin-bottom: 0;
} }
/* Horizontal form layout - auto-detected via parent */
dees-form[horizontal-layout] :host([inside-form]) { dees-form[horizontal-layout] :host([inside-form]) {
display: inline-block; display: inline-block;
margin-right: 16px; margin-right: 16px;
@ -107,114 +110,260 @@ export class DeesButton extends DeesElement {
} }
.button { .button {
transition: all 0.1s , color 0s;
position: relative; position: relative;
font-size: 14px; display: inline-flex;
font-weight: 400;
display: flex;
justify-content: center;
align-items: center; align-items: center;
background: ${cssManager.bdTheme('#fff', '#333')}; justify-content: center;
box-shadow: ${cssManager.bdTheme('0px 1px 3px rgba(0,0,0,0.3)', 'none')}; white-space: nowrap;
border: 1px solid ${cssManager.bdTheme('#eee', '#333')}; border-radius: 6px;
border-top: ${cssManager.bdTheme('1px solid #eee', '1px solid #444')}; font-weight: 500;
border-radius: 4px; transition: all 0.15s ease;
height: 40px; cursor: pointer;
padding: 0px 8px;
min-width: 100px;
user-select: none; user-select: none;
color: ${cssManager.bdTheme('#333', ' #ccc')}; outline: none;
max-width: 500px; letter-spacing: -0.01em;
gap: 8px;
} }
.button:hover { /* Size variants */
background: #0050b9; .button.size-default {
color: #ffffff; height: 36px;
border: 1px solid #0050b9; padding: 0 16px;
border-top: 1px solid #0050b9; font-size: 14px;
} }
.button:active { .button.size-sm {
background: #0069f2; height: 32px;
border-top: 1px solid #0069f2; padding: 0 12px;
font-size: 13px;
} }
.button.highlighted { .button.size-lg {
background: #e4002b; height: 44px;
padding: 0 24px;
font-size: 16px;
}
.button.size-icon {
height: 36px;
width: 36px;
padding: 0;
}
/* Default variant */
.button.default {
background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(215 20.2% 11.8%)')};
color: ${cssManager.bdTheme('hsl(215.3 25% 8.8%)', 'hsl(210 40% 98%)')};
border: 1px solid ${cssManager.bdTheme('hsl(214.3 31.8% 91.4%)', 'hsl(215 20.2% 16.8%)')};
box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);
}
.button.default:hover:not(.disabled) {
background: ${cssManager.bdTheme('hsl(210 40% 96.1%)', 'hsl(215 20.2% 10.2%)')};
border-color: ${cssManager.bdTheme('hsl(214.3 31.8% 85%)', 'hsl(215 20.2% 20%)')};
}
.button.default:active:not(.disabled) {
background: ${cssManager.bdTheme('hsl(214.3 31.8% 91.4%)', 'hsl(215 20.2% 9%)')};
}
/* Destructive variant */
.button.destructive {
background: hsl(0 84.2% 60.2%);
color: hsl(0 0% 98%);
border: 1px solid transparent;
}
.button.destructive:hover:not(.disabled) {
background: hsl(0 84.2% 56.2%);
}
.button.destructive:active:not(.disabled) {
background: hsl(0 84.2% 52.2%);
}
/* Outline variant */
.button.outline {
background: transparent;
color: ${cssManager.bdTheme('hsl(215.3 25% 8.8%)', 'hsl(210 40% 98%)')};
border: 1px solid ${cssManager.bdTheme('hsl(214.3 31.8% 91.4%)', 'hsl(215 20.2% 21.8%)')};
}
.button.outline:hover:not(.disabled) {
background: ${cssManager.bdTheme('hsl(210 40% 96.1%)', 'hsl(215 20.2% 16.8%)')};
border-color: ${cssManager.bdTheme('hsl(214.3 31.8% 85%)', 'hsl(215 20.2% 26.8%)')};
}
.button.outline:active:not(.disabled) {
background: ${cssManager.bdTheme('hsl(214.3 31.8% 91.4%)', 'hsl(215 20.2% 13.8%)')};
}
/* Secondary variant */
.button.secondary {
background: ${cssManager.bdTheme('hsl(210 40% 96.1%)', 'hsl(215 20.2% 16.8%)')};
color: ${cssManager.bdTheme('hsl(215.3 25% 8.8%)', 'hsl(210 40% 98%)')};
border: 1px solid transparent;
}
.button.secondary:hover:not(.disabled) {
background: ${cssManager.bdTheme('hsl(214.3 31.8% 91.4%)', 'hsl(215 20.2% 13.8%)')};
}
.button.secondary:active:not(.disabled) {
background: ${cssManager.bdTheme('hsl(214.3 31.8% 85%)', 'hsl(215 20.2% 11.8%)')};
}
/* Ghost variant */
.button.ghost {
background: transparent;
color: ${cssManager.bdTheme('hsl(215.3 25% 8.8%)', 'hsl(210 40% 98%)')};
border: 1px solid transparent;
}
.button.ghost:hover:not(.disabled) {
background: ${cssManager.bdTheme('hsl(210 40% 96.1%)', 'hsl(215 20.2% 16.8%)')};
}
.button.ghost:active:not(.disabled) {
background: ${cssManager.bdTheme('hsl(214.3 31.8% 91.4%)', 'hsl(215 20.2% 13.8%)')};
}
/* Link variant */
.button.link {
background: transparent;
color: ${cssManager.bdTheme('hsl(222.2 47.4% 51.2%)', 'hsl(213.1 93.9% 67.8%)')};
border: none; border: none;
color: #fff; text-decoration: underline;
text-decoration-color: transparent;
height: auto;
padding: 0;
} }
.button.highlighted:hover { .button.link:hover:not(.disabled) {
background: #b50021; text-decoration-color: currentColor;
border: none;
color: #fff;
} }
.button.discreet { /* Status states */
background: none; .button.pending,
border: 1px solid #9b9b9e; .button.success,
color: ${cssManager.bdTheme('#000', '#fff')}; .button.error {
pointer-events: none;
padding-left: 36px; /* Space for spinner */
}
.button.size-sm.pending,
.button.size-sm.success,
.button.size-sm.error {
padding-left: 32px;
}
.button.size-lg.pending,
.button.size-lg.success,
.button.size-lg.error {
padding-left: 44px;
} }
.button.discreet:hover { .button.pending {
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.1)', 'rgba(255, 255, 255, 0.1)')}; background: ${cssManager.bdTheme('hsl(222.2 47.4% 51.2%)', 'hsl(213.1 93.9% 67.8% / 0.2)')};
color: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(213.1 93.9% 67.8%)')};
border: 1px solid transparent;
} }
.button.success {
background: ${cssManager.bdTheme('hsl(142.1 76.2% 36.3%)', 'hsl(142.1 70.6% 45.3% / 0.2)')};
color: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(142.1 70.6% 45.3%)')};
border: 1px solid transparent;
}
.button.error {
background: ${cssManager.bdTheme('hsl(0 84.2% 60.2%)', 'hsl(0 62.8% 70.6% / 0.2)')};
color: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(0 62.8% 70.6%)')};
border: 1px solid transparent;
}
/* Disabled state */
.button.disabled { .button.disabled {
background: ${cssManager.bdTheme('#ffffff00', '#11111100')}; opacity: 0.5;
border: 1px dashed ${cssManager.bdTheme('#666666', '#666666')}; cursor: not-allowed;
color: #9b9b9e; pointer-events: none;
cursor: default;
} }
/* Hidden state */
.button.hidden { .button.hidden {
display: none; display: none;
} }
.button.big { /* Focus state */
width: 300px; .button:focus-visible {
line-height: 48px; outline: 2px solid ${cssManager.bdTheme('hsl(222.2 47.4% 51.2%)', 'hsl(213.1 93.9% 67.8%)')};
font-size: 16px; outline-offset: 2px;
padding: 0px 48px;
margin-top: 32px;
}
.button.pending {
border: 1px dashed ${cssManager.bdTheme('#0069f2', '#0069f270')};
background: ${cssManager.bdTheme('#0069f2', '#0069f270')};
color: #fff;
}
.button.success {
border: 1px dashed ${cssManager.bdTheme('#689F38', '#8BC34A70')};
background: ${cssManager.bdTheme('#689F38', '#8BC34A70')};
color: #fff;
}
.button.error {
border: 1px dashed ${cssManager.bdTheme('#B71C1C', '#E64A1970')};
background: ${cssManager.bdTheme('#B71C1C', '#E64A1970')};
color: #fff;
} }
/* Loading spinner */
dees-spinner { dees-spinner {
position: absolute; position: absolute;
left: 10px; left: 10px;
width: 16px;
height: 16px;
} }
.button.size-sm dees-spinner {
left: 8px;
width: 14px;
height: 14px;
}
.button.size-lg dees-spinner {
left: 14px;
width: 18px;
height: 18px;
}
/* Icon sizing within buttons */
.button dees-icon {
width: 16px;
height: 16px;
flex-shrink: 0;
}
.button.size-sm dees-icon {
width: 14px;
height: 14px;
}
.button.size-lg dees-icon {
width: 18px;
height: 18px;
}
`, `,
]; ];
public render(): TemplateResult { public render(): TemplateResult {
// Map old types to new types for backward compatibility
const typeMap: {[key: string]: string} = {
'normal': 'default',
'highlighted': 'destructive',
'discreet': 'outline',
'big': 'default' // Will use size instead
};
const actualType = typeMap[this.type] || this.type;
const actualSize = this.type === 'big' ? 'lg' : this.size;
return html` return html`
<div <div
class="button ${this.isHidden ? 'hidden' : 'block'} ${this.type} ${this.status} ${this.disabled class="button ${this.isHidden ? 'hidden' : ''} ${actualType} size-${actualSize} ${this.status} ${this.disabled
? 'disabled' ? 'disabled'
: null}" : ''}"
@click="${this.dispatchClick}" @click="${this.dispatchClick}"
> >
${this.status === 'normal' ? html``: html` ${this.status === 'normal' ? html``: html`
<dees-spinner .bnw=${true} status="${this.status}"></dees-spinner> <dees-spinner
.bnw=${true}
status="${this.status}"
size="${actualSize === 'sm' ? 14 : actualSize === 'lg' ? 18 : 16}"
></dees-spinner>
`} `}
<div class="textbox">${this.text || html`<slot>Button</slot>`}</div> <div class="textbox">${this.text || html`<slot>Button</slot>`}</div>
</div> </div>

View File

@ -1,4 +1,4 @@
import { html, css } from '@design.estate/dees-element'; import { html, css, cssManager } from '@design.estate/dees-element';
import type { DeesChartArea } from './dees-chart-area.js'; import type { DeesChartArea } from './dees-chart-area.js';
import '@design.estate/dees-wcctools/demotools'; import '@design.estate/dees-wcctools/demotools';
@ -402,7 +402,7 @@ export const demoFunc = () => {
${css` ${css`
.demoBox { .demoBox {
position: relative; position: relative;
background: #000000; background: ${cssManager.bdTheme('hsl(0 0% 95%)', 'hsl(0 0% 9%)')};
height: 100%; height: 100%;
width: 100%; width: 100%;
padding: 40px; padding: 40px;
@ -425,9 +425,9 @@ export const demoFunc = () => {
} }
.info { .info {
color: #666; color: ${cssManager.bdTheme('hsl(215.4 16.3% 56.9%)', 'hsl(215 20.2% 55.1%)')};
font-size: 11px; font-size: 12px;
font-family: 'Geist Sans', sans-serif; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Geist Sans', sans-serif;
text-align: center; text-align: center;
margin-top: 8px; margin-top: 8px;
} }

View File

@ -61,6 +61,23 @@ export class DeesChartArea extends DeesElement {
private resizeTimeout: number; private resizeTimeout: number;
private internalChartData: ApexAxisChartSeries = []; private internalChartData: ApexAxisChartSeries = [];
private autoScrollTimer: number | null = null; private autoScrollTimer: number | null = null;
private readonly DEBUG_RESIZE = false; // Set to true to enable resize debugging
// Chart color schemes
private readonly CHART_COLORS = {
dark: [
'hsl(217.2 91.2% 59.8%)', // Blue
'hsl(173.4 80.4% 40%)', // Teal
'hsl(280.3 87.4% 66.7%)', // Purple
'hsl(24.6 95% 53.1%)', // Orange
],
light: [
'hsl(222.2 47.4% 51.2%)', // Blue (shadcn primary)
'hsl(142.1 76.2% 36.3%)', // Green (shadcn success)
'hsl(280.3 47.7% 50.2%)', // Purple (muted)
'hsl(20.5 90.2% 48.2%)', // Orange (shadcn destructive variant)
]
};
constructor() { constructor() {
super(); super();
@ -73,46 +90,72 @@ export class DeesChartArea extends DeesElement {
} }
this.resizeTimeout = window.setTimeout(() => { this.resizeTimeout = window.setTimeout(() => {
for (let entry of entries) { // Simply resize if we have a chart, since we're only observing the mainbox
if (entry.target.classList.contains('mainbox') && this.chart) { if (this.chart) {
this.resizeChart(); // Log resize event for debugging
if (this.DEBUG_RESIZE && entries.length > 0) {
const entry = entries[0];
console.log('DeesChartArea - Resize detected:', {
width: entry.contentRect.width,
height: entry.contentRect.height
});
} }
this.resizeChart();
} }
}, 100); // 100ms debounce }, 100); // 100ms debounce
}); });
this.registerStartupFunction(async () => { // Note: ResizeObserver is now set up after chart initialization in firstUpdated()
this.updateComplete.then(() => { // to ensure proper timing and avoid race conditions
const mainbox = this.shadowRoot.querySelector('.mainbox');
if (mainbox) {
this.resizeObserver.observe(mainbox);
}
});
});
this.registerGarbageFunction(async () => { this.registerGarbageFunction(async () => {
if (this.resizeTimeout) { if (this.resizeTimeout) {
clearTimeout(this.resizeTimeout); clearTimeout(this.resizeTimeout);
} }
this.resizeObserver.disconnect(); if (this.resizeObserver) {
this.resizeObserver.disconnect();
}
this.stopAutoScroll(); this.stopAutoScroll();
// Critical: Destroy chart instance to prevent memory leak
if (this.chart) {
try {
this.chart.destroy();
this.chart = null;
} catch (error) {
console.error('Error destroying chart:', error);
}
}
}); });
} }
public async connectedCallback() {
super.connectedCallback();
// Trigger resize when element is connected to DOM
// This helps with dynamically added charts
if (this.chart) {
// Wait a frame for layout to settle
await new Promise(resolve => requestAnimationFrame(resolve));
await this.resizeChart();
}
}
public static styles = [ public static styles = [
cssManager.defaultStyles, cssManager.defaultStyles,
css` css`
:host { :host {
font-family: 'Geist Sans', sans-serif; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
color: #ccc; color: ${cssManager.bdTheme('hsl(0 0% 3.9%)', 'hsl(0 0% 98%)')};
font-weight: 600; font-weight: 400;
font-size: 12px; font-size: 14px;
} }
.mainbox { .mainbox {
position: relative; position: relative;
width: 100%; width: 100%;
height: 400px; height: 400px;
background: #111; background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(0 0% 3.9%)')};
border: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
border-radius: 8px; border-radius: 8px;
overflow: hidden; overflow: hidden;
} }
@ -122,9 +165,13 @@ export class DeesChartArea extends DeesElement {
top: 0; top: 0;
left: 0; left: 0;
width: 100%; width: 100%;
text-align: center; text-align: left;
padding-top: 16px; padding: 16px 24px;
z-index: 10; z-index: 10;
font-size: 14px;
font-weight: 500;
letter-spacing: -0.01em;
color: ${cssManager.bdTheme('hsl(0 0% 20%)', 'hsl(0 0% 63.9%)')};
} }
.chartContainer { .chartContainer {
position: absolute; position: absolute;
@ -132,8 +179,22 @@ export class DeesChartArea extends DeesElement {
left: 0px; left: 0px;
bottom: 0px; bottom: 0px;
right: 0px; right: 0px;
padding: 32px 16px 16px 0px; padding: 44px 16px 16px 0px;
overflow: hidden; overflow: hidden;
background: transparent; /* Ensure container doesn't override chart background */
}
/* ApexCharts theme overrides */
.apexcharts-canvas {
background: transparent !important;
}
.apexcharts-inner {
background: transparent !important;
}
.apexcharts-graphical {
background: transparent !important;
} }
`, `,
]; ];
@ -199,12 +260,17 @@ export class DeesChartArea extends DeesElement {
// Store internal data // Store internal data
this.internalChartData = chartSeries; this.internalChartData = chartSeries;
// Get current theme
const isDark = !this.goBright;
const theme = isDark ? 'dark' : 'light';
var options: ApexCharts.ApexOptions = { var options: ApexCharts.ApexOptions = {
series: chartSeries, series: chartSeries,
chart: { chart: {
width: initialWidth || 100, // Use actual width or fallback width: initialWidth || 100, // Use actual width or fallback
height: initialHeight || 100, // Use actual height or fallback height: initialHeight || 100, // Use actual height or fallback
type: 'area', type: 'area',
background: 'transparent', // Transparent background to inherit from container
toolbar: { toolbar: {
show: false, // This line disables the toolbar show: false, // This line disables the toolbar
}, },
@ -220,12 +286,18 @@ export class DeesChartArea extends DeesElement {
speed: 350 speed: 350
} }
}, },
zoom: {
enabled: false, // Disable zoom for cleaner interaction
},
selection: {
enabled: false, // Disable selection
},
}, },
dataLabels: { dataLabels: {
enabled: false, enabled: false,
}, },
stroke: { stroke: {
width: 1, width: 2,
curve: 'smooth', curve: 'smooth',
}, },
xaxis: { xaxis: {
@ -234,8 +306,10 @@ export class DeesChartArea extends DeesElement {
format: 'HH:mm:ss', // Time formatting with seconds format: 'HH:mm:ss', // Time formatting with seconds
datetimeUTC: false, datetimeUTC: false,
style: { style: {
colors: '#9e9e9e', // Label color colors: [isDark ? 'hsl(0 0% 63.9%)' : 'hsl(0 0% 20%)'], // Label color
fontSize: '11px', fontSize: '12px',
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif',
fontWeight: '400',
}, },
}, },
axisBorder: { axisBorder: {
@ -251,8 +325,10 @@ export class DeesChartArea extends DeesElement {
labels: { labels: {
formatter: this.yAxisFormatter, formatter: this.yAxisFormatter,
style: { style: {
colors: '#9e9e9e', // Label color colors: [isDark ? 'hsl(0 0% 63.9%)' : 'hsl(0 0% 20%)'], // Label color
fontSize: '12px', fontSize: '12px',
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif',
fontWeight: '400',
}, },
}, },
axisBorder: { axisBorder: {
@ -269,14 +345,30 @@ export class DeesChartArea extends DeesElement {
x: { x: {
format: 'dd/MM/yy HH:mm', format: 'dd/MM/yy HH:mm',
}, },
custom: function ({ series, dataPointIndex, w }: any) { custom: ({ series, dataPointIndex, w }: any) => {
// Iterate through each series and get its value // Iterate through each series and get its value
let tooltipContent = `<div style="padding: 10px; background: #1e1e2f; color: white; border-radius: 5px;">`; // Note: We can't access component instance here, so we'll use w.config.theme.mode
const currentTheme = w.config.theme.mode;
const isDarkMode = currentTheme === 'dark';
const bgColor = isDarkMode ? 'hsl(0 0% 9%)' : 'hsl(0 0% 100%)';
const textColor = isDarkMode ? 'hsl(0 0% 95%)' : 'hsl(0 0% 9%)';
const borderColor = isDarkMode ? 'hsl(0 0% 14.9%)' : 'hsl(0 0% 89.8%)';
// Get formatter from chart config
const formatter = w.config.yaxis[0]?.labels?.formatter || ((val: number) => val.toString());
let tooltipContent = `<div style="padding: 12px; background: ${bgColor}; color: ${textColor}; border-radius: 6px; box-shadow: 0 2px 8px 0 hsl(0 0% 0% / ${isDarkMode ? '0.2' : '0.1'}); border: 1px solid ${borderColor};font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; font-size: 12px;">`;
series.forEach((s: number[], index: number) => { series.forEach((s: number[], index: number) => {
const label = w.globals.seriesNames[index]; // Get series label const label = w.globals.seriesNames[index]; // Get series label
const value = s[dataPointIndex]; // Get value at data point const value = s[dataPointIndex]; // Get value at data point
tooltipContent += `<strong>${label}:</strong> ${value} Mbps<br/>`; const color = w.globals.colors[index];
const formattedValue = formatter(value);
tooltipContent += `<div style="display: flex; align-items: center; gap: 8px; margin: ${index > 0 ? '6px' : '0'} 0;">
<span style="display: inline-block; width: 10px; height: 10px; background: ${color}; border-radius: 2px;"></span>
<span style="font-weight: 500;">${label}:</span>
<span style="margin-left: auto; font-weight: 600;">${formattedValue}</span>
</div>`;
}); });
tooltipContent += `</div>`; tooltipContent += `</div>`;
@ -286,7 +378,7 @@ export class DeesChartArea extends DeesElement {
grid: { grid: {
xaxis: { xaxis: {
lines: { lines: {
show: true, // This enables the grid lines along the x-axis show: false, // Hide vertical grid lines for cleaner look
}, },
}, },
yaxis: { yaxis: {
@ -294,38 +386,67 @@ export class DeesChartArea extends DeesElement {
show: true, show: true,
}, },
}, },
borderColor: '#333', // Set the color of the grid lines borderColor: isDark ? 'hsl(0 0% 14.9%)' : 'hsl(0 0% 94%)', // Very subtle grid lines
strokeDashArray: 0, // Solid line strokeDashArray: 0, // Solid line
row: { padding: {
colors: [], // This can be used to alternate the shading of the horizontal rows top: 10,
opacity: 0.1, right: 20,
}, bottom: 10,
column: { left: 20,
colors: [], // For vertical column bands, not needed here but available for customization
opacity: 0.1,
}, },
}, },
fill: { fill: {
type: 'gradient', // Gradient fill for the area type: 'gradient', // Gradient fill for the area
gradient: { gradient: {
shade: 'dark', shade: isDark ? 'dark' : 'light',
type: 'vertical', type: 'vertical',
gradientToColors: ['#9c27b0'], // Gradient color ending shadeIntensity: 0.1,
opacityFrom: isDark ? 0.2 : 0.3,
opacityTo: 0,
stops: [0, 100], stops: [0, 100],
}, },
}, },
colors: isDark ? this.CHART_COLORS.dark : this.CHART_COLORS.light,
theme: {
mode: theme,
},
}; };
this.chart = new ApexCharts(this.shadowRoot.querySelector('.chartContainer'), options);
await this.chart.render();
// Give the chart a moment to fully initialize before resizing try {
await new Promise(resolve => setTimeout(resolve, 100)); this.chart = new ApexCharts(this.shadowRoot.querySelector('.chartContainer'), options);
await this.resizeChart(); await this.chart.render();
// Give the chart a moment to fully initialize before resizing
await new Promise(resolve => setTimeout(resolve, 100));
await this.resizeChart();
// Ensure resize observer is watching the mainbox
const mainbox = this.shadowRoot.querySelector('.mainbox');
if (mainbox && this.resizeObserver) {
// Disconnect any previous observations
this.resizeObserver.disconnect();
// Start observing the mainbox
this.resizeObserver.observe(mainbox);
if (this.DEBUG_RESIZE) {
console.log('DeesChartArea - ResizeObserver attached to mainbox');
}
}
} catch (error) {
console.error('Failed to initialize chart:', error);
// Optionally, you could set an error state here
// this.chartState = 'error';
// this.errorMessage = 'Failed to initialize chart';
}
} }
public async updated(changedProperties: Map<string, any>) { public async updated(changedProperties: Map<string, any>) {
super.updated(changedProperties); super.updated(changedProperties);
// Update chart theme when goBright changes
if (changedProperties.has('goBright') && this.chart) {
await this.updateChartTheme();
}
// Update chart if series data changes // Update chart if series data changes
if (changedProperties.has('series') && this.chart && this.series.length > 0) { if (changedProperties.has('series') && this.chart && this.series.length > 0) {
await this.updateSeries(this.series); await this.updateSeries(this.series);
@ -393,50 +514,55 @@ export class DeesChartArea extends DeesElement {
return; return;
} }
// Store the new data first try {
this.internalChartData = newSeries; // 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 // Handle rolling window if enabled
const filteredSeries = newSeries.map(series => ({ if (this.rollingWindow > 0 && this.realtimeMode) {
name: series.name, const now = Date.now();
data: (series.data as any[]).filter(point => { const cutoffTime = now - this.rollingWindow;
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); // 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);
}
}
await this.chart.updateSeries(filteredSeries, false);
}
} else {
await this.chart.updateSeries(newSeries, animate);
} }
} else { } catch (error) {
this.chart.updateSeries(newSeries, animate); console.error('Failed to update chart series:', error);
} }
} }
// New method to update just the x-axis for smooth scrolling // Update just the x-axis for smooth scrolling in realtime mode
// Public for advanced usage in demos, but typically handled automatically
public async updateTimeWindow() { public async updateTimeWindow() {
if (!this.chart || this.rollingWindow <= 0) { if (!this.chart || this.rollingWindow <= 0) {
return; return;
@ -453,8 +579,10 @@ export class DeesChartArea extends DeesElement {
format: 'HH:mm:ss', format: 'HH:mm:ss',
datetimeUTC: false, datetimeUTC: false,
style: { style: {
colors: '#9e9e9e', colors: [!this.goBright ? 'hsl(0 0% 63.9%)' : 'hsl(0 0% 20%)'],
fontSize: '11px', fontSize: '12px',
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif',
fontWeight: '400',
}, },
}, },
tickAmount: 6, tickAmount: 6,
@ -484,32 +612,61 @@ export class DeesChartArea extends DeesElement {
return; return;
} }
const mainbox: HTMLDivElement = this.shadowRoot.querySelector('.mainbox'); if (this.DEBUG_RESIZE) {
const chartContainer: HTMLDivElement = this.shadowRoot.querySelector('.chartContainer'); console.log('DeesChartArea - resizeChart called');
if (!mainbox || !chartContainer) {
return;
} }
try {
const mainbox: HTMLDivElement = this.shadowRoot.querySelector('.mainbox');
const chartContainer: HTMLDivElement = this.shadowRoot.querySelector('.chartContainer');
if (!mainbox || !chartContainer) {
return;
}
// Get computed style of the element // Force layout recalculation
const styleChartContainer = window.getComputedStyle(chartContainer); void mainbox.offsetHeight;
// Extract padding values // Get computed style of the element
const paddingTop = parseInt(styleChartContainer.paddingTop, 10); const styleChartContainer = window.getComputedStyle(chartContainer);
const paddingBottom = parseInt(styleChartContainer.paddingBottom, 10);
const paddingLeft = parseInt(styleChartContainer.paddingLeft, 10);
const paddingRight = parseInt(styleChartContainer.paddingRight, 10);
// Calculate the actual width and height to use, subtracting padding // Extract padding values
const actualWidth = mainbox.clientWidth - paddingLeft - paddingRight; const paddingTop = parseInt(styleChartContainer.paddingTop, 10);
const actualHeight = mainbox.offsetHeight - paddingTop - paddingBottom; const paddingBottom = parseInt(styleChartContainer.paddingBottom, 10);
const paddingLeft = parseInt(styleChartContainer.paddingLeft, 10);
const paddingRight = parseInt(styleChartContainer.paddingRight, 10);
await this.chart.updateOptions({ // Calculate the actual width and height to use, subtracting padding
chart: { const actualWidth = mainbox.clientWidth - paddingLeft - paddingRight;
width: actualWidth, const actualHeight = mainbox.offsetHeight - paddingTop - paddingBottom;
height: actualHeight,
}, // Validate dimensions
}); if (actualWidth > 0 && actualHeight > 0) {
if (this.DEBUG_RESIZE) {
console.log('DeesChartArea - Updating chart dimensions:', {
width: actualWidth,
height: actualHeight
});
}
await this.chart.updateOptions({
chart: {
width: actualWidth,
height: actualHeight,
},
}, true, false); // Redraw paths but don't animate
}
} catch (error) {
console.error('Failed to resize chart:', error);
}
}
/**
* Manually trigger a chart resize. Useful when automatic detection doesn't work.
* This is a convenience method that can be called from outside the component.
*/
public async forceResize() {
await this.resizeChart();
} }
private startAutoScroll() { private startAutoScroll() {
@ -528,4 +685,43 @@ export class DeesChartArea extends DeesElement {
this.autoScrollTimer = null; this.autoScrollTimer = null;
} }
} }
private async updateChartTheme() {
if (!this.chart) {
return;
}
const isDark = !this.goBright;
const theme = isDark ? 'dark' : 'light';
await this.chart.updateOptions({
theme: {
mode: theme,
},
colors: isDark ? this.CHART_COLORS.dark : this.CHART_COLORS.light,
xaxis: {
labels: {
style: {
colors: [isDark ? 'hsl(0 0% 63.9%)' : 'hsl(0 0% 20%)'],
},
},
},
yaxis: {
labels: {
style: {
colors: [isDark ? 'hsl(0 0% 63.9%)' : 'hsl(0 0% 20%)'],
},
},
},
grid: {
borderColor: isDark ? 'hsl(0 0% 14.9%)' : 'hsl(0 0% 94%)',
},
fill: {
gradient: {
shade: isDark ? 'dark' : 'light',
opacityFrom: isDark ? 0.2 : 0.3,
},
},
});
}
} }

View File

@ -53,17 +53,17 @@ export class DeesChartLog extends DeesElement {
cssManager.defaultStyles, cssManager.defaultStyles,
css` css`
:host { :host {
font-family: 'Geist Mono', 'Consolas', 'Monaco', monospace; font-family: 'SF Mono', 'Monaco', 'Consolas', 'Liberation Mono', 'Courier New', monospace;
color: #ccc; color: ${cssManager.bdTheme('hsl(0 0% 3.9%)', 'hsl(0 0% 98%)')};
font-size: 12px; font-size: 12px;
line-height: 1.4; line-height: 1.5;
} }
.mainbox { .mainbox {
position: relative; position: relative;
width: 100%; width: 100%;
height: 400px; height: 400px;
background: ${cssManager.bdTheme('#f8f9fa', '#0a0a0a')}; background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(0 0% 3.9%)')};
border: 1px solid ${cssManager.bdTheme('#dee2e6', '#333')}; border: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
border-radius: 8px; border-radius: 8px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -71,9 +71,9 @@ export class DeesChartLog extends DeesElement {
} }
.header { .header {
background: ${cssManager.bdTheme('#e9ecef', '#1a1a1a')}; background: ${cssManager.bdTheme('hsl(0 0% 97%)', 'hsl(0 0% 7%)')};
padding: 8px 16px; padding: 12px 16px;
border-bottom: 1px solid ${cssManager.bdTheme('#dee2e6', '#333')}; border-bottom: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
@ -81,8 +81,10 @@ export class DeesChartLog extends DeesElement {
} }
.title { .title {
font-weight: 600; font-weight: 500;
color: ${cssManager.bdTheme('#212529', '#fff')}; font-size: 14px;
color: ${cssManager.bdTheme('hsl(0 0% 9%)', 'hsl(0 0% 95%)')};
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
} }
.controls { .controls {
@ -91,44 +93,48 @@ export class DeesChartLog extends DeesElement {
} }
.control-button { .control-button {
background: ${cssManager.bdTheme('#e9ecef', '#2a2a2a')}; background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(0 0% 14.9%)')};
border: 1px solid ${cssManager.bdTheme('#ced4da', '#444')}; border: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
border-radius: 4px; border-radius: 6px;
padding: 4px 8px; padding: 6px 12px;
color: ${cssManager.bdTheme('#495057', '#ccc')}; color: ${cssManager.bdTheme('hsl(0 0% 45.1%)', 'hsl(0 0% 63.9%)')};
cursor: pointer; cursor: pointer;
font-size: 11px; font-size: 12px;
transition: all 0.2s; font-weight: 500;
transition: all 0.15s;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
} }
.control-button:hover { .control-button:hover {
background: ${cssManager.bdTheme('#dee2e6', '#3a3a3a')}; background: ${cssManager.bdTheme('hsl(0 0% 95.1%)', 'hsl(0 0% 14.9%)')};
border-color: ${cssManager.bdTheme('#adb5bd', '#555')}; border-color: ${cssManager.bdTheme('hsl(0 0% 79.8%)', 'hsl(0 0% 20.9%)')};
color: ${cssManager.bdTheme('hsl(0 0% 15%)', 'hsl(0 0% 93.9%)')};
} }
.control-button.active { .control-button.active {
background: ${cssManager.bdTheme('#007bff', '#4a4a4a')}; background: ${cssManager.bdTheme('hsl(0 0% 9%)', 'hsl(0 0% 93.9%)')};
color: ${cssManager.bdTheme('#fff', '#fff')}; color: ${cssManager.bdTheme('hsl(0 0% 98%)', 'hsl(0 0% 3.9%)')};
} }
.logContainer { .logContainer {
flex: 1; flex: 1;
overflow-y: auto; overflow-y: auto;
overflow-x: hidden; overflow-x: hidden;
padding: 8px 16px; padding: 16px;
font-size: 12px; font-size: 12px;
} }
.logEntry { .logEntry {
margin-bottom: 2px; margin-bottom: 4px;
display: flex; display: flex;
white-space: pre-wrap; white-space: pre-wrap;
word-break: break-all; word-break: break-all;
font-variant-numeric: tabular-nums;
} }
.timestamp { .timestamp {
color: ${cssManager.bdTheme('#6c757d', '#666')}; color: ${cssManager.bdTheme('hsl(0 0% 63.9%)', 'hsl(0 0% 45.1%)')};
margin-right: 8px; margin-right: 12px;
flex-shrink: 0; flex-shrink: 0;
} }
@ -143,38 +149,38 @@ export class DeesChartLog extends DeesElement {
} }
.level.debug { .level.debug {
color: ${cssManager.bdTheme('#6c757d', '#999')}; color: ${cssManager.bdTheme('hsl(0 0% 45.1%)', 'hsl(0 0% 63.9%)')};
background: ${cssManager.bdTheme('rgba(108, 117, 125, 0.1)', '#333')}; background: ${cssManager.bdTheme('hsl(0 0% 45.1% / 0.1)', 'hsl(0 0% 63.9% / 0.1)')};
} }
.level.info { .level.info {
color: ${cssManager.bdTheme('#0066cc', '#4a9eff')}; color: ${cssManager.bdTheme('hsl(222.2 47.4% 51.2%)', 'hsl(217.2 91.2% 59.8%)')};
background: ${cssManager.bdTheme('rgba(0, 102, 204, 0.1)', 'rgba(74, 158, 255, 0.1)')}; background: ${cssManager.bdTheme('hsl(222.2 47.4% 51.2% / 0.1)', 'hsl(217.2 91.2% 59.8% / 0.1)')};
} }
.level.warn { .level.warn {
color: ${cssManager.bdTheme('#ff8800', '#ffb84a')}; color: ${cssManager.bdTheme('hsl(25 95% 53%)', 'hsl(25 95% 63%)')};
background: ${cssManager.bdTheme('rgba(255, 136, 0, 0.1)', 'rgba(255, 184, 74, 0.1)')}; background: ${cssManager.bdTheme('hsl(25 95% 53% / 0.1)', 'hsl(25 95% 63% / 0.1)')};
} }
.level.error { .level.error {
color: ${cssManager.bdTheme('#dc3545', '#ff4a4a')}; color: ${cssManager.bdTheme('hsl(0 84.2% 60.2%)', 'hsl(0 72.2% 50.6%)')};
background: ${cssManager.bdTheme('rgba(220, 53, 69, 0.1)', 'rgba(255, 74, 74, 0.1)')}; background: ${cssManager.bdTheme('hsl(0 84.2% 60.2% / 0.1)', 'hsl(0 72.2% 50.6% / 0.1)')};
} }
.level.success { .level.success {
color: ${cssManager.bdTheme('#28a745', '#4aff88')}; color: ${cssManager.bdTheme('hsl(142.1 76.2% 36.3%)', 'hsl(142.1 70.6% 45.3%)')};
background: ${cssManager.bdTheme('rgba(40, 167, 69, 0.1)', 'rgba(74, 255, 136, 0.1)')}; background: ${cssManager.bdTheme('hsl(142.1 76.2% 36.3% / 0.1)', 'hsl(142.1 70.6% 45.3% / 0.1)')};
} }
.source { .source {
color: ${cssManager.bdTheme('#6c757d', '#888')}; color: ${cssManager.bdTheme('hsl(0 0% 45.1%)', 'hsl(0 0% 63.9%)')};
margin-right: 8px; margin-right: 8px;
flex-shrink: 0; flex-shrink: 0;
} }
.message { .message {
color: ${cssManager.bdTheme('#212529', '#ddd')}; color: ${cssManager.bdTheme('hsl(0 0% 15%)', 'hsl(0 0% 90%)')};
flex: 1; flex: 1;
} }
@ -183,7 +189,7 @@ export class DeesChartLog extends DeesElement {
align-items: center; align-items: center;
justify-content: center; justify-content: center;
height: 100%; height: 100%;
color: ${cssManager.bdTheme('#6c757d', '#666')}; color: ${cssManager.bdTheme('hsl(0 0% 45.1%)', 'hsl(0 0% 63.9%)')};
font-style: italic; font-style: italic;
} }
@ -193,16 +199,16 @@ export class DeesChartLog extends DeesElement {
} }
.logContainer::-webkit-scrollbar-track { .logContainer::-webkit-scrollbar-track {
background: ${cssManager.bdTheme('#e9ecef', '#1a1a1a')}; background: ${cssManager.bdTheme('hsl(0 0% 95%)', 'hsl(0 0% 10%)')};
} }
.logContainer::-webkit-scrollbar-thumb { .logContainer::-webkit-scrollbar-thumb {
background: ${cssManager.bdTheme('#adb5bd', '#444')}; background: ${cssManager.bdTheme('hsl(0 0% 70%)', 'hsl(0 0% 30%)')};
border-radius: 4px; border-radius: 4px;
} }
.logContainer::-webkit-scrollbar-thumb:hover { .logContainer::-webkit-scrollbar-thumb:hover {
background: ${cssManager.bdTheme('#6c757d', '#555')}; background: ${cssManager.bdTheme('hsl(0 0% 60%)', 'hsl(0 0% 40%)')};
} }
`, `,
]; ];

View File

@ -1,41 +1,112 @@
import { html } from '@design.estate/dees-element'; import { html, cssManager } from '@design.estate/dees-element';
export const demoFunc = () => html` export const demoFunc = () => html`
<style> <style>
.demoContainer { .demoContainer {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; gap: 32px;
justify-content: center; padding: 48px;
height: 100%; background: ${cssManager.bdTheme('#f8f9fa', '#0a0a0a')};
background: #222; min-height: 100vh;
}
.section {
background: ${cssManager.bdTheme('#ffffff', '#18181b')};
border: 1px solid ${cssManager.bdTheme('#e5e7eb', '#27272a')};
border-radius: 8px;
padding: 24px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.section-title {
font-size: 18px;
font-weight: 600;
margin-bottom: 16px;
color: ${cssManager.bdTheme('#09090b', '#fafafa')};
}
.section-description {
font-size: 14px;
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
margin-bottom: 16px;
} }
</style> </style>
<div class="demoContainer"> <div class="demoContainer">
<dees-chips <div class="section">
selectionMode="none" <div class="section-title">Non-Selectable Chips</div>
.selectableChips=${[ <div class="section-description">Basic chips without selection capability. Use for display-only tags.</div>
{ key: 'account1', value: 'Payment Account 1' }, <dees-chips
{ key: 'account2', value: 'PaymentAccount2' }, selectionMode="none"
{ key: 'account3', value: 'Payment Account 3' }, .selectableChips=${[
]} { key: 'status', value: 'Active' },
></dees-chips> { key: 'tier', value: 'Premium' },
<dees-chips { key: 'region', value: 'EU-West' },
selectionMode="single" { key: 'type', value: 'Enterprise' },
chipsAreRemovable ]}
.selectableChips=${[ ></dees-chips>
{ key: 'account1', value: 'Payment Account 1' }, </div>
{ key: 'account2', value: 'PaymentAccount2' },
{ key: 'account3', value: 'Payment Account 3' }, <div class="section">
]} <div class="section-title">Single Selection Chips</div>
></dees-chips> <div class="section-description">Click to select one chip at a time. Useful for filters and options.</div>
<dees-chips <dees-chips
selectionMode="multiple" selectionMode="single"
.selectableChips=${[ .selectableChips=${[
{ key: 'account1', value: 'Payment Account 1' }, { key: 'all', value: 'All Projects' },
{ key: 'account2', value: 'PaymentAccount2' }, { key: 'active', value: 'Active' },
{ key: 'account3', value: 'Payment Account 3' }, { key: 'archived', value: 'Archived' },
]} { key: 'drafts', value: 'Drafts' },
></dees-chips> ]}
></dees-chips>
</div>
<div class="section">
<div class="section-title">Multiple Selection Chips</div>
<div class="section-description">Select multiple chips simultaneously. Great for tag selection.</div>
<dees-chips
selectionMode="multiple"
.selectableChips=${[
{ key: 'js', value: 'JavaScript' },
{ key: 'ts', value: 'TypeScript' },
{ key: 'react', value: 'React' },
{ key: 'vue', value: 'Vue' },
{ key: 'angular', value: 'Angular' },
{ key: 'node', value: 'Node.js' },
]}
></dees-chips>
</div>
<div class="section">
<div class="section-title">Removable Chips with Keys</div>
<div class="section-description">Chips with remove buttons and key-value pairs. Perfect for dynamic lists.</div>
<dees-chips
selectionMode="single"
chipsAreRemovable
.selectableChips=${[
{ key: 'env', value: 'Production' },
{ key: 'version', value: '2.4.1' },
{ key: 'branch', value: 'main' },
{ key: 'author', value: 'John Doe' },
]}
></dees-chips>
</div>
<div class="section">
<div class="section-title">Mixed Content Example</div>
<div class="section-description">Combining different chip types for complex UIs.</div>
<dees-chips
selectionMode="multiple"
chipsAreRemovable
.selectableChips=${[
{ key: 'priority', value: 'High' },
{ key: 'status', value: 'In Progress' },
{ key: 'bug', value: 'Bug' },
{ key: 'feature', value: 'Feature' },
{ key: 'sprint', value: 'Sprint 23' },
{ key: 'assignee', value: 'Alice' },
]}
></dees-chips>
</div>
</div> </div>
`; `;

View File

@ -60,52 +60,93 @@ export class DeesChips extends DeesElement {
.mainbox { .mainbox {
user-select: none; user-select: none;
display: flex;
flex-wrap: wrap;
gap: 8px;
} }
.chip { .chip {
border-top: ${cssManager.bdTheme('1px solid #CCC', '1px solid #444')}; background: ${cssManager.bdTheme('#f4f4f5', '#27272a')};
background: #333333; border: 1px solid ${cssManager.bdTheme('#e5e7eb', '#3f3f46')};
display: inline-flex; display: inline-flex;
height: 20px; align-items: center;
line-height: 20px; height: 32px;
padding: 0px 8px; padding: 0px 12px;
font-size: 12px; font-size: 14px;
color: #fff; font-weight: 500;
border-radius: 40px; color: ${cssManager.bdTheme('#09090b', '#fafafa')};
margin-right: 4px; border-radius: 6px;
margin-bottom: 4px;
position: relative; position: relative;
overflow: hidden; cursor: pointer;
box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.3); transition: all 0.15s ease;
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
} }
.chip:hover { .chip:hover {
background: #666666; background: ${cssManager.bdTheme('#e5e7eb', '#3f3f46')};
border-color: ${cssManager.bdTheme('#d1d5db', '#52525b')};
}
.chip:active {
transform: scale(0.98);
} }
.chip.selected { .chip.selected {
background: #00a3ff; background: ${cssManager.bdTheme('#3b82f6', '#3b82f6')};
border-color: ${cssManager.bdTheme('#3b82f6', '#3b82f6')};
color: #ffffff;
}
.chip.selected:hover {
background: ${cssManager.bdTheme('#2563eb', '#2563eb')};
border-color: ${cssManager.bdTheme('#2563eb', '#2563eb')};
} }
.chipKey { .chipKey {
background: rgba(0, 0, 0, 0.3); background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.06)', 'rgba(255, 255, 255, 0.1)')};
height: 100%; height: 20px;
display: inline-block; line-height: 20px;
display: inline-flex;
align-items: center;
margin-left: -8px; margin-left: -8px;
padding-left: 8px; padding: 0px 8px;
padding-right: 8px;
margin-right: 8px; margin-right: 8px;
border-radius: 4px;
font-size: 12px;
font-weight: 600;
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
}
.chip.selected .chipKey {
background: rgba(255, 255, 255, 0.2);
color: rgba(255, 255, 255, 0.9);
} }
dees-icon { dees-icon {
padding: 0px 6px 0px 4px; display: flex;
margin-left: 4px; align-items: center;
margin-right: -8px; justify-content: center;
background: rgba(0, 0, 0, 0.3); width: 16px;
height: 16px;
margin-left: 8px;
margin-right: -6px;
border-radius: 3px;
transition: all 0.15s ease;
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
}
.chip.selected dees-icon {
color: rgba(255, 255, 255, 0.8);
} }
dees-icon:hover { dees-icon:hover {
background: #e4002b; background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.1)', 'rgba(255, 255, 255, 0.1)')};
color: ${cssManager.bdTheme('#ef4444', '#ef4444')};
}
.chip.selected dees-icon:hover {
background: rgba(255, 255, 255, 0.2);
color: #ffffff;
} }
`, `,
]; ];
@ -127,7 +168,7 @@ export class DeesChips extends DeesElement {
event.stopPropagation(); // prevent the selectChip event from being triggered event.stopPropagation(); // prevent the selectChip event from being triggered
this.removeChip(chip); this.removeChip(chip);
}} }}
.iconFA=${'xmark'} .icon=${'fa:xmark'}
></dees-icon> ></dees-icon>
` `
: html``} : html``}
@ -139,19 +180,25 @@ export class DeesChips extends DeesElement {
} }
public async firstUpdated() { public async firstUpdated() {
if (!this.textContent) { // Component initialized
this.textContent = 'Button';
this.performUpdate();
}
} }
private isSelected(chip: Tag): boolean { private isSelected(chip: Tag): boolean {
if (this.selectionMode === 'single') { if (this.selectionMode === 'single') {
return this.selectedChip?.key === chip.key; return this.selectedChip ? this.isSameChip(this.selectedChip, chip) : false;
} else { } else {
return this.selectedChips.some((selected) => selected.key === chip.key); return this.selectedChips.some((selected) => this.isSameChip(selected, chip));
} }
} }
private isSameChip(chip1: Tag, chip2: Tag): boolean {
// If both have keys, compare by key
if (chip1.key && chip2.key) {
return chip1.key === chip2.key;
}
// Otherwise compare by value (and key if present)
return chip1.value === chip2.value && chip1.key === chip2.key;
}
public async selectChip(chip: Tag) { public async selectChip(chip: Tag) {
if (this.selectionMode === 'none') { if (this.selectionMode === 'none') {
@ -168,7 +215,7 @@ export class DeesChips extends DeesElement {
} }
} else if (this.selectionMode === 'multiple') { } else if (this.selectionMode === 'multiple') {
if (this.isSelected(chip)) { if (this.isSelected(chip)) {
this.selectedChips = this.selectedChips.filter((selected) => selected.key !== chip.key); this.selectedChips = this.selectedChips.filter((selected) => !this.isSameChip(selected, chip));
} else { } else {
this.selectedChips = [...this.selectedChips, chip]; this.selectedChips = [...this.selectedChips, chip];
} }
@ -179,13 +226,13 @@ export class DeesChips extends DeesElement {
public removeChip(chipToRemove: Tag): void { public removeChip(chipToRemove: Tag): void {
// Remove the chip from selectableChips // Remove the chip from selectableChips
this.selectableChips = this.selectableChips.filter((chip) => chip.key !== chipToRemove.key); this.selectableChips = this.selectableChips.filter((chip) => !this.isSameChip(chip, chipToRemove));
// Remove the chip from selectedChips if present // Remove the chip from selectedChips if present
this.selectedChips = this.selectedChips.filter((chip) => chip.key !== chipToRemove.key); this.selectedChips = this.selectedChips.filter((chip) => !this.isSameChip(chip, chipToRemove));
// If the removed chip was the selectedChip, set selectedChip to null // If the removed chip was the selectedChip, set selectedChip to null
if (this.selectedChip && this.selectedChip.key === chipToRemove.key) { if (this.selectedChip && this.isSameChip(this.selectedChip, chipToRemove)) {
this.selectedChip = null; this.selectedChip = null;
} }

View File

@ -13,139 +13,203 @@ export const demoFunc = () => html`
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 20px; gap: 20px;
padding: 40px; padding: 20px;
background: #f5f5f5;
min-height: 400px; min-height: 400px;
} }
.demo-area { .demo-area {
background: white;
padding: 40px; padding: 40px;
border-radius: 8px; border-radius: 8px;
border: 1px solid #e0e0e0;
text-align: center; text-align: center;
cursor: context-menu; cursor: context-menu;
transition: background 0.2s;
}
.demo-area:hover {
background: rgba(0, 0, 0, 0.02);
} }
</style> </style>
<div class="demo-container"> <div class="demo-container">
<div class="demo-area" @contextmenu=${(eventArg: MouseEvent) => { <dees-panel heading="Basic Context Menu with Nested Submenus">
DeesContextmenu.openContextMenuWithOptions(eventArg, [ <div class="demo-area" @contextmenu=${(eventArg: MouseEvent) => {
{ DeesContextmenu.openContextMenuWithOptions(eventArg, [
name: 'Cut', {
iconName: 'scissors', name: 'File',
shortcut: 'Cmd+X', iconName: 'fileText',
action: async () => { action: async () => {}, // Parent items with submenus still need an action
console.log('Cut action'); submenu: [
{ name: 'New', iconName: 'filePlus', shortcut: 'Cmd+N', action: async () => console.log('New file') },
{ name: 'Open', iconName: 'folderOpen', shortcut: 'Cmd+O', action: async () => console.log('Open file') },
{ name: 'Save', iconName: 'save', shortcut: 'Cmd+S', action: async () => console.log('Save') },
{ divider: true },
{ name: 'Export as PDF', iconName: 'download', action: async () => console.log('Export PDF') },
{ name: 'Export as HTML', iconName: 'code', action: async () => console.log('Export HTML') },
]
}, },
}, {
{ name: 'Edit',
name: 'Copy', iconName: 'edit3',
iconName: 'copy', action: async () => {}, // Parent items with submenus still need an action
shortcut: 'Cmd+C', submenu: [
action: async () => { { name: 'Cut', iconName: 'scissors', shortcut: 'Cmd+X', action: async () => console.log('Cut') },
console.log('Copy action'); { name: 'Copy', iconName: 'copy', shortcut: 'Cmd+C', action: async () => console.log('Copy') },
{ name: 'Paste', iconName: 'clipboard', shortcut: 'Cmd+V', action: async () => console.log('Paste') },
{ divider: true },
{ name: 'Find', iconName: 'search', shortcut: 'Cmd+F', action: async () => console.log('Find') },
{ name: 'Replace', iconName: 'repeat', shortcut: 'Cmd+H', action: async () => console.log('Replace') },
]
}, },
}, {
{ name: 'View',
name: 'Paste', iconName: 'eye',
iconName: 'clipboard', action: async () => {}, // Parent items with submenus still need an action
shortcut: 'Cmd+V', submenu: [
action: async () => { { name: 'Zoom In', iconName: 'zoomIn', shortcut: 'Cmd++', action: async () => console.log('Zoom in') },
console.log('Paste action'); { name: 'Zoom Out', iconName: 'zoomOut', shortcut: 'Cmd+-', action: async () => console.log('Zoom out') },
{ name: 'Reset Zoom', iconName: 'maximize2', shortcut: 'Cmd+0', action: async () => console.log('Reset zoom') },
{ divider: true },
{ name: 'Full Screen', iconName: 'maximize', shortcut: 'F11', action: async () => console.log('Full screen') },
]
}, },
}, { divider: true },
{ divider: true }, {
{ name: 'Settings',
name: 'Delete', iconName: 'settings',
iconName: 'trash2', action: async () => console.log('Settings')
action: async () => {
console.log('Delete action');
}, },
}, {
{ divider: true }, name: 'Help',
{ iconName: 'helpCircle',
name: 'Select All', action: async () => {}, // Parent items with submenus still need an action
shortcut: 'Cmd+A', submenu: [
action: async () => { { name: 'Documentation', iconName: 'book', action: async () => console.log('Documentation') },
console.log('Select All action'); { name: 'Keyboard Shortcuts', iconName: 'keyboard', action: async () => console.log('Shortcuts') },
{ divider: true },
{ name: 'About', iconName: 'info', action: async () => console.log('About') },
]
}
]);
}}>
<h3>Right-click anywhere in this area</h3>
<p>A context menu with nested submenus will appear</p>
</div>
</dees-panel>
<dees-panel heading="Component-Specific Context Menu">
<dees-button style="margin: 20px;" @contextmenu=${(eventArg: MouseEvent) => {
DeesContextmenu.openContextMenuWithOptions(eventArg, [
{
name: 'Button Actions',
iconName: 'mousePointer',
action: async () => {}, // Parent items with submenus still need an action
submenu: [
{ name: 'Click', iconName: 'mouse', action: async () => console.log('Click action') },
{ name: 'Double Click', iconName: 'zap', action: async () => console.log('Double click') },
{ name: 'Long Press', iconName: 'clock', action: async () => console.log('Long press') },
]
}, },
}, {
]); name: 'Button State',
}}> iconName: 'toggleLeft',
<h3>Right-click anywhere in this area</h3> action: async () => {}, // Parent items with submenus still need an action
<p>A context menu will appear with various options</p> submenu: [
</div> { name: 'Enable', iconName: 'checkCircle', action: async () => console.log('Enable') },
{ name: 'Disable', iconName: 'xCircle', action: async () => console.log('Disable') },
{ divider: true },
{ name: 'Show', iconName: 'eye', action: async () => console.log('Show') },
{ name: 'Hide', iconName: 'eyeOff', action: async () => console.log('Hide') },
]
},
{ divider: true },
{
name: 'Disabled Action',
iconName: 'ban',
disabled: true,
action: async () => console.log('This should not run'),
},
{
name: 'Properties',
iconName: 'settings',
action: async () => console.log('Button properties'),
},
]);
}}>Right-click on this button</dees-button>
</dees-panel>
<dees-panel heading="Advanced Context Menu Example">
<div class="demo-area" @contextmenu=${(eventArg: MouseEvent) => {
DeesContextmenu.openContextMenuWithOptions(eventArg, [
{
name: 'Format',
iconName: 'type',
action: async () => {}, // Parent items with submenus still need an action
submenu: [
{ name: 'Bold', iconName: 'bold', shortcut: 'Cmd+B', action: async () => console.log('Bold') },
{ name: 'Italic', iconName: 'italic', shortcut: 'Cmd+I', action: async () => console.log('Italic') },
{ name: 'Underline', iconName: 'underline', shortcut: 'Cmd+U', action: async () => console.log('Underline') },
{ divider: true },
{ name: 'Font Size', iconName: 'type', action: async () => console.log('Font size menu') },
{ name: 'Font Color', iconName: 'palette', action: async () => console.log('Font color menu') },
]
},
{
name: 'Transform',
iconName: 'shuffle',
action: async () => {}, // Parent items with submenus still need an action
submenu: [
{ name: 'To Uppercase', iconName: 'arrowUp', action: async () => console.log('Uppercase') },
{ name: 'To Lowercase', iconName: 'arrowDown', action: async () => console.log('Lowercase') },
{ name: 'Capitalize', iconName: 'type', action: async () => console.log('Capitalize') },
]
},
{ divider: true },
{
name: 'Delete',
iconName: 'trash2',
action: async () => console.log('Delete')
}
]);
}}>
<h3>Advanced Nested Menu Example</h3>
<p>This shows deeply nested submenus and various formatting options</p>
</div>
</dees-panel>
<dees-button @contextmenu=${(eventArg: MouseEvent) => { <dees-panel heading="Static Context Menu (Always Visible)">
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 <dees-contextmenu
class="withMargin" class="withMargin"
.menuItems=${[ .menuItems=${[
{ {
name: 'New File', name: 'Project',
iconName: 'filePlus', iconName: 'folder',
shortcut: 'Cmd+N', action: async () => {}, // Parent items with submenus still need an action
action: async () => console.log('New file'), submenu: [
{ name: 'New Project', iconName: 'folderPlus', shortcut: 'Cmd+Shift+N', action: async () => console.log('New project') },
{ name: 'Open Project', iconName: 'folderOpen', shortcut: 'Cmd+Shift+O', action: async () => console.log('Open project') },
{ divider: true },
{ name: 'Recent Projects', iconName: 'clock', action: async () => {}, submenu: [
{ name: 'Project Alpha', action: async () => console.log('Open Alpha') },
{ name: 'Project Beta', action: async () => console.log('Open Beta') },
{ name: 'Project Gamma', action: async () => console.log('Open Gamma') },
]},
]
}, },
{ {
name: 'Open File', name: 'Tools',
iconName: 'folderOpen', iconName: 'tool',
shortcut: 'Cmd+O', action: async () => {}, // Parent items with submenus still need an action
action: async () => console.log('Open file'), submenu: [
}, { name: 'Terminal', iconName: 'terminal', shortcut: 'Cmd+T', action: async () => console.log('Terminal') },
{ { name: 'Console', iconName: 'monitor', shortcut: 'Cmd+K', action: async () => console.log('Console') },
name: 'Save', { divider: true },
iconName: 'save', { name: 'Extensions', iconName: 'package', action: async () => console.log('Extensions') },
shortcut: 'Cmd+S', ]
action: async () => console.log('Save'),
}, },
{ divider: true }, { divider: true },
{ {
name: 'Export', name: 'Preferences',
iconName: 'download', iconName: 'sliders',
action: async () => console.log('Export'), action: async () => console.log('Preferences'),
},
{
name: 'Import',
iconName: 'upload',
action: async () => console.log('Import'),
}, },
]} ]}
></dees-contextmenu> ></dees-contextmenu>
</div> </dees-panel>
</div> </div>
`; `;

View File

@ -31,7 +31,7 @@ export class DeesContextmenu extends DeesElement {
// STATIC // STATIC
// This will store all the accumulated menu items // This will store all the accumulated menu items
public static contextMenuDeactivated = false; public static contextMenuDeactivated = false;
public static accumulatedMenuItems: (plugins.tsclass.website.IMenuItem & { shortcut?: string; disabled?: boolean } | { divider: true })[] = []; public static accumulatedMenuItems: (plugins.tsclass.website.IMenuItem & { shortcut?: string; disabled?: boolean; submenu?: (plugins.tsclass.website.IMenuItem & { shortcut?: string; disabled?: boolean } | { divider: true })[] } | { divider: true })[] = [];
// Add a global event listener for the right-click context menu // Add a global event listener for the right-click context menu
public static initializeGlobalListener() { public static initializeGlobalListener() {
@ -41,16 +41,16 @@ export class DeesContextmenu extends DeesElement {
} }
event.preventDefault(); event.preventDefault();
// Get the target element of the right-click
let target: EventTarget | null = event.target;
// Clear previously accumulated items // Clear previously accumulated items
DeesContextmenu.accumulatedMenuItems = []; DeesContextmenu.accumulatedMenuItems = [];
// Traverse up the DOM tree to accumulate menu items // Use composedPath to properly traverse shadow DOM boundaries
while (target) { const path = event.composedPath();
if ((target as any).getContextMenuItems) {
const items = (target as any).getContextMenuItems(); // Traverse the composed path to accumulate menu items
for (const element of path) {
if ((element as any).getContextMenuItems) {
const items = (element as any).getContextMenuItems();
if (items && items.length > 0) { if (items && items.length > 0) {
if (DeesContextmenu.accumulatedMenuItems.length > 0) { if (DeesContextmenu.accumulatedMenuItems.length > 0) {
DeesContextmenu.accumulatedMenuItems.push({ divider: true }); DeesContextmenu.accumulatedMenuItems.push({ divider: true });
@ -58,7 +58,6 @@ export class DeesContextmenu extends DeesElement {
DeesContextmenu.accumulatedMenuItems.push(...items); DeesContextmenu.accumulatedMenuItems.push(...items);
} }
} }
target = (target as Node).parentNode;
} }
// Open the context menu with the accumulated items // Open the context menu with the accumulated items
@ -67,7 +66,7 @@ export class DeesContextmenu extends DeesElement {
} }
// allows opening of a contextmenu with options // allows opening of a contextmenu with options
public static async openContextMenuWithOptions(eventArg: MouseEvent, menuItemsArg: (plugins.tsclass.website.IMenuItem & { shortcut?: string; disabled?: boolean } | { divider: true })[]) { public static async openContextMenuWithOptions(eventArg: MouseEvent, menuItemsArg: (plugins.tsclass.website.IMenuItem & { shortcut?: string; disabled?: boolean; submenu?: (plugins.tsclass.website.IMenuItem & { shortcut?: string; disabled?: boolean } | { divider: true })[] } | { divider: true })[]) {
if (this.contextMenuDeactivated) { if (this.contextMenuDeactivated) {
return; return;
} }
@ -80,8 +79,13 @@ export class DeesContextmenu extends DeesElement {
contextMenu.style.transform = 'scale(0.95) translateY(-10px)'; contextMenu.style.transform = 'scale(0.95) translateY(-10px)';
contextMenu.menuItems = menuItemsArg; contextMenu.menuItems = menuItemsArg;
contextMenu.windowLayer = await DeesWindowLayer.createAndShow(); contextMenu.windowLayer = await DeesWindowLayer.createAndShow();
contextMenu.windowLayer.addEventListener('click', async () => { contextMenu.windowLayer.addEventListener('click', async (event) => {
await contextMenu.destroy(); // Check if click is on the context menu or its submenus
const clickedElement = event.target as HTMLElement;
const isContextMenu = clickedElement.closest('dees-contextmenu');
if (!isContextMenu) {
await contextMenu.destroy();
}
}) })
document.body.append(contextMenu); document.body.append(contextMenu);
@ -123,8 +127,12 @@ export class DeesContextmenu extends DeesElement {
@property({ @property({
type: Array, type: Array,
}) })
public menuItems: (plugins.tsclass.website.IMenuItem & { shortcut?: string; disabled?: boolean; divider?: never } | { divider: true })[] = []; public menuItems: (plugins.tsclass.website.IMenuItem & { shortcut?: string; disabled?: boolean; submenu?: (plugins.tsclass.website.IMenuItem & { shortcut?: string; disabled?: boolean } | { divider: true })[]; divider?: never } | { divider: true })[] = [];
windowLayer: DeesWindowLayer; windowLayer: DeesWindowLayer;
private submenu: DeesContextmenu | null = null;
private submenuTimeout: any = null;
private parentMenu: DeesContextmenu | null = null;
constructor() { constructor() {
super(); super();
@ -167,13 +175,22 @@ export class DeesContextmenu extends DeesElement {
cursor: default; cursor: default;
transition: background 0.1s; transition: background 0.1s;
line-height: 1; line-height: 1;
position: relative;
} }
.menuitem:hover { .menuitem:hover {
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.04)', 'rgba(255, 255, 255, 0.08)')}; background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.04)', 'rgba(255, 255, 255, 0.08)')};
} }
.menuitem.has-submenu::after {
content: '';
position: absolute;
right: 8px;
font-size: 16px;
opacity: 0.5;
}
.menuitem:active { .menuitem:active:not(.has-submenu) {
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.08)', 'rgba(255, 255, 255, 0.12)')}; background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.08)', 'rgba(255, 255, 255, 0.12)')};
} }
@ -215,14 +232,20 @@ export class DeesContextmenu extends DeesElement {
return html`<div class="menu-divider"></div>`; return html`<div class="menu-divider"></div>`;
} }
const menuItem = menuItemArg as plugins.tsclass.website.IMenuItem & { shortcut?: string; disabled?: boolean }; const menuItem = menuItemArg as plugins.tsclass.website.IMenuItem & { shortcut?: string; disabled?: boolean; submenu?: any };
const hasSubmenu = menuItem.submenu && menuItem.submenu.length > 0;
return html` return html`
<div class="menuitem ${menuItem.disabled ? 'disabled' : ''}" @click=${() => !menuItem.disabled && this.handleClick(menuItem)}> <div
class="menuitem ${menuItem.disabled ? 'disabled' : ''} ${hasSubmenu ? 'has-submenu' : ''}"
@click=${() => !menuItem.disabled && !hasSubmenu && this.handleClick(menuItem)}
@mouseenter=${() => this.handleMenuItemHover(menuItem, hasSubmenu)}
@mouseleave=${() => this.handleMenuItemLeave()}
>
${menuItem.iconName ? html` ${menuItem.iconName ? html`
<dees-icon .icon="${`lucide:${menuItem.iconName}`}"></dees-icon> <dees-icon .icon="${`lucide:${menuItem.iconName}`}"></dees-icon>
` : ''} ` : ''}
<span class="menuitem-text">${menuItem.name}</span> <span class="menuitem-text">${menuItem.name}</span>
${menuItem.shortcut ? html` ${menuItem.shortcut && !hasSubmenu ? html`
<span class="menuitem-shortcut">${menuItem.shortcut}</span> <span class="menuitem-shortcut">${menuItem.shortcut}</span>
` : ''} ` : ''}
</div> </div>
@ -282,17 +305,151 @@ export class DeesContextmenu extends DeesElement {
public async handleClick(menuItem: plugins.tsclass.website.IMenuItem & { shortcut?: string; disabled?: boolean }) { public async handleClick(menuItem: plugins.tsclass.website.IMenuItem & { shortcut?: string; disabled?: boolean }) {
menuItem.action(); menuItem.action();
await this.destroy();
// Close all menus in the chain (this menu and all parent menus)
await this.destroyAll();
}
private async handleMenuItemHover(menuItem: plugins.tsclass.website.IMenuItem & { submenu?: any }, hasSubmenu: boolean) {
// Clear any existing timeout
if (this.submenuTimeout) {
clearTimeout(this.submenuTimeout);
this.submenuTimeout = null;
}
// Hide any existing submenu if hovering a different item
if (this.submenu) {
await this.hideSubmenu();
}
// Show submenu if this item has one
if (hasSubmenu && menuItem.submenu) {
this.submenuTimeout = setTimeout(() => {
this.showSubmenu(menuItem);
}, 200); // Small delay to prevent accidental triggers
}
}
private handleMenuItemLeave() {
// Add a delay before hiding to allow moving to submenu
if (this.submenuTimeout) {
clearTimeout(this.submenuTimeout);
}
this.submenuTimeout = setTimeout(() => {
if (this.submenu && !this.submenu.matches(':hover')) {
this.hideSubmenu();
}
}, 300);
}
private async showSubmenu(menuItem: plugins.tsclass.website.IMenuItem & { submenu?: any }) {
if (!menuItem.submenu || menuItem.submenu.length === 0) return;
// Find the menu item element
const menuItems = Array.from(this.shadowRoot.querySelectorAll('.menuitem'));
const menuItemElement = menuItems.find(el => el.querySelector('.menuitem-text')?.textContent === menuItem.name) as HTMLElement;
if (!menuItemElement) return;
// Create submenu
this.submenu = new DeesContextmenu();
this.submenu.menuItems = menuItem.submenu;
this.submenu.parentMenu = this;
this.submenu.style.position = 'fixed';
this.submenu.style.zIndex = String(parseInt(this.style.zIndex) + 1);
this.submenu.style.opacity = '0';
this.submenu.style.transform = 'scale(0.95)';
// Don't create a window layer for submenus
document.body.append(this.submenu);
// Position submenu
await domtools.plugins.smartdelay.delayFor(0);
const itemRect = menuItemElement.getBoundingClientRect();
const menuRect = this.getBoundingClientRect();
const submenuRect = this.submenu.getBoundingClientRect();
const windowWidth = window.innerWidth;
let left = menuRect.right - 4; // Slight overlap
let top = itemRect.top;
// Check if submenu would go off right edge
if (left + submenuRect.width > windowWidth - 10) {
// Show on left side instead
left = menuRect.left - submenuRect.width + 4;
}
// Adjust vertical position if needed
if (top + submenuRect.height > window.innerHeight - 10) {
top = window.innerHeight - submenuRect.height - 10;
}
this.submenu.style.left = `${left}px`;
this.submenu.style.top = `${top}px`;
// Animate in
await domtools.plugins.smartdelay.delayFor(0);
this.submenu.style.opacity = '1';
this.submenu.style.transform = 'scale(1)';
// Handle submenu hover
this.submenu.addEventListener('mouseenter', () => {
if (this.submenuTimeout) {
clearTimeout(this.submenuTimeout);
this.submenuTimeout = null;
}
});
this.submenu.addEventListener('mouseleave', () => {
this.handleMenuItemLeave();
});
}
private async hideSubmenu() {
if (!this.submenu) return;
await this.submenu.destroy();
this.submenu = null;
} }
public async destroy() { public async destroy() {
if (this.windowLayer) { // Clear timeout
if (this.submenuTimeout) {
clearTimeout(this.submenuTimeout);
this.submenuTimeout = null;
}
// Destroy submenu first
if (this.submenu) {
await this.submenu.destroy();
this.submenu = null;
}
// Only destroy window layer if this is not a submenu
if (this.windowLayer && !this.parentMenu) {
this.windowLayer.destroy(); this.windowLayer.destroy();
} }
this.style.opacity = '0'; this.style.opacity = '0';
this.style.transform = 'scale(0.95) translateY(-10px)'; this.style.transform = 'scale(0.95) translateY(-10px)';
await domtools.plugins.smartdelay.delayFor(100); await domtools.plugins.smartdelay.delayFor(100);
this.parentElement.removeChild(this);
if (this.parentElement) {
this.parentElement.removeChild(this);
}
}
/**
* Destroys this menu and all parent menus in the chain
*/
public async destroyAll() {
// First destroy parent menus if they exist
if (this.parentMenu) {
await this.parentMenu.destroyAll();
} else {
// If we're at the top level, just destroy this menu
await this.destroy();
}
} }
} }

View File

@ -0,0 +1,191 @@
import { html, css, cssManager } from '@design.estate/dees-element';
import type { DeesDashboardgrid } from './dees-dashboardgrid.js';
import '@design.estate/dees-wcctools/demotools';
export const demoFunc = () => {
return html`
<dees-demowrapper .runAfterRender=${async (elementArg: HTMLElement) => {
const grid = elementArg.querySelector('#dashboardGrid') as DeesDashboardgrid;
// Set initial widgets
grid.widgets = [
{
id: 'metrics1',
x: 0,
y: 0,
w: 3,
h: 2,
title: 'Revenue',
icon: 'lucide:dollarSign',
content: html`
<div style="padding: 20px;">
<div style="font-size: 32px; font-weight: 700; color: ${cssManager.bdTheme('#09090b', '#fafafa')};">$124,563</div>
<div style="color: #22c55e; font-size: 14px; margin-top: 8px;">↑ 12.5% from last month</div>
</div>
`
},
{
id: 'metrics2',
x: 3,
y: 0,
w: 3,
h: 2,
title: 'Users',
icon: 'lucide:users',
content: html`
<div style="padding: 20px;">
<div style="font-size: 32px; font-weight: 700; color: ${cssManager.bdTheme('#09090b', '#fafafa')};">8,234</div>
<div style="color: #3b82f6; font-size: 14px; margin-top: 8px;"> 5.2% from last week</div>
</div>
`
},
{
id: 'chart1',
x: 6,
y: 0,
w: 6,
h: 4,
title: 'Analytics',
icon: 'lucide:lineChart',
content: html`
<div style="padding: 20px; height: 100%; display: flex; align-items: center; justify-content: center;">
<div style="text-align: center; color: #71717a;">
<dees-icon .icon=${'lucide:lineChart'} style="font-size: 48px; margin-bottom: 16px;"></dees-icon>
<div>Chart visualization area</div>
</div>
</div>
`
}
];
// Configure grid
grid.cellHeight = 80;
grid.margin = { top: 10, right: 10, bottom: 10, left: 10 };
grid.enableAnimation = true;
grid.showGridLines = false;
let widgetCounter = 4;
// Control buttons
const buttons = elementArg.querySelectorAll('dees-button');
buttons.forEach(button => {
const text = button.textContent?.trim();
if (text === 'Toggle Animation') {
button.addEventListener('click', () => {
grid.enableAnimation = !grid.enableAnimation;
});
} else if (text === 'Toggle Grid Lines') {
button.addEventListener('click', () => {
grid.showGridLines = !grid.showGridLines;
});
} else if (text === 'Add Widget') {
button.addEventListener('click', () => {
const newWidget = {
id: `widget${widgetCounter++}`,
x: 0,
y: 0,
w: 3,
h: 2,
autoPosition: true,
title: `Widget ${widgetCounter - 1}`,
icon: 'lucide:package',
content: html`
<div style="padding: 20px; text-align: center;">
<div style="color: #71717a;">New widget content</div>
<div style="margin-top: 8px; font-size: 24px; font-weight: 600; color: ${cssManager.bdTheme('#09090b', '#fafafa')};">${Math.floor(Math.random() * 1000)}</div>
</div>
`
};
grid.addWidget(newWidget, true);
});
} else if (text === 'Compact Grid') {
button.addEventListener('click', () => {
grid.compact();
});
} else if (text === 'Toggle Edit Mode') {
button.addEventListener('click', () => {
grid.editable = !grid.editable;
button.textContent = grid.editable ? 'Lock Grid' : 'Unlock Grid';
});
}
});
// Listen to grid events
grid.addEventListener('widget-move', (e: CustomEvent) => {
console.log('Widget moved:', e.detail.widget);
});
grid.addEventListener('widget-resize', (e: CustomEvent) => {
console.log('Widget resized:', e.detail.widget);
});
}}>
<style>
${css`
.demoBox {
position: relative;
background: ${cssManager.bdTheme('#f4f4f5', '#09090b')};
height: 100%;
width: 100%;
padding: 40px;
box-sizing: border-box;
display: flex;
flex-direction: column;
gap: 24px;
}
.demo-controls {
display: flex;
flex-wrap: wrap;
gap: 12px;
}
.demo-controls dees-button {
flex-shrink: 0;
}
.grid-container-wrapper {
flex: 1;
min-height: 600px;
position: relative;
}
.info {
color: ${cssManager.bdTheme('#71717a', '#71717a')};
font-size: 12px;
font-family: 'Geist Sans', sans-serif;
text-align: center;
}
`}
</style>
<div class="demoBox">
<div class="demo-controls">
<dees-button-group label="Animation:">
<dees-button>Toggle Animation</dees-button>
</dees-button-group>
<dees-button-group label="Display:">
<dees-button>Toggle Grid Lines</dees-button>
</dees-button-group>
<dees-button-group label="Actions:">
<dees-button>Add Widget</dees-button>
<dees-button>Compact Grid</dees-button>
</dees-button-group>
<dees-button-group label="Mode:">
<dees-button>Toggle Edit Mode</dees-button>
</dees-button-group>
</div>
<div class="grid-container-wrapper">
<dees-dashboardgrid id="dashboardGrid"></dees-dashboardgrid>
</div>
<div class="info">
Drag widgets to reposition • Resize from edges and corners • Add widgets with auto-positioning
</div>
</div>
</dees-demowrapper>
`;
};

View File

@ -0,0 +1,813 @@
import * as plugins from './00plugins.js';
import {
DeesElement,
type TemplateResult,
property,
customElement,
html,
css,
cssManager,
state,
} from '@design.estate/dees-element';
import * as domtools from '@design.estate/dees-domtools';
import './dees-icon.js';
import { demoFunc } from './dees-dashboardgrid.demo.js';
declare global {
interface HTMLElementTagNameMap {
'dees-dashboardgrid': DeesDashboardgrid;
}
}
export interface IDashboardWidget {
id: string;
x: number;
y: number;
w: number;
h: number;
minW?: number;
minH?: number;
maxW?: number;
maxH?: number;
content: TemplateResult | string;
title?: string;
icon?: string;
noMove?: boolean;
noResize?: boolean;
locked?: boolean;
autoPosition?: boolean; // Auto-position widget in first available space
}
@customElement('dees-dashboardgrid')
export class DeesDashboardgrid extends DeesElement {
// STATIC
public static demo = demoFunc;
// INSTANCE
@property({ type: Array })
public widgets: IDashboardWidget[] = [];
@property({ type: Number })
public cellHeight: number = 80;
@property({ type: Object })
public margin: number | { top?: number; right?: number; bottom?: number; left?: number } = 10;
@property({ type: Number })
public columns: number = 12;
@property({ type: Boolean })
public editable: boolean = true;
@property({ type: Boolean, reflect: true })
public enableAnimation: boolean = true;
@property({ type: String })
public cellHeightUnit: 'px' | 'em' | 'rem' | 'auto' = 'px';
@property({ type: Boolean })
public rtl: boolean = false; // Right-to-left support
@property({ type: Boolean })
public showGridLines: boolean = false;
@state()
private draggedWidget: IDashboardWidget | null = null;
@state()
private draggedElement: HTMLElement | null = null;
@state()
private dragOffsetX: number = 0;
@state()
private dragOffsetY: number = 0;
@state()
private dragMouseX: number = 0;
@state()
private dragMouseY: number = 0;
@state()
private placeholderPosition: { x: number; y: number } | null = null;
@state()
private resizingWidget: IDashboardWidget | null = null;
@state()
private resizeStartW: number = 0;
@state()
private resizeStartH: number = 0;
@state()
private resizeStartX: number = 0;
@state()
private resizeStartY: number = 0;
public static styles = [
cssManager.defaultStyles,
css`
:host {
display: block;
width: 100%;
height: 100%;
position: relative;
}
.grid-container {
position: relative;
width: 100%;
min-height: 400px;
box-sizing: border-box;
}
.grid-widget {
position: absolute;
will-change: auto;
}
:host([enableanimation]) .grid-widget {
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.grid-widget.dragging {
z-index: 1000;
transition: none !important;
opacity: 0.8;
cursor: grabbing;
pointer-events: none;
will-change: transform;
}
.grid-widget.placeholder {
pointer-events: none;
z-index: 1;
}
.grid-widget.placeholder .widget-content {
background: ${cssManager.bdTheme('rgba(59, 130, 246, 0.1)', 'rgba(59, 130, 246, 0.1)')};
border: 2px dashed ${cssManager.bdTheme('#3b82f6', '#3b82f6')};
box-shadow: none;
}
.grid-widget.resizing {
transition: none !important;
}
.widget-content {
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
overflow: hidden;
background: ${cssManager.bdTheme('#ffffff', '#09090b')};
border: 1px solid ${cssManager.bdTheme('#e5e7eb', '#27272a')};
border-radius: 8px;
box-shadow: ${cssManager.bdTheme(
'0 1px 3px rgba(0, 0, 0, 0.1)',
'0 1px 3px rgba(0, 0, 0, 0.3)'
)};
transition: box-shadow 0.2s ease;
}
.grid-widget:hover .widget-content {
box-shadow: ${cssManager.bdTheme(
'0 4px 12px rgba(0, 0, 0, 0.15)',
'0 4px 12px rgba(0, 0, 0, 0.4)'
)};
}
.grid-widget.dragging .widget-content {
box-shadow: ${cssManager.bdTheme(
'0 16px 48px rgba(0, 0, 0, 0.25)',
'0 16px 48px rgba(0, 0, 0, 0.6)'
)};
transform: scale(1.05);
}
.widget-header {
padding: 12px 16px;
border-bottom: 1px solid ${cssManager.bdTheme('#e5e7eb', '#27272a')};
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
font-weight: 600;
color: ${cssManager.bdTheme('#09090b', '#fafafa')};
background: ${cssManager.bdTheme('#f9fafb', '#0a0a0a')};
cursor: grab;
user-select: none;
}
.widget-header:hover {
background: ${cssManager.bdTheme('#f4f4f5', '#18181b')};
}
.widget-header:active {
cursor: grabbing;
}
.widget-header.locked {
cursor: default;
}
.widget-header.locked:hover {
background: ${cssManager.bdTheme('#f9fafb', '#0a0a0a')};
}
.widget-header dees-icon {
font-size: 16px;
color: ${cssManager.bdTheme('#71717a', '#71717a')};
}
.widget-body {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
overflow: auto;
color: ${cssManager.bdTheme('#09090b', '#fafafa')};
}
.widget-body.has-header {
top: 45px;
}
/* Resize handles */
.resize-handle {
position: absolute;
background: transparent;
z-index: 10;
}
.resize-handle:hover {
background: ${cssManager.bdTheme('#3b82f6', '#3b82f6')};
opacity: 0.3;
}
.resize-handle-e {
cursor: ew-resize;
width: 12px;
right: -6px;
top: 10%;
height: 80%;
}
.resize-handle-s {
cursor: ns-resize;
height: 12px;
width: 80%;
bottom: -6px;
left: 10%;
}
.resize-handle-se {
cursor: se-resize;
width: 20px;
height: 20px;
right: -2px;
bottom: -2px;
opacity: 0;
transition: opacity 0.2s ease;
}
.resize-handle-se::after {
content: '';
position: absolute;
right: 4px;
bottom: 4px;
width: 6px;
height: 6px;
border-right: 2px solid ${cssManager.bdTheme('#71717a', '#71717a')};
border-bottom: 2px solid ${cssManager.bdTheme('#71717a', '#71717a')};
}
.grid-widget:hover .resize-handle-se {
opacity: 0.7;
}
.resize-handle-se:hover {
opacity: 1 !important;
}
.resize-handle-se:hover::after {
border-color: ${cssManager.bdTheme('#3b82f6', '#3b82f6')};
}
/* Placeholder */
.grid-placeholder {
position: absolute;
background: ${cssManager.bdTheme('#3b82f6', '#3b82f6')};
opacity: 0.1;
border-radius: 8px;
border: 2px dashed ${cssManager.bdTheme('#3b82f6', '#3b82f6')};
transition: all 0.2s ease;
pointer-events: none;
}
/* Empty state */
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 400px;
color: ${cssManager.bdTheme('#71717a', '#71717a')};
text-align: center;
padding: 32px;
}
.empty-state dees-icon {
font-size: 48px;
margin-bottom: 16px;
opacity: 0.5;
}
/* Grid lines */
.grid-lines {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
pointer-events: none;
z-index: -1;
}
.grid-line-vertical {
position: absolute;
top: 0;
bottom: 0;
width: 1px;
background: ${cssManager.bdTheme('#e5e7eb', '#27272a')};
opacity: 0.3;
}
.grid-line-horizontal {
position: absolute;
left: 0;
right: 0;
height: 1px;
background: ${cssManager.bdTheme('#e5e7eb', '#27272a')};
opacity: 0.3;
}
`,
];
public render(): TemplateResult {
if (this.widgets.length === 0) {
return html`
<div class="empty-state">
<dees-icon .icon=${'lucide:layoutGrid'}></dees-icon>
<div>No widgets configured</div>
<div style="font-size: 14px; margin-top: 8px;">Add widgets to populate the dashboard</div>
</div>
`;
}
const margins = this.getMargins();
const maxY = Math.max(...this.widgets.map(w => w.y + w.h), 4);
const cellHeightValue = this.getCellHeight();
const gridHeight = maxY * cellHeightValue + (maxY + 1) * margins.vertical;
return html`
<div class="grid-container" style="height: ${gridHeight}px;">
${this.showGridLines ? this.renderGridLines(gridHeight) : ''}
${this.widgets.map(widget => this.renderWidget(widget))}
${this.placeholderPosition && this.draggedWidget ? this.renderPlaceholder() : ''}
</div>
`;
}
private renderGridLines(gridHeight: number): TemplateResult {
const margins = this.getMargins();
const cellHeightValue = this.getCellHeight();
// Convert margin to percentage for consistent calculation
const containerWidth = this.getBoundingClientRect().width;
const marginHorizontalPercent = (margins.horizontal / containerWidth) * 100;
const cellWidth = (100 - marginHorizontalPercent * (this.columns + 1)) / this.columns;
const verticalLines = [];
const horizontalLines = [];
// Vertical lines
for (let i = 0; i <= this.columns; i++) {
const left = i * cellWidth + i * marginHorizontalPercent;
verticalLines.push(html`
<div class="grid-line-vertical" style="left: ${left}%;"></div>
`);
}
// Horizontal lines
const numHorizontalLines = Math.ceil(gridHeight / (cellHeightValue + margins.vertical));
for (let i = 0; i <= numHorizontalLines; i++) {
const top = i * cellHeightValue + i * margins.vertical;
horizontalLines.push(html`
<div class="grid-line-horizontal" style="top: ${top}px;"></div>
`);
}
return html`
<div class="grid-lines">
${verticalLines}
${horizontalLines}
</div>
`;
}
private renderWidget(widget: IDashboardWidget): TemplateResult {
const isDragging = this.draggedWidget?.id === widget.id;
const isResizing = this.resizingWidget?.id === widget.id;
const isLocked = widget.locked || !this.editable;
const margins = this.getMargins();
const cellHeightValue = this.getCellHeight();
// Convert margin to percentage of container width for consistent calculation
const containerWidth = this.getBoundingClientRect().width;
const marginHorizontalPercent = (margins.horizontal / containerWidth) * 100;
const cellWidth = (100 - marginHorizontalPercent * (this.columns + 1)) / this.columns;
const left = widget.x * cellWidth + (widget.x + 1) * marginHorizontalPercent;
const top = widget.y * cellHeightValue + (widget.y + 1) * margins.vertical;
const width = widget.w * cellWidth + (widget.w - 1) * marginHorizontalPercent;
const height = widget.h * cellHeightValue + (widget.h - 1) * margins.vertical;
// Apply transform when dragging for smooth movement
let transform = '';
if (isDragging && this.draggedElement) {
const containerRect = this.getBoundingClientRect();
const translateX = this.dragMouseX - containerRect.left - this.dragOffsetX - (left / 100 * containerRect.width);
const translateY = this.dragMouseY - containerRect.top - this.dragOffsetY - top;
transform = `transform: translate(${translateX}px, ${translateY}px);`;
}
return html`
<div
class="grid-widget ${isDragging ? 'dragging' : ''} ${isResizing ? 'resizing' : ''}"
style="
${this.rtl ? 'right' : 'left'}: ${left}%;
top: ${top}px;
width: ${width}%;
height: ${height}px;
${transform}
"
data-widget-id="${widget.id}"
>
<div class="widget-content">
${widget.title ? html`
<div
class="widget-header ${isLocked ? 'locked' : ''}"
@mousedown=${!isLocked && !widget.noMove ? (e: MouseEvent) => this.startDrag(e, widget) : null}
>
${widget.icon ? html`<dees-icon .icon=${widget.icon}></dees-icon>` : ''}
${widget.title}
</div>
` : ''}
<div class="widget-body ${widget.title ? 'has-header' : ''}">
${widget.content}
</div>
${!isLocked && !widget.noResize ? html`
<div class="resize-handle resize-handle-e" @mousedown=${(e: MouseEvent) => this.startResize(e, widget, 'e')}></div>
<div class="resize-handle resize-handle-s" @mousedown=${(e: MouseEvent) => this.startResize(e, widget, 's')}></div>
<div class="resize-handle resize-handle-se" @mousedown=${(e: MouseEvent) => this.startResize(e, widget, 'se')}></div>
` : ''}
</div>
</div>
`;
}
private renderPlaceholder(): TemplateResult {
if (!this.placeholderPosition || !this.draggedWidget) return html``;
const margins = this.getMargins();
const cellHeightValue = this.getCellHeight();
// Convert margin to percentage of container width for consistent calculation
const containerWidth = this.getBoundingClientRect().width;
const marginHorizontalPercent = (margins.horizontal / containerWidth) * 100;
const cellWidth = (100 - marginHorizontalPercent * (this.columns + 1)) / this.columns;
const left = this.placeholderPosition.x * cellWidth + (this.placeholderPosition.x + 1) * marginHorizontalPercent;
const top = this.placeholderPosition.y * cellHeightValue + (this.placeholderPosition.y + 1) * margins.vertical;
const width = this.draggedWidget.w * cellWidth + (this.draggedWidget.w - 1) * marginHorizontalPercent;
const height = this.draggedWidget.h * cellHeightValue + (this.draggedWidget.h - 1) * margins.vertical;
return html`
<div
class="grid-widget placeholder"
style="
${this.rtl ? 'right' : 'left'}: ${left}%;
top: ${top}px;
width: ${width}%;
height: ${height}px;
"
>
<div class="widget-content"></div>
</div>
`;
}
private startDrag(e: MouseEvent, widget: IDashboardWidget) {
e.preventDefault();
this.draggedWidget = widget;
this.draggedElement = (e.currentTarget as HTMLElement).closest('.grid-widget') as HTMLElement;
const rect = this.draggedElement.getBoundingClientRect();
this.dragOffsetX = e.clientX - rect.left;
this.dragOffsetY = e.clientY - rect.top;
// Initialize mouse position
this.dragMouseX = e.clientX;
this.dragMouseY = e.clientY;
// Initialize placeholder at current widget position
this.placeholderPosition = { x: widget.x, y: widget.y };
document.addEventListener('mousemove', this.handleDrag);
document.addEventListener('mouseup', this.endDrag);
this.requestUpdate();
}
private handleDrag = (e: MouseEvent) => {
if (!this.draggedWidget || !this.draggedElement) return;
// Update mouse position for smooth dragging
this.dragMouseX = e.clientX;
this.dragMouseY = e.clientY;
const containerRect = this.getBoundingClientRect();
const margins = this.getMargins();
const cellHeightValue = this.getCellHeight();
// Get widget position relative to grid container
const mouseX = e.clientX - containerRect.left - this.dragOffsetX;
const mouseY = e.clientY - containerRect.top - this.dragOffsetY;
// Use pixel calculations for accuracy
const totalWidth = containerRect.width;
const totalMarginWidth = margins.horizontal * (this.columns + 1);
const availableWidth = totalWidth - totalMarginWidth;
const cellWidthPx = availableWidth / this.columns;
// Calculate grid X position
// Account for the initial margin and then repeating pattern of cell+margin
let gridX = 0;
if (mouseX > margins.horizontal) {
const adjustedX = mouseX - margins.horizontal;
const cellPlusMargin = cellWidthPx + margins.horizontal;
gridX = Math.floor(adjustedX / cellPlusMargin + 0.5); // +0.5 for rounding to nearest
}
// Calculate grid Y position
let gridY = 0;
if (mouseY > margins.vertical) {
const adjustedY = mouseY - margins.vertical;
const cellPlusMargin = cellHeightValue + margins.vertical;
gridY = Math.floor(adjustedY / cellPlusMargin + 0.5); // +0.5 for rounding to nearest
}
const clampedX = Math.max(0, Math.min(gridX, this.columns - this.draggedWidget.w));
const clampedY = Math.max(0, gridY);
// Update placeholder position instead of widget position during drag
if (!this.placeholderPosition ||
clampedX !== this.placeholderPosition.x ||
clampedY !== this.placeholderPosition.y) {
const collision = this.checkCollision(this.draggedWidget, clampedX, clampedY);
if (!collision) {
this.placeholderPosition = { x: clampedX, y: clampedY };
this.requestUpdate();
}
}
};
private endDrag = () => {
// Apply final position from placeholder
if (this.draggedWidget && this.placeholderPosition) {
this.draggedWidget.x = this.placeholderPosition.x;
this.draggedWidget.y = this.placeholderPosition.y;
this.dispatchEvent(new CustomEvent('widget-move', {
detail: { widget: this.draggedWidget },
bubbles: true,
composed: true,
}));
}
// Clear drag state
this.draggedWidget = null;
this.draggedElement = null;
this.placeholderPosition = null;
this.dragMouseX = 0;
this.dragMouseY = 0;
document.removeEventListener('mousemove', this.handleDrag);
document.removeEventListener('mouseup', this.endDrag);
this.requestUpdate();
};
private startResize(e: MouseEvent, widget: IDashboardWidget, handle: string) {
e.preventDefault();
e.stopPropagation();
this.resizingWidget = widget;
this.resizeStartW = widget.w;
this.resizeStartH = widget.h;
this.resizeStartX = e.clientX;
this.resizeStartY = e.clientY;
const handleResize = (e: MouseEvent) => {
if (!this.resizingWidget) return;
const containerRect = this.getBoundingClientRect();
const margins = this.getMargins();
const cellHeightValue = this.getCellHeight();
const cellWidth = (containerRect.width - margins.horizontal * (this.columns + 1)) / this.columns;
const deltaX = e.clientX - this.resizeStartX;
const deltaY = e.clientY - this.resizeStartY;
if (handle.includes('e')) {
const newW = Math.round(this.resizeStartW + deltaX / (cellWidth + margins.horizontal));
const maxW = widget.maxW || (this.columns - this.resizingWidget.x);
this.resizingWidget.w = Math.max(widget.minW || 1, Math.min(newW, maxW));
}
if (handle.includes('s')) {
const newH = Math.round(this.resizeStartH + deltaY / (cellHeightValue + margins.vertical));
const maxH = widget.maxH || Infinity;
this.resizingWidget.h = Math.max(widget.minH || 1, Math.min(newH, maxH));
}
this.requestUpdate();
this.dispatchEvent(new CustomEvent('widget-resize', {
detail: { widget: this.resizingWidget },
bubbles: true,
composed: true,
}));
};
const endResize = () => {
this.resizingWidget = null;
document.removeEventListener('mousemove', handleResize);
document.removeEventListener('mouseup', endResize);
};
document.addEventListener('mousemove', handleResize);
document.addEventListener('mouseup', endResize);
}
public removeWidget(widgetId: string) {
this.widgets = this.widgets.filter(w => w.id !== widgetId);
}
public updateWidget(widgetId: string, updates: Partial<IDashboardWidget>) {
this.widgets = this.widgets.map(w =>
w.id === widgetId ? { ...w, ...updates } : w
);
}
public getLayout(): Array<{ id: string; x: number; y: number; w: number; h: number }> {
return this.widgets.map(({ id, x, y, w, h }) => ({ id, x, y, w, h }));
}
public setLayout(layout: Array<{ id: string; x: number; y: number; w: number; h: number }>) {
this.widgets = this.widgets.map(widget => {
const layoutItem = layout.find(l => l.id === widget.id);
return layoutItem ? { ...widget, ...layoutItem } : widget;
});
}
public lockGrid() {
this.editable = false;
}
public unlockGrid() {
this.editable = true;
}
private getMargins(): { horizontal: number; vertical: number; top: number; right: number; bottom: number; left: number } {
if (typeof this.margin === 'number') {
return {
horizontal: this.margin,
vertical: this.margin,
top: this.margin,
right: this.margin,
bottom: this.margin,
left: this.margin,
};
}
const margins = {
top: this.margin.top ?? 10,
right: this.margin.right ?? 10,
bottom: this.margin.bottom ?? 10,
left: this.margin.left ?? 10,
};
return {
...margins,
horizontal: (margins.left + margins.right) / 2,
vertical: (margins.top + margins.bottom) / 2,
};
}
private getCellHeight(): number {
if (this.cellHeightUnit === 'auto') {
// Calculate square cells based on container width
const containerWidth = this.getBoundingClientRect().width;
const margins = this.getMargins();
const cellWidth = (containerWidth - margins.horizontal * (this.columns + 1)) / this.columns;
return cellWidth;
}
return this.cellHeight;
}
private checkCollision(widget: IDashboardWidget, newX: number, newY: number): boolean {
const widgets = this.widgets.filter(w => w.id !== widget.id);
for (const other of widgets) {
if (newX < other.x + other.w &&
newX + widget.w > other.x &&
newY < other.y + other.h &&
newY + widget.h > other.y) {
return true;
}
}
return false;
}
public addWidget(widget: IDashboardWidget, autoPosition = false) {
if (autoPosition || widget.autoPosition) {
// Find first available position
const position = this.findAvailablePosition(widget.w, widget.h);
widget.x = position.x;
widget.y = position.y;
}
this.widgets = [...this.widgets, widget];
}
private findAvailablePosition(width: number, height: number): { x: number; y: number } {
// Try to find space starting from top-left
for (let y = 0; y < 100; y++) { // Reasonable limit
for (let x = 0; x <= this.columns - width; x++) {
const testWidget = { id: 'test', x, y, w: width, h: height, content: '' } as IDashboardWidget;
if (!this.checkCollision(testWidget, x, y)) {
return { x, y };
}
}
}
// If no space found, place at bottom
const maxY = Math.max(...this.widgets.map(w => w.y + w.h), 0);
return { x: 0, y: maxY };
}
public compact(direction: 'vertical' | 'horizontal' = 'vertical') {
const sortedWidgets = [...this.widgets].sort((a, b) => {
if (direction === 'vertical') {
if (a.y !== b.y) return a.y - b.y;
return a.x - b.x;
} else {
if (a.x !== b.x) return a.x - b.x;
return a.y - b.y;
}
});
for (const widget of sortedWidgets) {
if (widget.locked || widget.noMove) continue;
if (direction === 'vertical') {
// Move up as far as possible
while (widget.y > 0 && !this.checkCollision(widget, widget.x, widget.y - 1)) {
widget.y--;
}
} else {
// Move left as far as possible
while (widget.x > 0 && !this.checkCollision(widget, widget.x - 1, widget.y)) {
widget.x--;
}
}
}
this.requestUpdate();
}
}

View File

@ -1,18 +1,199 @@
import { html } from '@design.estate/dees-element'; import { html, cssManager } from '@design.estate/dees-element';
export const demoFunc = () => html` <style> export const demoFunc = () => html`
.demoWrapper { <style>
box-sizing: border-box; .demoWrapper {
position: absolute; box-sizing: border-box;
width: 100%; position: relative;
height: 100%; width: 100%;
padding: 20px; min-height: 100vh;
background: none; padding: 48px;
background: ${cssManager.bdTheme('#f8f9fa', '#0a0a0a')};
display: flex;
flex-direction: column;
gap: 32px;
}
.section {
max-width: 900px;
width: 100%;
margin: 0 auto;
}
.section-title {
font-size: 18px;
font-weight: 600;
margin-bottom: 16px;
color: ${cssManager.bdTheme('#09090b', '#fafafa')};
}
.section-description {
font-size: 14px;
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
margin-bottom: 16px;
}
</style>
<div class="demoWrapper">
<div class="section">
<div class="section-title">TypeScript Code Example</div>
<div class="section-description">A comprehensive TypeScript code example with various syntax highlighting.</div>
<dees-dataview-codebox proglang="typescript">
interface User {
id: number;
name: string;
email: string;
isActive: boolean;
} }
</style>
<div class="demoWrapper"> class UserService {
<dees-dataview-codebox proglang="typescript"> private users: User[] = [];
import * as text from './hello'; const hiThere = 'nice'; const myFunction = async () => {
console.log('nice one'); } constructor(private apiUrl: string) {
</dees-dataview-codebox> console.log('UserService initialized');
</div>` }
async getUsers(): Promise<User[]> {
try {
const response = await fetch(this.apiUrl);
const data = await response.json();
return data.users;
} catch (error) {
console.error('Failed to fetch users:', error);
return [];
}
}
addUser(user: User): void {
this.users.push(user);
}
}
// Usage example
const service = new UserService('https://api.example.com/users');
const users = await service.getUsers();
console.log('Found users:', users.length);
</dees-dataview-codebox>
</div>
<div class="section">
<div class="section-title">JavaScript Example</div>
<div class="section-description">Modern JavaScript with ES6+ features.</div>
<dees-dataview-codebox proglang="javascript">
// Array manipulation examples
const numbers = [1, 2, 3, 4, 5];
const doubled = numbers.map(n => n * 2);
const filtered = numbers.filter(n => n > 3);
// Object destructuring
const user = { name: 'John', age: 30, city: 'New York' };
const { name, age } = user;
// Promise handling
const fetchData = async (url) => {
const response = await fetch(url);
return response.json();
};
// Modern syntax
const greet = (name = 'World') => \`Hello, \${name}!\`;
console.log(greet('ShadCN'));
</dees-dataview-codebox>
</div>
<div class="section">
<div class="section-title">Python Example</div>
<div class="section-description">Python code with classes and type hints.</div>
<dees-dataview-codebox proglang="python">
from typing import List, Optional
import asyncio
class DataProcessor:
"""A simple data processor class"""
def __init__(self, name: str):
self.name = name
self.data: List[dict] = []
async def process_data(self, items: List[dict]) -> List[dict]:
"""Process data items asynchronously"""
results = []
for item in items:
# Simulate async processing
await asyncio.sleep(0.1)
results.append({
'id': item.get('id'),
'processed': True,
'processor': self.name
})
return results
def get_summary(self) -> dict:
return {
'processor': self.name,
'items_processed': len(self.data)
}
# Usage
processor = DataProcessor("Main")
data = await processor.process_data([{'id': 1}, {'id': 2}])
</dees-dataview-codebox>
</div>
<div class="section">
<div class="section-title">CSS Example</div>
<div class="section-description">Modern CSS with custom properties and animations. Note the shorter language label.</div>
<dees-dataview-codebox proglang="css">
/* Modern CSS with custom properties */
:root {
--primary-color: #3b82f6;
--secondary-color: #10b981;
--background: #ffffff;
--text-color: #09090b;
--border-radius: 6px;
}
.card {
background: var(--background);
border: 1px solid #e5e7eb;
border-radius: var(--border-radius);
padding: 24px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
transition: all 0.2s ease;
}
.card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
</dees-dataview-codebox>
</div>
<div class="section">
<div class="section-title">JSON Example</div>
<div class="section-description">JSON configuration with proper formatting.</div>
<dees-dataview-codebox proglang="json">
{
"name": "@design.estate/dees-catalog",
"version": "1.10.7",
"description": "A comprehensive catalog of web components",
"main": "dist_ts_web/index.js",
"type": "module",
"scripts": {
"build": "tsbuild tsfolders --allowimplicitany && tsbundle element --production",
"watch": "tswatch element",
"test": "tstest test/ --web --verbose"
},
"dependencies": {
"@design.estate/dees-element": "^2.0.45",
"highlight.js": "^11.9.0"
}
}
</dees-dataview-codebox>
</div>
</div>
`

View File

@ -8,6 +8,7 @@ import {
state, state,
cssManager, cssManager,
} from '@design.estate/dees-element'; } from '@design.estate/dees-element';
import { cssGeistFontFamily, cssMonoFontFamily } from './00fonts.js';
import hlight from 'highlight.js'; import hlight from 'highlight.js';
@ -48,27 +49,27 @@ export class DeesDataviewCodebox extends DeesElement {
display: block; display: block;
text-align: left; text-align: left;
font-size: 16px; font-size: 16px;
font-family: 'Geist Sans', sans-serif; font-family: ${cssGeistFontFamily};
} }
.mainbox { .mainbox {
position: relative; position: relative;
color: ${this.goBright ? '#333333' : '#ffffff'}; color: ${cssManager.bdTheme('#09090b', '#fafafa')};
border-top: 1px solid ${this.goBright ? '#ffffff' : '#333333'}; border: 1px solid ${cssManager.bdTheme('#e5e7eb', '#27272a')};
box-shadow: 0px 0px 5px ${this.goBright ? 'rgba(0,0,0,0.1)' : 'rgba(0,0,0,0.5)'}; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1), 0 1px 2px rgba(0, 0, 0, 0.06);
background: ${this.goBright ? '#ffffff' : '#191919'}; background: ${cssManager.bdTheme('#ffffff', '#09090b')};
border-radius: 16px; border-radius: 6px;
overflow: hidden; overflow: hidden;
} }
.appbar { .appbar {
position: relative; position: relative;
color: ${cssManager.bdTheme('#333', '#ccc')}; color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
background: ${cssManager.bdTheme('#ffffff', '#161616')}; background: ${cssManager.bdTheme('#f9fafb', '#18181b')};
border-bottom: 1px solid ${cssManager.bdTheme('#eeeeeb', '#222222')}; border-bottom: 1px solid ${cssManager.bdTheme('#e5e7eb', '#27272a')};
height: 24px; height: 32px;
display: flex; display: flex;
font-size: 12px; font-size: 13px;
line-height: 24px; line-height: 32px;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
} }
@ -81,31 +82,38 @@ export class DeesDataviewCodebox extends DeesElement {
} }
.bottomBar { .bottomBar {
color: ${cssManager.bdTheme('#333', '#ccc')}; position: relative;
background: ${cssManager.bdTheme('#ffffff', '#161616')}; color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
border-top: 1px solid ${cssManager.bdTheme('#eeeeeb', '#222222')}; background: ${cssManager.bdTheme('#f9fafb', '#18181b')};
height: 24px; border-top: 1px solid ${cssManager.bdTheme('#e5e7eb', '#27272a')};
height: 28px;
font-size: 12px; font-size: 12px;
line-height: 24px; line-height: 28px;
text-align: right; display: flex;
padding-right: 100px; justify-content: flex-end;
align-items: stretch;
overflow: hidden;
}
.spacesLabel {
padding: 0 16px;
display: flex;
align-items: center;
} }
.languageLabel { .languageLabel {
color: ${cssManager.bdTheme('#333', '#ccc')}; color: ${cssManager.bdTheme('#3b82f6', '#3b82f6')};
font-size: 12px; font-size: 12px;
line-height: 24px; line-height: 28px;
z-index: 10; background: ${cssManager.bdTheme('rgba(59, 130, 246, 0.1)', 'rgba(59, 130, 246, 0.1)')};
background: #6596ff20; padding: 0px 16px;
display: inline-block; font-weight: 500;
position: absolute; display: flex;
bottom: 0px; align-items: center;
right: 0px;
padding: 0px 16px 0px 8px;
} }
.hljs-keyword { .hljs-keyword {
color: #ff65ec; color: ${cssManager.bdTheme('#dc2626', '#f87171')};
} }
.codegrid { .codegrid {
@ -115,10 +123,10 @@ export class DeesDataviewCodebox extends DeesElement {
} }
.lineNumbers { .lineNumbers {
color: ${this.goBright ? '#acacac' : '#666666'}; color: ${cssManager.bdTheme('#71717a', '#52525b')};
padding: 30px 16px 0px 0px; padding: 24px 16px 0px 0px;
text-align: right; text-align: right;
border-right: 1px solid ${this.goBright ? '#eaeaea' : '#222222'}; border-right: 1px solid ${cssManager.bdTheme('#e5e7eb', '#27272a')};
} }
.lineCounter:last-child { .lineCounter:last-child {
@ -128,11 +136,11 @@ export class DeesDataviewCodebox extends DeesElement {
pre { pre {
overflow-x: auto; overflow-x: auto;
margin: 0px; margin: 0px;
padding: 30px 40px; padding: 24px 24px;
} }
code { code {
font-weight: ${this.goBright ? '400' : '300'}; font-weight: 400;
padding: 0px; padding: 0px;
margin: 0px; margin: 0px;
} }
@ -142,27 +150,43 @@ export class DeesDataviewCodebox extends DeesElement {
.lineNumbers { .lineNumbers {
line-height: 1.4em; line-height: 1.4em;
font-weight: 200; font-weight: 200;
font-family: 'Intel One Mono', 'Geist Mono', 'monospace'; font-family: ${cssMonoFontFamily};
} }
.hljs-string { .hljs-string {
color: #ffa465; color: ${cssManager.bdTheme('#059669', '#10b981')};
} }
.hljs-built_in { .hljs-built_in {
color: #65ff6a; color: ${cssManager.bdTheme('#8b5cf6', '#a78bfa')};
} }
.hljs-function { .hljs-function {
color: ${this.goBright ? '#2765DF' : '#6596ff'}; color: ${cssManager.bdTheme('#3b82f6', '#60a5fa')};
} }
.hljs-params { .hljs-params {
color: ${this.goBright ? '#3DB420' : '#65d5ff'}; color: ${cssManager.bdTheme('#0891b2', '#06b6d4')};
} }
.hljs-comment { .hljs-comment {
color: ${this.goBright ? '#EF9300' : '#ffd765'}; color: ${cssManager.bdTheme('#71717a', '#71717a')};
}
.hljs-number {
color: ${cssManager.bdTheme('#ea580c', '#fb923c')};
}
.hljs-literal {
color: ${cssManager.bdTheme('#dc2626', '#f87171')};
}
.hljs-attr {
color: ${cssManager.bdTheme('#8b5cf6', '#a78bfa')};
}
.hljs-variable {
color: ${cssManager.bdTheme('#09090b', '#fafafa')};
} }
</style> </style>
<div <div
@ -197,7 +221,7 @@ export class DeesDataviewCodebox extends DeesElement {
<pre><code></code></pre> <pre><code></code></pre>
</div> </div>
<div class="bottomBar"> <div class="bottomBar">
Spaces: 2 <div class="spacesLabel">Spaces: 2</div>
<div class="languageLabel">${this.progLang}</div> <div class="languageLabel">${this.progLang}</div>
</div> </div>
</div> </div>

View File

@ -3,47 +3,162 @@ import * as tsclass from '@tsclass/tsclass';
export const demoFunc = () => html` <style> export const demoFunc = () => html` <style>
.demo { .demo {
background: ${cssManager.bdTheme('#eeeeeb', '#000000')}; background: ${cssManager.bdTheme('#f5f5f5', '#0a0a0a')};
display: block; display: block;
content: ''; content: '';
padding: 40px; padding: 40px;
} }
.demo-grid {
display: grid;
gap: 24px;
max-width: 800px;
margin: 0 auto;
}
.demo-section {
display: flex;
flex-direction: column;
gap: 16px;
}
.demo-title {
font-size: 14px;
font-weight: 600;
color: ${cssManager.bdTheme('hsl(0 0% 45.1%)', 'hsl(0 0% 63.9%)')};
margin-bottom: 8px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
}
.demo-note {
font-size: 12px;
color: ${cssManager.bdTheme('hsl(0 0% 45.1%)', 'hsl(0 0% 63.9%)')};
margin-bottom: 24px;
text-align: center;
font-style: italic;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
}
</style> </style>
<div class="demo"> <div class="demo">
<dees-dataview-statusobject <div class="demo-note">
.statusObject=${{ Right-click on any detail row to copy the value, key, or key:value combination
id: '1', </div>
name: 'Demo Item', <div class="demo-grid">
combinedStatus: 'partly_ok', <div class="demo-section">
combinedStatusText: 'partly_ok', <div class="demo-title">Service Health Status</div>
details: [ <dees-dataview-statusobject
{ .statusObject=${{
name: 'Detail 1', id: '1',
value: 'Value 1', name: 'API Gateway Service',
status: 'ok', combinedStatus: 'ok',
statusText: 'OK', combinedStatusText: 'All systems operational',
}, details: [
{ {
name: 'Detail 2', name: 'Response Time',
value: 'Value 2', value: '45ms (avg)',
status: 'partly_ok', status: 'ok',
statusText: 'partly_ok', statusText: 'Within normal range',
}, },
{ {
name: 'Detail 3', name: 'Uptime',
value: 'Value 3', value: '99.99% (30 days)',
status: 'not_ok', status: 'ok',
statusText: 'not_ok', statusText: 'Excellent uptime',
}, },
{ {
name: 'Detail 4', name: 'Active Connections',
value: value: '1,234 / 10,000',
'Value 4 jhdkfjhalskdfjhfdjskalsdkfjhfdjskalskdjfhjdkslaksjdhfjdkslaskdfjhfjdkslaskdjfhjdskalskdjhfdjskalskdjfhdjskl', status: 'ok',
status: 'ok', statusText: 'Normal load',
statusText: 'OK', },
}, {
], name: 'SSL Certificate',
} as tsclass.code.IStatusObject} value: 'Valid until 2024-12-31',
> status: 'ok',
</dees-dataview-statusobject> statusText: 'Certificate valid',
},
],
} as tsclass.code.IStatusObject}
>
</dees-dataview-statusobject>
</div>
<div class="demo-section">
<div class="demo-title">Database Cluster Status</div>
<dees-dataview-statusobject
.statusObject=${{
id: '2',
name: 'PostgreSQL Cluster',
combinedStatus: 'partly_ok',
combinedStatusText: 'Minor issues detected',
details: [
{
name: 'Primary Node',
value: 'db-primary-01 (healthy)',
status: 'ok',
statusText: 'Operating normally',
},
{
name: 'Replica Lag',
value: '2.5 seconds',
status: 'partly_ok',
statusText: 'Slightly elevated',
},
{
name: 'Disk Usage',
value: '78% (312GB / 400GB)',
status: 'partly_ok',
statusText: 'Approaching threshold',
},
{
name: 'Connection Pool',
value: '89 / 100 connections',
status: 'ok',
statusText: 'Within limits',
},
],
} as tsclass.code.IStatusObject}
>
</dees-dataview-statusobject>
</div>
<div class="demo-section">
<div class="demo-title">Build Pipeline Status</div>
<dees-dataview-statusobject
.statusObject=${{
id: '3',
name: 'CI/CD Pipeline',
combinedStatus: 'not_ok',
combinedStatusText: 'Build failure',
details: [
{
name: 'Last Build',
value: 'Build #1234 - Failed',
status: 'not_ok',
statusText: 'Test failures',
},
{
name: 'Failed Tests',
value: '3 tests failed: auth.spec.ts, user.spec.ts, api.spec.ts',
status: 'not_ok',
statusText: 'Unit test failures',
},
{
name: 'Code Coverage',
value: '82.5% (target: 85%)',
status: 'partly_ok',
statusText: 'Below target',
},
{
name: 'Build Duration',
value: '12m 34s',
status: 'ok',
statusText: 'Normal duration',
},
],
} as tsclass.code.IStatusObject}
>
</dees-dataview-statusobject>
</div>
</div>
</div>`; </div>`;

View File

@ -15,6 +15,7 @@ import {
} from '@design.estate/dees-element'; } from '@design.estate/dees-element';
import * as tsclass from '@tsclass/tsclass'; import * as tsclass from '@tsclass/tsclass';
import { DeesContextmenu } from './dees-contextmenu.js';
declare global { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {
@ -31,109 +32,128 @@ export class DeesDataviewStatusobject extends DeesElement {
public static styles = [ public static styles = [
cssManager.defaultStyles, cssManager.defaultStyles,
css` css`
:host {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
}
.mainbox { .mainbox {
border-radius: 8px; border-radius: 8px;
background: ${cssManager.bdTheme('#fff', '#1b1b1b')}; background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(0 0% 3.9%)')};
box-shadow: 0px 1px 3px #00000030; border: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
box-shadow: 0 1px 3px 0 hsl(0 0% 0% / 0.1), 0 1px 2px -1px hsl(0 0% 0% / 0.1);
min-height: 48px; min-height: 48px;
color: ${cssManager.bdTheme('#000', '#fff')}; color: ${cssManager.bdTheme('hsl(0 0% 9%)', 'hsl(0 0% 98%)')};
border-top: ${cssManager.bdTheme('none', '1px solid #ffffff10')};
cursor: default; cursor: default;
overflow: hidden;
} }
.heading { .heading {
display: grid; display: grid;
align-items: center; align-items: center;
grid-template-columns: 40px auto 120px; grid-template-columns: 48px auto 100px;
height: 56px;
padding: 0 16px;
background: ${cssManager.bdTheme('hsl(0 0% 97%)', 'hsl(0 0% 7%)')};
border-bottom: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
} }
h1 { h1 {
display: block; display: block;
margin: 0px; margin: 0px;
padding: 0px; padding: 0px 12px;
height: 48px; font-size: 14px;
text-transform: uppercase; font-weight: 500;
font-size: 12px; letter-spacing: -0.01em;
line-height: 48px; color: ${cssManager.bdTheme('hsl(0 0% 15%)', 'hsl(0 0% 93.9%)')};
} }
.statusdot { .statusdot {
height: 8px; height: 10px;
width: 8px; width: 10px;
border-radius: 6px; border-radius: 50%;
background: grey; background: ${cssManager.bdTheme('hsl(0 0% 63.9%)', 'hsl(0 0% 45.1%)')};
margin: auto; margin: auto;
box-shadow: 0 0 0 3px ${cssManager.bdTheme('hsl(0 0% 63.9% / 0.2)', 'hsl(0 0% 45.1% / 0.2)')};
transition: all 0.2s ease;
} }
.copyMain { .copyMain {
font-size: 10px; font-size: 12px;
font-weight: 600; font-weight: 500;
text-transform: uppercase; background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(0 0% 14.9%)')};
border: 1px solid ${cssManager.bdTheme('#e0e0e0', '#444')}; border: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
text-align: center; text-align: center;
padding: 4px; padding: 6px 12px;
border-radius: 3px; border-radius: 6px;
margin-right: 16px; color: ${cssManager.bdTheme('hsl(0 0% 45.1%)', 'hsl(0 0% 63.9%)')};
color: ${cssManager.bdTheme('#333', '#ffffff80')};
user-select: none; user-select: none;
cursor: pointer;
transition: all 0.15s ease;
} }
.copyMain:hover { .copyMain:hover {
background: ${cssManager.bdTheme(colors.bright.blue, colors.dark.blue)}; background: ${cssManager.bdTheme('hsl(0 0% 95.1%)', 'hsl(0 0% 14.9%)')};
border: 1px solid ${cssManager.bdTheme(colors.bright.blue, colors.dark.blue)}; border-color: ${cssManager.bdTheme('hsl(0 0% 79.8%)', 'hsl(0 0% 20.9%)')};
color: #fff; color: ${cssManager.bdTheme('hsl(0 0% 15%)', 'hsl(0 0% 93.9%)')};
} }
.copyMain:active { .copyMain:active {
background: ${cssManager.bdTheme(colors.bright.blueActive, colors.dark.blueActive)}; background: ${cssManager.bdTheme('hsl(0 0% 91%)', 'hsl(0 0% 14.9%)')};
border: 1px solid ${cssManager.bdTheme(colors.bright.blueActive, colors.dark.blueActive)}; transform: scale(0.98);
color: #fff;
} }
.statusdot.ok { .statusdot.ok {
background: green; background: ${cssManager.bdTheme('hsl(142.1 76.2% 36.3%)', 'hsl(142.1 70.6% 45.3%)')};
box-shadow: 0 0 0 3px ${cssManager.bdTheme('hsl(142.1 76.2% 36.3% / 0.2)', 'hsl(142.1 70.6% 45.3% / 0.2)')};
} }
.statusdot.not_ok{ .statusdot.not_ok {
background: red; background: ${cssManager.bdTheme('hsl(0 84.2% 60.2%)', 'hsl(0 72.2% 50.6%)')};
box-shadow: 0 0 0 3px ${cssManager.bdTheme('hsl(0 84.2% 60.2% / 0.2)', 'hsl(0 72.2% 50.6% / 0.2)')};
} }
.statusdot.partly_ok { .statusdot.partly_ok {
background: orange; background: ${cssManager.bdTheme('hsl(25 95% 53%)', 'hsl(25 95% 63%)')};
box-shadow: 0 0 0 3px ${cssManager.bdTheme('hsl(25 95% 53% / 0.2)', 'hsl(25 95% 63% / 0.2)')};
} }
.detail { .detail {
min-height: 60px; min-height: 60px;
align-items: center; align-items: center;
display: grid; display: grid;
grid-template-columns: 40px auto; grid-template-columns: 48px auto;
border-top: 1px dotted ${cssManager.bdTheme('#e0e0e0', '#282828')}; border-top: 1px solid ${cssManager.bdTheme('hsl(0 0% 94%)', 'hsl(0 0% 14.9%)')};
transition: all 0.2s; transition: background-color 0.15s ease;
padding-right: 16px;
cursor: context-menu;
} }
.detail:hover { .detail:hover {
background: #ffffff05; background: ${cssManager.bdTheme('hsl(0 0% 97%)', 'hsl(0 0% 7%)')};
} }
.detail:active { .detail:active {
background: #ffffff10; background: ${cssManager.bdTheme('hsl(0 0% 95%)', 'hsl(0 0% 9%)')};
} }
.detail .detailsText { .detail .detailsText {
padding-top: 8px; padding: 12px;
padding-bottom: 8px;
padding-right: 8px;
word-break: break-all; word-break: break-all;
} }
.detail .detailsText .label { .detail .detailsText .label {
font-size: 12px; font-size: 12px;
color: #ffffff80 font-weight: 500;
color: ${cssManager.bdTheme('hsl(0 0% 45.1%)', 'hsl(0 0% 63.9%)')}
margin-bottom: 2px;
letter-spacing: -0.01em;
} }
.detail .detailsText .value { .detail .detailsText .value {
font-size: 14px; font-size: 14px;
font-family: 'Intel One Mono', 'Geist Mono'; font-family: 'Intel One Mono', 'Geist Mono', monospace;
color: ${cssManager.bdTheme('hsl(0 0% 15%)', 'hsl(0 0% 90%)')};
line-height: 1.5;
} }
`, `,
]; ];
@ -143,12 +163,40 @@ export class DeesDataviewStatusobject extends DeesElement {
<div class="mainbox"> <div class="mainbox">
<div class="heading"> <div class="heading">
<div class="statusdot ${this.statusObject?.combinedStatus}"></div> <div class="statusdot ${this.statusObject?.combinedStatus}"></div>
<h1>${this.statusObject?.name || 'no status object assigned'}</h1> <h1>${this.statusObject?.name || 'No status object assigned'}</h1>
<div class="copyMain">Copy as JSON</div> <div class="copyMain" @click=${this.handleCopyAsJson}>Copy JSON</div>
</div> </div>
${this.statusObject?.details?.map((detailArg) => { ${this.statusObject?.details?.map((detailArg) => {
return html` return html`
<div class="detail"> <div
class="detail"
@contextmenu=${(event: MouseEvent) => {
event.preventDefault();
DeesContextmenu.openContextMenuWithOptions(event, [
{
name: 'Copy Value',
iconName: 'lucide:copy',
action: async () => {
await this.copyToClipboard(detailArg.value, 'Value');
},
},
{
name: 'Copy Key',
iconName: 'lucide:key',
action: async () => {
await this.copyToClipboard(detailArg.name, 'Key');
},
},
{
name: 'Copy Key:Value',
iconName: 'lucide:copy-plus',
action: async () => {
await this.copyToClipboard(`${detailArg.name}: ${detailArg.value}`, 'Key:Value');
},
},
]);
}}
>
<div class="statusdot ${detailArg.status}"></div> <div class="statusdot ${detailArg.status}"></div>
<div class="detailsText"> <div class="detailsText">
<div class="label">${detailArg.name}</div> <div class="label">${detailArg.name}</div>
@ -162,4 +210,42 @@ export class DeesDataviewStatusobject extends DeesElement {
} }
async firstUpdated() {} async firstUpdated() {}
private async copyToClipboard(text: string, type: string = 'Text') {
try {
await navigator.clipboard.writeText(text);
console.log(`${type} copied to clipboard`);
// You could add visual feedback here if needed
} catch (err) {
console.error(`Failed to copy ${type}:`, err);
}
}
private async handleCopyAsJson() {
if (!this.statusObject) return;
try {
await navigator.clipboard.writeText(JSON.stringify(this.statusObject, null, 2));
// Show feedback
const button = this.shadowRoot.querySelector('.copyMain') as HTMLElement;
const originalText = button.textContent;
button.textContent = 'Copied!';
// Apply success styles based on theme
const isDark = !this.goBright;
button.style.background = isDark ? 'hsl(142.1 70.6% 45.3% / 0.1)' : 'hsl(142.1 76.2% 36.3% / 0.1)';
button.style.borderColor = isDark ? 'hsl(142.1 70.6% 45.3%)' : 'hsl(142.1 76.2% 36.3%)';
button.style.color = isDark ? 'hsl(142.1 70.6% 45.3%)' : 'hsl(142.1 76.2% 36.3%)';
setTimeout(() => {
button.textContent = originalText;
button.style.background = '';
button.style.borderColor = '';
button.style.color = '';
}, 1500);
} catch (err) {
console.error('Failed to copy:', err);
}
}
} }

View File

@ -3,40 +3,91 @@ import type { DeesForm } from './dees-form.js';
import '@design.estate/dees-wcctools/demotools'; import '@design.estate/dees-wcctools/demotools';
export const demoFunc = () => html` export const demoFunc = () => html`
<dees-demowrapper> <style>
<style> ${css`
${css` .demo-container {
.demo-container { display: flex;
display: flex; flex-direction: column;
flex-direction: column; gap: 24px;
gap: 24px; padding: 24px;
padding: 24px; max-width: 1200px;
max-width: 1200px; margin: 0 auto;
margin: 0 auto; }
}
dees-panel {
margin-bottom: 24px;
}
dees-panel:last-child {
margin-bottom: 0;
}
.form-output {
margin-top: 16px;
padding: 12px;
background: ${cssManager.bdTheme('hsl(210 40% 96.1%)', 'hsl(215 20.2% 16.8%)')};
border-radius: 6px;
font-size: 14px;
font-family: monospace;
white-space: pre-wrap;
}
.status-message {
margin-top: 16px;
padding: 12px;
border-radius: 6px;
font-size: 14px;
}
.status-message.success {
background: ${cssManager.bdTheme('hsl(142.1 70.6% 45.3% / 0.1)', 'hsl(142.1 70.6% 45.3% / 0.2)')};
color: ${cssManager.bdTheme('hsl(142.1 70.6% 35.3%)', 'hsl(142.1 70.6% 65.3%)')};
}
.status-message.error {
background: ${cssManager.bdTheme('hsl(0 72.2% 50.6% / 0.1)', 'hsl(0 72.2% 50.6% / 0.2)')};
color: ${cssManager.bdTheme('hsl(0 72.2% 40.6%)', 'hsl(0 72.2% 60.6%)')};
}
`}
</style>
<div class="demo-container">
<dees-demowrapper .runAfterRender=${async (elementArg: HTMLElement) => {
const form = elementArg.querySelector('dees-form') as DeesForm;
const outputDiv = elementArg.querySelector('.form-output');
if (form && outputDiv) {
form.addEventListener('formData', async (eventArg: CustomEvent) => {
const data = eventArg.detail.data;
console.log('Form submitted with data:', data);
// Show processing state
form.setStatus('pending', 'Processing your registration...');
outputDiv.innerHTML = `<strong>Submitted Data:</strong>\n${JSON.stringify(data, null, 2)}`;
// Simulate API call
await domtools.plugins.smartdelay.delayFor(2000);
// Show success
form.setStatus('success', 'Registration completed successfully!');
// Reset form after delay
await domtools.plugins.smartdelay.delayFor(2000);
form.reset();
outputDiv.innerHTML = '<em>Form has been reset</em>';
});
dees-panel { // Track individual field changes
margin-bottom: 24px; const inputs = form.querySelectorAll('dees-input-text, dees-input-dropdown, dees-input-checkbox');
} inputs.forEach((input) => {
input.addEventListener('changeSubject', () => {
dees-panel:last-child { console.log('Field changed:', input.getAttribute('key'));
margin-bottom: 0; });
} });
`} }
</style> }}>
<div class="demo-container">
<dees-panel .heading="Complete Form Example" .description="A comprehensive form with various input types, validation, and form submission handling"> <dees-panel .heading="Complete Form Example" .description="A comprehensive form with various input types, validation, and form submission handling">
<dees-form <dees-form>
@formData=${async (eventArg) => {
const form: DeesForm = eventArg.currentTarget;
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 <dees-input-text
.required=${true} .required=${true}
key="firstName" key="firstName"
@ -92,13 +143,47 @@ export const demoFunc = () => html`
<dees-form-submit>Create Account</dees-form-submit> <dees-form-submit>Create Account</dees-form-submit>
</dees-form> </dees-form>
<div class="form-output">
<em>Submit the form to see the collected data...</em>
</div>
</dees-panel> </dees-panel>
</dees-demowrapper>
<dees-demowrapper .runAfterRender=${async (elementArg: HTMLElement) => {
const form = elementArg.querySelector('dees-form') as DeesForm;
if (form) {
// Track horizontal layout behavior
console.log('Horizontal form layout active');
// Monitor filter changes
form.addEventListener('formData', (event: CustomEvent) => {
const filters = event.detail.data;
console.log('Filter applied:', filters);
// Simulate search
const resultsCount = Math.floor(Math.random() * 100) + 1;
console.log(`Found ${resultsCount} results with filters:`, filters);
});
// Setup real-time filter updates
const inputs = form.querySelectorAll('[key]');
inputs.forEach((input) => {
input.addEventListener('changeSubject', async () => {
// Get current form data
const formData = await form.collectFormData();
console.log('Live filter update:', formData);
});
});
}
}}>
<dees-panel .heading="Horizontal Form Layout" .description="Compact form with inputs arranged horizontally - perfect for filters and quick forms"> <dees-panel .heading="Horizontal Form Layout" .description="Compact form with inputs arranged horizontally - perfect for filters and quick forms">
<dees-form horizontal-layout> <dees-form horizontal-layout>
<dees-input-text <dees-input-text
key="search" key="search"
label="Search" label="Search"
placeholder="Enter keywords..."
></dees-input-text> ></dees-input-text>
<dees-input-dropdown <dees-input-dropdown
@ -132,16 +217,55 @@ export const demoFunc = () => html`
></dees-input-checkbox> ></dees-input-checkbox>
</dees-form> </dees-form>
</dees-panel> </dees-panel>
</dees-demowrapper>
<dees-demowrapper .runAfterRender=${async (elementArg: HTMLElement) => {
const form = elementArg.querySelector('dees-form') as DeesForm;
const statusDiv = elementArg.querySelector('#status-display');
if (form) {
form.addEventListener('formData', async (eventArg: CustomEvent) => {
const data = eventArg.detail.data;
console.log('Advanced form data:', data);
// Show validation in progress
form.setStatus('pending', 'Validating your information...');
// Simulate validation
await domtools.plugins.smartdelay.delayFor(1500);
// Check IBAN validity (simple check)
if (data.iban && data.iban.length > 15) {
form.setStatus('success', 'Application submitted successfully!');
if (statusDiv) {
statusDiv.className = 'status-message success';
statusDiv.textContent = '✓ Your application has been submitted. We will contact you soon.';
}
} else {
form.setStatus('error', 'Please check your IBAN');
if (statusDiv) {
statusDiv.className = 'status-message error';
statusDiv.textContent = '✗ Invalid IBAN format. Please check and try again.';
}
}
console.log('Form data logged:', data);
});
// Monitor file uploads
const fileUpload = form.querySelector('dees-input-fileupload');
if (fileUpload) {
fileUpload.addEventListener('change', (event: any) => {
const files = event.detail?.files || [];
console.log(`${files.length} file(s) selected for upload`);
});
}
}
}}>
<dees-panel .heading="Advanced Form Features" .description="Form with specialized input types and complex validation"> <dees-panel .heading="Advanced Form Features" .description="Form with specialized input types and complex validation">
<dees-form <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 <dees-input-iban
key="iban" key="iban"
label="IBAN" label="IBAN"
@ -181,7 +305,9 @@ export const demoFunc = () => html`
<dees-form-submit>Submit Application</dees-form-submit> <dees-form-submit>Submit Application</dees-form-submit>
</dees-form> </dees-form>
<div id="status-display"></div>
</dees-panel> </dees-panel>
</div> </dees-demowrapper>
</dees-demowrapper> </div>
`; `;

View File

@ -9,6 +9,7 @@ import {
import * as domtools from '@design.estate/dees-domtools'; import * as domtools from '@design.estate/dees-domtools';
import { DeesInputCheckbox } from './dees-input-checkbox.js'; import { DeesInputCheckbox } from './dees-input-checkbox.js';
import { DeesInputDatepicker } from './dees-input-datepicker.js';
import { DeesInputText } from './dees-input-text.js'; import { DeesInputText } from './dees-input-text.js';
import { DeesInputQuantitySelector } from './dees-input-quantityselector.js'; import { DeesInputQuantitySelector } from './dees-input-quantityselector.js';
import { DeesInputRadiogroup } from './dees-input-radiogroup.js'; import { DeesInputRadiogroup } from './dees-input-radiogroup.js';
@ -25,6 +26,7 @@ import { demoFunc } from './dees-form.demo.js';
// Unified set for form input types // Unified set for form input types
const FORM_INPUT_TYPES = [ const FORM_INPUT_TYPES = [
DeesInputCheckbox, DeesInputCheckbox,
DeesInputDatepicker,
DeesInputDropdown, DeesInputDropdown,
DeesInputFileupload, DeesInputFileupload,
DeesInputIban, DeesInputIban,
@ -39,6 +41,7 @@ const FORM_INPUT_TYPES = [
export type TFormInputElement = export type TFormInputElement =
| DeesInputCheckbox | DeesInputCheckbox
| DeesInputDatepicker
| DeesInputDropdown | DeesInputDropdown
| DeesInputFileupload | DeesInputFileupload
| DeesInputIban | DeesInputIban

View File

@ -10,6 +10,7 @@ import {
} from '@design.estate/dees-element'; } from '@design.estate/dees-element';
import { demoFunc } from './dees-heading.demo.js'; import { demoFunc } from './dees-heading.demo.js';
import { cssCalSansFontFamily } from './00fonts.js';
declare global { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {
@ -39,7 +40,7 @@ export class DeesHeading extends DeesElement {
font-weight: 600; font-weight: 600;
color: ${cssManager.bdTheme('#000', '#fff')}; color: ${cssManager.bdTheme('#000', '#fff')};
} }
h1 { font-size: 32px; font-family: 'Cal Sans'; letter-spacing: 0.025em;} h1 { font-size: 32px; font-family: ${cssCalSansFontFamily}; letter-spacing: 0.025em;}
h2 { font-size: 28px; } h2 { font-size: 28px; }
h3 { font-size: 24px; } h3 { font-size: 24px; }
h4 { font-size: 20px; } h4 { font-size: 20px; }

View File

@ -1,5 +1,6 @@
import { html, css } from '@design.estate/dees-element'; import { html, css, cssManager } from '@design.estate/dees-element';
import '@design.estate/dees-wcctools/demotools'; import '@design.estate/dees-wcctools/demotools';
import './dees-panel.js';
import type { DeesInputCheckbox } from './dees-input-checkbox.js'; import type { DeesInputCheckbox } from './dees-input-checkbox.js';
import './dees-button.js'; import './dees-button.js';
@ -41,62 +42,49 @@ export const demoFunc = () => html`
margin: 0 auto; margin: 0 auto;
} }
.demo-section { dees-panel {
background: #f8f9fa; margin-bottom: 24px;
border-radius: 8px;
padding: 24px;
} }
@media (prefers-color-scheme: dark) { dees-panel:last-child {
.demo-section { margin-bottom: 0;
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 { .checkbox-group {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 8px; gap: 12px;
} }
.feature-list { .horizontal-checkboxes {
background: #f0f0f0; display: flex;
border-radius: 4px; gap: 24px;
flex-wrap: wrap;
}
.interactive-section {
background: ${cssManager.bdTheme('hsl(210 40% 96.1%)', 'hsl(215 20.2% 16.8%)')};
border-radius: 8px;
padding: 16px; padding: 16px;
margin-bottom: 16px; margin-top: 16px;
} }
@media (prefers-color-scheme: dark) { .output-text {
.feature-list { font-family: monospace;
background: #0a0a0a; font-size: 13px;
} color: ${cssManager.bdTheme('hsl(215.3 25% 26.7%)', 'hsl(210 40% 80%)')};
padding: 8px;
background: ${cssManager.bdTheme('hsl(210 40% 98%)', 'hsl(215 20.2% 11.8%)')};
border-radius: 4px;
min-height: 24px;
}
.form-section {
background: ${cssManager.bdTheme('hsl(0 0% 97%)', 'hsl(0 0% 7%)')};
border: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
border-radius: 8px;
padding: 20px;
margin-top: 16px;
} }
.button-group { .button-group {
@ -104,70 +92,112 @@ export const demoFunc = () => html`
gap: 8px; gap: 8px;
margin-bottom: 16px; margin-bottom: 16px;
} }
.feature-list {
background: ${cssManager.bdTheme('hsl(210 40% 96.1%)', 'hsl(215 20.2% 11.8%)')};
border: 1px solid ${cssManager.bdTheme('hsl(214.3 31.8% 91.4%)', 'hsl(215 20.2% 16.8%)')};
border-radius: 6px;
padding: 16px;
}
.section-title {
font-size: 16px;
font-weight: 600;
margin-bottom: 16px;
color: ${cssManager.bdTheme('hsl(215.3 25% 8.8%)', 'hsl(210 40% 98%)')};
}
`} `}
</style> </style>
<div class="demo-container"> <div class="demo-container">
<div class="demo-section"> <dees-panel .title=${'Basic Checkboxes'} .subtitle=${'Simple checkbox examples with various labels'}>
<h3>Basic Checkboxes</h3> <div class="checkbox-group">
<p>Standard checkbox inputs for boolean selections</p> <dees-input-checkbox
.label=${'I agree to the Terms and Conditions'}
<dees-input-checkbox .value=${true}
.label=${'I agree to the Terms and Conditions'} .key=${'terms'}
.value=${true} ></dees-input-checkbox>
.key=${'terms'}
></dees-input-checkbox> <dees-input-checkbox
.label=${'Subscribe to newsletter'}
<dees-input-checkbox .value=${false}
.label=${'Subscribe to newsletter'} .key=${'newsletter'}
.value=${false} ></dees-input-checkbox>
.key=${'newsletter'}
></dees-input-checkbox> <dees-input-checkbox
.label=${'Enable notifications'}
<dees-input-checkbox .value=${false}
.label=${'Enable notifications'} .description=${'Receive email updates about your account'}
.required=${true} .key=${'notifications'}
.key=${'notifications'} ></dees-input-checkbox>
></dees-input-checkbox> </div>
</div> </dees-panel>
<div class="demo-section"> <dees-panel .title=${'Checkbox States'} .subtitle=${'Different checkbox states and configurations'}>
<h3>Horizontal Layout</h3> <div class="checkbox-group">
<p>Checkboxes arranged horizontally for compact forms</p> <dees-input-checkbox
.label=${'Default state'}
<div class="horizontal-group"> .value=${false}
></dees-input-checkbox>
<dees-input-checkbox
.label=${'Checked state'}
.value=${true}
></dees-input-checkbox>
<dees-input-checkbox
.label=${'Disabled unchecked'}
.value=${false}
.disabled=${true}
></dees-input-checkbox>
<dees-input-checkbox
.label=${'Disabled checked'}
.value=${true}
.disabled=${true}
></dees-input-checkbox>
<dees-input-checkbox
.label=${'Required checkbox'}
.required=${true}
.key=${'required'}
></dees-input-checkbox>
</div>
</dees-panel>
<dees-panel .title=${'Horizontal Layout'} .subtitle=${'Checkboxes arranged horizontally for compact forms'}>
<div class="horizontal-checkboxes">
<dees-input-checkbox <dees-input-checkbox
.label=${'Option A'} .label=${'Option A'}
.value=${false}
.layoutMode=${'horizontal'} .layoutMode=${'horizontal'}
.key=${'optionA'} .key=${'optionA'}
></dees-input-checkbox> ></dees-input-checkbox>
<dees-input-checkbox <dees-input-checkbox
.label=${'Option B'} .label=${'Option B'}
.layoutMode=${'horizontal'}
.value=${true} .value=${true}
.layoutMode=${'horizontal'}
.key=${'optionB'} .key=${'optionB'}
></dees-input-checkbox> ></dees-input-checkbox>
<dees-input-checkbox <dees-input-checkbox
.label=${'Option C'} .label=${'Option C'}
.value=${false}
.layoutMode=${'horizontal'} .layoutMode=${'horizontal'}
.key=${'optionC'} .key=${'optionC'}
></dees-input-checkbox> ></dees-input-checkbox>
<dees-input-checkbox <dees-input-checkbox
.label=${'Option D'} .label=${'Option D'}
.layoutMode=${'horizontal'}
.value=${true} .value=${true}
.layoutMode=${'horizontal'}
.key=${'optionD'} .key=${'optionD'}
></dees-input-checkbox> ></dees-input-checkbox>
</div> </div>
</div> </dees-panel>
<div class="demo-section"> <dees-panel .title=${'Feature Selection Example'} .subtitle=${'Common use case for feature toggles with batch operations'}>
<h3>Feature Selection Example</h3>
<p>Common use case for feature toggles with batch operations</p>
<div class="button-group"> <div class="button-group">
<dees-button id="select-all-btn" type="secondary">Select All</dees-button> <dees-button id="select-all-btn" type="secondary">Select All</dees-button>
<dees-button id="clear-all-btn" type="secondary">Clear All</dees-button> <dees-button id="clear-all-btn" type="secondary">Clear All</dees-button>
@ -206,62 +236,72 @@ export const demoFunc = () => html`
></dees-input-checkbox> ></dees-input-checkbox>
</div> </div>
</div> </div>
</div> </dees-panel>
<div class="demo-section"> <dees-panel .title=${'Privacy Settings Example'} .subtitle=${'Checkboxes in a typical form context'}>
<h3>States</h3> <div class="form-section">
<p>Different checkbox states and configurations</p> <h4 class="section-title">Privacy Preferences</h4>
<dees-input-checkbox <div class="checkbox-group">
.label=${'Disabled Unchecked'} <dees-input-checkbox
.disabled=${true} .label=${'Share analytics data'}
.key=${'disabled1'} .value=${true}
></dees-input-checkbox> .description=${'Help us improve by sharing anonymous usage data'}
></dees-input-checkbox>
<dees-input-checkbox
.label=${'Disabled Checked'} <dees-input-checkbox
.disabled=${true} .label=${'Personalized recommendations'}
.value=${true} .value=${true}
.key=${'disabled2'} .description=${'Get suggestions based on your activity'}
></dees-input-checkbox> ></dees-input-checkbox>
<dees-input-checkbox <dees-input-checkbox
.label=${'Required Checkbox'} .label=${'Marketing communications'}
.required=${true} .value=${false}
.key=${'required'} .description=${'Receive promotional emails and special offers'}
></dees-input-checkbox> ></dees-input-checkbox>
</div>
<dees-input-checkbox
.label=${'Third-party integrations'}
.value=${false}
.description=${'Allow approved partners to access your data'}
></dees-input-checkbox>
</div>
</div>
</dees-panel>
<div class="demo-section"> <dees-panel .title=${'Interactive Example'} .subtitle=${'Click checkboxes to see value changes'}>
<h3>Real-world Examples</h3>
<p>Common checkbox patterns in applications</p>
<div class="checkbox-group"> <div class="checkbox-group">
<dees-input-checkbox <dees-input-checkbox
.label=${'Remember me on this device'} .label=${'Feature toggle'}
.value=${true}
.key=${'rememberMe'}
></dees-input-checkbox>
<dees-input-checkbox
.label=${'Make my profile public'}
.value=${false} .value=${false}
.key=${'publicProfile'} @changeSubject=${(event: CustomEvent) => {
const output = document.querySelector('#checkbox-output');
if (output && event.detail) {
const isChecked = event.detail.getValue();
output.textContent = `Feature is ${isChecked ? 'enabled' : 'disabled'}`;
}
}}
></dees-input-checkbox> ></dees-input-checkbox>
<dees-input-checkbox <dees-input-checkbox
.label=${'Allow others to find me by email'} .label=${'Debug mode'}
.value=${false} .value=${false}
.key=${'findByEmail'} @changeSubject=${(event: CustomEvent) => {
></dees-input-checkbox> const output = document.querySelector('#debug-output');
if (output && event.detail) {
<dees-input-checkbox const isChecked = event.detail.getValue();
.label=${'Send me product updates and announcements'} output.textContent = `Debug mode: ${isChecked ? 'ON' : 'OFF'}`;
.value=${true} }
.key=${'productUpdates'} }}
></dees-input-checkbox> ></dees-input-checkbox>
</div> </div>
</div>
<div class="interactive-section">
<div id="checkbox-output" class="output-text">Feature is disabled</div>
<div id="debug-output" class="output-text" style="margin-top: 8px;">Debug mode: OFF</div>
</div>
</dees-panel>
</div> </div>
</dees-demowrapper> </dees-demowrapper>
`; `;

View File

@ -8,6 +8,7 @@ import {
} from '@design.estate/dees-element'; } from '@design.estate/dees-element';
import { DeesInputBase } from './dees-input-base.js'; import { DeesInputBase } from './dees-input-base.js';
import { demoFunc } from './dees-input-checkbox.demo.js'; import { demoFunc } from './dees-input-checkbox.demo.js';
import { cssGeistFontFamily } from './00fonts.js';
declare global { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {
@ -44,120 +45,106 @@ export class DeesInputCheckbox extends DeesInputBase<DeesInputCheckbox> {
:host { :host {
position: relative; position: relative;
cursor: default; cursor: default;
} font-family: ${cssGeistFontFamily};
:host(:hover) {
filter: ${cssManager.bdTheme('brightness(0.95)', 'brightness(1.1)')};
} }
.maincontainer { .maincontainer {
display: flex; display: inline-flex;
align-items: center; align-items: flex-start;
gap: 12px; gap: 8px;
padding: 8px 0px;
color: ${cssManager.bdTheme('#333', '#ccc')};
cursor: pointer; cursor: pointer;
user-select: none; user-select: none;
transition: all 0.2s; transition: all 0.15s ease;
}
.maincontainer:hover {
color: ${cssManager.bdTheme('#000', '#fff')};
}
.maincontainer:hover .checkbox {
border-color: ${cssManager.bdTheme('#999', '#888')};
}
input:focus {
outline: none;
border-bottom: 1px solid #e4002b;
} }
.checkbox { .checkbox {
transition: all 0.1s; position: relative;
box-sizing: border-box; height: 18px;
border: 1px solid ${cssManager.bdTheme('#CCC', '#999')}; width: 18px;
border-radius: 2px;
height: 24px;
width: 24px;
display: inline-block;
background: ${cssManager.bdTheme('#fafafa', '#222')};
flex-shrink: 0; flex-shrink: 0;
border-radius: 4px;
border: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(0 0% 3.9%)')};
transition: all 0.15s ease;
margin-top: 1px;
}
.maincontainer:hover .checkbox {
border-color: ${cssManager.bdTheme('hsl(0 0% 79.8%)', 'hsl(0 0% 20.9%)')};
} }
.checkbox.selected { .checkbox.selected {
background: #0050b9; background: ${cssManager.bdTheme('hsl(222.2 47.4% 51.2%)', 'hsl(217.2 91.2% 59.8%)')};
border: 1px solid #0050b9; border-color: ${cssManager.bdTheme('hsl(222.2 47.4% 51.2%)', 'hsl(217.2 91.2% 59.8%)')};
} }
.checkbox.disabled { .checkbox:focus-visible {
background: none; outline: none;
border: 1px dashed ${cssManager.bdTheme('#666666', '#666666')}; box-shadow: 0 0 0 3px ${cssManager.bdTheme('hsl(222.2 47.4% 51.2% / 0.1)', 'hsl(217.2 91.2% 59.8% / 0.1)')};
} }
/* Checkmark using Lucide icon style */
.checkbox .checkmark { .checkbox .checkmark {
display: inline-block;
width: 22px;
height: 22px;
-ms-transform: rotate(45deg); /* IE 9 */
-webkit-transform: rotate(45deg); /* Chrome, Safari, Opera */
transform: rotate(45deg);
}
.checkbox .checkmark_stem {
position: absolute; position: absolute;
width: 3px; top: 50%;
height: 9px; left: 50%;
background-color: #fff; transform: translate(-50%, -50%);
left: 11px; opacity: 0;
top: 6px; transition: opacity 0.15s ease;
} }
.checkbox .checkmark_kick { .checkbox.selected .checkmark {
position: absolute; opacity: 1;
width: 3px;
height: 3px;
background-color: #fff;
left: 8px;
top: 12px;
} }
.checkbox.disabled .checkmark_stem, .checkbox.disabled .checkmark_kick { .checkbox .checkmark svg {
background-color: ${cssManager.bdTheme('#333', '#fff')}; width: 12px;
} height: 12px;
stroke: white;
img { stroke-width: 3;
padding: 4px;
}
.checkbox-label {
font-size: 14px;
transition: color 0.2s ease;
}
.maincontainer:hover .checkbox-label {
color: ${cssManager.bdTheme('#1a1a1a', '#ffffff')};
} }
/* Disabled state */
.maincontainer.disabled { .maincontainer.disabled {
cursor: not-allowed; cursor: not-allowed;
opacity: 0.5; opacity: 0.5;
} }
.maincontainer.disabled:hover { .checkbox.disabled {
color: ${cssManager.bdTheme('#333', '#ccc')}; background: ${cssManager.bdTheme('hsl(0 0% 95.1%)', 'hsl(0 0% 14.9%)')};
border-color: ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
} }
.maincontainer.disabled:hover .checkbox { /* Label */
border-color: ${cssManager.bdTheme('#ccc', '#333')}; .label-container {
display: flex;
flex-direction: column;
gap: 2px;
flex: 1;
} }
.checkbox-label {
font-size: 14px;
font-weight: 500;
line-height: 20px;
color: ${cssManager.bdTheme('hsl(0 0% 15%)', 'hsl(0 0% 90%)')};
transition: color 0.15s ease;
letter-spacing: -0.01em;
}
.maincontainer:hover .checkbox-label {
color: ${cssManager.bdTheme('hsl(0 0% 9%)', 'hsl(0 0% 95%)')};
}
.maincontainer.disabled:hover .checkbox-label {
color: ${cssManager.bdTheme('hsl(0 0% 15%)', 'hsl(0 0% 90%)')};
}
/* Description */
.description-text { .description-text {
font-size: 12px; font-size: 12px;
color: ${cssManager.bdTheme('#666', '#999')}; color: ${cssManager.bdTheme('hsl(0 0% 45.1%)', 'hsl(0 0% 63.9%)')};
margin-top: 4px; line-height: 1.5;
line-height: 1.4;
padding-left: 36px;
} }
`, `,
]; ];
@ -166,21 +153,26 @@ export class DeesInputCheckbox extends DeesInputBase<DeesInputCheckbox> {
return html` return html`
<div class="input-wrapper"> <div class="input-wrapper">
<div class="maincontainer ${this.disabled ? 'disabled' : ''}" @click="${this.toggleSelected}"> <div class="maincontainer ${this.disabled ? 'disabled' : ''}" @click="${this.toggleSelected}">
<div class="checkbox ${this.value ? 'selected' : ''} ${this.disabled ? 'disabled' : ''}" tabindex="0"> <div
class="checkbox ${this.value ? 'selected' : ''} ${this.disabled ? 'disabled' : ''}"
tabindex="${this.disabled ? '-1' : '0'}"
@keydown="${this.handleKeydown}"
>
${this.value ${this.value
? html` ? html`
<span class="checkmark"> <span class="checkmark">
<div class="checkmark_stem"></div> <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<div class="checkmark_kick"></div> <path d="M20 6L9 17L4 12" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</span> </span>
` `
: html``} : html``}
</div> </div>
${this.label ? html`<div class="checkbox-label">${this.label}</div>` : ''} <div class="label-container">
${this.label ? html`<div class="checkbox-label">${this.label}</div>` : ''}
${this.description ? html`<div class="description-text">${this.description}</div>` : ''}
</div>
</div> </div>
${this.description ? html`
<div class="description-text">${this.description}</div>
` : ''}
</div> </div>
`; `;
} }
@ -213,4 +205,11 @@ export class DeesInputCheckbox extends DeesInputBase<DeesInputCheckbox> {
(checkboxDiv as any).focus(); (checkboxDiv as any).focus();
} }
} }
private handleKeydown(event: KeyboardEvent) {
if (event.key === ' ' || event.key === 'Enter') {
event.preventDefault();
this.toggleSelected();
}
}
} }

View File

@ -0,0 +1,410 @@
import { html, css } from '@design.estate/dees-element';
import '@design.estate/dees-wcctools/demotools';
import './dees-panel.js';
import './dees-input-datepicker.js';
import type { DeesInputDatepicker } from './dees-input-datepicker.js';
export const demoFunc = () => html`
<style>
${css`
.demo-container {
display: flex;
flex-direction: column;
gap: 24px;
padding: 24px;
max-width: 1200px;
margin: 0 auto;
}
dees-panel {
margin-bottom: 24px;
}
dees-panel:last-child {
margin-bottom: 0;
}
.demo-output {
margin-top: 16px;
padding: 12px;
background: rgba(0, 105, 242, 0.1);
border-radius: 4px;
font-size: 14px;
font-family: monospace;
}
.date-group {
display: flex;
gap: 16px;
flex-wrap: wrap;
}
`}
</style>
<div class="demo-container">
<dees-demowrapper .runAfterRender=${async (elementArg: HTMLElement) => {
// Demonstrate basic date picker functionality
const datePicker = elementArg.querySelector('dees-input-datepicker');
if (datePicker) {
datePicker.addEventListener('change', (event: CustomEvent) => {
console.log('Basic date selected:', (event.target as DeesInputDatepicker).value);
});
}
}}>
<dees-panel .title=${'Basic Date Picker'} .subtitle=${'Simple date selection without time'}>
<dees-input-datepicker
label="Select Date"
description="Choose a date from the calendar"
></dees-input-datepicker>
</dees-panel>
</dees-demowrapper>
<dees-demowrapper .runAfterRender=${async (elementArg: HTMLElement) => {
// Demonstrate date and time picker
const dateTimePicker = elementArg.querySelector('dees-input-datepicker[label="Event Date & Time"]');
const appointmentPicker = elementArg.querySelector('dees-input-datepicker[label="Appointment"]');
if (dateTimePicker) {
dateTimePicker.addEventListener('change', (event: CustomEvent) => {
const value = (event.target as DeesInputDatepicker).value;
console.log('24h format datetime:', value);
});
}
if (appointmentPicker) {
appointmentPicker.addEventListener('change', (event: CustomEvent) => {
const value = (event.target as DeesInputDatepicker).value;
console.log('12h format datetime:', value);
});
}
}}>
<dees-panel .title=${'Date and Time Selection'} .subtitle=${'Date pickers with time selection in different formats'}>
<dees-input-datepicker
label="Event Date & Time"
description="Select both date and time (24-hour format)"
.enableTime=${true}
timeFormat="24h"
></dees-input-datepicker>
<dees-input-datepicker
label="Appointment"
description="Date and time with AM/PM selector (15-minute increments)"
.enableTime=${true}
timeFormat="12h"
.minuteIncrement=${15}
></dees-input-datepicker>
</dees-panel>
</dees-demowrapper>
<dees-demowrapper .runAfterRender=${async (elementArg: HTMLElement) => {
// Demonstrate timezone functionality
const timezonePickers = elementArg.querySelectorAll('dees-input-datepicker');
timezonePickers.forEach((picker) => {
picker.addEventListener('change', (event: CustomEvent) => {
const target = event.target as DeesInputDatepicker;
console.log(`${target.label} value:`, target.value);
const input = target.shadowRoot?.querySelector('.date-input') as HTMLInputElement;
if (input) {
console.log(`${target.label} formatted:`, input.value);
}
});
});
}}>
<dees-panel .title=${'Timezone Support'} .subtitle=${'Date and time selection with timezone awareness'}>
<dees-input-datepicker
label="Meeting Time (with Timezone)"
description="Select a date/time and timezone for the meeting"
.enableTime=${true}
.enableTimezone=${true}
timeFormat="24h"
timezone="America/New_York"
></dees-input-datepicker>
<dees-input-datepicker
label="Global Event Schedule"
description="Schedule an event across different timezones"
.enableTime=${true}
.enableTimezone=${true}
timeFormat="12h"
timezone="Europe/London"
.minuteIncrement=${30}
></dees-input-datepicker>
</dees-panel>
</dees-demowrapper>
<dees-demowrapper .runAfterRender=${async (elementArg: HTMLElement) => {
// Demonstrate date constraints
const futureDatePicker = elementArg.querySelector('dees-input-datepicker');
if (futureDatePicker) {
// Show the min/max constraints in action
futureDatePicker.addEventListener('change', (event: CustomEvent) => {
const value = (event.target as DeesInputDatepicker).value;
if (value) {
const selectedDate = new Date(value);
const today = new Date();
const daysDiff = Math.floor((selectedDate.getTime() - today.getTime()) / (1000 * 60 * 60 * 24));
console.log(`Selected date is ${daysDiff} days from today`);
}
});
}
}}>
<dees-panel .title=${'Date Range Constraints'} .subtitle=${'Limit selectable dates with min and max values'}>
<dees-input-datepicker
label="Future Date Only"
description="Can only select dates from today to 90 days in the future"
.minDate=${new Date().toISOString()}
.maxDate=${new Date(Date.now() + 90 * 24 * 60 * 60 * 1000).toISOString()}
></dees-input-datepicker>
</dees-panel>
</dees-demowrapper>
<dees-demowrapper .runAfterRender=${async (elementArg: HTMLElement) => {
// Demonstrate different date formats
const formatters = {
'DD/MM/YYYY': 'European',
'MM/DD/YYYY': 'US',
'YYYY-MM-DD': 'ISO'
};
const datePickers = elementArg.querySelectorAll('dees-input-datepicker');
datePickers.forEach((picker) => {
picker.addEventListener('change', (event: CustomEvent) => {
const target = event.target as DeesInputDatepicker;
// Log the formatted value that's displayed in the input
const input = target.shadowRoot?.querySelector('.date-input') as HTMLInputElement;
if (input) {
console.log(`${target.label} format:`, input.value);
}
});
});
}}>
<dees-panel .title=${'Date Formats'} .subtitle=${'Different date display formats for various regions'}>
<div class="date-group">
<dees-input-datepicker
label="European Format"
dateFormat="DD/MM/YYYY"
.value=${new Date().toISOString()}
></dees-input-datepicker>
<dees-input-datepicker
label="US Format"
dateFormat="MM/DD/YYYY"
.value=${new Date().toISOString()}
></dees-input-datepicker>
<dees-input-datepicker
label="ISO Format"
dateFormat="YYYY-MM-DD"
.value=${new Date().toISOString()}
></dees-input-datepicker>
</div>
</dees-panel>
</dees-demowrapper>
<dees-demowrapper .runAfterRender=${async (elementArg: HTMLElement) => {
// Demonstrate required field validation
const requiredPicker = elementArg.querySelector('dees-input-datepicker[required]');
if (requiredPicker) {
// Monitor blur events for validation
requiredPicker.addEventListener('blur', () => {
const picker = requiredPicker as DeesInputDatepicker;
const value = picker.getValue();
if (!value) {
console.log('Required date field is empty');
}
});
}
}}>
<dees-panel .title=${'Form States'} .subtitle=${'Required and disabled states'}>
<dees-input-datepicker
label="Birth Date"
description="This field is required"
.required=${true}
placeholder="Select your birth date"
></dees-input-datepicker>
<dees-input-datepicker
label="Disabled Date"
description="This field cannot be edited"
.disabled=${true}
.value=${new Date().toISOString()}
></dees-input-datepicker>
</dees-panel>
</dees-demowrapper>
<dees-demowrapper .runAfterRender=${async (elementArg: HTMLElement) => {
// Demonstrate week start customization
const usPicker = elementArg.querySelector('dees-input-datepicker[label="US Calendar"]');
const euPicker = elementArg.querySelector('dees-input-datepicker[label="EU Calendar"]');
if (usPicker) {
console.log('US Calendar starts on Sunday (0)');
}
if (euPicker) {
console.log('EU Calendar starts on Monday (1)');
}
}}>
<dees-panel .title=${'Calendar Customization'} .subtitle=${'Different week start days for various regions'}>
<div class="date-group">
<dees-input-datepicker
label="US Calendar"
description="Week starts on Sunday"
.weekStartsOn=${0}
></dees-input-datepicker>
<dees-input-datepicker
label="EU Calendar"
description="Week starts on Monday"
.weekStartsOn=${1}
></dees-input-datepicker>
</div>
</dees-panel>
</dees-demowrapper>
<dees-demowrapper .runAfterRender=${async (elementArg: HTMLElement) => {
// Generate weekend dates for the current month
const generateWeekends = () => {
const weekends = [];
const now = new Date();
const year = now.getFullYear();
const month = now.getMonth();
// Get all weekends for current month
const date = new Date(year, month, 1);
while (date.getMonth() === month) {
if (date.getDay() === 0 || date.getDay() === 6) {
weekends.push(new Date(date).toISOString());
}
date.setDate(date.getDate() + 1);
}
return weekends;
};
const picker = elementArg.querySelector('dees-input-datepicker');
if (picker) {
picker.disabledDates = generateWeekends();
console.log('Disabled weekend dates for current month');
}
}}>
<dees-panel .title=${'Disabled Dates'} .subtitle=${'Calendar with specific dates disabled (weekends in current month)'}>
<dees-input-datepicker
label="Availability Calendar"
description="Weekends are disabled for the current month"
></dees-input-datepicker>
</dees-panel>
</dees-demowrapper>
<dees-demowrapper .runAfterRender=${async (elementArg: HTMLElement) => {
// Generate sample events for the calendar
const today = new Date();
const currentMonth = today.getMonth();
const currentYear = today.getFullYear();
const sampleEvents = [
// Current week events
{
date: `${currentYear}-${(currentMonth + 1).toString().padStart(2, '0')}-${today.getDate().toString().padStart(2, '0')}`,
title: "Team Meeting",
type: "info" as const,
count: 2
},
{
date: `${currentYear}-${(currentMonth + 1).toString().padStart(2, '0')}-${(today.getDate() + 1).toString().padStart(2, '0')}`,
title: "Project Deadline",
type: "warning" as const
},
{
date: `${currentYear}-${(currentMonth + 1).toString().padStart(2, '0')}-${(today.getDate() + 2).toString().padStart(2, '0')}`,
title: "Release Day",
type: "success" as const
},
{
date: `${currentYear}-${(currentMonth + 1).toString().padStart(2, '0')}-${(today.getDate() + 5).toString().padStart(2, '0')}`,
title: "Urgent Fix Required",
type: "error" as const
},
// Multiple events on one day
{
date: `${currentYear}-${(currentMonth + 1).toString().padStart(2, '0')}-${(today.getDate() + 7).toString().padStart(2, '0')}`,
title: "Multiple Events Today",
type: "info" as const,
count: 5
},
// Next month event
{
date: `${currentYear}-${(currentMonth + 2).toString().padStart(2, '0')}-15`,
title: "Future Planning Session",
type: "info" as const
}
];
const picker = elementArg.querySelector('dees-input-datepicker');
if (picker) {
picker.events = sampleEvents;
console.log('Calendar events loaded:', sampleEvents);
}
}}>
<dees-panel .title=${'Calendar with Events'} .subtitle=${'Visual feedback for scheduled events'}>
<dees-input-datepicker
label="Event Calendar"
description="Days with colored dots have events. Hover to see details."
></dees-input-datepicker>
<div class="demo-output" style="margin-top: 16px;">
<strong>Event Legend:</strong><br>
<span style="color: #0969da;">● Info</span> |
<span style="color: #d29922;">● Warning</span> |
<span style="color: #2ea043;">● Success</span> |
<span style="color: #cf222e;">● Error</span><br>
<em>Days with more than 3 events show a count badge</em>
</div>
</dees-panel>
</dees-demowrapper>
<dees-demowrapper .runAfterRender=${async (elementArg: HTMLElement) => {
// Interactive event demonstration
const picker = elementArg.querySelector('dees-input-datepicker');
const output = elementArg.querySelector('#event-output');
if (picker && output) {
picker.addEventListener('change', (event: CustomEvent) => {
const target = event.target as DeesInputDatepicker;
const value = target.value;
if (value) {
const date = new Date(value);
// Get the formatted value from the input element
const input = target.shadowRoot?.querySelector('.date-input') as HTMLInputElement;
const formattedValue = input?.value || 'N/A';
output.innerHTML = `
<strong>Event triggered!</strong><br>
ISO Value: ${value}<br>
Formatted: ${formattedValue}<br>
Date object: ${date.toLocaleString()}
`;
} else {
output.innerHTML = '<em>Date cleared</em>';
}
});
picker.addEventListener('blur', () => {
console.log('Datepicker lost focus');
});
}
}}>
<dees-panel .title=${'Event Handling'} .subtitle=${'Interactive demonstration of change events'}>
<dees-input-datepicker
label="Event Demo"
description="Select a date to see the event details"
></dees-input-datepicker>
<div id="event-output" class="demo-output">
<em>Select a date to see event details...</em>
</div>
</dees-panel>
</dees-demowrapper>
</div>
`;

File diff suppressed because it is too large Load Diff

View File

@ -1,75 +1,68 @@
import { html, css } from '@design.estate/dees-element'; import { html, css } from '@design.estate/dees-element';
import '@design.estate/dees-wcctools/demotools'; import '@design.estate/dees-wcctools/demotools';
import './dees-panel.js';
import './dees-form.js';
import './dees-form-submit.js';
export const demoFunc = () => html` export const demoFunc = () => html`
<dees-demowrapper> <style>
<style> ${css`
${css` .demo-container {
.demo-container { display: flex;
display: flex; flex-direction: column;
flex-direction: column; gap: 24px;
gap: 24px; padding: 24px;
padding: 24px; max-width: 1200px;
max-width: 1200px; margin: 0 auto;
margin: 0 auto; }
}
dees-panel {
.demo-section { margin-bottom: 24px;
background: #f8f9fa; }
border-radius: 8px;
padding: 24px; dees-panel:last-child {
position: relative; margin-bottom: 0;
} }
@media (prefers-color-scheme: dark) { .horizontal-group {
.demo-section { display: flex;
background: #1a1a1a; align-items: center;
} gap: 16px;
} flex-wrap: wrap;
}
.demo-section h3 {
margin-top: 0; .spacer {
margin-bottom: 16px; height: 200px;
color: #0069f2; display: flex;
font-size: 18px; align-items: center;
} justify-content: center;
color: #999;
.demo-section p { font-size: 14px;
margin-top: 0; }
margin-bottom: 16px; `}
color: #666; </style>
font-size: 14px;
} <div class="demo-container">
<dees-demowrapper .runAfterRender=${async (elementArg: HTMLElement) => {
@media (prefers-color-scheme: dark) { // Demonstrate programmatic interaction with basic dropdowns
.demo-section p { const countryDropdown = elementArg.querySelector('dees-input-dropdown[label="Select Country"]');
color: #999; const roleDropdown = elementArg.querySelector('dees-input-dropdown[label="Select Role"]');
}
} // Log when country changes
if (countryDropdown) {
.horizontal-group { countryDropdown.addEventListener('selectedOption', (event: CustomEvent) => {
display: flex; console.log('Country selected:', event.detail);
align-items: center; });
gap: 16px; }
flex-wrap: wrap;
} // Log when role changes
if (roleDropdown) {
.spacer { roleDropdown.addEventListener('selectedOption', (event: CustomEvent) => {
height: 200px; console.log('Role selected:', event.detail);
display: flex; });
align-items: center; }
justify-content: center; }}>
color: #999; <dees-panel .title=${'1. Basic Dropdowns'} .subtitle=${'Standard dropdown with search functionality and various options'}>
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 <dees-input-dropdown
.label=${'Select Country'} .label=${'Select Country'}
.options=${[ .options=${[
@ -94,12 +87,20 @@ export const demoFunc = () => html`
{ option: 'Guest', key: 'guest' } { option: 'Guest', key: 'guest' }
]} ]}
></dees-input-dropdown> ></dees-input-dropdown>
</div> </dees-panel>
</dees-demowrapper>
<dees-demowrapper .runAfterRender=${async (elementArg: HTMLElement) => {
// Demonstrate simpler dropdown without search
const priorityDropdown = elementArg.querySelector('dees-input-dropdown');
<div class="demo-section"> if (priorityDropdown) {
<h3>Without Search</h3> priorityDropdown.addEventListener('selectedOption', (event: CustomEvent) => {
<p>Dropdown with search functionality disabled for simpler selection</p> console.log(`Priority changed to: ${event.detail.option}`);
});
}
}}>
<dees-panel .title=${'2. Without Search'} .subtitle=${'Dropdown with search functionality disabled for simpler selection'}>
<dees-input-dropdown <dees-input-dropdown
.label=${'Priority Level'} .label=${'Priority Level'}
.enableSearch=${false} .enableSearch=${false}
@ -110,12 +111,22 @@ export const demoFunc = () => html`
]} ]}
.selectedOption=${{ option: 'Medium', key: 'medium' }} .selectedOption=${{ option: 'Medium', key: 'medium' }}
></dees-input-dropdown> ></dees-input-dropdown>
</div> </dees-panel>
</dees-demowrapper>
<dees-demowrapper .runAfterRender=${async (elementArg: HTMLElement) => {
// Demonstrate horizontal layout with multiple dropdowns
const dropdowns = elementArg.querySelectorAll('dees-input-dropdown');
<div class="demo-section"> // Log all changes from horizontal dropdowns
<h3>Horizontal Layout</h3> dropdowns.forEach((dropdown) => {
<p>Multiple dropdowns in a horizontal layout for compact forms</p> dropdown.addEventListener('selectedOption', (event: CustomEvent) => {
const label = dropdown.getAttribute('label');
console.log(`${label}: ${event.detail.option}`);
});
});
}}>
<dees-panel .title=${'3. Horizontal Layout'} .subtitle=${'Multiple dropdowns in a horizontal layout for compact forms'}>
<div class="horizontal-group"> <div class="horizontal-group">
<dees-input-dropdown <dees-input-dropdown
.label=${'Department'} .label=${'Department'}
@ -150,12 +161,21 @@ export const demoFunc = () => html`
]} ]}
></dees-input-dropdown> ></dees-input-dropdown>
</div> </div>
</div> </dees-panel>
</dees-demowrapper>
<dees-demowrapper .runAfterRender=${async (elementArg: HTMLElement) => {
// Demonstrate state handling
const requiredDropdown = elementArg.querySelector('dees-input-dropdown[required]');
<div class="demo-section"> if (requiredDropdown) {
<h3>States</h3> // Show validation state changes
<p>Different states and configurations</p> requiredDropdown.addEventListener('blur', () => {
console.log('Required dropdown lost focus');
});
}
}}>
<dees-panel .title=${'4. States'} .subtitle=${'Different states and configurations'}>
<dees-input-dropdown <dees-input-dropdown
.label=${'Required Field'} .label=${'Required Field'}
.required=${true} .required=${true}
@ -174,16 +194,27 @@ export const demoFunc = () => html`
]} ]}
.selectedOption=${{ option: 'Cannot Select', key: 'disabled' }} .selectedOption=${{ option: 'Cannot Select', key: 'disabled' }}
></dees-input-dropdown> ></dees-input-dropdown>
</div> </dees-panel>
</dees-demowrapper>
<div class="spacer">
(Spacer to test dropdown positioning)
</div>
<dees-demowrapper .runAfterRender=${async (elementArg: HTMLElement) => {
// This dropdown demonstrates automatic positioning
const dropdown = elementArg.querySelector('dees-input-dropdown');
<div class="spacer"> if (dropdown) {
(Spacer to test dropdown positioning) dropdown.addEventListener('selectedOption', (event: CustomEvent) => {
</div> console.log('Bottom dropdown selected:', event.detail);
});
<div class="demo-section">
<h3>Bottom Positioning</h3>
<p>Dropdown that opens upward when near bottom of viewport</p>
// Note: The dropdown automatically detects available space
// and opens upward when near the bottom of the viewport
}
}}>
<dees-panel .title=${'5. Bottom Positioning'} .subtitle=${'Dropdown that opens upward when near bottom of viewport'}>
<dees-input-dropdown <dees-input-dropdown
.label=${'Opens Upward'} .label=${'Opens Upward'}
.options=${[ .options=${[
@ -194,7 +225,99 @@ export const demoFunc = () => html`
{ option: 'Fifth Option', key: 'fifth' } { option: 'Fifth Option', key: 'fifth' }
]} ]}
></dees-input-dropdown> ></dees-input-dropdown>
</div> </dees-panel>
</div> </dees-demowrapper>
</dees-demowrapper>
<dees-demowrapper .runAfterRender=${async (elementArg: HTMLElement) => {
// Setup the interactive payload display
const dropdown = elementArg.querySelector('dees-input-dropdown');
const output = elementArg.querySelector('#selection-output');
if (dropdown && output) {
// Initialize output
output.innerHTML = '<em>Select a product to see details...</em>';
// Handle dropdown changes
dropdown.addEventListener('change', (event: CustomEvent) => {
if (event.detail.value) {
output.innerHTML = `
<strong>Selected:</strong> ${event.detail.value.option}<br>
<strong>Key:</strong> ${event.detail.value.key}<br>
<strong>Price:</strong> $${event.detail.value.payload?.price || 'N/A'}<br>
<strong>Features:</strong> ${event.detail.value.payload?.features?.join(', ') || 'N/A'}
`;
}
});
}
}}>
<dees-panel .title=${'6. Event Handling & Payload'} .subtitle=${'Dropdown with payload data and change event handling'}>
<dees-input-dropdown
.label=${'Select Product'}
.options=${[
{ option: 'Basic Plan', key: 'basic', payload: { price: 9.99, features: ['Feature A'] } },
{ option: 'Pro Plan', key: 'pro', payload: { price: 19.99, features: ['Feature A', 'Feature B'] } },
{ option: 'Enterprise Plan', key: 'enterprise', payload: { price: 49.99, features: ['Feature A', 'Feature B', 'Feature C'] } }
]}
></dees-input-dropdown>
<div id="selection-output" style="margin-top: 16px; padding: 12px; background: rgba(0, 105, 242, 0.1); border-radius: 4px; font-size: 14px;"></div>
</dees-panel>
</dees-demowrapper>
<dees-demowrapper .runAfterRender=${async (elementArg: HTMLElement) => {
// Demonstrate form integration and validation
const form = elementArg.querySelector('dees-form');
const projectTypeDropdown = elementArg.querySelector('dees-input-dropdown[key="projectType"]');
const frameworkDropdown = elementArg.querySelector('dees-input-dropdown[key="framework"]');
if (form) {
form.addEventListener('formData', (event: CustomEvent) => {
console.log('Form submitted with data:', event.detail.data);
});
}
if (projectTypeDropdown && frameworkDropdown) {
// Filter frameworks based on project type
projectTypeDropdown.addEventListener('selectedOption', (event: CustomEvent) => {
const selectedType = event.detail.key;
console.log(`Project type changed to: ${selectedType}`);
// In a real app, you could filter the framework options based on project type
// For demo purposes, we just log the change
});
}
}}>
<dees-panel .title=${'7. Form Integration'} .subtitle=${'Dropdown working within a form with validation'}>
<dees-form>
<dees-input-dropdown
.label=${'Project Type'}
.key=${'projectType'}
.required=${true}
.options=${[
{ option: 'Web Application', key: 'web' },
{ option: 'Mobile Application', key: 'mobile' },
{ option: 'Desktop Application', key: 'desktop' },
{ option: 'API Service', key: 'api' }
]}
></dees-input-dropdown>
<dees-input-dropdown
.label=${'Development Framework'}
.key=${'framework'}
.required=${true}
.options=${[
{ option: 'React', key: 'react', payload: { type: 'web' } },
{ option: 'Vue.js', key: 'vue', payload: { type: 'web' } },
{ option: 'Angular', key: 'angular', payload: { type: 'web' } },
{ option: 'React Native', key: 'react-native', payload: { type: 'mobile' } },
{ option: 'Flutter', key: 'flutter', payload: { type: 'mobile' } },
{ option: 'Electron', key: 'electron', payload: { type: 'desktop' } }
]}
></dees-input-dropdown>
<dees-form-submit .text=${'Create Project'}></dees-form-submit>
</dees-form>
</dees-panel>
</dees-demowrapper>
</div>
` `

View File

@ -9,8 +9,8 @@ import {
} from '@design.estate/dees-element'; } from '@design.estate/dees-element';
import * as domtools from '@design.estate/dees-domtools'; import * as domtools from '@design.estate/dees-domtools';
import { demoFunc } from './dees-input-dropdown.demo.js'; import { demoFunc } from './dees-input-dropdown.demo.js';
import { DeesWindowLayer } from './dees-windowlayer.js';
import { DeesInputBase } from './dees-input-base.js'; import { DeesInputBase } from './dees-input-base.js';
import { cssGeistFontFamily } from './00fonts.js';
declare global { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {
@ -39,13 +39,11 @@ export class DeesInputDropdown extends DeesInputBase<DeesInputDropdown> {
this.selectedOption = val; this.selectedOption = val;
} }
@property({ @property({
type: Boolean, type: Boolean,
}) })
public enableSearch: boolean = true; public enableSearch: boolean = true;
@state() @state()
public opensToTop: boolean = false; public opensToTop: boolean = false;
@ -58,6 +56,9 @@ export class DeesInputDropdown extends DeesInputBase<DeesInputDropdown> {
@state() @state()
public isOpened = false; public isOpened = false;
@state()
private searchValue: string = '';
public static styles = [ public static styles = [
...DeesInputBase.baseStyles, ...DeesInputBase.baseStyles,
cssManager.defaultStyles, cssManager.defaultStyles,
@ -67,123 +68,201 @@ export class DeesInputDropdown extends DeesInputBase<DeesInputDropdown> {
} }
:host { :host {
font-family: Roboto; font-family: ${cssGeistFontFamily};
position: relative; position: relative;
color: ${cssManager.bdTheme('#222', '#fff')}; color: ${cssManager.bdTheme('hsl(0 0% 15%)', 'hsl(0 0% 90%)')};
} }
.maincontainer { .maincontainer {
display: block; display: block;
position: relative;
} }
.selectedBox { .selectedBox {
user-select: none; user-select: none;
position: relative; position: relative;
max-width: 420px; width: 100%;
height: 40px; height: 40px;
line-height: 40px; line-height: 38px;
padding: 0px 8px; padding: 0 40px 0 12px;
background: ${cssManager.bdTheme('#fafafa', '#222222')}; background: transparent;
box-shadow: ${cssManager.bdTheme('0px 1px 4px rgba(0,0,0,0.3)', 'none')}; border: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
border-radius: 3px; border-radius: 6px;
border-top: ${cssManager.bdTheme('1px solid #CCC', '1px solid #ffffff10')}; transition: all 0.15s ease;
border-bottom: ${cssManager.bdTheme('1px solid #CCC', '1px solid #222')}; font-size: 14px;
transition: all 0.2s ease; color: ${cssManager.bdTheme('hsl(0 0% 9%)', 'hsl(0 0% 95%)')};
font-size: 16px; cursor: pointer;
color: ${cssManager.bdTheme('#222', '#ccc')}; overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
} }
.selectedBox:hover { .selectedBox:hover:not(.disabled) {
filter: ${cssManager.bdTheme('brightness(0.95)', 'brightness(1.1)')}; border-color: ${cssManager.bdTheme('hsl(0 0% 79.8%)', 'hsl(0 0% 20.9%)')};
} }
.accentBottom { .selectedBox:focus-visible {
filter: none !important; outline: none;
border-color: ${cssManager.bdTheme('hsl(222.2 47.4% 51.2%)', 'hsl(217.2 91.2% 59.8%)')};
box-shadow: 0 0 0 3px ${cssManager.bdTheme('hsl(222.2 47.4% 51.2% / 0.1)', 'hsl(217.2 91.2% 59.8% / 0.1)')};
} }
.accentTop { .selectedBox.disabled {
filter: none !important; background: ${cssManager.bdTheme('hsl(0 0% 95.1%)', 'hsl(0 0% 14.9%)')};
border-color: ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
color: ${cssManager.bdTheme('hsl(0 0% 63.9%)', 'hsl(0 0% 45.1%)')};
cursor: not-allowed;
opacity: 0.5;
}
/* Dropdown arrow */
.selectedBox::after {
content: '';
position: absolute;
right: 12px;
top: 50%;
transform: translateY(-50%);
width: 0;
height: 0;
border-left: 4px solid transparent;
border-right: 4px solid transparent;
border-top: 4px solid ${cssManager.bdTheme('hsl(0 0% 45.1%)', 'hsl(0 0% 63.9%)')};
transition: transform 0.15s ease;
}
.selectedBox.open::after {
transform: translateY(-50%) rotate(180deg);
} }
.selectionBox { .selectionBox {
will-change: transform; will-change: transform, opacity;
pointer-events: none; pointer-events: none;
transition: all 0.2s ease; transition: all 0.15s ease;
opacity: 0; opacity: 0;
background: ${cssManager.bdTheme('#ffffff', '#222222')}; transform: translateY(-8px) scale(0.98);
max-width: 420px; background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(0 0% 3.9%)')};
box-shadow: 0px 0px 5px rgba(0, 0, 0, 0.2); border: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
box-shadow: 0 4px 6px -1px hsl(0 0% 0% / 0.1), 0 2px 4px -2px hsl(0 0% 0% / 0.1);
min-height: 40px; min-height: 40px;
border-radius: 8px; max-height: 300px;
padding: 4px 8px; overflow: hidden;
border-radius: 6px;
position: absolute; position: absolute;
user-select: none; user-select: none;
margin: 3px 0px 0px 0px; margin-top: 4px;
border-top: ${cssManager.bdTheme('1px solid #CCC', '1px solid #ffffff10')}; z-index: 50;
left: 0;
right: 0;
} }
.selectionBox.top { .selectionBox.top {
transform: translate(0px, 4px); bottom: calc(100% + 4px);
top: auto;
margin-top: 0;
margin-bottom: 4px;
transform: translateY(8px) scale(0.98);
} }
.selectionBox.bottom { .selectionBox.bottom {
transform: translate(0px, -4px); top: 100%;
} }
.selectionBox.show { .selectionBox.show {
pointer-events: all; pointer-events: all;
transform: scale(1, 1) translate(0px, 0px); transform: translateY(0) scale(1);
opacity: 1; opacity: 1;
box-shadow: 0px 0px 8px rgba(0, 0, 0, 0.8);
} }
/* Options container */
.options-container {
max-height: 250px;
overflow-y: auto;
padding: 4px;
}
/* Options */
.option { .option {
transition: all 0.1s; transition: all 0.15s ease;
line-height: 32px; line-height: 32px;
padding: 0px 4px; padding: 0 8px;
border-radius: 3px; border-radius: 4px;
margin: 4px 0px; margin: 2px 0;
cursor: pointer;
font-size: 14px;
color: ${cssManager.bdTheme('hsl(0 0% 15%)', 'hsl(0 0% 90%)')};
} }
.option.highlighted { .option.highlighted {
border-left: 2px solid #0069f2; background: ${cssManager.bdTheme('hsl(0 0% 95.1%)', 'hsl(0 0% 14.9%)')};
padding-left: 6px;
background: #ffffff20;
} }
.option:hover { .option:hover {
color: #fff; background: ${cssManager.bdTheme('hsl(0 0% 95.1%)', 'hsl(0 0% 14.9%)')};
padding-left: 8px; color: ${cssManager.bdTheme('hsl(0 0% 9%)', 'hsl(0 0% 95%)')};
background: #0069f2;
} }
.search.top { /* No options message */
padding-top: 4px; .no-options {
padding: 8px;
text-align: center;
font-size: 14px;
color: ${cssManager.bdTheme('hsl(0 0% 45.1%)', 'hsl(0 0% 63.9%)')};
font-style: italic;
} }
/* Search */
.search {
padding: 4px;
border-bottom: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
margin-bottom: 4px;
}
.search.bottom { .search.bottom {
padding-bottom: 4px; border-bottom: none;
border-top: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
margin-bottom: 0;
margin-top: 4px;
} }
.search input { .search input {
display: block; display: block;
background: none;
border: none;
height: 24px;
color: inherit;
text-align: left;
font-size: 12px;
font-weight: 600;
width: 100%; width: 100%;
margin: auto; height: 32px;
padding: 0 8px;
background: transparent;
border: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
border-radius: 4px;
color: inherit;
font-size: 14px;
font-family: inherit;
outline: none;
transition: border-color 0.15s ease;
} }
.search.top input { .search input::placeholder {
border-bottom: 1px dotted #333; color: ${cssManager.bdTheme('hsl(0 0% 63.9%)', 'hsl(0 0% 45.1%)')};
}
.search.bottom input {
border-top: 1px dotted #333;
} }
.search input:focus { .search input:focus {
outline: none; border-color: ${cssManager.bdTheme('hsl(222.2 47.4% 51.2%)', 'hsl(217.2 91.2% 59.8%)')};
}
/* Scrollbar styling */
.options-container::-webkit-scrollbar {
width: 8px;
}
.options-container::-webkit-scrollbar-track {
background: transparent;
}
.options-container::-webkit-scrollbar-thumb {
background: ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
border-radius: 4px;
}
.options-container::-webkit-scrollbar-thumb:hover {
background: ${cssManager.bdTheme('hsl(0 0% 79.8%)', 'hsl(0 0% 20.9%)')};
} }
`, `,
]; ];
@ -191,61 +270,78 @@ export class DeesInputDropdown extends DeesInputBase<DeesInputDropdown> {
public render(): TemplateResult { public render(): TemplateResult {
return html` return html`
<div class="input-wrapper"> <div class="input-wrapper">
<dees-label .label=${this.label}></dees-label> <dees-label .label=${this.label} .description=${this.description} .required=${this.required}></dees-label>
<div class="maincontainer" @keydown="${this.isOpened ? this.handleKeyDown : undefined}"> <div class="maincontainer">
<div class="selectionBox"> <div
${this.enableSearch && !this.opensToTop class="selectedBox ${this.isOpened ? 'open' : ''} ${this.disabled ? 'disabled' : ''}"
? html` @click="${() => !this.disabled && this.toggleSelectionBox()}"
<div class="search top"> tabindex="${this.disabled ? '-1' : '0'}"
<input type="text" placeholder="Search" @input="${this.handleSearch}" /> @keydown="${this.handleSelectedBoxKeydown}"
</div> >
` ${this.selectedOption?.option || 'Select an option'}
: null} </div>
${this.filteredOptions.map((option, index) => { <div class="selectionBox ${this.isOpened ? 'show' : ''} ${this.opensToTop ? 'top' : 'bottom'}">
const isHighlighted = this.highlightedIndex === index; ${this.enableSearch
return html` ? html`
<div <div class="search">
class="option ${isHighlighted ? 'highlighted' : ''}" <input
@click=${() => { type="text"
this.updateSelection(option); placeholder="Search options..."
}} .value="${this.searchValue}"
> @input="${this.handleSearch}"
${option.option} @click="${(e: Event) => e.stopPropagation()}"
</div> @keydown="${this.handleSearchKeydown}"
`; />
})} </div>
${this.enableSearch && this.opensToTop `
? html` : null}
<div class="search bottom"> <div class="options-container">
<input type="text" placeholder="Search" @input="${this.handleSearch}" /> ${this.filteredOptions.length === 0
</div> ? html`<div class="no-options">No options found</div>`
` : this.filteredOptions.map((option, index) => {
: null} const isHighlighted = this.highlightedIndex === index;
</div> return html`
<div <div
class="selectedBox" class="option ${isHighlighted ? 'highlighted' : ''}"
@click="${(event) => { @click="${() => this.updateSelection(option)}"
if (!this.isElevated) { @mouseenter="${() => this.highlightedIndex = index}"
this.toggleSelectionBox(); >
} else { ${option.option}
this.updateSelection(this.selectedOption); </div>
} `;
}}" })
> }
${this.selectedOption?.option || 'Select...'} </div>
</div> </div>
</div> </div>
</div> </div>
`; `;
} }
firstUpdated() { async connectedCallback() {
this.selectedOption = this.selectedOption || null; super.connectedCallback();
this.filteredOptions = this.options; // Initialize filteredOptions this.handleClickOutside = this.handleClickOutside.bind(this);
} }
public async updateSelection(selectedOption) { firstUpdated() {
this.selectedOption = this.selectedOption || null;
this.filteredOptions = this.options;
}
updated(changedProperties: Map<string, any>) {
super.updated(changedProperties);
if (changedProperties.has('options')) {
this.filteredOptions = this.options;
}
}
public async updateSelection(selectedOption: { option: string; key: string; payload?: any }) {
this.selectedOption = selectedOption; this.selectedOption = selectedOption;
this.isOpened = false;
this.searchValue = '';
this.filteredOptions = this.options;
this.highlightedIndex = 0;
this.dispatchEvent( this.dispatchEvent(
new CustomEvent('selectedOption', { new CustomEvent('selectedOption', {
@ -253,110 +349,105 @@ export class DeesInputDropdown extends DeesInputBase<DeesInputDropdown> {
bubbles: true, bubbles: true,
}) })
); );
if (this.isElevated) {
this.toggleSelectionBox();
}
this.changeSubject.next(this); this.changeSubject.next(this);
} }
private isElevated: boolean = false; private handleClickOutside = (event: MouseEvent) => {
private windowOverlay: DeesWindowLayer; const path = event.composedPath();
if (!path.includes(this)) {
this.isOpened = false;
this.searchValue = '';
this.filteredOptions = this.options;
document.removeEventListener('click', this.handleClickOutside);
}
};
public async toggleSelectionBox() { public async toggleSelectionBox() {
this.isOpened = !this.isOpened; this.isOpened = !this.isOpened;
const domtoolsInstance = await this.domtoolsPromise;
const selectedBox: HTMLElement = this.shadowRoot.querySelector('.selectedBox'); if (this.isOpened) {
const selectionBox: HTMLElement = this.shadowRoot.querySelector('.selectionBox'); // Check available space and set position
if (!this.isElevated) { const selectedBox = this.shadowRoot.querySelector('.selectedBox') as HTMLElement;
this.windowOverlay = await DeesWindowLayer.createAndShow({ const rect = selectedBox.getBoundingClientRect();
blur: false, const spaceBelow = window.innerHeight - rect.bottom;
}); const spaceAbove = rect.top;
const elevatedDropdown = new DeesInputDropdown();
elevatedDropdown.isElevated = true; // Determine if we should open upwards
elevatedDropdown.label = this.label; this.opensToTop = spaceBelow < 300 && spaceAbove > spaceBelow;
elevatedDropdown.enableSearch = this.enableSearch;
elevatedDropdown.required = this.required; // Focus search input if present
elevatedDropdown.disabled = this.disabled; await this.updateComplete;
elevatedDropdown.style.position = 'fixed'; const searchInput = this.shadowRoot.querySelector('.search input') as HTMLInputElement;
elevatedDropdown.style.top = this.getBoundingClientRect().top + 'px'; if (searchInput) {
elevatedDropdown.style.left = this.getBoundingClientRect().left + 'px'; searchInput.focus();
elevatedDropdown.style.width = this.clientWidth + 'px';
elevatedDropdown.options = this.options;
elevatedDropdown.selectedOption = this.selectedOption;
elevatedDropdown.highlightedIndex = elevatedDropdown.selectedOption ? elevatedDropdown.options.indexOf(
elevatedDropdown.options.find((option) => option.key === this.selectedOption.key)
) : -1;
console.log(elevatedDropdown.options);
console.log(elevatedDropdown.selectedOption);
console.log(elevatedDropdown.highlightedIndex);
this.windowOverlay.appendChild(elevatedDropdown);
await domtoolsInstance.convenience.smartdelay.delayFor(0);
elevatedDropdown.toggleSelectionBox();
const destroyOverlay = async () => {
(elevatedDropdown.shadowRoot.querySelector('.selectionBox') as HTMLElement).style.opacity =
'0';
elevatedDropdown.removeEventListener('selectedOption', handleSelection);
this.windowOverlay.removeEventListener('clicked', destroyOverlay);
this.windowOverlay.destroy();
};
const handleSelection = async (event) => {
await this.updateSelection(elevatedDropdown.selectedOption);
destroyOverlay();
};
elevatedDropdown.addEventListener('selectedOption', handleSelection);
this.windowOverlay.addEventListener('clicked', destroyOverlay);
} else {
if (!selectionBox.classList.contains('show')) {
selectionBox.style.width = selectedBox.clientWidth + 'px';
const spaceData = selectedBox.getBoundingClientRect();
if (300 > window.innerHeight - spaceData.bottom) {
this.opensToTop = true;
selectedBox.classList.add('accentTop');
selectionBox.classList.add('top');
selectionBox.style.bottom = selectedBox.clientHeight + 2 + 'px';
} else {
selectedBox.classList.add('accentBottom');
selectionBox.classList.add('bottom');
this.opensToTop = false;
const labelOffset = this.label ? 24 : 0;
selectionBox.style.top = selectedBox.clientHeight + labelOffset + 'px';
}
await domtoolsInstance.convenience.smartdelay.delayFor(0);
const searchInput = selectionBox.querySelector('input');
searchInput?.focus();
selectionBox.classList.add('show');
} else {
selectedBox.style.pointerEvents = 'none';
selectionBox.classList.remove('show');
// selectedBox.style.opacity = '0';
} }
// Add click outside listener
setTimeout(() => {
document.addEventListener('click', this.handleClickOutside);
}, 0);
} else {
// Cleanup
this.searchValue = '';
this.filteredOptions = this.options;
document.removeEventListener('click', this.handleClickOutside);
} }
} }
private handleSearch(event: Event): void { private handleSearch(event: Event): void {
const searchTerm = (event.target as HTMLInputElement).value.toLowerCase(); const searchTerm = (event.target as HTMLInputElement).value;
this.searchValue = searchTerm;
const searchLower = searchTerm.toLowerCase();
this.filteredOptions = this.options.filter((option) => this.filteredOptions = this.options.filter((option) =>
option.option.toLowerCase().includes(searchTerm) option.option.toLowerCase().includes(searchLower)
); );
this.highlightedIndex = 0; // Reset highlighted index this.highlightedIndex = 0;
} }
private handleKeyDown(event: KeyboardEvent): void { private handleKeyDown(event: KeyboardEvent): void {
if (!this.isOpened) {
console.log('discarded key event. Check why this function is called.');
return;
}
const key = event.key; const key = event.key;
const maxIndex = this.filteredOptions.length - 1; const maxIndex = this.filteredOptions.length - 1;
if (key === 'ArrowDown') { if (key === 'ArrowDown') {
event.preventDefault();
this.highlightedIndex = this.highlightedIndex + 1 > maxIndex ? 0 : this.highlightedIndex + 1; this.highlightedIndex = this.highlightedIndex + 1 > maxIndex ? 0 : this.highlightedIndex + 1;
event.preventDefault();
} else if (key === 'ArrowUp') { } else if (key === 'ArrowUp') {
event.preventDefault();
this.highlightedIndex = this.highlightedIndex - 1 < 0 ? maxIndex : this.highlightedIndex - 1; this.highlightedIndex = this.highlightedIndex - 1 < 0 ? maxIndex : this.highlightedIndex - 1;
event.preventDefault();
} else if (key === 'Enter') { } else if (key === 'Enter') {
this.updateSelection(this.filteredOptions[this.highlightedIndex]);
event.preventDefault(); event.preventDefault();
if (this.filteredOptions[this.highlightedIndex]) {
this.updateSelection(this.filteredOptions[this.highlightedIndex]);
}
} else if (key === 'Escape') {
event.preventDefault();
this.isOpened = false;
}
}
private handleSearchKeydown(event: KeyboardEvent): void {
if (event.key === 'ArrowDown' || event.key === 'ArrowUp' || event.key === 'Enter') {
this.handleKeyDown(event);
}
}
private handleSelectedBoxKeydown(event: KeyboardEvent) {
if (this.disabled) return;
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
this.toggleSelectionBox();
} else if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
event.preventDefault();
if (!this.isOpened) {
this.toggleSelectionBox();
}
} else if (event.key === 'Escape') {
event.preventDefault();
if (this.isOpened) {
this.isOpened = false;
}
} }
} }
@ -367,4 +458,9 @@ export class DeesInputDropdown extends DeesInputBase<DeesInputDropdown> {
public setValue(value: { option: string; key: string; payload?: any }): void { public setValue(value: { option: string; key: string; payload?: any }): void {
this.selectedOption = value; this.selectedOption = value;
} }
}
async disconnectedCallback() {
await super.disconnectedCallback();
document.removeEventListener('click', this.handleClickOutside);
}
}

View File

@ -37,6 +37,9 @@ export class DeesInputFileupload extends DeesInputBase<DeesInputFileupload> {
@property() @property()
public state: 'idle' | 'dragOver' | 'dropped' | 'uploading' | 'completed' = 'idle'; public state: 'idle' | 'dragOver' | 'dropped' | 'uploading' | 'completed' = 'idle';
@property({ type: Boolean })
private isLoading: boolean = false;
@property({ @property({
type: String, type: String,
}) })
@ -68,7 +71,7 @@ export class DeesInputFileupload extends DeesInputBase<DeesInputFileupload> {
:host { :host {
position: relative; position: relative;
display: block; display: block;
color: ${cssManager.bdTheme('#333', '#ccc')}; color: ${cssManager.bdTheme('hsl(0 0% 15%)', 'hsl(0 0% 90%)')};
} }
.hidden { .hidden {
@ -83,16 +86,16 @@ export class DeesInputFileupload extends DeesInputBase<DeesInputFileupload> {
.maincontainer { .maincontainer {
position: relative; position: relative;
border-radius: 8px; border-radius: 6px;
padding: 16px; padding: 16px;
background: ${cssManager.bdTheme('#f8f9fa', '#1a1a1a')}; background: ${cssManager.bdTheme('hsl(210 40% 98%)', 'hsl(215 20.2% 11.8%)')};
color: ${cssManager.bdTheme('#333', '#ccc')}; color: ${cssManager.bdTheme('hsl(0 0% 9%)', 'hsl(0 0% 95%)')};
border: 1px solid ${cssManager.bdTheme('#e0e0e0', '#333')}; border: 1px solid ${cssManager.bdTheme('hsl(215 20.2% 65.1%)', 'hsl(215 20.2% 35.1%)')};
transition: all 0.2s ease; transition: all 0.15s ease;
} }
.maincontainer:hover { .maincontainer:hover {
border-color: ${cssManager.bdTheme('#ccc', '#444')}; border-color: ${cssManager.bdTheme('hsl(215 20.2% 55.1%)', 'hsl(215 20.2% 45.1%)')};
} }
:host([disabled]) .maincontainer { :host([disabled]) .maincontainer {
@ -102,69 +105,69 @@ export class DeesInputFileupload extends DeesInputBase<DeesInputFileupload> {
} }
:host([validationState="invalid"]) .maincontainer { :host([validationState="invalid"]) .maincontainer {
border-color: #e74c3c; border-color: ${cssManager.bdTheme('hsl(0 72.2% 50.6%)', 'hsl(0 62.8% 30.6%)')};
} }
:host([validationState="valid"]) .maincontainer { :host([validationState="valid"]) .maincontainer {
border-color: #27ae60; border-color: ${cssManager.bdTheme('hsl(142.1 70.6% 45.3%)', 'hsl(142.1 76.2% 36.3%)')};
} }
:host([validationState="warn"]) .maincontainer { :host([validationState="warn"]) .maincontainer {
border-color: #f39c12; border-color: ${cssManager.bdTheme('hsl(45.4 93.4% 47.5%)', 'hsl(45.4 93.4% 47.5%)')};
} }
.maincontainer::after { .maincontainer::after {
top: 2px; top: 1px;
right: 2px; right: 1px;
left: 2px; left: 1px;
bottom: 2px; bottom: 1px;
transform: scale3d(0.98, 0.9, 1); transform: scale3d(0.98, 0.95, 1);
position: absolute; position: absolute;
content: ''; content: '';
display: block; display: block;
border: 2px dashed transparent; border: 2px dashed transparent;
border-radius: 6px; border-radius: 5px;
transition: all 0.3s ease; transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
pointer-events: none; pointer-events: none;
background: transparent; background: transparent;
} }
.maincontainer.dragOver { .maincontainer.dragOver {
border-color: ${cssManager.bdTheme('#0084ff', '#0084ff')}; border-color: ${cssManager.bdTheme('hsl(217.2 91.2% 59.8%)', 'hsl(213.1 93.9% 67.8%)')};
background: ${cssManager.bdTheme('#f0f8ff', '#001933')}; background: ${cssManager.bdTheme('hsl(217.2 91.2% 59.8% / 0.05)', 'hsl(213.1 93.9% 67.8% / 0.05)')};
} }
.maincontainer.dragOver::after { .maincontainer.dragOver::after {
transform: scale3d(1, 1, 1); transform: scale3d(1, 1, 1);
border: 2px dashed ${cssManager.bdTheme('#0084ff', '#0084ff')}; border: 2px dashed ${cssManager.bdTheme('hsl(217.2 91.2% 59.8%)', 'hsl(213.1 93.9% 67.8%)')};
} }
.uploadButton { .uploadButton {
position: relative; position: relative;
padding: 12px 24px; padding: 10px 20px;
background: ${cssManager.bdTheme('#0084ff', '#0084ff')}; background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(0 0% 7.8%)')};
color: white; color: ${cssManager.bdTheme('hsl(0 0% 9%)', 'hsl(0 0% 95%)')};
border: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
border-radius: 6px; border-radius: 6px;
text-align: center; text-align: center;
font-size: 14px; font-size: 14px;
font-weight: 500; font-weight: 500;
cursor: pointer; cursor: pointer;
transition: all 0.2s ease; transition: all 0.15s ease;
border: none;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
gap: 8px; gap: 8px;
line-height: 20px;
} }
.uploadButton:hover { .uploadButton:hover {
background: ${cssManager.bdTheme('#0073e6', '#0073e6')}; background: ${cssManager.bdTheme('hsl(0 0% 95.1%)', 'hsl(0 0% 14.9%)')};
transform: translateY(-1px); border-color: ${cssManager.bdTheme('hsl(0 0% 79.8%)', 'hsl(0 0% 20.9%)')};
box-shadow: 0 2px 8px rgba(0, 132, 255, 0.3);
} }
.uploadButton:active { .uploadButton:active {
transform: translateY(0); background: ${cssManager.bdTheme('hsl(0 0% 91%)', 'hsl(0 0% 11%)')};
} }
.uploadButton dees-icon { .uploadButton dees-icon {
@ -181,21 +184,21 @@ export class DeesInputFileupload extends DeesInputBase<DeesInputFileupload> {
.uploadCandidate { .uploadCandidate {
display: grid; display: grid;
grid-template-columns: 40px 1fr auto; grid-template-columns: 40px 1fr auto;
background: ${cssManager.bdTheme('#ffffff', '#2a2a2a')}; background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(215 20.2% 16.8%)')};
padding: 12px; padding: 12px;
text-align: left; text-align: left;
border-radius: 6px; border-radius: 6px;
color: ${cssManager.bdTheme('#333', '#ccc')}; color: ${cssManager.bdTheme('hsl(0 0% 9%)', 'hsl(0 0% 95%)')};
cursor: default; cursor: default;
transition: all 0.2s; transition: all 0.15s ease;
border: 1px solid ${cssManager.bdTheme('#e0e0e0', '#333')}; border: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
position: relative; position: relative;
overflow: hidden; overflow: hidden;
} }
.uploadCandidate:hover { .uploadCandidate:hover {
background: ${cssManager.bdTheme('#f5f5f5', '#333')}; background: ${cssManager.bdTheme('hsl(0 0% 95.1%)', 'hsl(215 20.2% 20.8%)')};
border-color: ${cssManager.bdTheme('#ccc', '#444')}; border-color: ${cssManager.bdTheme('hsl(0 0% 79.8%)', 'hsl(0 0% 20.9%)')};
} }
.uploadCandidate .icon { .uploadCandidate .icon {
@ -203,19 +206,19 @@ export class DeesInputFileupload extends DeesInputBase<DeesInputFileupload> {
align-items: center; align-items: center;
justify-content: center; justify-content: center;
font-size: 20px; font-size: 20px;
color: ${cssManager.bdTheme('#666', '#999')}; color: ${cssManager.bdTheme('hsl(215.4 16.3% 56.9%)', 'hsl(215 20.2% 55.1%)')};
} }
.uploadCandidate.image-file .icon { .uploadCandidate.image-file .icon {
color: #4CAF50; color: ${cssManager.bdTheme('hsl(142.1 70.6% 45.3%)', 'hsl(142.1 76.2% 36.3%)')};
} }
.uploadCandidate.pdf-file .icon { .uploadCandidate.pdf-file .icon {
color: #f44336; color: ${cssManager.bdTheme('hsl(0 72.2% 50.6%)', 'hsl(0 62.8% 30.6%)')};
} }
.uploadCandidate.doc-file .icon { .uploadCandidate.doc-file .icon {
color: #2196F3; color: ${cssManager.bdTheme('hsl(217.2 91.2% 59.8%)', 'hsl(213.1 93.9% 67.8%)')};
} }
.uploadCandidate .info { .uploadCandidate .info {
@ -235,7 +238,7 @@ export class DeesInputFileupload extends DeesInputBase<DeesInputFileupload> {
.uploadCandidate .filesize { .uploadCandidate .filesize {
font-size: 12px; font-size: 12px;
color: ${cssManager.bdTheme('#666', '#999')}; color: ${cssManager.bdTheme('hsl(215.4 16.3% 56.9%)', 'hsl(215 20.2% 55.1%)')};
} }
.uploadCandidate .actions { .uploadCandidate .actions {
@ -254,13 +257,13 @@ export class DeesInputFileupload extends DeesInputBase<DeesInputFileupload> {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
transition: all 0.2s; transition: all 0.15s ease;
color: ${cssManager.bdTheme('#666', '#999')}; color: ${cssManager.bdTheme('hsl(215.4 16.3% 56.9%)', 'hsl(215 20.2% 55.1%)')};
} }
.remove-button:hover { .remove-button:hover {
background: ${cssManager.bdTheme('#fee', '#4a1c1c')}; background: ${cssManager.bdTheme('hsl(0 72.2% 50.6% / 0.1)', 'hsl(0 62.8% 30.6% / 0.1)')};
color: ${cssManager.bdTheme('#e74c3c', '#ff6b6b')}; color: ${cssManager.bdTheme('hsl(0 72.2% 50.6%)', 'hsl(0 62.8% 30.6%)')};
} }
.clear-all-button { .clear-all-button {
@ -271,36 +274,37 @@ export class DeesInputFileupload extends DeesInputBase<DeesInputFileupload> {
.clear-all-button button { .clear-all-button button {
background: none; background: none;
border: none; border: none;
color: ${cssManager.bdTheme('#666', '#999')}; color: ${cssManager.bdTheme('hsl(215.4 16.3% 56.9%)', 'hsl(215 20.2% 55.1%)')};
cursor: pointer; cursor: pointer;
font-size: 12px; font-size: 12px;
padding: 4px 8px; padding: 4px 8px;
border-radius: 4px; border-radius: 4px;
transition: all 0.2s; transition: all 0.15s ease;
} }
.clear-all-button button:hover { .clear-all-button button:hover {
background: ${cssManager.bdTheme('#fee', '#4a1c1c')}; background: ${cssManager.bdTheme('hsl(0 72.2% 50.6% / 0.1)', 'hsl(0 62.8% 30.6% / 0.1)')};
color: ${cssManager.bdTheme('#e74c3c', '#ff6b6b')}; color: ${cssManager.bdTheme('hsl(0 72.2% 50.6%)', 'hsl(0 62.8% 30.6%)')};
} }
.validation-message { .validation-message {
font-size: 12px; font-size: 13px;
margin-top: 4px; margin-top: 6px;
color: #e74c3c; color: ${cssManager.bdTheme('hsl(0 72.2% 50.6%)', 'hsl(0 62.8% 30.6%)')};
line-height: 1.5;
} }
.drop-hint { .drop-hint {
text-align: center; text-align: center;
padding: 40px 20px; padding: 40px 20px;
color: ${cssManager.bdTheme('#999', '#666')}; color: ${cssManager.bdTheme('hsl(215.4 16.3% 56.9%)', 'hsl(215 20.2% 55.1%)')};
font-size: 14px; font-size: 14px;
} }
.drop-hint dees-icon { .drop-hint dees-icon {
font-size: 48px; font-size: 48px;
margin-bottom: 16px; margin-bottom: 16px;
opacity: 0.3; opacity: 0.2;
} }
.image-preview { .image-preview {
@ -311,10 +315,57 @@ export class DeesInputFileupload extends DeesInputBase<DeesInputFileupload> {
} }
.description-text { .description-text {
font-size: 12px; font-size: 13px;
color: ${cssManager.bdTheme('#666', '#999')}; color: ${cssManager.bdTheme('hsl(215.4 16.3% 56.9%)', 'hsl(215 20.2% 55.1%)')};
margin-top: 4px; margin-top: 6px;
line-height: 1.4; line-height: 1.5;
}
/* Loading state styles */
.uploadButton.loading {
pointer-events: none;
opacity: 0.8;
}
.uploadButton .button-content {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
.loading-spinner {
width: 16px;
height: 16px;
border: 2px solid ${cssManager.bdTheme('rgba(0, 0, 0, 0.1)', 'rgba(255, 255, 255, 0.1)')};
border-top-color: ${cssManager.bdTheme('#3b82f6', '#60a5fa')};
border-radius: 50%;
animation: spin 0.6s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
@keyframes pulse {
0% {
transform: scale(1);
opacity: 1;
}
50% {
transform: scale(1.02);
opacity: 0.9;
}
100% {
transform: scale(1);
opacity: 1;
}
}
.uploadButton.loading {
animation: pulse 1s ease-in-out infinite;
} }
`, `,
]; ];
@ -352,7 +403,7 @@ export class DeesInputFileupload extends DeesInputBase<DeesInputFileupload> {
${isImage && this.canShowPreview(fileArg) ? html` ${isImage && this.canShowPreview(fileArg) ? html`
<img class="image-preview" src="${URL.createObjectURL(fileArg)}" alt="${fileArg.name}"> <img class="image-preview" src="${URL.createObjectURL(fileArg)}" alt="${fileArg.name}">
` : html` ` : html`
<dees-icon .iconName=${this.getFileIcon(fileArg)}></dees-icon> <dees-icon .icon=${this.getFileIcon(fileArg)}></dees-icon>
`} `}
</div> </div>
<div class="info"> <div class="info">
@ -365,7 +416,7 @@ export class DeesInputFileupload extends DeesInputBase<DeesInputFileupload> {
@click=${() => this.removeFile(fileArg)} @click=${() => this.removeFile(fileArg)}
title="Remove file" title="Remove file"
> >
<dees-icon .iconName=${'lucide:x'}></dees-icon> <dees-icon .icon=${'lucide:x'}></dees-icon>
</button> </button>
</div> </div>
</div> </div>
@ -374,13 +425,20 @@ export class DeesInputFileupload extends DeesInputBase<DeesInputFileupload> {
</div> </div>
` : html` ` : html`
<div class="drop-hint"> <div class="drop-hint">
<dees-icon .iconName=${'lucide:cloud-upload'}></dees-icon> <dees-icon .icon=${'lucide:cloud-upload'}></dees-icon>
<div>Drag files here or click to browse</div> <div>Drag files here or click to browse</div>
</div> </div>
`} `}
<div class="uploadButton" @click=${this.openFileSelector}> <div class="uploadButton ${this.isLoading ? 'loading' : ''}" @click=${this.openFileSelector}>
<dees-icon .iconName=${'lucide:upload'}></dees-icon> <div class="button-content">
${this.buttonText} ${this.isLoading ? html`
<div class="loading-spinner"></div>
<span>Opening...</span>
` : html`
<dees-icon .icon=${'lucide:upload'}></dees-icon>
${this.buttonText}
`}
</div>
</div> </div>
</div> </div>
${this.description ? html` ${this.description ? html`
@ -481,8 +539,25 @@ export class DeesInputFileupload extends DeesInputBase<DeesInputFileupload> {
} }
public async openFileSelector() { public async openFileSelector() {
if (this.disabled) return; if (this.disabled || this.isLoading) return;
// Set loading state
this.isLoading = true;
const inputFile: HTMLInputElement = this.shadowRoot.querySelector('input[type="file"]'); const inputFile: HTMLInputElement = this.shadowRoot.querySelector('input[type="file"]');
// Set up a focus handler to detect when the dialog is closed without selection
const handleFocus = () => {
setTimeout(() => {
// Check if no file was selected
if (!inputFile.files || inputFile.files.length === 0) {
this.isLoading = false;
}
window.removeEventListener('focus', handleFocus);
}, 300);
};
window.addEventListener('focus', handleFocus);
inputFile.click(); inputFile.click();
} }
@ -515,6 +590,10 @@ export class DeesInputFileupload extends DeesInputBase<DeesInputFileupload> {
inputFile.addEventListener('change', async (event: Event) => { inputFile.addEventListener('change', async (event: Event) => {
const target = event.target as HTMLInputElement; const target = event.target as HTMLInputElement;
const newFiles = Array.from(target.files); const newFiles = Array.from(target.files);
// Always reset loading state when file dialog interaction completes
this.isLoading = false;
await this.addFiles(newFiles); await this.addFiles(newFiles);
// Reset the input value to allow selecting the same file again if needed // Reset the input value to allow selecting the same file again if needed
target.value = ''; target.value = '';

View File

@ -1,4 +1,4 @@
import { html, css } from '@design.estate/dees-element'; import { html, css, cssManager } from '@design.estate/dees-element';
export const demoFunc = () => html` export const demoFunc = () => html`
<dees-demowrapper> <dees-demowrapper>
@ -7,10 +7,31 @@ export const demoFunc = () => html`
.demo-container { .demo-container {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 24px; gap: 32px;
padding: 48px;
background: ${cssManager.bdTheme('#f8f9fa', '#0a0a0a')};
min-height: 100vh;
}
.section {
background: ${cssManager.bdTheme('#ffffff', '#18181b')};
border: 1px solid ${cssManager.bdTheme('#e5e7eb', '#27272a')};
border-radius: 8px;
padding: 24px; padding: 24px;
max-width: 1200px; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
margin: 0 auto; }
.section-title {
font-size: 18px;
font-weight: 600;
margin-bottom: 8px;
color: ${cssManager.bdTheme('#09090b', '#fafafa')};
}
.section-description {
font-size: 14px;
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
margin-bottom: 24px;
} }
.settings-grid { .settings-grid {
@ -28,7 +49,10 @@ export const demoFunc = () => html`
</style> </style>
<div class="demo-container"> <div class="demo-container">
<dees-panel .title=${'Multi-Option Toggle'} .subtitle=${'Select from multiple options with a sliding indicator'}> <div class="section">
<div class="section-title">Multi-Option Toggle</div>
<div class="section-description">Select from multiple options with a smooth sliding indicator animation.</div>
<dees-input-multitoggle <dees-input-multitoggle
.label=${'Display Mode'} .label=${'Display Mode'}
.description=${'Choose how content is displayed'} .description=${'Choose how content is displayed'}
@ -36,15 +60,20 @@ export const demoFunc = () => html`
.selectedOption=${'Grid View'} .selectedOption=${'Grid View'}
></dees-input-multitoggle> ></dees-input-multitoggle>
<br><br>
<dees-input-multitoggle <dees-input-multitoggle
.label=${'T-Shirt Size'} .label=${'T-Shirt Size'}
.description=${'Select your preferred size'} .description=${'Select your preferred size'}
.options=${['XS', 'S', 'M', 'L', 'XL', 'XXL']} .options=${['XS', 'S', 'M', 'L', 'XL', 'XXL']}
.selectedOption=${'M'} .selectedOption=${'M'}
></dees-input-multitoggle> ></dees-input-multitoggle>
</dees-panel> </div>
<dees-panel .title=${'Boolean Toggle'} .subtitle=${'Simple on/off switches with custom labels'}> <div class="section">
<div class="section-title">Boolean Toggle</div>
<div class="section-description">Simple on/off switches with customizable labels for clearer context.</div>
<dees-input-multitoggle <dees-input-multitoggle
.label=${'Notifications'} .label=${'Notifications'}
.description=${'Enable or disable push notifications'} .description=${'Enable or disable push notifications'}
@ -52,6 +81,8 @@ export const demoFunc = () => html`
.selectedOption=${'true'} .selectedOption=${'true'}
></dees-input-multitoggle> ></dees-input-multitoggle>
<br><br>
<dees-input-multitoggle <dees-input-multitoggle
.label=${'Theme Mode'} .label=${'Theme Mode'}
.description=${'Switch between light and dark theme'} .description=${'Switch between light and dark theme'}
@ -60,13 +91,15 @@ export const demoFunc = () => html`
.booleanFalseName=${'Light'} .booleanFalseName=${'Light'}
.selectedOption=${'Dark'} .selectedOption=${'Dark'}
></dees-input-multitoggle> ></dees-input-multitoggle>
</dees-panel> </div>
<dees-panel .title=${'Settings Panel'} .subtitle=${'Configuration options in a horizontal layout'}> <div class="section">
<div class="section-title">Settings Grid</div>
<div class="section-description">Configuration options arranged in a responsive grid layout.</div>
<div class="settings-grid"> <div class="settings-grid">
<dees-input-multitoggle <dees-input-multitoggle
.label=${'Auto-Save'} .label=${'Auto-Save'}
.layoutMode=${'horizontal'}
.type=${'boolean'} .type=${'boolean'}
.booleanTrueName=${'Enabled'} .booleanTrueName=${'Enabled'}
.booleanFalseName=${'Disabled'} .booleanFalseName=${'Disabled'}
@ -75,30 +108,30 @@ export const demoFunc = () => html`
<dees-input-multitoggle <dees-input-multitoggle
.label=${'Language'} .label=${'Language'}
.layoutMode=${'horizontal'}
.options=${['English', 'German', 'French', 'Spanish']} .options=${['English', 'German', 'French', 'Spanish']}
.selectedOption=${'English'} .selectedOption=${'English'}
></dees-input-multitoggle> ></dees-input-multitoggle>
<dees-input-multitoggle <dees-input-multitoggle
.label=${'Quality'} .label=${'Quality'}
.layoutMode=${'horizontal'}
.options=${['Low', 'Medium', 'High', 'Ultra']} .options=${['Low', 'Medium', 'High', 'Ultra']}
.selectedOption=${'High'} .selectedOption=${'High'}
></dees-input-multitoggle> ></dees-input-multitoggle>
<dees-input-multitoggle <dees-input-multitoggle
.label=${'Privacy'} .label=${'Privacy'}
.layoutMode=${'horizontal'}
.type=${'boolean'} .type=${'boolean'}
.booleanTrueName=${'Private'} .booleanTrueName=${'Private'}
.booleanFalseName=${'Public'} .booleanFalseName=${'Public'}
.selectedOption=${'Private'} .selectedOption=${'Private'}
></dees-input-multitoggle> ></dees-input-multitoggle>
</div> </div>
</dees-panel> </div>
<dees-panel .title=${'States & Form Integration'} .subtitle=${'Disabled states and form usage'}> <div class="section">
<div class="section-title">States & Form Integration</div>
<div class="section-description">Examples of disabled states and integration within forms.</div>
<dees-input-multitoggle <dees-input-multitoggle
.label=${'Account Type'} .label=${'Account Type'}
.description=${'This setting is locked'} .description=${'This setting is locked'}
@ -107,6 +140,8 @@ export const demoFunc = () => html`
.disabled=${true} .disabled=${true}
></dees-input-multitoggle> ></dees-input-multitoggle>
<br><br>
<dees-form> <dees-form>
<dees-input-text .label=${'Project Name'} .required=${true}></dees-input-text> <dees-input-text .label=${'Project Name'} .required=${true}></dees-input-text>
<dees-input-multitoggle <dees-input-multitoggle
@ -122,7 +157,7 @@ export const demoFunc = () => html`
.selectedOption=${'MIT'} .selectedOption=${'MIT'}
></dees-input-multitoggle> ></dees-input-multitoggle>
</dees-form> </dees-form>
</dees-panel> </div>
</div> </div>
</dees-demowrapper> </dees-demowrapper>
`; `;

View File

@ -10,7 +10,7 @@ import { DeesInputBase } from './dees-input-base.js';
import * as colors from './00colors.js' import * as colors from './00colors.js'
const { demoFunc } = await import('./dees-input-multitoggle.demo.js'); import { demoFunc } from './dees-input-multitoggle.demo.js';
declare global { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {
@ -57,9 +57,12 @@ export class DeesInputMultitoggle extends DeesInputBase<DeesInputMultitoggle> {
} else { } else {
this.selectedOption = val as string; this.selectedOption = val as string;
} }
this.requestUpdate();
// Defer indicator update to next frame if component not yet updated // Defer indicator update to next frame if component not yet updated
if (this.hasUpdated) { if (this.hasUpdated) {
this.setIndicator(); requestAnimationFrame(() => {
this.setIndicator();
});
} }
} }
@ -68,59 +71,71 @@ export class DeesInputMultitoggle extends DeesInputBase<DeesInputMultitoggle> {
cssManager.defaultStyles, cssManager.defaultStyles,
css` css`
:host { :host {
color: ${cssManager.bdTheme('#333', '#ccc')}; color: ${cssManager.bdTheme('#09090b', '#fafafa')};
user-select: none; user-select: none;
} }
.selections { .selections {
position: relative; position: relative;
display: flex; display: inline-flex;
flex-direction: row; align-items: center;
flex-wrap: nowrap; background: ${cssManager.bdTheme('#ffffff', '#18181b')};
background: ${cssManager.bdTheme('#fff', '#222')}; border: 1px solid ${cssManager.bdTheme('#e5e7eb', '#27272a')};
width: min-content; padding: 4px;
border-radius: 20px; border-radius: 8px;
height: 32px; box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
border-top: 1px solid ${cssManager.bdTheme('rgba(0,0,0,0.1)', 'rgba(255,255,255,0.1)')};
} }
.option { .option {
color: ${cssManager.bdTheme('#666', '#999')};
position: relative; position: relative;
padding: 0px 16px; padding: 8px 20px;
line-height: 32px; border-radius: 6px;
cursor: default; cursor: pointer;
width: min-content; /* Make the width as per the content */ white-space: nowrap;
white-space: nowrap; /* Prevent text wrapping */ transition: color 0.2s ease;
transition: all 0.1s;
font-size: 14px; font-size: 14px;
transform: translateY(-1px); font-weight: 500;
color: ${cssManager.bdTheme('#71717a', '#71717a')};
line-height: 1;
z-index: 2;
} }
.option:hover { .option:hover {
color: ${cssManager.bdTheme('#333', '#fff')}; color: ${cssManager.bdTheme('#18181b', '#e4e4e7')};
} }
.option.selected { .option.selected {
color: ${cssManager.bdTheme('#fff', '#fff')}; color: ${cssManager.bdTheme('#3b82f6', '#60a5fa')};
} }
.indicator { .indicator {
opacity: 0; opacity: 0;
position: absolute; position: absolute;
height: 24px; height: calc(100% - 8px);
left: 4px; top: 4px;
top: 3px; border-radius: 6px;
border-radius: 16px; background: ${cssManager.bdTheme('rgba(59, 130, 246, 0.15)', 'rgba(59, 130, 246, 0.15)')};
background: ${cssManager.bdTheme(colors.bright.blueActive, colors.dark.blueActive)}; transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
min-width: 24px; z-index: 1;
transition: all 0.1s ease-in-out;
} }
.indicator.no-transition { .indicator.no-transition {
transition: none; transition: none;
} }
:host([disabled]) .selections {
opacity: 0.5;
cursor: not-allowed;
}
:host([disabled]) .option {
cursor: not-allowed;
pointer-events: none;
}
:host([disabled]) .indicator {
background: ${cssManager.bdTheme('rgba(113, 113, 122, 0.15)', 'rgba(113, 113, 122, 0.15)')};
}
`, `,
]; ];
@ -148,6 +163,14 @@ export class DeesInputMultitoggle extends DeesInputBase<DeesInputMultitoggle> {
// Initialize boolean options early // Initialize boolean options early
if (this.type === 'boolean' && this.options.length === 0) { if (this.type === 'boolean' && this.options.length === 0) {
this.options = [this.booleanTrueName || 'true', this.booleanFalseName || 'false']; this.options = [this.booleanTrueName || 'true', this.booleanFalseName || 'false'];
// Set default selection for boolean if not set
if (!this.selectedOption) {
this.selectedOption = this.booleanFalseName || 'false';
}
}
// Set default selection to first option if not set
if (!this.selectedOption && this.options.length > 0) {
this.selectedOption = this.options[0];
} }
} }
@ -159,16 +182,28 @@ export class DeesInputMultitoggle extends DeesInputBase<DeesInputMultitoggle> {
} }
// Wait for the next frame to ensure DOM is fully rendered // Wait for the next frame to ensure DOM is fully rendered
await this.updateComplete; await this.updateComplete;
requestAnimationFrame(() => {
this.setIndicator(); // Wait for fonts to load
}); if (document.fonts) {
} await document.fonts.ready;
}
public async handleSelection(optionArg: string) {
this.selectedOption = optionArg; // Wait one more frame after fonts are loaded
await new Promise(resolve => requestAnimationFrame(resolve));
// Now set the indicator
this.setIndicator(); this.setIndicator();
} }
public async handleSelection(optionArg: string) {
if (this.disabled) return;
this.selectedOption = optionArg;
this.requestUpdate();
this.changeSubject.next(this);
await this.updateComplete;
this.setIndicator();
}
private indicatorInitialized = false; private indicatorInitialized = false;
public async setIndicator() { public async setIndicator() {
@ -199,8 +234,8 @@ export class DeesInputMultitoggle extends DeesInputBase<DeesInputMultitoggle> {
}, 50); }, 50);
} }
indicator.style.width = `${option.clientWidth - 8}px`; indicator.style.width = `${option.clientWidth}px`;
indicator.style.left = `${option.offsetLeft + 4}px`; indicator.style.left = `${option.offsetLeft}px`;
indicator.style.opacity = '1'; indicator.style.opacity = '1';
} }
} }
@ -218,8 +253,11 @@ export class DeesInputMultitoggle extends DeesInputBase<DeesInputMultitoggle> {
} else { } else {
this.selectedOption = value as string; this.selectedOption = value as string;
} }
this.requestUpdate();
if (this.hasUpdated) { if (this.hasUpdated) {
this.setIndicator(); requestAnimationFrame(() => {
this.setIndicator();
});
} }
} }
} }

View File

@ -1,4 +1,5 @@
import { html, css, cssManager } from '@design.estate/dees-element'; import { html, css, cssManager } from '@design.estate/dees-element';
import './dees-shopping-productcard.js';
export const demoFunc = () => html` export const demoFunc = () => html`
<dees-demowrapper> <dees-demowrapper>
@ -15,25 +16,44 @@ export const demoFunc = () => html`
.shopping-grid { .shopping-grid {
display: grid; display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 16px; gap: 20px;
} }
.product-card { .cart-summary {
padding: 16px; margin-top: 24px;
background: ${cssManager.bdTheme('#fff', '#2a2a2a')}; padding: 20px;
border-radius: 4px; background: ${cssManager.bdTheme('hsl(210 40% 96.1%)', 'hsl(215 20.2% 16.8%)')};
box-shadow: 0 2px 4px ${cssManager.bdTheme('rgba(0,0,0,0.1)', 'rgba(0,0,0,0.3)')}; border: 1px solid ${cssManager.bdTheme('hsl(214.3 31.8% 91.4%)', 'hsl(215 20.2% 21.8%)')};
border-radius: 8px;
} }
.product-name { .cart-summary-title {
font-size: 18px;
font-weight: 600; font-weight: 600;
margin-bottom: 8px;
}
.product-price {
color: #1976d2;
margin-bottom: 16px; margin-bottom: 16px;
color: ${cssManager.bdTheme('hsl(0 0% 9%)', 'hsl(0 0% 95%)')};
}
.cart-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 0;
font-size: 14px;
color: ${cssManager.bdTheme('hsl(215.3 25% 26.7%)', 'hsl(217.9 10.6% 74.9%)')};
}
.cart-total {
display: flex;
justify-content: space-between;
align-items: center;
padding-top: 16px;
margin-top: 16px;
border-top: 2px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
font-size: 18px;
font-weight: 600;
color: ${cssManager.bdTheme('hsl(0 0% 9%)', 'hsl(0 0% 95%)')};
} }
`} `}
</style> </style>
@ -53,36 +73,97 @@ export const demoFunc = () => html`
></dees-input-quantityselector> ></dees-input-quantityselector>
</dees-panel> </dees-panel>
<dees-panel .title=${'Shopping Cart'} .subtitle=${'Product cards with quantity selectors'}> <dees-panel .title=${'Shopping Cart'} .subtitle=${'Modern e-commerce product cards with interactive quantity selectors'} .runAfterRender=${async (elementArg: HTMLElement) => {
const updateCartSummary = () => {
const card1 = elementArg.querySelector('#headphones-qty') as any;
const card2 = elementArg.querySelector('#mouse-qty') as any;
const card3 = elementArg.querySelector('#keyboard-qty') as any;
const qty1 = card1?.quantity || 0;
const qty2 = card2?.quantity || 0;
const qty3 = card3?.quantity || 0;
const price1 = 349.99 * qty1;
const price2 = 99.99 * qty2;
const price3 = 79.99 * qty3;
const total = price1 + price2 + price3;
const summary = elementArg.querySelector('#cart-summary-content');
if (summary) {
summary.innerHTML = `
${qty1 > 0 ? `<div class="cart-item">
<span>Sony WH-1000XM5 (${qty1})</span>
<span>$${price1.toFixed(2)}</span>
</div>` : ''}
${qty2 > 0 ? `<div class="cart-item">
<span>Logitech MX Master 3S (${qty2})</span>
<span>$${price2.toFixed(2)}</span>
</div>` : ''}
${qty3 > 0 ? `<div class="cart-item">
<span>Keychron K2 (${qty3})</span>
<span>$${price3.toFixed(2)}</span>
</div>` : ''}
${total === 0 ? '<div class="cart-item" style="text-align: center; color: #999;">Your cart is empty</div>' : ''}
<div class="cart-total">
<span>Total</span>
<span>$${total.toFixed(2)}</span>
</div>
`;
}
};
// Initial update
setTimeout(updateCartSummary, 100);
// Set up listeners
elementArg.querySelectorAll('dees-shopping-productcard').forEach(card => {
card.addEventListener('quantityChange', updateCartSummary);
});
}}>
<div class="shopping-grid"> <div class="shopping-grid">
<div class="product-card"> <dees-shopping-productcard
<div class="product-name">Premium Headphones</div> id="headphones-qty"
<div class="product-price">$199.99</div> .productData=${{
<dees-input-quantityselector name: 'Sony WH-1000XM5 Wireless Headphones',
.label=${'Quantity'} category: 'Audio',
.layoutMode=${'horizontal'} description: 'Industry-leading noise canceling with Auto NC Optimizer',
.value=${1} price: 349.99,
></dees-input-quantityselector> originalPrice: 399.99,
</div> iconName: 'lucide:headphones'
}}
.quantity=${1}
></dees-shopping-productcard>
<div class="product-card"> <dees-shopping-productcard
<div class="product-name">Wireless Mouse</div> id="mouse-qty"
<div class="product-price">$49.99</div> .productData=${{
<dees-input-quantityselector name: 'Logitech MX Master 3S',
.label=${'Quantity'} category: 'Accessories',
.layoutMode=${'horizontal'} description: 'Performance wireless mouse with ultra-fast scrolling',
.value=${2} price: 99.99,
></dees-input-quantityselector> iconName: 'lucide:mouse-pointer'
</div> }}
.quantity=${2}
></dees-shopping-productcard>
<div class="product-card"> <dees-shopping-productcard
<div class="product-name">USB-C Cable</div> id="keyboard-qty"
<div class="product-price">$19.99</div> .productData=${{
<dees-input-quantityselector name: 'Keychron K2 Wireless Mechanical Keyboard',
.label=${'Quantity'} category: 'Keyboards',
.layoutMode=${'horizontal'} description: 'Compact 75% layout with hot-swappable switches',
.value=${1} price: 79.99,
></dees-input-quantityselector> originalPrice: 94.99,
iconName: 'lucide:keyboard'
}}
.quantity=${1}
></dees-shopping-productcard>
</div>
<div class="cart-summary">
<h3 class="cart-summary-title">Order Summary</h3>
<div id="cart-summary-content">
<!-- Content will be dynamically updated -->
</div> </div>
</div> </div>
</dees-panel> </dees-panel>

View File

@ -32,48 +32,91 @@ export class DeesInputQuantitySelector extends DeesInputBase<DeesInputQuantitySe
} }
.quantity-container { .quantity-container {
transition: all 0.1s; transition: all 0.15s ease;
font-size: 14px; font-size: 14px;
display: grid; display: inline-flex;
grid-template-columns: 33% 34% 33%; align-items: center;
text-align: center; background: transparent;
background: ${cssManager.bdTheme('#fafafa', '#222222')}; height: 40px;
line-height: 40px; padding: 0;
padding: 0px; min-width: 120px;
min-width: 110px; color: ${cssManager.bdTheme('hsl(0 0% 9%)', 'hsl(0 0% 95%)')};
color: ${cssManager.bdTheme('#666', '#CCC')}; border: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
border: 1px solid ${cssManager.bdTheme('#CCC', '#444')}; border-radius: 6px;
border-radius: 4px; overflow: hidden;
} }
.quantity-container.disabled { .quantity-container.disabled {
background: ${cssManager.bdTheme('hsl(0 0% 95.1%)', 'hsl(0 0% 14.9%)')};
border-color: ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
opacity: 0.5; opacity: 0.5;
pointer-events: none; pointer-events: none;
} }
.quantity-container:hover { .quantity-container:hover:not(.disabled) {
color: ${cssManager.bdTheme('#333', '#fff')}; border-color: ${cssManager.bdTheme('hsl(0 0% 79.8%)', 'hsl(0 0% 20.9%)')};
border-color: ${cssManager.bdTheme('#999', '#666')};
} }
.minus { .quantity-container:focus-within {
padding-left: 5px; border-color: ${cssManager.bdTheme('hsl(222.2 47.4% 51.2%)', 'hsl(217.2 91.2% 59.8%)')};
} box-shadow: 0 0 0 3px ${cssManager.bdTheme('hsl(222.2 47.4% 51.2% / 0.1)', 'hsl(217.2 91.2% 59.8% / 0.1)')};
.plus {
padding-right: 5px;
} }
.selector { .selector {
text-align: center; flex: 0 0 40px;
font-size: 20px; height: 100%;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
font-weight: 500;
cursor: pointer;
transition: all 0.15s ease;
color: ${cssManager.bdTheme('hsl(215.4 16.3% 56.9%)', 'hsl(215 20.2% 55.1%)')};
position: relative;
} }
.selector:hover { .selector:hover {
background: ${cssManager.bdTheme('hsl(0 0% 95.1%)', 'hsl(0 0% 14.9%)')};
color: ${cssManager.bdTheme('hsl(0 0% 9%)', 'hsl(0 0% 95%)')};
}
.selector:active {
background: ${cssManager.bdTheme('hsl(0 0% 91%)', 'hsl(0 0% 11%)')};
}
.selector.minus {
border-right: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
}
.selector.plus {
border-left: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
} }
.quantity { .quantity {
flex: 1;
text-align: center; text-align: center;
font-weight: 500;
font-variant-numeric: tabular-nums;
letter-spacing: -0.006em;
}
/* Keyboard navigation focus styles */
.selector:focus {
outline: none;
background: ${cssManager.bdTheme('hsl(210 40% 96.1%)', 'hsl(215 20.2% 16.8%)')};
z-index: 1;
}
/* Min value state */
.quantity-container[data-min="true"] .selector.minus {
opacity: 0.3;
cursor: not-allowed;
}
.quantity-container[data-min="true"] .selector.minus:hover {
background: transparent;
color: ${cssManager.bdTheme('hsl(215.4 16.3% 56.9%)', 'hsl(215 20.2% 55.1%)')};
} }
`, `,
@ -82,11 +125,38 @@ export class DeesInputQuantitySelector extends DeesInputBase<DeesInputQuantitySe
public render(): TemplateResult { public render(): TemplateResult {
return html` return html`
<div class="input-wrapper"> <div class="input-wrapper">
<dees-label .label=${this.label}></dees-label> ${this.label ? html`<dees-label .label=${this.label} .description=${this.description} .required=${this.required}></dees-label>` : ''}
<div class="quantity-container ${this.disabled ? 'disabled' : ''}"> <div
<div class="selector minus" @click="${() => {this.decrease();}}">-</div> class="quantity-container ${this.disabled ? 'disabled' : ''}"
<div class="quantity">${this.value}</div> data-min="${this.value <= 0}"
<div class="selector plus" @click="${() => {this.increase();}}">+</div> >
<div
class="selector minus"
@click="${() => {this.decrease();}}"
tabindex="${this.disabled ? '-1' : '0'}"
@keydown="${(e: KeyboardEvent) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
this.decrease();
}
}}"
role="button"
aria-label="Decrease quantity"
></div>
<div class="quantity" aria-live="polite" aria-atomic="true">${this.value}</div>
<div
class="selector plus"
@click="${() => {this.increase();}}"
tabindex="${this.disabled ? '-1' : '0'}"
@keydown="${(e: KeyboardEvent) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
this.increase();
}
}}"
role="button"
aria-label="Increase quantity"
>+</div>
</div> </div>
</div> </div>
`; `;

View File

@ -69,80 +69,97 @@ export class DeesInputRadiogroup extends DeesInputBase<string | object> {
:host { :host {
display: block; display: block;
position: relative; position: relative;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
} }
.maincontainer { .maincontainer {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 8px; gap: 10px;
} }
.maincontainer.horizontal { .maincontainer.horizontal {
flex-direction: row; flex-direction: row;
flex-wrap: wrap; flex-wrap: wrap;
gap: 16px; gap: 20px;
} }
.radio-option { .radio-option {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 12px; gap: 10px;
padding: 8px 0; padding: 6px 0;
cursor: pointer; cursor: pointer;
transition: all 0.2s ease; transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
user-select: none; user-select: none;
position: relative; position: relative;
border-radius: 4px;
} }
.maincontainer.horizontal .radio-option { .maincontainer.horizontal .radio-option {
padding: 8px 16px 8px 0; padding: 6px 20px 6px 0;
} }
.radio-option:hover .radio-circle { .radio-option:hover .radio-circle {
border-color: ${cssManager.bdTheme('#0050b9', '#0084ff')}; border-color: ${cssManager.bdTheme('hsl(215 20.2% 65.1%)', 'hsl(215 20.2% 35.1%)')};
background: ${cssManager.bdTheme('hsl(210 40% 96.1%)', 'hsl(215 20.2% 11.8%)')};
} }
.radio-option:hover .radio-label { .radio-option:hover .radio-label {
color: ${cssManager.bdTheme('#1a1a1a', '#ffffff')}; color: ${cssManager.bdTheme('hsl(215.3 25% 8.8%)', 'hsl(210 40% 98%)')};
} }
.radio-circle { .radio-circle {
width: 20px; width: 20px;
height: 20px; height: 20px;
border-radius: 50%; border-radius: 50%;
border: 2px solid ${cssManager.bdTheme('#999', '#666')}; border: 2px solid ${cssManager.bdTheme('hsl(215 20.2% 65.1%)', 'hsl(215 20.2% 35.1%)')};
background: ${cssManager.bdTheme('#fff', '#1a1a1a')}; background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(215 30% 6.8%)')};
transition: all 0.2s ease; transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
position: relative; position: relative;
flex-shrink: 0; flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
} }
.radio-option.selected .radio-circle { .radio-option.selected .radio-circle {
border-color: ${cssManager.bdTheme('#0050b9', '#0084ff')}; border-color: ${cssManager.bdTheme('hsl(217.2 91.2% 59.8%)', 'hsl(213.1 93.9% 67.8%)')};
background: ${cssManager.bdTheme('#0050b9', '#0084ff')}; background: ${cssManager.bdTheme('hsl(217.2 91.2% 59.8%)', 'hsl(213.1 93.9% 67.8%)')};
} }
.radio-option.selected .radio-circle::after { .radio-option.selected .radio-circle::after {
content: ''; content: '';
position: absolute; position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 8px; width: 8px;
height: 8px; height: 8px;
border-radius: 50%; border-radius: 50%;
background: white; background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(215 30% 6.8%)')};
transform: scale(0);
transition: transform 0.2s cubic-bezier(0.4, 0, 0.2, 1);
}
.radio-option.selected .radio-circle::after {
transform: scale(1);
}
.radio-circle:focus-visible {
outline: none;
box-shadow: 0 0 0 2px ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(215 30% 3.9%)')},
0 0 0 4px ${cssManager.bdTheme('hsl(217.2 91.2% 59.8%)', 'hsl(213.1 93.9% 67.8%)')};
} }
.radio-label { .radio-label {
font-size: 14px; font-size: 14px;
color: ${cssManager.bdTheme('#666', '#999')}; font-weight: 500;
transition: color 0.2s ease; color: ${cssManager.bdTheme('hsl(215.3 25% 26.7%)', 'hsl(217.9 10.6% 74.9%)')};
transition: color 0.2s cubic-bezier(0.4, 0, 0.2, 1);
letter-spacing: -0.006em;
line-height: 20px;
} }
.radio-option.selected .radio-label { .radio-option.selected .radio-label {
color: ${cssManager.bdTheme('#1a1a1a', '#ffffff')}; color: ${cssManager.bdTheme('hsl(215.3 25% 8.8%)', 'hsl(210 40% 98%)')};
font-weight: 500;
} }
:host([disabled]) .radio-option { :host([disabled]) .radio-option {
@ -151,40 +168,49 @@ export class DeesInputRadiogroup extends DeesInputBase<string | object> {
} }
:host([disabled]) .radio-option:hover .radio-circle { :host([disabled]) .radio-option:hover .radio-circle {
border-color: ${cssManager.bdTheme('#999', '#666')}; border-color: ${cssManager.bdTheme('hsl(215 20.2% 65.1%)', 'hsl(215 20.2% 35.1%)')};
background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(215 30% 6.8%)')};
} }
:host([disabled]) .radio-option:hover .radio-label { :host([disabled]) .radio-option:hover .radio-label {
color: ${cssManager.bdTheme('#666', '#999')}; color: ${cssManager.bdTheme('hsl(215.3 25% 26.7%)', 'hsl(217.9 10.6% 74.9%)')};
} }
.label-text { .label-text {
font-size: 14px; font-size: 14px;
font-weight: 500; font-weight: 500;
color: ${cssManager.bdTheme('#333', '#ccc')}; color: ${cssManager.bdTheme('hsl(215.3 25% 8.8%)', 'hsl(210 40% 98%)')};
margin-bottom: 8px; margin-bottom: 10px;
letter-spacing: -0.006em;
line-height: 20px;
} }
.description-text { .description-text {
font-size: 12px; font-size: 13px;
color: ${cssManager.bdTheme('#666', '#999')}; color: ${cssManager.bdTheme('hsl(215.4 16.3% 56.9%)', 'hsl(215 20.2% 55.1%)')};
margin-top: 8px; margin-top: 10px;
line-height: 1.4; line-height: 1.5;
letter-spacing: -0.003em;
} }
/* Validation styles */ /* Validation styles */
:host([validationState="invalid"]) .radio-circle { :host([validationState="invalid"]) .radio-circle {
border-color: #e74c3c; border-color: ${cssManager.bdTheme('hsl(0 72.2% 50.6%)', 'hsl(0 62.8% 30.6%)')};
}
:host([validationState="invalid"]) .radio-option.selected .radio-circle {
border-color: ${cssManager.bdTheme('hsl(0 72.2% 50.6%)', 'hsl(0 62.8% 30.6%)')};
background: ${cssManager.bdTheme('hsl(0 72.2% 50.6%)', 'hsl(0 62.8% 30.6%)')};
} }
:host([validationState="valid"]) .radio-option.selected .radio-circle { :host([validationState="valid"]) .radio-option.selected .radio-circle {
border-color: #27ae60; border-color: ${cssManager.bdTheme('hsl(142.1 70.6% 45.3%)', 'hsl(142.1 76.2% 36.3%)')};
background: #27ae60; background: ${cssManager.bdTheme('hsl(142.1 70.6% 45.3%)', 'hsl(142.1 76.2% 36.3%)')};
} }
:host([validationState="warn"]) .radio-option.selected .radio-circle { :host([validationState="warn"]) .radio-option.selected .radio-circle {
border-color: #f39c12; border-color: ${cssManager.bdTheme('hsl(45.4 93.4% 47.5%)', 'hsl(45.4 93.4% 47.5%)')};
background: #f39c12; background: ${cssManager.bdTheme('hsl(45.4 93.4% 47.5%)', 'hsl(45.4 93.4% 47.5%)')};
} }
/* Override base grid layout for radiogroup to prevent large gaps */ /* Override base grid layout for radiogroup to prevent large gaps */
@ -212,8 +238,15 @@ export class DeesInputRadiogroup extends DeesInputBase<string | object> {
<div <div
class="radio-option ${isSelected ? 'selected' : ''}" class="radio-option ${isSelected ? 'selected' : ''}"
@click="${() => this.selectOption(optionKey)}" @click="${() => this.selectOption(optionKey)}"
@keydown="${(e: KeyboardEvent) => this.handleKeydown(e, optionKey)}"
> >
<div class="radio-circle"></div> <div
class="radio-circle"
tabindex="${this.disabled ? '-1' : '0'}"
role="radio"
aria-checked="${isSelected}"
aria-label="${optionLabel}"
></div>
<div class="radio-label">${optionLabel}</div> <div class="radio-label">${optionLabel}</div>
</div> </div>
`; `;
@ -292,4 +325,33 @@ export class DeesInputRadiogroup extends DeesInputBase<string | object> {
this.selectedOption = this.getOptionKey(firstOption); this.selectedOption = this.getOptionKey(firstOption);
} }
} }
private handleKeydown(event: KeyboardEvent, optionKey: string) {
if (this.disabled) return;
if (event.key === ' ' || event.key === 'Enter') {
event.preventDefault();
this.selectOption(optionKey);
} else if (event.key === 'ArrowDown' || event.key === 'ArrowRight') {
event.preventDefault();
this.focusNextOption();
} else if (event.key === 'ArrowUp' || event.key === 'ArrowLeft') {
event.preventDefault();
this.focusPreviousOption();
}
}
private focusNextOption() {
const radioCircles = Array.from(this.shadowRoot.querySelectorAll('.radio-circle'));
const currentIndex = radioCircles.findIndex(el => el === this.shadowRoot.activeElement);
const nextIndex = (currentIndex + 1) % radioCircles.length;
(radioCircles[nextIndex] as HTMLElement).focus();
}
private focusPreviousOption() {
const radioCircles = Array.from(this.shadowRoot.querySelectorAll('.radio-circle'));
const currentIndex = radioCircles.findIndex(el => el === this.shadowRoot.activeElement);
const prevIndex = currentIndex <= 0 ? radioCircles.length - 1 : currentIndex - 1;
(radioCircles[prevIndex] as HTMLElement).focus();
}
} }

View File

@ -96,27 +96,27 @@ export class DeesInputRichtext extends DeesInputBase<string> {
margin-bottom: 8px; margin-bottom: 8px;
font-size: 14px; font-size: 14px;
font-weight: 500; font-weight: 500;
color: ${cssManager.bdTheme('#374151', '#e4e4e7')}; color: ${cssManager.bdTheme('hsl(0 0% 15%)', 'hsl(0 0% 93.9%)')};
} }
.editor-container { .editor-container {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
min-height: ${cssManager.bdTheme('200px', '200px')}; min-height: ${cssManager.bdTheme('200px', '200px')};
border: 1px solid ${cssManager.bdTheme('#e1e5e9', '#2c2c2c')}; border: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
border-radius: 8px; border-radius: 6px;
background: ${cssManager.bdTheme('#ffffff', '#141414')}; background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(0 0% 9%)')};
overflow: hidden; overflow: hidden;
transition: all 0.2s ease; transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
} }
.editor-container:hover { .editor-container:hover {
border-color: ${cssManager.bdTheme('#d1d5db', '#404040')}; border-color: ${cssManager.bdTheme('hsl(0 0% 79.8%)', 'hsl(0 0% 20.9%)')};
} }
.editor-container.focused { .editor-container.focused {
border-color: ${cssManager.bdTheme('#0050b9', '#0069f2')}; border-color: ${cssManager.bdTheme('hsl(0 0% 9%)', 'hsl(0 0% 98%)')};
box-shadow: 0 0 0 3px ${cssManager.bdTheme('rgba(0, 80, 185, 0.1)', 'rgba(0, 105, 242, 0.1)')}; box-shadow: 0 0 0 2px ${cssManager.bdTheme('hsl(0 0% 9% / 0.05)', 'hsl(0 0% 98% / 0.05)')};
} }
.editor-toolbar { .editor-toolbar {
@ -124,8 +124,8 @@ export class DeesInputRichtext extends DeesInputBase<string> {
flex-wrap: wrap; flex-wrap: wrap;
gap: 4px; gap: 4px;
padding: 8px 12px; padding: 8px 12px;
background: ${cssManager.bdTheme('#f8f9fa', '#1a1a1a')}; background: ${cssManager.bdTheme('hsl(210 40% 96.1%)', 'hsl(0 0% 14.9%)')};
border-bottom: 1px solid ${cssManager.bdTheme('#e1e5e9', '#2c2c2c')}; border-bottom: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
align-items: center; align-items: center;
position: relative; position: relative;
} }
@ -142,8 +142,8 @@ export class DeesInputRichtext extends DeesInputBase<string> {
cursor: pointer; cursor: pointer;
font-size: 14px; font-size: 14px;
font-weight: 500; font-weight: 500;
color: ${cssManager.bdTheme('#374151', '#9ca3af')}; color: ${cssManager.bdTheme('hsl(215.4 16.3% 46.9%)', 'hsl(215 20.2% 65.1%)')};
transition: all 0.2s; transition: all 0.15s ease;
user-select: none; user-select: none;
} }
@ -153,13 +153,13 @@ export class DeesInputRichtext extends DeesInputBase<string> {
} }
.toolbar-button:hover { .toolbar-button:hover {
background: ${cssManager.bdTheme('#e5e7eb', '#2c2c2c')}; background: ${cssManager.bdTheme('hsl(0 0% 95.1%)', 'hsl(0 0% 14.9%)')};
color: ${cssManager.bdTheme('#1f2937', '#e4e4e7')}; color: ${cssManager.bdTheme('hsl(0 0% 9%)', 'hsl(0 0% 95%)')};
} }
.toolbar-button.active { .toolbar-button.active {
background: ${cssManager.bdTheme('#0050b9', '#0069f2')}; background: ${cssManager.bdTheme('hsl(0 0% 9%)', 'hsl(0 0% 98%)')};
color: white; color: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(0 0% 3.9%)')};
} }
.toolbar-button:disabled { .toolbar-button:disabled {
@ -170,7 +170,7 @@ export class DeesInputRichtext extends DeesInputBase<string> {
.toolbar-divider { .toolbar-divider {
width: 1px; width: 1px;
height: 24px; height: 24px;
background: ${cssManager.bdTheme('#d1d5db', '#404040')}; background: ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
margin: 0 4px; margin: 0 4px;
} }
@ -184,7 +184,7 @@ export class DeesInputRichtext extends DeesInputBase<string> {
.editor-content .ProseMirror { .editor-content .ProseMirror {
outline: none; outline: none;
line-height: 1.6; line-height: 1.6;
color: ${cssManager.bdTheme('#374151', '#e4e4e7')}; color: ${cssManager.bdTheme('hsl(0 0% 3.9%)', 'hsl(0 0% 98%)')};
min-height: 100%; min-height: 100%;
} }
@ -232,25 +232,25 @@ export class DeesInputRichtext extends DeesInputBase<string> {
} }
.editor-content .ProseMirror blockquote { .editor-content .ProseMirror blockquote {
border-left: 4px solid ${cssManager.bdTheme('#d1d5db', '#404040')}; border-left: 4px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
margin: 1em 0; margin: 1em 0;
padding-left: 1em; padding-left: 1em;
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')}; color: ${cssManager.bdTheme('hsl(215.4 16.3% 46.9%)', 'hsl(215 20.2% 65.1%)')};
font-style: italic; font-style: italic;
} }
.editor-content .ProseMirror code { .editor-content .ProseMirror code {
background: ${cssManager.bdTheme('#f3f4f6', '#2c2c2c')}; background: ${cssManager.bdTheme('hsl(0 0% 95.1%)', 'hsl(0 0% 14.9%)')};
border-radius: 4px; border-radius: 3px;
padding: 0.2em 0.4em; padding: 0.2em 0.4em;
font-family: 'Fira Code', 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace; font-family: 'Intel One Mono', 'Fira Code', 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
font-size: 0.9em; font-size: 0.9em;
color: ${cssManager.bdTheme('#e11d48', '#f87171')}; color: ${cssManager.bdTheme('hsl(0 0% 15%)', 'hsl(0 0% 93.9%)')};
} }
.editor-content .ProseMirror pre { .editor-content .ProseMirror pre {
background: ${cssManager.bdTheme('#1f2937', '#0a0a0a')}; background: ${cssManager.bdTheme('hsl(0 0% 3.9%)', 'hsl(0 0% 98%)')};
color: ${cssManager.bdTheme('#f9fafb', '#e4e4e7')}; color: ${cssManager.bdTheme('hsl(0 0% 98%)', 'hsl(0 0% 3.9%)')};
border-radius: 6px; border-radius: 6px;
padding: 1em; padding: 1em;
margin: 1em 0; margin: 1em 0;
@ -265,21 +265,21 @@ export class DeesInputRichtext extends DeesInputBase<string> {
} }
.editor-content .ProseMirror a { .editor-content .ProseMirror a {
color: ${cssManager.bdTheme('#0050b9', '#0069f2')}; color: ${cssManager.bdTheme('hsl(222.2 47.4% 51.2%)', 'hsl(217.2 91.2% 59.8%)')};
text-decoration: underline; text-decoration: underline;
cursor: pointer; cursor: pointer;
} }
.editor-content .ProseMirror a:hover { .editor-content .ProseMirror a:hover {
color: ${cssManager.bdTheme('#0069f2', '#0084ff')}; color: ${cssManager.bdTheme('hsl(222.2 47.4% 41.2%)', 'hsl(217.2 91.2% 69.8%)')};
} }
.editor-footer { .editor-footer {
padding: 8px 12px; padding: 8px 12px;
background: ${cssManager.bdTheme('#f8f9fa', '#1a1a1a')}; background: ${cssManager.bdTheme('hsl(210 40% 96.1%)', 'hsl(0 0% 14.9%)')};
border-top: 1px solid ${cssManager.bdTheme('#e1e5e9', '#2c2c2c')}; border-top: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
font-size: 12px; font-size: 12px;
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')}; color: ${cssManager.bdTheme('hsl(215.4 16.3% 46.9%)', 'hsl(215 20.2% 65.1%)')};
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
@ -295,8 +295,8 @@ export class DeesInputRichtext extends DeesInputBase<string> {
top: 100%; top: 100%;
left: 0; left: 0;
right: 0; right: 0;
background: ${cssManager.bdTheme('#ffffff', '#1a1a1a')}; background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(0 0% 9%)')};
border: 1px solid ${cssManager.bdTheme('#d1d5db', '#404040')}; border: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
border-radius: 6px; border-radius: 6px;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1); box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
padding: 12px; padding: 12px;
@ -310,17 +310,18 @@ export class DeesInputRichtext extends DeesInputBase<string> {
.link-input input { .link-input input {
width: 100%; width: 100%;
padding: 8px 12px; padding: 8px 12px;
border: 1px solid ${cssManager.bdTheme('#d1d5db', '#404040')}; border: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
border-radius: 4px; border-radius: 6px;
outline: none; outline: none;
font-size: 14px; font-size: 14px;
background: ${cssManager.bdTheme('#ffffff', '#0a0a0a')}; background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(0 0% 9%)')};
color: ${cssManager.bdTheme('#374151', '#e4e4e7')}; color: ${cssManager.bdTheme('hsl(0 0% 3.9%)', 'hsl(0 0% 98%)')};
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
} }
.link-input input:focus { .link-input input:focus {
border-color: ${cssManager.bdTheme('#0050b9', '#0069f2')}; border-color: ${cssManager.bdTheme('hsl(0 0% 9%)', 'hsl(0 0% 98%)')};
box-shadow: 0 0 0 2px ${cssManager.bdTheme('rgba(0, 80, 185, 0.1)', 'rgba(0, 105, 242, 0.1)')}; box-shadow: 0 0 0 2px ${cssManager.bdTheme('hsl(0 0% 9% / 0.05)', 'hsl(0 0% 98% / 0.05)')};
} }
.link-input-buttons { .link-input-buttons {
@ -331,33 +332,36 @@ export class DeesInputRichtext extends DeesInputBase<string> {
.link-input-buttons button { .link-input-buttons button {
padding: 6px 12px; padding: 6px 12px;
border: 1px solid ${cssManager.bdTheme('#d1d5db', '#404040')}; border: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
border-radius: 4px; border-radius: 4px;
background: ${cssManager.bdTheme('#ffffff', '#1a1a1a')}; background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(0 0% 9%)')};
cursor: pointer; cursor: pointer;
font-size: 12px; font-size: 12px;
color: ${cssManager.bdTheme('#374151', '#e4e4e7')}; color: ${cssManager.bdTheme('hsl(0 0% 45.1%)', 'hsl(0 0% 63.9%)')};
transition: all 0.2s; transition: all 0.15s ease;
font-weight: 500;
} }
.link-input-buttons button:hover { .link-input-buttons button:hover {
background: ${cssManager.bdTheme('#f3f4f6', '#2c2c2c')}; background: ${cssManager.bdTheme('hsl(0 0% 95.1%)', 'hsl(0 0% 14.9%)')};
color: ${cssManager.bdTheme('hsl(0 0% 9%)', 'hsl(0 0% 95%)')};
} }
.link-input-buttons button.primary { .link-input-buttons button.primary {
background: ${cssManager.bdTheme('#0050b9', '#0069f2')}; background: ${cssManager.bdTheme('hsl(0 0% 9%)', 'hsl(0 0% 98%)')};
color: white; color: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(0 0% 3.9%)')};
border-color: ${cssManager.bdTheme('#0050b9', '#0069f2')}; border-color: ${cssManager.bdTheme('hsl(0 0% 9%)', 'hsl(0 0% 98%)')};
} }
.link-input-buttons button.primary:hover { .link-input-buttons button.primary:hover {
background: ${cssManager.bdTheme('#0069f2', '#0084ff')}; background: ${cssManager.bdTheme('hsl(0 0% 15%)', 'hsl(0 0% 93.9%)')};
border-color: ${cssManager.bdTheme('hsl(0 0% 15%)', 'hsl(0 0% 93.9%)')};
} }
.description { .description {
margin-top: 8px; margin-top: 8px;
font-size: 12px; font-size: 12px;
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')}; color: ${cssManager.bdTheme('hsl(215.4 16.3% 46.9%)', 'hsl(215 20.2% 65.1%)')};
line-height: 1.4; line-height: 1.4;
} }

View File

@ -0,0 +1,248 @@
import { html, css } from '@design.estate/dees-element';
import '@design.estate/dees-wcctools/demotools';
import './dees-panel.js';
export const demoFunc = () => html`
<dees-demowrapper>
<style>
${css`
.demo-container {
display: flex;
flex-direction: column;
gap: 24px;
padding: 24px;
max-width: 1200px;
margin: 0 auto;
}
dees-panel {
margin-bottom: 24px;
}
dees-panel:last-child {
margin-bottom: 0;
}
.grid-layout {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
}
@media (max-width: 768px) {
.grid-layout {
grid-template-columns: 1fr;
}
}
.output-preview {
margin-top: 16px;
padding: 16px;
background: #f3f4f6;
border-radius: 4px;
font-size: 12px;
color: #374151;
word-break: break-all;
max-height: 200px;
overflow-y: auto;
}
@media (prefers-color-scheme: dark) {
.output-preview {
background: #2c2c2c;
color: #e4e4e7;
}
}
.tag-preview {
display: flex;
flex-wrap: wrap;
gap: 8px;
padding: 12px;
background: #f9fafb;
border-radius: 4px;
min-height: 40px;
align-items: center;
}
@media (prefers-color-scheme: dark) {
.tag-preview {
background: #1f2937;
}
}
.tag-preview-item {
display: inline-block;
padding: 4px 12px;
background: #e0e7ff;
color: #4338ca;
border-radius: 12px;
font-size: 14px;
}
@media (prefers-color-scheme: dark) {
.tag-preview-item {
background: #312e81;
color: #c7d2fe;
}
}
`}
</style>
<div class="demo-container">
<dees-panel .title=${'1. Basic Tags Input'} .subtitle=${'Simple tag input with common programming languages'}>
<dees-input-tags
.label=${'Programming Languages'}
.placeholder=${'Add a language...'}
.value=${['JavaScript', 'TypeScript', 'Python', 'Go']}
.description=${'Press Enter or comma to add tags'}
></dees-input-tags>
</dees-panel>
<dees-panel .title=${'2. Tags with Suggestions'} .subtitle=${'Auto-complete suggestions for faster input'}>
<dees-input-tags
.label=${'Tech Stack'}
.placeholder=${'Type to see suggestions...'}
.suggestions=${[
'React', 'Vue', 'Angular', 'Svelte', 'Lit', 'Next.js', 'Nuxt', 'SvelteKit',
'Node.js', 'Deno', 'Bun', 'Express', 'Fastify', 'Nest.js', 'Koa',
'MongoDB', 'PostgreSQL', 'Redis', 'MySQL', 'SQLite', 'Cassandra',
'Docker', 'Kubernetes', 'AWS', 'Azure', 'GCP', 'Vercel', 'Netlify'
]}
.value=${['React', 'Node.js', 'PostgreSQL', 'Docker']}
.description=${'Start typing to see suggestions from popular technologies'}
></dees-input-tags>
</dees-panel>
<dees-panel .title=${'3. Limited Tags'} .subtitle=${'Restrict the number of tags users can add'}>
<div class="grid-layout">
<dees-input-tags
.label=${'Top 3 Skills'}
.placeholder=${'Add up to 3 skills...'}
.maxTags=${3}
.value=${['Design', 'Development']}
.description=${'Maximum 3 tags allowed'}
></dees-input-tags>
<dees-input-tags
.label=${'Categories (Max 5)'}
.placeholder=${'Select categories...'}
.maxTags=${5}
.suggestions=${['Blog', 'Tutorial', 'News', 'Review', 'Guide', 'Case Study', 'Interview']}
.value=${['Tutorial', 'Guide']}
.description=${'Choose up to 5 categories'}
></dees-input-tags>
</div>
</dees-panel>
<dees-panel .title=${'4. Required & Validation'} .subtitle=${'Tags input with validation requirements'}>
<dees-input-tags
.label=${'Project Tags'}
.placeholder=${'Add at least one tag...'}
.required=${true}
.description=${'This field is required - add at least one tag'}
></dees-input-tags>
</dees-panel>
<dees-panel .title=${'5. Disabled State'} .subtitle=${'Read-only tags display'}>
<dees-input-tags
.label=${'System Tags'}
.value=${['System', 'Protected', 'Read-Only', 'Archive']}
.disabled=${true}
.description=${'These tags cannot be modified'}
></dees-input-tags>
</dees-panel>
<dees-panel .title=${'6. Form Integration'} .subtitle=${'Tags input working within a form context'}>
<dees-form>
<dees-input-text
.label=${'Project Name'}
.placeholder=${'My Awesome Project'}
.required=${true}
.key=${'name'}
></dees-input-text>
<div class="grid-layout">
<dees-input-tags
.label=${'Technologies Used'}
.placeholder=${'Add technologies...'}
.required=${true}
.key=${'technologies'}
.suggestions=${[
'TypeScript', 'JavaScript', 'Python', 'Go', 'Rust',
'React', 'Vue', 'Angular', 'Svelte',
'Node.js', 'Deno', 'Express', 'FastAPI'
]}
></dees-input-tags>
<dees-input-tags
.label=${'Project Tags'}
.placeholder=${'Add descriptive tags...'}
.key=${'tags'}
.maxTags=${10}
.suggestions=${[
'frontend', 'backend', 'fullstack', 'mobile', 'desktop',
'web', 'api', 'database', 'devops', 'ui/ux',
'opensource', 'saas', 'enterprise', 'startup'
]}
></dees-input-tags>
</div>
<dees-input-text
.label=${'Description'}
.inputType=${'textarea'}
.placeholder=${'Describe your project...'}
.key=${'description'}
></dees-input-text>
<dees-form-submit .text=${'Create Project'}></dees-form-submit>
</dees-form>
</dees-panel>
<dees-panel .title=${'7. Interactive Demo'} .subtitle=${'Add tags and see them collected in real-time'}>
<dees-input-tags
id="interactive-tags"
.label=${'Your Interests'}
.placeholder=${'Type your interests...'}
.suggestions=${[
'Music', 'Movies', 'Books', 'Travel', 'Photography',
'Cooking', 'Gaming', 'Sports', 'Art', 'Technology',
'Fashion', 'Fitness', 'Nature', 'Science', 'History'
]}
@change=${(e: CustomEvent) => {
const preview = document.querySelector('#tags-preview');
const tags = e.detail.value;
if (preview) {
if (tags.length === 0) {
preview.innerHTML = '<em style="color: #999;">No tags added yet...</em>';
} else {
preview.innerHTML = tags.map((tag: string) =>
`<span class="tag-preview-item">${tag}</span>`
).join('');
}
}
}}
></dees-input-tags>
<div class="tag-preview" id="tags-preview">
<em style="color: #999;">No tags added yet...</em>
</div>
<div class="output-preview" id="tags-json">
<em>JSON output will appear here...</em>
</div>
<script>
// Update JSON preview
const tagsInput = document.querySelector('#interactive-tags');
tagsInput?.addEventListener('change', (e) => {
const jsonPreview = document.querySelector('#tags-json');
if (jsonPreview) {
jsonPreview.textContent = JSON.stringify(e.detail.value, null, 2);
}
});
</script>
</dees-panel>
</div>
</dees-demowrapper>
`;

View File

@ -0,0 +1,431 @@
import {
customElement,
html,
css,
cssManager,
property,
state,
type TemplateResult,
} from '@design.estate/dees-element';
import { DeesInputBase } from './dees-input-base.js';
import './dees-icon.js';
import { demoFunc } from './dees-input-tags.demo.js';
declare global {
interface HTMLElementTagNameMap {
'dees-input-tags': DeesInputTags;
}
}
@customElement('dees-input-tags')
export class DeesInputTags extends DeesInputBase<DeesInputTags> {
// STATIC
public static demo = demoFunc;
// INSTANCE
@property({ type: Array })
public value: string[] = [];
@property({ type: String })
public placeholder: string = 'Add tags...';
@property({ type: Number })
public maxTags: number = 0; // 0 means unlimited
@property({ type: Array })
public suggestions: string[] = [];
@state()
private inputValue: string = '';
@state()
private showSuggestions: boolean = false;
@state()
private highlightedSuggestionIndex: number = -1;
@property({ type: String })
public validationText: string = '';
public static styles = [
...DeesInputBase.baseStyles,
cssManager.defaultStyles,
css`
:host {
display: block;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
}
.input-wrapper {
width: 100%;
}
.tags-container {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 6px;
padding: 6px 10px;
min-height: 40px;
background: transparent;
border: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
border-radius: 6px;
transition: all 0.15s ease;
cursor: text;
}
.tags-container:hover:not(.disabled) {
border-color: ${cssManager.bdTheme('hsl(0 0% 79.8%)', 'hsl(0 0% 20.9%)')};
}
.tags-container:focus-within {
border-color: ${cssManager.bdTheme('hsl(222.2 47.4% 51.2%)', 'hsl(217.2 91.2% 59.8%)')};
box-shadow: 0 0 0 3px ${cssManager.bdTheme('hsl(222.2 47.4% 51.2% / 0.1)', 'hsl(217.2 91.2% 59.8% / 0.1)')};
}
.tags-container.disabled {
background: ${cssManager.bdTheme('hsl(0 0% 95.1%)', 'hsl(0 0% 14.9%)')};
border-color: ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
cursor: not-allowed;
opacity: 0.5;
}
.tag {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 2px 8px;
background: ${cssManager.bdTheme('hsl(215 20.2% 65.1% / 0.2)', 'hsl(215 20.2% 35.1% / 0.2)')};
color: ${cssManager.bdTheme('hsl(215.3 25% 26.7%)', 'hsl(217.9 10.6% 74.9%)')};
border: 1px solid ${cssManager.bdTheme('hsl(215 20.2% 65.1% / 0.3)', 'hsl(215 20.2% 35.1% / 0.3)')};
border-radius: 4px;
font-size: 13px;
font-weight: 500;
line-height: 18px;
user-select: none;
animation: tagAppear 0.15s cubic-bezier(0.4, 0, 0.2, 1);
}
@keyframes tagAppear {
from {
transform: scale(0.95);
opacity: 0;
}
to {
transform: scale(1);
opacity: 1;
}
}
.tag-remove {
display: flex;
align-items: center;
justify-content: center;
width: 14px;
height: 14px;
margin-left: 2px;
border-radius: 3px;
cursor: pointer;
transition: all 0.15s ease;
color: ${cssManager.bdTheme('hsl(215.3 25% 46.7%)', 'hsl(217.9 10.6% 54.9%)')};
}
.tag-remove:hover {
background: ${cssManager.bdTheme('hsl(0 0% 0% / 0.08)', 'hsl(0 0% 100% / 0.08)')};
color: ${cssManager.bdTheme('hsl(215.3 25% 26.7%)', 'hsl(217.9 10.6% 74.9%)')};
}
.tag-remove dees-icon {
width: 10px;
height: 10px;
}
.tag-input {
flex: 1;
min-width: 120px;
border: none;
background: transparent;
outline: none;
font-size: 14px;
font-family: inherit;
color: ${cssManager.bdTheme('hsl(0 0% 9%)', 'hsl(0 0% 95%)')};
padding: 2px 4px;
line-height: 20px;
}
.tag-input::placeholder {
color: ${cssManager.bdTheme('hsl(0 0% 63.9%)', 'hsl(0 0% 45.1%)')};
}
.tag-input:disabled {
cursor: not-allowed;
}
/* Suggestions dropdown */
.suggestions-container {
position: relative;
}
.suggestions-dropdown {
position: absolute;
top: 100%;
left: 0;
right: 0;
margin-top: 4px;
background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(0 0% 3.9%)')};
border: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
border-radius: 6px;
box-shadow: 0 4px 6px -1px hsl(0 0% 0% / 0.1), 0 2px 4px -2px hsl(0 0% 0% / 0.1);
max-height: 200px;
overflow-y: auto;
z-index: 1000;
}
.suggestion {
padding: 6px 10px;
cursor: pointer;
transition: all 0.15s ease;
font-size: 14px;
color: ${cssManager.bdTheme('hsl(0 0% 15%)', 'hsl(0 0% 90%)')};
}
.suggestion:hover {
background: ${cssManager.bdTheme('hsl(0 0% 95.1%)', 'hsl(0 0% 14.9%)')};
}
.suggestion.highlighted {
background: ${cssManager.bdTheme('hsl(210 40% 96.1%)', 'hsl(215 20.2% 16.8%)')};
color: ${cssManager.bdTheme('hsl(0 0% 9%)', 'hsl(0 0% 95%)')};
}
/* Validation styles */
.validation-message {
color: ${cssManager.bdTheme('hsl(0 72.2% 50.6%)', 'hsl(0 62.8% 30.6%)')};
font-size: 13px;
margin-top: 6px;
line-height: 1.5;
}
/* Description styles */
.description {
color: ${cssManager.bdTheme('hsl(215.4 16.3% 56.9%)', 'hsl(215 20.2% 55.1%)')};
font-size: 13px;
margin-top: 6px;
line-height: 1.5;
}
/* Scrollbar styling */
.suggestions-dropdown::-webkit-scrollbar {
width: 8px;
}
.suggestions-dropdown::-webkit-scrollbar-track {
background: transparent;
}
.suggestions-dropdown::-webkit-scrollbar-thumb {
background: ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
border-radius: 4px;
}
.suggestions-dropdown::-webkit-scrollbar-thumb:hover {
background: ${cssManager.bdTheme('hsl(0 0% 79.8%)', 'hsl(0 0% 20.9%)')};
}
`,
];
public render(): TemplateResult {
const filteredSuggestions = this.suggestions.filter(
suggestion =>
!this.value.includes(suggestion) &&
suggestion.toLowerCase().includes(this.inputValue.toLowerCase())
);
return html`
<div class="input-wrapper">
${this.label ? html`<dees-label .label=${this.label} .required=${this.required}></dees-label>` : ''}
<div class="suggestions-container">
<div
class="tags-container ${this.disabled ? 'disabled' : ''}"
@click=${this.handleContainerClick}
>
${this.value.map(tag => html`
<div class="tag">
<span>${tag}</span>
${!this.disabled ? html`
<div class="tag-remove" @click=${(e: Event) => this.removeTag(e, tag)}>
<dees-icon .icon=${'lucide:x'}></dees-icon>
</div>
` : ''}
</div>
`)}
${!this.disabled && (!this.maxTags || this.value.length < this.maxTags) ? html`
<input
type="text"
class="tag-input"
.placeholder=${this.placeholder}
.value=${this.inputValue}
@input=${this.handleInput}
@keydown=${this.handleKeyDown}
@focus=${this.handleFocus}
@blur=${this.handleBlur}
?disabled=${this.disabled}
/>
` : ''}
</div>
${this.showSuggestions && filteredSuggestions.length > 0 ? html`
<div class="suggestions-dropdown">
${filteredSuggestions.map((suggestion, index) => html`
<div
class="suggestion ${index === this.highlightedSuggestionIndex ? 'highlighted' : ''}"
@mousedown=${(e: Event) => {
e.preventDefault(); // Prevent blur
this.addTag(suggestion);
}}
@mouseenter=${() => this.highlightedSuggestionIndex = index}
>
${suggestion}
</div>
`)}
</div>
` : ''}
</div>
${this.validationText ? html`
<div class="validation-message">${this.validationText}</div>
` : ''}
${this.description ? html`
<div class="description">${this.description}</div>
` : ''}
</div>
`;
}
private handleContainerClick(e: Event) {
if (this.disabled) return;
const input = this.shadowRoot?.querySelector('.tag-input') as HTMLInputElement;
if (input && e.target !== input) {
input.focus();
}
}
private handleInput(e: Event) {
const input = e.target as HTMLInputElement;
this.inputValue = input.value;
// Check for comma or semicolon to add tag
if (this.inputValue.includes(',') || this.inputValue.includes(';')) {
const tag = this.inputValue.replace(/[,;]/g, '').trim();
if (tag) {
this.addTag(tag);
}
}
}
private handleKeyDown(e: KeyboardEvent) {
const input = e.target as HTMLInputElement;
if (e.key === 'Enter') {
e.preventDefault();
if (this.highlightedSuggestionIndex >= 0 && this.showSuggestions) {
const filteredSuggestions = this.suggestions.filter(
suggestion =>
!this.value.includes(suggestion) &&
suggestion.toLowerCase().includes(this.inputValue.toLowerCase())
);
if (filteredSuggestions[this.highlightedSuggestionIndex]) {
this.addTag(filteredSuggestions[this.highlightedSuggestionIndex]);
}
} else if (this.inputValue.trim()) {
this.addTag(this.inputValue.trim());
}
} else if (e.key === 'Backspace' && !this.inputValue && this.value.length > 0) {
// Remove last tag when backspace is pressed on empty input
this.removeTag(e, this.value[this.value.length - 1]);
} else if (e.key === 'ArrowDown' && this.showSuggestions) {
e.preventDefault();
const filteredCount = this.suggestions.filter(
s => !this.value.includes(s) && s.toLowerCase().includes(this.inputValue.toLowerCase())
).length;
this.highlightedSuggestionIndex = Math.min(
this.highlightedSuggestionIndex + 1,
filteredCount - 1
);
} else if (e.key === 'ArrowUp' && this.showSuggestions) {
e.preventDefault();
this.highlightedSuggestionIndex = Math.max(this.highlightedSuggestionIndex - 1, 0);
} else if (e.key === 'Escape') {
this.showSuggestions = false;
this.highlightedSuggestionIndex = -1;
}
}
private handleFocus() {
if (this.suggestions.length > 0) {
this.showSuggestions = true;
}
}
private handleBlur() {
// Delay to allow click on suggestions
setTimeout(() => {
this.showSuggestions = false;
this.highlightedSuggestionIndex = -1;
}, 200);
}
private addTag(tag: string) {
if (!tag || this.value.includes(tag)) return;
if (this.maxTags && this.value.length >= this.maxTags) return;
this.value = [...this.value, tag];
this.inputValue = '';
this.showSuggestions = false;
this.highlightedSuggestionIndex = -1;
// Clear the input
const input = this.shadowRoot?.querySelector('.tag-input') as HTMLInputElement;
if (input) {
input.value = '';
}
this.emitChange();
}
private removeTag(e: Event, tag: string) {
e.stopPropagation();
this.value = this.value.filter(t => t !== tag);
this.emitChange();
}
private emitChange() {
this.dispatchEvent(new CustomEvent('change', {
detail: { value: this.value },
bubbles: true,
composed: true
}));
this.changeSubject.next(this);
}
public getValue(): string[] {
return this.value;
}
public setValue(value: string[]): void {
this.value = value || [];
}
public async validate(): Promise<boolean> {
if (this.required && (!this.value || this.value.length === 0)) {
this.validationText = 'At least one tag is required';
return false;
}
this.validationText = '';
return true;
}
}

View File

@ -1,77 +1,88 @@
import { html, css } from '@design.estate/dees-element'; import { html, css, cssManager } from '@design.estate/dees-element';
import '@design.estate/dees-wcctools/demotools'; import '@design.estate/dees-wcctools/demotools';
import './dees-panel.js';
import type { DeesInputText } from './dees-input-text.js';
export const demoFunc = () => html` export const demoFunc = () => html`
<dees-demowrapper> <style>
<style> ${css`
${css` .demo-container {
.demo-container { display: flex;
display: flex; flex-direction: column;
flex-direction: column; gap: 24px;
gap: 24px; padding: 24px;
padding: 24px; max-width: 1200px;
max-width: 1200px; margin: 0 auto;
margin: 0 auto; }
}
dees-panel {
.demo-section { margin-bottom: 24px;
background: #f8f9fa; }
border-radius: 8px;
padding: 24px; dees-panel:last-child {
} margin-bottom: 0;
}
@media (prefers-color-scheme: dark) {
.demo-section { .horizontal-group {
background: #1a1a1a; display: flex;
} align-items: center;
} gap: 16px;
flex-wrap: wrap;
.demo-section h3 { }
margin-top: 0;
margin-bottom: 16px; .grid-layout {
color: #0069f2; display: grid;
font-size: 18px; grid-template-columns: 1fr 1fr;
} gap: 16px;
}
.demo-section p {
margin-top: 0; @media (max-width: 768px) {
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 { .grid-layout {
display: grid; grid-template-columns: 1fr;
grid-template-columns: 1fr 1fr;
gap: 16px;
} }
}
.interactive-section {
background: ${cssManager.bdTheme('hsl(210 40% 96.1%)', 'hsl(215 20.2% 16.8%)')};
border-radius: 8px;
padding: 16px;
margin-top: 16px;
}
.output-text {
font-family: monospace;
font-size: 13px;
color: ${cssManager.bdTheme('hsl(215.3 25% 26.7%)', 'hsl(210 40% 80%)')};
padding: 8px;
background: ${cssManager.bdTheme('hsl(210 40% 98%)', 'hsl(215 20.2% 11.8%)')};
border-radius: 4px;
min-height: 24px;
}
`}
</style>
<div class="demo-container">
<dees-demowrapper .runAfterRender=${async (elementArg: HTMLElement) => {
// Demonstrate basic text input functionality
const inputs = elementArg.querySelectorAll('dees-input-text');
inputs.forEach((input: DeesInputText) => {
input.addEventListener('changeSubject', (event: CustomEvent) => {
console.log(`Input "${input.label}" changed to:`, input.getValue());
});
@media (max-width: 768px) { input.addEventListener('blur', () => {
.grid-layout { console.log(`Input "${input.label}" lost focus`);
grid-template-columns: 1fr; });
} });
}
`} // Show password visibility toggle
</style> const passwordInput = elementArg.querySelector('dees-input-text[key="password"]') as DeesInputText;
if (passwordInput) {
<div class="demo-container"> console.log('Password input includes visibility toggle');
<div class="demo-section"> }
<h3>Basic Text Inputs</h3> }}>
<p>Standard text inputs with labels and descriptions</p> <dees-panel .title=${'Basic Text Inputs'} .subtitle=${'Standard text inputs with labels and descriptions'}>
<dees-input-text <dees-input-text
.label=${'Username'} .label=${'Username'}
.value=${'johndoe'} .value=${'johndoe'}
@ -91,12 +102,35 @@ export const demoFunc = () => html`
.value=${'secret123'} .value=${'secret123'}
.key=${'password'} .key=${'password'}
></dees-input-text> ></dees-input-text>
</div> </dees-panel>
</dees-demowrapper>
<dees-demowrapper .runAfterRender=${async (elementArg: HTMLElement) => {
// Demonstrate horizontal layout behavior
const horizontalInputs = elementArg.querySelectorAll('dees-input-text');
<div class="demo-section"> // Check that inputs are properly spaced horizontally
<h3>Horizontal Layout</h3> horizontalInputs.forEach((input: DeesInputText) => {
<p>Multiple inputs arranged horizontally for compact forms</p> const computedStyle = window.getComputedStyle(input);
console.log(`Horizontal input "${input.label}" display:`, computedStyle.display);
});
// Track value changes
const firstNameInput = elementArg.querySelector('dees-input-text[key="firstName"]');
const lastNameInput = elementArg.querySelector('dees-input-text[key="lastName"]');
if (firstNameInput && lastNameInput) {
const updateFullName = () => {
const firstName = (firstNameInput as DeesInputText).getValue();
const lastName = (lastNameInput as DeesInputText).getValue();
console.log(`Full name: ${firstName} ${lastName}`);
};
firstNameInput.addEventListener('changeSubject', updateFullName);
lastNameInput.addEventListener('changeSubject', updateFullName);
}
}}>
<dees-panel .title=${'Horizontal Layout'} .subtitle=${'Multiple inputs arranged horizontally for compact forms'}>
<div class="horizontal-group"> <div class="horizontal-group">
<dees-input-text <dees-input-text
.label=${'First Name'} .label=${'First Name'}
@ -119,12 +153,25 @@ export const demoFunc = () => html`
.key=${'age'} .key=${'age'}
></dees-input-text> ></dees-input-text>
</div> </div>
</div> </dees-panel>
</dees-demowrapper>
<dees-demowrapper .runAfterRender=${async (elementArg: HTMLElement) => {
// Demonstrate different label positions
const inputs = elementArg.querySelectorAll('dees-input-text');
<div class="demo-section"> inputs.forEach((input: DeesInputText) => {
<h3>Label Positions</h3> const position = input.labelPosition;
<p>Different label positioning options for various layouts</p> console.log(`Input "${input.label}" has label position: ${position}`);
});
// Show how label position affects layout
const leftLabelInputs = elementArg.querySelectorAll('dees-input-text[labelPosition="left"]');
if (leftLabelInputs.length > 0) {
console.log(`${leftLabelInputs.length} inputs have left-aligned labels for inline layout`);
}
}}>
<dees-panel .title=${'Label Positions'} .subtitle=${'Different label positioning options for various layouts'}>
<dees-input-text <dees-input-text
.label=${'Label on Top (Default)'} .label=${'Label on Top (Default)'}
.value=${'Standard layout'} .value=${'Standard layout'}
@ -150,12 +197,43 @@ export const demoFunc = () => html`
.labelPosition=${'left'} .labelPosition=${'left'}
></dees-input-text> ></dees-input-text>
</div> </div>
</div> </dees-panel>
</dees-demowrapper>
<dees-demowrapper .runAfterRender=${async (elementArg: HTMLElement) => {
// Demonstrate validation states
const requiredInput = elementArg.querySelector('dees-input-text[required]') as DeesInputText;
const disabledInput = elementArg.querySelector('dees-input-text[disabled]') as DeesInputText;
const errorInput = elementArg.querySelector('dees-input-text[validationState="invalid"]') as DeesInputText;
<div class="demo-section"> if (requiredInput) {
<h3>Validation & States</h3> // Show validation on blur for empty required field
<p>Different validation states and input configurations</p> requiredInput.addEventListener('blur', () => {
if (!requiredInput.getValue()) {
console.log('Required field is empty!');
}
});
}
if (disabledInput) {
console.log('Disabled input cannot be edited');
}
if (errorInput) {
console.log('Error input shows validation message:', errorInput.validationText);
// Simulate fixing the error
errorInput.addEventListener('changeSubject', () => {
const value = errorInput.getValue();
if (value.includes('@') && value.includes('.')) {
errorInput.validationState = 'valid';
errorInput.validationText = '';
console.log('Email validation passed!');
}
});
}
}}>
<dees-panel .title=${'Validation & States'} .subtitle=${'Different validation states and input configurations'}>
<dees-input-text <dees-input-text
.label=${'Required Field'} .label=${'Required Field'}
.required=${true} .required=${true}
@ -174,12 +252,33 @@ export const demoFunc = () => html`
.validationText=${'Please enter a valid email address'} .validationText=${'Please enter a valid email address'}
.validationState=${'invalid'} .validationState=${'invalid'}
></dees-input-text> ></dees-input-text>
</div> </dees-panel>
</dees-demowrapper>
<dees-demowrapper .runAfterRender=${async (elementArg: HTMLElement) => {
// Track password visibility toggles
const passwordInputs = elementArg.querySelectorAll('dees-input-text[isPasswordBool]');
<div class="demo-section"> passwordInputs.forEach((input: DeesInputText) => {
<h3>Advanced Features</h3> // Monitor for toggle button clicks within shadow DOM
<p>Password visibility toggle and other advanced features</p> const checkToggle = () => {
const inputEl = input.shadowRoot?.querySelector('input');
if (inputEl) {
console.log(`Password field "${input.label}" type:`, inputEl.type);
}
};
// Use MutationObserver to detect changes
if (input.shadowRoot) {
const observer = new MutationObserver(checkToggle);
const inputEl = input.shadowRoot.querySelector('input');
if (inputEl) {
observer.observe(inputEl, { attributes: true, attributeFilter: ['type'] });
}
}
});
}}>
<dees-panel .title=${'Advanced Features'} .subtitle=${'Password visibility toggle and other advanced features'}>
<dees-input-text <dees-input-text
.label=${'Password with Toggle'} .label=${'Password with Toggle'}
.isPasswordBool=${true} .isPasswordBool=${true}
@ -193,7 +292,48 @@ export const demoFunc = () => html`
.value=${'sk-1234567890abcdef'} .value=${'sk-1234567890abcdef'}
.description=${'Keep this key secure and never share it'} .description=${'Keep this key secure and never share it'}
></dees-input-text> ></dees-input-text>
</div> </dees-panel>
</div> </dees-demowrapper>
</dees-demowrapper>
<dees-demowrapper .runAfterRender=${async (elementArg: HTMLElement) => {
// Set up interactive example
const dynamicInput = elementArg.querySelector('dees-input-text');
const output = elementArg.querySelector('#text-input-output');
if (dynamicInput && output) {
// Update output on every change
dynamicInput.addEventListener('changeSubject', (event: CustomEvent) => {
const value = (event.detail as DeesInputText).getValue();
output.textContent = `Current value: "${value}"`;
});
// Also track focus/blur events
dynamicInput.addEventListener('focus', () => {
console.log('Input focused');
});
dynamicInput.addEventListener('blur', () => {
console.log('Input blurred');
});
// Track keypress events
let keypressCount = 0;
dynamicInput.addEventListener('keydown', () => {
keypressCount++;
console.log(`Keypress count: ${keypressCount}`);
});
}
}}>
<dees-panel .title=${'Interactive Example'} .subtitle=${'Try typing in the inputs to see real-time value changes'}>
<dees-input-text
.label=${'Dynamic Input'}
.placeholder=${'Type something here...'}
></dees-input-text>
<div class="interactive-section">
<div id="text-input-output" class="output-text">Current value: ""</div>
</div>
</dees-panel>
</dees-demowrapper>
</div>
`; `;

View File

@ -1,6 +1,7 @@
import * as colors from './00colors.js'; import * as colors from './00colors.js';
import { DeesInputBase } from './dees-input-base.js'; import { DeesInputBase } from './dees-input-base.js';
import { demoFunc } from './dees-input-text.demo.js'; import { demoFunc } from './dees-input-text.demo.js';
import { cssGeistFontFamily, cssMonoFontFamily } from './00fonts.js';
import { import {
customElement, customElement,
@ -65,77 +66,129 @@ export class DeesInputText extends DeesInputBase {
:host { :host {
position: relative; position: relative;
z-index: auto; z-index: auto;
font-family: ${cssGeistFontFamily};
} }
.maincontainer { .maincontainer {
color: ${cssManager.bdTheme('#333', '#ccc')}; position: relative;
color: ${cssManager.bdTheme('hsl(0 0% 15%)', 'hsl(0 0% 90%)')};
} }
input { input {
margin-top: 0px; display: flex;
background: ${cssManager.bdTheme('#fafafa', '#222')}; height: 40px;
border-top: ${cssManager.bdTheme('1px solid #CCC', '1px solid #ffffff10')};
border-bottom: ${cssManager.bdTheme('1px solid #CCC', '1px solid #222')};
border-right: ${cssManager.bdTheme('1px solid #CCC', 'none')};
border-left: ${cssManager.bdTheme('1px solid #CCC', 'none')};
padding-left: 10px;
padding-right: 10px;
border-radius: 2px;
width: 100%; width: 100%;
line-height: 36px; padding: 0 12px;
transition: all 0.2s; font-size: 14px;
line-height: 40px;
background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(0 0% 9%)')};
border: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
border-radius: 6px;
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
outline: none; outline: none;
font-size: 16px; cursor: text;
position: relative; font-family: inherit;
z-index: 2; color: ${cssManager.bdTheme('hsl(0 0% 3.9%)', 'hsl(0 0% 98%)')};
cursor: default;
} }
input:disabled { input::placeholder {
background: ${cssManager.bdTheme('#ffffff00', '#11111100')}; color: ${cssManager.bdTheme('hsl(0 0% 45.1%)', 'hsl(0 0% 63.9%)')};
border: 1px dashed ${cssManager.bdTheme('#666666', '#666666')}; }
color: #9b9b9e;
cursor: default; input:hover:not(:disabled):not(:focus) {
border-color: ${cssManager.bdTheme('hsl(0 0% 79.8%)', 'hsl(0 0% 20.9%)')};
} }
input:focus { input:focus {
outline: none; outline: none;
border-bottom: 1px solid border-color: ${cssManager.bdTheme('hsl(0 0% 9%)', 'hsl(0 0% 98%)')};
${cssManager.bdTheme(colors.bright.blueActive, colors.dark.blueActive)}; box-shadow: 0 0 0 2px ${cssManager.bdTheme('hsl(0 0% 9% / 0.05)', 'hsl(0 0% 98% / 0.05)')};
cursor: text;
} }
input:hover { input:disabled {
filter: ${cssManager.bdTheme('brightness(0.95)', 'brightness(1.1)')}; background: ${cssManager.bdTheme('hsl(0 0% 95.1%)', 'hsl(0 0% 14.9%)')};
border-color: ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
color: ${cssManager.bdTheme('hsl(0 0% 45.1%)', 'hsl(0 0% 63.9%)')};
cursor: not-allowed;
opacity: 0.5;
} }
/* Password toggle button */
.showPassword { .showPassword {
position: absolute; position: absolute;
bottom: 7px; right: 1px;
right: 10px; top: 50%;
border: 1px dashed #444; transform: translateY(-50%);
border-radius: 7px; display: flex;
padding: 4px 0px; align-items: center;
width: 40px; justify-content: center;
z-index: 3; width: 38px;
text-align: center; height: 38px;
cursor: pointer;
color: ${cssManager.bdTheme('hsl(0 0% 45.1%)', 'hsl(0 0% 63.9%)')};
transition: all 0.15s ease;
border-radius: 0 5px 5px 0;
} }
.showPassword:hover { .showPassword:hover {
background: ${cssManager.bdTheme('#00000010', '#ffffff10')}; background: ${cssManager.bdTheme('hsl(0 0% 95.1%)', 'hsl(0 0% 14.9%)')};
color: ${cssManager.bdTheme('hsl(0 0% 15%)', 'hsl(0 0% 93.9%)')};
} }
/* Validation styles */
.validationContainer { .validationContainer {
text-align: center; margin-top: 4px;
padding: 6px 2px 2px 2px; padding: 4px 8px;
margin-top: -4px;
font-size: 12px; font-size: 12px;
color: #fff; font-weight: 500;
background: #e4002b; border-radius: 4px;
position: relative; transition: all 0.2s ease;
z-index: 1; overflow: hidden;
border-radius: 3px; }
transition: all 0.2s;
.validationContainer.error {
background: ${cssManager.bdTheme('hsl(0 84.2% 60.2% / 0.1)', 'hsl(0 72.2% 50.6% / 0.1)')};
color: ${cssManager.bdTheme('hsl(0 84.2% 60.2%)', 'hsl(0 72.2% 50.6%)')};
}
.validationContainer.warn {
background: ${cssManager.bdTheme('hsl(25 95% 53% / 0.1)', 'hsl(25 95% 63% / 0.1)')};
color: ${cssManager.bdTheme('hsl(25 95% 53%)', 'hsl(25 95% 63%)')};
}
.validationContainer.valid {
background: ${cssManager.bdTheme('hsl(142.1 76.2% 36.3% / 0.1)', 'hsl(142.1 70.6% 45.3% / 0.1)')};
color: ${cssManager.bdTheme('hsl(142.1 76.2% 36.3%)', 'hsl(142.1 70.6% 45.3%)')};
}
/* Error state for input */
:host([validation-state="invalid"]) input {
border-color: ${cssManager.bdTheme('hsl(0 84.2% 60.2%)', 'hsl(0 72.2% 50.6%)')};
}
:host([validation-state="invalid"]) input:focus {
border-color: ${cssManager.bdTheme('hsl(0 84.2% 60.2%)', 'hsl(0 72.2% 50.6%)')};
box-shadow: 0 0 0 2px ${cssManager.bdTheme('hsl(0 84.2% 60.2% / 0.05)', 'hsl(0 72.2% 50.6% / 0.05)')};
}
/* Warning state for input */
:host([validation-state="warn"]) input {
border-color: ${cssManager.bdTheme('hsl(25 95% 53%)', 'hsl(25 95% 63%)')};
}
:host([validation-state="warn"]) input:focus {
border-color: ${cssManager.bdTheme('hsl(25 95% 53%)', 'hsl(25 95% 63%)')};
box-shadow: 0 0 0 2px ${cssManager.bdTheme('hsl(25 95% 53% / 0.05)', 'hsl(25 95% 63% / 0.05)')};
}
/* Valid state for input */
:host([validation-state="valid"]) input {
border-color: ${cssManager.bdTheme('hsl(142.1 76.2% 36.3%)', 'hsl(142.1 70.6% 45.3%)')};
}
:host([validation-state="valid"]) input:focus {
border-color: ${cssManager.bdTheme('hsl(142.1 76.2% 36.3%)', 'hsl(142.1 70.6% 45.3%)')};
box-shadow: 0 0 0 2px ${cssManager.bdTheme('hsl(142.1 76.2% 36.3% / 0.05)', 'hsl(142.1 70.6% 45.3% / 0.05)')};
} }
`, `,
]; ];
@ -144,42 +197,51 @@ export class DeesInputText extends DeesInputBase {
return html` return html`
<style> <style>
input { input {
font-family: ${this.isPasswordBool ? 'monospace' : 'Geist Sans'}; font-family: ${this.isPasswordBool ? cssMonoFontFamily : 'inherit'};
letter-spacing: ${this.isPasswordBool ? '1px' : 'normal'}; letter-spacing: ${this.isPasswordBool ? '0.5px' : 'normal'};
color: ${this.goBright ? '#333' : '#ccc'}; padding-right: ${this.isPasswordBool ? '48px' : '12px'};
} }
${this.validationText ${this.validationText
? css` ? css`
.validationContainer { .validationContainer {
height: 22px; height: auto;
opacity: 1; opacity: 1;
transform: translateY(0);
} }
` `
: css` : css`
.validationContainer { .validationContainer {
height: 4px; height: 0;
padding: 2px !important; padding: 0 !important;
opacity: 0; opacity: 0;
transform: translateY(-4px);
} }
`} `}
</style> </style>
<div class="input-wrapper"> <div class="input-wrapper">
<dees-label .label=${this.label} .description=${this.description}></dees-label> <dees-label .label=${this.label} .description=${this.description} .required=${this.required}></dees-label>
<div class="maincontainer"> <div class="maincontainer">
<input <input
type="${this.isPasswordBool && !this.showPasswordBool ? 'password' : 'text'}" type="${this.isPasswordBool && !this.showPasswordBool ? 'password' : 'text'}"
.value=${this.value} .value=${this.value}
@input="${this.updateValue}" @input="${this.updateValue}"
.disabled=${this.disabled} .disabled=${this.disabled}
placeholder="${this.label ? '' : 'Enter text...'}"
/> />
<div class="validationContainer">${this.validationText}</div>
${this.isPasswordBool ${this.isPasswordBool
? html` ? html`
<div class="showPassword" @click=${this.togglePasswordView}> <div class="showPassword" @click=${this.togglePasswordView}>
<dees-icon .iconFA=${this.showPasswordBool ? 'eye' : 'eyeSlash'}></dees-icon> <dees-icon .icon=${this.showPasswordBool ? 'lucide:eye' : 'lucide:eye-off'}></dees-icon>
</div> </div>
` `
: html``} : html``}
${this.validationText
? html`
<div class="validationContainer ${this.validationState || 'error'}">
${this.validationText}
</div>
`
: html`<div class="validationContainer"></div>`}
</div> </div>
</div> </div>
`; `;
@ -205,7 +267,6 @@ export class DeesInputText extends DeesInputBase {
public async togglePasswordView() { public async togglePasswordView() {
this.showPasswordBool = !this.showPasswordBool; this.showPasswordBool = !this.showPasswordBool;
console.log(`this.showPasswordBool is: ${this.showPasswordBool}`);
} }
public async focus() { public async focus() {

View File

@ -10,7 +10,7 @@ import {
import * as domtools from '@design.estate/dees-domtools'; import * as domtools from '@design.estate/dees-domtools';
import { DeesInputBase } from './dees-input-base.js'; import { DeesInputBase } from './dees-input-base.js';
const { demoFunc } = await import('./dees-input-typelist.demo.js'); import { demoFunc } from './dees-input-typelist.demo.js';
@customElement('dees-input-typelist') @customElement('dees-input-typelist')
export class DeesInputTypelist extends DeesInputBase<DeesInputTypelist> { export class DeesInputTypelist extends DeesInputBase<DeesInputTypelist> {
@ -35,12 +35,24 @@ export class DeesInputTypelist extends DeesInputBase<DeesInputTypelist> {
} }
.mainbox { .mainbox {
border-radius: 3px; border-radius: 3px;
background: #222; background: ${cssManager.bdTheme('#fafafa', '#222222')};
overflow: hidden; overflow: hidden;
border-top: ${cssManager.bdTheme('1px solid #CCC', '1px solid #444')}; border-top: ${cssManager.bdTheme('1px solid #CCC', '1px solid #ffffff10')};
border-bottom: ${cssManager.bdTheme('1px solid #CCC', '1px solid #333')}; border-bottom: ${cssManager.bdTheme('1px solid #CCC', '1px solid #222')};
border-right: ${cssManager.bdTheme('1px solid #CCC', 'none')}; border-right: ${cssManager.bdTheme('1px solid #CCC', '1px solid #ffffff10')};
border-left: ${cssManager.bdTheme('1px solid #CCC', 'none')}; border-left: ${cssManager.bdTheme('1px solid #CCC', '1px solid #ffffff10')};
box-shadow: ${cssManager.bdTheme('0px 1px 4px rgba(0,0,0,0.3)', 'none')};
transition: all 0.2s;
position: relative;
}
.mainbox:hover {
filter: ${cssManager.bdTheme('brightness(0.98)', 'brightness(1.05)')};
}
.mainbox:focus-within {
outline: 2px solid ${cssManager.bdTheme('#0069f2', '#0084ff')};
outline-offset: -2px;
} }
.tags { .tags {
@ -50,14 +62,15 @@ export class DeesInputTypelist extends DeesInputBase<DeesInputTypelist> {
.notags { .notags {
text-align: center; text-align: center;
opacity: 0.5; color: ${cssManager.bdTheme('#999', '#666')};
font-size: 12px; font-size: 13px;
font-style: italic;
} }
input { input {
display: block; display: block;
box-sizing: border-box; box-sizing: border-box;
background: #181818; background: ${cssManager.bdTheme('#f5f5f5', '#181818')};
width: 100%; width: 100%;
outline: none; outline: none;
border: none; border: none;
@ -67,30 +80,68 @@ export class DeesInputTypelist extends DeesInputBase<DeesInputTypelist> {
line-height: 32px; line-height: 32px;
height: 0px; height: 0px;
transition: height 0.2s; transition: height 0.2s;
border-top: 1px solid ${cssManager.bdTheme('#e0e0e0', '#333')};
} }
input:focus { input:focus {
height: 32px; height: 32px;
background: ${cssManager.bdTheme('#fafafa', '#1a1a1a')};
}
input::placeholder {
color: ${cssManager.bdTheme('#999', '#666')};
} }
.tag { .tag {
display: inline-block; display: inline-block;
background: ${cssManager.bdTheme('#e0e0e0', '#444')}; background: ${cssManager.bdTheme('#e8f5e9', '#2d3a2d')};
color: ${cssManager.bdTheme('#333', '#fff')}; color: ${cssManager.bdTheme('#2e7d32', '#81c784')};
padding: 4px 8px; padding: 4px 10px;
border-radius: 3px; border-radius: 4px;
margin: 2px; margin: 3px;
font-size: 12px; font-size: 13px;
font-weight: 500;
transition: all 0.2s;
border: 1px solid ${cssManager.bdTheme('#c8e6c9', '#1b5e20')};
}
.tag:hover {
background: ${cssManager.bdTheme('#c8e6c9', '#3d4f3d')};
transform: translateY(-1px);
} }
.tag .remove { .tag .remove {
margin-left: 6px; margin-left: 8px;
cursor: pointer; cursor: pointer;
opacity: 0.6; opacity: 0.7;
font-weight: 700;
font-size: 16px;
line-height: 1;
transition: opacity 0.2s;
} }
.tag .remove:hover { .tag .remove:hover {
opacity: 1; opacity: 1;
color: ${cssManager.bdTheme('#c62828', '#ef5350')};
}
/* Disabled state */
:host([disabled]) .mainbox {
opacity: 0.6;
cursor: not-allowed;
}
:host([disabled]) .tags {
cursor: not-allowed;
}
:host([disabled]) .tag {
pointer-events: none;
}
:host([disabled]) input {
cursor: not-allowed;
background: ${cssManager.bdTheme('#f0f0f0', '#1a1a1a')};
} }
`, `,
]; ];

View File

@ -32,20 +32,43 @@ export class DeesLabel extends DeesElement {
}) })
public description: string; public description: string;
@property({
type: Boolean,
reflect: true,
})
public required: boolean = false;
public static styles = [ public static styles = [
cssManager.defaultStyles, cssManager.defaultStyles,
css` css`
:host {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
}
.label { .label {
color: ${cssManager.bdTheme('#333', '#ccc')}; display: inline-block;
color: ${cssManager.bdTheme('hsl(0 0% 15%)', 'hsl(0 0% 90%)')};
font-size: 14px; font-size: 14px;
margin-bottom: 8px; font-weight: 500;
line-height: 1.5;
margin-bottom: 6px;
cursor: default; cursor: default;
user-select: none; user-select: none;
letter-spacing: -0.01em;
} }
.required {
color: ${cssManager.bdTheme('hsl(0 84.2% 60.2%)', 'hsl(0 72.2% 50.6%)')};
margin-left: 2px;
}
dees-icon { dees-icon {
display: inline-block; display: inline-block;
font-size: 14px; font-size: 12px;
transform: translateY(1.5px); transform: translateY(1px);
margin-left: 4px;
color: ${cssManager.bdTheme('hsl(0 0% 45.1%)', 'hsl(0 0% 63.9%)')};
cursor: help;
} }
`, `,
]; ];
@ -56,9 +79,10 @@ export class DeesLabel extends DeesElement {
? html` ? html`
<div class="label"> <div class="label">
${this.label} ${this.label}
${this.required ? html`<span class="required">*</span>` : ''}
${this.description ${this.description
? html` ? html`
<dees-icon .iconFA=${'circleInfo'}></dees-icon> <dees-icon .icon=${'lucide:info'}></dees-icon>
<dees-speechbubble .text=${this.description}></dees-speechbubble> <dees-speechbubble .text=${this.description}></dees-speechbubble>
` `
: html``} : html``}

View File

@ -0,0 +1,215 @@
import { html, css } from '@design.estate/dees-element';
import './dees-button.js';
import './dees-panel.js';
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: 800px;
margin: 0 auto;
}
.demo-buttons {
display: flex;
gap: 16px;
flex-wrap: wrap;
}
`}
</style>
<div class="demo-container">
<dees-panel .title=${'Mobile Navigation'} .subtitle=${'Shadcn-style slide-in navigation menu with icons'}>
<div class="demo-buttons">
<dees-button
@click=${async () => {
const { DeesMobilenavigation } = await import('./dees-mobilenavigation.js');
DeesMobilenavigation.createAndShow([
{
name: 'Dashboard',
iconName: 'lucide:layout-dashboard',
action: async (nav) => {
console.log('Navigate to dashboard');
},
},
{
name: 'Profile',
iconName: 'lucide:user',
action: async (nav) => {
console.log('Navigate to profile');
},
},
{
name: 'Messages',
iconName: 'lucide:mail',
action: async (nav) => {
console.log('Navigate to messages');
},
},
{
name: 'Settings',
iconName: 'lucide:settings',
action: async (nav) => {
console.log('Navigate to settings');
},
},
{ divider: true } as any,
{
name: 'Help & Support',
iconName: 'lucide:help-circle',
action: async (nav) => {
console.log('Show help');
},
},
{
name: 'Sign Out',
iconName: 'lucide:log-out',
action: async (nav) => {
console.log('Sign out');
},
},
]);
}}
>
Open Navigation Menu
</dees-button>
<dees-button
type="secondary"
@click=${async () => {
const { DeesMobilenavigation } = await import('./dees-mobilenavigation.js');
const nav = await DeesMobilenavigation.createAndShow([
{
name: 'New Document',
iconName: 'lucide:file-plus',
action: async () => console.log('New document'),
},
{
name: 'Upload File',
iconName: 'lucide:upload',
action: async () => console.log('Upload file'),
},
{
name: 'Download',
iconName: 'lucide:download',
action: async () => console.log('Download'),
},
{ divider: true } as any,
{
name: 'Share',
iconName: 'lucide:share-2',
action: async () => console.log('Share'),
},
{
name: 'Export',
iconName: 'lucide:export',
action: async () => console.log('Export'),
},
]);
nav.heading = 'File Actions';
}}
>
File Actions Menu
</dees-button>
<dees-button
type="outline"
@click=${async () => {
const { DeesMobilenavigation } = await import('./dees-mobilenavigation.js');
const nav = await DeesMobilenavigation.createAndShow([
{
name: 'Cut',
iconName: 'lucide:scissors',
action: async () => console.log('Cut'),
},
{
name: 'Copy',
iconName: 'lucide:copy',
action: async () => console.log('Copy'),
},
{
name: 'Paste',
iconName: 'lucide:clipboard',
action: async () => console.log('Paste'),
},
{ divider: true } as any,
{
name: 'Select All',
iconName: 'lucide:square-check',
action: async () => console.log('Select all'),
},
{
name: 'Find',
iconName: 'lucide:search',
action: async () => console.log('Find'),
},
{
name: 'Replace',
iconName: 'lucide:replace',
action: async () => console.log('Replace'),
},
]);
nav.heading = 'Edit';
}}
>
Edit Menu
</dees-button>
</div>
</dees-panel>
<dees-panel .title=${'Features'} .subtitle=${'Modern shadcn-inspired mobile navigation'}>
<div style="padding: 16px;">
<ul style="margin: 0; padding-left: 24px; display: flex; flex-direction: column; gap: 8px;">
<li>Smooth slide-in animation from the right</li>
<li>Z-index registry integration for proper stacking</li>
<li>Backdrop blur with window layer</li>
<li>Support for icons using Lucide icons</li>
<li>Menu item dividers for grouping</li>
<li>Staggered animation for menu items</li>
<li>Responsive design that adapts to mobile screens</li>
<li>Clean, modern shadcn-style aesthetics</li>
<li>Dark/light theme support</li>
<li>Singleton pattern ensures only one instance</li>
</ul>
</div>
</dees-panel>
<dees-panel .title=${'Code Example'} .subtitle=${'How to use the mobile navigation'}>
<div style="padding: 16px; background: var(--background-secondary); border-radius: 8px;">
<pre style="margin: 0; font-family: monospace; font-size: 13px; line-height: 1.6;"><code>import { DeesMobilenavigation } from '@design.estate/dees-catalog';
DeesMobilenavigation.createAndShow([
{
name: 'Dashboard',
iconName: 'lucide:layout-dashboard',
action: async (nav) => {
console.log('Navigate to dashboard');
},
},
{
name: 'Settings',
iconName: 'lucide:settings',
action: async (nav) => {
console.log('Navigate to settings');
},
},
{ divider: true },
{
name: 'Sign Out',
iconName: 'lucide:log-out',
action: async (nav) => {
console.log('Sign out');
},
},
]);</code></pre>
</div>
</dees-panel>
</div>
</dees-demowrapper>
`;

View File

@ -1,5 +1,6 @@
import * as plugins from './00plugins.js'; import * as plugins from './00plugins.js';
import { zIndexLayers } from './00zindex.js'; import { zIndexRegistry } from './00zindex.js';
import { cssGeistFontFamily } from './00fonts.js';
import { import {
cssManager, cssManager,
css, css,
@ -9,8 +10,10 @@ import {
domtools, domtools,
html, html,
property, property,
state,
} from '@design.estate/dees-element'; } from '@design.estate/dees-element';
import { DeesWindowLayer } from './dees-windowlayer.js'; import { DeesWindowLayer } from './dees-windowlayer.js';
import './dees-icon.js';
@customElement('dees-mobilenavigation') @customElement('dees-mobilenavigation')
export class DeesMobilenavigation extends DeesElement { export class DeesMobilenavigation extends DeesElement {
@ -19,14 +22,48 @@ export class DeesMobilenavigation extends DeesElement {
<dees-button @click=${() => { <dees-button @click=${() => {
DeesMobilenavigation.createAndShow([ DeesMobilenavigation.createAndShow([
{ {
name: 'Test', name: 'Dashboard',
iconName: 'lucide:layout-dashboard',
action: async (deesMobileNav) => { action: async (deesMobileNav) => {
alert('test'); console.log('Navigate to dashboard');
return null;
},
},
{
name: 'Profile',
iconName: 'lucide:user',
action: async (deesMobileNav) => {
console.log('Navigate to profile');
return null;
},
},
{
name: 'Settings',
iconName: 'lucide:settings',
action: async (deesMobileNav) => {
console.log('Navigate to settings');
return null;
},
},
{ divider: true } as any,
{
name: 'Help',
iconName: 'lucide:help-circle',
action: async (deesMobileNav) => {
console.log('Show help');
return null;
},
},
{
name: 'Sign Out',
iconName: 'lucide:log-out',
action: async (deesMobileNav) => {
console.log('Sign out');
return null; return null;
}, },
}, },
]); ]);
}}></dees-button> }}>Open Mobile Navigation</dees-button>
`; `;
private static singletonRef: DeesMobilenavigation; private static singletonRef: DeesMobilenavigation;
@ -44,15 +81,18 @@ export class DeesMobilenavigation extends DeesElement {
// INSTANCE // INSTANCE
@property({ @property({
type: Array, type: String,
}) })
public heading: string = `MENU`; public heading: string = `Menu`;
@property({ @property({
type: Array, type: Array,
}) })
public menuItems: plugins.tsclass.website.IMenuItem[] = []; public menuItems: plugins.tsclass.website.IMenuItem[] = [];
@state()
private mobileNavZIndex: number = 1000;
readyDeferred: plugins.smartpromise.Deferred<any> = domtools.plugins.smartpromise.defer(); readyDeferred: plugins.smartpromise.Deferred<any> = domtools.plugins.smartpromise.defer();
constructor() { constructor() {
@ -74,25 +114,32 @@ export class DeesMobilenavigation extends DeesElement {
cssManager.defaultStyles, cssManager.defaultStyles,
css` css`
:host { :host {
font-family: ${cssGeistFontFamily};
} }
.main { .main {
transition: all 0.3s cubic-bezier(0.22, 1, 0.36, 1); transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
will-change: transform; will-change: transform;
position: fixed; position: fixed;
height: 100vh; height: 100vh;
min-width: 280px; width: 100%;
transform: translateX(200px); max-width: 320px;
color: ${cssManager.bdTheme('#333', '#fff')}; transform: translateX(100%);
z-index: ${zIndexLayers.fixed.mobileNav}; color: ${cssManager.bdTheme('#09090b', '#fafafa')};
z-index: var(--z-index);
opacity: 0; opacity: 0;
padding: 16px 32px;
right: 0px; right: 0px;
top: 0px; top: 0px;
bottom: 0px; bottom: 0px;
background: ${cssManager.bdTheme('#eeeeeb', '#000')}; background: ${cssManager.bdTheme('#ffffff', '#09090b')};
border-left: 1px solid ${cssManager.bdTheme('#eeeeeb', '#222')}; border-left: 1px solid ${cssManager.bdTheme('#e5e7eb', '#27272a')};
pointer-events: none; pointer-events: none;
box-shadow: ${cssManager.bdTheme(
'-20px 0 25px -5px rgba(0, 0, 0, 0.1), -10px 0 10px -5px rgba(0, 0, 0, 0.04)',
'-20px 0 25px -5px rgba(0, 0, 0, 0.3), -10px 0 10px -5px rgba(0, 0, 0, 0.2)'
)};
display: flex;
flex-direction: column;
} }
.main.show { .main.show {
@ -101,47 +148,151 @@ export class DeesMobilenavigation extends DeesElement {
opacity: 1; opacity: 1;
} }
.menuItem { .header {
text-align: left; padding: 24px;
padding: 8px; border-bottom: 1px solid ${cssManager.bdTheme('#e5e7eb', '#27272a')};
margin-left: -8px;
margin-right: -8px;
border-radius: 3px;
}
.menuItem:hover {
background: ${cssManager.bdTheme('#CCC', '#333')};;
} }
.heading { .heading {
text-align: left; font-size: 18px;
font-size: 24px; font-weight: 600;
padding: 8px 0px; letter-spacing: -0.02em;
font-family: 'Geist Sans', sans-serif; color: ${cssManager.bdTheme('#09090b', '#fafafa')};
font-weight: 300; margin: 0;
border-bottom: 1px dashed #444; }
margin-top: 16px;
margin-bottom: 16px; .menu-container {
flex: 1;
overflow-y: auto;
padding: 8px;
}
.menuItem {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 16px;
margin-bottom: 2px;
border-radius: 6px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.15s ease;
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
position: relative;
user-select: none;
}
.menuItem:hover {
background: ${cssManager.bdTheme('#f4f4f5', '#27272a')};
color: ${cssManager.bdTheme('#09090b', '#fafafa')};
}
.menuItem:active {
background: ${cssManager.bdTheme('#e5e7eb', '#3f3f46')};
transform: scale(0.98);
}
.menuItem dees-icon {
flex-shrink: 0;
color: ${cssManager.bdTheme('#71717a', '#71717a')};
transition: color 0.15s ease;
}
.menuItem:hover dees-icon {
color: ${cssManager.bdTheme('#09090b', '#fafafa')};
}
.menuItem-text {
flex: 1;
letter-spacing: -0.01em;
}
.menuItem-divider {
height: 1px;
background: ${cssManager.bdTheme('#e5e7eb', '#27272a')};
margin: 8px 16px;
}
/* Mobile responsiveness */
@media (max-width: 400px) {
.main {
max-width: 100vw;
width: 85vw;
}
}
/* Animation for menu items */
@keyframes slideInRight {
from {
opacity: 0;
transform: translateX(20px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
.main.show .menuItem {
animation: slideInRight 0.3s ease-out forwards;
animation-delay: calc(var(--item-index, 0) * 0.05s);
opacity: 0;
}
/* Scrollbar styling */
.menu-container::-webkit-scrollbar {
width: 6px;
}
.menu-container::-webkit-scrollbar-track {
background: transparent;
}
.menu-container::-webkit-scrollbar-thumb {
background: ${cssManager.bdTheme('#e5e7eb', '#3f3f46')};
border-radius: 3px;
}
.menu-container::-webkit-scrollbar-thumb:hover {
background: ${cssManager.bdTheme('#d1d5db', '#52525b')};
} }
`, `,
]; ];
public render() { public render() {
return html` return html`
<style>
.main {
--z-index: ${this.mobileNavZIndex};
}
</style>
<div class="main"> <div class="main">
<div class="heading">${this.heading}</div> <div class="header">
${this.menuItems.map((menuItem) => { <h2 class="heading">${this.heading}</h2>
return html` </div>
<div <div class="menu-container">
class="menuItem" ${this.menuItems.map((menuItem, index) => {
@click="${() => { if ('divider' in menuItem && menuItem.divider) {
this.hide(); return html`<div class="menuItem-divider"></div>`;
menuItem.action(this); }
}}" return html`
> <div
${menuItem.name} class="menuItem"
</div> style="--item-index: ${index}"
`; @click="${() => {
})} this.hide();
menuItem.action(this);
}}"
>
${menuItem.iconName ? html`
<dees-icon .icon=${menuItem.iconName} size="20"></dees-icon>
` : ''}
<span class="menuItem-text">${menuItem.name}</span>
</div>
`;
})}
</div>
</div> </div>
`; `;
} }
@ -154,18 +305,25 @@ export class DeesMobilenavigation extends DeesElement {
public async show() { public async show() {
const domtools = await this.domtoolsPromise; const domtools = await this.domtoolsPromise;
const main = this.shadowRoot.querySelector('.main'); const main = this.shadowRoot.querySelector('.main');
// Create window layer first (it will get its own z-index)
if (!this.windowLayer) { if (!this.windowLayer) {
this.windowLayer = new DeesWindowLayer(); this.windowLayer = await DeesWindowLayer.createAndShow({
this.windowLayer.options.blur = true; blur: true,
});
this.windowLayer.addEventListener('click', () => { this.windowLayer.addEventListener('click', () => {
this.hide(); this.hide();
}); });
} else {
document.body.append(this.windowLayer);
await this.windowLayer.show();
} }
document.body.append(this.windowLayer);
await domtools.convenience.smartdelay.delayFor(0); // Get z-index for mobile nav (will be above window layer)
this.windowLayer.show(); this.mobileNavZIndex = zIndexRegistry.getNextZIndex();
zIndexRegistry.register(this, this.mobileNavZIndex);
await domtools.convenience.smartdelay.delayFor(0); await domtools.convenience.smartdelay.delayFor(10);
main.classList.add('show'); main.classList.add('show');
} }
@ -176,10 +334,23 @@ export class DeesMobilenavigation extends DeesElement {
const domtools = await this.domtoolsPromise; const domtools = await this.domtoolsPromise;
const main = this.shadowRoot.querySelector('.main'); const main = this.shadowRoot.querySelector('.main');
main.classList.remove('show'); main.classList.remove('show');
this.windowLayer.hide();
// Unregister from z-index registry
zIndexRegistry.unregister(this);
if (this.windowLayer) {
await this.windowLayer.destroy();
}
} }
async disconnectedCallback() { async disconnectedCallback() {
document.body.removeChild(this.windowLayer); super.disconnectedCallback();
// Cleanup
zIndexRegistry.unregister(this);
if (this.windowLayer) {
await this.windowLayer.destroy();
}
} }
} }

View File

@ -1,6 +1,7 @@
import * as colors from './00colors.js'; import * as colors from './00colors.js';
import * as plugins from './00plugins.js'; import * as plugins from './00plugins.js';
import { zIndexLayers } from './00zindex.js'; import { zIndexLayers, zIndexRegistry } from './00zindex.js';
import { cssGeistFontFamily } from './00fonts.js';
import { demoFunc } from './dees-modal.demo.js'; import { demoFunc } from './dees-modal.demo.js';
import { import {
@ -42,6 +43,7 @@ export class DeesModal extends DeesElement {
showCloseButton?: boolean; showCloseButton?: boolean;
showHelpButton?: boolean; showHelpButton?: boolean;
onHelp?: () => void | Promise<void>; onHelp?: () => void | Promise<void>;
mobileFullscreen?: boolean;
}) { }) {
const body = document.body; const body = document.body;
const modal = new DeesModal(); const modal = new DeesModal();
@ -54,6 +56,7 @@ export class DeesModal extends DeesElement {
if (optionsArg.showCloseButton !== undefined) modal.showCloseButton = optionsArg.showCloseButton; if (optionsArg.showCloseButton !== undefined) modal.showCloseButton = optionsArg.showCloseButton;
if (optionsArg.showHelpButton !== undefined) modal.showHelpButton = optionsArg.showHelpButton; if (optionsArg.showHelpButton !== undefined) modal.showHelpButton = optionsArg.showHelpButton;
if (optionsArg.onHelp) modal.onHelp = optionsArg.onHelp; if (optionsArg.onHelp) modal.onHelp = optionsArg.onHelp;
if (optionsArg.mobileFullscreen !== undefined) modal.mobileFullscreen = optionsArg.mobileFullscreen;
modal.windowLayer = await DeesWindowLayer.createAndShow({ modal.windowLayer = await DeesWindowLayer.createAndShow({
blur: true, blur: true,
}); });
@ -62,6 +65,11 @@ export class DeesModal extends DeesElement {
}); });
body.append(modal.windowLayer); body.append(modal.windowLayer);
body.append(modal); body.append(modal);
// Get z-index for modal (should be above window layer)
modal.modalZIndex = zIndexRegistry.getNextZIndex();
zIndexRegistry.register(modal, modal.modalZIndex);
return modal; return modal;
} }
@ -95,6 +103,12 @@ export class DeesModal extends DeesElement {
@property({ attribute: false }) @property({ attribute: false })
public onHelp: () => void | Promise<void>; public onHelp: () => void | Promise<void>;
@property({ type: Boolean })
public mobileFullscreen: boolean = false;
@state()
private modalZIndex: number = 1000;
constructor() { constructor() {
super(); super();
@ -104,7 +118,7 @@ export class DeesModal extends DeesElement {
cssManager.defaultStyles, cssManager.defaultStyles,
css` css`
:host { :host {
font-family: 'Geist Sans', sans-serif; font-family: ${cssGeistFontFamily};
color: ${cssManager.bdTheme('#333', '#fff')}; color: ${cssManager.bdTheme('#333', '#fff')};
will-change: transform; will-change: transform;
} }
@ -118,20 +132,23 @@ export class DeesModal extends DeesElement {
box-sizing: border-box; box-sizing: border-box;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
z-index: ${zIndexLayers.overlay.modal};
} }
.modal { .modal {
will-change: transform; will-change: transform;
transform: translateY(0px) scale(0.95); transform: translateY(0px) scale(0.95);
opacity: 0; opacity: 0;
min-height: 120px; min-height: 120px;
background: ${cssManager.bdTheme('#ffffff', '#111')}; max-height: calc(100vh - 40px);
border-radius: 8px; background: ${cssManager.bdTheme('#ffffff', '#09090b')};
border: 1px solid ${cssManager.bdTheme('#e0e0e0', '#333')}; border-radius: 6px;
transition: all 0.2s; border: 1px solid ${cssManager.bdTheme('#e5e7eb', '#27272a')};
transition: all 0.2s ease;
overflow: hidden; overflow: hidden;
box-shadow: ${cssManager.bdTheme('0px 2px 10px rgba(0, 0, 0, 0.1)', '0px 2px 5px rgba(0, 0, 0, 0.5)')}; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1), 0 1px 2px rgba(0, 0, 0, 0.06);
margin: 20px; margin: 20px;
display: flex;
flex-direction: column;
overscroll-behavior: contain;
} }
/* Width variations */ /* Width variations */
@ -158,6 +175,26 @@ export class DeesModal extends DeesElement {
width: calc(100vw - 40px) !important; width: calc(100vw - 40px) !important;
max-width: none !important; max-width: none !important;
} }
/* Allow full height on mobile when content needs it */
.modalContainer {
padding: 10px;
}
.modal {
margin: 10px;
max-height: calc(100vh - 20px);
}
/* Full screen mode on mobile */
.modal.mobile-fullscreen {
width: 100vw !important;
height: 100vh !important;
max-height: 100vh !important;
margin: 0;
border-radius: 0;
border: none;
}
} }
.modal.show { .modal.show {
@ -172,13 +209,15 @@ export class DeesModal extends DeesElement {
.modal .heading { .modal .heading {
height: 40px; height: 40px;
font-family: 'Geist Sans', sans-serif; min-height: 40px;
font-family: ${cssGeistFontFamily};
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
padding: 0 12px; padding: 0 12px;
border-bottom: 1px solid ${cssManager.bdTheme('#e0e0e0', '#333')}; border-bottom: 1px solid ${cssManager.bdTheme('#e5e7eb', '#27272a')};
position: relative; position: relative;
flex-shrink: 0;
} }
.modal .heading .header-buttons { .modal .heading .header-buttons {
@ -194,23 +233,23 @@ export class DeesModal extends DeesElement {
.modal .heading .header-button { .modal .heading .header-button {
width: 28px; width: 28px;
height: 28px; height: 28px;
border-radius: 6px; border-radius: 4px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
cursor: pointer; cursor: pointer;
transition: all 0.2s; transition: all 0.15s ease;
background: transparent; background: transparent;
color: ${cssManager.bdTheme('#666', '#999')}; color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
} }
.modal .heading .header-button:hover { .modal .heading .header-button:hover {
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.08)', 'rgba(255, 255, 255, 0.08)')}; background: ${cssManager.bdTheme('#f4f4f5', '#27272a')};
color: ${cssManager.bdTheme('#333', '#fff')}; color: ${cssManager.bdTheme('#09090b', '#fafafa')};
} }
.modal .heading .header-button:active { .modal .heading .header-button:active {
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.12)', 'rgba(255, 255, 255, 0.12)')}; background: ${cssManager.bdTheme('#e5e7eb', '#3f3f46')};
} }
.modal .heading .header-button dees-icon { .modal .heading .header-button dees-icon {
@ -226,55 +265,65 @@ export class DeesModal extends DeesElement {
font-size: 14px; font-size: 14px;
line-height: 40px; line-height: 40px;
padding: 0 40px; padding: 0 40px;
color: ${cssManager.bdTheme('#09090b', '#fafafa')};
} }
.modal .content { .modal .content {
padding: 16px; padding: 16px;
flex: 1;
overflow-y: auto;
overflow-x: hidden;
overscroll-behavior: contain;
} }
.modal .bottomButtons { .modal .bottomButtons {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
border-top: 1px solid ${cssManager.bdTheme('#e0e0e0', '#333')}; border-top: 1px solid ${cssManager.bdTheme('#e5e7eb', '#27272a')};
justify-content: flex-end; justify-content: flex-end;
gap: 8px; gap: 8px;
padding: 8px; padding: 8px;
flex-shrink: 0;
} }
.modal .bottomButtons .bottomButton { .modal .bottomButtons .bottomButton {
padding: 8px 16px; padding: 8px 16px;
border-radius: 6px; border-radius: 4px;
line-height: 16px; line-height: 16px;
text-align: center; text-align: center;
font-size: 14px; font-size: 14px;
font-weight: 500; font-weight: 500;
cursor: pointer; cursor: pointer;
user-select: none; user-select: none;
transition: all 0.2s; transition: all 0.15s ease;
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.05)', 'rgba(255, 255, 255, 0.05)')}; background: ${cssManager.bdTheme('#ffffff', '#27272a')};
border: 1px solid ${cssManager.bdTheme('#e5e7eb', '#3f3f46')};
color: ${cssManager.bdTheme('#09090b', '#fafafa')};
white-space: nowrap; white-space: nowrap;
} }
.modal .bottomButtons .bottomButton:hover { .modal .bottomButtons .bottomButton:hover {
background: ${cssManager.bdTheme(colors.bright.blue, colors.dark.blue)}; background: ${cssManager.bdTheme('#f4f4f5', '#3f3f46')};
color: #ffffff; border-color: ${cssManager.bdTheme('#d1d5db', '#52525b')};
} }
.modal .bottomButtons .bottomButton:active { .modal .bottomButtons .bottomButton:active {
background: ${cssManager.bdTheme(colors.bright.blueActive, colors.dark.blueActive)}; background: ${cssManager.bdTheme('#e5e7eb', '#52525b')};
color: #ffffff;
} }
.modal .bottomButtons .bottomButton:last-child { .modal .bottomButtons .bottomButton:last-child {
border-right: none; border-right: none;
} }
.modal .bottomButtons .bottomButton.primary { .modal .bottomButtons .bottomButton.primary {
background: ${cssManager.bdTheme(colors.bright.blue, colors.dark.blue)}; background: ${cssManager.bdTheme('#3b82f6', '#3b82f6')};
border-color: ${cssManager.bdTheme('#3b82f6', '#3b82f6')};
color: #ffffff; color: #ffffff;
} }
.modal .bottomButtons .bottomButton.primary:hover { .modal .bottomButtons .bottomButton.primary:hover {
background: ${cssManager.bdTheme(colors.bright.blueActive, colors.dark.blueActive)}; background: ${cssManager.bdTheme('#2563eb', '#2563eb')};
border-color: ${cssManager.bdTheme('#2563eb', '#2563eb')};
} }
.modal .bottomButtons .bottomButton.primary:active { .modal .bottomButtons .bottomButton.primary:active {
background: ${cssManager.bdTheme(colors.bright.blueMuted, colors.dark.blueMuted)}; background: ${cssManager.bdTheme('#1d4ed8', '#1d4ed8')};
border-color: ${cssManager.bdTheme('#1d4ed8', '#1d4ed8')};
} }
`, `,
]; ];
@ -284,6 +333,7 @@ export class DeesModal extends DeesElement {
const customWidth = typeof this.width === 'number' ? `${this.width}px` : ''; const customWidth = typeof this.width === 'number' ? `${this.width}px` : '';
const maxWidthStyle = this.maxWidth ? `${this.maxWidth}px` : ''; const maxWidthStyle = this.maxWidth ? `${this.maxWidth}px` : '';
const minWidthStyle = this.minWidth ? `${this.minWidth}px` : ''; const minWidthStyle = this.minWidth ? `${this.minWidth}px` : '';
const mobileFullscreenClass = this.mobileFullscreen ? 'mobile-fullscreen' : '';
return html` return html`
<style> <style>
@ -291,8 +341,8 @@ export class DeesModal extends DeesElement {
${maxWidthStyle ? `.modal { max-width: ${maxWidthStyle}; }` : ''} ${maxWidthStyle ? `.modal { max-width: ${maxWidthStyle}; }` : ''}
${minWidthStyle ? `.modal { min-width: ${minWidthStyle}; }` : ''} ${minWidthStyle ? `.modal { min-width: ${minWidthStyle}; }` : ''}
</style> </style>
<div class="modalContainer" @click=${this.handleOutsideClick}> <div class="modalContainer" @click=${this.handleOutsideClick} style="z-index: ${this.modalZIndex}">
<div class="modal ${widthClass}"> <div class="modal ${widthClass} ${mobileFullscreenClass}">
<div class="heading"> <div class="heading">
<div class="heading-text">${this.heading}</div> <div class="heading-text">${this.heading}</div>
<div class="header-buttons"> <div class="header-buttons">
@ -349,6 +399,9 @@ export class DeesModal extends DeesElement {
await domtools.convenience.smartdelay.delayFor(200); await domtools.convenience.smartdelay.delayFor(200);
document.body.removeChild(this); document.body.removeChild(this);
await this.windowLayer.destroy(); await this.windowLayer.destroy();
// Unregister from z-index registry
zIndexRegistry.unregister(this);
} }
private async handleHelp() { private async handleHelp() {

View File

@ -5,7 +5,7 @@ export const demoFunc = () => html`
${css` ${css`
.demo-background { .demo-background {
padding: 24px; padding: 24px;
background: ${cssManager.bdTheme('#f0f0f0', '#0a0a0a')}; background: ${cssManager.bdTheme('hsl(0 0% 95%)', 'hsl(0 0% 5%)')};
min-height: 100vh; min-height: 100vh;
} }
@ -17,65 +17,156 @@ export const demoFunc = () => html`
gap: 24px; gap: 24px;
} }
.section-title {
font-size: 24px;
font-weight: 700;
margin: 32px 0 16px 0;
color: ${cssManager.bdTheme('hsl(0 0% 9%)', 'hsl(0 0% 95%)')};
letter-spacing: -0.025em;
}
.section-title:first-child {
margin-top: 0;
}
.grid-layout { .grid-layout {
display: grid; display: grid;
grid-template-columns: 1fr 1fr; grid-template-columns: 1fr 1fr;
gap: 24px; gap: 24px;
} }
.grid-3col {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 24px;
}
@media (max-width: 968px) {
.grid-3col {
grid-template-columns: 1fr;
}
}
@media (max-width: 768px) { @media (max-width: 768px) {
.grid-layout { .grid-layout {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
} }
code {
background: ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
padding: 2px 6px;
border-radius: 3px;
font-size: 13px;
}
`} `}
</style> </style>
<div class="demo-background"> <div class="demo-background">
<div class="demo-container"> <div class="demo-container">
<dees-panel .title=${'Panel Component'}> <h2 class="section-title">Default Panels</h2>
<dees-panel .title=${'Panel Component'} .subtitle=${'The default panel variant with shadcn-inspired styling'}>
<p>The panel component automatically follows the theme and provides consistent styling for grouped content.</p> <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> <p>It's perfect for creating sections in your application with proper spacing and borders.</p>
</dees-panel> </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"> <div class="grid-layout">
<dees-panel .title=${'Feature 1'}> <dees-panel .title=${'Feature Overview'} .subtitle=${'Key capabilities'}>
<p>Grid layouts work great with panels for creating dashboards and feature sections.</p> <p>Grid layouts work great with panels for creating dashboards and feature sections.</p>
<dees-button>Action</dees-button> <dees-button>Learn More</dees-button>
</dees-panel> </dees-panel>
<dees-panel .title=${'Feature 2'}> <dees-panel .title=${'Quick Actions'} .subtitle=${'Common tasks'}>
<p>Each panel maintains consistent spacing and styling.</p> <p>Each panel maintains consistent spacing and styling across your application.</p>
<dees-button>Another Action</dees-button> <dees-button>Get Started</dees-button>
</dees-panel> </dees-panel>
</div> </div>
<dees-panel .title=${'Complex Content'}> <h2 class="section-title">Panel Variants</h2>
<h4>Nested Elements</h4>
<p>Panels can contain any type of content:</p> <dees-panel .title=${'Default Variant'} .variant=${'default'}>
<ul> <p>The default variant has a white background, subtle border, and minimal shadow. It's the standard choice for most content.</p>
<li>Text and paragraphs</li> <p>Use <code>variant="default"</code> or omit the variant property.</p>
<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>
<dees-panel .title=${'Outline Variant'} .subtitle=${'Transparent background with border'} .variant=${'outline'}>
<p>The outline variant removes the background color and shadow, keeping only the border.</p>
<p>Use <code>variant="outline"</code> for a lighter visual weight.</p>
</dees-panel>
<dees-panel .title=${'Ghost Variant'} .subtitle=${'Minimal styling for subtle sections'} .variant=${'ghost'}>
<p>The ghost variant has no border or background by default, only showing a subtle background on hover.</p>
<p>Use <code>variant="ghost"</code> for the most minimal appearance.</p>
</dees-panel>
<h2 class="section-title">Panel Sizes</h2>
<div class="grid-3col">
<dees-panel .title=${'Small Panel'} .size=${'sm'}>
<p>Compact padding for dense layouts.</p>
<p>Use <code>size="sm"</code></p>
</dees-panel>
<dees-panel .title=${'Medium Panel'} .size=${'md'}>
<p>Default size with balanced spacing.</p>
<p>Use <code>size="md"</code> or omit.</p>
</dees-panel>
<dees-panel .title=${'Large Panel'} .size=${'lg'}>
<p>Generous padding for prominent sections.</p>
<p>Use <code>size="lg"</code></p>
</dees-panel>
</div>
<h2 class="section-title">Complex Examples</h2>
<dees-panel .title=${'Form Example'} .subtitle=${'Panels work great for organizing form sections'}>
<dees-form>
<dees-input-text .label=${'Project Name'} .required=${true}></dees-input-text>
<dees-input-text .label=${'Description'} .inputType=${'textarea'}></dees-input-text>
<dees-input-dropdown
.label=${'Category'}
.options=${[
{ option: 'Web Development', key: 'web' },
{ option: 'Mobile App', key: 'mobile' },
{ option: 'Desktop Software', key: 'desktop' }
]}
></dees-input-dropdown>
<dees-form-submit>Create Project</dees-form-submit>
</dees-form>
</dees-panel>
<dees-panel .title=${'Nested Panels'} .subtitle=${'Panels can be nested for hierarchical organization'}>
<p>You can nest panels to create more complex layouts:</p>
<dees-panel .title=${'Nested Panel 1'} .variant=${'outline'} .size=${'sm'}>
<p>This is a nested panel with outline variant and small size.</p>
</dees-panel>
<dees-panel .title=${'Nested Panel 2'} .variant=${'ghost'} .size=${'sm'}>
<p>This is another nested panel with ghost variant.</p>
</dees-panel>
</dees-panel>
<h2 class="section-title">Untitled Panels</h2>
<dees-panel> <dees-panel>
<p>Panels work great even without a title for simple content grouping.</p> <p>Panels work great even without a title for simple content grouping.</p>
<p>They provide visual separation and consistent padding.</p> <p>They provide visual separation and consistent padding throughout your interface.</p>
</dees-panel> </dees-panel>
<div class="grid-layout">
<dees-panel .variant=${'outline'}>
<h4 style="margin-top: 0;">Custom Content</h4>
<p>You can add your own headings and structure within untitled panels.</p>
</dees-panel>
<dees-panel .variant=${'ghost'}>
<h4 style="margin-top: 0;">Minimal Style</h4>
<p>Ghost panels without titles create very subtle content sections.</p>
</dees-panel>
</div>
</div> </div>
</div> </div>
`; `;

View File

@ -8,6 +8,7 @@ import {
type TemplateResult, type TemplateResult,
} from '@design.estate/dees-element'; } from '@design.estate/dees-element';
import { demoFunc } from './dees-panel.demo.js'; import { demoFunc } from './dees-panel.demo.js';
import { cssGeistFontFamily } from './00fonts.js';
declare global { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {
@ -25,33 +26,113 @@ export class DeesPanel extends DeesElement {
@property({ type: String }) @property({ type: String })
public subtitle: string = ''; public subtitle: string = '';
@property({ type: String })
public variant: 'default' | 'outline' | 'ghost' = 'default';
@property({ type: String })
public size: 'sm' | 'md' | 'lg' = 'md';
@property({ attribute: false })
public runAfterRender?: (elementArg: HTMLElement) => void | Promise<void>;
public static styles = [ public static styles = [
cssManager.defaultStyles, cssManager.defaultStyles,
css` css`
:host { :host {
display: block; display: block;
background: ${cssManager.bdTheme('#ffffff', '#1a1a1a')}; font-family: ${cssGeistFontFamily};
border-radius: 8px; background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(0 0% 3.9%)')};
border-radius: 6px;
padding: 24px; 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('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
border: 1px solid ${cssManager.bdTheme('rgba(0,0,0,0.1)', 'rgba(255,255,255,0.1)')}; transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
}
/* Variant: default */
:host([variant="default"]) {
box-shadow: 0 1px 2px 0 hsl(0 0% 0% / 0.05);
}
/* Variant: outline */
:host([variant="outline"]) {
background: transparent;
box-shadow: none;
}
/* Variant: ghost */
:host([variant="ghost"]) {
background: transparent;
border-color: transparent;
box-shadow: none;
padding: 16px;
}
/* Size variations */
:host([size="sm"]) {
padding: 16px;
}
:host([size="lg"]) {
padding: 32px;
}
.header {
margin-bottom: 16px;
}
.header:empty {
display: none;
} }
.title { .title {
margin: 0 0 16px 0; margin: 0;
font-size: 18px; font-size: 18px;
font-weight: 500; font-weight: 600;
color: ${cssManager.bdTheme('#0069f2', '#0099ff')}; color: ${cssManager.bdTheme('hsl(0 0% 9%)', 'hsl(0 0% 95%)')};
letter-spacing: -0.025em;
line-height: 1.5;
}
/* Title size variations */
:host([size="sm"]) .title {
font-size: 16px;
}
:host([size="lg"]) .title {
font-size: 20px;
} }
.subtitle { .subtitle {
margin: -12px 0 16px 0; margin: 4px 0 0 0;
font-size: 14px; font-size: 14px;
color: ${cssManager.bdTheme('#666', '#999')}; color: ${cssManager.bdTheme('hsl(215.4 16.3% 56.9%)', 'hsl(215 20.2% 55.1%)')};
letter-spacing: -0.006em;
line-height: 1.5;
}
/* Subtitle size variations */
:host([size="sm"]) .subtitle {
font-size: 13px;
}
:host([size="lg"]) .subtitle {
font-size: 15px;
margin-top: 6px;
} }
.content { .content {
color: ${cssManager.bdTheme('#333', '#ccc')}; color: ${cssManager.bdTheme('hsl(215.3 25% 26.7%)', 'hsl(217.9 10.6% 84.9%)')};
font-size: 14px;
line-height: 1.6;
}
/* Content size variations */
:host([size="sm"]) .content {
font-size: 13px;
}
:host([size="lg"]) .content {
font-size: 15px;
} }
/* Remove margins from first and last children */ /* Remove margins from first and last children */
@ -62,16 +143,57 @@ export class DeesPanel extends DeesElement {
.content ::slotted(*:last-child) { .content ::slotted(*:last-child) {
margin-bottom: 0; margin-bottom: 0;
} }
/* Interactive states for default variant */
:host([variant="default"]:hover) {
border-color: ${cssManager.bdTheme('hsl(0 0% 79.8%)', 'hsl(0 0% 20.9%)')};
box-shadow: 0 4px 6px -1px hsl(0 0% 0% / 0.1), 0 2px 4px -2px hsl(0 0% 0% / 0.1);
}
/* Interactive states for outline variant */
:host([variant="outline"]:hover) {
border-color: ${cssManager.bdTheme('hsl(0 0% 79.8%)', 'hsl(0 0% 20.9%)')};
background: ${cssManager.bdTheme('hsl(0 0% 98%)', 'hsl(0 0% 7.8%)')};
}
/* Interactive states for ghost variant */
:host([variant="ghost"]:hover) {
background: ${cssManager.bdTheme('hsl(0 0% 95.1%)', 'hsl(0 0% 14.9%)')};
}
/* Focus states */
:host(:focus-within) {
outline: none;
border-color: ${cssManager.bdTheme('hsl(222.2 47.4% 51.2%)', 'hsl(217.2 91.2% 59.8%)')};
box-shadow: 0 0 0 3px ${cssManager.bdTheme('hsl(222.2 47.4% 51.2% / 0.1)', 'hsl(217.2 91.2% 59.8% / 0.1)')};
}
/* Nested panels spacing */
::slotted(dees-panel) {
margin-top: 16px;
}
::slotted(dees-panel:first-child) {
margin-top: 0;
}
`, `,
]; ];
public render(): TemplateResult { public render(): TemplateResult {
return html` return html`
${this.title ? html`<h3 class="title">${this.title}</h3>` : ''} <div class="header">
${this.subtitle ? html`<p class="subtitle">${this.subtitle}</p>` : ''} ${this.title ? html`<h3 class="title">${this.title}</h3>` : ''}
${this.subtitle ? html`<p class="subtitle">${this.subtitle}</p>` : ''}
</div>
<div class="content"> <div class="content">
<slot></slot> <slot></slot>
</div> </div>
`; `;
} }
public async firstUpdated() {
if (this.runAfterRender) {
await this.runAfterRender(this);
}
}
} }

View File

@ -0,0 +1,332 @@
import { html, css, cssManager } from '@design.estate/dees-element';
import '@design.estate/dees-wcctools/demotools';
import './dees-panel.js';
import type { DeesShoppingProductcard } from './dees-shopping-productcard.js';
export const demoFunc = () => html`
<dees-demowrapper>
<style>
${css`
.demo-container {
display: flex;
flex-direction: column;
gap: 24px;
padding: 24px;
max-width: 1200px;
margin: 0 auto;
}
.product-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 20px;
}
.cart-summary {
margin-top: 24px;
padding: 20px;
background: ${cssManager.bdTheme('hsl(210 40% 96.1%)', 'hsl(215 20.2% 16.8%)')};
border: 1px solid ${cssManager.bdTheme('hsl(214.3 31.8% 91.4%)', 'hsl(215 20.2% 21.8%)')};
border-radius: 8px;
}
.cart-summary-title {
font-size: 18px;
font-weight: 600;
margin-bottom: 16px;
color: ${cssManager.bdTheme('hsl(0 0% 9%)', 'hsl(0 0% 95%)')};
}
.cart-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 0;
font-size: 14px;
color: ${cssManager.bdTheme('hsl(215.3 25% 26.7%)', 'hsl(217.9 10.6% 74.9%)')};
}
.cart-total {
display: flex;
justify-content: space-between;
align-items: center;
padding-top: 16px;
margin-top: 16px;
border-top: 2px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
font-size: 18px;
font-weight: 600;
color: ${cssManager.bdTheme('hsl(0 0% 9%)', 'hsl(0 0% 95%)')};
}
.selected-products {
padding: 16px;
background: ${cssManager.bdTheme('hsl(210 40% 96.1%)', 'hsl(215 20.2% 16.8%)')};
border-radius: 6px;
font-size: 14px;
color: ${cssManager.bdTheme('hsl(215.3 25% 26.7%)', 'hsl(217.9 10.6% 74.9%)')};
}
`}
</style>
<div class="demo-container">
<dees-panel .title=${'Basic Product Cards'} .subtitle=${'Simple product display with various configurations'}>
<div class="product-grid">
<dees-shopping-productcard
.productData=${{
name: 'Wireless Bluetooth Headphones',
category: 'Audio',
description: 'Premium sound quality with active noise cancellation',
price: 149.99,
originalPrice: 199.99,
iconName: 'lucide:headphones'
}}
.quantity=${1}
></dees-shopping-productcard>
<dees-shopping-productcard
.productData=${{
name: 'Smart Watch Series 7',
category: 'Wearables',
description: 'Track your fitness and stay connected on the go',
price: 399.00,
iconName: 'lucide:watch'
}}
.quantity=${1}
></dees-shopping-productcard>
<dees-shopping-productcard
.productData=${{
name: 'USB-C Hub',
category: 'Accessories',
price: 49.99,
iconName: 'lucide:usb',
inStock: false
}}
.quantity=${0}
></dees-shopping-productcard>
</div>
</dees-panel>
<dees-panel .title=${'Interactive Shopping Cart'} .subtitle=${'Product cards with dynamic cart calculation'} .runAfterRender=${async (elementArg: HTMLElement) => {
const products = [
{ id: 'laptop', element: null, data: { name: 'MacBook Pro 14"', category: 'Computers', description: 'M3 Pro chip with 18GB RAM', price: 1999, originalPrice: 2199, iconName: 'lucide:laptop' }},
{ id: 'ipad', element: null, data: { name: 'iPad Air', category: 'Tablets', description: '10.9" Liquid Retina display', price: 599, iconName: 'lucide:tablet' }},
{ id: 'keyboard', element: null, data: { name: 'Magic Keyboard', category: 'Accessories', description: 'Wireless keyboard with Touch ID', price: 149, iconName: 'lucide:keyboard' }}
];
const updateCartSummary = () => {
let total = 0;
const items = [];
products.forEach(product => {
const element = elementArg.querySelector(`#${product.id}`) as DeesShoppingProductcard;
if (element && element.quantity > 0) {
const subtotal = product.data.price * element.quantity;
total += subtotal;
items.push(`
<div class="cart-item">
<span>${product.data.name} (${element.quantity})</span>
<span>$${subtotal.toFixed(2)}</span>
</div>
`);
}
});
const summary = elementArg.querySelector('#interactive-cart-summary');
if (summary) {
summary.innerHTML = `
${items.join('')}
${items.length === 0 ? '<div class="cart-item" style="text-align: center; color: #999;">Your cart is empty</div>' : ''}
<div class="cart-total">
<span>Total</span>
<span>$${total.toFixed(2)}</span>
</div>
`;
}
};
// Initial update
setTimeout(updateCartSummary, 100);
// Set up listeners
elementArg.querySelectorAll('dees-shopping-productcard').forEach(card => {
card.addEventListener('quantityChange', updateCartSummary);
});
}}>
<div class="product-grid">
<dees-shopping-productcard
id="laptop"
.productData=${{
name: 'MacBook Pro 14"',
category: 'Computers',
description: 'M3 Pro chip with 18GB RAM',
price: 1999,
originalPrice: 2199,
iconName: 'lucide:laptop'
}}
.quantity=${1}
></dees-shopping-productcard>
<dees-shopping-productcard
id="ipad"
.productData=${{
name: 'iPad Air',
category: 'Tablets',
description: '10.9" Liquid Retina display',
price: 599,
iconName: 'lucide:tablet'
}}
.quantity=${0}
></dees-shopping-productcard>
<dees-shopping-productcard
id="keyboard"
.productData=${{
name: 'Magic Keyboard',
category: 'Accessories',
description: 'Wireless keyboard with Touch ID',
price: 149,
iconName: 'lucide:keyboard'
}}
.quantity=${2}
></dees-shopping-productcard>
</div>
<div class="cart-summary">
<h3 class="cart-summary-title">Shopping Cart</h3>
<div id="interactive-cart-summary">
<!-- Dynamically updated -->
</div>
</div>
</dees-panel>
<dees-panel .title=${'Selectable Product Cards'} .subtitle=${'Click cards or checkboxes to select products'}>
<div class="product-grid">
<dees-shopping-productcard
.productData=${{
name: 'Sony Alpha 7 IV',
category: 'Cameras',
description: 'Full-frame mirrorless camera',
price: 2498,
iconName: 'lucide:camera'
}}
.selectable=${true}
.showQuantitySelector=${false}
@selectionChange=${(e: CustomEvent) => {
const output = document.querySelector('#selection-output');
if (output) {
const selectedCards = document.querySelectorAll('dees-shopping-productcard[selectable]');
const selectedProducts = [];
selectedCards.forEach((card: DeesShoppingProductcard) => {
if (card.selected) {
selectedProducts.push(card.productData.name);
}
});
output.textContent = selectedProducts.length > 0
? `Selected: ${selectedProducts.join(', ')}`
: 'No products selected';
}
}}
></dees-shopping-productcard>
<dees-shopping-productcard
.productData=${{
name: 'DJI Mini 3 Pro',
category: 'Drones',
description: 'Lightweight drone with 4K camera',
price: 759,
iconName: 'lucide:plane'
}}
.selectable=${true}
.showQuantitySelector=${false}
@selectionChange=${(e: CustomEvent) => {
const output = document.querySelector('#selection-output');
if (output) {
const selectedCards = document.querySelectorAll('dees-shopping-productcard[selectable]');
const selectedProducts = [];
selectedCards.forEach((card: DeesShoppingProductcard) => {
if (card.selected) {
selectedProducts.push(card.productData.name);
}
});
output.textContent = selectedProducts.length > 0
? `Selected: ${selectedProducts.join(', ')}`
: 'No products selected';
}
}}
></dees-shopping-productcard>
<dees-shopping-productcard
.productData=${{
name: 'GoPro HERO12',
category: 'Action Cameras',
description: '5.3K video with HyperSmooth 6.0',
price: 399,
originalPrice: 449,
iconName: 'lucide:video'
}}
.selectable=${true}
.showQuantitySelector=${false}
@selectionChange=${(e: CustomEvent) => {
const output = document.querySelector('#selection-output');
if (output) {
const selectedCards = document.querySelectorAll('dees-shopping-productcard[selectable]');
const selectedProducts = [];
selectedCards.forEach((card: DeesShoppingProductcard) => {
if (card.selected) {
selectedProducts.push(card.productData.name);
}
});
output.textContent = selectedProducts.length > 0
? `Selected: ${selectedProducts.join(', ')}`
: 'No products selected';
}
}}
></dees-shopping-productcard>
</div>
<div class="selected-products" id="selection-output" style="margin-top: 16px;">
No products selected
</div>
</dees-panel>
<dees-panel .title=${'Product Variations'} .subtitle=${'Different states and configurations'}>
<div class="product-grid">
<dees-shopping-productcard
.productData=${{
name: 'Limited Edition Sneakers',
category: 'Footwear',
description: 'Exclusive colorway - Only 500 pairs',
price: 299,
iconName: 'lucide:footprints',
inStock: false,
stockText: 'Sold Out'
}}
.quantity=${0}
></dees-shopping-productcard>
<dees-shopping-productcard
.productData=${{
name: 'Minimalist Wallet',
price: 39.99,
iconName: 'lucide:wallet'
}}
.quantity=${1}
></dees-shopping-productcard>
<dees-shopping-productcard
.productData=${{
name: 'Premium Coffee Beans',
category: 'Food & Beverage',
description: 'Single origin, medium roast',
price: 18.50,
iconName: 'lucide:coffee',
currency: '€'
}}
.quantity=${2}
></dees-shopping-productcard>
</div>
</dees-panel>
</div>
</dees-demowrapper>
`;

View File

@ -0,0 +1,335 @@
import {
customElement,
property,
html,
css,
cssManager,
type TemplateResult,
DeesElement,
} from '@design.estate/dees-element';
import { demoFunc } from './dees-shopping-productcard.demo.js';
declare global {
interface HTMLElementTagNameMap {
'dees-shopping-productcard': DeesShoppingProductcard;
}
}
export interface IProductData {
name: string;
category?: string;
description?: string;
price: number;
originalPrice?: number;
currency?: string;
inStock?: boolean;
stockText?: string;
imageUrl?: string;
iconName?: string;
}
@customElement('dees-shopping-productcard')
export class DeesShoppingProductcard extends DeesElement {
public static demo = demoFunc;
@property({ type: Object })
public productData: IProductData = {
name: 'Product Name',
price: 0,
};
@property({ type: Number })
public quantity: number = 0;
@property({ type: Boolean })
public showQuantitySelector: boolean = true;
@property({ type: Boolean })
public selectable: boolean = false;
@property({ type: Boolean })
public selected: boolean = false;
public static styles = [
cssManager.defaultStyles,
css`
:host {
display: block;
}
.product-card {
background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(215 20.2% 11.8%)')};
border: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
border-radius: 8px;
overflow: hidden;
transition: all 0.2s ease;
display: flex;
flex-direction: column;
height: 100%;
position: relative;
}
.product-card:hover {
border-color: ${cssManager.bdTheme('hsl(0 0% 79.8%)', 'hsl(0 0% 20.9%)')};
box-shadow: 0 4px 6px -1px hsl(0 0% 0% / 0.1), 0 2px 4px -2px hsl(0 0% 0% / 0.1);
}
.product-card.selectable {
cursor: pointer;
}
.product-card.selected {
border-color: ${cssManager.bdTheme('hsl(217.2 91.2% 59.8%)', 'hsl(213.1 93.9% 67.8%)')};
box-shadow: 0 0 0 3px ${cssManager.bdTheme('hsl(217.2 91.2% 59.8% / 0.1)', 'hsl(213.1 93.9% 67.8% / 0.1)')};
}
.product-image {
width: 100%;
height: 180px;
background: ${cssManager.bdTheme('hsl(210 40% 96.1%)', 'hsl(215 20.2% 16.8%)')};
display: flex;
align-items: center;
justify-content: center;
position: relative;
overflow: hidden;
}
.product-image img {
width: 100%;
height: 100%;
object-fit: cover;
}
.product-image dees-icon {
font-size: 48px;
color: ${cssManager.bdTheme('hsl(215 20.2% 65.1%)', 'hsl(215 20.2% 35.1%)')};
}
.selection-checkbox {
position: absolute;
top: 12px;
right: 12px;
width: 20px;
height: 20px;
background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(215 30% 6.8%)')};
border: 2px solid ${cssManager.bdTheme('hsl(215 20.2% 65.1%)', 'hsl(215 20.2% 35.1%)')};
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
cursor: pointer;
}
.selection-checkbox.checked {
background: ${cssManager.bdTheme('hsl(217.2 91.2% 59.8%)', 'hsl(213.1 93.9% 67.8%)')};
border-color: ${cssManager.bdTheme('hsl(217.2 91.2% 59.8%)', 'hsl(213.1 93.9% 67.8%)')};
}
.selection-checkbox dees-icon {
color: white;
font-size: 12px;
opacity: 0;
transform: scale(0);
transition: all 0.2s ease;
}
.selection-checkbox.checked dees-icon {
opacity: 1;
transform: scale(1);
}
.product-content {
padding: 16px;
display: flex;
flex-direction: column;
gap: 12px;
flex: 1;
}
.product-header {
display: flex;
flex-direction: column;
gap: 4px;
}
.product-category {
font-size: 12px;
font-weight: 500;
color: ${cssManager.bdTheme('hsl(215.4 16.3% 56.9%)', 'hsl(215 20.2% 55.1%)')};
text-transform: uppercase;
letter-spacing: 0.05em;
line-height: 1.3;
}
.product-name {
font-size: 16px;
font-weight: 600;
color: ${cssManager.bdTheme('hsl(0 0% 9%)', 'hsl(0 0% 95%)')};
line-height: 1.4;
}
.product-description {
font-size: 13px;
color: ${cssManager.bdTheme('hsl(215.4 16.3% 46.9%)', 'hsl(215 20.2% 65.1%)')};
line-height: 1.5;
flex: 1;
}
.product-footer {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
padding-top: 12px;
border-top: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
}
.product-price {
display: flex;
flex-direction: column;
gap: 2px;
}
.price-current {
font-size: 20px;
font-weight: 600;
color: ${cssManager.bdTheme('hsl(0 0% 9%)', 'hsl(0 0% 95%)')};
}
.price-original {
font-size: 14px;
color: ${cssManager.bdTheme('hsl(215.4 16.3% 56.9%)', 'hsl(215 20.2% 55.1%)')};
text-decoration: line-through;
}
.stock-status {
display: inline-flex;
align-items: center;
gap: 4px;
font-size: 12px;
margin-top: 8px;
}
.stock-status.in-stock {
color: ${cssManager.bdTheme('hsl(142.1 70.6% 45.3%)', 'hsl(142.1 76.2% 36.3%)')};
}
.stock-status.out-of-stock {
color: ${cssManager.bdTheme('hsl(0 72.2% 50.6%)', 'hsl(0 62.8% 30.6%)')};
}
.stock-status dees-icon {
font-size: 14px;
}
`,
];
public render(): TemplateResult {
const {
name,
category,
description,
price,
originalPrice,
currency = '$',
inStock = true,
stockText = inStock ? 'In Stock' : 'Out of Stock',
imageUrl,
iconName = 'lucide:package',
} = this.productData;
const formatPrice = (value: number) => {
return `${currency}${value.toFixed(2)}`;
};
return html`
<div
class="product-card ${this.selectable ? 'selectable' : ''} ${this.selected ? 'selected' : ''}"
@click=${this.handleCardClick}
>
<div class="product-image">
${imageUrl ? html`
<img src="${imageUrl}" alt="${name}">
` : html`
<dees-icon .icon=${iconName}></dees-icon>
`}
${this.selectable ? html`
<div
class="selection-checkbox ${this.selected ? 'checked' : ''}"
@click=${(e: Event) => {
e.stopPropagation();
this.handleSelectionToggle();
}}
>
<dees-icon .icon=${'lucide:check'}></dees-icon>
</div>
` : ''}
</div>
<div class="product-content">
<div class="product-header">
${category ? html`<div class="product-category">${category}</div>` : ''}
<div class="product-name">${name}</div>
</div>
${description ? html`
<div class="product-description">${description}</div>
` : ''}
<div class="stock-status ${inStock ? 'in-stock' : 'out-of-stock'}">
<dees-icon .icon=${inStock ? 'lucide:check-circle' : 'lucide:x-circle'}></dees-icon>
${stockText}
</div>
<div class="product-footer">
<div class="product-price">
<span class="price-current">${formatPrice(price)}</span>
${originalPrice && originalPrice > price ? html`
<span class="price-original">${formatPrice(originalPrice)}</span>
` : ''}
</div>
${this.showQuantitySelector ? html`
<dees-input-quantityselector
.value=${this.quantity}
@changeSubject=${(e: CustomEvent) => {
this.quantity = e.detail.getValue();
this.dispatchEvent(new CustomEvent('quantityChange', {
detail: {
quantity: this.quantity,
productData: this.productData
},
bubbles: true,
composed: true
}));
}}
></dees-input-quantityselector>
` : ''}
</div>
</div>
</div>
`;
}
private handleCardClick() {
if (this.selectable) {
this.selected = !this.selected;
this.dispatchEvent(new CustomEvent('selectionChange', {
detail: {
selected: this.selected,
productData: this.productData
},
bubbles: true,
composed: true
}));
}
}
private handleSelectionToggle() {
this.selected = !this.selected;
this.dispatchEvent(new CustomEvent('selectionChange', {
detail: {
selected: this.selected,
productData: this.productData
},
bubbles: true,
composed: true
}));
}
}

View File

@ -120,9 +120,9 @@ export class DeesSpinner extends DeesElement {
<div class="${this.status}" id="loading"> <div class="${this.status}" id="loading">
${(() => { ${(() => {
if (this.status === 'success') { if (this.status === 'success') {
return html`<dees-icon style="transform: translateX(1%) translateY(3%);" .iconFA=${'circleCheck' as any}></dees-icon>`; return html`<dees-icon style="transform: translateX(1%) translateY(3%);" .icon=${'fa:circle-check'}></dees-icon>`;
} else if (this.status === 'error') { } else if (this.status === 'error') {
return html`<dees-icon .iconFA=${'circleXmark' as any}></dees-icon>`; return html`<dees-icon .icon=${'fa:circle-xmark'}></dees-icon>`;
} }
})()} })()}
</div> </div>

View File

@ -1,389 +1,518 @@
import { html, cssManager } from '@design.estate/dees-element'; import { html, css, cssManager } from '@design.estate/dees-element';
import '@design.estate/dees-wcctools/demotools';
import './dees-panel.js';
import type { IStatsTile } from './dees-statsgrid.js'; import type { IStatsTile } from './dees-statsgrid.js';
export const demoFunc = () => { export const demoFunc = () => {
// Demo data with different tile types
const demoTiles: IStatsTile[] = [
{
id: 'revenue',
title: 'Total Revenue',
value: 125420,
unit: '$',
type: 'number',
icon: 'faDollarSign',
description: '+12.5% from last month',
color: '#22c55e',
actions: [
{
name: 'View Details',
iconName: 'faChartLine',
action: async () => {
console.log('Viewing revenue details for tile:', 'revenue');
console.log('Current value:', 125420);
alert(`Revenue Details: $125,420 (+12.5%)`);
}
},
{
name: 'Export Data',
iconName: 'faFileExport',
action: async () => {
console.log('Exporting revenue data');
alert('Revenue data exported to CSV');
}
}
]
},
{
id: 'users',
title: 'Active Users',
value: 3847,
type: 'number',
icon: 'faUsers',
description: '324 new this week',
actions: [
{
name: 'View User List',
iconName: 'faList',
action: async () => {
console.log('Viewing user list');
}
}
]
},
{
id: 'cpu',
title: 'CPU Usage',
value: 73,
type: 'gauge',
icon: 'faMicrochip',
gaugeOptions: {
min: 0,
max: 100,
thresholds: [
{ value: 0, color: '#22c55e' },
{ value: 60, color: '#f59e0b' },
{ value: 80, color: '#ef4444' }
]
}
},
{
id: 'storage',
title: 'Storage Used',
value: 65,
type: 'percentage',
icon: 'faHardDrive',
description: '650 GB of 1 TB',
color: '#3b82f6'
},
{
id: 'memory',
title: 'Memory Usage',
value: 45,
type: 'gauge',
icon: 'faMemory',
gaugeOptions: {
min: 0,
max: 100,
thresholds: [
{ value: 0, color: '#22c55e' },
{ value: 70, color: '#f59e0b' },
{ value: 90, color: '#ef4444' }
]
}
},
{
id: 'requests',
title: 'API Requests',
value: '1.2k',
unit: '/min',
type: 'trend',
icon: 'faServer',
trendData: [45, 52, 38, 65, 72, 68, 75, 82, 79, 85, 88, 92]
},
{
id: 'uptime',
title: 'System Uptime',
value: '99.95%',
type: 'text',
icon: 'faCheckCircle',
color: '#22c55e',
description: 'Last 30 days'
},
{
id: 'latency',
title: 'Response Time',
value: 142,
unit: 'ms',
type: 'trend',
icon: 'faClock',
trendData: [150, 145, 148, 142, 138, 140, 135, 145, 142],
description: 'P95 latency'
},
{
id: 'errors',
title: 'Error Rate',
value: 0.03,
unit: '%',
type: 'number',
icon: 'faExclamationTriangle',
color: '#ef4444',
actions: [
{
name: 'View Error Logs',
iconName: 'faFileAlt',
action: async () => {
console.log('Viewing error logs');
}
}
]
}
];
// Grid actions for the demo
const gridActions = [
{
name: 'Refresh',
iconName: 'faSync',
action: async () => {
console.log('Refreshing stats...');
// Simulate refresh animation
const grid = document.querySelector('dees-statsgrid');
if (grid) {
grid.style.opacity = '0.5';
setTimeout(() => {
grid.style.opacity = '1';
}, 500);
}
}
},
{
name: 'Export Report',
iconName: 'faFileExport',
action: async () => {
console.log('Exporting stats report...');
}
},
{
name: 'Settings',
iconName: 'faCog',
action: async () => {
console.log('Opening settings...');
}
}
];
return html` return html`
<dees-demowrapper>
<style> <style>
.demo-container { ${css`
padding: 32px; .demo-container {
background: ${cssManager.bdTheme('#f8f9fa', '#0a0a0a')}; display: flex;
min-height: 100vh; flex-direction: column;
} gap: 24px;
padding: 24px;
.demo-section { max-width: 1400px;
margin-bottom: 48px; margin: 0 auto;
} }
.demo-title { dees-panel {
font-size: 24px; margin-bottom: 24px;
font-weight: 600; }
margin-bottom: 16px;
color: ${cssManager.bdTheme('#333', '#fff')}; dees-panel:last-child {
} margin-bottom: 0;
}
.demo-description {
font-size: 14px; .tile-config {
color: ${cssManager.bdTheme('#666', '#aaa')}; display: grid;
margin-bottom: 24px; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
} gap: 16px;
margin-top: 16px;
.theme-toggle { }
position: fixed;
top: 16px; .config-section {
right: 16px; padding: 16px;
padding: 8px 16px; background: ${cssManager.bdTheme('hsl(210 40% 96.1%)', 'hsl(215 20.2% 16.8%)')};
background: ${cssManager.bdTheme('#fff', '#1a1a1a')}; border-radius: 6px;
border: 1px solid ${cssManager.bdTheme('#e0e0e0', '#2a2a2a')}; }
border-radius: 8px;
cursor: pointer; .config-title {
z-index: 100; font-size: 14px;
} font-weight: 600;
margin-bottom: 8px;
color: ${cssManager.bdTheme('hsl(215.3 25% 8.8%)', 'hsl(210 40% 98%)')};
}
.config-description {
font-size: 13px;
color: ${cssManager.bdTheme('hsl(215.4 16.3% 56.9%)', 'hsl(215 20.2% 55.1%)')};
}
.code-block {
background: ${cssManager.bdTheme('hsl(210 40% 96.1%)', 'hsl(215 20.2% 11.8%)')};
border: 1px solid ${cssManager.bdTheme('hsl(214.3 31.8% 91.4%)', 'hsl(215 20.2% 16.8%)')};
border-radius: 6px;
padding: 16px;
font-family: monospace;
font-size: 13px;
overflow-x: auto;
white-space: pre;
}
`}
</style> </style>
<div class="demo-container"> <div class="demo-container">
<button class="theme-toggle" @click=${() => { <dees-panel .title=${'1. Comprehensive Dashboard'} .subtitle=${'Full-featured stats grid with various tile types, actions, and Lucide icons'}>
document.body.classList.toggle('bright');
}}>Toggle Theme</button>
<div class="demo-section">
<h2 class="demo-title">Full Featured Stats Grid</h2>
<p class="demo-description">
A comprehensive dashboard with various tile types, actions, and real-time updates.
</p>
<dees-statsgrid
.tiles=${demoTiles}
.gridActions=${gridActions}
.minTileWidth=${250}
.gap=${16}
></dees-statsgrid>
</div>
<div class="demo-section">
<h2 class="demo-title">Compact Grid (Smaller Tiles)</h2>
<p class="demo-description">
Same data displayed with smaller minimum tile width for more compact layouts.
</p>
<dees-statsgrid
.tiles=${demoTiles.slice(0, 6)}
.minTileWidth=${180}
.gap=${12}
></dees-statsgrid>
</div>
<div class="demo-section">
<h2 class="demo-title">Simple Metrics (No Actions)</h2>
<p class="demo-description">
Clean display without interactive elements for pure visualization.
</p>
<dees-statsgrid <dees-statsgrid
.tiles=${[ .tiles=${[
{ {
id: 'metric1', id: 'revenue',
title: 'Total Sales', title: 'Total Revenue',
value: 48293, value: 125420,
type: 'number',
icon: 'faShoppingCart'
},
{
id: 'metric2',
title: 'Conversion Rate',
value: 3.4,
unit: '%',
type: 'number',
icon: 'faChartLine'
},
{
id: 'metric3',
title: 'Avg Order Value',
value: 127.50,
unit: '$', unit: '$',
type: 'number', type: 'number',
icon: 'faReceipt' icon: 'lucide:dollar-sign',
description: '+12.5% from last month',
actions: [
{
name: 'View Details',
iconName: 'lucide:trending-up',
action: async () => {
const output = document.querySelector('#action-output');
if (output) {
output.textContent = 'Viewing revenue details: $125,420 (+12.5%)';
}
}
},
{
name: 'Export Data',
iconName: 'lucide:download',
action: async () => {
const output = document.querySelector('#action-output');
if (output) {
output.textContent = 'Exporting revenue data to CSV...';
}
}
}
]
}, },
{ {
id: 'metric4', id: 'users',
title: 'Customer Satisfaction', title: 'Active Users',
value: 92, value: 3847,
type: 'percentage', type: 'number',
icon: 'faSmile', icon: 'lucide:users',
color: '#22c55e' description: '324 new this week',
} actions: [
]} {
.minTileWidth=${220} name: 'View User List',
.gap=${16} iconName: 'lucide:list',
></dees-statsgrid> action: async () => {
</div> const output = document.querySelector('#action-output');
if (output) {
<div class="demo-section"> output.textContent = 'Opening user list...';
<h2 class="demo-title">Performance Monitoring</h2> }
<p class="demo-description"> }
Real-time performance metrics with gauge visualizations and thresholds. }
</p> ]
<dees-statsgrid },
.tiles=${[
{ {
id: 'perf1', id: 'cpu',
title: 'Database Load', title: 'CPU Usage',
value: 42, value: 73,
unit: '%',
type: 'gauge', type: 'gauge',
icon: 'faDatabase', icon: 'lucide:cpu',
gaugeOptions: { gaugeOptions: {
min: 0, min: 0,
max: 100, max: 100,
thresholds: [ thresholds: [
{ value: 0, color: '#10b981' }, { value: 0, color: 'hsl(142.1 76.2% 36.3%)' },
{ value: 50, color: '#f59e0b' }, { value: 60, color: 'hsl(45.4 93.4% 47.5%)' },
{ value: 75, color: '#ef4444' } { value: 80, color: 'hsl(0 84.2% 60.2%)' }
] ]
} }
}, },
{ {
id: 'perf2', id: 'storage',
title: 'Network I/O', title: 'Storage Used',
value: 856, value: 65,
unit: 'MB/s',
type: 'trend',
icon: 'faNetworkWired',
trendData: [720, 780, 823, 845, 812, 876, 856]
},
{
id: 'perf3',
title: 'Cache Hit Rate',
value: 94.2,
type: 'percentage', type: 'percentage',
icon: 'faBolt', icon: 'lucide:hard-drive',
color: '#3b82f6' description: '650 GB of 1 TB',
}, },
{ {
id: 'perf4', id: 'latency',
title: 'Active Connections', title: 'Response Time',
value: 1428, value: 142,
type: 'number', unit: 'ms',
icon: 'faLink', type: 'trend',
description: 'Peak: 2,100' icon: 'lucide:activity',
trendData: [150, 145, 148, 142, 138, 140, 135, 145, 142],
description: 'P95'
},
{
id: 'uptime',
title: 'System Uptime',
value: '99.95%',
type: 'text',
icon: 'lucide:check-circle',
color: 'hsl(142.1 76.2% 36.3%)',
description: 'Last 30 days'
} }
]} ]}
.gridActions=${[ .gridActions=${[
{ {
name: 'Auto Refresh', name: 'Refresh',
iconName: 'faPlay', iconName: 'lucide:refresh-cw',
action: async () => { action: async () => {
console.log('Starting auto refresh...'); const grid = document.querySelector('dees-statsgrid');
if (grid) {
grid.style.opacity = '0.5';
setTimeout(() => {
grid.style.opacity = '1';
}, 300);
}
}
},
{
name: 'Export',
iconName: 'lucide:share',
action: async () => {
const output = document.querySelector('#action-output');
if (output) {
output.textContent = 'Exporting dashboard report...';
}
}
},
{
name: 'Settings',
iconName: 'lucide:settings',
action: async () => {
const output = document.querySelector('#action-output');
if (output) {
output.textContent = 'Opening dashboard settings...';
}
} }
} }
]} ]}
.minTileWidth=${250}
.gap=${16}
></dees-statsgrid>
<div id="action-output" style="margin-top: 16px; padding: 12px; background: ${cssManager.bdTheme('hsl(210 40% 96.1%)', 'hsl(215 20.2% 16.8%)')}; border-radius: 6px; font-size: 14px; font-family: monospace; color: ${cssManager.bdTheme('hsl(215.3 25% 8.8%)', 'hsl(210 40% 98%)')};">
<em>Click on tile actions or grid actions to see the result...</em>
</div>
</dees-panel>
<dees-panel .title=${'2. Tile Types'} .subtitle=${'Different visualization types available in the stats grid'}>
<dees-statsgrid
.tiles=${[
{
id: 'number-example',
title: 'Number Tile',
value: 42195,
unit: '$',
type: 'number',
icon: 'lucide:hash',
description: 'Simple numeric display'
},
{
id: 'gauge-example',
title: 'Gauge Tile',
value: 68,
unit: '%',
type: 'gauge',
icon: 'lucide:gauge',
gaugeOptions: {
min: 0,
max: 100,
thresholds: [
{ value: 0, color: 'hsl(142.1 76.2% 36.3%)' },
{ value: 50, color: 'hsl(45.4 93.4% 47.5%)' },
{ value: 80, color: 'hsl(0 84.2% 60.2%)' }
]
}
},
{
id: 'percentage-example',
title: 'Percentage Tile',
value: 78,
type: 'percentage',
icon: 'lucide:percent',
description: 'Progress bar visualization'
},
{
id: 'trend-example',
title: 'Trend Tile',
value: 892,
unit: 'ops/s',
type: 'trend',
icon: 'lucide:trending-up',
trendData: [720, 750, 780, 795, 810, 835, 850, 865, 880, 892],
description: 'avg'
},
{
id: 'text-example',
title: 'Text Tile',
value: 'Operational',
type: 'text',
icon: 'lucide:info',
color: 'hsl(142.1 76.2% 36.3%)',
description: 'Status display'
}
]}
.minTileWidth=${280} .minTileWidth=${280}
.gap=${16}
></dees-statsgrid>
<div class="tile-config">
<div class="config-section">
<div class="config-title">Configuration Options</div>
<div class="config-description">
Each tile type supports different properties:
<ul style="margin: 8px 0; padding-left: 20px;">
<li><strong>Number:</strong> value, unit, color, description</li>
<li><strong>Gauge:</strong> value, unit, gaugeOptions (min, max, thresholds)</li>
<li><strong>Percentage:</strong> value (0-100), color, description</li>
<li><strong>Trend:</strong> value, unit, trendData array, description</li>
<li><strong>Text:</strong> value (string), color, description</li>
</ul>
</div>
</div>
</div>
</dees-panel>
<dees-panel .title=${'3. Grid Configurations'} .subtitle=${'Different layout options and responsive behavior'}>
<h4 style="margin: 0 0 16px 0; font-size: 16px; font-weight: 600;">Compact Layout (180px tiles)</h4>
<dees-statsgrid
.tiles=${[
{ id: '1', title: 'Orders', value: 156, type: 'number', icon: 'lucide:shopping-cart' },
{ id: '2', title: 'Revenue', value: 8420, unit: '$', type: 'number', icon: 'lucide:dollar-sign' },
{ id: '3', title: 'Users', value: 423, type: 'number', icon: 'lucide:users' },
{ id: '4', title: 'Growth', value: 12.5, unit: '%', type: 'number', icon: 'lucide:trending-up', color: 'hsl(142.1 76.2% 36.3%)' }
]}
.minTileWidth=${180}
.gap=${12}
></dees-statsgrid>
<h4 style="margin: 24px 0 16px 0; font-size: 16px; font-weight: 600;">Spacious Layout (320px tiles)</h4>
<dees-statsgrid
.tiles=${[
{
id: 'spacious1',
title: 'Monthly Revenue',
value: 184500,
unit: '$',
type: 'number',
icon: 'lucide:credit-card',
description: 'Total revenue this month'
},
{
id: 'spacious2',
title: 'Customer Satisfaction',
value: 94,
type: 'percentage',
icon: 'lucide:smile',
description: 'Based on 1,234 reviews'
},
{
id: 'spacious3',
title: 'Server Response',
value: 98,
unit: 'ms',
type: 'trend',
icon: 'lucide:server',
trendData: [105, 102, 100, 99, 98, 98, 97, 98],
description: 'avg response time'
}
]}
.minTileWidth=${320}
.gap=${20} .gap=${20}
></dees-statsgrid> ></dees-statsgrid>
</div> </dees-panel>
<script> <dees-panel .title=${'4. Interactive Features'} .subtitle=${'Tiles with actions and real-time updates'}>
// Simulate real-time updates <dees-statsgrid
setInterval(() => { id="interactive-grid"
const grids = document.querySelectorAll('dees-statsgrid'); .tiles=${[
grids.forEach(grid => { {
if (grid.tiles && grid.tiles.length > 0) { id: 'live-cpu',
// Update some random values title: 'Live CPU',
const updatedTiles = [...grid.tiles]; value: 45,
unit: '%',
// Update trends with new data point type: 'gauge',
updatedTiles.forEach(tile => { icon: 'lucide:cpu',
if (tile.type === 'trend' && tile.trendData) { gaugeOptions: {
tile.trendData = [...tile.trendData.slice(1), min: 0,
tile.trendData[tile.trendData.length - 1] + Math.random() * 10 - 5 max: 100,
]; thresholds: [
{ value: 0, color: 'hsl(142.1 76.2% 36.3%)' },
{ value: 60, color: 'hsl(45.4 93.4% 47.5%)' },
{ value: 80, color: 'hsl(0 84.2% 60.2%)' }
]
}
},
{
id: 'live-requests',
title: 'Requests/sec',
value: 892,
type: 'trend',
icon: 'lucide:activity',
trendData: [850, 860, 870, 880, 885, 890, 892]
},
{
id: 'live-memory',
title: 'Memory Usage',
value: 62,
type: 'percentage',
icon: 'lucide:database'
},
{
id: 'counter',
title: 'Event Counter',
value: 0,
type: 'number',
icon: 'lucide:zap',
actions: [
{
name: 'Increment',
iconName: 'lucide:plus',
action: async () => {
const grid = document.querySelector('#interactive-grid') as any;
if (!grid) return;
const tile = grid.tiles.find((t: any) => t.id === 'counter');
tile.value = typeof tile.value === 'number' ? tile.value + 1 : 1;
grid.tiles = [...grid.tiles];
}
},
{
name: 'Reset',
iconName: 'lucide:rotate-ccw',
action: async () => {
const grid = document.querySelector('#interactive-grid') as any;
if (!grid) return;
const tile = grid.tiles.find((t: any) => t.id === 'counter');
tile.value = 0;
grid.tiles = [...grid.tiles];
}
} }
]
// Randomly update some numeric values
if (tile.type === 'number' && Math.random() > 0.7) {
const currentValue = typeof tile.value === 'number' ? tile.value : parseFloat(tile.value);
tile.value = Math.round(currentValue + (Math.random() * 10 - 5));
}
// Update gauge values
if (tile.type === 'gauge' && Math.random() > 0.5) {
const currentValue = typeof tile.value === 'number' ? tile.value : parseFloat(tile.value);
const newValue = currentValue + (Math.random() * 10 - 5);
tile.value = Math.max(tile.gaugeOptions?.min || 0,
Math.min(tile.gaugeOptions?.max || 100, Math.round(newValue)));
}
});
grid.tiles = updatedTiles;
} }
}); ]}
}, 3000); .gridActions=${[
</script> {
name: 'Start Live Updates',
iconName: 'lucide:play',
action: async function() {
// Toggle live updates
if (!(window as any).liveUpdateInterval) {
(window as any).liveUpdateInterval = setInterval(() => {
const grid = document.querySelector('#interactive-grid') as any;
if (grid) {
const tiles = [...grid.tiles];
// Update CPU gauge
const cpuTile = tiles.find(t => t.id === 'live-cpu');
cpuTile.value = Math.max(0, Math.min(100, cpuTile.value + (Math.random() * 20 - 10)));
// Update requests trend
const requestsTile = tiles.find(t => t.id === 'live-requests');
const newValue = requestsTile.value + Math.round(Math.random() * 50 - 25);
requestsTile.value = Math.max(800, newValue);
requestsTile.trendData = [...requestsTile.trendData.slice(1), requestsTile.value];
// Update memory percentage
const memoryTile = tiles.find(t => t.id === 'live-memory');
memoryTile.value = Math.max(0, Math.min(100, memoryTile.value + (Math.random() * 10 - 5)));
grid.tiles = tiles;
}
}, 1000);
this.name = 'Stop Live Updates';
this.iconName = 'lucide:pause';
} else {
clearInterval((window as any).liveUpdateInterval);
(window as any).liveUpdateInterval = null;
this.name = 'Start Live Updates';
this.iconName = 'lucide:play';
}
}
}
]}
.minTileWidth=${250}
.gap=${16}
></dees-statsgrid>
</dees-panel>
<dees-panel .title=${'5. Code Example'} .subtitle=${'How to implement a stats grid with TypeScript'}>
<div class="code-block">${`const tiles: IStatsTile[] = [
{
id: 'revenue',
title: 'Total Revenue',
value: 125420,
unit: '$',
type: 'number',
icon: 'lucide:dollar-sign',
description: '+12.5% from last month',
actions: [
{
name: 'View Details',
iconName: 'lucide:trending-up',
action: async () => {
console.log('View revenue details');
}
}
]
},
{
id: 'cpu',
title: 'CPU Usage',
value: 73,
unit: '%',
type: 'gauge',
icon: 'lucide:cpu',
gaugeOptions: {
min: 0,
max: 100,
thresholds: [
{ value: 0, color: 'hsl(142.1 76.2% 36.3%)' },
{ value: 60, color: 'hsl(45.4 93.4% 47.5%)' },
{ value: 80, color: 'hsl(0 84.2% 60.2%)' }
]
}
}
];
// Render the stats grid
html\`
<dees-statsgrid
.tiles=\${tiles}
.minTileWidth=\${250}
.gap=\${16}
.gridActions=\${[
{
name: 'Refresh',
iconName: 'lucide:refresh-cw',
action: async () => console.log('Refresh')
}
]}
></dees-statsgrid>
\`;`}</div>
</dees-panel>
</div> </div>
<script>
// Cleanup live updates on page unload
window.addEventListener('beforeunload', () => {
if ((window as any).liveUpdateInterval) {
clearInterval((window as any).liveUpdateInterval);
}
});
</script>
</dees-demowrapper>
`; `;
}; };

View File

@ -1,5 +1,6 @@
import { demoFunc } from './dees-statsgrid.demo.js'; import { demoFunc } from './dees-statsgrid.demo.js';
import * as plugins from './00plugins.js'; import * as plugins from './00plugins.js';
import { cssGeistFontFamily } from './00fonts.js';
import { import {
customElement, customElement,
html, html,
@ -79,30 +80,47 @@ export class DeesStatsGrid extends DeesElement {
:host { :host {
display: block; display: block;
width: 100%; width: 100%;
font-family: ${cssGeistFontFamily};
} }
/* CSS Variables for consistent spacing and sizing */
:host {
--grid-gap: 16px;
--tile-padding: 24px;
--header-spacing: 16px;
--content-min-height: 48px;
--value-font-size: 30px;
--unit-font-size: 16px;
--label-font-size: 13px;
--title-font-size: 14px;
--description-spacing: 12px;
--border-radius: 8px;
--transition-duration: 0.15s;
}
/* Grid Layout */
.grid-header { .grid-header {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
margin-bottom: ${unsafeCSS(16)}px; margin-bottom: calc(var(--grid-gap) * 1.5);
min-height: 32px; min-height: 40px;
} }
.grid-title { .grid-title {
font-size: 18px; font-size: 16px;
font-weight: 600; font-weight: 500;
color: ${cssManager.bdTheme('#333', '#fff')}; color: ${cssManager.bdTheme('#09090b', '#fafafa')};
letter-spacing: -0.01em;
} }
.grid-actions { .grid-actions {
display: flex; display: flex;
gap: 8px; gap: 6px;
} }
.grid-actions dees-button { .grid-actions dees-button {
font-size: 14px; font-size: var(--label-font-size);
min-width: auto;
} }
.stats-grid { .stats-grid {
@ -112,86 +130,107 @@ export class DeesStatsGrid extends DeesElement {
width: 100%; width: 100%;
} }
/* Tile Base Styles */
.stats-tile { .stats-tile {
background: ${cssManager.bdTheme('#fff', '#1a1a1a')}; background: ${cssManager.bdTheme('#ffffff', '#09090b')};
border: 1px solid ${cssManager.bdTheme('#e0e0e0', '#2a2a2a')}; border: 1px solid ${cssManager.bdTheme('hsl(214.3 31.8% 91.4%)', 'hsl(215 20.2% 11.8%)')};
border-radius: 12px; border-radius: var(--border-radius);
padding: 20px; padding: var(--tile-padding);
transition: all 0.3s ease; transition: all var(--transition-duration) ease;
cursor: pointer; cursor: default;
position: relative; position: relative;
overflow: hidden; overflow: hidden;
display: flex;
flex-direction: column;
} }
.stats-tile:hover { .stats-tile:hover {
transform: translateY(-2px); background: ${cssManager.bdTheme('hsl(210 40% 98%)', 'hsl(215 20.2% 10.2%)')};
box-shadow: 0 4px 12px ${cssManager.bdTheme('rgba(0,0,0,0.1)', 'rgba(0,0,0,0.3)')}; border-color: ${cssManager.bdTheme('hsl(214.3 31.8% 85%)', 'hsl(215 20.2% 16.8%)')};
border-color: ${cssManager.bdTheme('#d0d0d0', '#3a3a3a')};
} }
.stats-tile.clickable { .stats-tile.clickable {
cursor: pointer; cursor: pointer;
} }
.stats-tile.clickable:hover {
transform: translateY(-1px);
box-shadow: 0 2px 8px ${cssManager.bdTheme('rgba(0,0,0,0.04)', 'rgba(0,0,0,0.2)')};
}
/* Tile Header */
.tile-header { .tile-header {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: flex-start;
margin-bottom: 12px; margin-bottom: var(--header-spacing);
width: 100%; flex-shrink: 0;
} }
.tile-title { .tile-title {
font-size: 14px; font-size: var(--title-font-size);
font-weight: 500; font-weight: 500;
color: ${cssManager.bdTheme('#666', '#aaa')}; color: ${cssManager.bdTheme('hsl(215.4 16.3% 46.9%)', 'hsl(215 20.2% 65.1%)')};
margin: 0; margin: 0;
letter-spacing: -0.01em;
line-height: 1.2;
} }
.tile-icon { .tile-icon {
opacity: 0.6; opacity: 0.7;
color: ${cssManager.bdTheme('hsl(215.4 16.3% 46.9%)', 'hsl(215 20.2% 65.1%)')};
font-size: 16px;
flex-shrink: 0;
} }
/* Tile Content */
.tile-content { .tile-content {
height: 90px; min-height: var(--content-min-height);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: center; justify-content: center;
align-items: center; flex: 1;
position: relative;
} }
.tile-value { .tile-value {
font-size: 32px; font-size: var(--value-font-size);
font-weight: 600; font-weight: 600;
color: ${cssManager.bdTheme('#333', '#fff')}; color: ${cssManager.bdTheme('hsl(215.3 25% 8.8%)', 'hsl(210 40% 98%)')};
line-height: 1.2; line-height: 1.1;
display: flex; display: flex;
align-items: baseline; align-items: baseline;
justify-content: center; gap: 4px;
gap: 6px; letter-spacing: -0.025em;
width: 100%;
} }
.tile-unit { .tile-unit {
font-size: 18px; font-size: var(--unit-font-size);
font-weight: 400; font-weight: 400;
color: ${cssManager.bdTheme('#666', '#aaa')}; color: ${cssManager.bdTheme('hsl(215.4 16.3% 46.9%)', 'hsl(215 20.2% 65.1%)')};
letter-spacing: -0.01em;
} }
.tile-description { .tile-description {
font-size: 12px; font-size: var(--label-font-size);
color: ${cssManager.bdTheme('#888', '#777')}; color: ${cssManager.bdTheme('hsl(215.4 16.3% 56.9%)', 'hsl(215 20.2% 55.1%)')};
margin-top: 8px; margin-top: var(--description-spacing);
letter-spacing: -0.01em;
flex-shrink: 0;
}
/* Gauge Styles */
.gauge-wrapper {
width: 100%;
display: flex;
justify-content: center;
align-items: center;
} }
.gauge-container { .gauge-container {
width: 100%; width: 140px;
height: 80px; height: 80px;
position: relative; position: relative;
display: flex; margin-top: -10px;
align-items: center;
justify-content: center;
} }
.gauge-svg { .gauge-svg {
@ -201,96 +240,134 @@ export class DeesStatsGrid extends DeesElement {
.gauge-background { .gauge-background {
fill: none; fill: none;
stroke: ${cssManager.bdTheme('#e0e0e0', '#2a2a2a')}; stroke: ${cssManager.bdTheme('hsl(214.3 31.8% 91.4%)', 'hsl(215 20.2% 21.8%)')};
stroke-width: 6; stroke-width: 8;
} }
.gauge-fill { .gauge-fill {
fill: none; fill: none;
stroke-width: 6; stroke-width: 8;
stroke-linecap: round; stroke-linecap: round;
transition: stroke-dashoffset 0.5s ease; transition: stroke-dashoffset 0.6s cubic-bezier(0.4, 0, 0.2, 1);
} }
.gauge-text { .gauge-text {
fill: ${cssManager.bdTheme('#333', '#fff')}; fill: ${cssManager.bdTheme('hsl(215.3 25% 8.8%)', 'hsl(210 40% 98%)')};
font-size: 18px; font-family: ${cssGeistFontFamily};
font-size: var(--value-font-size);
font-weight: 600; font-weight: 600;
text-anchor: middle; text-anchor: middle;
letter-spacing: -0.025em;
}
.gauge-unit {
font-size: var(--unit-font-size);
fill: ${cssManager.bdTheme('hsl(215.4 16.3% 46.9%)', 'hsl(215 20.2% 65.1%)')};
font-weight: 400;
font-family: ${cssGeistFontFamily};
} }
.percentage-container { /* Percentage Styles */
.percentage-wrapper {
width: 100%; width: 100%;
height: 24px;
background: ${cssManager.bdTheme('#f0f0f0', '#2a2a2a')};
border-radius: 12px;
overflow: hidden;
position: relative; position: relative;
} }
.percentage-value {
font-size: var(--value-font-size);
font-weight: 600;
color: ${cssManager.bdTheme('hsl(215.3 25% 8.8%)', 'hsl(210 40% 98%)')};
line-height: 1.1;
letter-spacing: -0.025em;
margin-bottom: 8px;
}
.percentage-bar {
width: 100%;
height: 8px;
background: ${cssManager.bdTheme('hsl(214.3 31.8% 91.4%)', 'hsl(215 20.2% 21.8%)')};
border-radius: 4px;
overflow: hidden;
}
.percentage-fill { .percentage-fill {
height: 100%; height: 100%;
background: ${cssManager.bdTheme('#0084ff', '#0066cc')}; background: ${cssManager.bdTheme('hsl(215.3 25% 8.8%)', 'hsl(210 40% 98%)')};
transition: width 0.5s ease; transition: width 0.6s cubic-bezier(0.4, 0, 0.2, 1);
border-radius: 12px; border-radius: 4px;
}
.percentage-text {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 12px;
font-weight: 600;
color: ${cssManager.bdTheme('#333', '#fff')};
} }
/* Trend Styles */
.trend-container { .trend-container {
width: 100%; width: 100%;
height: 100%;
position: relative;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: center; gap: 8px;
align-items: center; }
gap: 4px;
.trend-header {
display: flex;
align-items: baseline;
gap: 8px;
}
.trend-value {
font-size: var(--value-font-size);
font-weight: 600;
color: ${cssManager.bdTheme('hsl(215.3 25% 8.8%)', 'hsl(210 40% 98%)')};
line-height: 1.1;
letter-spacing: -0.025em;
}
.trend-unit {
font-size: var(--unit-font-size);
font-weight: 400;
color: ${cssManager.bdTheme('hsl(215.4 16.3% 46.9%)', 'hsl(215 20.2% 65.1%)')};
letter-spacing: -0.01em;
}
.trend-label {
font-size: var(--label-font-size);
font-weight: 500;
color: ${cssManager.bdTheme('hsl(215.4 16.3% 56.9%)', 'hsl(215 20.2% 55.1%)')};
letter-spacing: -0.01em;
margin-left: auto;
}
.trend-graph {
width: 100%;
height: 32px;
position: relative;
} }
.trend-svg { .trend-svg {
width: 100%; width: 100%;
height: 40px; height: 100%;
flex-shrink: 0; display: block;
} }
.trend-line { .trend-line {
fill: none; fill: none;
stroke: ${cssManager.bdTheme('#0084ff', '#0066cc')}; stroke: ${cssManager.bdTheme('hsl(215.4 16.3% 66.9%)', 'hsl(215 20.2% 55.1%)')};
stroke-width: 2; stroke-width: 2;
stroke-linejoin: round;
stroke-linecap: round;
} }
.trend-area { .trend-area {
fill: ${cssManager.bdTheme('rgba(0, 132, 255, 0.1)', 'rgba(0, 102, 204, 0.2)')}; fill: ${cssManager.bdTheme('hsl(215.4 16.3% 66.9% / 0.1)', 'hsl(215 20.2% 55.1% / 0.08)')};
} }
/* Text Value Styles */
.text-value { .text-value {
font-size: 32px; font-size: var(--value-font-size);
font-weight: 600; font-weight: 600;
color: ${cssManager.bdTheme('#333', '#fff')}; color: ${cssManager.bdTheme('hsl(215.3 25% 8.8%)', 'hsl(210 40% 98%)')};
} line-height: 1.1;
letter-spacing: -0.025em;
.trend-value {
font-size: 32px;
font-weight: 600;
color: ${cssManager.bdTheme('#333', '#fff')};
display: flex;
align-items: baseline;
gap: 6px;
}
.trend-value .tile-unit {
font-size: 18px;
} }
/* Context Menu */
dees-contextmenu { dees-contextmenu {
position: fixed; position: fixed;
z-index: 1000; z-index: 1000;
@ -306,11 +383,15 @@ export class DeesStatsGrid extends DeesElement {
return html` return html`
${this.gridActions.length > 0 ? html` ${this.gridActions.length > 0 ? html`
<div class="grid-header"> <div class="grid-header">
<div class="grid-title">Statistics</div> <div class="grid-title"></div>
<div class="grid-actions"> <div class="grid-actions">
${this.gridActions.map(action => html` ${this.gridActions.map(action => html`
<dees-button @clicked=${() => this.handleGridAction(action)}> <dees-button
${action.iconName ? html`<dees-icon .iconFA=${action.iconName} size="small"></dees-icon>` : ''} @clicked=${() => this.handleGridAction(action)}
type="outline"
size="sm"
>
${action.iconName ? html`<dees-icon .icon=${action.iconName} size="small"></dees-icon>` : ''}
${action.name} ${action.name}
</dees-button> </dees-button>
`)} `)}
@ -326,7 +407,7 @@ export class DeesStatsGrid extends DeesElement {
<dees-contextmenu <dees-contextmenu
.x=${this.contextMenuPosition.x} .x=${this.contextMenuPosition.x}
.y=${this.contextMenuPosition.y} .y=${this.contextMenuPosition.y}
.menuItems=${this.contextMenuActions} .menuItems=${this.contextMenuActions as any}
@clicked=${() => this.contextMenuVisible = false} @clicked=${() => this.contextMenuVisible = false}
></dees-contextmenu> ></dees-contextmenu>
` : ''} ` : ''}
@ -346,7 +427,7 @@ export class DeesStatsGrid extends DeesElement {
<div class="tile-header"> <div class="tile-header">
<h3 class="tile-title">${tile.title}</h3> <h3 class="tile-title">${tile.title}</h3>
${tile.icon ? html` ${tile.icon ? html`
<dees-icon class="tile-icon" .iconFA=${tile.icon} size="small"></dees-icon> <dees-icon class="tile-icon" .icon=${tile.icon} size="small"></dees-icon>
` : ''} ` : ''}
</div> </div>
@ -354,7 +435,7 @@ export class DeesStatsGrid extends DeesElement {
${this.renderTileContent(tile)} ${this.renderTileContent(tile)}
</div> </div>
${tile.description ? html` ${tile.description && tile.type !== 'trend' ? html`
<div class="tile-description">${tile.description}</div> <div class="tile-description">${tile.description}</div>
` : ''} ` : ''}
</div> </div>
@ -396,12 +477,31 @@ export class DeesStatsGrid extends DeesElement {
const value = typeof tile.value === 'number' ? tile.value : parseFloat(tile.value); const value = typeof tile.value === 'number' ? tile.value : parseFloat(tile.value);
const options = tile.gaugeOptions || { min: 0, max: 100 }; const options = tile.gaugeOptions || { min: 0, max: 100 };
const percentage = ((value - options.min) / (options.max - options.min)) * 100; const percentage = ((value - options.min) / (options.max - options.min)) * 100;
const strokeDasharray = 188.5; // Circumference of circle with r=30
const strokeDashoffset = strokeDasharray - (strokeDasharray * percentage) / 100; // SVG dimensions and calculations
const width = 140;
const height = 80;
const strokeWidth = 8;
const padding = strokeWidth / 2 + 2;
const radius = 48;
const centerX = width / 2;
const centerY = height - padding;
// Arc path
const startX = centerX - radius;
const startY = centerY;
const endX = centerX + radius;
const endY = centerY;
const arcPath = `M ${startX} ${startY} A ${radius} ${radius} 0 0 1 ${endX} ${endY}`;
// Calculate stroke dasharray and dashoffset
const circumference = Math.PI * radius;
const strokeDashoffset = circumference - (circumference * percentage) / 100;
let strokeColor = tile.color || cssManager.bdTheme('#0084ff', '#0066cc'); let strokeColor = tile.color || cssManager.bdTheme('hsl(215.3 25% 28.8%)', 'hsl(210 40% 78%)');
if (options.thresholds) { if (options.thresholds) {
for (const threshold of options.thresholds.reverse()) { const sortedThresholds = [...options.thresholds].sort((a, b) => b.value - a.value);
for (const threshold of sortedThresholds) {
if (value >= threshold.value) { if (value >= threshold.value) {
strokeColor = threshold.color; strokeColor = threshold.color;
break; break;
@ -410,29 +510,28 @@ export class DeesStatsGrid extends DeesElement {
} }
return html` return html`
<div class="gauge-container"> <div class="gauge-wrapper">
<svg class="gauge-svg" viewBox="0 0 80 80"> <div class="gauge-container">
<circle <svg class="gauge-svg" viewBox="0 0 ${width} ${height}" preserveAspectRatio="xMidYMid meet">
class="gauge-background" <!-- Background arc -->
cx="40" <path
cy="40" class="gauge-background"
r="30" d="${arcPath}"
transform="rotate(-90 40 40)" />
/> <!-- Filled arc -->
<circle <path
class="gauge-fill" class="gauge-fill"
cx="40" d="${arcPath}"
cy="40" stroke="${strokeColor}"
r="30" stroke-dasharray="${circumference}"
transform="rotate(-90 40 40)" stroke-dashoffset="${strokeDashoffset}"
stroke="${strokeColor}" />
stroke-dasharray="${strokeDasharray}" <!-- Value text -->
stroke-dashoffset="${strokeDashoffset}" <text class="gauge-text" x="${centerX}" y="${centerY - 8}" dominant-baseline="middle">
/> <tspan>${value}</tspan>${tile.unit ? html`<tspan class="gauge-unit" dx="2" dy="0">${tile.unit}</tspan>` : ''}
<text class="gauge-text" x="40" y="40" dy="0.35em"> </text>
${value}${tile.unit || ''} </svg>
</text> </div>
</svg>
</div> </div>
`; `;
} }
@ -442,12 +541,14 @@ export class DeesStatsGrid extends DeesElement {
const percentage = Math.min(100, Math.max(0, value)); const percentage = Math.min(100, Math.max(0, value));
return html` return html`
<div class="percentage-container"> <div class="percentage-wrapper">
<div <div class="percentage-value">${percentage}%</div>
class="percentage-fill" <div class="percentage-bar">
style="width: ${percentage}%; ${tile.color ? `background: ${tile.color}` : ''}" <div
></div> class="percentage-fill"
<div class="percentage-text">${percentage}%</div> style="width: ${percentage}%; ${tile.color ? `background: ${tile.color}` : ''}"
></div>
</div>
</div> </div>
`; `;
} }
@ -461,11 +562,14 @@ export class DeesStatsGrid extends DeesElement {
const max = Math.max(...data); const max = Math.max(...data);
const min = Math.min(...data); const min = Math.min(...data);
const range = max - min || 1; const range = max - min || 1;
const width = 200; const width = 300;
const height = 40; const height = 32;
// Add padding to prevent clipping
const padding = 2;
const points = data.map((value, index) => { const points = data.map((value, index) => {
const x = (index / (data.length - 1)) * width; const x = (index / (data.length - 1)) * width;
const y = height - ((value - min) / range) * height; const y = padding + (height - 2 * padding) - ((value - min) / range) * (height - 2 * padding);
return `${x},${y}`; return `${x},${y}`;
}).join(' '); }).join(' ');
@ -473,13 +577,16 @@ export class DeesStatsGrid extends DeesElement {
return html` return html`
<div class="trend-container"> <div class="trend-container">
<svg class="trend-svg" viewBox="0 0 ${width} ${height}" preserveAspectRatio="none"> <div class="trend-header">
<polygon class="trend-area" points="${areaPoints}" /> <span class="trend-value">${tile.value}</span>
<polyline class="trend-line" points="${points}" /> ${tile.unit ? html`<span class="trend-unit">${tile.unit}</span>` : ''}
</svg> ${tile.description ? html`<span class="trend-label">${tile.description}</span>` : ''}
<div class="trend-value"> </div>
<span>${tile.value}</span> <div class="trend-graph">
${tile.unit ? html`<span class="tile-unit">${tile.unit}</span>` : ''} <svg class="trend-svg" viewBox="0 0 ${width} ${height}" preserveAspectRatio="none">
<polygon class="trend-area" points="${areaPoints}" />
<polyline class="trend-line" points="${points}" />
</svg>
</div> </div>
</div> </div>
`; `;

View File

@ -1,6 +1,6 @@
import { type ITableAction } from './dees-table.js'; import { type ITableAction } from './dees-table.js';
import * as plugins from './00plugins.js'; import * as plugins from './00plugins.js';
import { html } from '@design.estate/dees-element'; import { html, css, cssManager } from '@design.estate/dees-element';
interface ITableDemoData { interface ITableDemoData {
date: string; date: string;
@ -10,120 +10,423 @@ interface ITableDemoData {
export const demoFunc = () => html` export const demoFunc = () => html`
<style> <style>
.demoWrapper { ${css`
box-sizing: border-box; .demoWrapper {
position: absolute; box-sizing: border-box;
width: 100%; position: absolute;
height: 100%; width: 100%;
padding: 20px; height: 100%;
background: #000000; padding: 32px;
} background: ${cssManager.bdTheme('hsl(0 0% 95%)', 'hsl(0 0% 5%)')};
overflow-y: auto;
}
.demo-container {
max-width: 1200px;
margin: 0 auto;
}
.demo-section {
margin-bottom: 48px;
}
.demo-title {
font-size: 24px;
font-weight: 600;
margin-bottom: 8px;
color: ${cssManager.bdTheme('hsl(0 0% 9%)', 'hsl(0 0% 95%)')};
}
.demo-description {
font-size: 14px;
color: ${cssManager.bdTheme('hsl(215.4 16.3% 46.9%)', 'hsl(215 20.2% 65.1%)')};
margin-bottom: 24px;
}
.theme-toggle {
position: fixed;
top: 16px;
right: 16px;
z-index: 1000;
}
`}
</style> </style>
<div class="demoWrapper"> <div class="demoWrapper">
<dees-table <dees-button class="theme-toggle" @click=${() => {
heading1="Current Account Statement" document.body.classList.toggle('bright');
heading2="Bunq - Payment Account 2 - April 2021" document.body.classList.toggle('dark');
.editableFields="${['description']}" }}>Toggle Theme</dees-button>
.data=${[
{ <div class="demo-container">
date: '2021-04-01', <div class="demo-section">
amount: '2464.65 €', <h2 class="demo-title">Basic Table with Actions</h2>
description: 'Printing Paper (Office Supplies) - STAPLES BREMEN', <p class="demo-description">A standard table with row actions, editable fields, and context menu support. Double-click on descriptions to edit. Grid lines are enabled by default.</p>
}, <dees-table
{ heading1="Current Account Statement"
date: '2021-04-02', heading2="Bunq - Payment Account 2 - April 2021"
amount: '165.65 €', .editableFields="${['description']}"
description: 'Logitech Mouse (Hardware) - logi.com OnlineShop', .data=${[
}, {
{ date: '2021-04-01',
date: '2021-04-03', amount: '2464.65 €',
amount: '2999,00 €', description: 'Printing Paper (Office Supplies) - STAPLES BREMEN',
description: 'Macbook Pro 16inch (Hardware) - Apple.de OnlineShop', },
}, {
{ date: '2021-04-02',
date: '2021-04-01', amount: '165.65 €',
amount: '2464.65 €', description: 'Logitech Mouse (Hardware) - logi.com OnlineShop',
description: 'Office-Supplies - STAPLES BREMEN', },
}, {
{ date: '2021-04-03',
date: '2021-04-01', amount: '2999,00 €',
amount: '2464.65 €', description: 'Macbook Pro 16inch (Hardware) - Apple.de OnlineShop',
description: 'Office-Supplies - STAPLES BREMEN', },
}, {
]} date: '2021-04-01',
dataName="transactions" amount: '2464.65 €',
.dataActions="${[ description: 'Office-Supplies - STAPLES BREMEN',
{ },
name: 'upload', {
iconName: 'bell', date: '2021-04-01',
useTableBehaviour: 'upload', amount: '2464.65 €',
type: ['inRow'], description: 'Office-Supplies - STAPLES BREMEN',
actionFunc: async (optionsArg) => { },
alert(optionsArg.item.amount); ]}
}, dataName="transactions"
}, .dataActions="${[
{ {
name: 'visibility', name: 'upload',
iconName: 'copy', iconName: 'bell',
type: ['inRow'], useTableBehaviour: 'upload',
useTableBehaviour: 'preview', type: ['inRow'],
actionFunc: async (itemArg: any) => {}, actionFunc: async (optionsArg) => {
}, alert(optionsArg.item.amount);
{ },
name: 'create new', },
iconName: 'instagram', {
type: ['header'], name: 'visibility',
useTableBehaviour: 'preview', iconName: 'copy',
actionFunc: async (itemArg: any) => {}, type: ['inRow'],
}, useTableBehaviour: 'preview',
{ actionFunc: async (itemArg: any) => {},
name: 'to gallery', },
iconName: 'message', {
type: ['footer'], name: 'create new',
useTableBehaviour: 'preview', iconName: 'instagram',
actionFunc: async (itemArg: any) => {}, type: ['header'],
}, useTableBehaviour: 'preview',
{ actionFunc: async (itemArg: any) => {},
name: 'copy', },
iconName: 'copySolid', {
type: ['contextmenu', 'inRow'], name: 'to gallery',
action: async () => { iconName: 'message',
return null; type: ['footer'],
}, useTableBehaviour: 'preview',
}, actionFunc: async (itemArg: any) => {},
{ },
name: 'edit (from demo)', {
iconName: 'penToSquare', name: 'copy',
type: ['contextmenu'], iconName: 'copySolid',
action: async () => { type: ['contextmenu', 'inRow'],
return null; action: async () => {
}, return null;
}, },
{ },
name: 'paste', {
iconName: 'pasteSolid', name: 'edit (from demo)',
type: ['contextmenu'], iconName: 'penToSquare',
action: async () => { type: ['contextmenu'],
return null; action: async () => {
}, return null;
}, },
{ },
name: 'preview', {
type: ['doubleClick', 'contextmenu'], name: 'paste',
iconName: 'eye', iconName: 'pasteSolid',
actionFunc: async (itemArg) => { type: ['contextmenu'],
alert(itemArg.item.amount); action: async () => {
return null; return null;
}, },
} },
] as (ITableAction<ITableDemoData>)[] as any}" {
.displayFunction=${(itemArg) => { name: 'preview',
return { type: ['doubleClick', 'contextmenu'],
...itemArg, iconName: 'eye',
onlyDisplayProp: 'onlyDisplay', actionFunc: async (itemArg) => {
}; alert(itemArg.item.amount);
}} return null;
>This is a slotted Text</dees-table },
> }
] as ITableAction[]}"
></dees-table>
</div>
<div class="demo-section">
<h2 class="demo-title">Table with Vertical Lines</h2>
<p class="demo-description">Enhanced column separation for better data tracking.</p>
<dees-table
heading1="Product Inventory"
heading2="Current stock levels across warehouses"
.showVerticalLines=${true}
.data=${[
{
product: 'MacBook Pro 16"',
warehouse_a: '45',
warehouse_b: '32',
warehouse_c: '28',
total: '105',
status: '✓ In Stock'
},
{
product: 'iPhone 15 Pro',
warehouse_a: '120',
warehouse_b: '89',
warehouse_c: '156',
total: '365',
status: '✓ In Stock'
},
{
product: 'AirPods Pro',
warehouse_a: '0',
warehouse_b: '12',
warehouse_c: '5',
total: '17',
status: '⚠ Low Stock'
},
{
product: 'iPad Air',
warehouse_a: '23',
warehouse_b: '45',
warehouse_c: '67',
total: '135',
status: '✓ In Stock'
}
]}
dataName="products"
></dees-table>
</div>
<div class="demo-section">
<h2 class="demo-title">Table with Full Grid</h2>
<p class="demo-description">Complete grid lines for maximum readability and structure.</p>
<dees-table
heading1="Server Monitoring Dashboard"
heading2="Real-time metrics across regions"
.showGrid=${true}
.data=${[
{
server: 'API-1',
region: 'US-East',
cpu: '45%',
memory: '62%',
disk: '78%',
latency: '12ms',
uptime: '99.9%',
status: '🟢 Healthy'
},
{
server: 'API-2',
region: 'EU-West',
cpu: '38%',
memory: '55%',
disk: '45%',
latency: '25ms',
uptime: '99.8%',
status: '🟢 Healthy'
},
{
server: 'DB-Master',
region: 'US-East',
cpu: '72%',
memory: '81%',
disk: '92%',
latency: '8ms',
uptime: '100%',
status: '🟡 Warning'
},
{
server: 'DB-Replica',
region: 'EU-West',
cpu: '23%',
memory: '34%',
disk: '45%',
latency: '15ms',
uptime: '99.7%',
status: '🟢 Healthy'
},
{
server: 'Cache-1',
region: 'AP-South',
cpu: '89%',
memory: '92%',
disk: '12%',
latency: '120ms',
uptime: '98.5%',
status: '🔴 Critical'
}
]}
dataName="servers"
.dataActions="${[
{
name: 'SSH Connect',
iconName: 'lucide:terminal',
type: ['inRow'],
actionFunc: async (optionsArg) => {
console.log('Connecting to:', optionsArg.item.server);
},
},
{
name: 'View Logs',
iconName: 'lucide:file-text',
type: ['inRow', 'contextmenu'],
actionFunc: async (optionsArg) => {
console.log('Viewing logs for:', optionsArg.item.server);
},
},
{
name: 'Restart Server',
iconName: 'lucide:refresh-cw',
type: ['contextmenu'],
actionFunc: async (optionsArg) => {
console.log('Restarting:', optionsArg.item.server);
},
}
] as ITableAction[]}"
></dees-table>
</div>
<div class="demo-section">
<h2 class="demo-title">Table with Horizontal Lines Only</h2>
<p class="demo-description">Emphasis on row separation without column dividers.</p>
<dees-table
heading1="Sales Performance"
heading2="Top performers this quarter"
.showHorizontalLines=${true}
.showVerticalLines=${false}
.data=${[
{
salesperson: 'Emily Johnson',
region: 'North America',
deals_closed: '42',
revenue: '$1.2M',
quota_achievement: '128%',
rating: '⭐⭐⭐⭐⭐'
},
{
salesperson: 'Michael Chen',
region: 'Asia Pacific',
deals_closed: '38',
revenue: '$980K',
quota_achievement: '115%',
rating: '⭐⭐⭐⭐⭐'
},
{
salesperson: 'Sarah Williams',
region: 'Europe',
deals_closed: '35',
revenue: '$875K',
quota_achievement: '108%',
rating: '⭐⭐⭐⭐'
},
{
salesperson: 'David Garcia',
region: 'Latin America',
deals_closed: '31',
revenue: '$750K',
quota_achievement: '95%',
rating: '⭐⭐⭐⭐'
}
]}
dataName="sales reps"
></dees-table>
</div>
<div class="demo-section">
<h2 class="demo-title">Simple Table (No Grid)</h2>
<p class="demo-description">Clean, minimal design without grid lines. Set showGrid to false to disable the default grid.</p>
<dees-table
heading1="Team Members"
heading2="Engineering Department"
.showGrid=${false}
.data=${[
{
name: 'Alice Johnson',
role: 'Lead Engineer',
email: 'alice@company.com',
location: 'San Francisco',
joined: '2020-03-15'
},
{
name: 'Bob Smith',
role: 'Senior Developer',
email: 'bob@company.com',
location: 'New York',
joined: '2019-07-22'
},
{
name: 'Charlie Davis',
role: 'DevOps Engineer',
email: 'charlie@company.com',
location: 'London',
joined: '2021-01-10'
},
{
name: 'Diana Martinez',
role: 'Frontend Developer',
email: 'diana@company.com',
location: 'Barcelona',
joined: '2022-05-18'
}
]}
dataName="team members"
></dees-table>
</div>
<div class="demo-section">
<h2 class="demo-title">Table with Custom Display Function</h2>
<p class="demo-description">Transform data for display using custom formatting.</p>
<dees-table
heading1="Sales Report"
heading2="Q4 2023 Performance"
.data=${[
{
product: 'Enterprise License',
units: 45,
revenue: 225000,
growth: 0.23,
forecast: 280000
},
{
product: 'Professional License',
units: 128,
revenue: 128000,
growth: 0.15,
forecast: 147000
},
{
product: 'Starter License',
units: 342,
revenue: 68400,
growth: 0.42,
forecast: 97000
}
]}
.displayFunction=${(item) => ({
Product: item.product,
'Units Sold': item.units.toLocaleString(),
Revenue: '$' + item.revenue.toLocaleString(),
Growth: (item.growth * 100).toFixed(1) + '%',
'Q1 2024 Forecast': '$' + item.forecast.toLocaleString()
})}
dataName="products"
></dees-table>
</div>
<div class="demo-section">
<h2 class="demo-title">Empty Table State</h2>
<p class="demo-description">How the table looks when no data is available.</p>
<dees-table
heading1="No Data Available"
heading2="This table is currently empty"
.data=${[]}
dataName="items"
></dees-table>
</div>
</div>
</div> </div>
`; `;

View File

@ -1,6 +1,6 @@
import * as colors from './00colors.js';
import * as plugins from './00plugins.js'; import * as plugins from './00plugins.js';
import { demoFunc } from './dees-table.demo.js'; import { demoFunc } from './dees-table.demo.js';
import { cssGeistFontFamily } from './00fonts.js';
import { import {
customElement, customElement,
html, html,
@ -9,9 +9,6 @@ import {
type TemplateResult, type TemplateResult,
cssManager, cssManager,
css, css,
unsafeCSS,
type CSSResult,
state,
directives, directives,
} from '@design.estate/dees-element'; } from '@design.estate/dees-element';
@ -113,7 +110,7 @@ export class DeesTable<T> extends DeesElement {
get value() { get value() {
return this.data; return this.data;
} }
set value(valueArg) {} set value(_valueArg) {}
public changeSubject = new domtools.plugins.smartrx.rxjs.Subject<DeesTable<T>>(); public changeSubject = new domtools.plugins.smartrx.rxjs.Subject<DeesTable<T>>();
// end dees-form compatibility ----------------------------------------- // end dees-form compatibility -----------------------------------------
@ -157,6 +154,27 @@ export class DeesTable<T> extends DeesElement {
}) })
public editableFields: string[] = []; public editableFields: string[] = [];
@property({
type: Boolean,
reflect: true,
attribute: 'show-vertical-lines'
})
public showVerticalLines: boolean = false;
@property({
type: Boolean,
reflect: true,
attribute: 'show-horizontal-lines'
})
public showHorizontalLines: boolean = false;
@property({
type: Boolean,
reflect: true,
attribute: 'show-grid'
})
public showGrid: boolean = true;
public files: File[] = []; public files: File[] = [];
public fileWeakMap = new WeakMap(); public fileWeakMap = new WeakMap();
@ -169,238 +187,358 @@ export class DeesTable<T> extends DeesElement {
public static styles = [ public static styles = [
cssManager.defaultStyles, cssManager.defaultStyles,
css` css`
.mainbox { :host {
color: ${cssManager.bdTheme('#333', '#fff')};
font-family: 'Geist Sans', sans-serif;
font-weight: 400;
font-size: 14px;
padding: 16px;
display: block; display: block;
width: 100%; width: 100%;
min-height: 50px; }
background: ${cssManager.bdTheme('#ffffff', '#222222')};
border-radius: 3px; .mainbox {
border-top: 1px solid ${cssManager.bdTheme('#fff', '#ffffff10')}; color: ${cssManager.bdTheme('hsl(0 0% 3.9%)', 'hsl(0 0% 98%)')};
box-shadow: 0px 1px 4px rgba(0, 0, 0, 0.3); font-family: ${cssGeistFontFamily};
overflow-x: auto; font-weight: 400;
font-size: 14px;
display: block;
width: 100%;
background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(0 0% 3.9%)')};
border: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
border-radius: 8px;
overflow: hidden;
cursor: default; cursor: default;
} }
.header { .header {
display: flex; display: flex;
justify-content: flex-end; justify-content: space-between;
align-items: center; align-items: center;
font-family: 'Geist Sans', sans-serif; padding: 16px 24px;
min-height: 64px;
border-bottom: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
} }
.headingContainer { .headingContainer {
flex: 1;
} }
.heading { .heading {
line-height: 1.5;
} }
.heading1 { .heading1 {
font-size: 18px;
font-weight: 600; font-weight: 600;
color: ${cssManager.bdTheme('hsl(0 0% 9%)', 'hsl(0 0% 95%)')};
letter-spacing: -0.025em;
} }
.heading2 { .heading2 {
opacity: 0.6; font-size: 14px;
color: ${cssManager.bdTheme('hsl(215.4 16.3% 56.9%)', 'hsl(215 20.2% 55.1%)')};
margin-top: 2px;
} }
.headingSeparation { .headingSeparation {
margin-top: 7px; display: none;
border-bottom: 1px solid ${cssManager.bdTheme('#bcbcbc', '#444444')};
} }
.headerActions { .headerActions {
user-select: none; user-select: none;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
margin-left: auto; gap: 8px;
} }
.headerAction { .headerAction {
display: flex; display: flex;
flex-direction: row; align-items: center;
color: ${cssManager.bdTheme('#333', '#ccc')}; gap: 6px;
margin-left: 16px; padding: 6px 12px;
font-size: 14px;
font-weight: 500;
color: ${cssManager.bdTheme('hsl(0 0% 45.1%)', 'hsl(0 0% 63.9%)')};
background: transparent;
border: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
border-radius: 6px;
cursor: pointer;
transition: all 0.15s ease;
} }
.headerAction:hover { .headerAction:hover {
color: ${cssManager.bdTheme('#555', '#fff')}; color: ${cssManager.bdTheme('hsl(0 0% 9%)', 'hsl(0 0% 95%)')};
background: ${cssManager.bdTheme('hsl(0 0% 95.1%)', 'hsl(0 0% 14.9%)')};
border-color: ${cssManager.bdTheme('hsl(0 0% 79.8%)', 'hsl(0 0% 20.9%)')};
} }
.headerAction dees-icon { .headerAction dees-icon {
margin-right: 8px; width: 14px;
height: 14px;
} }
.searchGrid { .searchGrid {
background: ${cssManager.bdTheme('#fff', '#111111')};
display: grid; display: grid;
grid-gap: 16px; grid-gap: 16px;
grid-template-columns: 1fr 200px; grid-template-columns: 1fr 200px;
margin-top: 16px; padding: 16px 24px;
padding: 0px 16px; background: ${cssManager.bdTheme('hsl(210 40% 98%)', 'hsl(0 0% 3.9%)')};
border-top: 1px solid ${cssManager.bdTheme('#fff', '#ffffff20')}; border-bottom: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
border-radius: 8px; transition: all 0.2s ease;
} }
.searchGrid.hidden { .searchGrid.hidden {
height: 0px; height: 0px;
opacity: 0; opacity: 0;
overflow: hidden; overflow: hidden;
margin-top: 0px; padding: 0px 24px;
border-bottom-width: 0px;
} }
table, table {
.noDataSet {
margin-top: 16px;
color: ${cssManager.bdTheme('#333', '#fff')};
border-collapse: collapse;
width: 100%; width: 100%;
caption-side: bottom;
font-size: 14px;
border-collapse: separate;
border-spacing: 0;
} }
.noDataSet { .noDataSet {
padding: 48px 24px;
text-align: center; text-align: center;
color: ${cssManager.bdTheme('hsl(215.4 16.3% 56.9%)', 'hsl(215 20.2% 55.1%)')};
} }
tr {
border-bottom: 1px dashed ${cssManager.bdTheme('#999', '#808080')}; thead {
text-align: left; background: ${cssManager.bdTheme('hsl(210 40% 96.1%)', 'hsl(0 0% 9%)')};
border-bottom: 1px solid ${cssManager.bdTheme('hsl(0 0% 79.8%)', 'hsl(0 0% 20.9%)')};
} }
tr:last-child {
tbody tr {
transition: background-color 0.15s ease;
position: relative;
}
/* Default horizontal lines (bottom border only) */
tbody tr {
border-bottom: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
}
tbody tr:last-child {
border-bottom: none; border-bottom: none;
text-align: left;
} }
tr:hover {
/* Full horizontal lines when enabled */
:host([show-horizontal-lines]) tbody tr {
border-top: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
border-bottom: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
} }
tr:hover td {
background: ${cssManager.bdTheme('#22222210', '#ffffff10')}; :host([show-horizontal-lines]) tbody tr:first-child {
border-top: none;
} }
tr:first-child:hover {
cursor: auto; :host([show-horizontal-lines]) tbody tr:last-child {
border-bottom: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
} }
tr:first-child:hover .innerCellContainer {
background: none; tbody tr:hover {
background: ${cssManager.bdTheme('hsl(210 40% 96.1% / 0.5)', 'hsl(0 0% 14.9% / 0.5)')};
} }
tr.selected td {
background: ${cssManager.bdTheme('#22222220', '#ffffff20')}; /* Column hover effect for better traceability */
td {
position: relative;
}
td::after {
content: '';
position: absolute;
top: -1000px;
bottom: -1000px;
left: 0;
right: 0;
background: ${cssManager.bdTheme('hsl(210 40% 96.1% / 0.3)', 'hsl(0 0% 14.9% / 0.3)')};
opacity: 0;
pointer-events: none;
transition: opacity 0.15s ease;
z-index: -1;
}
td:hover::after {
opacity: 1;
}
/* Grid mode - shows both vertical and horizontal lines */
:host([show-grid]) th {
border: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
border-left: none;
border-top: none;
}
:host([show-grid]) td {
border: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
border-left: none;
border-top: none;
}
:host([show-grid]) th:first-child,
:host([show-grid]) td:first-child {
border-left: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
}
:host([show-grid]) tbody tr:first-child td {
border-top: none;
}
tbody tr.selected {
background: ${cssManager.bdTheme('hsl(210 40% 96.1%)', 'hsl(0 0% 14.9%)')};
} }
tr.hasAttachment td { tbody tr.hasAttachment {
background: ${cssManager.bdTheme('#0098847c', '#0098847c')}; background: ${cssManager.bdTheme('hsl(142.1 76.2% 36.3% / 0.1)', 'hsl(142.1 76.2% 36.3% / 0.1)')};
} }
th { th {
text-transform: none; height: 48px;
font-family: 'Geist Sans', sans-serif; padding: 12px 24px;
text-align: left;
font-weight: 500; font-weight: 500;
color: ${cssManager.bdTheme('hsl(215.4 16.3% 46.9%)', 'hsl(215 20.2% 65.1%)')};
letter-spacing: -0.01em;
} }
th,
:host([show-vertical-lines]) th {
border-right: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
}
td { td {
position: relative; padding: 12px 24px;
vertical-align: top; vertical-align: middle;
color: ${cssManager.bdTheme('hsl(0 0% 3.9%)', 'hsl(0 0% 98%)')};
padding: 0px;
border-right: 1px dashed ${cssManager.bdTheme('#999', '#808080')};
} }
.innerCellContainer {
min-height: 36px; :host([show-vertical-lines]) td {
position: relative; border-right: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
height: 100%;
width: 100%;
padding: 6px 8px;
line-height: 24px;
} }
th:first-child .innerCellContainer,
td:first-child .innerCellContainer { th:first-child,
padding-left: 0px; td:first-child {
} padding-left: 24px;
th:last-child .innerCellContainer,
td:last-child .innerCellContainer {
padding-right: 0px;
} }
th:last-child, th:last-child,
td:last-child { td:last-child {
padding-right: 24px;
}
:host([show-vertical-lines]) th:last-child,
:host([show-vertical-lines]) td:last-child {
border-right: none; border-right: none;
} }
.innerCellContainer {
position: relative;
min-height: 24px;
line-height: 24px;
}
td input { td input {
width: 100%;
height: 100%;
outline: none;
border: 2px solid #fa6101;
top: 0px;
bottom: 0px;
right: 0px;
left: 0px;
position: absolute; position: absolute;
background: #fa610140; top: 4px;
color: ${cssManager.bdTheme('#333', '#fff')}; bottom: 4px;
left: 20px;
right: 20px;
width: calc(100% - 40px);
height: calc(100% - 8px);
padding: 0 12px;
outline: none;
border: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
border-radius: 6px;
background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(0 0% 9%)')};
color: ${cssManager.bdTheme('hsl(0 0% 3.9%)', 'hsl(0 0% 98%)')};
font-family: inherit; font-family: inherit;
font-size: inherit; font-size: inherit;
font-weight: inherit; font-weight: inherit;
padding: 0px 6px; transition: all 0.15s ease;
box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);
}
td input:focus {
border-color: ${cssManager.bdTheme('hsl(222.2 47.4% 51.2%)', 'hsl(217.2 91.2% 59.8%)')};
outline: 2px solid transparent;
outline-offset: 2px;
box-shadow: 0 0 0 2px ${cssManager.bdTheme('hsl(222.2 47.4% 51.2% / 0.2)', 'hsl(217.2 91.2% 59.8% / 0.2)')};
} }
.actionsContainer { .actionsContainer {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
height: 24px; gap: 4px;
transform: translateY(-4px);
margin-left: -6px;
} }
.action { .action {
position: relative; display: flex;
padding: 8px 10px; align-items: center;
line-height: 24px; justify-content: center;
width: 32px;
height: 32px; height: 32px;
size: 16px; border-radius: 6px;
border-radius: 8px; color: ${cssManager.bdTheme('hsl(215.4 16.3% 46.9%)', 'hsl(215 20.2% 65.1%)')};
cursor: pointer;
transition: all 0.15s ease;
} }
.action:hover { .action:hover {
background: ${cssManager.bdTheme(colors.bright.blue, colors.dark.blue)}; background: ${cssManager.bdTheme('hsl(210 40% 96.1%)', 'hsl(0 0% 14.9%)')};
color: ${cssManager.bdTheme('hsl(0 0% 9%)', 'hsl(0 0% 95%)')};
} }
.action:active { .action:active {
background: ${cssManager.bdTheme(colors.bright.blue, colors.dark.blueActive)}; background: ${cssManager.bdTheme('hsl(210 40% 96.1%)', 'hsl(0 0% 11.8%)')};
} }
.action:hover dees-icon { .action dees-icon {
filter: ${cssManager.bdTheme('invert(1) brightness(3)', '')}; width: 16px;
height: 16px;
} }
.footer { .footer {
font-family: 'Geist Sans', sans-serif;
font-size: 14px;
color: ${cssManager.bdTheme('#111', '#ffffff90')};
background: ${cssManager.bdTheme('#eeeeeb', '#00000050')};
margin: 16px -16px -16px -16px;
border-bottom-left-radius: 3px;
border-bottom-right-radius: 3px;
display: flex; display: flex;
align-items: center;
justify-content: space-between;
height: 52px;
padding: 0 24px;
font-size: 14px;
color: ${cssManager.bdTheme('hsl(215.4 16.3% 46.9%)', 'hsl(215 20.2% 65.1%)')};
background: ${cssManager.bdTheme('hsl(210 40% 96.1%)', 'hsl(0 0% 9%)')};
border-top: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
} }
.tableStatistics { .tableStatistics {
padding: 8px 16px; font-weight: 500;
} }
.footerActions { .footerActions {
margin-left: auto; display: flex;
gap: 8px;
} }
.footerActions .footerAction { .footerActions .footerAction {
padding: 8px 16px;
display: flex; display: flex;
align-items: center;
gap: 6px;
padding: 6px 12px;
font-weight: 500;
color: ${cssManager.bdTheme('hsl(215.4 16.3% 46.9%)', 'hsl(215 20.2% 65.1%)')};
border-radius: 6px;
cursor: pointer;
user-select: none; user-select: none;
transition: all 0.15s ease;
} }
.footerActions .footerAction:hover { .footerActions .footerAction:hover {
background: ${cssManager.bdTheme(colors.bright.blue, colors.dark.blue)}; background: ${cssManager.bdTheme('hsl(0 0% 95.1%)', 'hsl(0 0% 14.9%)')};
color: #fff; color: ${cssManager.bdTheme('hsl(0 0% 9%)', 'hsl(0 0% 95%)')};
} }
.footerActions .footerAction dees-icon { .footerActions .footerAction dees-icon {
display: flex; width: 14px;
margin-right: 8px; height: 14px;
}
.footerActions .footerAction:hover dees-icon {
} }
`, `,
]; ];
@ -430,7 +568,7 @@ export class DeesTable<T> extends DeesElement {
}} }}
> >
${action.iconName ${action.iconName
? html`<dees-icon .iconSize=${14} .iconFA=${action.iconName}></dees-icon> ? html`<dees-icon .iconSize=${14} .icon=${action.iconName}></dees-icon>
${action.name}` ${action.name}`
: action.name} : action.name}
</div>` </div>`
@ -478,24 +616,23 @@ export class DeesTable<T> extends DeesElement {
const headings: string[] = Object.keys(firstTransformedItem); const headings: string[] = Object.keys(firstTransformedItem);
return html` return html`
<table> <table>
<tr> <thead>
${headings.map( <tr>
(headingArg) => html` ${headings.map(
<th> (headingArg) => html`
<div class="innerCellContainer">${headingArg}</div> <th>${headingArg}</th>
</th> `
` )}
)} ${(() => {
${(() => { if (this.dataActions && this.dataActions.length > 0) {
if (this.dataActions && this.dataActions.length > 0) { return html`
return html` <th>Actions</th>
<th> `;
<div class="innerCellContainer">Actions</div> }
</th> })()}
`; </tr>
} </thead>
})()} <tbody>
</tr>
${this.data.map((itemArg) => { ${this.data.map((itemArg) => {
const transformedItem = this.displayFunction(itemArg); const transformedItem = this.displayFunction(itemArg);
const getTr = (elementArg: HTMLElement): HTMLElement => { const getTr = (elementArg: HTMLElement): HTMLElement => {
@ -592,10 +729,9 @@ export class DeesTable<T> extends DeesElement {
if (this.dataActions && this.dataActions.length > 0) { if (this.dataActions && this.dataActions.length > 0) {
return html` return html`
<td> <td>
<div class="innerCellContainer"> <div class="actionsContainer">
<div class="actionsContainer"> ${this.getActionsForType('inRow').map(
${this.getActionsForType('inRow').map( (actionArg) => html`
(actionArg) => html`
<div <div
class="action" class="action"
@click=${() => @click=${() =>
@ -607,14 +743,13 @@ export class DeesTable<T> extends DeesElement {
${actionArg.iconName ${actionArg.iconName
? html` ? html`
<dees-icon <dees-icon
.iconFA=${actionArg.iconName} .icon=${actionArg.iconName}
></dees-icon> ></dees-icon>
` `
: actionArg.name} : actionArg.name}
</div> </div>
` `
)} )}
</div>
</div> </div>
</td> </td>
`; `;
@ -623,6 +758,7 @@ export class DeesTable<T> extends DeesElement {
</tr> </tr>
`; `;
})} })}
</tbody>
</table> </table>
`; `;
})() })()
@ -649,7 +785,7 @@ export class DeesTable<T> extends DeesElement {
}} }}
> >
${action.iconName ${action.iconName
? html`<dees-icon .iconSize=${14} .iconFA=${action.iconName}></dees-icon> ? html`<dees-icon .iconSize=${14} .icon=${action.iconName}></dees-icon>
${action.name}` ${action.name}`
: action.name} : action.name}
</div>` </div>`
@ -743,7 +879,7 @@ export class DeesTable<T> extends DeesElement {
} }
async handleCellEditing(event: Event, itemArg: T, key: string) { async handleCellEditing(event: Event, itemArg: T, key: string) {
const domtools = await this.domtoolsPromise; await this.domtoolsPromise;
const target = event.target as HTMLElement; const target = event.target as HTMLElement;
const originalColor = target.style.color; const originalColor = target.style.color;
target.style.color = 'transparent'; target.style.color = 'transparent';

View File

@ -3,6 +3,7 @@ import { customElement, DeesElement, type TemplateResult, html, css, property, c
import * as domtools from '@design.estate/dees-domtools'; import * as domtools from '@design.estate/dees-domtools';
import { zIndexLayers } from './00zindex.js'; import { zIndexLayers } from './00zindex.js';
import { demoFunc } from './dees-toast.demo.js'; import { demoFunc } from './dees-toast.demo.js';
import { cssGeistFontFamily } from './00fonts.js';
declare global { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {
@ -106,6 +107,11 @@ export class DeesToast extends DeesElement {
return toast; return toast;
} }
// Alias for consistency with DeesModal
public static async createAndShow(options: IToastOptions | string) {
return this.show(options);
}
// Convenience methods // Convenience methods
public static info(message: string, duration?: number) { public static info(message: string, duration?: number) {
return this.show({ message, type: 'info', duration }); return this.show({ message, type: 'info', duration });
@ -147,7 +153,7 @@ export class DeesToast extends DeesElement {
:host { :host {
display: block; display: block;
pointer-events: auto; pointer-events: auto;
font-family: 'Geist Sans', sans-serif; font-family: ${cssGeistFontFamily};
opacity: 0; opacity: 0;
transform: translateY(-10px); transform: translateY(-10px);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);

View File

@ -1,5 +1,5 @@
import { customElement, DeesElement, domtools, type TemplateResult, html, property, type CSSResult, state, } from '@design.estate/dees-element'; import { customElement, DeesElement, domtools, type TemplateResult, html, property, type CSSResult, state, } from '@design.estate/dees-element';
import { zIndexLayers } from './00zindex.js'; import { zIndexLayers, zIndexRegistry } from './00zindex.js';
declare global { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {
@ -33,6 +33,12 @@ export class DeesWindowLayer extends DeesElement {
public options: IOptions_DeesWindowLayer = { public options: IOptions_DeesWindowLayer = {
blur: false blur: false
}; };
@state()
private backdropZIndex: number = 1000;
@state()
private contentZIndex: number = 1001;
// INSTANCE // INSTANCE
@property({ @property({
@ -63,7 +69,7 @@ export class DeesWindowLayer extends DeesElement {
background: rgba(0, 0, 0, 0.0); background: rgba(0, 0, 0, 0.0);
backdrop-filter: brightness(1) ${this.options.blur ? 'blur(0px)' : ''}; backdrop-filter: brightness(1) ${this.options.blur ? 'blur(0px)' : ''};
pointer-events: none; pointer-events: none;
z-index: ${zIndexLayers.backdrop.dropdown}; z-index: ${this.backdropZIndex};
} }
.slotContent { .slotContent {
position: fixed; position: fixed;
@ -72,7 +78,12 @@ export class DeesWindowLayer extends DeesElement {
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
z-index: ${zIndexLayers.overlay.dropdown}; z-index: ${this.contentZIndex};
pointer-events: none;
}
.slotContent > * {
pointer-events: auto;
} }
.visible { .visible {
@ -81,9 +92,9 @@ export class DeesWindowLayer extends DeesElement {
pointer-events: all; pointer-events: all;
} }
</style> </style>
<div class="windowOverlay ${this.visible ? 'visible' : null}"> <div @click=${this.dispatchClicked} class="windowOverlay ${this.visible ? 'visible' : null}">
</div> </div>
<div @click=${this.dispatchClicked} class="slotContent"> <div class="slotContent">
<slot></slot> <slot></slot>
</div> </div>
`; `;
@ -102,9 +113,21 @@ export class DeesWindowLayer extends DeesElement {
public toggleVisibility () { public toggleVisibility () {
this.visible = !this.visible; this.visible = !this.visible;
} }
public getContentZIndex(): number {
return this.contentZIndex;
}
public async show() { public async show() {
const domtools = await this.domtoolsPromise; const domtools = await this.domtoolsPromise;
// Get z-indexes from registry
this.backdropZIndex = zIndexRegistry.getNextZIndex();
this.contentZIndex = zIndexRegistry.getNextZIndex();
// Register this element
zIndexRegistry.register(this, this.backdropZIndex);
await domtools.convenience.smartdelay.delayFor(0); await domtools.convenience.smartdelay.delayFor(0);
this.visible = true; this.visible = true;
} }
@ -119,6 +142,10 @@ export class DeesWindowLayer extends DeesElement {
const domtools = await this.domtoolsPromise; const domtools = await this.domtoolsPromise;
await this.hide(); await this.hide();
await domtools.convenience.smartdelay.delayFor(300); await domtools.convenience.smartdelay.delayFor(300);
// Unregister from z-index registry
zIndexRegistry.unregister(this);
this.remove(); this.remove();
} }
} }

View File

@ -18,6 +18,7 @@ export * from './dees-chips.js';
export * from './dees-contextmenu.js'; export * from './dees-contextmenu.js';
export * from './dees-dataview-codebox.js'; export * from './dees-dataview-codebox.js';
export * from './dees-dataview-statusobject.js'; export * from './dees-dataview-statusobject.js';
export * from './dees-dashboardgrid.js';
export * from './dees-editor.js'; export * from './dees-editor.js';
export * from './dees-editor-markdown.js'; export * from './dees-editor-markdown.js';
export * from './dees-editor-markdownoutlet.js'; export * from './dees-editor-markdownoutlet.js';
@ -27,9 +28,11 @@ export * from './dees-heading.js';
export * from './dees-hint.js'; export * from './dees-hint.js';
export * from './dees-icon.js'; export * from './dees-icon.js';
export * from './dees-input-checkbox.js'; export * from './dees-input-checkbox.js';
export * from './dees-input-datepicker.js';
export * from './dees-input-dropdown.js'; export * from './dees-input-dropdown.js';
export * from './dees-input-fileupload.js'; export * from './dees-input-fileupload.js';
export * from './dees-input-iban.js'; export * from './dees-input-iban.js';
export * from './profilepicture/dees-input-profilepicture.js';
export * from './dees-input-typelist.js'; export * from './dees-input-typelist.js';
export * from './dees-input-phone.js'; export * from './dees-input-phone.js';
export * from './dees-input-wysiwyg.js'; export * from './dees-input-wysiwyg.js';
@ -37,6 +40,7 @@ export * from './dees-progressbar.js';
export * from './dees-input-quantityselector.js'; export * from './dees-input-quantityselector.js';
export * from './dees-input-radiogroup.js'; export * from './dees-input-radiogroup.js';
export * from './dees-input-richtext.js'; export * from './dees-input-richtext.js';
export * from './dees-input-tags.js';
export * from './dees-input-text.js'; export * from './dees-input-text.js';
export * from './dees-label.js'; export * from './dees-label.js';
export * from './dees-mobilenavigation.js'; export * from './dees-mobilenavigation.js';
@ -45,6 +49,7 @@ export * from './dees-input-multitoggle.js';
export * from './dees-panel.js'; export * from './dees-panel.js';
export * from './dees-pdf.js'; export * from './dees-pdf.js';
export * from './dees-searchbar.js'; export * from './dees-searchbar.js';
export * from './dees-shopping-productcard.js';
export * from './dees-simple-appdash.js'; export * from './dees-simple-appdash.js';
export * from './dees-simple-login.js'; export * from './dees-simple-login.js';
export * from './dees-speechbubble.js'; export * from './dees-speechbubble.js';

View File

@ -0,0 +1,208 @@
import { html, css } from '@design.estate/dees-element';
import '@design.estate/dees-wcctools/demotools';
import '../dees-panel.js';
import './dees-input-profilepicture.js';
import type { DeesInputProfilePicture } from './dees-input-profilepicture.js';
export const demoFunc = () => html`
<style>
${css`
.demo-container {
display: flex;
flex-direction: column;
gap: 24px;
padding: 24px;
max-width: 1200px;
margin: 0 auto;
}
dees-panel {
margin-bottom: 24px;
}
.demo-row {
display: flex;
gap: 48px;
align-items: center;
flex-wrap: wrap;
}
.demo-output {
margin-top: 16px;
padding: 12px;
background: rgba(0, 105, 242, 0.1);
border-radius: 4px;
font-size: 14px;
font-family: monospace;
word-break: break-all;
max-height: 100px;
overflow-y: auto;
}
.feature-list {
margin-top: 16px;
padding-left: 20px;
}
.feature-list li {
margin-bottom: 8px;
}
`}
</style>
<div class="demo-container">
<dees-demowrapper .runAfterRender=${async (elementArg: HTMLElement) => {
// Basic demo with round profile picture
const roundProfile = elementArg.querySelector('dees-input-profilepicture[shape="round"]');
if (roundProfile) {
roundProfile.addEventListener('change', (event: CustomEvent) => {
const target = event.target as DeesInputProfilePicture;
console.log('Round profile picture changed:', target.value?.substring(0, 50) + '...');
});
}
}}>
<dees-panel .title=${'Profile Picture Input'} .subtitle=${'Basic usage with round and square shapes'}>
<div class="demo-row">
<dees-input-profilepicture
label="Profile Picture (Round)"
description="Click to upload or drag & drop an image"
shape="round"
size="120"
></dees-input-profilepicture>
<dees-input-profilepicture
label="Profile Picture (Square)"
description="Supports JPEG, PNG, and WebP formats"
shape="square"
size="120"
></dees-input-profilepicture>
</div>
</dees-panel>
</dees-demowrapper>
<dees-demowrapper .runAfterRender=${async (elementArg: HTMLElement) => {
// Different sizes demo
const profiles = elementArg.querySelectorAll('dees-input-profilepicture');
profiles.forEach((profile) => {
profile.addEventListener('change', (event: CustomEvent) => {
const target = event.target as DeesInputProfilePicture;
console.log(`Profile (size ${target.size}) changed`);
});
});
}}>
<dees-panel .title=${'Size Variations'} .subtitle=${'Profile pictures in different sizes'}>
<div class="demo-row">
<dees-input-profilepicture
label="Small (80px)"
shape="round"
size="80"
></dees-input-profilepicture>
<dees-input-profilepicture
label="Medium (120px)"
shape="round"
size="120"
></dees-input-profilepicture>
<dees-input-profilepicture
label="Large (160px)"
shape="round"
size="160"
></dees-input-profilepicture>
</div>
</dees-panel>
</dees-demowrapper>
<dees-demowrapper .runAfterRender=${async (elementArg: HTMLElement) => {
// Pre-filled profile with placeholder
const sampleImageUrl = 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAwIiBoZWlnaHQ9IjIwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KICA8ZGVmcz4KICAgIDxsaW5lYXJHcmFkaWVudCBpZD0iZ3JhZGllbnQiIHgxPSIwJSIgeTE9IjAlIiB4Mj0iMTAwJSIgeTI9IjEwMCUiPgogICAgICA8c3RvcCBvZmZzZXQ9IjAlIiBzdG9wLWNvbG9yPSIjNjY3ZWVhIiAvPgogICAgICA8c3RvcCBvZmZzZXQ9IjEwMCUiIHN0b3AtY29sb3I9IiM3NjRiYTIiIC8+CiAgICA8L2xpbmVhckdyYWRpZW50PgogIDwvZGVmcz4KICA8cmVjdCB3aWR0aD0iMjAwIiBoZWlnaHQ9IjIwMCIgZmlsbD0idXJsKCNncmFkaWVudCkiIC8+CiAgPHRleHQgeD0iNTAlIiB5PSI1MCUiIGRvbWluYW50LWJhc2VsaW5lPSJtaWRkbGUiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtZmFtaWx5PSJBcmlhbCIgZm9udC1zaXplPSI4MCIgZmlsbD0id2hpdGUiPkpEPC90ZXh0Pgo8L3N2Zz4=';
const prefilledProfile = elementArg.querySelector('#prefilled-profile') as DeesInputProfilePicture;
if (prefilledProfile) {
prefilledProfile.value = sampleImageUrl;
prefilledProfile.addEventListener('change', (event: CustomEvent) => {
const target = event.target as DeesInputProfilePicture;
const output = elementArg.querySelector('#prefilled-output');
if (output) {
output.textContent = target.value ?
`Image data: ${target.value.substring(0, 80)}...` :
'No image selected';
}
});
}
}}>
<dees-panel .title=${'Pre-filled and Value Binding'} .subtitle=${'Profile picture with initial value and change tracking'}>
<dees-input-profilepicture
id="prefilled-profile"
label="Edit Existing Profile"
description="Click the edit button to change or delete to remove"
shape="round"
size="150"
></dees-input-profilepicture>
<div id="prefilled-output" class="demo-output">
Image data will appear here when changed
</div>
</dees-panel>
</dees-demowrapper>
<dees-demowrapper .runAfterRender=${async (elementArg: HTMLElement) => {
// Disabled state demo
const disabledProfile = elementArg.querySelector('#disabled-profile') as DeesInputProfilePicture;
if (disabledProfile) {
disabledProfile.value = 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAwIiBoZWlnaHQ9IjIwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KICA8cmVjdCB3aWR0aD0iMjAwIiBoZWlnaHQ9IjIwMCIgZmlsbD0iI2NjY2NjYyIgLz4KICA8dGV4dCB4PSI1MCUiIHk9IjUwJSIgZG9taW5hbnQtYmFzZWxpbmU9Im1pZGRsZSIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1mYW1pbHk9IkFyaWFsIiBmb250LXNpemU9IjYwIiBmaWxsPSJ3aGl0ZSI+TkE8L3RleHQ+Cjwvc3ZnPg==';
}
}}>
<dees-panel .title=${'Form States'} .subtitle=${'Different states and configurations'}>
<div class="demo-row">
<dees-input-profilepicture
label="Required Field"
description="This field is required"
shape="round"
.required=${true}
></dees-input-profilepicture>
<dees-input-profilepicture
id="disabled-profile"
label="Disabled State"
description="Cannot be edited"
shape="square"
.disabled=${true}
></dees-input-profilepicture>
<dees-input-profilepicture
label="Upload Only"
description="Delete not allowed"
shape="round"
.allowDelete=${false}
></dees-input-profilepicture>
</div>
</dees-panel>
</dees-demowrapper>
<dees-demowrapper>
<dees-panel .title=${'Features'} .subtitle=${'Complete feature set of the profile picture input'}>
<ul class="feature-list">
<li><strong>Image Upload:</strong> Click to upload or drag & drop images</li>
<li><strong>Image Cropping:</strong> Interactive crop tool with resize handles</li>
<li><strong>Shape Support:</strong> Round or square profile pictures</li>
<li><strong>Size Customization:</strong> Adjustable dimensions</li>
<li><strong>Preview & Edit:</strong> Hover overlay with edit and delete options</li>
<li><strong>File Validation:</strong> Format and size restrictions</li>
<li><strong>Responsive Design:</strong> Works on desktop and mobile devices</li>
<li><strong>Form Integration:</strong> Standard form value binding and validation</li>
<li><strong>Accessibility:</strong> Keyboard navigation and screen reader support</li>
<li><strong>Z-Index Management:</strong> Proper modal stacking with registry</li>
</ul>
<div style="margin-top: 24px;">
<strong>Supported Formats:</strong> JPEG, PNG, WebP<br>
<strong>Max File Size:</strong> 5MB (configurable)<br>
<strong>Output Format:</strong> Base64 encoded JPEG
</div>
</dees-panel>
</dees-demowrapper>
</div>
`;

View File

@ -0,0 +1,455 @@
import {
customElement,
html,
property,
css,
cssManager,
state,
type TemplateResult,
} from '@design.estate/dees-element';
import { DeesInputBase } from '../dees-input-base.js';
import '../dees-icon.js';
import '../dees-label.js';
import { ProfilePictureModal } from './profilepicture.modal.js';
import { demoFunc } from './dees-input-profilepicture.demo.js';
declare global {
interface HTMLElementTagNameMap {
'dees-input-profilepicture': DeesInputProfilePicture;
}
}
export type ProfileShape = 'square' | 'round';
@customElement('dees-input-profilepicture')
export class DeesInputProfilePicture extends DeesInputBase<DeesInputProfilePicture> {
public static demo = demoFunc;
@property({ type: String })
public value: string = ''; // Base64 encoded image or URL
@property({ type: String })
public shape: ProfileShape = 'round';
@property({ type: Number })
public size: number = 120;
@property({ type: String })
public placeholder: string = '';
@property({ type: Boolean })
public allowUpload: boolean = true;
@property({ type: Boolean })
public allowDelete: boolean = true;
@property({ type: Number })
public maxFileSize: number = 5 * 1024 * 1024; // 5MB
@property({ type: Array })
public acceptedFormats: string[] = ['image/jpeg', 'image/png', 'image/webp'];
@property({ type: Number })
public outputSize: number = 800; // Output resolution in pixels
@property({ type: Number })
public outputQuality: number = 0.95; // 0-1 quality for JPEG
@state()
private isHovered: boolean = false;
@state()
private isDragging: boolean = false;
@state()
private isLoading: boolean = false;
private modalInstance: ProfilePictureModal | null = null;
public static styles = [
...DeesInputBase.baseStyles,
cssManager.defaultStyles,
css`
:host {
display: block;
position: relative;
}
.input-wrapper {
display: flex;
flex-direction: column;
gap: 16px;
}
.profile-container {
position: relative;
display: inline-block;
cursor: pointer;
transition: all 0.3s ease;
}
.profile-container:hover {
transform: scale(1.02);
}
.profile-picture {
width: var(--size, 120px);
height: var(--size, 120px);
background: ${cssManager.bdTheme('#f5f5f5', '#18181b')};
border: 3px solid ${cssManager.bdTheme('#e5e7eb', '#27272a')};
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
position: relative;
transition: all 0.3s ease;
}
.profile-picture.round {
border-radius: 50%;
}
.profile-picture.square {
border-radius: 12px;
}
.profile-picture.dragging {
border-color: ${cssManager.bdTheme('#3b82f6', '#60a5fa')};
box-shadow: 0 0 0 4px ${cssManager.bdTheme('rgba(59, 130, 246, 0.15)', 'rgba(96, 165, 250, 0.15)')};
}
.profile-picture:hover {
border-color: ${cssManager.bdTheme('#d4d4d8', '#3f3f46')};
}
.profile-picture:disabled {
cursor: not-allowed;
opacity: 0.5;
}
.profile-image {
width: 100%;
height: 100%;
object-fit: cover;
}
.placeholder-icon {
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
}
.overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.6);
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
transition: opacity 0.3s ease;
pointer-events: none;
}
.profile-container:hover .overlay {
opacity: 1;
}
.overlay-content {
display: flex;
gap: 12px;
}
.overlay-button {
width: 40px;
height: 40px;
border-radius: 50%;
background: ${cssManager.bdTheme('rgba(255, 255, 255, 0.95)', 'rgba(39, 39, 42, 0.95)')};
border: 1px solid ${cssManager.bdTheme('rgba(0, 0, 0, 0.1)', 'rgba(255, 255, 255, 0.1)')};
color: ${cssManager.bdTheme('#09090b', '#fafafa')};
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.2s ease;
pointer-events: auto;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.overlay-button:hover {
background: ${cssManager.bdTheme('#ffffff', '#3f3f46')};
transform: scale(1.1);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
}
.overlay-button.delete {
background: ${cssManager.bdTheme('rgba(239, 68, 68, 0.9)', 'rgba(220, 38, 38, 0.9)')};
color: white;
border-color: transparent;
}
.overlay-button.delete:hover {
background: ${cssManager.bdTheme('#ef4444', '#dc2626')};
}
.drop-zone-text {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
text-align: center;
color: white;
font-weight: 500;
pointer-events: none;
}
.hidden-input {
display: none;
}
/* Loading animation */
.loading-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: ${cssManager.bdTheme('rgba(255, 255, 255, 0.8)', 'rgba(0, 0, 0, 0.8)')};
display: flex;
align-items: center;
justify-content: center;
border-radius: inherit;
opacity: 0;
pointer-events: none;
transition: opacity 0.2s ease;
}
.loading-overlay.show {
opacity: 1;
pointer-events: auto;
}
.loading-spinner {
width: 40px;
height: 40px;
border: 3px solid ${cssManager.bdTheme('rgba(0, 0, 0, 0.1)', 'rgba(255, 255, 255, 0.1)')};
border-top-color: ${cssManager.bdTheme('#3b82f6', '#60a5fa')};
border-radius: 50%;
animation: spin 0.6s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
@keyframes pulse {
0% {
transform: scale(1);
opacity: 1;
}
50% {
transform: scale(1.05);
opacity: 0.8;
}
100% {
transform: scale(1);
opacity: 1;
}
}
.profile-picture.clicking {
animation: pulse 0.3s ease-out;
}
`,
];
render(): TemplateResult {
return html`
<div class="input-wrapper">
<dees-label .label=${this.label} .description=${this.description} .required=${this.required}></dees-label>
<div
class="profile-container"
@click=${this.handleClick}
@dragover=${this.handleDragOver}
@dragleave=${this.handleDragLeave}
@drop=${this.handleDrop}
style="--size: ${this.size}px"
>
<div class="profile-picture ${this.shape} ${this.isDragging ? 'dragging' : ''} ${this.isLoading && !this.value ? 'clicking' : ''}">
${this.value ? html`
<img class="profile-image" src="${this.value}" alt="Profile picture" />
` : html`
<dees-icon class="placeholder-icon" icon="lucide:user" iconSize="${this.size * 0.5}"></dees-icon>
`}
${this.isDragging ? html`
<div class="overlay" style="opacity: 1">
<div class="drop-zone-text">
Drop image here
</div>
</div>
` : ''}
${this.value && !this.disabled ? html`
<div class="overlay">
<div class="overlay-content">
${this.allowUpload ? html`
<button class="overlay-button" @click=${(e: Event) => { e.stopPropagation(); this.openModal(); }} title="Change picture">
<dees-icon icon="lucide:pencil" iconSize="20"></dees-icon>
</button>
` : ''}
${this.allowDelete ? html`
<button class="overlay-button delete" @click=${(e: Event) => { e.stopPropagation(); this.deletePicture(); }} title="Delete picture">
<dees-icon icon="lucide:trash2" iconSize="20"></dees-icon>
</button>
` : ''}
</div>
</div>
` : ''}
${this.isLoading && !this.value ? html`
<div class="loading-overlay show">
<div class="loading-spinner"></div>
</div>
` : ''}
</div>
</div>
<input
type="file"
class="hidden-input"
accept="${this.acceptedFormats.join(',')}"
@change=${this.handleFileSelect}
/>
</div>
`;
}
private handleClick(): void {
if (this.disabled || !this.allowUpload) return;
if (!this.value) {
// If no image, open file picker
this.isLoading = true;
const input = this.shadowRoot!.querySelector('.hidden-input') as HTMLInputElement;
// Set up a focus handler to detect when the dialog is closed without selection
const handleFocus = () => {
setTimeout(() => {
// Check if no file was selected
if (!input.files || input.files.length === 0) {
this.isLoading = false;
}
window.removeEventListener('focus', handleFocus);
}, 300);
};
window.addEventListener('focus', handleFocus);
input.click();
}
}
private handleFileSelect(event: Event): void {
const input = event.target as HTMLInputElement;
const file = input.files?.[0];
// Always reset loading state when file dialog interaction completes
this.isLoading = false;
if (file) {
this.processFile(file);
}
// Reset input to allow selecting the same file again
input.value = '';
}
private handleDragOver(event: DragEvent): void {
event.preventDefault();
if (!this.disabled && this.allowUpload) {
this.isDragging = true;
}
}
private handleDragLeave(): void {
this.isDragging = false;
}
private handleDrop(event: DragEvent): void {
event.preventDefault();
this.isDragging = false;
if (this.disabled || !this.allowUpload) return;
const file = event.dataTransfer?.files[0];
if (file) {
this.processFile(file);
}
}
private async processFile(file: File): Promise<void> {
// Validate file type
if (!this.acceptedFormats.includes(file.type)) {
console.error('Invalid file type:', file.type);
return;
}
// Validate file size
if (file.size > this.maxFileSize) {
console.error('File too large:', file.size);
return;
}
// Read file as base64
const reader = new FileReader();
reader.onload = async (e) => {
const base64 = e.target?.result as string;
// Open modal for cropping
await this.openModal(base64);
};
reader.readAsDataURL(file);
}
private async openModal(initialImage?: string): Promise<void> {
const imageToEdit = initialImage || this.value;
if (!imageToEdit) {
// If no image provided, open file picker
const input = this.shadowRoot!.querySelector('.hidden-input') as HTMLInputElement;
input.click();
return;
}
// Create and show modal
this.modalInstance = new ProfilePictureModal();
this.modalInstance.shape = this.shape;
this.modalInstance.initialImage = imageToEdit;
this.modalInstance.outputSize = this.outputSize;
this.modalInstance.outputQuality = this.outputQuality;
this.modalInstance.addEventListener('save', (event: CustomEvent) => {
this.value = event.detail.croppedImage;
this.changeSubject.next(this);
});
document.body.appendChild(this.modalInstance);
}
private deletePicture(): void {
this.value = '';
this.changeSubject.next(this);
}
public getValue(): string {
return this.value;
}
public setValue(value: string): void {
this.value = value;
}
}

View File

@ -0,0 +1,3 @@
export * from './dees-input-profilepicture.js';
export * from './profilepicture.modal.js';
export * from './profilepicture.cropper.js';

View File

@ -0,0 +1,456 @@
import type { ProfileShape } from './dees-input-profilepicture.js';
export interface CropperOptions {
container: HTMLElement;
image: string;
shape: ProfileShape;
aspectRatio: number;
minSize?: number;
outputSize?: number;
outputQuality?: number;
}
export class ImageCropper {
private options: CropperOptions;
private canvas: HTMLCanvasElement;
private ctx: CanvasRenderingContext2D;
private img: HTMLImageElement;
private overlayCanvas: HTMLCanvasElement;
private overlayCtx: CanvasRenderingContext2D;
// Crop area properties
private cropX: number = 0;
private cropY: number = 0;
private cropSize: number = 200;
private minCropSize: number = 50;
// Interaction state
private isDragging: boolean = false;
private isResizing: boolean = false;
private dragStartX: number = 0;
private dragStartY: number = 0;
private resizeHandle: string = '';
// Image properties
private imageScale: number = 1;
private imageOffsetX: number = 0;
private imageOffsetY: number = 0;
constructor(options: CropperOptions) {
this.options = {
minSize: 50,
outputSize: 800, // Higher default resolution
outputQuality: 0.95, // Higher quality
...options
};
this.canvas = document.createElement('canvas');
this.ctx = this.canvas.getContext('2d')!;
this.overlayCanvas = document.createElement('canvas');
this.overlayCtx = this.overlayCanvas.getContext('2d')!;
this.img = new Image();
}
async initialize(): Promise<void> {
// Load image
await this.loadImage();
// Setup canvases
this.setupCanvases();
// Setup event listeners
this.setupEventListeners();
// Initial render
this.render();
}
private async loadImage(): Promise<void> {
return new Promise((resolve, reject) => {
this.img.onload = () => resolve();
this.img.onerror = reject;
this.img.src = this.options.image;
});
}
private setupCanvases(): void {
const container = this.options.container;
const containerSize = Math.min(container.clientWidth, container.clientHeight);
// Set canvas sizes
this.canvas.width = containerSize;
this.canvas.height = containerSize;
this.canvas.style.width = '100%';
this.canvas.style.height = '100%';
this.canvas.style.position = 'absolute';
this.canvas.style.top = '0';
this.canvas.style.left = '0';
this.overlayCanvas.width = containerSize;
this.overlayCanvas.height = containerSize;
this.overlayCanvas.style.width = '100%';
this.overlayCanvas.style.height = '100%';
this.overlayCanvas.style.position = 'absolute';
this.overlayCanvas.style.top = '0';
this.overlayCanvas.style.left = '0';
this.overlayCanvas.style.cursor = 'move';
container.appendChild(this.canvas);
container.appendChild(this.overlayCanvas);
// Calculate image scale to fit within container (not fill)
const scale = Math.min(
containerSize / this.img.width,
containerSize / this.img.height
);
this.imageScale = scale;
this.imageOffsetX = (containerSize - this.img.width * scale) / 2;
this.imageOffsetY = (containerSize - this.img.height * scale) / 2;
// Initialize crop area
// Make the crop area fit within the actual image bounds
const scaledImageWidth = this.img.width * scale;
const scaledImageHeight = this.img.height * scale;
const maxCropSize = Math.min(scaledImageWidth, scaledImageHeight, containerSize * 0.8);
this.cropSize = maxCropSize * 0.8; // Start at 80% of max possible size
this.cropX = (containerSize - this.cropSize) / 2;
this.cropY = (containerSize - this.cropSize) / 2;
}
private setupEventListeners(): void {
this.overlayCanvas.addEventListener('mousedown', this.handleMouseDown.bind(this));
this.overlayCanvas.addEventListener('mousemove', this.handleMouseMove.bind(this));
this.overlayCanvas.addEventListener('mouseup', this.handleMouseUp.bind(this));
this.overlayCanvas.addEventListener('mouseleave', this.handleMouseUp.bind(this));
// Touch events
this.overlayCanvas.addEventListener('touchstart', this.handleTouchStart.bind(this));
this.overlayCanvas.addEventListener('touchmove', this.handleTouchMove.bind(this));
this.overlayCanvas.addEventListener('touchend', this.handleTouchEnd.bind(this));
}
private handleMouseDown(e: MouseEvent): void {
const rect = this.overlayCanvas.getBoundingClientRect();
const x = (e.clientX - rect.left) * (this.overlayCanvas.width / rect.width);
const y = (e.clientY - rect.top) * (this.overlayCanvas.height / rect.height);
const handle = this.getResizeHandle(x, y);
if (handle) {
this.isResizing = true;
this.resizeHandle = handle;
} else if (this.isInsideCropArea(x, y)) {
this.isDragging = true;
}
this.dragStartX = x;
this.dragStartY = y;
}
private handleMouseMove(e: MouseEvent): void {
const rect = this.overlayCanvas.getBoundingClientRect();
const x = (e.clientX - rect.left) * (this.overlayCanvas.width / rect.width);
const y = (e.clientY - rect.top) * (this.overlayCanvas.height / rect.height);
// Update cursor
const handle = this.getResizeHandle(x, y);
if (handle) {
this.overlayCanvas.style.cursor = this.getResizeCursor(handle);
} else if (this.isInsideCropArea(x, y)) {
this.overlayCanvas.style.cursor = 'move';
} else {
this.overlayCanvas.style.cursor = 'default';
}
// Handle dragging
if (this.isDragging) {
const dx = x - this.dragStartX;
const dy = y - this.dragStartY;
// Constrain crop area to image bounds
const minX = this.imageOffsetX;
const maxX = this.imageOffsetX + this.img.width * this.imageScale - this.cropSize;
const minY = this.imageOffsetY;
const maxY = this.imageOffsetY + this.img.height * this.imageScale - this.cropSize;
this.cropX = Math.max(minX, Math.min(maxX, this.cropX + dx));
this.cropY = Math.max(minY, Math.min(maxY, this.cropY + dy));
this.dragStartX = x;
this.dragStartY = y;
this.render();
}
// Handle resizing
if (this.isResizing) {
this.handleResize(x, y);
this.dragStartX = x;
this.dragStartY = y;
this.render();
}
}
private handleMouseUp(): void {
this.isDragging = false;
this.isResizing = false;
this.resizeHandle = '';
}
private handleTouchStart(e: TouchEvent): void {
e.preventDefault();
const touch = e.touches[0];
const mouseEvent = new MouseEvent('mousedown', {
clientX: touch.clientX,
clientY: touch.clientY
});
this.handleMouseDown(mouseEvent);
}
private handleTouchMove(e: TouchEvent): void {
e.preventDefault();
const touch = e.touches[0];
const mouseEvent = new MouseEvent('mousemove', {
clientX: touch.clientX,
clientY: touch.clientY
});
this.handleMouseMove(mouseEvent);
}
private handleTouchEnd(e: TouchEvent): void {
e.preventDefault();
this.handleMouseUp();
}
private getResizeHandle(x: number, y: number): string {
const handleSize = 20;
const handles = {
'nw': { x: this.cropX, y: this.cropY },
'ne': { x: this.cropX + this.cropSize, y: this.cropY },
'sw': { x: this.cropX, y: this.cropY + this.cropSize },
'se': { x: this.cropX + this.cropSize, y: this.cropY + this.cropSize }
};
for (const [key, pos] of Object.entries(handles)) {
if (Math.abs(x - pos.x) < handleSize && Math.abs(y - pos.y) < handleSize) {
return key;
}
}
return '';
}
private getResizeCursor(handle: string): string {
const cursors: Record<string, string> = {
'nw': 'nw-resize',
'ne': 'ne-resize',
'sw': 'sw-resize',
'se': 'se-resize'
};
return cursors[handle] || 'default';
}
private isInsideCropArea(x: number, y: number): boolean {
return x >= this.cropX && x <= this.cropX + this.cropSize &&
y >= this.cropY && y <= this.cropY + this.cropSize;
}
private handleResize(x: number, y: number): void {
const dx = x - this.dragStartX;
const dy = y - this.dragStartY;
// Get image bounds
const imgLeft = this.imageOffsetX;
const imgTop = this.imageOffsetY;
const imgRight = this.imageOffsetX + this.img.width * this.imageScale;
const imgBottom = this.imageOffsetY + this.img.height * this.imageScale;
switch (this.resizeHandle) {
case 'se':
this.cropSize = Math.max(this.minCropSize, Math.min(
this.cropSize + Math.max(dx, dy),
Math.min(
imgRight - this.cropX,
imgBottom - this.cropY
)
));
break;
case 'nw':
const newSize = Math.max(this.minCropSize, this.cropSize - Math.max(dx, dy));
const sizeDiff = this.cropSize - newSize;
const newX = this.cropX + sizeDiff;
const newY = this.cropY + sizeDiff;
if (newX >= imgLeft && newY >= imgTop) {
this.cropX = newX;
this.cropY = newY;
this.cropSize = newSize;
}
break;
case 'ne':
const neSizeDx = Math.max(dx, -dy);
const neNewSize = Math.max(this.minCropSize, this.cropSize + neSizeDx);
const neSizeDiff = neNewSize - this.cropSize;
const neNewY = this.cropY - neSizeDiff;
if (neNewY >= imgTop && this.cropX + neNewSize <= imgRight) {
this.cropY = neNewY;
this.cropSize = neNewSize;
}
break;
case 'sw':
const swSizeDx = Math.max(-dx, dy);
const swNewSize = Math.max(this.minCropSize, this.cropSize + swSizeDx);
const swSizeDiff = swNewSize - this.cropSize;
const swNewX = this.cropX - swSizeDiff;
if (swNewX >= imgLeft && this.cropY + swNewSize <= imgBottom) {
this.cropX = swNewX;
this.cropSize = swNewSize;
}
break;
}
}
private render(): void {
// Clear canvases
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
this.overlayCtx.clearRect(0, 0, this.overlayCanvas.width, this.overlayCanvas.height);
// Fill background
this.ctx.fillStyle = '#000000';
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
// Draw image
this.ctx.drawImage(
this.img,
this.imageOffsetX,
this.imageOffsetY,
this.img.width * this.imageScale,
this.img.height * this.imageScale
);
// Draw overlay only over the image area
this.overlayCtx.fillStyle = 'rgba(0, 0, 0, 0.5)';
this.overlayCtx.fillRect(
this.imageOffsetX,
this.imageOffsetY,
this.img.width * this.imageScale,
this.img.height * this.imageScale
);
// Clear crop area
this.overlayCtx.save();
if (this.options.shape === 'round') {
this.overlayCtx.beginPath();
this.overlayCtx.arc(
this.cropX + this.cropSize / 2,
this.cropY + this.cropSize / 2,
this.cropSize / 2,
0,
Math.PI * 2
);
this.overlayCtx.clip();
} else {
this.overlayCtx.beginPath();
this.overlayCtx.rect(this.cropX, this.cropY, this.cropSize, this.cropSize);
this.overlayCtx.clip();
}
this.overlayCtx.clearRect(0, 0, this.overlayCanvas.width, this.overlayCanvas.height);
this.overlayCtx.restore();
// Draw crop border
this.overlayCtx.strokeStyle = 'white';
this.overlayCtx.lineWidth = 2;
if (this.options.shape === 'round') {
this.overlayCtx.beginPath();
this.overlayCtx.arc(
this.cropX + this.cropSize / 2,
this.cropY + this.cropSize / 2,
this.cropSize / 2,
0,
Math.PI * 2
);
this.overlayCtx.stroke();
} else {
this.overlayCtx.strokeRect(this.cropX, this.cropY, this.cropSize, this.cropSize);
}
// Draw resize handles
this.drawResizeHandles();
}
private drawResizeHandles(): void {
const handleSize = 8;
const handles = [
{ x: this.cropX, y: this.cropY },
{ x: this.cropX + this.cropSize, y: this.cropY },
{ x: this.cropX, y: this.cropY + this.cropSize },
{ x: this.cropX + this.cropSize, y: this.cropY + this.cropSize }
];
this.overlayCtx.fillStyle = 'white';
handles.forEach(handle => {
this.overlayCtx.beginPath();
this.overlayCtx.arc(handle.x, handle.y, handleSize, 0, Math.PI * 2);
this.overlayCtx.fill();
});
}
async getCroppedImage(): Promise<string> {
const cropCanvas = document.createElement('canvas');
const cropCtx = cropCanvas.getContext('2d')!;
// Calculate the actual crop size in original image pixels
const scale = 1 / this.imageScale;
const originalCropSize = this.cropSize * scale;
// Use requested output size, but warn if upscaling
const outputSize = this.options.outputSize!;
if (outputSize > originalCropSize) {
console.info(`Profile picture: Upscaling from ${Math.round(originalCropSize)}px to ${outputSize}px`);
}
cropCanvas.width = outputSize;
cropCanvas.height = outputSize;
// Calculate source coordinates
const sx = (this.cropX - this.imageOffsetX) * scale;
const sy = (this.cropY - this.imageOffsetY) * scale;
const sSize = this.cropSize * scale;
// Apply shape mask if round
if (this.options.shape === 'round') {
cropCtx.beginPath();
cropCtx.arc(outputSize / 2, outputSize / 2, outputSize / 2, 0, Math.PI * 2);
cropCtx.clip();
}
// Enable image smoothing for quality
cropCtx.imageSmoothingEnabled = true;
cropCtx.imageSmoothingQuality = 'high';
// Draw cropped image
cropCtx.drawImage(
this.img,
sx, sy, sSize, sSize,
0, 0, outputSize, outputSize
);
// Detect format from original image
const isPng = this.options.image.includes('image/png');
const format = isPng ? 'image/png' : 'image/jpeg';
return cropCanvas.toDataURL(format, this.options.outputQuality);
}
destroy(): void {
this.canvas.remove();
this.overlayCanvas.remove();
}
}

View File

@ -0,0 +1,395 @@
import {
DeesElement,
customElement,
html,
property,
css,
cssManager,
state,
type TemplateResult,
} from '@design.estate/dees-element';
import * as colors from '../00colors.js';
import { cssGeistFontFamily } from '../00fonts.js';
import { zIndexRegistry } from '../00zindex.js';
import '../dees-icon.js';
import '../dees-button.js';
import '../dees-windowlayer.js';
import { DeesWindowLayer } from '../dees-windowlayer.js';
import { ImageCropper } from './profilepicture.cropper.js';
import type { ProfileShape } from './dees-input-profilepicture.js';
@customElement('dees-profilepicture-modal')
export class ProfilePictureModal extends DeesElement {
@property({ type: String })
public initialImage: string = '';
@property({ type: String })
public shape: ProfileShape = 'round';
@property({ type: Number })
public outputSize: number = 800;
@property({ type: Number })
public outputQuality: number = 0.95;
@state()
private currentStep: 'crop' | 'preview' = 'crop';
@state()
private croppedImage: string = '';
@state()
private isProcessing: boolean = false;
private cropper: ImageCropper | null = null;
private windowLayer: any;
private zIndex: number = 0;
public static styles = [
cssManager.defaultStyles,
css`
:host {
font-family: ${cssGeistFontFamily};
color: ${cssManager.bdTheme('#333', '#fff')};
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
align-items: center;
justify-content: center;
z-index: var(--z-index);
}
.modal-container {
background: ${cssManager.bdTheme('#ffffff', '#0a0a0a')};
border-radius: 12px;
border: 1px solid ${cssManager.bdTheme('rgba(0, 0, 0, 0.08)', 'rgba(255, 255, 255, 0.08)')};
box-shadow: ${cssManager.bdTheme(
'0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04)',
'0 20px 25px -5px rgba(0, 0, 0, 0.3), 0 10px 10px -5px rgba(0, 0, 0, 0.2)'
)};
width: 480px;
max-width: calc(100vw - 32px);
display: flex;
flex-direction: column;
overflow: hidden;
transform: translateY(10px) scale(0.98);
opacity: 0;
animation: modalShow 0.25s cubic-bezier(0.4, 0, 0.2, 1) forwards;
}
@keyframes modalShow {
to {
opacity: 1;
transform: translateY(0px) scale(1);
}
}
.modal-header {
height: 52px;
padding: 0 20px;
border-bottom: 1px solid ${cssManager.bdTheme('rgba(0, 0, 0, 0.06)', 'rgba(255, 255, 255, 0.06)')};
display: flex;
align-items: center;
justify-content: center;
position: relative;
flex-shrink: 0;
}
.modal-title {
font-size: 15px;
font-weight: 600;
color: ${cssManager.bdTheme('#09090b', '#fafafa')};
letter-spacing: -0.01em;
}
.close-button {
position: absolute;
right: 10px;
top: 50%;
transform: translateY(-50%);
width: 32px;
height: 32px;
border: none;
background: transparent;
cursor: pointer;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
color: ${cssManager.bdTheme('#71717a', '#71717a')};
transition: all 0.15s ease;
}
.close-button:hover {
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.05)', 'rgba(255, 255, 255, 0.05)')};
color: ${cssManager.bdTheme('#09090b', '#fafafa')};
}
.close-button:active {
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.08)', 'rgba(255, 255, 255, 0.08)')};
}
.modal-body {
flex: 1;
padding: 24px;
overflow-y: auto;
display: flex;
flex-direction: column;
align-items: center;
gap: 20px;
}
.cropper-container {
width: 100%;
max-width: 360px;
aspect-ratio: 1;
position: relative;
background: ${cssManager.bdTheme('#000000', '#000000')};
border-radius: 12px;
overflow: hidden;
box-shadow: ${cssManager.bdTheme(
'inset 0 2px 4px rgba(0, 0, 0, 0.06)',
'inset 0 2px 4px rgba(0, 0, 0, 0.2)'
)};
}
.preview-container {
display: flex;
flex-direction: column;
align-items: center;
gap: 20px;
}
.preview-image {
width: 180px;
height: 180px;
object-fit: cover;
border: 4px solid ${cssManager.bdTheme('#ffffff', '#18181b')};
box-shadow: ${cssManager.bdTheme(
'0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05)',
'0 10px 15px -3px rgba(0, 0, 0, 0.3), 0 4px 6px -2px rgba(0, 0, 0, 0.2)'
)};
}
.preview-image.round {
border-radius: 50%;
}
.preview-image.square {
border-radius: 16px;
}
.success-message {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 20px;
background: ${cssManager.bdTheme('#10b981', '#10b981')};
color: white;
border-radius: 100px;
font-weight: 500;
font-size: 14px;
animation: successPulse 0.4s ease-out;
}
@keyframes successPulse {
0% { transform: scale(0.9); opacity: 0; }
50% { transform: scale(1.02); }
100% { transform: scale(1); opacity: 1; }
}
.modal-footer {
padding: 20px 24px;
border-top: 1px solid ${cssManager.bdTheme('rgba(0, 0, 0, 0.06)', 'rgba(255, 255, 255, 0.06)')};
display: flex;
gap: 10px;
justify-content: flex-end;
}
.instructions {
text-align: center;
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
font-size: 13px;
line-height: 1.5;
max-width: 320px;
}
.loading-spinner {
width: 40px;
height: 40px;
border: 3px solid ${cssManager.bdTheme('rgba(0, 0, 0, 0.1)', 'rgba(255, 255, 255, 0.1)')};
border-top-color: ${cssManager.bdTheme('#3b82f6', '#60a5fa')};
border-radius: 50%;
animation: spin 0.6s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
@media (max-width: 768px) {
.modal-container {
width: calc(100vw - 32px);
margin: 16px;
}
.modal-body {
padding: 24px;
}
}
`,
];
async connectedCallback() {
super.connectedCallback();
// Create window layer first (it will get its own z-index)
this.windowLayer = await DeesWindowLayer.createAndShow({
blur: true,
});
this.windowLayer.addEventListener('click', () => this.close());
// Now get z-index for modal (will be above window layer)
this.zIndex = zIndexRegistry.getNextZIndex();
this.style.setProperty('--z-index', this.zIndex.toString());
// Register with z-index registry
zIndexRegistry.register(this, this.zIndex);
}
async disconnectedCallback() {
super.disconnectedCallback();
// Cleanup
if (this.cropper) {
this.cropper.destroy();
}
if (this.windowLayer) {
await this.windowLayer.destroy();
}
// Unregister from z-index registry
zIndexRegistry.unregister(this);
}
render(): TemplateResult {
return html`
<div class="modal-container" @click=${(e: Event) => e.stopPropagation()}>
<div class="modal-header">
<h3 class="modal-title">
${this.currentStep === 'crop' ? 'Adjust Image' : 'Success'}
</h3>
<button class="close-button" @click=${this.close} title="Close">
<dees-icon icon="lucide:x" iconSize="16"></dees-icon>
</button>
</div>
<div class="modal-body">
${this.currentStep === 'crop' ? html`
<div class="instructions">
Position and resize the square to select your profile area
</div>
<div class="cropper-container" id="cropperContainer"></div>
` : html`
<div class="preview-container">
${this.isProcessing ? html`
<div class="loading-spinner"></div>
<div class="instructions">Saving...</div>
` : html`
<img
class="preview-image ${this.shape}"
src="${this.croppedImage}"
alt="Cropped preview"
/>
<div class="success-message">
<dees-icon icon="lucide:check" iconSize="16"></dees-icon>
<span>Looking good!</span>
</div>
`}
</div>
`}
</div>
<div class="modal-footer">
${this.currentStep === 'crop' ? html`
<dees-button type="destructive" size="sm" @click=${this.close}>
Cancel
</dees-button>
<dees-button type="default" size="sm" @click=${this.handleCrop}>
Save
</dees-button>
` : ''}
</div>
</div>
`;
}
async firstUpdated() {
if (this.currentStep === 'crop') {
await this.initializeCropper();
}
}
private async initializeCropper(): Promise<void> {
await this.updateComplete;
const container = this.shadowRoot!.getElementById('cropperContainer');
if (!container) return;
this.cropper = new ImageCropper({
container,
image: this.initialImage,
shape: this.shape,
aspectRatio: 1,
outputSize: this.outputSize,
outputQuality: this.outputQuality,
});
await this.cropper.initialize();
}
private async handleCrop(): Promise<void> {
if (!this.cropper) return;
try {
this.isProcessing = true;
this.currentStep = 'preview';
await this.updateComplete;
// Get cropped image
const croppedData = await this.cropper.getCroppedImage();
this.croppedImage = croppedData;
// Simulate processing time for better UX
await new Promise(resolve => setTimeout(resolve, 800));
this.isProcessing = false;
// Emit save event
this.dispatchEvent(new CustomEvent('save', {
detail: { croppedImage: this.croppedImage },
bubbles: true,
composed: true
}));
// Auto close after showing success
setTimeout(() => {
this.close();
}, 1500);
} catch (error) {
console.error('Error cropping image:', error);
this.isProcessing = false;
}
}
private close(): void {
this.remove();
}
}

View File

@ -3,6 +3,8 @@ import type { IBlock } from '../../wysiwyg.types.js';
import { cssManager } from '@design.estate/dees-element'; import { cssManager } from '@design.estate/dees-element';
import { WysiwygSelection } from '../../wysiwyg.selection.js'; import { WysiwygSelection } from '../../wysiwyg.selection.js';
import hlight from 'highlight.js'; import hlight from 'highlight.js';
import { cssGeistFontFamily, cssMonoFontFamily } from '../../../00fonts.js';
import { PROGRAMMING_LANGUAGES } from '../../wysiwyg.constants.js';
/** /**
* CodeBlockHandler with improved architecture * CodeBlockHandler with improved architecture
@ -20,7 +22,7 @@ export class CodeBlockHandler extends BaseBlockHandler {
private highlightTimer: any = null; private highlightTimer: any = null;
render(block: IBlock, isSelected: boolean): string { render(block: IBlock, isSelected: boolean): string {
const language = block.metadata?.language || 'javascript'; const language = block.metadata?.language || 'typescript';
const content = block.content || ''; const content = block.content || '';
const lineCount = content.split('\n').length; const lineCount = content.split('\n').length;
@ -30,10 +32,18 @@ export class CodeBlockHandler extends BaseBlockHandler {
lineNumbersHtml += `<div class="line-number">${i}</div>`; lineNumbersHtml += `<div class="line-number">${i}</div>`;
} }
// Generate language options
const languageOptions = PROGRAMMING_LANGUAGES.map(lang => {
const value = lang.toLowerCase();
return `<option value="${value}" ${value === language ? 'selected' : ''}>${lang}</option>`;
}).join('');
return ` return `
<div class="code-block-container${isSelected ? ' selected' : ''}" data-language="${language}"> <div class="code-block-container${isSelected ? ' selected' : ''}" data-language="${language}">
<div class="code-header"> <div class="code-header">
<span class="language-label">${language}</span> <select class="language-selector" data-block-id="${block.id}">
${languageOptions}
</select>
<button class="copy-button" title="Copy code"> <button class="copy-button" title="Copy code">
<svg class="copy-icon" width="16" height="16" viewBox="0 0 16 16" fill="currentColor"> <svg class="copy-icon" width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<path d="M0 6.75C0 5.784.784 5 1.75 5h1.5a.75.75 0 010 1.5h-1.5a.25.25 0 00-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 00.25-.25v-1.5a.75.75 0 011.5 0v1.5A1.75 1.75 0 019.25 16h-7.5A1.75 1.75 0 010 14.25v-7.5z"></path> <path d="M0 6.75C0 5.784.784 5 1.75 5h1.5a.75.75 0 010 1.5h-1.5a.25.25 0 00-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 00.25-.25v-1.5a.75.75 0 011.5 0v1.5A1.75 1.75 0 019.25 16h-7.5A1.75 1.75 0 010 14.25v-7.5z"></path>
@ -60,9 +70,29 @@ export class CodeBlockHandler extends BaseBlockHandler {
const editor = element.querySelector('.code-editor') as HTMLElement; const editor = element.querySelector('.code-editor') as HTMLElement;
const container = element.querySelector('.code-block-container') as HTMLElement; const container = element.querySelector('.code-block-container') as HTMLElement;
const copyButton = element.querySelector('.copy-button') as HTMLButtonElement; const copyButton = element.querySelector('.copy-button') as HTMLButtonElement;
const languageSelector = element.querySelector('.language-selector') as HTMLSelectElement;
if (!editor || !container) return; if (!editor || !container) return;
// Setup language selector
if (languageSelector) {
languageSelector.addEventListener('change', (e) => {
const newLanguage = (e.target as HTMLSelectElement).value;
block.metadata = { ...block.metadata, language: newLanguage };
container.setAttribute('data-language', newLanguage);
// Update the syntax highlighting if content exists and not focused
if (block.content && document.activeElement !== editor) {
this.applyHighlighting(element, block);
}
// Notify about the change
if (handlers.onInput) {
handlers.onInput(new InputEvent('input'));
}
});
}
// Setup copy button // Setup copy button
if (copyButton) { if (copyButton) {
copyButton.addEventListener('click', async () => { copyButton.addEventListener('click', async () => {
@ -285,7 +315,7 @@ export class CodeBlockHandler extends BaseBlockHandler {
// Get plain text content // Get plain text content
const content = editor.textContent || ''; const content = editor.textContent || '';
const language = block.metadata?.language || 'javascript'; const language = block.metadata?.language || 'typescript';
// Apply highlighting // Apply highlighting
try { try {
@ -336,7 +366,7 @@ export class CodeBlockHandler extends BaseBlockHandler {
type: 'code', type: 'code',
content: content, content: content,
metadata: { metadata: {
language: element.querySelector('.code-block-container')?.getAttribute('data-language') || 'javascript' language: element.querySelector('.code-block-container')?.getAttribute('data-language') || 'typescript'
} }
}; };
this.applyHighlighting(element, block); this.applyHighlighting(element, block);
@ -440,13 +470,30 @@ export class CodeBlockHandler extends BaseBlockHandler {
align-items: center; align-items: center;
} }
.language-label { .language-selector {
font-size: 12px; font-size: 12px;
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')}; color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
font-weight: 500; font-weight: 500;
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 0.05em; letter-spacing: 0.05em;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; font-family: ${cssGeistFontFamily};
background: transparent;
border: 1px solid transparent;
border-radius: 4px;
padding: 4px 8px;
cursor: pointer;
transition: all 0.15s ease;
outline: none;
}
.language-selector:hover {
background: ${cssManager.bdTheme('#f9fafb', '#1f2937')};
border-color: ${cssManager.bdTheme('#e5e7eb', '#374151')};
color: ${cssManager.bdTheme('#374151', '#e5e7eb')};
}
.language-selector:focus {
border-color: ${cssManager.bdTheme('#9ca3af', '#6b7280')};
} }
/* Copy Button - Minimal */ /* Copy Button - Minimal */
@ -460,7 +507,7 @@ export class CodeBlockHandler extends BaseBlockHandler {
border-radius: 4px; border-radius: 4px;
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')}; color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
font-size: 12px; font-size: 12px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; font-family: ${cssGeistFontFamily};
cursor: pointer; cursor: pointer;
transition: all 0.15s ease; transition: all 0.15s ease;
outline: none; outline: none;
@ -515,7 +562,7 @@ export class CodeBlockHandler extends BaseBlockHandler {
.line-number { .line-number {
padding: 0 12px 0 8px; padding: 0 12px 0 8px;
color: ${cssManager.bdTheme('#9ca3af', '#4b5563')}; color: ${cssManager.bdTheme('#9ca3af', '#4b5563')};
font-family: 'SF Mono', Monaco, Consolas, monospace; font-family: ${cssMonoFontFamily};
font-size: 13px; font-size: 13px;
line-height: 20px; line-height: 20px;
height: 20px; height: 20px;
@ -538,7 +585,7 @@ export class CodeBlockHandler extends BaseBlockHandler {
display: block; display: block;
padding: 12px 16px; padding: 12px 16px;
margin: 0; margin: 0;
font-family: 'SF Mono', Monaco, Consolas, monospace; font-family: ${cssMonoFontFamily};
font-size: 13px; font-size: 13px;
line-height: 20px; line-height: 20px;
color: ${cssManager.bdTheme('#111827', '#f9fafb')}; color: ${cssManager.bdTheme('#111827', '#f9fafb')};

View File

@ -7,7 +7,7 @@ import {
css, css,
state, state,
} from '@design.estate/dees-element'; } from '@design.estate/dees-element';
import { zIndexLayers } from '../00zindex.js'; import { zIndexRegistry } from '../00zindex.js';
import { WysiwygFormatting } from './wysiwyg.formatting.js'; import { WysiwygFormatting } from './wysiwyg.formatting.js';
@ -35,6 +35,9 @@ export class DeesFormattingMenu extends DeesElement {
@state() @state()
private position: { x: number; y: number } = { x: 0, y: 0 }; private position: { x: number; y: number } = { x: 0, y: 0 };
@state()
private menuZIndex: number = 1000;
private callback: ((command: string) => void | Promise<void>) | null = null; private callback: ((command: string) => void | Promise<void>) | null = null;
public static styles = [ public static styles = [
@ -42,12 +45,15 @@ export class DeesFormattingMenu extends DeesElement {
css` css`
:host { :host {
position: fixed; position: fixed;
z-index: ${zIndexLayers.wysiwygMenus};
pointer-events: none; pointer-events: none;
top: 0;
left: 0;
width: 0;
height: 0;
} }
.formatting-menu { .formatting-menu {
position: absolute; position: fixed;
background: ${cssManager.bdTheme('#ffffff', '#262626')}; background: ${cssManager.bdTheme('#ffffff', '#262626')};
border: 1px solid ${cssManager.bdTheme('#e0e0e0', '#404040')}; border: 1px solid ${cssManager.bdTheme('#e0e0e0', '#404040')};
border-radius: 6px; border-radius: 6px;
@ -119,6 +125,9 @@ export class DeesFormattingMenu extends DeesElement {
render(): TemplateResult { render(): TemplateResult {
if (!this.visible) return html``; if (!this.visible) return html``;
// Apply z-index to host element
this.style.zIndex = this.menuZIndex.toString();
return html` return html`
<div <div
class="formatting-menu" class="formatting-menu"
@ -153,13 +162,21 @@ export class DeesFormattingMenu extends DeesElement {
console.log('FormattingMenu.show called:', { position, visible: this.visible }); console.log('FormattingMenu.show called:', { position, visible: this.visible });
this.position = position; this.position = position;
this.callback = callback; this.callback = callback;
// Get z-index from registry and apply immediately
this.menuZIndex = zIndexRegistry.getNextZIndex();
zIndexRegistry.register(this, this.menuZIndex);
this.style.zIndex = this.menuZIndex.toString();
this.visible = true; this.visible = true;
console.log('FormattingMenu.show - visible set to:', this.visible);
} }
public hide(): void { public hide(): void {
this.visible = false; this.visible = false;
this.callback = null; this.callback = null;
// Unregister from z-index registry
zIndexRegistry.unregister(this);
} }
public updatePosition(position: { x: number; y: number }): void { public updatePosition(position: { x: number; y: number }): void {

View File

@ -235,6 +235,7 @@ export class DeesInputWysiwyg extends DeesInputBase<string> {
const blockComponent = document.createElement('dees-wysiwyg-block') as any; const blockComponent = document.createElement('dees-wysiwyg-block') as any;
blockComponent.block = block; blockComponent.block = block;
blockComponent.isSelected = this.selectedBlockId === block.id; blockComponent.isSelected = this.selectedBlockId === block.id;
blockComponent.wysiwygComponent = this; // Pass parent reference
blockComponent.handlers = { blockComponent.handlers = {
onInput: (e: InputEvent) => this.inputHandler.handleBlockInput(e, block), onInput: (e: InputEvent) => this.inputHandler.handleBlockInput(e, block),
onKeyDown: (e: KeyboardEvent) => this.keyboardHandler.handleBlockKeyDown(e, block), onKeyDown: (e: KeyboardEvent) => this.keyboardHandler.handleBlockKeyDown(e, block),
@ -247,28 +248,7 @@ export class DeesInputWysiwyg extends DeesInputBase<string> {
}; };
wrapper.appendChild(blockComponent); wrapper.appendChild(blockComponent);
// Add settings button for non-divider blocks // Remove settings button - context menu will handle this
if (block.type !== 'divider') {
const settings = document.createElement('div');
settings.className = 'block-settings';
settings.innerHTML = `
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
<circle cx="12" cy="5" r="2"></circle>
<circle cx="12" cy="12" r="2"></circle>
<circle cx="12" cy="19" r="2"></circle>
</svg>
`;
settings.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
WysiwygModalManager.showBlockSettingsModal(block, () => {
this.updateValue();
// Re-render only the updated block
this.updateBlockElement(block.id);
});
});
wrapper.appendChild(settings);
}
// Add drag event listeners // Add drag event listeners
wrapper.addEventListener('dragover', (e) => this.dragDropHandler.handleDragOver(e, block)); wrapper.addEventListener('dragover', (e) => this.dragDropHandler.handleDragOver(e, block));
@ -507,13 +487,9 @@ export class DeesInputWysiwyg extends DeesInputBase<string> {
// Close menu // Close menu
this.closeSlashMenu(false); this.closeSlashMenu(false);
// If it's a code block, ask for language // If it's a code block, default to TypeScript
if (type === 'code') { if (type === 'code') {
const language = await WysiwygModalManager.showLanguageSelectionModal(); currentBlock.metadata = { language: 'typescript' };
if (!language) {
return; // User cancelled
}
currentBlock.metadata = { language };
} }
// Transform the current block // Transform the current block

View File

@ -1,6 +1,5 @@
import { import {
customElement, customElement,
property,
html, html,
DeesElement, DeesElement,
type TemplateResult, type TemplateResult,
@ -8,7 +7,8 @@ import {
css, css,
state, state,
} from '@design.estate/dees-element'; } from '@design.estate/dees-element';
import { zIndexLayers } from '../00zindex.js'; import { zIndexRegistry } from '../00zindex.js';
import '../dees-icon.js';
import { type ISlashMenuItem } from './wysiwyg.types.js'; import { type ISlashMenuItem } from './wysiwyg.types.js';
import { WysiwygShortcuts } from './wysiwyg.shortcuts.js'; import { WysiwygShortcuts } from './wysiwyg.shortcuts.js';
@ -43,6 +43,9 @@ export class DeesSlashMenu extends DeesElement {
@state() @state()
private selectedIndex: number = 0; private selectedIndex: number = 0;
@state()
private menuZIndex: number = 1000;
private callback: ((type: string) => void) | null = null; private callback: ((type: string) => void) | null = null;
public static styles = [ public static styles = [
@ -50,16 +53,19 @@ export class DeesSlashMenu extends DeesElement {
css` css`
:host { :host {
position: fixed; position: fixed;
z-index: ${zIndexLayers.wysiwygMenus};
pointer-events: none; pointer-events: none;
top: 0;
left: 0;
width: 0;
height: 0;
} }
.slash-menu { .slash-menu {
position: absolute; position: fixed;
background: ${cssManager.bdTheme('#ffffff', '#262626')}; background: ${cssManager.bdTheme('#ffffff', '#09090b')};
border: 1px solid ${cssManager.bdTheme('#e0e0e0', '#404040')}; border: 1px solid ${cssManager.bdTheme('#e5e7eb', '#27272a')};
border-radius: 8px; border-radius: 4px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12); box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1), 0 1px 2px rgba(0, 0, 0, 0.06);
padding: 4px; padding: 4px;
min-width: 220px; min-width: 220px;
max-height: 300px; max-height: 300px;
@ -72,7 +78,7 @@ export class DeesSlashMenu extends DeesElement {
@keyframes fadeInScale { @keyframes fadeInScale {
from { from {
opacity: 0; opacity: 0;
transform: scale(0.95) translateY(-10px); transform: scale(0.98) translateY(-2px);
} }
to { to {
opacity: 1; opacity: 1;
@ -81,37 +87,35 @@ export class DeesSlashMenu extends DeesElement {
} }
.slash-menu-item { .slash-menu-item {
padding: 10px 12px; padding: 8px 10px;
cursor: pointer; cursor: pointer;
transition: all 0.15s ease; transition: all 0.15s ease;
display: flex; display: flex;
align-items: center; align-items: center;
gap: 12px; gap: 12px;
border-radius: 4px; border-radius: 3px;
color: ${cssManager.bdTheme('#000000', '#e0e0e0')}; color: ${cssManager.bdTheme('#09090b', '#fafafa')};
font-size: 14px; font-size: 14px;
} }
.slash-menu-item:hover, .slash-menu-item:hover,
.slash-menu-item.selected { .slash-menu-item.selected {
background: ${cssManager.bdTheme('#f0f0f0', '#333333')}; background: ${cssManager.bdTheme('#f4f4f5', '#27272a')};
color: ${cssManager.bdTheme('#000000', '#ffffff')}; color: ${cssManager.bdTheme('#09090b', '#fafafa')};
} }
.slash-menu-item .icon { .slash-menu-item .icon {
width: 24px; width: 20px;
height: 24px; height: 20px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
font-size: 16px; color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
color: ${cssManager.bdTheme('#666', '#999')};
font-weight: 600;
} }
.slash-menu-item:hover .icon, .slash-menu-item:hover .icon,
.slash-menu-item.selected .icon { .slash-menu-item.selected .icon {
color: ${cssManager.bdTheme('#0066cc', '#4d94ff')}; color: ${cssManager.bdTheme('#3b82f6', '#3b82f6')};
} }
`, `,
]; ];
@ -119,6 +123,9 @@ export class DeesSlashMenu extends DeesElement {
render(): TemplateResult { render(): TemplateResult {
if (!this.visible) return html``; if (!this.visible) return html``;
// Ensure z-index is applied to host element
this.style.zIndex = this.menuZIndex.toString();
const menuItems = this.getFilteredMenuItems(); const menuItems = this.getFilteredMenuItems();
return html` return html`
@ -134,7 +141,7 @@ export class DeesSlashMenu extends DeesElement {
data-item-type="${item.type}" data-item-type="${item.type}"
data-item-index="${index}" data-item-index="${index}"
> >
<span class="icon">${item.icon}</span> <dees-icon class="icon" .icon="${item.icon}" iconSize="16"></dees-icon>
<span>${item.label}</span> <span>${item.label}</span>
</div> </div>
`)} `)}
@ -162,6 +169,12 @@ export class DeesSlashMenu extends DeesElement {
this.callback = callback; this.callback = callback;
this.filter = ''; this.filter = '';
this.selectedIndex = 0; this.selectedIndex = 0;
// Get z-index from registry and apply immediately
this.menuZIndex = zIndexRegistry.getNextZIndex();
zIndexRegistry.register(this, this.menuZIndex);
this.style.zIndex = this.menuZIndex.toString();
this.visible = true; this.visible = true;
} }
@ -170,6 +183,9 @@ export class DeesSlashMenu extends DeesElement {
this.callback = null; this.callback = null;
this.filter = ''; this.filter = '';
this.selectedIndex = 0; this.selectedIndex = 0;
// Unregister from z-index registry
zIndexRegistry.unregister(this);
} }
public updateFilter(filter: string): void { public updateFilter(filter: string): void {

View File

@ -13,6 +13,8 @@ import { WysiwygBlocks } from './wysiwyg.blocks.js';
import { WysiwygSelection } from './wysiwyg.selection.js'; import { WysiwygSelection } from './wysiwyg.selection.js';
import { BlockRegistry, type IBlockEventHandlers } from './blocks/index.js'; import { BlockRegistry, type IBlockEventHandlers } from './blocks/index.js';
import './wysiwyg.blockregistration.js'; import './wysiwyg.blockregistration.js';
import { WysiwygShortcuts } from './wysiwyg.shortcuts.js';
import '../dees-contextmenu.js';
declare global { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {
@ -37,6 +39,9 @@ export class DeesWysiwygBlock extends DeesElement {
@property({ type: Object }) @property({ type: Object })
public handlers: IBlockEventHandlers; public handlers: IBlockEventHandlers;
@property({ type: Object })
public wysiwygComponent: any; // Reference to parent dees-input-wysiwyg
// Reference to the editable block element // Reference to the editable block element
private blockElement: HTMLDivElement | null = null; private blockElement: HTMLDivElement | null = null;
@ -685,6 +690,125 @@ export class DeesWysiwygBlock extends DeesElement {
/**
* Get context menu items for this block
*/
public getContextMenuItems(): any[] {
if (!this.block || this.block.type === 'divider') {
return [];
}
const blockTypes = WysiwygShortcuts.getSlashMenuItems();
const currentType = this.block.type;
// Use the parent reference passed from dees-input-wysiwyg
const wysiwygComponent = this.wysiwygComponent;
const blockId = this.block.id;
// Create submenu items for block type change
const blockTypeItems = blockTypes
.filter(item => item.type !== currentType && item.type !== 'divider')
.map(item => ({
name: item.label,
iconName: item.icon.replace('lucide:', ''),
action: async () => {
if (wysiwygComponent && wysiwygComponent.blockOperations) {
// Transform the block type
const blockToTransform = wysiwygComponent.blocks.find((b: IBlock) => b.id === blockId);
if (blockToTransform) {
blockToTransform.type = item.type;
blockToTransform.content = blockToTransform.content || '';
// Handle special metadata for different block types
if (item.type === 'code') {
blockToTransform.metadata = { language: 'typescript' };
} else if (item.type === 'list') {
blockToTransform.metadata = { listType: 'bullet' };
} else if (item.type === 'image') {
blockToTransform.content = '';
blockToTransform.metadata = { url: '', loading: false };
} else if (item.type === 'youtube') {
blockToTransform.content = '';
blockToTransform.metadata = { videoId: '', url: '' };
} else if (item.type === 'markdown') {
blockToTransform.metadata = { showPreview: false };
} else if (item.type === 'html') {
blockToTransform.metadata = { showPreview: false };
} else if (item.type === 'attachment') {
blockToTransform.content = '';
blockToTransform.metadata = { files: [] };
}
// Update the block element
wysiwygComponent.updateBlockElement(blockId);
wysiwygComponent.updateValue();
// Focus the block after transformation
requestAnimationFrame(() => {
wysiwygComponent.blockOperations.focusBlock(blockId);
});
}
}
}
}));
const menuItems: any[] = [
{
name: 'Change Type',
iconName: 'type',
submenu: blockTypeItems
}
];
// Add copy/cut/paste for editable blocks
if (!['image', 'divider', 'youtube', 'attachment'].includes(this.block.type)) {
menuItems.push(
{ divider: true },
{
name: 'Cut',
iconName: 'scissors',
shortcut: 'Cmd+X',
action: async () => {
document.execCommand('cut');
}
},
{
name: 'Copy',
iconName: 'copy',
shortcut: 'Cmd+C',
action: async () => {
document.execCommand('copy');
}
},
{
name: 'Paste',
iconName: 'clipboard',
shortcut: 'Cmd+V',
action: async () => {
document.execCommand('paste');
}
}
);
}
// Add delete option
menuItems.push(
{ divider: true },
{
name: 'Delete Block',
iconName: 'trash2',
action: async () => {
if (wysiwygComponent && wysiwygComponent.blockOperations) {
wysiwygComponent.blockOperations.deleteBlock(blockId);
}
}
}
);
return menuItems;
}
/** /**
* Gets content split at cursor position * Gets content split at cursor position
*/ */

View File

@ -11,6 +11,8 @@ export class WysiwygDragDropHandler {
private initialBlockY: number = 0; private initialBlockY: number = 0;
private draggedBlockElement: HTMLElement | null = null; private draggedBlockElement: HTMLElement | null = null;
private draggedBlockHeight: number = 0; private draggedBlockHeight: number = 0;
private draggedBlockContentHeight: number = 0;
private draggedBlockMarginTop: number = 0;
private lastUpdateTime: number = 0; private lastUpdateTime: number = 0;
private updateThrottle: number = 80; // milliseconds private updateThrottle: number = 80; // milliseconds
@ -48,11 +50,33 @@ export class WysiwygDragDropHandler {
this.initialMouseY = e.clientY; this.initialMouseY = e.clientY;
this.draggedBlockElement = this.component.editorContentRef.querySelector(`[data-block-id="${block.id}"]`); this.draggedBlockElement = this.component.editorContentRef.querySelector(`[data-block-id="${block.id}"]`);
if (this.draggedBlockElement) { if (this.draggedBlockElement) {
// Get the wrapper rect for measurements
const rect = this.draggedBlockElement.getBoundingClientRect(); const rect = this.draggedBlockElement.getBoundingClientRect();
this.draggedBlockHeight = rect.height;
this.initialBlockY = rect.top; this.initialBlockY = rect.top;
// Get the inner block element for proper measurements
const innerBlock = this.draggedBlockElement.querySelector('.block');
if (innerBlock) {
const innerRect = innerBlock.getBoundingClientRect();
const computedStyle = window.getComputedStyle(innerBlock);
this.draggedBlockMarginTop = parseInt(computedStyle.marginTop) || 0;
this.draggedBlockContentHeight = innerRect.height;
}
// The drop indicator should match the wrapper height exactly
// The wrapper already includes all the space the block occupies
this.draggedBlockHeight = rect.height;
console.log('Drag measurements:', {
wrapperHeight: rect.height,
marginTop: this.draggedBlockMarginTop,
dropIndicatorHeight: this.draggedBlockHeight,
contentHeight: this.draggedBlockContentHeight,
blockId: block.id
});
// Create drop indicator // Create drop indicator
this.createDropIndicator(); this.createDropIndicator();
@ -98,6 +122,8 @@ export class WysiwygDragDropHandler {
this.dragOverPosition = null; this.dragOverPosition = null;
this.draggedBlockElement = null; this.draggedBlockElement = null;
this.draggedBlockHeight = 0; this.draggedBlockHeight = 0;
this.draggedBlockContentHeight = 0;
this.draggedBlockMarginTop = 0;
this.initialBlockY = 0; this.initialBlockY = 0;
// Update component state // Update component state
@ -284,34 +310,93 @@ export class WysiwygDragDropHandler {
if (!this.dropIndicator || !this.draggedBlockElement) return; if (!this.dropIndicator || !this.draggedBlockElement) return;
this.dropIndicator.style.display = 'block'; this.dropIndicator.style.display = 'block';
this.dropIndicator.style.height = `${this.draggedBlockHeight}px`;
const containerRect = this.component.editorContentRef.getBoundingClientRect(); const containerRect = this.component.editorContentRef.getBoundingClientRect();
// Calculate where the block will actually land
let topPosition = 0; let topPosition = 0;
if (targetIndex === 0) { // Build array of visual block positions (excluding dragged block)
// Before first block const visualBlocks: { index: number, top: number, bottom: number }[] = [];
topPosition = 0;
} else { for (let i = 0; i < blocks.length; i++) {
// After a specific block if (i === draggedIndex) continue; // Skip the dragged block
const prevIndex = targetIndex - 1;
let blockCount = 0;
// Find the visual position of the block that will be before our dropped block const block = blocks[i];
for (let i = 0; i < blocks.length; i++) { const rect = block.getBoundingClientRect();
if (i === draggedIndex) continue; // Skip the dragged block let top = rect.top - containerRect.top;
let bottom = rect.bottom - containerRect.top;
if (blockCount === prevIndex) {
const rect = blocks[i].getBoundingClientRect(); // Account for any transforms
topPosition = rect.bottom - containerRect.top + 16; // 16px gap const transform = window.getComputedStyle(block).transform;
break; if (transform && transform !== 'none') {
const matrix = new DOMMatrix(transform);
const yOffset = matrix.m42;
top += yOffset;
bottom += yOffset;
}
visualBlocks.push({ index: i, top, bottom });
}
// Sort by visual position
visualBlocks.sort((a, b) => a.top - b.top);
// Adjust targetIndex to account for excluded dragged block
let adjustedTargetIndex = targetIndex;
if (targetIndex > draggedIndex) {
adjustedTargetIndex--; // Reduce by 1 since dragged block is not in visualBlocks
}
// Calculate drop position
// Get the margin that will be applied based on the dragged block type
let blockMargin = 16; // default margin
if (this.draggedBlockElement) {
const draggedBlock = this.component.blocks.find(b => b.id === this.draggedBlockId);
if (draggedBlock) {
const blockType = draggedBlock.type;
if (blockType === 'heading-1' || blockType === 'heading-2' || blockType === 'heading-3') {
blockMargin = 24;
} else if (blockType === 'code' || blockType === 'quote') {
blockMargin = 20;
} }
blockCount++;
} }
} }
this.dropIndicator.style.top = `${topPosition}px`; if (adjustedTargetIndex === 0) {
// Insert at the very top - no margin needed for first block
topPosition = 0;
} else if (adjustedTargetIndex >= visualBlocks.length) {
// Insert at the end
const lastBlock = visualBlocks[visualBlocks.length - 1];
if (lastBlock) {
topPosition = lastBlock.bottom;
// Add margin that will be applied to the dropped block
topPosition += blockMargin;
}
} else {
// Insert between blocks
const blockBefore = visualBlocks[adjustedTargetIndex - 1];
if (blockBefore) {
topPosition = blockBefore.bottom;
// Add margin that will be applied to the dropped block
topPosition += blockMargin;
}
}
// Set the indicator height to match the dragged block
this.dropIndicator.style.height = `${this.draggedBlockHeight}px`;
// Set position
this.dropIndicator.style.top = `${Math.max(0, topPosition)}px`;
console.log('Drop indicator update:', {
targetIndex,
adjustedTargetIndex,
draggedIndex,
topPosition,
height: this.draggedBlockHeight,
blockMargin,
visualBlocks: visualBlocks.map(b => ({ index: b.index, top: b.top, bottom: b.bottom }))
});
} }
/** /**

View File

@ -49,19 +49,19 @@ export class WysiwygShortcuts {
static getSlashMenuItems(): ISlashMenuItem[] { static getSlashMenuItems(): ISlashMenuItem[] {
return [ return [
{ type: 'paragraph', label: 'Paragraph', icon: '' }, { type: 'paragraph', label: 'Paragraph', icon: 'lucide:pilcrow' },
{ type: 'heading-1', label: 'Heading 1', icon: 'H₁' }, { type: 'heading-1', label: 'Heading 1', icon: 'lucide:heading1' },
{ type: 'heading-2', label: 'Heading 2', icon: 'H₂' }, { type: 'heading-2', label: 'Heading 2', icon: 'lucide:heading2' },
{ type: 'heading-3', label: 'Heading 3', icon: 'H₃' }, { type: 'heading-3', label: 'Heading 3', icon: 'lucide:heading3' },
{ type: 'quote', label: 'Quote', icon: '"' }, { type: 'quote', label: 'Quote', icon: 'lucide:quote' },
{ type: 'code', label: 'Code', icon: '<>' }, { type: 'code', label: 'Code Block', icon: 'lucide:fileCode' },
{ type: 'list', label: 'List', icon: '' }, { type: 'list', label: 'Bullet List', icon: 'lucide:list' },
{ type: 'image', label: 'Image', icon: '🖼' }, { type: 'image', label: 'Image', icon: 'lucide:image' },
{ type: 'divider', label: 'Divider', icon: '' }, { type: 'divider', label: 'Divider', icon: 'lucide:minus' },
{ type: 'youtube', label: 'YouTube', icon: '▶️' }, { type: 'youtube', label: 'YouTube', icon: 'lucide:youtube' },
{ type: 'markdown', label: 'Markdown', icon: 'M↓' }, { type: 'markdown', label: 'Markdown', icon: 'lucide:fileText' },
{ type: 'html', label: 'HTML', icon: '</>' }, { type: 'html', label: 'HTML', icon: 'lucide:code' },
{ type: 'attachment', label: 'File Attachment', icon: '📎' }, { type: 'attachment', label: 'File Attachment', icon: 'lucide:paperclip' },
]; ];
} }

View File

@ -7,23 +7,25 @@ export const wysiwygStyles = css`
} }
.wysiwyg-container { .wysiwyg-container {
background: ${cssManager.bdTheme('#ffffff', '#1a1a1a')}; background: ${cssManager.bdTheme('#ffffff', '#09090b')};
border: 1px solid ${cssManager.bdTheme('#e0e0e0', '#333')}; border: 1px solid ${cssManager.bdTheme('#e5e7eb', '#27272a')};
border-radius: 8px; border-radius: 6px;
min-height: 200px; min-height: 200px;
padding: 32px 40px; padding: 24px;
position: relative; position: relative;
transition: all 0.2s ease; transition: all 0.2s ease;
color: ${cssManager.bdTheme('#000000', '#ffffff')}; color: ${cssManager.bdTheme('#09090b', '#fafafa')};
} }
.wysiwyg-container:hover { .wysiwyg-container:hover {
border-color: ${cssManager.bdTheme('#d0d0d0', '#444')}; border-color: ${cssManager.bdTheme('#d1d5db', '#3f3f46')};
} }
.wysiwyg-container:focus-within { .wysiwyg-container:focus-within {
border-color: ${cssManager.bdTheme('#0066cc', '#4d94ff')}; outline: 2px solid transparent;
box-shadow: 0 0 0 3px ${cssManager.bdTheme('rgba(0, 102, 204, 0.1)', 'rgba(77, 148, 255, 0.1)')}; outline-offset: 2px;
box-shadow: 0 0 0 2px ${cssManager.bdTheme('#f4f4f5', '#18181b')}, 0 0 0 4px ${cssManager.bdTheme('rgba(59, 130, 246, 0.5)', 'rgba(59, 130, 246, 0.5)')};
border-color: ${cssManager.bdTheme('#3b82f6', '#3b82f6')};
} }
/* Visual hint for text selection */ /* Visual hint for text selection */
@ -44,7 +46,7 @@ export const wysiwygStyles = css`
position: relative; position: relative;
transition: all 0.15s ease; transition: all 0.15s ease;
min-height: 1.6em; min-height: 1.6em;
color: ${cssManager.bdTheme('#000000', '#e0e0e0')}; color: ${cssManager.bdTheme('#09090b', '#fafafa')};
} }
/* First and last blocks don't need extra spacing */ /* First and last blocks don't need extra spacing */
@ -57,8 +59,9 @@ export const wysiwygStyles = css`
} }
.block.selected { .block.selected {
background: ${cssManager.bdTheme('rgba(0, 102, 204, 0.05)', 'rgba(77, 148, 255, 0.08)')}; background: ${cssManager.bdTheme('rgba(59, 130, 246, 0.05)', 'rgba(59, 130, 246, 0.05)')};
box-shadow: inset 0 0 0 2px ${cssManager.bdTheme('rgba(0, 102, 204, 0.2)', 'rgba(77, 148, 255, 0.2)')}; outline: 2px solid ${cssManager.bdTheme('rgba(59, 130, 246, 0.2)', 'rgba(59, 130, 246, 0.2)')};
outline-offset: -2px;
border-radius: 4px; border-radius: 4px;
margin-left: -8px; margin-left: -8px;
margin-right: -8px; margin-right: -8px;
@ -78,7 +81,7 @@ export const wysiwygStyles = css`
.block.paragraph:empty::before { .block.paragraph:empty::before {
content: "Type '/' for commands..."; content: "Type '/' for commands...";
color: ${cssManager.bdTheme('#999', '#666')}; color: ${cssManager.bdTheme('#71717a', '#71717a')};
pointer-events: none; pointer-events: none;
font-size: 16px; font-size: 16px;
line-height: 1.6; line-height: 1.6;
@ -89,12 +92,12 @@ export const wysiwygStyles = css`
font-size: 32px; font-size: 32px;
font-weight: 700; font-weight: 700;
line-height: 1.2; line-height: 1.2;
color: ${cssManager.bdTheme('#000000', '#ffffff')}; color: ${cssManager.bdTheme('#09090b', '#fafafa')};
} }
.block.heading-1:empty::before { .block.heading-1:empty::before {
content: "Heading 1"; content: "Heading 1";
color: ${cssManager.bdTheme('#999', '#666')}; color: ${cssManager.bdTheme('#71717a', '#71717a')};
pointer-events: none; pointer-events: none;
font-size: 32px; font-size: 32px;
line-height: 1.2; line-height: 1.2;
@ -105,12 +108,12 @@ export const wysiwygStyles = css`
font-size: 24px; font-size: 24px;
font-weight: 600; font-weight: 600;
line-height: 1.3; line-height: 1.3;
color: ${cssManager.bdTheme('#000000', '#ffffff')}; color: ${cssManager.bdTheme('#09090b', '#fafafa')};
} }
.block.heading-2:empty::before { .block.heading-2:empty::before {
content: "Heading 2"; content: "Heading 2";
color: ${cssManager.bdTheme('#999', '#666')}; color: ${cssManager.bdTheme('#71717a', '#71717a')};
pointer-events: none; pointer-events: none;
font-size: 24px; font-size: 24px;
line-height: 1.3; line-height: 1.3;
@ -121,12 +124,12 @@ export const wysiwygStyles = css`
font-size: 20px; font-size: 20px;
font-weight: 600; font-weight: 600;
line-height: 1.4; line-height: 1.4;
color: ${cssManager.bdTheme('#000000', '#ffffff')}; color: ${cssManager.bdTheme('#09090b', '#fafafa')};
} }
.block.heading-3:empty::before { .block.heading-3:empty::before {
content: "Heading 3"; content: "Heading 3";
color: ${cssManager.bdTheme('#999', '#666')}; color: ${cssManager.bdTheme('#71717a', '#71717a')};
pointer-events: none; pointer-events: none;
font-size: 20px; font-size: 20px;
line-height: 1.4; line-height: 1.4;
@ -134,10 +137,10 @@ export const wysiwygStyles = css`
} }
.block.quote { .block.quote {
border-left: 3px solid ${cssManager.bdTheme('#0066cc', '#4d94ff')}; border-left: 2px solid ${cssManager.bdTheme('#e5e7eb', '#27272a')};
padding-left: 20px; padding-left: 20px;
font-style: italic; font-style: italic;
color: ${cssManager.bdTheme('#555', '#b0b0b0')}; color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
margin-left: 0; margin-left: 0;
margin-right: 0; margin-right: 0;
line-height: 1.6; line-height: 1.6;
@ -145,7 +148,7 @@ export const wysiwygStyles = css`
.block.quote:empty::before { .block.quote:empty::before {
content: "Quote"; content: "Quote";
color: ${cssManager.bdTheme('#999', '#666')}; color: ${cssManager.bdTheme('#71717a', '#71717a')};
pointer-events: none; pointer-events: none;
font-size: 16px; font-size: 16px;
line-height: 1.6; line-height: 1.6;
@ -162,33 +165,33 @@ export const wysiwygStyles = css`
position: absolute; position: absolute;
top: 0; top: 0;
right: 0; right: 0;
background: ${cssManager.bdTheme('#e1e4e8', '#333333')}; background: ${cssManager.bdTheme('#f4f4f5', '#27272a')};
color: ${cssManager.bdTheme('#586069', '#8b949e')}; color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
padding: 4px 12px; padding: 4px 12px;
font-size: 12px; font-size: 12px;
border-radius: 0 6px 0 6px; border-radius: 0 4px 0 4px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
text-transform: lowercase; text-transform: lowercase;
z-index: 1; z-index: 1;
} }
.block.code { .block.code {
background: ${cssManager.bdTheme('#f8f8f8', '#0d0d0d')}; background: ${cssManager.bdTheme('#f4f4f5', '#18181b')};
border: 1px solid ${cssManager.bdTheme('#e0e0e0', '#2a2a2a')}; border: 1px solid ${cssManager.bdTheme('#e5e7eb', '#27272a')};
border-radius: 6px; border-radius: 4px;
padding: 16px 20px; padding: 16px;
padding-top: 32px; /* Make room for language indicator */ padding-top: 32px; /* Make room for language indicator */
font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Fira Code', monospace; font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Fira Code', monospace;
font-size: 14px; font-size: 14px;
line-height: 1.5; line-height: 1.5;
white-space: pre-wrap; white-space: pre-wrap;
color: ${cssManager.bdTheme('#24292e', '#e1e4e8')}; color: ${cssManager.bdTheme('#09090b', '#fafafa')};
overflow-x: auto; overflow-x: auto;
} }
.block.code:empty::before { .block.code:empty::before {
content: "// Code block"; content: "// Code block";
color: ${cssManager.bdTheme('#999', '#666')}; color: ${cssManager.bdTheme('#71717a', '#71717a')};
pointer-events: none; pointer-events: none;
font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Fira Code', monospace; font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Fira Code', monospace;
font-size: 14px; font-size: 14px;
@ -233,16 +236,16 @@ export const wysiwygStyles = css`
.block.divider hr { .block.divider hr {
border: none; border: none;
border-top: 1px solid ${cssManager.bdTheme('#e0e0e0', '#333')}; border-top: 1px solid ${cssManager.bdTheme('#e5e7eb', '#27272a')};
margin: 0; margin: 0;
} }
.slash-menu { .slash-menu {
position: absolute; position: absolute;
background: ${cssManager.bdTheme('#ffffff', '#262626')}; background: ${cssManager.bdTheme('#ffffff', '#09090b')};
border: 1px solid ${cssManager.bdTheme('#e0e0e0', '#404040')}; border: 1px solid ${cssManager.bdTheme('#e5e7eb', '#27272a')};
border-radius: 8px; border-radius: 4px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12); box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1), 0 1px 2px rgba(0, 0, 0, 0.06);
padding: 4px; padding: 4px;
z-index: 1000; z-index: 1000;
min-width: 220px; min-width: 220px;
@ -253,21 +256,21 @@ export const wysiwygStyles = css`
} }
.slash-menu-item { .slash-menu-item {
padding: 10px 12px; padding: 8px 10px;
cursor: pointer; cursor: pointer;
transition: all 0.15s ease; transition: all 0.15s ease;
display: flex; display: flex;
align-items: center; align-items: center;
gap: 12px; gap: 12px;
border-radius: 4px; border-radius: 3px;
color: ${cssManager.bdTheme('#000000', '#e0e0e0')}; color: ${cssManager.bdTheme('#09090b', '#fafafa')};
font-size: 14px; font-size: 14px;
} }
.slash-menu-item:hover, .slash-menu-item:hover,
.slash-menu-item.selected { .slash-menu-item.selected {
background: ${cssManager.bdTheme('#f0f0f0', '#333333')}; background: ${cssManager.bdTheme('#f4f4f5', '#27272a')};
color: ${cssManager.bdTheme('#000000', '#ffffff')}; color: ${cssManager.bdTheme('#09090b', '#fafafa')};
} }
.slash-menu-item .icon { .slash-menu-item .icon {
@ -277,23 +280,23 @@ export const wysiwygStyles = css`
align-items: center; align-items: center;
justify-content: center; justify-content: center;
font-size: 16px; font-size: 16px;
color: ${cssManager.bdTheme('#666', '#999')}; color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
font-weight: 600; font-weight: 600;
} }
.slash-menu-item:hover .icon, .slash-menu-item:hover .icon,
.slash-menu-item.selected .icon { .slash-menu-item.selected .icon {
color: ${cssManager.bdTheme('#0066cc', '#4d94ff')}; color: ${cssManager.bdTheme('#3b82f6', '#3b82f6')};
} }
.toolbar { .toolbar {
position: absolute; position: absolute;
top: -40px; top: -40px;
left: 0; left: 0;
background: ${cssManager.bdTheme('#ffffff', '#262626')}; background: ${cssManager.bdTheme('#ffffff', '#09090b')};
border: 1px solid ${cssManager.bdTheme('#e0e0e0', '#404040')}; border: 1px solid ${cssManager.bdTheme('#e5e7eb', '#27272a')};
border-radius: 6px; border-radius: 4px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1); box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1), 0 1px 2px rgba(0, 0, 0, 0.06);
padding: 4px; padding: 4px;
display: none; display: none;
gap: 4px; gap: 4px;
@ -310,17 +313,17 @@ export const wysiwygStyles = css`
border: none; border: none;
background: transparent; background: transparent;
cursor: pointer; cursor: pointer;
border-radius: 4px; border-radius: 3px;
transition: all 0.15s ease; transition: all 0.15s ease;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
color: ${cssManager.bdTheme('#000000', '#e0e0e0')}; color: ${cssManager.bdTheme('#09090b', '#fafafa')};
} }
.toolbar-button:hover { .toolbar-button:hover {
background: ${cssManager.bdTheme('#f0f0f0', '#333333')}; background: ${cssManager.bdTheme('#f4f4f5', '#27272a')};
color: ${cssManager.bdTheme('#0066cc', '#4d94ff')}; color: ${cssManager.bdTheme('#3b82f6', '#3b82f6')};
} }
/* Drag and Drop Styles */ /* Drag and Drop Styles */
@ -360,7 +363,7 @@ export const wysiwygStyles = css`
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
color: ${cssManager.bdTheme('#999', '#666')}; color: ${cssManager.bdTheme('#71717a', '#71717a')};
border-radius: 4px; border-radius: 4px;
} }
@ -375,13 +378,13 @@ export const wysiwygStyles = css`
} }
.drag-handle:hover { .drag-handle:hover {
color: ${cssManager.bdTheme('#666', '#999')}; color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.05)', 'rgba(255, 255, 255, 0.05)')}; background: ${cssManager.bdTheme('#f4f4f5', '#27272a')};
} }
.drag-handle:active { .drag-handle:active {
cursor: grabbing; cursor: grabbing;
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.1)', 'rgba(255, 255, 255, 0.1)')}; background: ${cssManager.bdTheme('#e5e7eb', '#3f3f46')};
} }
.block-wrapper.dragging { .block-wrapper.dragging {
@ -407,9 +410,9 @@ export const wysiwygStyles = css`
position: absolute; position: absolute;
left: 0; left: 0;
right: 0; right: 0;
background: ${cssManager.bdTheme('rgba(0, 102, 204, 0.08)', 'rgba(77, 148, 255, 0.08)')}; background: ${cssManager.bdTheme('rgba(59, 130, 246, 0.05)', 'rgba(59, 130, 246, 0.05)')};
border: 2px dashed ${cssManager.bdTheme('#0066cc', '#4d94ff')}; border: 2px dashed ${cssManager.bdTheme('#3b82f6', '#3b82f6')};
border-radius: 8px; border-radius: 4px;
transition: top 0.2s ease, height 0.2s ease; transition: top 0.2s ease, height 0.2s ease;
pointer-events: none; pointer-events: none;
z-index: 1999; z-index: 1999;
@ -426,50 +429,21 @@ export const wysiwygStyles = css`
user-select: none; user-select: none;
} }
/* Block Settings Button */ /* Block Settings Button - Removed in favor of context menu */
.block-settings {
position: absolute;
right: -40px;
top: 50%;
transform: translateY(-50%);
width: 28px;
height: 28px;
cursor: pointer;
opacity: 0;
transition: opacity 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
color: ${cssManager.bdTheme('#999', '#666')};
border-radius: 4px;
}
.block-wrapper:hover .block-settings {
opacity: 1;
}
.block-settings:hover {
color: ${cssManager.bdTheme('#666', '#999')};
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.05)', 'rgba(255, 255, 255, 0.05)')};
}
.block-settings:active {
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.1)', 'rgba(255, 255, 255, 0.1)')};
}
/* Text Selection Styles */ /* Text Selection Styles */
.block ::selection { .block ::selection {
background: ${cssManager.bdTheme('rgba(0, 102, 204, 0.3)', 'rgba(77, 148, 255, 0.3)')}; background: ${cssManager.bdTheme('rgba(59, 130, 246, 0.2)', 'rgba(59, 130, 246, 0.2)')};
color: inherit; color: inherit;
} }
/* Formatting Menu */ /* Formatting Menu */
.formatting-menu { .formatting-menu {
position: absolute; position: absolute;
background: ${cssManager.bdTheme('#ffffff', '#262626')}; background: ${cssManager.bdTheme('#ffffff', '#09090b')};
border: 1px solid ${cssManager.bdTheme('#e0e0e0', '#404040')}; border: 1px solid ${cssManager.bdTheme('#e5e7eb', '#27272a')};
border-radius: 6px; border-radius: 4px;
box-shadow: 0 2px 16px rgba(0, 0, 0, 0.15); box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1), 0 1px 2px rgba(0, 0, 0, 0.06);
padding: 4px; padding: 4px;
display: flex; display: flex;
gap: 2px; gap: 2px;
@ -480,7 +454,7 @@ export const wysiwygStyles = css`
@keyframes fadeInScale { @keyframes fadeInScale {
from { from {
opacity: 0; opacity: 0;
transform: scale(0.95) translateY(5px); transform: scale(0.98) translateY(2px);
} }
to { to {
opacity: 1; opacity: 1;
@ -494,20 +468,20 @@ export const wysiwygStyles = css`
border: none; border: none;
background: transparent; background: transparent;
cursor: pointer; cursor: pointer;
border-radius: 4px; border-radius: 3px;
transition: all 0.15s ease; transition: all 0.15s ease;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
color: ${cssManager.bdTheme('#000000', '#e0e0e0')}; color: ${cssManager.bdTheme('#09090b', '#fafafa')};
font-weight: 600; font-weight: 600;
font-size: 14px; font-size: 14px;
position: relative; position: relative;
} }
.format-button:hover { .format-button:hover {
background: ${cssManager.bdTheme('#f0f0f0', '#333333')}; background: ${cssManager.bdTheme('#f4f4f5', '#27272a')};
color: ${cssManager.bdTheme('#0066cc', '#4d94ff')}; color: ${cssManager.bdTheme('#3b82f6', '#3b82f6')};
} }
.format-button:active { .format-button:active {
@ -535,7 +509,7 @@ export const wysiwygStyles = css`
.block strong, .block strong,
.block b { .block b {
font-weight: 600; font-weight: 600;
color: ${cssManager.bdTheme('#000000', '#ffffff')}; color: ${cssManager.bdTheme('#09090b', '#fafafa')};
} }
.block em, .block em,
@ -554,22 +528,22 @@ export const wysiwygStyles = css`
} }
.block code { .block code {
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.06)', 'rgba(255, 255, 255, 0.1)')}; background: ${cssManager.bdTheme('#f4f4f5', '#27272a')};
padding: 2px 6px; padding: 2px 6px;
border-radius: 3px; border-radius: 3px;
font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Fira Code', monospace; font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Fira Code', monospace;
font-size: 0.9em; font-size: 0.9em;
color: ${cssManager.bdTheme('#d14', '#ff6b6b')}; color: ${cssManager.bdTheme('#09090b', '#fafafa')};
} }
.block a { .block a {
color: ${cssManager.bdTheme('#0066cc', '#4d94ff')}; color: ${cssManager.bdTheme('#3b82f6', '#3b82f6')};
text-decoration: none; text-decoration: none;
border-bottom: 1px solid transparent; border-bottom: 1px solid transparent;
transition: border-color 0.15s ease; transition: border-color 0.15s ease;
} }
.block a:hover { .block a:hover {
border-bottom-color: ${cssManager.bdTheme('#0066cc', '#4d94ff')}; border-bottom-color: ${cssManager.bdTheme('#3b82f6', '#3b82f6')};
} }
`; `;

View File

@ -1,2 +1,3 @@
export * from './mainpage.js'; export * from './mainpage.js';
export * from './input-showcase.js'; export * from './input-showcase.js';
export * from './zindex-showcase.js';

View File

@ -377,6 +377,29 @@ export const inputShowcase = () => html`
.placeholder=${'Type and press Enter'} .placeholder=${'Type and press Enter'}
></dees-input-typelist> ></dees-input-typelist>
</dees-panel> </dees-panel>
<dees-panel .title=${'Tags Input'} .subtitle=${'Add and manage tags with suggestions'}>
<div class="demo-grid">
<dees-input-tags
.label=${'Project Tags'}
.placeholder=${'Add tags...'}
.value=${['frontend', 'typescript', 'webcomponents']}
.description=${'Press Enter or comma to add'}
></dees-input-tags>
<dees-input-tags
.label=${'Technologies'}
.placeholder=${'Type to see suggestions...'}
.suggestions=${[
'React', 'Vue', 'Angular', 'Svelte',
'Node.js', 'Deno', 'Express', 'MongoDB'
]}
.value=${['React', 'Node.js']}
.maxTags=${5}
.description=${'Maximum 5 tags, with suggestions'}
></dees-input-tags>
</div>
</dees-panel>
</section> </section>
<!-- Numeric Inputs Section --> <!-- Numeric Inputs Section -->
@ -420,6 +443,32 @@ export const inputShowcase = () => html`
Specialized input components for specific data types like phone numbers, IBAN, and file uploads. Specialized input components for specific data types like phone numbers, IBAN, and file uploads.
</p> </p>
<dees-panel .title=${'Date & Time Picker'} .subtitle=${'Calendar-based date selection'}>
<div class="demo-grid">
<dees-input-datepicker
.label=${'Event Date'}
.placeholder=${'Select date'}
.description=${'Choose a date from the calendar'}
></dees-input-datepicker>
<dees-input-datepicker
.label=${'Appointment Time'}
.enableTime=${true}
.timeFormat=${'12h'}
.description=${'Date and time with AM/PM'}
></dees-input-datepicker>
<dees-input-datepicker
.label=${'Deadline'}
.enableTime=${true}
.timeFormat=${'24h'}
.minuteIncrement=${15}
.minDate=${new Date().toISOString()}
.description=${'Future dates only, 15 min increments'}
></dees-input-datepicker>
</div>
</dees-panel>
<dees-panel .title=${'Phone & IBAN'}> <dees-panel .title=${'Phone & IBAN'}>
<div class="demo-grid"> <div class="demo-grid">
<dees-input-phone <dees-input-phone
@ -451,6 +500,31 @@ export const inputShowcase = () => html`
.accept=${'image/*'} .accept=${'image/*'}
></dees-input-fileupload> ></dees-input-fileupload>
</dees-panel> </dees-panel>
<dees-panel .title=${'Profile Picture Input'} .subtitle=${'Image upload with cropping'}>
<div class="demo-grid">
<dees-input-profilepicture
.label=${'User Avatar'}
.description=${'Round profile picture'}
.shape=${'round'}
.size=${120}
></dees-input-profilepicture>
<dees-input-profilepicture
.label=${'Company Logo'}
.description=${'Square format'}
.shape=${'square'}
.size=${120}
></dees-input-profilepicture>
<dees-input-profilepicture
.label=${'Team Member'}
.description=${'Larger profile image'}
.shape=${'round'}
.size=${150}
></dees-input-profilepicture>
</div>
</dees-panel>
</section> </section>
<!-- Rich Editors Section --> <!-- Rich Editors Section -->

Some files were not shown because too many files have changed in this diff Show More