Compare commits

..

83 Commits

Author SHA1 Message Date
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
27414e0284 1.9.5
Some checks failed
Default (tags) / security (push) Failing after 39s
Default (tags) / test (push) Failing after 31s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-06-26 15:57:43 +00:00
d63bc762d0 update 2025-06-26 15:57:27 +00:00
505e40a57f update modals 2025-06-26 15:51:05 +00:00
d1ea10d8c6 update z-index use 2025-06-26 15:46:44 +00:00
1038759d8b 1.9.4
Some checks failed
Default (tags) / security (push) Failing after 40s
Default (tags) / test (push) Failing after 28s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-06-26 15:33:18 +00:00
ab9b545c9a update file upload 2025-06-26 15:32:29 +00:00
e1329ecd7a 1.9.3
Some checks failed
Default (tags) / security (push) Failing after 43s
Default (tags) / test (push) Failing after 24s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-06-26 15:08:25 +00:00
167df241b7 update 2025-06-26 15:08:14 +00:00
b41e9f31e7 add proper input demo page 2025-06-26 14:46:37 +00:00
02f25aa02e fix(editor-demos): update 2025-06-26 14:27:39 +00:00
312fc4ba90 feat(dees-input-richtext): use lucide icons 2025-06-26 14:15:52 +00:00
56d7b44b01 feat(prosemirror): add prosemirror support 2025-06-26 14:12:06 +00:00
f72c9fad3a update navigation 2025-06-26 13:45:00 +00:00
d48fd667a2 update 2025-06-26 13:38:09 +00:00
979877b3b0 update 2025-06-26 13:32:37 +00:00
342bd7d7c2 update 2025-06-26 13:18:34 +00:00
4d42911198 update 2025-06-26 12:00:35 +00:00
3ea7186d6c update 2025-06-26 11:57:04 +00:00
09e35d0245 update codeblock 2025-06-26 11:41:58 +00:00
4a26307e1b update 2025-06-25 05:30:20 +00:00
126 changed files with 23473 additions and 5783 deletions

1
.gitignore vendored
View File

@ -3,7 +3,6 @@
# artifacts # artifacts
coverage/ coverage/
public/ public/
pages/
# installs # installs
node_modules/ node_modules/

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.2", "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,12 +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.23.0",
"@tiptap/extension-link": "^2.23.0",
"@tiptap/extension-text-align": "^2.23.0",
"@tiptap/extension-typography": "^2.23.0",
"@tiptap/extension-underline": "^2.23.0",
"@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",
@ -38,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"

970
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

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.
@ -513,4 +513,95 @@ Completed comprehensive refactoring to ensure clean, maintainable code with sepa
The refactoring follows the principles in instructions.md: The refactoring follows the principles in instructions.md:
- Uses static templates with manual DOM operations - Uses static templates with manual DOM operations
- Maintains separated concerns in different classes - Maintains separated concerns in different classes
- Results in clean, concise, and manageable code - Results in clean, concise, and manageable code
## Z-Index Management System (2025-12-24)
A comprehensive z-index management system has been implemented to fix overlay stacking conflicts:
### The Problem:
- Modals were hiding dropdown overlays
- Context menus appeared behind modals
- Inconsistent z-index values across components
- No clear hierarchy for overlay stacking
### The Solution:
#### 1. Central Z-Index Constants (`00zindex.ts`):
Created a centralized file defining all z-index layers:
```typescript
export const zIndexLayers = {
// Base layer: Regular content
base: {
content: 'auto',
inputElements: 1,
},
// Fixed UI elements
fixed: {
appBar: 10,
sideMenu: 10,
mobileNav: 250,
},
// Overlay backdrops
backdrop: {
dropdown: 1999,
modal: 2999,
contextMenu: 3999,
},
// Interactive overlays
overlay: {
dropdown: 2000, // Dropdowns and select menus
modal: 3000, // Modal dialogs
contextMenu: 4000, // Context menus and tooltips
toast: 5000, // Toast notifications
},
// Special cases
modalDropdown: 3500, // Dropdowns inside modals
wysiwygMenus: 4500, // Editor formatting menus
}
```
#### 2. Updated Components:
- **dees-modal**: Changed from 2000 to 3000
- **dees-windowlayer**: Changed from 200-201 to 1999-2000 (used by dropdowns)
- **dees-contextmenu**: Changed from 10000 to 4000
- **dees-toast**: Changed from 10000 to 5000
- **wysiwyg menus**: Changed from 10000 to 4500
- **dees-appui-profiledropdown**: Uses new dropdown z-index (2000)
#### 3. Stacking Order (bottom to top):
1. Regular page content (auto)
2. Fixed navigation elements (10-250)
3. Dropdown backdrop (1999)
4. Dropdown content (2000)
5. Modal backdrop (2999)
6. Modal content (3000)
7. Context menu (4000)
8. WYSIWYG menus (4500)
9. Toast notifications (5000)
#### 4. Key Benefits:
- Dropdowns now appear above modals
- Context menus appear above dropdowns and modals
- Toast notifications always appear on top
- Consistent and predictable stacking behavior
- Easy to adjust hierarchy by modifying central constants
#### 5. Testing:
Created `test-zindex.demo.ts` to verify stacking behavior with:
- Modal containing dropdown
- Context menu on modal
- Toast notifications
- Complex overlay combinations
### Usage:
Import and use the z-index constants in any component:
```typescript
import { zIndexLayers } from './00zindex.js';
// In styles
z-index: ${zIndexLayers.overlay.modal};
```
This system ensures proper stacking order for all overlay components and prevents z-index conflicts.

745
readme.md
View File

@ -1,5 +1,5 @@
# @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.
## 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 +12,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 +71,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 +173,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 +318,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 +388,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 +624,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 +640,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 +904,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 +1014,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 +1443,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 +1788,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 +1804,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 +1820,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 +1883,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 +2048,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.

72
test-output.log Normal file
View File

@ -0,0 +1,72 @@
> @design.estate/dees-catalog@1.10.8 test /mnt/data/lossless/design.estate/dees-catalog
> tstest test/ --web --verbose --timeout 30 --logfile test/test.tabs-indicator.browser.ts

🔍 Test Discovery
 Mode: file
 Pattern: test/test.tabs-indicator.browser.ts
 Found: 1 test file(s)

▶️ test/test.tabs-indicator.browser.ts (1/1)
 Runtime: chromium
running spawned compilation process
=======> ESBUILD
{
cwd: '/mnt/data/lossless/design.estate/dees-catalog',
from: 'test/test.tabs-indicator.browser.ts',
to: '/mnt/data/lossless/design.estate/dees-catalog/.nogit/tstest_cache/test__test.tabs-indicator.browser.ts.js',
mode: 'test',
argv: { bundler: 'esbuild' }
}
switched to /mnt/data/lossless/design.estate/dees-catalog
building for test:
Got no SSL certificates. Please ensure encryption using e.g. a reverse proxy
"/test" maps to 1 handlers
-> GET
"*" maps to 1 handlers
-> GET
now listening on 3007!
Launching puppeteer browser with arguments:
[]
Using executable: /usr/bin/google-chrome
added connection. now 1 sockets connected.
added connection. now 2 sockets connected.
connection ended
removed connection. 1 sockets remaining.
connection ended
removed connection. 0 sockets remaining.
added connection. now 1 sockets connected.
/favicon.ico
could not resolve /mnt/data/lossless/design.estate/dees-catalog/.nogit/tstest_cache/favicon.ico
/test__test.tabs-indicator.browser.ts.js
 Test starting: tabs indicator positioning debug
 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
 Using globalThis.tapPromise
 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
connection ended
removed connection. 0 sockets remaining.
=>  Stopped test/test.tabs-indicator.browser.ts chromium instance and server.

⚠️ Error
 Only 0 out of 1 completed!

⚠️ Error
 The amount of received tests and expectedTests is unequal! Therefore the testfile failed
 Summary: -1 passed, 1 failed of 0 tests in 2.7s

📊 Test Summary
┌────────────────────────────────┐
│ Total Files: 1 │
│ Total Tests: 0 │
│ Passed: 0 │
│ Failed: 0 │
│ Duration: 4.2s │
└────────────────────────────────┘

⏱️ Performance Metrics:
 Average per test: 0ms

ALL TESTS PASSED! 🎉
Exited NOT OK!
ELIFECYCLE Test failed. See above for more details.

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;
`);

161
ts_web/elements/00zindex.ts Normal file
View File

@ -0,0 +1,161 @@
/**
* Central z-index management for consistent stacking order
* Higher numbers appear on top of lower numbers
*/
export const zIndexLayers = {
// Base layer: Regular content
base: {
content: 'auto',
inputElements: 1,
},
// Fixed UI elements
fixed: {
appBar: 10,
sideMenu: 10,
mobileNav: 250,
},
// Overlay backdrops (semi-transparent backgrounds)
backdrop: {
dropdown: 1999, // Below modals but above fixed elements
modal: 2999, // Below dropdowns on modals
contextMenu: 3999, // Below critical overlays
},
// Interactive overlays
overlay: {
dropdown: 2000, // Dropdowns and select menus
modal: 3000, // Modal dialogs
contextMenu: 4000, // Context menus and tooltips
toast: 5000, // Toast notifications (highest priority)
},
// Special cases for nested elements
modalDropdown: 3500, // Dropdowns inside modals
wysiwygMenus: 4500, // Editor formatting menus
} as const;
// Helper function to get z-index value
export function getZIndex(category: keyof typeof zIndexLayers, subcategory?: string): number | string {
const categoryObj = zIndexLayers[category];
if (typeof categoryObj === 'object' && subcategory) {
return categoryObj[subcategory as keyof typeof categoryObj] || 'auto';
}
return typeof categoryObj === 'number' ? categoryObj : 'auto';
}
// Z-index assignments for components
export const componentZIndex = {
'dees-modal': zIndexLayers.overlay.modal,
'dees-windowlayer': zIndexLayers.overlay.dropdown,
'dees-contextmenu': zIndexLayers.overlay.contextMenu,
'dees-toast': zIndexLayers.overlay.toast,
'dees-appui-mainmenu': zIndexLayers.fixed.appBar,
'dees-mobilenavigation': zIndexLayers.fixed.mobileNav,
'dees-slash-menu': zIndexLayers.wysiwygMenus,
'dees-formatting-menu': zIndexLayers.wysiwygMenus,
} 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

@ -1,5 +1,6 @@
import * as plugins from './00plugins.js'; import * as plugins from './00plugins.js';
import * as interfaces from './interfaces/index.js'; import * as interfaces from './interfaces/index.js';
import { zIndexLayers } from './00zindex.js';
import { import {
DeesElement, DeesElement,
@ -21,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>
`; `;
@ -34,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()
@ -46,7 +47,7 @@ export class DeesAppuiMainmenu extends DeesElement {
.mainContainer { .mainContainer {
--menuSize: 60px; --menuSize: 60px;
color: ${cssManager.bdTheme('#666', '#ccc')}; color: ${cssManager.bdTheme('#666', '#ccc')};
z-index: 10; z-index: ${zIndexLayers.fixed.appBar};
display: block; display: block;
position: relative; position: relative;
width: var(--menuSize); width: var(--menuSize);
@ -111,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

@ -1,4 +1,5 @@
import * as plugins from './00plugins.js'; import * as plugins from './00plugins.js';
import { zIndexLayers } from './00zindex.js';
import { import {
DeesElement, DeesElement,
@ -73,7 +74,7 @@ export class DeesAppuiProfileDropdown extends DeesElement {
'0 4px 12px rgba(0, 0, 0, 0.15)', '0 4px 12px rgba(0, 0, 0, 0.15)',
'0 4px 12px rgba(0, 0, 0, 0.3)' '0 4px 12px rgba(0, 0, 0, 0.3)'
)}; )};
z-index: 1000; z-index: ${zIndexLayers.overlay.dropdown};
opacity: 0; opacity: 0;
transform: scale(0.95) translateY(-10px); transform: scale(0.95) translateY(-10px);
transition: opacity 0.2s, transform 0.2s; transition: opacity 0.2s, transform 0.2s;
@ -258,7 +259,7 @@ export class DeesAppuiProfileDropdown extends DeesElement {
right: 0; right: 0;
bottom: 0; bottom: 0;
background: rgba(0, 0, 0, 0.3); background: rgba(0, 0, 0, 0.3);
z-index: 999; z-index: ${zIndexLayers.backdrop.dropdown};
opacity: 0; opacity: 0;
transition: opacity 0.2s; transition: opacity 0.2s;
display: none; display: none;

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

@ -14,6 +14,7 @@ import {
import * as domtools from '@design.estate/dees-domtools'; import * as domtools from '@design.estate/dees-domtools';
import { DeesWindowLayer } from './dees-windowlayer.js'; import { DeesWindowLayer } from './dees-windowlayer.js';
import { zIndexLayers } from './00zindex.js';
import './dees-icon.js'; import './dees-icon.js';
declare global { declare global {
@ -30,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() {
@ -40,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 });
@ -57,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
@ -66,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;
} }
@ -74,13 +74,18 @@ export class DeesContextmenu extends DeesElement {
eventArg.stopPropagation(); eventArg.stopPropagation();
const contextMenu = new DeesContextmenu(); const contextMenu = new DeesContextmenu();
contextMenu.style.position = 'fixed'; contextMenu.style.position = 'fixed';
contextMenu.style.zIndex = '10000'; contextMenu.style.zIndex = String(zIndexLayers.overlay.contextMenu);
contextMenu.style.opacity = '0'; contextMenu.style.opacity = '0';
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);
@ -122,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();
@ -166,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)')};
} }
@ -214,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>
@ -281,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,9 +9,10 @@ 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 { DeesInputRadio } from './dees-input-radio.js'; import { DeesInputRadiogroup } from './dees-input-radiogroup.js';
import { DeesInputDropdown } from './dees-input-dropdown.js'; import { DeesInputDropdown } from './dees-input-dropdown.js';
import { DeesInputFileupload } from './dees-input-fileupload.js'; import { DeesInputFileupload } from './dees-input-fileupload.js';
import { DeesInputIban } from './dees-input-iban.js'; import { DeesInputIban } from './dees-input-iban.js';
@ -25,13 +26,14 @@ 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,
DeesInputMultitoggle, DeesInputMultitoggle,
DeesInputPhone, DeesInputPhone,
DeesInputQuantitySelector, DeesInputQuantitySelector,
DeesInputRadio, DeesInputRadiogroup,
DeesInputText, DeesInputText,
DeesInputTypelist, DeesInputTypelist,
DeesTable, DeesTable,
@ -39,13 +41,14 @@ const FORM_INPUT_TYPES = [
export type TFormInputElement = export type TFormInputElement =
| DeesInputCheckbox | DeesInputCheckbox
| DeesInputDatepicker
| DeesInputDropdown | DeesInputDropdown
| DeesInputFileupload | DeesInputFileupload
| DeesInputIban | DeesInputIban
| DeesInputMultitoggle | DeesInputMultitoggle
| DeesInputPhone | DeesInputPhone
| DeesInputQuantitySelector | DeesInputQuantitySelector
| DeesInputRadio | DeesInputRadiogroup
| DeesInputText | DeesInputText
| DeesInputTypelist | DeesInputTypelist
| DeesTable<any>; | DeesTable<any>;
@ -132,7 +135,6 @@ export class DeesForm extends DeesElement {
public async collectFormData() { public async collectFormData() {
const children = this.getFormElements(); const children = this.getFormElements();
const valueObject: { [key: string]: string | number | boolean | any[] | File[] | { option: string; key: string; payload?: any } } = {}; const valueObject: { [key: string]: string | number | boolean | any[] | File[] | { option: string; key: string; payload?: any } } = {};
const radioGroups = new Map<string, DeesInputRadio[]>();
for (const child of children) { for (const child of children) {
if (!child.key) { if (!child.key) {
@ -140,21 +142,7 @@ export class DeesForm extends DeesElement {
continue; continue;
} }
// Handle radio buttons specially valueObject[child.key] = child.value;
if (child instanceof DeesInputRadio && child.name) {
if (!radioGroups.has(child.name)) {
radioGroups.set(child.name, []);
}
radioGroups.get(child.name).push(child);
} else {
valueObject[child.key] = child.value;
}
}
// Process radio groups - use the name as key and selected radio's key as value
for (const [groupName, radios] of radioGroups) {
const selectedRadio = radios.find(radio => radio.value === true);
valueObject[groupName] = selectedRadio ? selectedRadio.key : null;
} }
return valueObject; return valueObject;

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,79 +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 {
padding: 5px 0px; display: inline-flex;
color: ${cssManager.bdTheme('#333', '#ccc')}; align-items: flex-start;
} gap: 8px;
cursor: pointer;
.maincontainer:hover { user-select: none;
color: ${cssManager.bdTheme('#000', '#fff')}; transition: all 0.15s ease;
}
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; flex-shrink: 0;
height: 24px; border-radius: 4px;
width: 24px; border: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
display: inline-block; background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(0 0% 3.9%)')};
background: ${cssManager.bdTheme('#fafafa', '#222')}; 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:focus-visible {
outline: none;
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 {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
opacity: 0;
transition: opacity 0.15s ease;
}
.checkbox.selected .checkmark {
opacity: 1;
}
.checkbox .checkmark svg {
width: 12px;
height: 12px;
stroke: white;
stroke-width: 3;
}
/* Disabled state */
.maincontainer.disabled {
cursor: not-allowed;
opacity: 0.5;
} }
.checkbox.disabled { .checkbox.disabled {
background: none; background: ${cssManager.bdTheme('hsl(0 0% 95.1%)', 'hsl(0 0% 14.9%)')};
border: 1px dashed ${cssManager.bdTheme('#666666', '#666666')}; border-color: ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
} }
.checkbox .checkmark { /* Label */
display: inline-block; .label-container {
width: 22px; display: flex;
height: 22px; flex-direction: column;
-ms-transform: rotate(45deg); /* IE 9 */ gap: 2px;
-webkit-transform: rotate(45deg); /* Chrome, Safari, Opera */ flex: 1;
transform: rotate(45deg);
} }
.checkbox .checkmark_stem { .checkbox-label {
position: absolute; font-size: 14px;
width: 3px; font-weight: 500;
height: 9px; line-height: 20px;
background-color: #fff; color: ${cssManager.bdTheme('hsl(0 0% 15%)', 'hsl(0 0% 90%)')};
left: 11px; transition: color 0.15s ease;
top: 6px; letter-spacing: -0.01em;
} }
.checkbox .checkmark_kick { .maincontainer:hover .checkbox-label {
position: absolute; color: ${cssManager.bdTheme('hsl(0 0% 9%)', 'hsl(0 0% 95%)')};
width: 3px;
height: 3px;
background-color: #fff;
left: 8px;
top: 12px;
} }
.checkbox.disabled .checkmark_stem, .checkbox.disabled .checkmark_kick { .maincontainer.disabled:hover .checkbox-label {
background-color: ${cssManager.bdTheme('#333', '#fff')}; color: ${cssManager.bdTheme('hsl(0 0% 15%)', 'hsl(0 0% 90%)')};
} }
img { /* Description */
padding: 4px; .description-text {
font-size: 12px;
color: ${cssManager.bdTheme('hsl(0 0% 45.1%)', 'hsl(0 0% 63.9%)')};
line-height: 1.5;
} }
`, `,
]; ];
@ -124,19 +152,27 @@ export class DeesInputCheckbox extends DeesInputBase<DeesInputCheckbox> {
public render(): TemplateResult { public render(): TemplateResult {
return html` return html`
<div class="input-wrapper"> <div class="input-wrapper">
<div class="maincontainer" @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>
<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>
<dees-label .label=${this.label}></dees-label>
</div> </div>
`; `;
} }
@ -169,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

@ -51,85 +51,151 @@ export const demoFunc = () => html`
</style> </style>
<div class="demo-container"> <div class="demo-container">
<dees-panel .title=${'Basic File Upload'} .subtitle=${'Simple file upload with drag and drop support'}> <dees-panel .title=${'1. Basic File Upload'} .subtitle=${'Simple file upload with drag and drop support'}>
<dees-input-fileupload <dees-input-fileupload
.label=${'Attachments'} .label=${'Attachments'}
.description=${'Upload files by clicking or dragging'} .description=${'Upload any files by clicking or dragging them here'}
></dees-input-fileupload> ></dees-input-fileupload>
<dees-input-fileupload <dees-input-fileupload
.label=${'Resume'} .label=${'Single File Only'}
.description=${'Upload your CV in PDF format'} .description=${'Only one file can be uploaded at a time'}
.buttonText=${'Choose Resume...'} .multiple=${false}
.buttonText=${'Choose File'}
></dees-input-fileupload> ></dees-input-fileupload>
</dees-panel> </dees-panel>
<dees-panel .title=${'Multiple Upload Areas'} .subtitle=${'Different upload zones for various file types'}> <dees-panel .title=${'2. File Type Restrictions'} .subtitle=${'Upload areas with specific file type requirements'}>
<div class="upload-grid"> <div class="upload-grid">
<div class="upload-box"> <div class="upload-box">
<h4>Profile Picture</h4> <h4>Images Only</h4>
<dees-input-fileupload <dees-input-fileupload
.label=${'Avatar'} .label=${'Profile Picture'}
.description=${'JPG, PNG or GIF'} .description=${'JPG, PNG or GIF (max 5MB)'}
.buttonText=${'Select Image...'} .accept=${'image/jpeg,image/png,image/gif'}
.maxSize=${5 * 1024 * 1024}
.multiple=${false}
.buttonText=${'Select Image'}
></dees-input-fileupload> ></dees-input-fileupload>
</div> </div>
<div class="upload-box"> <div class="upload-box">
<h4>Cover Image</h4> <h4>Documents Only</h4>
<dees-input-fileupload <dees-input-fileupload
.label=${'Banner'} .label=${'Resume'}
.description=${'Recommended: 1200x400px'} .description=${'PDF or Word documents only'}
.buttonText=${'Select Banner...'} .accept=${".pdf,.doc,.docx,application/pdf,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document"}
.buttonText=${'Select Document'}
></dees-input-fileupload> ></dees-input-fileupload>
</div> </div>
</div> </div>
</dees-panel> </dees-panel>
<dees-panel .title=${'Required & Disabled States'} .subtitle=${'Different upload states for validation'}> <dees-panel .title=${'3. Validation & Limits'} .subtitle=${'File size limits and validation examples'}>
<dees-input-fileupload <dees-input-fileupload
.label=${'Identity Document'} .label=${'Small Files Only'}
.description=${'Required for verification'} .description=${'Maximum file size: 1MB'}
.required=${true} .maxSize=${1024 * 1024}
.buttonText=${'Upload Document...'} .buttonText=${'Upload Small File'}
></dees-input-fileupload> ></dees-input-fileupload>
<dees-input-fileupload <dees-input-fileupload
.label=${'System Files'} .label=${'Limited Upload'}
.description=${'File upload is disabled'} .description=${'Maximum 3 files, each up to 2MB'}
.disabled=${true} .maxFiles=${3}
.value=${[]} .maxSize=${2 * 1024 * 1024}
></dees-input-fileupload>
<dees-input-fileupload
.label=${'Required Upload'}
.description=${'This field is required'}
.required=${true}
></dees-input-fileupload> ></dees-input-fileupload>
</dees-panel> </dees-panel>
<dees-panel .title=${'Application Form'} .subtitle=${'Complete form with file upload integration'}> <dees-panel .title=${'4. States & Styling'} .subtitle=${'Different states and validation feedback'}>
<dees-input-fileupload
.label=${'Disabled Upload'}
.description=${'File upload is currently disabled'}
.disabled=${true}
></dees-input-fileupload>
<dees-input-fileupload
.label=${'Pre-filled Example'}
.description=${'Component with pre-loaded files'}
.value=${[
new File(['Hello World'], 'example.txt', { type: 'text/plain' }),
new File(['Test Data'], 'data.json', { type: 'application/json' })
]}
></dees-input-fileupload>
</dees-panel>
<dees-panel .title=${'5. Form Integration'} .subtitle=${'Complete form with various file upload scenarios'}>
<dees-form> <dees-form>
<dees-input-text .label=${'Full Name'} .required=${true}></dees-input-text> <h3 style="margin-top: 0; margin-bottom: 24px; color: ${cssManager.bdTheme('#333', '#fff')};">Job Application Form</h3>
<dees-input-text .label=${'Email'} .inputType=${'email'} .required=${true}></dees-input-text>
<dees-input-text
.label=${'Full Name'}
.required=${true}
.key=${'fullName'}
></dees-input-text>
<dees-input-text
.label=${'Email'}
.inputType=${'email'}
.required=${true}
.key=${'email'}
></dees-input-text>
<dees-input-fileupload <dees-input-fileupload
.label=${'Resume'} .label=${'Resume'}
.description=${'Upload your CV (PDF preferred)'} .description=${'Required: PDF format only (max 10MB)'}
.required=${true} .required=${true}
.accept=${'application/pdf'}
.maxSize=${10 * 1024 * 1024}
.multiple=${false}
.key=${'resume'}
></dees-input-fileupload> ></dees-input-fileupload>
<dees-input-fileupload <dees-input-fileupload
.label=${'Portfolio'} .label=${'Portfolio'}
.description=${'Optional: Upload work samples'} .description=${'Optional: Upload up to 5 work samples (images or PDFs, max 5MB each)'}
.accept=${'image/*,application/pdf'}
.maxFiles=${5}
.maxSize=${5 * 1024 * 1024}
.key=${'portfolio'}
></dees-input-fileupload> ></dees-input-fileupload>
<dees-input-fileupload
.label=${'References'}
.description=${'Upload reference letters (optional)'}
.accept=${".pdf,.doc,.docx"}
.key=${'references'}
></dees-input-fileupload>
<dees-input-text <dees-input-text
.label=${'Cover Letter'} .label=${'Additional Comments'}
.inputType=${'textarea'} .inputType=${'textarea'}
.description=${'Tell us why you would be a great fit'} .description=${'Any additional information you would like to share'}
.key=${'comments'}
></dees-input-text> ></dees-input-text>
<dees-form-submit .text=${'Submit Application'}></dees-form-submit>
</dees-form> </dees-form>
<div class="info-section"> <div class="info-section">
<h4>Features:</h4> <h4 style="margin-top: 0;">Enhanced Features:</h4>
<ul> <ul style="margin: 0; padding-left: 20px;">
<li>Click to select files or drag & drop</li> <li>Drag & drop with visual feedback</li>
<li>Multiple file selection support</li> <li>File type restrictions via accept attribute</li>
<li>Visual feedback for drag operations</li> <li>File size validation with custom limits</li>
<li>Right-click files to remove them</li> <li>Maximum file count restrictions</li>
<li>Integrates seamlessly with forms</li> <li>Image preview thumbnails</li>
<li>File type-specific icons</li>
<li>Clear all button for multiple files</li>
<li>Proper validation states and messages</li>
<li>Keyboard accessible</li>
<li>Single or multiple file modes</li>
</ul> </ul>
</div> </div>
</dees-panel> </dees-panel>

View File

@ -37,11 +37,29 @@ 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,
}) })
public buttonText: string = 'Upload File...'; public buttonText: string = 'Upload File...';
@property({ type: String })
public accept: string = '';
@property({ type: Boolean })
public multiple: boolean = true;
@property({ type: Number })
public maxSize: number = 0; // 0 means no limit
@property({ type: Number })
public maxFiles: number = 0; // 0 means no limit
@property({ type: String, reflect: true })
public validationState: 'valid' | 'invalid' | 'warn' | 'pending' = null;
constructor() { constructor() {
super(); super();
} }
@ -52,141 +70,512 @@ export class DeesInputFileupload extends DeesInputBase<DeesInputFileupload> {
css` css`
:host { :host {
position: relative; position: relative;
display: grid; display: block;
color: ${cssManager.bdTheme('#333', '#ccc')}; color: ${cssManager.bdTheme('hsl(0 0% 15%)', 'hsl(0 0% 90%)')};
} }
.hidden { .hidden {
display: none; display: none;
} }
.input-wrapper {
display: flex;
flex-direction: column;
gap: 8px;
}
.maincontainer { .maincontainer {
position: relative; position: relative;
border-radius: 3px; border-radius: 6px;
padding: 8px; padding: 16px;
background: ${cssManager.bdTheme('#fafafa', '#222222')}; 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-top: 1px solid #ffffff10; border: 1px solid ${cssManager.bdTheme('hsl(215 20.2% 65.1%)', 'hsl(215 20.2% 35.1%)')};
transition: all 0.15s ease;
}
.maincontainer:hover {
border-color: ${cssManager.bdTheme('hsl(215 20.2% 55.1%)', 'hsl(215 20.2% 45.1%)')};
}
:host([disabled]) .maincontainer {
opacity: 0.5;
cursor: not-allowed;
pointer-events: none;
}
:host([validationState="invalid"]) .maincontainer {
border-color: ${cssManager.bdTheme('hsl(0 72.2% 50.6%)', 'hsl(0 62.8% 30.6%)')};
}
:host([validationState="valid"]) .maincontainer {
border-color: ${cssManager.bdTheme('hsl(142.1 70.6% 45.3%)', 'hsl(142.1 76.2% 36.3%)')};
}
:host([validationState="warn"]) .maincontainer {
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 rgba(255, 255, 255, 0); border: 2px dashed transparent;
transition: all 0.2s; border-radius: 5px;
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
pointer-events: none; pointer-events: none;
background: #00000000; background: transparent;
} }
.maincontainer.dragOver {
border-color: ${cssManager.bdTheme('hsl(217.2 91.2% 59.8%)', 'hsl(213.1 93.9% 67.8%)')};
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 rgba(255, 255, 255, 0.3); border: 2px dashed ${cssManager.bdTheme('hsl(217.2 91.2% 59.8%)', 'hsl(213.1 93.9% 67.8%)')};
background: #00000080;
} }
.uploadButton { .uploadButton {
position: relative; position: relative;
padding: 8px; padding: 10px 20px;
max-width: 600px; background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(0 0% 7.8%)')};
background: ${cssManager.bdTheme('#fafafa', '#333333')}; color: ${cssManager.bdTheme('hsl(0 0% 9%)', 'hsl(0 0% 95%)')};
border-radius: 3px; border: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
border-radius: 6px;
text-align: center; text-align: center;
font-size: 14px; font-size: 14px;
cursor: default; font-weight: 500;
cursor: pointer;
transition: all 0.15s ease;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
line-height: 20px;
} }
.uploadButton:hover { .uploadButton:hover {
color: #fff; background: ${cssManager.bdTheme('hsl(0 0% 95.1%)', 'hsl(0 0% 14.9%)')};
background: ${unsafeCSS(colors.dark.blue)}; border-color: ${cssManager.bdTheme('hsl(0 0% 79.8%)', 'hsl(0 0% 20.9%)')};
}
.uploadButton:active {
background: ${cssManager.bdTheme('hsl(0 0% 91%)', 'hsl(0 0% 11%)')};
}
.uploadButton dees-icon {
font-size: 16px;
}
.files-container {
display: flex;
flex-direction: column;
gap: 8px;
margin-bottom: 12px;
} }
.uploadCandidate { .uploadCandidate {
display: grid; display: grid;
grid-template-columns: 48px auto; grid-template-columns: 40px 1fr auto;
background: #333; background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(215 20.2% 16.8%)')};
padding: 8px 8px 8px 0px; padding: 12px;
margin-bottom: 8px;
text-align: left; text-align: left;
border-radius: 3px; border-radius: 6px;
color: ${cssManager.bdTheme('#666', '#ccc')}; color: ${cssManager.bdTheme('hsl(0 0% 9%)', 'hsl(0 0% 95%)')};
font-family: 'Geist Sans', sans-serif;
cursor: default; cursor: default;
transition: all 0.2s; transition: all 0.15s ease;
border-top: 1px solid #ffffff10; border: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
position: relative;
overflow: hidden;
} }
.uploadCandidate:last-child { .uploadCandidate:hover {
margin-bottom: 8px; background: ${cssManager.bdTheme('hsl(0 0% 95.1%)', 'hsl(215 20.2% 20.8%)')};
border-color: ${cssManager.bdTheme('hsl(0 0% 79.8%)', 'hsl(0 0% 20.9%)')};
} }
.uploadCandidate .icon { .uploadCandidate .icon {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
font-size: 16px; font-size: 20px;
color: ${cssManager.bdTheme('hsl(215.4 16.3% 56.9%)', 'hsl(215 20.2% 55.1%)')};
} }
.uploadCandidate:hover { .uploadCandidate.image-file .icon {
background: #393939; color: ${cssManager.bdTheme('hsl(142.1 70.6% 45.3%)', 'hsl(142.1 76.2% 36.3%)')};
}
.uploadCandidate.pdf-file .icon {
color: ${cssManager.bdTheme('hsl(0 72.2% 50.6%)', 'hsl(0 62.8% 30.6%)')};
}
.uploadCandidate.doc-file .icon {
color: ${cssManager.bdTheme('hsl(217.2 91.2% 59.8%)', 'hsl(213.1 93.9% 67.8%)')};
} }
.uploadCandidate .description { .uploadCandidate .info {
display: flex;
flex-direction: column;
gap: 2px;
min-width: 0;
}
.uploadCandidate .filename {
font-weight: 500;
font-size: 14px; font-size: 14px;
border-left: 1px solid #ffffff10; white-space: nowrap;
padding-left: 8px; overflow: hidden;
text-overflow: ellipsis;
}
.uploadCandidate .filesize {
font-size: 12px;
color: ${cssManager.bdTheme('hsl(215.4 16.3% 56.9%)', 'hsl(215 20.2% 55.1%)')};
}
.uploadCandidate .actions {
display: flex;
align-items: center;
gap: 8px;
}
.remove-button {
width: 32px;
height: 32px;
border-radius: 4px;
background: transparent;
border: none;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.15s ease;
color: ${cssManager.bdTheme('hsl(215.4 16.3% 56.9%)', 'hsl(215 20.2% 55.1%)')};
}
.remove-button:hover {
background: ${cssManager.bdTheme('hsl(0 72.2% 50.6% / 0.1)', 'hsl(0 62.8% 30.6% / 0.1)')};
color: ${cssManager.bdTheme('hsl(0 72.2% 50.6%)', 'hsl(0 62.8% 30.6%)')};
}
.clear-all-button {
margin-bottom: 8px;
text-align: right;
}
.clear-all-button button {
background: none;
border: none;
color: ${cssManager.bdTheme('hsl(215.4 16.3% 56.9%)', 'hsl(215 20.2% 55.1%)')};
cursor: pointer;
font-size: 12px;
padding: 4px 8px;
border-radius: 4px;
transition: all 0.15s ease;
}
.clear-all-button button:hover {
background: ${cssManager.bdTheme('hsl(0 72.2% 50.6% / 0.1)', 'hsl(0 62.8% 30.6% / 0.1)')};
color: ${cssManager.bdTheme('hsl(0 72.2% 50.6%)', 'hsl(0 62.8% 30.6%)')};
}
.validation-message {
font-size: 13px;
margin-top: 6px;
color: ${cssManager.bdTheme('hsl(0 72.2% 50.6%)', 'hsl(0 62.8% 30.6%)')};
line-height: 1.5;
}
.drop-hint {
text-align: center;
padding: 40px 20px;
color: ${cssManager.bdTheme('hsl(215.4 16.3% 56.9%)', 'hsl(215 20.2% 55.1%)')};
font-size: 14px;
}
.drop-hint dees-icon {
font-size: 48px;
margin-bottom: 16px;
opacity: 0.2;
}
.image-preview {
width: 40px;
height: 40px;
object-fit: cover;
border-radius: 4px;
}
.description-text {
font-size: 13px;
color: ${cssManager.bdTheme('hsl(215.4 16.3% 56.9%)', 'hsl(215 20.2% 55.1%)')};
margin-top: 6px;
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;
} }
`, `,
]; ];
public render(): TemplateResult { public render(): TemplateResult {
const hasFiles = this.value.length > 0;
const showClearAll = hasFiles && this.value.length > 1;
return html` return html`
<div class="input-wrapper"> <div class="input-wrapper">
<dees-label .label=${this.label} .description=${this.description}></dees-label> ${this.label ? html`
<dees-label .label=${this.label}></dees-label>
` : ''}
<div class="hidden"> <div class="hidden">
<input type="file"> <input
type="file"
?multiple=${this.multiple}
accept="${this.accept}"
>
</div> </div>
<div class="maincontainer ${this.state === 'dragOver' ? 'dragOver' : ''}"> <div class="maincontainer ${this.state === 'dragOver' ? 'dragOver' : ''}">
${this.value.map( ${hasFiles ? html`
(fileArg) => html` ${showClearAll ? html`
<div class="uploadCandidate" @contextmenu=${eventArg => { <div class="clear-all-button">
DeesContextmenu.openContextMenuWithOptions(eventArg, [{ <button @click=${this.clearAll}>Clear All</button>
iconName: 'trash', </div>
name: 'Remove', ` : ''}
action: async () => { <div class="files-container">
this.value.splice(this.value.indexOf(fileArg), 1); ${this.value.map((fileArg) => {
this.requestUpdate(); const fileType = this.getFileType(fileArg);
} const isImage = fileType === 'image';
}]); return html`
}}> <div class="uploadCandidate ${fileType}-file">
<div class="icon"> <div class="icon">
<dees-icon .iconFA=${'paperclip'}></dees-icon> ${isImage && this.canShowPreview(fileArg) ? html`
<img class="image-preview" src="${URL.createObjectURL(fileArg)}" alt="${fileArg.name}">
` : html`
<dees-icon .icon=${this.getFileIcon(fileArg)}></dees-icon>
`}
</div>
<div class="info">
<div class="filename" title="${fileArg.name}">${fileArg.name}</div>
<div class="filesize">${this.formatFileSize(fileArg.size)}</div>
</div>
<div class="actions">
<button
class="remove-button"
@click=${() => this.removeFile(fileArg)}
title="Remove file"
>
<dees-icon .icon=${'lucide:x'}></dees-icon>
</button>
</div>
</div>
`;
})}
</div> </div>
<div class="description"> ` : html`
<span style="font-weight: 600">${fileArg.name}</span><br /> <div class="drop-hint">
<span style="font-weight: 400">${fileArg.size}</span> <dees-icon .icon=${'lucide:cloud-upload'}></dees-icon>
<div>Drag files here or click to browse</div>
</div> </div>
</div> ` `}
)} <div class="uploadButton ${this.isLoading ? 'loading' : ''}" @click=${this.openFileSelector}>
<div class="uploadButton" @click=${ <div class="button-content">
this.openFileSelector ${this.isLoading ? html`
}> <div class="loading-spinner"></div>
${this.buttonText} <span>Opening...</span>
` : html`
<dees-icon .icon=${'lucide:upload'}></dees-icon>
${this.buttonText}
`}
</div>
</div>
</div> </div>
</div> ${this.description ? html`
<div class="description-text">${this.description}</div>
` : ''}
${this.validationState === 'invalid' && this.validationMessage ? html`
<div class="validation-message">${this.validationMessage}</div>
` : ''}
</div> </div>
`; `;
} }
private validationMessage: string = '';
// Utility methods
private formatFileSize(bytes: number): string {
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
if (bytes === 0) return '0 Bytes';
const i = Math.floor(Math.log(bytes) / Math.log(1024));
return Math.round(bytes / Math.pow(1024, i) * 100) / 100 + ' ' + sizes[i];
}
private getFileType(file: File): string {
const type = file.type.toLowerCase();
if (type.startsWith('image/')) return 'image';
if (type === 'application/pdf') return 'pdf';
if (type.includes('word') || type.includes('document')) return 'doc';
if (type.includes('sheet') || type.includes('excel')) return 'spreadsheet';
if (type.includes('presentation') || type.includes('powerpoint')) return 'presentation';
if (type.startsWith('video/')) return 'video';
if (type.startsWith('audio/')) return 'audio';
if (type.includes('zip') || type.includes('compressed')) return 'archive';
return 'file';
}
private getFileIcon(file: File): string {
const type = this.getFileType(file);
const iconMap = {
'image': 'lucide:image',
'pdf': 'lucide:file-text',
'doc': 'lucide:file-text',
'spreadsheet': 'lucide:table',
'presentation': 'lucide:presentation',
'video': 'lucide:video',
'audio': 'lucide:music',
'archive': 'lucide:archive',
'file': 'lucide:file'
};
return iconMap[type] || 'lucide:file';
}
private canShowPreview(file: File): boolean {
return file.type.startsWith('image/') && file.size < 5 * 1024 * 1024; // 5MB limit for previews
}
private validateFile(file: File): boolean {
// Check file size
if (this.maxSize > 0 && file.size > this.maxSize) {
this.validationMessage = `File "${file.name}" exceeds maximum size of ${this.formatFileSize(this.maxSize)}`;
this.validationState = 'invalid';
return false;
}
// Check file type
if (this.accept) {
const acceptedTypes = this.accept.split(',').map(s => s.trim());
let isAccepted = false;
for (const acceptType of acceptedTypes) {
if (acceptType.startsWith('.')) {
// Extension check
if (file.name.toLowerCase().endsWith(acceptType.toLowerCase())) {
isAccepted = true;
break;
}
} else if (acceptType.endsWith('/*')) {
// MIME type wildcard check
const mimePrefix = acceptType.slice(0, -2);
if (file.type.startsWith(mimePrefix)) {
isAccepted = true;
break;
}
} else if (file.type === acceptType) {
// Exact MIME type check
isAccepted = true;
break;
}
}
if (!isAccepted) {
this.validationMessage = `File type not accepted. Please upload: ${this.accept}`;
this.validationState = 'invalid';
return false;
}
}
return true;
}
public async openFileSelector() { public async openFileSelector() {
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();
this.state = 'idle'; }
this.buttonText = 'Upload more files...';
private removeFile(file: File) {
const index = this.value.indexOf(file);
if (index > -1) {
this.value.splice(index, 1);
this.requestUpdate();
this.validate();
this.changeSubject.next(this);
}
}
private clearAll() {
this.value = [];
this.requestUpdate();
this.validate();
this.changeSubject.next(this);
} }
public async updateValue(eventArg: Event) { public async updateValue(eventArg: Event) {
@ -198,52 +587,135 @@ export class DeesInputFileupload extends DeesInputBase<DeesInputFileupload> {
public firstUpdated(_changedProperties: Map<string | number | symbol, unknown>) { public firstUpdated(_changedProperties: Map<string | number | symbol, unknown>) {
super.firstUpdated(_changedProperties); super.firstUpdated(_changedProperties);
const inputFile: HTMLInputElement = this.shadowRoot.querySelector('input[type="file"]'); const inputFile: HTMLInputElement = this.shadowRoot.querySelector('input[type="file"]');
inputFile.addEventListener('change', (event: Event) => { inputFile.addEventListener('change', async (event: Event) => {
const target = event.target as HTMLInputElement; const target = event.target as HTMLInputElement;
for (const file of Array.from(target.files)) { const newFiles = Array.from(target.files);
this.value.push(file);
} // Always reset loading state when file dialog interaction completes
this.requestUpdate(); this.isLoading = false;
console.log(`Got ${this.value.length} files!`);
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 = '';
}); });
// lets handle drag and drop // Handle drag and drop
const dropArea = this.shadowRoot.querySelector('.maincontainer'); const dropArea = this.shadowRoot.querySelector('.maincontainer');
const handlerFunction = (eventArg: DragEvent) => { const handlerFunction = async (eventArg: DragEvent) => {
eventArg.preventDefault(); eventArg.preventDefault();
eventArg.stopPropagation();
switch (eventArg.type) { switch (eventArg.type) {
case 'dragenter':
case 'dragover': case 'dragover':
this.state = 'dragOver'; this.state = 'dragOver';
this.buttonText = 'release to upload file...';
break; break;
case 'dragleave': case 'dragleave':
this.state = 'idle'; // Check if we're actually leaving the drop area
this.buttonText = 'Upload File...'; const rect = dropArea.getBoundingClientRect();
const x = eventArg.clientX;
const y = eventArg.clientY;
if (x <= rect.left || x >= rect.right || y <= rect.top || y >= rect.bottom) {
this.state = 'idle';
}
break; break;
case 'drop': case 'drop':
this.state = 'idle'; this.state = 'idle';
this.buttonText = 'Upload more files...'; const files = Array.from(eventArg.dataTransfer.files);
await this.addFiles(files);
break;
} }
console.log(eventArg);
for (const file of Array.from(eventArg.dataTransfer.files)) {
this.value.push(file);
this.requestUpdate();
}
console.log(`Got ${this.value.length} files!`);
}; };
dropArea.addEventListener('dragenter', handlerFunction, false); dropArea.addEventListener('dragenter', handlerFunction, false);
dropArea.addEventListener('dragleave', handlerFunction, false); dropArea.addEventListener('dragleave', handlerFunction, false);
dropArea.addEventListener('dragover', handlerFunction, false); dropArea.addEventListener('dragover', handlerFunction, false);
dropArea.addEventListener('drop', handlerFunction, false); dropArea.addEventListener('drop', handlerFunction, false);
} }
private async addFiles(files: File[]) {
const filesToAdd: File[] = [];
for (const file of files) {
if (this.validateFile(file)) {
filesToAdd.push(file);
}
}
if (filesToAdd.length === 0) return;
// Check max files limit
if (this.maxFiles > 0) {
const totalFiles = this.value.length + filesToAdd.length;
if (totalFiles > this.maxFiles) {
const allowedCount = this.maxFiles - this.value.length;
if (allowedCount <= 0) {
this.validationMessage = `Maximum ${this.maxFiles} files allowed`;
this.validationState = 'invalid';
return;
}
filesToAdd.splice(allowedCount);
this.validationMessage = `Only ${allowedCount} more file(s) can be added`;
this.validationState = 'warn';
}
}
// Add files
if (!this.multiple && filesToAdd.length > 0) {
this.value = [filesToAdd[0]];
} else {
this.value.push(...filesToAdd);
}
this.requestUpdate();
this.validate();
this.changeSubject.next(this);
// Update button text
if (this.value.length > 0) {
this.buttonText = this.multiple ? 'Add more files' : 'Replace file';
}
}
public async validate(): Promise<boolean> {
this.validationMessage = '';
if (this.required && this.value.length === 0) {
this.validationState = 'invalid';
this.validationMessage = 'Please select at least one file';
return false;
}
// Validate all files
for (const file of this.value) {
if (!this.validateFile(file)) {
return false;
}
}
this.validationState = 'valid';
return true;
}
public getValue(): File[] { public getValue(): File[] {
return this.value; return this.value;
} }
public setValue(value: File[]): void { public setValue(value: File[]): void {
this.value = value; this.value = value;
this.requestUpdate();
if (value.length > 0) {
this.buttonText = this.multiple ? 'Add more files' : 'Replace file';
} else {
this.buttonText = 'Upload File...';
}
}
public updated(changedProperties: Map<string, any>) {
super.updated(changedProperties);
if (changedProperties.has('value')) {
this.validate();
}
} }
} }

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

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

View File

@ -1,135 +0,0 @@
import {customElement, type TemplateResult, property, html, css, cssManager} from '@design.estate/dees-element';
import { DeesInputBase } from './dees-input-base.js';
import { demoFunc } from './dees-input-radio.demo.js';
declare global {
interface HTMLElementTagNameMap {
'dees-input-radio': DeesInputRadio;
}
}
@customElement('dees-input-radio')
export class DeesInputRadio extends DeesInputBase<DeesInputRadio> {
public static demo = demoFunc;
// INSTANCE
@property()
public value: boolean = false;
@property({ type: String })
public name: string = '';
constructor() {
super();
this.labelPosition = 'right'; // Radio buttons default to label on the right
}
public static styles = [
...DeesInputBase.baseStyles,
cssManager.defaultStyles,
css`
* {
box-sizing: border-box;
}
:host {
position: relative;
}
.maincontainer {
transition: all 0.3s;
padding: 5px 0px;
color: #ccc;
}
.maincontainer:hover {
color: #fff;
}
input:focus {
outline: none;
border-bottom: 1px solid #e4002b;
}
.checkbox {
transition: all 0.3s;
box-sizing: border-box;
border-radius: 20px;
border: 1px solid #999;
height: 24px;
width: 24px;
display: inline-block;
background: #222;
}
.checkbox.selected {
background: #0050b9;
border: 1px solid #0050b9;
}
.maincontainer:hover .checkbox.selected {
background: #03A9F4;
}
.innercircle {
transition: all 0.3s;
margin: 6px 0px 0px 6px;
background: #222;
width: 10px;
height: 10px;
border-radius: 10px;
}
`,
];
public render(): TemplateResult {
return html`
<div class="input-wrapper">
<div class="maincontainer" @click="${this.toggleSelected}">
<div class="checkbox ${this.value ? 'selected' : ''}">
${this.value ? html`<div class="innercircle"></div>`: html``}
</div>
</div>
<dees-label .label=${this.label}></dees-label>
</div>
`;
}
public async toggleSelected () {
// Radio buttons can only be selected, not deselected by clicking
if (this.value) {
return;
}
// If this radio has a name, find and deselect other radios in the same group
if (this.name) {
// Try to find a form container first, then fall back to document
const container = this.closest('dees-form') ||
this.closest('dees-demowrapper') ||
this.closest('.radio-group')?.parentElement ||
document;
const allRadios = container.querySelectorAll(`dees-input-radio[name="${this.name}"]`);
allRadios.forEach((radio: DeesInputRadio) => {
if (radio !== this && radio.value) {
radio.value = false;
}
});
}
this.value = true;
this.dispatchEvent(new CustomEvent('newValue', {
detail: this.value,
bubbles: true
}));
this.changeSubject.next(this);
}
public getValue(): boolean {
return this.value;
}
public setValue(value: boolean): void {
this.value = value;
}
}

View File

@ -0,0 +1,200 @@
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;
}
.demo-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 24px;
}
.result-display {
margin-top: 16px;
padding: 12px;
background: rgba(0, 105, 242, 0.1);
border-radius: 4px;
font-family: monospace;
font-size: 14px;
}
`}
</style>
<div class="demo-container">
<dees-panel .title=${'1. Basic Radio Groups'} .subtitle=${'Simple string options for common use cases'}>
<div class="demo-grid">
<dees-input-radiogroup
.label=${'Subscription Plan'}
.options=${['Basic - $9/month', 'Pro - $29/month', 'Enterprise - $99/month']}
.selectedOption=${'Pro - $29/month'}
.description=${'Choose your subscription tier'}
></dees-input-radiogroup>
<dees-input-radiogroup
.label=${'Priority Level'}
.options=${['High', 'Medium', 'Low']}
.selectedOption=${'Medium'}
.required=${true}
></dees-input-radiogroup>
</div>
</dees-panel>
<dees-panel .title=${'2. Horizontal Layout'} .subtitle=${'Radio groups with horizontal arrangement'}>
<dees-input-radiogroup
.label=${'Do you agree with the terms?'}
.options=${['Yes', 'No', 'Maybe']}
.direction=${'horizontal'}
.selectedOption=${'Yes'}
></dees-input-radiogroup>
<dees-input-radiogroup
.label=${'Experience Level'}
.options=${['Beginner', 'Intermediate', 'Expert']}
.direction=${'horizontal'}
.selectedOption=${'Intermediate'}
.description=${'Select your experience level with web development'}
></dees-input-radiogroup>
</dees-panel>
<dees-panel .title=${'3. Advanced Options'} .subtitle=${'Using object format with keys and payloads'}>
<dees-input-radiogroup
id="advanced-radio"
.label=${'Select Region'}
.options=${[
{ option: 'United States (US East)', key: 'us-east', payload: { region: 'us-east-1', latency: 20 } },
{ option: 'Europe (Frankfurt)', key: 'eu-central', payload: { region: 'eu-central-1', latency: 50 } },
{ option: 'Asia Pacific (Singapore)', key: 'ap-southeast', payload: { region: 'ap-southeast-1', latency: 120 } }
]}
.selectedOption=${'eu-central'}
.description=${'Choose the closest region for optimal performance'}
@change=${(e: CustomEvent) => {
const display = document.querySelector('#region-result');
if (display) {
display.textContent = 'Selected: ' + JSON.stringify(e.detail.value, null, 2);
}
}}
></dees-input-radiogroup>
<div id="region-result" class="result-display">Selected: { "region": "eu-central-1", "latency": 50 }</div>
</dees-panel>
<dees-panel .title=${'4. Survey Example'} .subtitle=${'Multiple radio groups for surveys and forms'}>
<div class="demo-grid">
<dees-input-radiogroup
.label=${'How satisfied are you?'}
.options=${['Very Satisfied', 'Satisfied', 'Neutral', 'Dissatisfied', 'Very Dissatisfied']}
.selectedOption=${'Satisfied'}
></dees-input-radiogroup>
<dees-input-radiogroup
.label=${'Would you recommend us?'}
.options=${['Definitely', 'Probably', 'Not Sure', 'Probably Not', 'Definitely Not']}
.selectedOption=${'Probably'}
></dees-input-radiogroup>
</div>
</dees-panel>
<dees-panel .title=${'5. States & Validation'} .subtitle=${'Different states and validation examples'}>
<div class="demo-grid">
<dees-input-radiogroup
.label=${'Required Selection'}
.options=${['Option A', 'Option B', 'Option C']}
.required=${true}
.description=${'This field is required'}
></dees-input-radiogroup>
<dees-input-radiogroup
.label=${'Disabled State'}
.options=${['Disabled Option 1', 'Disabled Option 2', 'Disabled Option 3']}
.selectedOption=${'Disabled Option 2'}
.disabled=${true}
></dees-input-radiogroup>
</div>
</dees-panel>
<dees-panel .title=${'6. Settings Example'} .subtitle=${'Common patterns in application settings'}>
<dees-input-radiogroup
.label=${'Theme Preference'}
.options=${[
{ option: 'Light Theme', key: 'light', payload: 'light' },
{ option: 'Dark Theme', key: 'dark', payload: 'dark' },
{ option: 'System Default', key: 'system', payload: 'auto' }
]}
.selectedOption=${'dark'}
.description=${'Choose how the application should appear'}
></dees-input-radiogroup>
<dees-input-radiogroup
.label=${'Notification Frequency'}
.options=${['All Notifications', 'Important Only', 'None']}
.selectedOption=${'Important Only'}
.description=${'Control how often you receive notifications'}
></dees-input-radiogroup>
<dees-input-radiogroup
.label=${'Language'}
.options=${['English', 'German', 'French', 'Spanish', 'Japanese']}
.selectedOption=${'English'}
.direction=${'horizontal'}
></dees-input-radiogroup>
</dees-panel>
<dees-panel .title=${'7. Form Integration'} .subtitle=${'Works seamlessly with dees-form'}>
<dees-form>
<dees-input-text
.label=${'Product Name'}
.required=${true}
.key=${'productName'}
></dees-input-text>
<dees-input-radiogroup
.label=${'Product Category'}
.options=${['Electronics', 'Clothing', 'Books', 'Home & Garden', 'Sports']}
.required=${true}
.key=${'category'}
></dees-input-radiogroup>
<dees-input-radiogroup
.label=${'Condition'}
.options=${['New', 'Like New', 'Good', 'Fair', 'Poor']}
.direction=${'horizontal'}
.key=${'condition'}
.selectedOption=${'New'}
></dees-input-radiogroup>
<dees-input-radiogroup
.label=${'Shipping Speed'}
.options=${[
{ option: 'Standard (5-7 days)', key: 'standard', payload: { days: 7, price: 0 } },
{ option: 'Express (2-3 days)', key: 'express', payload: { days: 3, price: 10 } },
{ option: 'Overnight', key: 'overnight', payload: { days: 1, price: 25 } }
]}
.selectedOption=${'standard'}
.key=${'shipping'}
></dees-input-radiogroup>
<dees-form-submit .text=${'Submit Product'}></dees-form-submit>
</dees-form>
</dees-panel>
</div>
</dees-demowrapper>
`;

View File

@ -0,0 +1,357 @@
import {
customElement,
type TemplateResult,
property,
html,
css,
cssManager,
} from '@design.estate/dees-element';
import { DeesInputBase } from './dees-input-base.js';
import { demoFunc } from './dees-input-radiogroup.demo.js';
declare global {
interface HTMLElementTagNameMap {
'dees-input-radiogroup': DeesInputRadiogroup;
}
}
type RadioOption = string | { option: string; key: string; payload?: any };
@customElement('dees-input-radiogroup')
export class DeesInputRadiogroup extends DeesInputBase<string | object> {
public static demo = demoFunc;
// INSTANCE
@property({ type: Array })
public options: RadioOption[] = [];
@property()
public selectedOption: string = '';
@property({ type: String })
public direction: 'vertical' | 'horizontal' = 'vertical';
@property({ type: String, reflect: true })
public validationState: 'valid' | 'invalid' | 'warn' | 'pending' = null;
// Form compatibility
public get value() {
const option = this.getOptionByKey(this.selectedOption);
if (typeof option === 'object' && option.payload !== undefined) {
return option.payload;
}
return this.selectedOption;
}
public set value(val: string | any) {
if (typeof val === 'string') {
this.selectedOption = val;
} else {
// Try to find option by payload
const option = this.options.find(opt =>
typeof opt === 'object' && opt.payload === val
);
if (option && typeof option === 'object') {
this.selectedOption = option.key;
}
}
}
public static styles = [
...DeesInputBase.baseStyles,
cssManager.defaultStyles,
css`
* {
box-sizing: border-box;
}
:host {
display: block;
position: relative;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
}
.maincontainer {
display: flex;
flex-direction: column;
gap: 10px;
}
.maincontainer.horizontal {
flex-direction: row;
flex-wrap: wrap;
gap: 20px;
}
.radio-option {
display: flex;
align-items: center;
gap: 10px;
padding: 6px 0;
cursor: pointer;
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
user-select: none;
position: relative;
border-radius: 4px;
}
.maincontainer.horizontal .radio-option {
padding: 6px 20px 6px 0;
}
.radio-option:hover .radio-circle {
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 {
color: ${cssManager.bdTheme('hsl(215.3 25% 8.8%)', 'hsl(210 40% 98%)')};
}
.radio-circle {
width: 20px;
height: 20px;
border-radius: 50%;
border: 2px solid ${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%)')};
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
}
.radio-option.selected .radio-circle {
border-color: ${cssManager.bdTheme('hsl(217.2 91.2% 59.8%)', 'hsl(213.1 93.9% 67.8%)')};
background: ${cssManager.bdTheme('hsl(217.2 91.2% 59.8%)', 'hsl(213.1 93.9% 67.8%)')};
}
.radio-option.selected .radio-circle::after {
content: '';
position: absolute;
width: 8px;
height: 8px;
border-radius: 50%;
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 {
font-size: 14px;
font-weight: 500;
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 {
color: ${cssManager.bdTheme('hsl(215.3 25% 8.8%)', 'hsl(210 40% 98%)')};
}
:host([disabled]) .radio-option {
cursor: not-allowed;
opacity: 0.5;
}
:host([disabled]) .radio-option:hover .radio-circle {
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 {
color: ${cssManager.bdTheme('hsl(215.3 25% 26.7%)', 'hsl(217.9 10.6% 74.9%)')};
}
.label-text {
font-size: 14px;
font-weight: 500;
color: ${cssManager.bdTheme('hsl(215.3 25% 8.8%)', 'hsl(210 40% 98%)')};
margin-bottom: 10px;
letter-spacing: -0.006em;
line-height: 20px;
}
.description-text {
font-size: 13px;
color: ${cssManager.bdTheme('hsl(215.4 16.3% 56.9%)', 'hsl(215 20.2% 55.1%)')};
margin-top: 10px;
line-height: 1.5;
letter-spacing: -0.003em;
}
/* Validation styles */
:host([validationState="invalid"]) .radio-circle {
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 {
border-color: ${cssManager.bdTheme('hsl(142.1 70.6% 45.3%)', 'hsl(142.1 76.2% 36.3%)')};
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 {
border-color: ${cssManager.bdTheme('hsl(45.4 93.4% 47.5%)', 'hsl(45.4 93.4% 47.5%)')};
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 */
:host([label-position="left"]) .input-wrapper {
grid-template-columns: auto auto;
}
:host([label-position="right"]) .input-wrapper {
grid-template-columns: auto auto;
}
`,
];
public render(): TemplateResult {
return html`
<div class="input-wrapper">
${this.label ? html`<div class="label-text">${this.label}</div>` : ''}
<div class="maincontainer ${this.direction}">
${this.options.map((option) => {
const optionKey = this.getOptionKey(option);
const optionLabel = this.getOptionLabel(option);
const isSelected = this.selectedOption === optionKey;
return html`
<div
class="radio-option ${isSelected ? 'selected' : ''}"
@click="${() => this.selectOption(optionKey)}"
@keydown="${(e: KeyboardEvent) => this.handleKeydown(e, optionKey)}"
>
<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>
`;
})}
</div>
${this.description ? html`<div class="description-text">${this.description}</div>` : ''}
</div>
`;
}
private getOptionKey(option: RadioOption): string {
if (typeof option === 'string') {
return option;
}
return option.key;
}
private getOptionLabel(option: RadioOption): string {
if (typeof option === 'string') {
return option;
}
return option.option;
}
private getOptionByKey(key: string): RadioOption | undefined {
return this.options.find(opt => this.getOptionKey(opt) === key);
}
private selectOption(key: string): void {
if (this.disabled) {
return;
}
const oldValue = this.selectedOption;
this.selectedOption = key;
if (oldValue !== key) {
this.dispatchEvent(new CustomEvent('change', {
detail: { value: this.value },
bubbles: true,
composed: true,
}));
this.dispatchEvent(new CustomEvent('input', {
detail: { value: this.value },
bubbles: true,
composed: true,
}));
this.changeSubject.next(this);
}
}
public getValue(): string | any {
return this.value;
}
public setValue(val: string | any): void {
this.value = val;
}
public async validate(): Promise<boolean> {
if (this.required && !this.selectedOption) {
this.validationState = 'invalid';
return false;
}
this.validationState = 'valid';
return true;
}
public async firstUpdated() {
// Auto-select first option if none selected and not required
if (!this.selectedOption && this.options.length > 0 && !this.required) {
const firstOption = this.options[0];
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

@ -0,0 +1,133 @@
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;
}
}
`}
</style>
<div class="demo-container">
<dees-panel .title=${'1. Basic Rich Text Editor'} .subtitle=${'A full-featured rich text editor with formatting toolbar'}>
<dees-input-richtext
.label=${'Article Content'}
.value=${'<h1>Welcome to the Rich Text Editor!</h1><p>This is a feature-rich editor built with TipTap. You can:</p><ul><li><strong>Format text</strong> with <em>various</em> <u>styles</u></li><li>Create different heading levels</li><li>Add <a href="https://example.com">links</a> to external resources</li><li>Write <code>inline code</code> or code blocks</li></ul><blockquote><p>Use the toolbar above to explore all the formatting options available!</p></blockquote><p>Start typing to see the magic happen...</p>'}
.description=${'Use the toolbar to format your content with headings, lists, links, and more'}
.showWordCount=${true}
></dees-input-richtext>
</dees-panel>
<dees-panel .title=${'2. With Placeholder'} .subtitle=${'Empty editor with placeholder text'}>
<dees-input-richtext
.label=${'Blog Post'}
.placeholder=${'Start writing your blog post here...'}
.showWordCount=${true}
></dees-input-richtext>
</dees-panel>
<dees-panel .title=${'3. Different Heights'} .subtitle=${'Editors with different minimum heights for various use cases'}>
<div class="grid-layout">
<dees-input-richtext
.label=${'Short Note'}
.minHeight=${150}
.placeholder=${'Quick note...'}
.showWordCount=${false}
></dees-input-richtext>
<dees-input-richtext
.label=${'Extended Content'}
.minHeight=${300}
.placeholder=${'Write your extended content here...'}
.showWordCount=${true}
></dees-input-richtext>
</div>
</dees-panel>
<dees-panel .title=${'4. Code Examples'} .subtitle=${'Editor pre-filled with code examples'}>
<dees-input-richtext
.label=${'Technical Documentation'}
.value=${'<h2>Installation Guide</h2><p>To install the package, run the following command:</p><pre><code>npm install @design.estate/dees-catalog</code></pre><p>Then import the component in your TypeScript file:</p><pre><code>import { DeesInputRichtext } from "@design.estate/dees-catalog";</code></pre><p>You can now use the <code>&lt;dees-input-richtext&gt;</code> element in your templates.</p>'}
.minHeight=${250}
.showWordCount=${true}
></dees-input-richtext>
</dees-panel>
<dees-panel .title=${'5. Disabled State'} .subtitle=${'Read-only rich text content'}>
<dees-input-richtext
.label=${'Published Article (Read Only)'}
.value=${'<h2>The Future of Web Components</h2><p>Web Components have revolutionized how we build modern web applications...</p><blockquote><p>"The future of web development lies in reusable, encapsulated components."</p></blockquote>'}
.disabled=${true}
.showWordCount=${true}
></dees-input-richtext>
</dees-panel>
<dees-panel .title=${'6. Interactive Demo'} .subtitle=${'Type in the editor below and see the HTML output'}>
<dees-input-richtext
id="interactive-editor"
.label=${'Try it yourself'}
.placeholder=${'Type something here...'}
.showWordCount=${true}
@change=${(e: CustomEvent) => {
const output = document.querySelector('#output-preview');
if (output) {
output.textContent = e.detail.value;
}
}}
></dees-input-richtext>
<div class="output-preview" id="output-preview">
<em>HTML output will appear here...</em>
</div>
</dees-panel>
</div>
</dees-demowrapper>
`;

View File

@ -0,0 +1,714 @@
import * as colors from './00colors.js';
import { DeesInputBase } from './dees-input-base.js';
import { demoFunc } from './dees-input-richtext.demo.js';
import './dees-icon.js';
import {
customElement,
type TemplateResult,
property,
html,
css,
cssManager,
state,
query,
} from '@design.estate/dees-element';
import * as domtools from '@design.estate/dees-domtools';
import { Editor } from '@tiptap/core';
import StarterKit from '@tiptap/starter-kit';
import Underline from '@tiptap/extension-underline';
import TextAlign from '@tiptap/extension-text-align';
import Link from '@tiptap/extension-link';
import Typography from '@tiptap/extension-typography';
declare global {
interface HTMLElementTagNameMap {
'dees-input-richtext': DeesInputRichtext;
}
}
interface IToolbarButton {
name: string;
icon?: string;
action?: () => void;
isActive?: () => boolean;
title: string;
isDivider?: boolean;
}
@customElement('dees-input-richtext')
export class DeesInputRichtext extends DeesInputBase<string> {
public static demo = demoFunc;
// INSTANCE
@property({
type: String,
reflect: true,
})
public value: string = '';
@property({
type: String,
})
public placeholder: string = '';
@property({
type: Boolean,
})
public showWordCount: boolean = true;
@property({
type: Number,
})
public minHeight: number = 200;
@state()
private showLinkInput: boolean = false;
@state()
private wordCount: number = 0;
@query('.editor-content')
private editorElement: HTMLElement;
@query('.link-input input')
private linkInputElement: HTMLInputElement;
private editor: Editor;
public static styles = [
...DeesInputBase.baseStyles,
cssManager.defaultStyles,
css`
:host {
display: block;
position: relative;
font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
.input-wrapper {
position: relative;
}
.label {
display: block;
margin-bottom: 8px;
font-size: 14px;
font-weight: 500;
color: ${cssManager.bdTheme('hsl(0 0% 15%)', 'hsl(0 0% 93.9%)')};
}
.editor-container {
display: flex;
flex-direction: column;
min-height: ${cssManager.bdTheme('200px', '200px')};
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%)')};
overflow: hidden;
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
}
.editor-container:hover {
border-color: ${cssManager.bdTheme('hsl(0 0% 79.8%)', 'hsl(0 0% 20.9%)')};
}
.editor-container.focused {
border-color: ${cssManager.bdTheme('hsl(0 0% 9%)', 'hsl(0 0% 98%)')};
box-shadow: 0 0 0 2px ${cssManager.bdTheme('hsl(0 0% 9% / 0.05)', 'hsl(0 0% 98% / 0.05)')};
}
.editor-toolbar {
display: flex;
flex-wrap: wrap;
gap: 4px;
padding: 8px 12px;
background: ${cssManager.bdTheme('hsl(210 40% 96.1%)', 'hsl(0 0% 14.9%)')};
border-bottom: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
align-items: center;
position: relative;
}
.toolbar-button {
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
border: none;
border-radius: 4px;
background: transparent;
cursor: pointer;
font-size: 14px;
font-weight: 500;
color: ${cssManager.bdTheme('hsl(215.4 16.3% 46.9%)', 'hsl(215 20.2% 65.1%)')};
transition: all 0.15s ease;
user-select: none;
}
.toolbar-button dees-icon {
width: 16px;
height: 16px;
}
.toolbar-button: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%)')};
}
.toolbar-button.active {
background: ${cssManager.bdTheme('hsl(0 0% 9%)', 'hsl(0 0% 98%)')};
color: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(0 0% 3.9%)')};
}
.toolbar-button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.toolbar-divider {
width: 1px;
height: 24px;
background: ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
margin: 0 4px;
}
.editor-content {
flex: 1;
padding: 16px;
overflow-y: auto;
min-height: var(--min-height, 200px);
}
.editor-content .ProseMirror {
outline: none;
line-height: 1.6;
color: ${cssManager.bdTheme('hsl(0 0% 3.9%)', 'hsl(0 0% 98%)')};
min-height: 100%;
}
.editor-content .ProseMirror p {
margin: 0.5em 0;
}
.editor-content .ProseMirror p:first-child {
margin-top: 0;
}
.editor-content .ProseMirror p:last-child {
margin-bottom: 0;
}
.editor-content .ProseMirror h1 {
font-size: 2em;
font-weight: bold;
margin: 1em 0 0.5em 0;
line-height: 1.2;
}
.editor-content .ProseMirror h2 {
font-size: 1.5em;
font-weight: bold;
margin: 1em 0 0.5em 0;
line-height: 1.3;
}
.editor-content .ProseMirror h3 {
font-size: 1.25em;
font-weight: bold;
margin: 1em 0 0.5em 0;
line-height: 1.4;
}
.editor-content .ProseMirror ul,
.editor-content .ProseMirror ol {
padding-left: 1.5em;
margin: 0.5em 0;
}
.editor-content .ProseMirror li {
margin: 0.25em 0;
}
.editor-content .ProseMirror blockquote {
border-left: 4px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
margin: 1em 0;
padding-left: 1em;
color: ${cssManager.bdTheme('hsl(215.4 16.3% 46.9%)', 'hsl(215 20.2% 65.1%)')};
font-style: italic;
}
.editor-content .ProseMirror code {
background: ${cssManager.bdTheme('hsl(0 0% 95.1%)', 'hsl(0 0% 14.9%)')};
border-radius: 3px;
padding: 0.2em 0.4em;
font-family: 'Intel One Mono', 'Fira Code', 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
font-size: 0.9em;
color: ${cssManager.bdTheme('hsl(0 0% 15%)', 'hsl(0 0% 93.9%)')};
}
.editor-content .ProseMirror pre {
background: ${cssManager.bdTheme('hsl(0 0% 3.9%)', 'hsl(0 0% 98%)')};
color: ${cssManager.bdTheme('hsl(0 0% 98%)', 'hsl(0 0% 3.9%)')};
border-radius: 6px;
padding: 1em;
margin: 1em 0;
overflow-x: auto;
}
.editor-content .ProseMirror pre code {
background: none;
color: inherit;
padding: 0;
border-radius: 0;
}
.editor-content .ProseMirror a {
color: ${cssManager.bdTheme('hsl(222.2 47.4% 51.2%)', 'hsl(217.2 91.2% 59.8%)')};
text-decoration: underline;
cursor: pointer;
}
.editor-content .ProseMirror a:hover {
color: ${cssManager.bdTheme('hsl(222.2 47.4% 41.2%)', 'hsl(217.2 91.2% 69.8%)')};
}
.editor-footer {
padding: 8px 12px;
background: ${cssManager.bdTheme('hsl(210 40% 96.1%)', 'hsl(0 0% 14.9%)')};
border-top: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
font-size: 12px;
color: ${cssManager.bdTheme('hsl(215.4 16.3% 46.9%)', 'hsl(215 20.2% 65.1%)')};
display: flex;
justify-content: space-between;
align-items: center;
}
.word-count {
font-weight: 500;
}
.link-input {
display: none;
position: absolute;
top: 100%;
left: 0;
right: 0;
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;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
padding: 12px;
z-index: 1000;
}
.link-input.show {
display: block;
}
.link-input input {
width: 100%;
padding: 8px 12px;
border: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
border-radius: 6px;
outline: none;
font-size: 14px;
background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(0 0% 9%)')};
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 {
border-color: ${cssManager.bdTheme('hsl(0 0% 9%)', 'hsl(0 0% 98%)')};
box-shadow: 0 0 0 2px ${cssManager.bdTheme('hsl(0 0% 9% / 0.05)', 'hsl(0 0% 98% / 0.05)')};
}
.link-input-buttons {
display: flex;
gap: 8px;
margin-top: 8px;
}
.link-input-buttons button {
padding: 6px 12px;
border: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
border-radius: 4px;
background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(0 0% 9%)')};
cursor: pointer;
font-size: 12px;
color: ${cssManager.bdTheme('hsl(0 0% 45.1%)', 'hsl(0 0% 63.9%)')};
transition: all 0.15s ease;
font-weight: 500;
}
.link-input-buttons button: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%)')};
}
.link-input-buttons button.primary {
background: ${cssManager.bdTheme('hsl(0 0% 9%)', 'hsl(0 0% 98%)')};
color: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(0 0% 3.9%)')};
border-color: ${cssManager.bdTheme('hsl(0 0% 9%)', 'hsl(0 0% 98%)')};
}
.link-input-buttons button.primary:hover {
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 {
margin-top: 8px;
font-size: 12px;
color: ${cssManager.bdTheme('hsl(215.4 16.3% 46.9%)', 'hsl(215 20.2% 65.1%)')};
line-height: 1.4;
}
:host([disabled]) .editor-container {
opacity: 0.6;
cursor: not-allowed;
}
:host([disabled]) .toolbar-button,
:host([disabled]) .editor-content {
pointer-events: none;
}
`,
];
public render(): TemplateResult {
return html`
<div class="input-wrapper">
${this.label ? html`<label class="label">${this.label}</label>` : ''}
<div class="editor-container ${this.editor?.isFocused ? 'focused' : ''}" style="--min-height: ${this.minHeight}px">
<div class="editor-toolbar">
${this.renderToolbar()}
<div class="link-input ${this.showLinkInput ? 'show' : ''}">
<input type="url" placeholder="Enter URL..." @keydown=${this.handleLinkInputKeydown} />
<div class="link-input-buttons">
<button class="primary" @click=${this.saveLink}>Save</button>
<button @click=${this.removeLink}>Remove</button>
<button @click=${this.hideLinkInput}>Cancel</button>
</div>
</div>
</div>
<div class="editor-content"></div>
${this.showWordCount
? html`
<div class="editor-footer">
<span class="word-count">${this.wordCount} word${this.wordCount !== 1 ? 's' : ''}</span>
</div>
`
: ''}
</div>
${this.description ? html`<div class="description">${this.description}</div>` : ''}
</div>
`;
}
private renderToolbar(): TemplateResult {
const buttons: IToolbarButton[] = this.getToolbarButtons();
return html`
${buttons.map((button) => {
if (button.isDivider) {
return html`<div class="toolbar-divider"></div>`;
}
return html`
<button
class="toolbar-button ${button.isActive?.() ? 'active' : ''}"
@click=${button.action}
title=${button.title}
?disabled=${this.disabled || !this.editor}
>
<dees-icon .icon=${button.icon}></dees-icon>
</button>
`;
})}
`;
}
private getToolbarButtons(): IToolbarButton[] {
if (!this.editor) return [];
return [
{
name: 'bold',
icon: 'lucide:bold',
title: 'Bold (Ctrl+B)',
action: () => this.editor.chain().focus().toggleBold().run(),
isActive: () => this.editor.isActive('bold'),
},
{
name: 'italic',
icon: 'lucide:italic',
title: 'Italic (Ctrl+I)',
action: () => this.editor.chain().focus().toggleItalic().run(),
isActive: () => this.editor.isActive('italic'),
},
{
name: 'underline',
icon: 'lucide:underline',
title: 'Underline (Ctrl+U)',
action: () => this.editor.chain().focus().toggleUnderline().run(),
isActive: () => this.editor.isActive('underline'),
},
{
name: 'strike',
icon: 'lucide:strikethrough',
title: 'Strikethrough',
action: () => this.editor.chain().focus().toggleStrike().run(),
isActive: () => this.editor.isActive('strike'),
},
{ name: 'divider1', title: '', isDivider: true },
{
name: 'h1',
icon: 'lucide:heading1',
title: 'Heading 1',
action: () => this.editor.chain().focus().toggleHeading({ level: 1 }).run(),
isActive: () => this.editor.isActive('heading', { level: 1 }),
},
{
name: 'h2',
icon: 'lucide:heading2',
title: 'Heading 2',
action: () => this.editor.chain().focus().toggleHeading({ level: 2 }).run(),
isActive: () => this.editor.isActive('heading', { level: 2 }),
},
{
name: 'h3',
icon: 'lucide:heading3',
title: 'Heading 3',
action: () => this.editor.chain().focus().toggleHeading({ level: 3 }).run(),
isActive: () => this.editor.isActive('heading', { level: 3 }),
},
{ name: 'divider2', title: '', isDivider: true },
{
name: 'bulletList',
icon: 'lucide:list',
title: 'Bullet List',
action: () => this.editor.chain().focus().toggleBulletList().run(),
isActive: () => this.editor.isActive('bulletList'),
},
{
name: 'orderedList',
icon: 'lucide:listOrdered',
title: 'Numbered List',
action: () => this.editor.chain().focus().toggleOrderedList().run(),
isActive: () => this.editor.isActive('orderedList'),
},
{
name: 'blockquote',
icon: 'lucide:quote',
title: 'Quote',
action: () => this.editor.chain().focus().toggleBlockquote().run(),
isActive: () => this.editor.isActive('blockquote'),
},
{
name: 'code',
icon: 'lucide:code',
title: 'Code',
action: () => this.editor.chain().focus().toggleCode().run(),
isActive: () => this.editor.isActive('code'),
},
{
name: 'codeBlock',
icon: 'lucide:fileCode',
title: 'Code Block',
action: () => this.editor.chain().focus().toggleCodeBlock().run(),
isActive: () => this.editor.isActive('codeBlock'),
},
{ name: 'divider3', title: '', isDivider: true },
{
name: 'link',
icon: 'lucide:link',
title: 'Add Link',
action: () => this.toggleLink(),
isActive: () => this.editor.isActive('link'),
},
{
name: 'alignLeft',
icon: 'lucide:alignLeft',
title: 'Align Left',
action: () => this.editor.chain().focus().setTextAlign('left').run(),
isActive: () => this.editor.isActive({ textAlign: 'left' }),
},
{
name: 'alignCenter',
icon: 'lucide:alignCenter',
title: 'Align Center',
action: () => this.editor.chain().focus().setTextAlign('center').run(),
isActive: () => this.editor.isActive({ textAlign: 'center' }),
},
{
name: 'alignRight',
icon: 'lucide:alignRight',
title: 'Align Right',
action: () => this.editor.chain().focus().setTextAlign('right').run(),
isActive: () => this.editor.isActive({ textAlign: 'right' }),
},
{ name: 'divider4', title: '', isDivider: true },
{
name: 'undo',
icon: 'lucide:undo',
title: 'Undo (Ctrl+Z)',
action: () => this.editor.chain().focus().undo().run(),
},
{
name: 'redo',
icon: 'lucide:redo',
title: 'Redo (Ctrl+Y)',
action: () => this.editor.chain().focus().redo().run(),
},
];
}
public async firstUpdated() {
await this.updateComplete;
this.initializeEditor();
}
private initializeEditor(): void {
if (this.disabled) return;
this.editor = new Editor({
element: this.editorElement,
extensions: [
StarterKit.configure({
heading: {
levels: [1, 2, 3],
},
}),
Underline,
TextAlign.configure({
types: ['heading', 'paragraph'],
}),
Link.configure({
openOnClick: false,
HTMLAttributes: {
class: 'editor-link',
},
}),
Typography,
],
content: this.value || (this.placeholder ? `<p>${this.placeholder}</p>` : ''),
onUpdate: ({ editor }) => {
this.value = editor.getHTML();
this.updateWordCount();
this.dispatchEvent(
new CustomEvent('input', {
detail: { value: this.value },
bubbles: true,
composed: true,
})
);
this.dispatchEvent(
new CustomEvent('change', {
detail: { value: this.value },
bubbles: true,
composed: true,
})
);
},
onSelectionUpdate: () => {
this.requestUpdate();
},
onFocus: () => {
this.requestUpdate();
},
onBlur: () => {
this.requestUpdate();
},
});
this.updateWordCount();
}
private updateWordCount(): void {
if (!this.editor) return;
const text = this.editor.getText();
this.wordCount = text.trim() ? text.trim().split(/\s+/).length : 0;
}
private toggleLink(): void {
if (!this.editor) return;
if (this.editor.isActive('link')) {
const href = this.editor.getAttributes('link').href;
this.showLinkInput = true;
requestAnimationFrame(() => {
if (this.linkInputElement) {
this.linkInputElement.value = href || '';
this.linkInputElement.focus();
this.linkInputElement.select();
}
});
} else {
this.showLinkInput = true;
requestAnimationFrame(() => {
if (this.linkInputElement) {
this.linkInputElement.value = '';
this.linkInputElement.focus();
}
});
}
}
private saveLink(): void {
if (!this.editor || !this.linkInputElement) return;
const url = this.linkInputElement.value;
if (url) {
this.editor.chain().focus().setLink({ href: url }).run();
}
this.hideLinkInput();
}
private removeLink(): void {
if (!this.editor) return;
this.editor.chain().focus().unsetLink().run();
this.hideLinkInput();
}
private hideLinkInput(): void {
this.showLinkInput = false;
this.editor?.commands.focus();
}
private handleLinkInputKeydown(e: KeyboardEvent): void {
if (e.key === 'Enter') {
e.preventDefault();
this.saveLink();
} else if (e.key === 'Escape') {
e.preventDefault();
this.hideLinkInput();
}
}
public setValue(value: string): void {
this.value = value;
if (this.editor && value !== this.editor.getHTML()) {
this.editor.commands.setContent(value);
}
}
public getValue(): string {
return this.value;
}
public clear(): void {
this.setValue('');
}
public focus(): void {
this.editor?.commands.focus();
}
public async disconnectedCallback(): Promise<void> {
await super.disconnectedCallback();
if (this.editor) {
this.editor.destroy();
}
}
}

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

@ -1,5 +1,6 @@
import { html, css, type TemplateResult } from '@design.estate/dees-element'; import { html, css, type TemplateResult } from '@design.estate/dees-element';
import '@design.estate/dees-wcctools/demotools'; import '@design.estate/dees-wcctools/demotools';
import './dees-panel.js';
import type { DeesInputWysiwyg } from './dees-input-wysiwyg.js'; import type { DeesInputWysiwyg } from './dees-input-wysiwyg.js';
import type { IBlock } from './wysiwyg/wysiwyg.types.js'; import type { IBlock } from './wysiwyg/wysiwyg.types.js';
@ -630,7 +631,7 @@ export const demoFunc = (): TemplateResult => html`
</style> </style>
<div class="demo-container"> <div class="demo-container">
<dees-panel heading="🚀 Modern WYSIWYG Editor"> <dees-panel .title=${'1. 🚀 Modern WYSIWYG Editor'}>
<p class="panel-description"> <p class="panel-description">
A powerful block-based editor with slash commands, keyboard shortcuts, and multiple output formats. A powerful block-based editor with slash commands, keyboard shortcuts, and multiple output formats.
Perfect for content creation, blog posts, documentation, and more. Perfect for content creation, blog posts, documentation, and more.
@ -708,7 +709,7 @@ export const demoFunc = (): TemplateResult => html`
</div> </div>
</dees-panel> </dees-panel>
<dees-panel heading="📝 Blog Post Example"> <dees-panel .title=${'2. 📝 Blog Post Example'}>
<p class="panel-description"> <p class="panel-description">
Perfect for creating rich content with multiple block types. Perfect for creating rich content with multiple block types.
The editor preserves formatting and provides a clean editing experience. The editor preserves formatting and provides a clean editing experience.
@ -722,7 +723,7 @@ export const demoFunc = (): TemplateResult => html`
></dees-input-wysiwyg> ></dees-input-wysiwyg>
</dees-panel> </dees-panel>
<dees-panel heading="🔀 Drag & Drop Reordering"> <dees-panel .title=${'3. 🔀 Drag & Drop Reordering'}>
<p class="panel-description"> <p class="panel-description">
Easily rearrange your content blocks by dragging them. Easily rearrange your content blocks by dragging them.
Hover over any block to reveal the drag handle on the left side. Hover over any block to reveal the drag handle on the left side.
@ -746,7 +747,7 @@ export const demoFunc = (): TemplateResult => html`
</div> </div>
</dees-panel> </dees-panel>
<dees-panel heading="📚 Tutorial & Documentation"> <dees-panel .title=${'4. 📚 Tutorial & Documentation'}>
<p class="panel-description"> <p class="panel-description">
Create comprehensive tutorials and documentation with code examples, lists, and structured content. Create comprehensive tutorials and documentation with code examples, lists, and structured content.
</p> </p>
@ -850,7 +851,7 @@ git merge feature-branch
></dees-input-wysiwyg> ></dees-input-wysiwyg>
</dees-panel> </dees-panel>
<dees-panel heading="🔄 Output Formats"> <dees-panel .title=${'5. 🔄 Output Formats'}>
<p class="panel-description"> <p class="panel-description">
Choose between HTML and Markdown output formats depending on your needs. Choose between HTML and Markdown output formats depending on your needs.
Perfect for static site generators, documentation systems, or any content management workflow. Perfect for static site generators, documentation systems, or any content management workflow.
@ -930,7 +931,7 @@ Gradually blend in flour mixture, then stir in chocolate chips. Drop rounded tab
</div> </div>
</dees-panel> </dees-panel>
<dees-panel heading="🎨 Advanced Editing"> <dees-panel .title=${'6. 🎨 Advanced Editing'}>
<p class="panel-description"> <p class="panel-description">
Create complex documents with mixed content types. The editor handles all formatting seamlessly. Create complex documents with mixed content types. The editor handles all formatting seamlessly.
</p> </p>
@ -949,7 +950,7 @@ Gradually blend in flour mixture, then stir in chocolate chips. Drop rounded tab
></dees-input-wysiwyg> ></dees-input-wysiwyg>
</dees-panel> </dees-panel>
<dees-panel heading="⚙️ Form Integration"> <dees-panel .title=${'7. ⚙️ Form Integration'}>
<p class="panel-description"> <p class="panel-description">
Seamlessly integrates with dees-form for complete form solutions. Seamlessly integrates with dees-form for complete form solutions.
All standard form features like validation, required fields, and data binding work out of the box. All standard form features like validation, required fields, and data binding work out of the box.
@ -977,7 +978,7 @@ Gradually blend in flour mixture, then stir in chocolate chips. Drop rounded tab
</dees-form> </dees-form>
</dees-panel> </dees-panel>
<dees-panel heading="🧩 Programmatic Block Creation"> <dees-panel .title=${'8. 🧩 Programmatic Block Creation'}>
<p class="panel-description"> <p class="panel-description">
Create content programmatically using the block API for dynamic document generation. Create content programmatically using the block API for dynamic document generation.
</p> </p>
@ -1003,7 +1004,7 @@ Gradually blend in flour mixture, then stir in chocolate chips. Drop rounded tab
</div> </div>
</dees-panel> </dees-panel>
<dees-panel heading="📤 Export/Import Features"> <dees-panel .title=${'9. 📤 Export/Import Features'}>
<p class="panel-description"> <p class="panel-description">
The WYSIWYG editor provides multiple export formats and lossless save/restore capabilities for maximum flexibility. The WYSIWYG editor provides multiple export formats and lossless save/restore capabilities for maximum flexibility.
</p> </p>

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,4 +1,6 @@
import * as plugins from './00plugins.js'; import * as plugins from './00plugins.js';
import { zIndexRegistry } from './00zindex.js';
import { cssGeistFontFamily } from './00fonts.js';
import { import {
cssManager, cssManager,
css, css,
@ -8,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 {
@ -18,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;
@ -43,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() {
@ -73,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: 250; 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 {
@ -100,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>
`; `;
} }
@ -153,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');
} }
@ -175,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,37 +1,356 @@
import { html } from '@design.estate/dees-element'; import { html, css, cssManager } from '@design.estate/dees-element';
import { DeesModal } from './dees-modal.js'; import { DeesModal } from './dees-modal.js';
export const demoFunc = () => html` export const demoFunc = () => html`
<dees-button @click=${() => { <style>
DeesModal.createAndShow({ ${css`
heading: 'This is a heading', .demo-container {
content: html` display: flex;
<dees-form> flex-direction: column;
<dees-input-text gap: 24px;
.label=${'Username'} padding: 24px;
> max-width: 1200px;
</dees-input-text> margin: 0 auto;
<dees-input-text }
.label=${'Password'}
> .demo-section {
</dees-input-text> background: ${cssManager.bdTheme('#f8f9fa', '#1a1a1a')};
</dees-form> border-radius: 8px;
`, padding: 24px;
menuOptions: [{ border: 1px solid ${cssManager.bdTheme('#e0e0e0', '#333')};
name: 'Cancel', }
iconName: null,
action: async (deesModalArg) => { .demo-section h3 {
deesModalArg.destroy(); margin-top: 0;
return null; margin-bottom: 16px;
} color: ${cssManager.bdTheme('#333', '#fff')};
}, { }
name: 'Ok',
iconName: null, .demo-section p {
action: async (deesModalArg) => { color: ${cssManager.bdTheme('#666', '#999')};
deesModalArg.destroy(); margin-bottom: 16px;
return null; }
}
}], .button-grid {
}); display: grid;
}}>open modal</dees-button> grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 16px;
}
`}
</style>
<div class="demo-container">
<div class="demo-section">
<h3>Header Buttons</h3>
<p>Modals can have optional header buttons for help and closing.</p>
<div class="button-grid">
<dees-button @click=${() => {
DeesModal.createAndShow({
heading: 'With Help Button',
showHelpButton: true,
onHelp: async () => {
const helpModal = await DeesModal.createAndShow({
heading: 'Help',
width: 'small',
showCloseButton: true,
showHelpButton: false,
content: html`
<p>This is the help content for the modal.</p>
<p>You can provide context-specific help here.</p>
`,
menuOptions: [{
name: 'Got it',
action: async (modal) => modal.destroy()
}],
});
},
content: html`
<p>This modal has a help button in the header. Click it to see help content.</p>
<p>The close button is also visible by default.</p>
`,
menuOptions: [{
name: 'OK',
action: async (modal) => modal.destroy()
}],
});
}}>With Help Button</dees-button>
<dees-button @click=${() => {
DeesModal.createAndShow({
heading: 'No Close Button',
showCloseButton: false,
content: html`
<p>This modal has no close button in the header.</p>
<p>You must use the action buttons or click outside to close it.</p>
`,
menuOptions: [{
name: 'Close',
action: async (modal) => modal.destroy()
}],
});
}}>No Close Button</dees-button>
<dees-button @click=${() => {
DeesModal.createAndShow({
heading: 'Both Buttons',
showHelpButton: true,
showCloseButton: true,
onHelp: () => alert('Help clicked!'),
content: html`
<p>This modal has both help and close buttons.</p>
`,
menuOptions: [{
name: 'Done',
action: async (modal) => modal.destroy()
}],
});
}}>Both Buttons</dees-button>
<dees-button @click=${() => {
DeesModal.createAndShow({
heading: 'Clean Header',
showCloseButton: false,
showHelpButton: false,
content: html`
<p>This modal has a clean header with no buttons.</p>
`,
menuOptions: [{
name: 'Close',
action: async (modal) => modal.destroy()
}],
});
}}>Clean Header</dees-button>
</div>
</div>
<div class="demo-section">
<h3>Modal Width Variations</h3>
<p>Modals can have different widths: small, medium, large, fullscreen, or custom pixel values.</p>
<div class="button-grid">
<dees-button @click=${() => {
DeesModal.createAndShow({
heading: 'Small Modal',
width: 'small',
content: html`
<p>This is a small modal with a width of 380px. Perfect for simple confirmations or brief messages.</p>
`,
menuOptions: [{
name: 'Cancel',
action: async (modal) => modal.destroy()
}, {
name: 'OK',
action: async (modal) => modal.destroy()
}],
});
}}>Small Modal</dees-button>
<dees-button @click=${() => {
DeesModal.createAndShow({
heading: 'Medium Modal (Default)',
width: 'medium',
content: html`
<dees-form>
<dees-input-text .label=${'Username'}></dees-input-text>
<dees-input-text .label=${'Email'} .inputType=${'email'}></dees-input-text>
<dees-input-text .label=${'Password'} .inputType=${'password'}></dees-input-text>
</dees-form>
`,
menuOptions: [{
name: 'Cancel',
action: async (modal) => modal.destroy()
}, {
name: 'Sign Up',
action: async (modal) => modal.destroy()
}],
});
}}>Medium Modal</dees-button>
<dees-button @click=${() => {
DeesModal.createAndShow({
heading: 'Large Modal',
width: 'large',
content: html`
<h4>Wide Content Area</h4>
<p>This large modal is 800px wide and perfect for displaying more complex content like forms with multiple columns, tables, or detailed information.</p>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 16px; margin-top: 16px;">
<dees-input-text .label=${'First Name'}></dees-input-text>
<dees-input-text .label=${'Last Name'}></dees-input-text>
<dees-input-text .label=${'Company'}></dees-input-text>
<dees-input-text .label=${'Position'}></dees-input-text>
</div>
`,
menuOptions: [{
name: 'Cancel',
action: async (modal) => modal.destroy()
}, {
name: 'Save',
action: async (modal) => modal.destroy()
}],
});
}}>Large Modal</dees-button>
<dees-button @click=${() => {
DeesModal.createAndShow({
heading: 'Fullscreen Editor',
width: 'fullscreen',
showHelpButton: true,
onHelp: async () => {
alert('In a real app, this would show editor documentation');
},
content: html`
<h4>Fullscreen Experience with Header Controls</h4>
<p>This modal takes up almost the entire viewport with a 20px margin on all sides. The header buttons are particularly useful in fullscreen mode.</p>
<p>The content area can be as tall as needed and will scroll if necessary.</p>
<div style="height: 200px; background: ${cssManager.bdTheme('#f0f0f0', '#2a2a2a')}; border-radius: 8px; display: flex; align-items: center; justify-content: center; margin-top: 16px;">
<span style="color: ${cssManager.bdTheme('#999', '#666')}">Large content area</span>
</div>
`,
menuOptions: [{
name: 'Save',
action: async (modal) => modal.destroy()
}, {
name: 'Cancel',
action: async (modal) => modal.destroy()
}],
});
}}>Fullscreen Modal</dees-button>
</div>
</div>
<div class="demo-section">
<h3>Custom Width & Constraints</h3>
<p>You can also set custom pixel widths and min/max constraints.</p>
<div class="button-grid">
<dees-button @click=${() => {
DeesModal.createAndShow({
heading: 'Custom Width (700px)',
width: 700,
content: html`
<p>This modal has a custom width of exactly 700 pixels.</p>
`,
menuOptions: [{
name: 'Close',
action: async (modal) => modal.destroy()
}],
});
}}>Custom 700px</dees-button>
<dees-button @click=${() => {
DeesModal.createAndShow({
heading: 'With Max Width',
width: 'large',
maxWidth: 600,
content: html`
<p>This modal is set to 'large' but constrained by a maxWidth of 600px.</p>
`,
menuOptions: [{
name: 'Got it',
action: async (modal) => modal.destroy()
}],
});
}}>Max Width 600px</dees-button>
<dees-button @click=${() => {
DeesModal.createAndShow({
heading: 'With Min Width',
width: 300,
minWidth: 400,
content: html`
<p>This modal width is set to 300px but has a minWidth of 400px, so it will be 400px wide.</p>
`,
menuOptions: [{
name: 'OK',
action: async (modal) => modal.destroy()
}],
});
}}>Min Width 400px</dees-button>
</div>
</div>
<div class="demo-section">
<h3>Button Variations</h3>
<p>Modals can have different button configurations with proper spacing.</p>
<div class="button-grid">
<dees-button @click=${() => {
DeesModal.createAndShow({
heading: 'Multiple Actions',
content: html`
<p>This modal demonstrates multiple buttons with proper spacing between them.</p>
`,
menuOptions: [{
name: 'Delete',
action: async (modal) => modal.destroy()
}, {
name: 'Cancel',
action: async (modal) => modal.destroy()
}, {
name: 'Save Changes',
action: async (modal) => modal.destroy()
}],
});
}}>Three Buttons</dees-button>
<dees-button @click=${() => {
DeesModal.createAndShow({
heading: 'Single Action',
content: html`
<p>Sometimes you just need one button.</p>
`,
menuOptions: [{
name: 'Acknowledge',
action: async (modal) => modal.destroy()
}],
});
}}>Single Button</dees-button>
<dees-button @click=${() => {
DeesModal.createAndShow({
heading: 'No Actions',
content: html`
<p>This modal has no bottom buttons. Use the X button or click outside to close.</p>
<p style="margin-top: 16px; color: ${cssManager.bdTheme('#666', '#999')};">This is useful for informational modals that don't require user action.</p>
`,
menuOptions: [],
});
}}>No Buttons</dees-button>
<dees-button @click=${() => {
DeesModal.createAndShow({
heading: 'Long Button Labels',
content: html`
<p>Testing button layout with longer labels.</p>
`,
menuOptions: [{
name: 'Discard All Changes',
action: async (modal) => modal.destroy()
}, {
name: 'Save and Continue Editing',
action: async (modal) => modal.destroy()
}],
});
}}>Long Labels</dees-button>
</div>
</div>
<div class="demo-section">
<h3>Responsive Behavior</h3>
<p>All modals automatically become full-width on mobile devices (< 768px viewport width) for better usability.</p>
<dees-button @click=${() => {
DeesModal.createAndShow({
heading: 'Responsive Modal',
width: 'large',
showHelpButton: true,
onHelp: () => console.log('Help requested for responsive modal'),
content: html`
<p>Resize your browser window to see how this modal adapts. On mobile viewports, it will automatically take the full width minus margins.</p>
<p>The header buttons remain accessible at all viewport sizes.</p>
`,
menuOptions: [{
name: 'Close',
action: async (modal) => modal.destroy()
}],
});
}}>Test Responsive</dees-button>
</div>
</div>
` `

View File

@ -1,5 +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, 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 {
@ -18,6 +20,7 @@ import {
import * as domtools from '@design.estate/dees-domtools'; import * as domtools from '@design.estate/dees-domtools';
import { DeesWindowLayer } from './dees-windowlayer.js'; import { DeesWindowLayer } from './dees-windowlayer.js';
import './dees-icon.js';
declare global { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {
@ -34,12 +37,26 @@ export class DeesModal extends DeesElement {
heading: string; heading: string;
content: TemplateResult; content: TemplateResult;
menuOptions: plugins.tsclass.website.IMenuItem<DeesModal>[]; menuOptions: plugins.tsclass.website.IMenuItem<DeesModal>[];
width?: 'small' | 'medium' | 'large' | 'fullscreen' | number;
maxWidth?: number;
minWidth?: number;
showCloseButton?: boolean;
showHelpButton?: boolean;
onHelp?: () => void | Promise<void>;
mobileFullscreen?: boolean;
}) { }) {
const body = document.body; const body = document.body;
const modal = new DeesModal(); const modal = new DeesModal();
modal.heading = optionsArg.heading; modal.heading = optionsArg.heading;
modal.content = optionsArg.content; modal.content = optionsArg.content;
modal.menuOptions = optionsArg.menuOptions; modal.menuOptions = optionsArg.menuOptions;
if (optionsArg.width) modal.width = optionsArg.width;
if (optionsArg.maxWidth) modal.maxWidth = optionsArg.maxWidth;
if (optionsArg.minWidth) modal.minWidth = optionsArg.minWidth;
if (optionsArg.showCloseButton !== undefined) modal.showCloseButton = optionsArg.showCloseButton;
if (optionsArg.showHelpButton !== undefined) modal.showHelpButton = optionsArg.showHelpButton;
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,
}); });
@ -48,6 +65,12 @@ 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;
} }
// INSTANCE // INSTANCE
@ -63,6 +86,30 @@ export class DeesModal extends DeesElement {
@state({}) @state({})
public menuOptions: plugins.tsclass.website.IMenuItem<DeesModal>[] = []; public menuOptions: plugins.tsclass.website.IMenuItem<DeesModal>[] = [];
@property({ type: String })
public width: 'small' | 'medium' | 'large' | 'fullscreen' | number = 'medium';
@property({ type: Number })
public maxWidth: number;
@property({ type: Number })
public minWidth: number;
@property({ type: Boolean })
public showCloseButton: boolean = true;
@property({ type: Boolean })
public showHelpButton: boolean = false;
@property({ attribute: false })
public onHelp: () => void | Promise<void>;
@property({ type: Boolean })
public mobileFullscreen: boolean = false;
@state()
private modalZIndex: number = 1000;
constructor() { constructor() {
super(); super();
} }
@ -71,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;
} }
@ -85,20 +132,69 @@ 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: 2000;
} }
.modal { .modal {
will-change: transform; will-change: transform;
transform: translateY(0px) scale(0.95); transform: translateY(0px) scale(0.95);
opacity: 0; opacity: 0;
width: 480px;
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;
display: flex;
flex-direction: column;
overscroll-behavior: contain;
}
/* Width variations */
.modal.width-small {
width: 380px;
}
.modal.width-medium {
width: 560px;
}
.modal.width-large {
width: 800px;
}
.modal.width-fullscreen {
width: calc(100vw - 40px);
height: calc(100vh - 40px);
max-height: calc(100vh - 40px);
}
@media (max-width: 768px) {
.modal {
width: calc(100vw - 40px) !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 {
@ -112,90 +208,168 @@ export class DeesModal extends DeesElement {
} }
.modal .heading { .modal .heading {
height: 32px; height: 40px;
font-family: 'Geist Sans', sans-serif; min-height: 40px;
line-height: 32px; font-family: ${cssGeistFontFamily};
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 12px;
border-bottom: 1px solid ${cssManager.bdTheme('#e5e7eb', '#27272a')};
position: relative;
flex-shrink: 0;
}
.modal .heading .header-buttons {
display: flex;
align-items: center;
gap: 4px;
position: absolute;
right: 8px;
top: 50%;
transform: translateY(-50%);
}
.modal .heading .header-button {
width: 28px;
height: 28px;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.15s ease;
background: transparent;
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
}
.modal .heading .header-button:hover {
background: ${cssManager.bdTheme('#f4f4f5', '#27272a')};
color: ${cssManager.bdTheme('#09090b', '#fafafa')};
}
.modal .heading .header-button:active {
background: ${cssManager.bdTheme('#e5e7eb', '#3f3f46')};
}
.modal .heading .header-button dees-icon {
width: 16px;
height: 16px;
display: block;
}
.modal .heading .heading-text {
flex: 1;
text-align: center; text-align: center;
font-weight: 600; font-weight: 600;
font-size: 12px; font-size: 14px;
border-bottom: 1px solid ${cssManager.bdTheme('#e0e0e0', '#333')}; line-height: 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;
padding: 8px;
flex-shrink: 0;
} }
.modal .bottomButtons .bottomButton { .modal .bottomButtons .bottomButton {
margin: 8px 0px; padding: 8px 16px;
padding: 8px 12px;
border-radius: 4px; border-radius: 4px;
line-height: 16px; line-height: 16px;
text-align: center; text-align: center;
font-size: 14px; font-size: 14px;
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')};
.modal .bottomButtons .bottomButton:first-child { white-space: nowrap;
margin-left: 8px;
}
.modal .bottomButtons .bottomButton:last-child {
margin-right: 8px;
} }
.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')};
} }
`, `,
]; ];
public render(): TemplateResult { public render(): TemplateResult {
const widthClass = typeof this.width === 'string' ? `width-${this.width}` : '';
const customWidth = typeof this.width === 'number' ? `${this.width}px` : '';
const maxWidthStyle = this.maxWidth ? `${this.maxWidth}px` : '';
const minWidthStyle = this.minWidth ? `${this.minWidth}px` : '';
const mobileFullscreenClass = this.mobileFullscreen ? 'mobile-fullscreen' : '';
return html` return html`
<style> <style>
.modal .bottomButtons { ${customWidth ? `.modal { width: ${customWidth}; }` : ''}
grid-template-columns: ${cssManager.cssGridColumns(this.menuOptions.length, 0)}; ${maxWidthStyle ? `.modal { max-width: ${maxWidthStyle}; }` : ''}
} ${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"> <div class="modal ${widthClass} ${mobileFullscreenClass}">
<div class="heading">${this.heading}</div> <div class="heading">
<div class="content">${this.content}</div> <div class="heading-text">${this.heading}</div>
<div class="bottomButtons"> <div class="header-buttons">
${this.menuOptions.map( ${this.showHelpButton ? html`
(actionArg, index) => html` <div class="header-button" @click=${this.handleHelp} title="Help">
<div class="bottomButton ${index === this.menuOptions.length - 1 ? 'primary' : ''} ${actionArg.name === 'OK' ? 'ok' : ''}" @click=${() => { <dees-icon .icon=${'lucide:helpCircle'}></dees-icon>
actionArg.action(this); </div>
}}>${actionArg.name}</div> ` : ''}
` ${this.showCloseButton ? html`
)} <div class="header-button" @click=${() => this.destroy()} title="Close">
<dees-icon .icon=${'lucide:x'}></dees-icon>
</div>
` : ''}
</div>
</div> </div>
<div class="content">${this.content}</div>
${this.menuOptions.length > 0 ? html`
<div class="bottomButtons">
${this.menuOptions.map(
(actionArg, index) => html`
<div class="bottomButton ${index === this.menuOptions.length - 1 ? 'primary' : ''} ${actionArg.name === 'OK' ? 'ok' : ''}" @click=${() => {
actionArg.action(this);
}}>${actionArg.name}</div>
`
)}
</div>
` : ''}
</div> </div>
</div> </div>
`; `;
@ -225,5 +399,14 @@ 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() {
if (this.onHelp) {
await this.onHelp();
}
} }
} }

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

@ -4,7 +4,7 @@ import './dees-form.js';
import './dees-input-text.js'; import './dees-input-text.js';
import './dees-input-checkbox.js'; import './dees-input-checkbox.js';
import './dees-input-dropdown.js'; import './dees-input-dropdown.js';
import './dees-input-radio.js'; import './dees-input-radiogroup.js';
import './dees-form-submit.js'; import './dees-form-submit.js';
import './dees-statsgrid.js'; import './dees-statsgrid.js';
import type { IStatsTile } from './dees-statsgrid.js'; import type { IStatsTile } from './dees-statsgrid.js';
@ -230,13 +230,12 @@ class DemoViewSettings extends DeesElement {
<div class="settings-section"> <div class="settings-section">
<h2>Notification Settings</h2> <h2>Notification Settings</h2>
<dees-form> <dees-form>
<div style="margin-bottom: 16px;"> <dees-input-radiogroup
<div style="font-weight: 500; margin-bottom: 8px;">Email Frequency:</div> .label=${'Email Frequency'}
<dees-input-radio label="Real-time" value="true" key="email-realtime"></dees-input-radio> .options=${['Real-time', 'Daily Digest', 'Weekly Summary', 'Never']}
<dees-input-radio label="Daily Digest" key="email-daily"></dees-input-radio> .selectedOption=${'Real-time'}
<dees-input-radio label="Weekly Summary" key="email-weekly"></dees-input-radio> .key=${'emailFrequency'}
<dees-input-radio label="Never" key="email-never"></dees-input-radio> ></dees-input-radiogroup>
</div>
<dees-input-checkbox key="pushNotifications" label="Enable Push Notifications" value="true"></dees-input-checkbox> <dees-input-checkbox key="pushNotifications" label="Enable Push Notifications" value="true"></dees-input-checkbox>
<dees-input-checkbox key="soundAlerts" label="Play Sound for Alerts" value="true"></dees-input-checkbox> <dees-input-checkbox key="soundAlerts" label="Play Sound for Alerts" value="true"></dees-input-checkbox>
<dees-form-submit>Update Notifications</dees-form-submit> <dees-form-submit>Update Notifications</dees-form-submit>

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

@ -1,7 +1,9 @@
import { customElement, DeesElement, type TemplateResult, html, css, property, cssManager } from '@design.estate/dees-element'; import { customElement, DeesElement, type TemplateResult, html, css, property, cssManager } from '@design.estate/dees-element';
import * as domtools from '@design.estate/dees-domtools'; import * as domtools from '@design.estate/dees-domtools';
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 {
@ -32,7 +34,7 @@ export class DeesToast extends DeesElement {
container.className = `toast-container toast-container-${position}`; container.className = `toast-container toast-container-${position}`;
container.style.cssText = ` container.style.cssText = `
position: fixed; position: fixed;
z-index: 10000; z-index: ${zIndexLayers.overlay.toast};
pointer-events: none; pointer-events: none;
padding: 16px; padding: 16px;
display: flex; display: flex;
@ -105,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 });
@ -146,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,4 +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, zIndexRegistry } from './00zindex.js';
declare global { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {
@ -32,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({
@ -62,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: 200; z-index: ${this.backdropZIndex};
} }
.slotContent { .slotContent {
position: fixed; position: fixed;
@ -71,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: 201; z-index: ${this.contentZIndex};
pointer-events: none;
}
.slotContent > * {
pointer-events: auto;
} }
.visible { .visible {
@ -80,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>
`; `;
@ -101,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;
} }
@ -118,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

@ -1,3 +1,4 @@
export * from './00zindex.js';
export * from './dees-appui-activitylog.js'; export * from './dees-appui-activitylog.js';
export * from './dees-appui-appbar.js'; export * from './dees-appui-appbar.js';
export * from './dees-appui-base.js'; export * from './dees-appui-base.js';
@ -17,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';
@ -26,15 +28,19 @@ 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';
export * from './dees-progressbar.js'; export * from './dees-progressbar.js';
export * from './dees-input-quantityselector.js'; export * from './dees-input-quantityselector.js';
export * from './dees-input-radio.js'; export * from './dees-input-radiogroup.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';
@ -43,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

@ -0,0 +1,65 @@
# WYSIWYG Block Cleanup Status
## Overview
This document tracks the cleanup of `dees-wysiwyg-block.ts` after migrating all block types to the new block handler architecture.
## Completed ✅
All cleanup tasks have been successfully completed on 2025-06-26.
## Cleanup Tasks
### 1. ✅ Remove Block-Specific Styles (lines 101-219)
- [x] Remove `.block.heading-1/2/3` styles → Now in `heading.block.ts`
- [x] Remove `.block.quote` styles → Now in `quote.block.ts`
- [x] Remove `.block.list` styles → Now in `list.block.ts`
- [x] Remove `.block.paragraph` styles → Now in `paragraph.block.ts`
### 2. ✅ Remove Code Block Specific Logic
- [x] Remove code block rendering in `renderBlockContent()` (lines 508-521)
- [x] Remove all `type === 'code'` conditional branches
- [x] Simplify element selection to not special-case code blocks
### 3. ✅ Remove List Block Specific Logic
- [x] Remove `focusListItem()` method (lines 814-821)
- [x] Remove list-specific handling in `getContent()` (lines 732-734)
- [x] Remove list-specific handling in `setContent()` (lines 764-765)
- [x] Remove list content rendering in `firstUpdated()` (line 479)
### 4. ✅ Remove getPlaceholder() Method
- [x] Remove entire method (lines 538-553)
- [x] Update renderBlockContent() to not use placeholders
### 5. ✅ Clean Up Excessive Empty Lines
- [x] Remove consecutive blank lines throughout the file
### 6. ✅ Centralize nonEditableTypes
- [x] Create a single source of truth for non-editable block types
- [x] Remove duplicate arrays
### 7. ✅ Simplify Handler Delegation
- [x] Keep handler delegation pattern but ensure consistency
### 8. ✅ Remove Unused Properties (if confirmed unused)
- [x] Keep `contentInitialized` - still used for tracking
- [x] Keep `blockElement` - used for caching
- [x] Keep cursor tracking properties - used for selection
## Implementation Notes
### Block Types Now Fully Handled by Handlers:
1. **Text blocks**: paragraph, heading-1/2/3, quote, code, list
2. **Media blocks**: image, youtube, attachment
3. **Content blocks**: divider, markdown, html
### Remaining Responsibilities of dees-wysiwyg-block.ts:
1. Shadow DOM container management
2. Handler delegation for all operations
3. Generic block wrapper styles
4. Selection/cursor tracking
5. Event listener setup (until fully delegated to handlers)
## Future Improvements
- Consider moving all event handling to block handlers
- Simplify the handler delegation pattern
- Move generic block styles to a shared location
- Consider removing the need for special-casing any block types

View File

@ -23,14 +23,14 @@ This document tracks the progress of migrating all WYSIWYG blocks to the new blo
- All three heading levels (h1, h2, h3) using unified handler - All three heading levels (h1, h2, h3) using unified handler
- See `phase4-summary.md` for details - See `phase4-summary.md` for details
### 🔄 Phase 5: Other Text Blocks (In Progress) ### Phase 5: Other Text Blocks
- [ ] Quote block - [x] Quote block - Completed with custom styling
- [ ] Code block - [x] Code block - Completed with syntax highlighting, line numbers, and copy button
- [ ] List block - [x] List block - Completed with bullet and numbered list support
### 📋 Phase 6: Media Blocks (Planned) ### 🔄 Phase 6: Media Blocks (In Progress)
- [ ] Image block - [x] Image block - Completed with click upload, drag-drop, and base64 encoding
- [ ] YouTube block - [x] YouTube block - Completed with URL parsing and video embedding
- [ ] Attachment block - [ ] Attachment block
### 📋 Phase 7: Content Blocks (Planned) ### 📋 Phase 7: Content Blocks (Planned)
@ -46,14 +46,14 @@ This document tracks the progress of migrating all WYSIWYG blocks to the new blo
| heading-1 | ✅ | ✅ | ✅ | Using HeadingBlockHandler | | heading-1 | ✅ | ✅ | ✅ | Using HeadingBlockHandler |
| heading-2 | ✅ | ✅ | ✅ | Using HeadingBlockHandler | | heading-2 | ✅ | ✅ | ✅ | Using HeadingBlockHandler |
| heading-3 | ✅ | ✅ | ✅ | Using HeadingBlockHandler | | heading-3 | ✅ | ✅ | ✅ | Using HeadingBlockHandler |
| quote | | | | | | quote | | | | Complete with custom styling |
| code | | | | | | code | | | | Complete with highlighting, line numbers, copy |
| list | | | | | | list | | | | Complete with bullet/numbered support |
| image | | | | | | image | | | | Complete with upload, drag-drop support |
| youtube | | | | | | youtube | | | | Complete with URL parsing, video embedding |
| markdown | ❌ | ❌ | ❌ | | | attachment | ❌ | ❌ | ❌ | Phase 6 |
| html | ❌ | ❌ | ❌ | | | markdown | ❌ | ❌ | ❌ | Phase 7 |
| attachment | ❌ | ❌ | ❌ | | | html | ❌ | ❌ | ❌ | Phase 7 |
## Files Modified During Migration ## Files Modified During Migration
@ -68,11 +68,20 @@ This document tracks the progress of migrating all WYSIWYG blocks to the new blo
- `blocks/content/divider.block.ts` - `blocks/content/divider.block.ts`
- `blocks/text/paragraph.block.ts` - `blocks/text/paragraph.block.ts`
- `blocks/text/heading.block.ts` - `blocks/text/heading.block.ts`
- `blocks/text/quote.block.ts`
- `blocks/text/code.block.ts`
- `blocks/text/list.block.ts`
- `blocks/media/image.block.ts`
- `blocks/media/youtube.block.ts`
### Main Component Updates ### Main Component Updates
- `dees-wysiwyg-block.ts` - Updated to use registry pattern - `dees-wysiwyg-block.ts` - Updated to use registry pattern
## Next Steps ## Next Steps
1. Continue with quote block migration 1. Begin Phase 6: Media blocks migration
2. Follow established patterns from paragraph/heading handlers - Start with image block (most common media type)
3. Test thoroughly after each migration - Implement YouTube block for video embedding
- Create attachment block for file uploads
2. Follow established patterns from existing handlers
3. Test thoroughly after each migration
4. Update documentation as blocks are completed

View File

@ -1,4 +1,8 @@
import type { IBlock } from '../wysiwyg.types.js'; import type { IBlock } from '../wysiwyg.types.js';
import type { IBlockEventHandlers } from '../wysiwyg.interfaces.js';
// Re-export types from the interfaces
export type { IBlockEventHandlers } from '../wysiwyg.interfaces.js';
export interface IBlockContext { export interface IBlockContext {
shadowRoot: ShadowRoot; shadowRoot: ShadowRoot;
@ -23,15 +27,6 @@ export interface IBlockHandler {
getSplitContent?(element: HTMLElement, context?: IBlockContext): { before: string; after: string } | null; getSplitContent?(element: HTMLElement, context?: IBlockContext): { before: string; after: string } | null;
} }
export interface IBlockEventHandlers {
onInput: (e: InputEvent) => void;
onKeyDown: (e: KeyboardEvent) => void;
onFocus: () => void;
onBlur: () => void;
onCompositionStart: () => void;
onCompositionEnd: () => void;
onMouseUp?: (e: MouseEvent) => void;
}
export abstract class BaseBlockHandler implements IBlockHandler { export abstract class BaseBlockHandler implements IBlockHandler {
abstract type: string; abstract type: string;

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