Compare commits

..

111 Commits

Author SHA1 Message Date
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
113c013ea9 1.9.2
Some checks failed
Default (tags) / security (push) Failing after 26s
Default (tags) / test (push) Failing after 14s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-06-24 23:57:32 +00:00
0571d5bf4b fi(wysiwyg): fix navigation 2025-06-24 23:56:40 +00:00
5f86fdba72 update 2025-06-24 23:46:52 +00:00
474385a939 update 2025-06-24 23:15:56 +00:00
71d64fccb8 1.9.1
Some checks failed
Default (tags) / security (push) Failing after 28s
Default (tags) / test (push) Failing after 15s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-06-24 22:46:24 +00:00
e9541da8ff refactor 2025-06-24 22:45:50 +00:00
68b4e9ec8e feat(wysiwyg): Add more block types 2025-06-24 20:32:03 +00:00
856d354b5a update 2025-06-24 18:52:48 +00:00
89a4a15e78 update 2025-06-24 18:43:51 +00:00
fca3638f7f implement image upload 2025-06-24 17:16:13 +00:00
90fc8bed35 update selection reversal 2025-06-24 17:09:19 +00:00
bd223f77d0 selection manipulation 2025-06-24 16:53:54 +00:00
1041814823 update 2025-06-24 16:49:40 +00:00
366544befc fix(wysiwyg): Improve text selection detection with block-level approach
- Remove global selectionchange listener in favor of block-level detection
- Add comprehensive debugging logs to track selection detection
- Add multiple event listeners (mouseup, keyup, selectstart) for better coverage
- Add debounced selection checking to avoid race conditions
- Add click-outside handler to hide formatting menu
- Simplify selection detection logic by removing complex shadow DOM traversal
2025-06-24 16:31:00 +00:00
3b93bd63a7 fix(wysiwyg): Fix text selection detection for formatting menu in Shadow DOM
- Update selection detection to properly handle Shadow DOM boundaries
- Use getComposedRanges API correctly according to MDN documentation
- Add direct selection detection within block components
- Dispatch custom events from blocks when text is selected
- Fix formatting menu positioning using selection rect from events
2025-06-24 16:17:00 +00:00
ca525ce7e3 feat(wysiwyg): implement backspace 2025-06-24 15:52:28 +00:00
83f153f654 update 2025-06-24 15:17:37 +00:00
75637c7793 fix(wysiwyg): fix demo 2025-06-24 15:15:46 +00:00
e0a125c9bd fix(wysiwyg): cursor position 2025-06-24 13:53:47 +00:00
4b2178cedd fix(wysiwyg):Improve Wysiwyg editor 2025-06-24 13:41:12 +00:00
08a4c361fa fix(wysiwyg):Improve Wysiwyg editor 2025-06-24 12:24:02 +00:00
35a648d450 refactor(wysiwyg): Clean up code and ensure drag-drop works with programmatic rendering
- Update drag handler to work without requestUpdate calls
- Remove duplicate modal methods (using WysiwygModalManager)
- Clean up unused imports and methods
- Ensure all DOM updates are programmatic
- Add comprehensive test files for all features
- Follow separation of concerns principles
2025-06-24 11:12:56 +00:00
1c76ade150 fix(wysiwyg): Implement programmatic rendering to eliminate focus loss during typing
- Convert parent component to use static rendering with programmatic DOM manipulation
- Remove all reactive state that could trigger re-renders during editing
- Delay content sync to avoid interference with typing (2s auto-save)
- Update all block operations to use manual DOM updates instead of Lit re-renders
- Fix specific issue where typing + arrow keys caused focus loss
- Add comprehensive focus management documentation
2025-06-24 11:06:02 +00:00
8b02c5aea3 fix(dees-modal): theming 2025-06-24 10:45:06 +00:00
c82c407350 fix(dees-moadl): theming 2025-06-24 08:23:24 +00:00
169f74aa2e Improve Wysiwyg editor 2025-06-24 08:19:53 +00:00
e4a042907a Improve Wysiwyg editor 2025-06-24 07:21:09 +00:00
7ce282c500 feat(wysiwyg): Add language selection for code blocks, block settings menu, fix cursor navigation, and update demo to use dees-panel
- Added modal to select programming language when creating code blocks
- Added settings button (3 dots) on each block for configuration
- Fixed cursor not jumping to new block after pressing Enter
- Special handling for code blocks: Enter creates new line, Shift+Enter creates new block
- Display language indicator on code blocks
- Updated demo to use dees-panel instead of demo-section divs
- Added language selection in block settings modal for existing code blocks
2025-06-23 21:28:58 +00:00
302777feff feat(editor): Add wysiwyg editor 2025-06-23 21:18:03 +00:00
cdcd4f79c8 feat(editor): Add wysiwyg editor 2025-06-23 21:15:04 +00:00
f2e6342a61 feat(editor): Add wysiwyg editor 2025-06-23 18:12:24 +00:00
0f02e7d00f feat(editor): Add wysiwyg editor 2025-06-23 18:07:46 +00:00
a1079cbbdd feat(editor): Add wysiwyg editor 2025-06-23 18:02:40 +00:00
58af08cb0d feat(editor): Add wysiwyg editor 2025-06-23 17:52:10 +00:00
6626726029 feat(editor): Add wysiwyg editor 2025-06-23 17:36:39 +00:00
f3afbb2e48 feat(editor): Add wysiwyg editor 2025-06-23 17:14:47 +00:00
fbd52ee9a5 feat(editor): Add wysiwyg editor 2025-06-23 17:09:53 +00:00
3284d91c2a feat(editor): Add wysiwyg editor 2025-06-23 17:05:28 +00:00
22e6b74c4f 1.9.0
Some checks failed
Default (tags) / security (push) Failing after 27s
Default (tags) / test (push) Failing after 12s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-06-22 20:32:59 +00:00
4de835474b feat(form-inputs): Improve form input consistency and auto spacing across inputs and buttons 2025-06-22 20:32:59 +00:00
024d8af40d 1.8.20
Some checks failed
Default (tags) / security (push) Failing after 15s
Default (tags) / test (push) Failing after 21s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-06-20 00:11:30 +00:00
808b74fa17 fix(deps): Update dependency versions: bump @design.estate/dees-domtools from ^2.1.1 to ^2.3.3, @design.estate/dees-element from ^2.0.42 to ^2.0.44, lucide from ^0.515.0 to ^0.518.0, and @git.zone/tsbundle from ^2.0.15 to ^2.4.0 2025-06-20 00:11:30 +00:00
140 changed files with 29474 additions and 3513 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,82 @@
# Changelog # Changelog
## 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)
Improve form input consistency and auto spacing across inputs and buttons
- Add an 'insideForm' property to dees-button for auto-detection and proper margin adjustment in forms.
- Update dees-input-radio to include a 'name' property so that radio buttons in the same group are mutually exclusive.
- Enhance dees-form to group radio inputs properly when collecting form data.
- Revise readme.hints.md and readme.plan.md to document changes and provide guidance for dees-input-radio.
- Update demos for dees-button and dees-form to showcase correct spacing in vertical and horizontal layouts.
## 2025-06-20 - 1.8.20 - fix(deps)
Update dependency versions: bump @design.estate/dees-domtools from ^2.1.1 to ^2.3.3, @design.estate/dees-element from ^2.0.42 to ^2.0.44, lucide from ^0.515.0 to ^0.518.0, and @git.zone/tsbundle from ^2.0.15 to ^2.4.0
- Upgrade @design.estate/dees-domtools from ^2.1.1 to ^2.3.3
- Upgrade @design.estate/dees-element from ^2.0.42 to ^2.0.44
- Upgrade lucide from ^0.515.0 to ^0.518.0
- Upgrade @git.zone/tsbundle from ^2.0.15 to ^2.4.0
## 2025-06-10 - 1.8.1 - fix(dees-statsgrid) ## 2025-06-10 - 1.8.1 - fix(dees-statsgrid)
Adjust stats grid styling for better alignment and improved visualizations in gauge and trend tiles. Adjust stats grid styling for better alignment and improved visualizations in gauge and trend tiles.

View File

@ -1,23 +1,23 @@
{ {
"name": "@design.estate/dees-catalog", "name": "@design.estate/dees-catalog",
"version": "1.8.19", "version": "1.10.9",
"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", "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"
}, },
"author": "Lossless GmbH", "author": "Lossless GmbH",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@design.estate/dees-domtools": "^2.1.1", "@design.estate/dees-domtools": "^2.3.3",
"@design.estate/dees-element": "^2.0.42", "@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.515.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.0.15", "@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"

1518
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.
@ -59,3 +59,549 @@
- Action grouping - Action grouping
- Navigation options - Navigation options
- Filter controls - Filter controls
## Form Components
### dees-input-radio
- Radio button component with proper group behavior
- Properties:
- `name`: Group name for mutually exclusive selection
- `key`: Unique identifier for the radio option
- `value`: Boolean indicating selection state
- `label`: Display label
- Features:
- Automatic group management (radios with same name are mutually exclusive)
- Cannot be deselected by clicking (proper radio behavior)
- Form integration: Radio groups are collected by name, value is the selected radio's key
- Works both inside and outside forms
- Supports disabled state
- Fixed: Radio buttons now properly deselect others in the group on first click
- Note: When using in forms, set both `name` (for grouping) and `key` (for the value)
## WYSIWYG Editor Architecture
### Recent Refactoring (2025-06-24)
The WYSIWYG editor has been refactored to improve maintainability and separation of concerns:
#### New Handler Classes
1. **WysiwygBlockOperations** (`wysiwyg.blockoperations.ts`)
- Manages all block-related operations
- Methods: createBlock, insertBlockAfter, removeBlock, findBlock, focusBlock, etc.
- Centralized block manipulation logic
2. **WysiwygInputHandler** (`wysiwyg.inputhandler.ts`)
- Handles all input events for blocks
- Manages block content updates based on type
- Detects block type transformations
- Handles slash commands
- Manages auto-save with debouncing
3. **WysiwygKeyboardHandler** (`wysiwyg.keyboardhandler.ts`)
- Handles all keyboard events
- Manages formatting shortcuts (Cmd/Ctrl + B/I/U/K)
- Handles special keys: Tab, Enter, Backspace
- Manages slash menu navigation
4. **WysiwygDragDropHandler** (`wysiwyg.dragdrophandler.ts`)
- Manages drag and drop operations
- Tracks drag state
- Handles visual feedback during drag
- Manages block reordering
5. **WysiwygModalManager** (`wysiwyg.modalmanager.ts`)
- Static methods for showing modals
- Language selection for code blocks
- Block settings modal
- Reusable modal patterns
#### Main Component Updates
The main `DeesInputWysiwyg` component now:
- Instantiates handler classes in `connectedCallback`
- Delegates complex operations to appropriate handlers
- Maintains cleaner, more focused code
- Better separation of concerns
#### Benefits
- Reduced main component size from 1100+ lines
- Each handler class is focused on a single responsibility
- Easier to test individual components
- Better code organization
- Improved maintainability
#### Fixed Issues
- Enter key no longer duplicates content in new blocks
- Removed problematic `setBlockContents()` method
- Content is now managed directly through DOM properties
- Better timing for block creation and focus
- Slash menu no longer disappears immediately on first "/" press
- Focus is properly maintained when slash menu opens
- Removed duplicate event handling methods from main component
- Simplified focus management throughout the editor
#### Additional Refactoring (2025-06-24 - Part 2)
- **Removed duplicate code**: handleBlockInput and handleBlockKeyDown methods removed from main component
- **Simplified focus management**: Removed complex lifecycle methods and timers
- **Fixed slash menu behavior**: Changed to click events and proper event prevention
- **dees-wysiwyg-block component**: Now uses static HTML rendering for better content control
- **Improved formatting preservation**: HTML formatting (bold, italic, etc.) properly preserved in all block types
#### Notes
- All input handling now goes through WysiwygInputHandler
- All keyboard handling goes through WysiwygKeyboardHandler
- The slash menu uses click events instead of mousedown for better UX
- Focus is maintained using requestAnimationFrame for better timing
- The refactoring maintains all existing functionality with improved reliability
### Global Menu Architecture (2025-06-24 - Part 3)
The slash menu and formatting menu have been refactored to render globally instead of inside the wysiwyg component. This fixes focus loss issues that were occurring when the menus were re-rendered with the component.
#### Key Components:
1. **DeesSlashMenu** (`dees-slash-menu.ts`)
- Singleton component that renders globally in the document body
- Accessed via `DeesSlashMenu.getInstance()`
- Manages its own visibility, position, and filtering
- Emits callbacks when items are selected
2. **DeesFormattingMenu** (`dees-formatting-menu.ts`)
- Singleton component that renders globally in the document body
- Accessed via `DeesFormattingMenu.getInstance()`
- Shows when text is selected
- Applies formatting commands via callback
3. **Integration in DeesInputWysiwyg**
- Stores singleton instances: `private slashMenu = DeesSlashMenu.getInstance()`
- Shows menus with absolute positioning
- Menus handle their own rendering and state management
#### Benefits:
- No focus loss when menus appear/disappear
- Better performance (menus don't re-render with component)
- Cleaner separation of concerns
- Menus persist across component updates
#### Usage:
```typescript
// Show slash menu
this.slashMenu.show(
{ x: cursorX, y: cursorY },
(type: string) => this.insertBlock(type)
);
// Show formatting menu
this.formattingMenu.show(
{ x: selectionX, y: selectionY },
(command: string) => this.applyFormat(command)
);
```
#### Previous Issues Fixed:
- Slash menu was disappearing immediately on first "/" press
- Focus was lost when menus appeared
- Text selection was not working properly
- Cursor position was lost after menu interactions
### Arrow Key Navigation (2025-06-24 - Part 4)
Enhanced arrow key handling for seamless navigation between blocks:
#### Features:
1. **ArrowUp at block start**: Automatically navigates to the end of the previous block
2. **ArrowDown at block end**: Automatically navigates to the beginning of the next block
3. **Smart detection**: Checks actual cursor position within the block content
4. **Slash menu integration**: When slash menu is open, arrow keys navigate menu items instead
5. **No focus loss**: Navigation maintains focus throughout
#### Implementation:
- Added `handleArrowUp()` and `handleArrowDown()` methods to `WysiwygKeyboardHandler`
- Smart cursor position detection for different block types (text, lists, etc.)
- Helper method `getLastTextNode()` for finding the last text position in complex HTML
- Prevents default behavior only when navigating between blocks
- Skips divider blocks during navigation
### Focus Management Improvements (2025-06-24 - Part 5)
Enhanced focus management to prevent focus loss during various operations:
#### Key Improvements:
1. **Formatting Without execCommand**:
- Replaced deprecated `document.execCommand` with modern DOM manipulation
- Proper selection restoration after formatting
- Async formatting operations to maintain focus
2. **Link Dialog**:
- Replaced `prompt()` with custom modal dialog
- Maintains focus context during async operations
- Auto-focuses input field in modal
3. **Robust Focus Methods**:
- Double `requestAnimationFrame` for DOM update timing
- Fallback focus attempts with microtasks
- Contenteditable attribute verification
4. **Cursor Positioning**:
- Enhanced `setCursorToStart/End` with edge case handling
- Zero-width space insertion for empty elements
- Recursive node traversal for complex HTML structures
5. **Async Keyboard Shortcuts**:
- Formatting shortcuts use Promise resolution
- Prevents focus loss during rapid keyboard input
#### Implementation Details:
- `focusWithCursor()` method now handles empty blocks and complex HTML
- `applyFormat()` is async and properly restores selection
- Link creation no longer uses blocking `prompt()` dialog
- All focus operations use proper timing with RAF and microtasks
### Focus Loss Prevention for Menus (2025-06-24 - Part 6)
Fixed focus loss issues when slash menu and formatting menu appear:
#### Key Fixes:
1. **Timeout Reduction**:
- Replaced 50ms setTimeout with requestAnimationFrame
- Immediate focus attempt before falling back to RAF
- Reduced delay when inserting blocks
2. **Menu Focus Prevention**:
- Added `tabindex="-1"` to prevent menus from taking focus
- Added focus event prevention on menus
- Menus now use mousedown prevention consistently
3. **Blur Event Handling**:
- Skip value updates when slash menu is visible
- Prevent auto-save during slash menu interaction
- Maintain focus after menu appears with RAF
4. **Block Focus Optimization**:
- Try immediate focus if block element exists
- Fall back to RAF only when necessary
- Consistent focus handling across all block types
#### Implementation:
- `handleBlockBlur()` checks if slash menu is visible before updating
- `scheduleAutoSave()` skips saving when slash menu is open
- Slash menu show adds RAF to restore focus if lost
- Reduced timing delays throughout the focus chain
### Slash Command Cleanup (2025-06-24 - Part 7)
Fixed the issue where "/" remained in the editor after selecting a block type:
#### The Fix:
1. **In `insertBlock()`**:
- Clear slash command before transforming block type
- Use regex `/^\/[^\s]*\s*/` to match slash + filter text
- Trim the result to ensure clean content
- Set content to empty for transformed blocks
2. **Improved Content Handling**:
- Wait for `updateComplete` before focusing
- Ensure lists start with empty content
- Consistent cleanup in both `insertBlock` and `closeSlashMenu`
3. **Edge Cases**:
- Handle filtered commands (e.g., "/hea" for heading)
- Clear content even with partial matches
- Proper content reset for all block types
Now when selecting a block type from the slash menu, the "/" and any filter text is properly removed before the block transformation occurs.
### Enhanced Enter Key and Block Settings (2025-06-24 - Part 8)
Added two major improvements to the wysiwyg editor:
#### 1. Smart Enter Key Behavior:
When pressing Enter, content after the cursor is now moved to the next block:
- **Content Splitting**: Uses Range API to extract content after cursor
- **HTML Preservation**: Maintains formatting when splitting blocks
- **Clean Split**: Current block keeps content before cursor, new block gets content after
- **Empty Block**: If cursor is at end, creates empty new block
Implementation in `WysiwygKeyboardHandler.handleEnter()`:
```typescript
// Clone the range to extract content after cursor
const afterRange = range.cloneRange();
afterRange.selectNodeContents(target);
afterRange.setStart(range.endContainer, range.endOffset);
// Extract content after cursor
const afterContent = afterRange.extractContents();
```
#### 2. Block Type Changing via Settings Menu:
The block settings menu (three dots) now includes block type selection:
- **Type Selector Grid**: Shows all available block types with icons
- **Smart Metadata Handling**:
- Clears code language when changing from code block
- Clears list type when changing from list
- Prompts for language when changing to code block
- **Visual Feedback**: Currently selected type is highlighted
- **Instant Update**: Block transforms immediately on selection
Features:
- Works for all block types (not just code blocks)
- Preserves content during type transformation
- Handles special cases like code block language selection
- Modal closes automatically after selection
### Complete WYSIWYG Refactoring (2025-06-24 - Part 9)
Major architectural improvements to fix Enter key behavior and left arrow focus loss:
#### 1. Async Operation Architecture:
- All focus operations are now async with proper Promise handling
- `insertBlockAfter()` waits for component updates before focusing
- `focusBlock()` ensures DOM is ready with `updateComplete`
- Eliminated arbitrary timeouts in favor of proper async/await
#### 2. Enter Key Split Content Fix:
- Added `getSplitContent()` method to block component
- Properly extracts content before/after cursor using Range API
- Updates current block and creates new block atomically
- Content after cursor correctly moves to new block
```typescript
// In block component
public getSplitContent(): { before: string; after: string } | null {
const beforeRange = range.cloneRange();
beforeRange.selectNodeContents(this.blockElement);
beforeRange.setEnd(range.startContainer, range.startOffset);
// ... extract and return split content
}
```
#### 3. Arrow Key Navigation:
- Added ArrowLeft/ArrowRight handlers for block boundaries
- Prevents focus loss when navigating between blocks
- Only intercepts at block boundaries, normal navigation otherwise
- All arrow key operations are async for proper timing
#### 4. Interface Architecture:
Created `wysiwyg.interfaces.ts` with proper typing:
- `IWysiwygComponent` - Main component contract
- `IBlockOperations` - Block operation methods
- `IWysiwygBlockComponent` - Block component interface
- `IBlockEventHandlers` - Event handler signatures
#### 5. Focus Management Improvements:
- Eliminated double RAF in favor of single async flow
- Focus operations wait for DOM updates via `updateComplete`
- Proper cursor positioning after all operations
- No more focus loss during navigation
#### Key Changes:
1. Keyboard handler methods are now async
2. Block operations return Promises
3. Enter key properly splits content at cursor
4. Arrow keys handle block navigation without focus loss
5. All timing is handled via proper async/await patterns
The refactoring eliminates race conditions and timing issues that were causing focus loss and content duplication problems.
### Programmatic Rendering Solution (2025-06-24 - Part 10)
Fixed persistent focus loss issue by implementing fully programmatic rendering:
#### The Problem:
- User would click in a block, type text, then press arrow keys and lose focus
- Root cause: Lit was re-rendering components when block content was mutated
- Even with shouldUpdate() preventing re-renders, parent re-evaluation caused focus loss
#### The Solution:
1. **Static Parent Rendering**:
- Parent component renders only once with empty editor content div
- All blocks are created and managed programmatically via DOM manipulation
- No Lit re-renders triggered by state changes
2. **Manual Block Management**:
- `renderBlocksProgrammatically()` creates all block elements manually
- `createBlockElement()` builds block wrapper with all event handlers
- `updateBlockElement()` replaces individual blocks when needed
- No reactive properties trigger parent re-renders
3. **Content Update Strategy**:
- During typing, content is NOT immediately synced to data model
- Auto-save delayed to 2 seconds to avoid interference
- Content synced from DOM only on blur or before save
- `syncAllBlockContent()` reads from DOM when needed
4. **Focus Preservation**:
- Block components prevent re-renders with `shouldUpdate()`
- Parent never re-renders after initial load
- Focus remains stable during all editing operations
- Arrow key navigation works without focus loss
5. **Implementation Details**:
```typescript
// Parent render method - static after first render
render(): TemplateResult {
return html`
<div class="editor-content" id="editor-content">
<!-- Blocks rendered programmatically -->
</div>
`;
}
// All block operations use DOM manipulation
private renderBlocksProgrammatically() {
this.editorContentRef.innerHTML = '';
this.blocks.forEach(block => {
const blockWrapper = this.createBlockElement(block);
this.editorContentRef.appendChild(blockWrapper);
});
}
```
This approach completely eliminates focus loss by taking full control of the DOM and preventing any framework-induced re-renders during editing.
### Code Refactoring and Cleanup (2025-06-24 - Part 11)
Completed comprehensive refactoring to ensure clean, maintainable code with separated concerns:
#### Refactoring Changes:
1. **Drag and Drop Handler Cleanup**:
- Removed all `requestUpdate()` calls from drag handler
- Handler now only updates internal state
- Parent component handles DOM updates programmatically
- Simplified drag state management
2. **Unused Code Removal**:
- Removed duplicate `showBlockSettingsModal` method (using WysiwygModalManager)
- Removed duplicate `showLanguageSelectionModal` method
- Removed unused `renderBlock` method
- Cleaned up unused imports (WysiwygBlocks, ISlashMenuItem)
3. **Import Cleanup**:
- Removed unused type imports
- Organized imports logically
- Kept only necessary dependencies
4. **Separated Concerns**:
- Modal management in WysiwygModalManager
- Block operations in WysiwygBlockOperations
- Input handling in WysiwygInputHandler
- Keyboard handling in WysiwygKeyboardHandler
- Drag/drop in WysiwygDragDropHandler
- Each class has a single responsibility
5. **Programmatic DOM Management**:
- All DOM updates happen through explicit methods
- No reactive re-renders during user interaction
- Manual class management for drag states
- Direct DOM manipulation for performance
6. **Test Files Created**:
- `test-focus-fix.html` - Verifies focus management
- `test-drag-drop.html` - Tests drag and drop functionality
- `test-comprehensive.html` - Tests all features together
The refactoring follows the principles in instructions.md:
- Uses static templates with manual DOM operations
- Maintains separated concerns in different classes
- 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.

479
readme.md
View File

@ -12,14 +12,15 @@ npm install @design.estate/dees-catalog
| Category | Components | | Category | Components |
|----------|------------| |----------|------------|
| Core UI | `DeesButton`, `DeesBadge`, `DeesChips`, `DeesIcon`, `DeesLabel`, `DeesSpinner`, `DeesToast` | | Core UI | `DeesButton`, `DeesButtonExit`, `DeesButtonGroup`, `DeesBadge`, `DeesChips`, `DeesHeading`, `DeesHint`, `DeesIcon`, `DeesLabel`, `DeesPanel`, `DeesSearchbar`, `DeesSpinner`, `DeesToast`, `DeesWindowcontrols` |
| Forms | `DeesForm`, `DeesInputText`, `DeesInputCheckbox`, `DeesInputDropdown`, `DeesInputRadio`, `DeesInputFileupload`, `DeesInputIban`, `DeesInputPhone`, `DeesInputQuantitySelector`, `DeesInputMultitoggle`, `DeesFormSubmit` | | Forms | `DeesForm`, `DeesInputText`, `DeesInputCheckbox`, `DeesInputDropdown`, `DeesInputRadiogroup`, `DeesInputFileupload`, `DeesInputIban`, `DeesInputPhone`, `DeesInputQuantitySelector`, `DeesInputMultitoggle`, `DeesInputTags`, `DeesInputTypelist`, `DeesInputRichtext`, `DeesInputWysiwyg`, `DeesFormSubmit` |
| Layout | `DeesAppuiBase`, `DeesAppuiMainmenu`, `DeesAppuiMainselector`, `DeesAppuiMaincontent`, `DeesAppuiAppbar`, `DeesMobileNavigation` | | Layout | `DeesAppuiBase`, `DeesAppuiMainmenu`, `DeesAppuiMainselector`, `DeesAppuiMaincontent`, `DeesAppuiAppbar`, `DeesAppuiActivitylog`, `DeesAppuiProfiledropdown`, `DeesAppuiTabs`, `DeesAppuiView`, `DeesMobileNavigation` |
| Data Display | `DeesTable`, `DeesDataviewCodebox`, `DeesDataviewStatusobject`, `DeesPdf`, `DeesStatsGrid` | | Data Display | `DeesTable`, `DeesDataviewCodebox`, `DeesDataviewStatusobject`, `DeesPdf`, `DeesStatsGrid`, `DeesPagination` |
| Visualization | `DeesChartArea`, `DeesChartLog` | | Visualization | `DeesChartArea`, `DeesChartLog` |
| Dialogs & Overlays | `DeesModal`, `DeesContextmenu`, `DeesSpeechbubble`, `DeesWindowlayer` | | Dialogs & Overlays | `DeesModal`, `DeesContextmenu`, `DeesSpeechbubble`, `DeesWindowlayer` |
| Navigation | `DeesStepper`, `DeesProgressbar` | | Navigation | `DeesStepper`, `DeesProgressbar`, `DeesMobileNavigation` |
| Development | `DeesEditor`, `DeesEditorMarkdown`, `DeesTerminal`, `DeesUpdater` | | Development | `DeesEditor`, `DeesEditorMarkdown`, `DeesEditorMarkdownoutlet`, `DeesTerminal`, `DeesUpdater` |
| Auth & Utilities | `DeesSimpleAppdash`, `DeesSimpleLogin` |
## Detailed Component Documentation ## Detailed Component Documentation
@ -149,6 +150,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 +295,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 +365,121 @@ 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>
```
#### `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 +529,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 +545,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 +809,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.
@ -982,6 +1254,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 +1599,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 +1615,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 +1631,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 +1694,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 +1859,60 @@ 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
## 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.

Binary file not shown.

View File

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

82
readme.refactoring.md Normal file
View File

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

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

@ -1,6 +1,6 @@
import { tap, expect, webhelpers } from '@push.rocks/tapbundle'; import { tap, expect, webhelpers } from '@git.zone/tstest/tapbundle';
import * as deesCatalog from '../ts_web'; import * as deesCatalog from '../ts_web/index.js';
tap.test('should create a working button', async () => { tap.test('should create a working button', async () => {
const button: deesCatalog.DeesButton = await webhelpers.fixture( const button: deesCatalog.DeesButton = await webhelpers.fixture(
@ -9,4 +9,4 @@ tap.test('should create a working button', async () => {
expect(button).toBeInstanceOf(deesCatalog.DeesButton); expect(button).toBeInstanceOf(deesCatalog.DeesButton);
}); });
tap.start(); export default tap.start();

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,175 @@
import { expect, tap, webhelpers } from '@push.rocks/tapbundle';
import { DeesWysiwygBlock } from '../ts_web/elements/wysiwyg/dees-wysiwyg-block.js';
import { WysiwygSelection } from '../ts_web/elements/wysiwyg/wysiwyg.selection.js';
tap.test('Shadow DOM containment should work correctly', async () => {
console.log('=== Testing Shadow DOM Containment ===');
// Create a WYSIWYG block component
const block = await webhelpers.fixture<DeesWysiwygBlock>(
'<dees-wysiwyg-block></dees-wysiwyg-block>'
);
// Set the block data
block.block = {
id: 'test-1',
type: 'paragraph',
content: 'Hello world test content'
};
block.handlers = {
onInput: () => {},
onKeyDown: () => {},
onFocus: () => {},
onBlur: () => {},
onCompositionStart: () => {},
onCompositionEnd: () => {}
};
await block.updateComplete;
// Get the paragraph element inside Shadow DOM
const container = block.shadowRoot?.querySelector('.wysiwyg-block-container') as HTMLElement;
const paragraphBlock = container?.querySelector('.block.paragraph') as HTMLElement;
expect(paragraphBlock).toBeTruthy();
console.log('Found paragraph block:', paragraphBlock);
console.log('Paragraph text content:', paragraphBlock.textContent);
// Focus the paragraph
paragraphBlock.focus();
// Manually set cursor position
const textNode = paragraphBlock.firstChild;
if (textNode && textNode.nodeType === Node.TEXT_NODE) {
const range = document.createRange();
const selection = window.getSelection();
// Set cursor at position 11 (after "Hello world")
range.setStart(textNode, 11);
range.setEnd(textNode, 11);
selection?.removeAllRanges();
selection?.addRange(range);
console.log('Set cursor at position 11');
// Test the containment check
console.log('\n--- Testing containment ---');
const currentSelection = window.getSelection();
if (currentSelection && currentSelection.rangeCount > 0) {
const selRange = currentSelection.getRangeAt(0);
console.log('Selection range:', {
startContainer: selRange.startContainer,
startOffset: selRange.startOffset,
containerText: selRange.startContainer.textContent
});
// Test regular contains (should fail across Shadow DOM)
const regularContains = paragraphBlock.contains(selRange.startContainer);
console.log('Regular contains:', regularContains);
// Test Shadow DOM-aware contains
const shadowDOMContains = WysiwygSelection.containsAcrossShadowDOM(paragraphBlock, selRange.startContainer);
console.log('Shadow DOM contains:', shadowDOMContains);
// Since we're setting selection within the same shadow DOM, both should be true
expect(regularContains).toBeTrue();
expect(shadowDOMContains).toBeTrue();
}
// Test getSplitContent
console.log('\n--- Testing getSplitContent ---');
const splitResult = block.getSplitContent();
console.log('Split result:', splitResult);
expect(splitResult).toBeTruthy();
if (splitResult) {
console.log('Before:', JSON.stringify(splitResult.before));
console.log('After:', JSON.stringify(splitResult.after));
// Expected split at position 11
expect(splitResult.before).toEqual('Hello world');
expect(splitResult.after).toEqual(' test content');
}
}
});
tap.test('Shadow DOM containment across different shadow roots', async () => {
console.log('=== Testing Cross Shadow Root Containment ===');
// Create parent component with WYSIWYG editor
const parentDiv = document.createElement('div');
parentDiv.innerHTML = `
<dees-input-wysiwyg>
<dees-wysiwyg-block></dees-wysiwyg-block>
</dees-input-wysiwyg>
`;
document.body.appendChild(parentDiv);
// Wait for components to be ready
await new Promise(resolve => setTimeout(resolve, 100));
const wysiwygInput = parentDiv.querySelector('dees-input-wysiwyg') as any;
const blockElement = wysiwygInput?.shadowRoot?.querySelector('dees-wysiwyg-block') as DeesWysiwygBlock;
if (blockElement) {
// Set block data
blockElement.block = {
id: 'test-2',
type: 'paragraph',
content: 'Cross shadow DOM test'
};
blockElement.handlers = {
onInput: () => {},
onKeyDown: () => {},
onFocus: () => {},
onBlur: () => {},
onCompositionStart: () => {},
onCompositionEnd: () => {}
};
await blockElement.updateComplete;
// Get the paragraph inside the nested shadow DOM
const container = blockElement.shadowRoot?.querySelector('.wysiwyg-block-container') as HTMLElement;
const paragraphBlock = container?.querySelector('.block.paragraph') as HTMLElement;
if (paragraphBlock) {
console.log('Found nested paragraph block');
// Focus and set selection
paragraphBlock.focus();
const textNode = paragraphBlock.firstChild;
if (textNode && textNode.nodeType === Node.TEXT_NODE) {
const range = document.createRange();
range.setStart(textNode, 5);
range.setEnd(textNode, 5);
const selection = window.getSelection();
selection?.removeAllRanges();
selection?.addRange(range);
// Test containment from parent's perspective
const selRange = selection?.getRangeAt(0);
if (selRange) {
// This should fail because it crosses shadow DOM boundary
const regularContains = wysiwygInput.contains(selRange.startContainer);
console.log('Parent regular contains:', regularContains);
expect(regularContains).toBeFalse();
// This should work with our Shadow DOM-aware method
const shadowDOMContains = WysiwygSelection.containsAcrossShadowDOM(wysiwygInput, selRange.startContainer);
console.log('Parent shadow DOM contains:', shadowDOMContains);
expect(shadowDOMContains).toBeTrue();
}
}
}
}
// Clean up
document.body.removeChild(parentDiv);
});
export default tap.start();

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,9 @@
import { expect, tap } from '@git.zone/tstest/tapbundle';
import { DeesInputWysiwyg } from '../ts_web/elements/wysiwyg/dees-input-wysiwyg.js';
tap.test('should create wysiwyg editor', async () => {
const editor = new DeesInputWysiwyg();
expect(editor).toBeInstanceOf(DeesInputWysiwyg);
});
export default tap.start();

View File

@ -0,0 +1,69 @@
import { tap, expect, webhelpers } from '@push.rocks/tapbundle';
import * as deesCatalog from '../ts_web/index.js';
import { BlockRegistry } from '../ts_web/elements/wysiwyg/blocks/block.registry.js';
import { DeesWysiwygBlock } from '../ts_web/elements/wysiwyg/dees-wysiwyg-block.js';
// Import block registration to ensure handlers are registered
import '../ts_web/elements/wysiwyg/wysiwyg.blockregistration.js';
tap.test('Debug: should create empty wysiwyg block component', async () => {
try {
console.log('Creating DeesWysiwygBlock...');
const block: DeesWysiwygBlock = await webhelpers.fixture(
webhelpers.html`<dees-wysiwyg-block></dees-wysiwyg-block>`
);
console.log('Block created:', block);
expect(block).toBeDefined();
expect(block).toBeInstanceOf(DeesWysiwygBlock);
console.log('Initial block property:', block.block);
console.log('Initial handlers property:', block.handlers);
} catch (error) {
console.error('Error creating block:', error);
throw error;
}
});
tap.test('Debug: should set properties step by step', async () => {
try {
console.log('Step 1: Creating component...');
const block: DeesWysiwygBlock = document.createElement('dees-wysiwyg-block') as DeesWysiwygBlock;
expect(block).toBeDefined();
console.log('Step 2: Setting handlers...');
block.handlers = {
onInput: () => console.log('onInput'),
onKeyDown: () => console.log('onKeyDown'),
onFocus: () => console.log('onFocus'),
onBlur: () => console.log('onBlur'),
onCompositionStart: () => console.log('onCompositionStart'),
onCompositionEnd: () => console.log('onCompositionEnd')
};
console.log('Handlers set:', block.handlers);
console.log('Step 3: Setting block data...');
block.block = {
id: 'test-block',
type: 'divider',
content: ' '
};
console.log('Block set:', block.block);
console.log('Step 4: Appending to body...');
document.body.appendChild(block);
console.log('Step 5: Waiting for update...');
await block.updateComplete;
console.log('Update complete');
console.log('Step 6: Checking shadowRoot...');
expect(block.shadowRoot).toBeDefined();
console.log('ShadowRoot exists');
} catch (error) {
console.error('Error in step-by-step test:', error);
throw error;
}
});
export default tap.start();

View File

@ -0,0 +1,205 @@
import { tap, expect, webhelpers } from '@push.rocks/tapbundle';
import * as deesCatalog from '../ts_web/index.js';
import { BlockRegistry } from '../ts_web/elements/wysiwyg/blocks/block.registry.js';
import { DeesWysiwygBlock } from '../ts_web/elements/wysiwyg/dees-wysiwyg-block.js';
// Import block registration to ensure handlers are registered
import '../ts_web/elements/wysiwyg/wysiwyg.blockregistration.js';
tap.test('BlockRegistry should have registered handlers', async () => {
// Test divider handler
const dividerHandler = BlockRegistry.getHandler('divider');
expect(dividerHandler).toBeDefined();
expect(dividerHandler?.type).toEqual('divider');
// Test paragraph handler
const paragraphHandler = BlockRegistry.getHandler('paragraph');
expect(paragraphHandler).toBeDefined();
expect(paragraphHandler?.type).toEqual('paragraph');
// Test heading handlers
const heading1Handler = BlockRegistry.getHandler('heading-1');
expect(heading1Handler).toBeDefined();
expect(heading1Handler?.type).toEqual('heading-1');
const heading2Handler = BlockRegistry.getHandler('heading-2');
expect(heading2Handler).toBeDefined();
expect(heading2Handler?.type).toEqual('heading-2');
const heading3Handler = BlockRegistry.getHandler('heading-3');
expect(heading3Handler).toBeDefined();
expect(heading3Handler?.type).toEqual('heading-3');
// Test that getAllTypes returns all registered types
const allTypes = BlockRegistry.getAllTypes();
expect(allTypes).toContain('divider');
expect(allTypes).toContain('paragraph');
expect(allTypes).toContain('heading-1');
expect(allTypes).toContain('heading-2');
expect(allTypes).toContain('heading-3');
});
tap.test('should render divider block using handler', async () => {
const dividerBlock: DeesWysiwygBlock = await webhelpers.fixture(
webhelpers.html`<dees-wysiwyg-block></dees-wysiwyg-block>`
);
// Set required handlers
dividerBlock.handlers = {
onInput: () => {},
onKeyDown: () => {},
onFocus: () => {},
onBlur: () => {},
onCompositionStart: () => {},
onCompositionEnd: () => {}
};
// Set a divider block
dividerBlock.block = {
id: 'test-divider',
type: 'divider',
content: ' '
};
await dividerBlock.updateComplete;
// Check that the divider is rendered
const dividerElement = dividerBlock.shadowRoot?.querySelector('.block.divider');
expect(dividerElement).toBeDefined();
expect(dividerElement?.getAttribute('tabindex')).toEqual('0');
// Check for the divider icon
const icon = dividerBlock.shadowRoot?.querySelector('.divider-icon');
expect(icon).toBeDefined();
});
tap.test('should render paragraph block using handler', async () => {
const paragraphBlock: DeesWysiwygBlock = await webhelpers.fixture(
webhelpers.html`<dees-wysiwyg-block></dees-wysiwyg-block>`
);
// Set required handlers
paragraphBlock.handlers = {
onInput: () => {},
onKeyDown: () => {},
onFocus: () => {},
onBlur: () => {},
onCompositionStart: () => {},
onCompositionEnd: () => {},
onMouseUp: () => {}
};
// Set a paragraph block
paragraphBlock.block = {
id: 'test-paragraph',
type: 'paragraph',
content: 'Test paragraph content'
};
await paragraphBlock.updateComplete;
// Check that the paragraph is rendered
const paragraphElement = paragraphBlock.shadowRoot?.querySelector('.block.paragraph');
expect(paragraphElement).toBeDefined();
expect(paragraphElement?.getAttribute('contenteditable')).toEqual('true');
expect(paragraphElement?.textContent).toEqual('Test paragraph content');
});
tap.test('should render heading blocks using handler', async () => {
// Test heading-1
const heading1Block: DeesWysiwygBlock = await webhelpers.fixture(
webhelpers.html`<dees-wysiwyg-block></dees-wysiwyg-block>`
);
// Set required handlers
heading1Block.handlers = {
onInput: () => {},
onKeyDown: () => {},
onFocus: () => {},
onBlur: () => {},
onCompositionStart: () => {},
onCompositionEnd: () => {},
onMouseUp: () => {}
};
heading1Block.block = {
id: 'test-h1',
type: 'heading-1',
content: 'Heading 1 Test'
};
await heading1Block.updateComplete;
const h1Element = heading1Block.shadowRoot?.querySelector('.block.heading-1');
expect(h1Element).toBeDefined();
expect(h1Element?.textContent).toEqual('Heading 1 Test');
// Test heading-2
const heading2Block: DeesWysiwygBlock = await webhelpers.fixture(
webhelpers.html`<dees-wysiwyg-block></dees-wysiwyg-block>`
);
// Set required handlers
heading2Block.handlers = {
onInput: () => {},
onKeyDown: () => {},
onFocus: () => {},
onBlur: () => {},
onCompositionStart: () => {},
onCompositionEnd: () => {},
onMouseUp: () => {}
};
heading2Block.block = {
id: 'test-h2',
type: 'heading-2',
content: 'Heading 2 Test'
};
await heading2Block.updateComplete;
const h2Element = heading2Block.shadowRoot?.querySelector('.block.heading-2');
expect(h2Element).toBeDefined();
expect(h2Element?.textContent).toEqual('Heading 2 Test');
});
tap.test('paragraph block handler methods should work', async () => {
const paragraphBlock: DeesWysiwygBlock = await webhelpers.fixture(
webhelpers.html`<dees-wysiwyg-block></dees-wysiwyg-block>`
);
// Set required handlers
paragraphBlock.handlers = {
onInput: () => {},
onKeyDown: () => {},
onFocus: () => {},
onBlur: () => {},
onCompositionStart: () => {},
onCompositionEnd: () => {},
onMouseUp: () => {}
};
paragraphBlock.block = {
id: 'test-methods',
type: 'paragraph',
content: 'Initial content'
};
await paragraphBlock.updateComplete;
// Test getContent
const content = paragraphBlock.getContent();
expect(content).toEqual('Initial content');
// Test setContent
paragraphBlock.setContent('Updated content');
await paragraphBlock.updateComplete;
expect(paragraphBlock.getContent()).toEqual('Updated content');
// Test that the DOM is updated
const paragraphElement = paragraphBlock.shadowRoot?.querySelector('.block.paragraph');
expect(paragraphElement?.textContent).toEqual('Updated content');
});
export default 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,341 @@
import { expect, tap, webhelpers } from '@git.zone/tstest/tapbundle';
import { DeesInputWysiwyg } from '../ts_web/elements/wysiwyg/dees-input-wysiwyg.js';
import { DeesWysiwygBlock } from '../ts_web/elements/wysiwyg/dees-wysiwyg-block.js';
tap.test('Keyboard: Arrow navigation between blocks', async () => {
const editor: DeesInputWysiwyg = await webhelpers.fixture(
webhelpers.html`<dees-input-wysiwyg></dees-input-wysiwyg>`
);
// Import multiple blocks
editor.importBlocks([
{ id: 'block-1', type: 'paragraph', content: 'First paragraph' },
{ id: 'block-2', type: 'paragraph', content: 'Second paragraph' },
{ id: 'block-3', type: 'paragraph', content: 'Third paragraph' }
]);
await editor.updateComplete;
await new Promise(resolve => setTimeout(resolve, 100));
// Focus first block at end
const firstBlockWrapper = editor.shadowRoot?.querySelector('[data-block-id="block-1"]');
const firstBlockComponent = firstBlockWrapper?.querySelector('dees-wysiwyg-block') as DeesWysiwygBlock;
const firstBlockContainer = firstBlockComponent?.shadowRoot?.querySelector('.wysiwyg-block-container') as HTMLElement;
const firstParagraph = firstBlockContainer?.querySelector('.block.paragraph') as HTMLElement;
// Focus and set cursor at end of first block
firstParagraph.focus();
const textNode = firstParagraph.firstChild;
if (textNode && textNode.nodeType === Node.TEXT_NODE) {
const range = document.createRange();
const selection = window.getSelection();
range.setStart(textNode, textNode.textContent?.length || 0);
range.setEnd(textNode, textNode.textContent?.length || 0);
selection?.removeAllRanges();
selection?.addRange(range);
}
await new Promise(resolve => setTimeout(resolve, 100));
// Press ArrowRight to move to second block
const arrowRightEvent = new KeyboardEvent('keydown', {
key: 'ArrowRight',
code: 'ArrowRight',
bubbles: true,
cancelable: true,
composed: true
});
firstParagraph.dispatchEvent(arrowRightEvent);
await new Promise(resolve => setTimeout(resolve, 200));
// Check if second block is focused
const secondBlockWrapper = editor.shadowRoot?.querySelector('[data-block-id="block-2"]');
const secondBlockComponent = secondBlockWrapper?.querySelector('dees-wysiwyg-block') as DeesWysiwygBlock;
const secondBlockContainer = secondBlockComponent?.shadowRoot?.querySelector('.wysiwyg-block-container') as HTMLElement;
const secondParagraph = secondBlockContainer?.querySelector('.block.paragraph') as HTMLElement;
// Check if the second paragraph has focus
const activeElement = secondBlockComponent.shadowRoot?.activeElement;
expect(activeElement).toEqual(secondParagraph);
console.log('Arrow navigation test complete');
});
tap.test('Keyboard: Backspace merges blocks', async () => {
const editor: DeesInputWysiwyg = await webhelpers.fixture(
webhelpers.html`<dees-input-wysiwyg></dees-input-wysiwyg>`
);
// Import two blocks
editor.importBlocks([
{ id: 'merge-1', type: 'paragraph', content: 'First' },
{ id: 'merge-2', type: 'paragraph', content: 'Second' }
]);
await editor.updateComplete;
await new Promise(resolve => setTimeout(resolve, 100));
// Focus second block at beginning
const secondBlockWrapper = editor.shadowRoot?.querySelector('[data-block-id="merge-2"]');
const secondBlockComponent = secondBlockWrapper?.querySelector('dees-wysiwyg-block') as DeesWysiwygBlock;
const secondBlockContainer = secondBlockComponent?.shadowRoot?.querySelector('.wysiwyg-block-container') as HTMLElement;
const secondParagraph = secondBlockContainer?.querySelector('.block.paragraph') as HTMLElement;
// Focus and set cursor at beginning
secondParagraph.focus();
const textNode = secondParagraph.firstChild;
if (textNode && textNode.nodeType === Node.TEXT_NODE) {
const range = document.createRange();
const selection = window.getSelection();
range.setStart(textNode, 0);
range.setEnd(textNode, 0);
selection?.removeAllRanges();
selection?.addRange(range);
}
await new Promise(resolve => setTimeout(resolve, 100));
// Press Backspace to merge with previous block
const backspaceEvent = new KeyboardEvent('keydown', {
key: 'Backspace',
code: 'Backspace',
bubbles: true,
cancelable: true,
composed: true
});
secondParagraph.dispatchEvent(backspaceEvent);
await new Promise(resolve => setTimeout(resolve, 200));
// Check if blocks were merged
expect(editor.blocks.length).toEqual(1);
expect(editor.blocks[0].content).toContain('First');
expect(editor.blocks[0].content).toContain('Second');
console.log('Backspace merge test complete');
});
tap.test('Keyboard: Delete key on non-editable blocks', async () => {
const editor: DeesInputWysiwyg = await webhelpers.fixture(
webhelpers.html`<dees-input-wysiwyg></dees-input-wysiwyg>`
);
// Import blocks including a divider
editor.importBlocks([
{ id: 'para-1', type: 'paragraph', content: 'Before divider' },
{ id: 'div-1', type: 'divider', content: '' },
{ id: 'para-2', type: 'paragraph', content: 'After divider' }
]);
await editor.updateComplete;
await new Promise(resolve => setTimeout(resolve, 100));
// Focus the divider block
const dividerBlockWrapper = editor.shadowRoot?.querySelector('[data-block-id="div-1"]');
const dividerBlockComponent = dividerBlockWrapper?.querySelector('dees-wysiwyg-block') as DeesWysiwygBlock;
const dividerBlockContainer = dividerBlockComponent?.shadowRoot?.querySelector('.wysiwyg-block-container') as HTMLElement;
const dividerElement = dividerBlockContainer?.querySelector('.block.divider') as HTMLElement;
// Non-editable blocks need to be focused differently
dividerElement?.focus();
await new Promise(resolve => setTimeout(resolve, 100));
// Press Delete to remove the divider
const deleteEvent = new KeyboardEvent('keydown', {
key: 'Delete',
code: 'Delete',
bubbles: true,
cancelable: true,
composed: true
});
dividerElement.dispatchEvent(deleteEvent);
await new Promise(resolve => setTimeout(resolve, 200));
// Check if divider was removed
expect(editor.blocks.length).toEqual(2);
expect(editor.blocks.find(b => b.type === 'divider')).toBeUndefined();
console.log('Delete key on non-editable block test complete');
});
tap.test('Keyboard: Tab key in code block', async () => {
const editor: DeesInputWysiwyg = await webhelpers.fixture(
webhelpers.html`<dees-input-wysiwyg></dees-input-wysiwyg>`
);
// Import a code block
editor.importBlocks([
{ id: 'code-1', type: 'code', content: 'function test() {', metadata: { language: 'javascript' } }
]);
await editor.updateComplete;
await new Promise(resolve => setTimeout(resolve, 100));
// Focus code block
const codeBlockWrapper = editor.shadowRoot?.querySelector('[data-block-id="code-1"]');
const codeBlockComponent = codeBlockWrapper?.querySelector('dees-wysiwyg-block') as DeesWysiwygBlock;
const codeBlockContainer = codeBlockComponent?.shadowRoot?.querySelector('.wysiwyg-block-container') as HTMLElement;
const codeElement = codeBlockContainer?.querySelector('.block.code') as HTMLElement;
// Focus and set cursor at end
codeElement.focus();
const textNode = codeElement.firstChild;
if (textNode && textNode.nodeType === Node.TEXT_NODE) {
const range = document.createRange();
const selection = window.getSelection();
range.setStart(textNode, textNode.textContent?.length || 0);
range.setEnd(textNode, textNode.textContent?.length || 0);
selection?.removeAllRanges();
selection?.addRange(range);
}
await new Promise(resolve => setTimeout(resolve, 100));
// Press Tab to insert spaces
const tabEvent = new KeyboardEvent('keydown', {
key: 'Tab',
code: 'Tab',
bubbles: true,
cancelable: true,
composed: true
});
codeElement.dispatchEvent(tabEvent);
await new Promise(resolve => setTimeout(resolve, 200));
// Check if spaces were inserted
const updatedContent = codeElement.textContent || '';
expect(updatedContent).toContain(' '); // Tab should insert 2 spaces
console.log('Tab in code block test complete');
});
tap.test('Keyboard: ArrowUp/Down navigation', async () => {
const editor: DeesInputWysiwyg = await webhelpers.fixture(
webhelpers.html`<dees-input-wysiwyg></dees-input-wysiwyg>`
);
// Import multiple blocks
editor.importBlocks([
{ id: 'nav-1', type: 'paragraph', content: 'First line' },
{ id: 'nav-2', type: 'paragraph', content: 'Second line' },
{ id: 'nav-3', type: 'paragraph', content: 'Third line' }
]);
await editor.updateComplete;
await new Promise(resolve => setTimeout(resolve, 100));
// Focus second block
const secondBlockWrapper = editor.shadowRoot?.querySelector('[data-block-id="nav-2"]');
const secondBlockComponent = secondBlockWrapper?.querySelector('dees-wysiwyg-block') as DeesWysiwygBlock;
const secondBlockContainer = secondBlockComponent?.shadowRoot?.querySelector('.wysiwyg-block-container') as HTMLElement;
const secondParagraph = secondBlockContainer?.querySelector('.block.paragraph') as HTMLElement;
secondParagraph.focus();
await new Promise(resolve => setTimeout(resolve, 100));
// Press ArrowUp to move to first block
const arrowUpEvent = new KeyboardEvent('keydown', {
key: 'ArrowUp',
code: 'ArrowUp',
bubbles: true,
cancelable: true,
composed: true
});
secondParagraph.dispatchEvent(arrowUpEvent);
await new Promise(resolve => setTimeout(resolve, 200));
// Check if first block is focused
const firstBlockWrapper = editor.shadowRoot?.querySelector('[data-block-id="nav-1"]');
const firstBlockComponent = firstBlockWrapper?.querySelector('dees-wysiwyg-block') as DeesWysiwygBlock;
const firstParagraph = firstBlockComponent?.shadowRoot?.querySelector('.block.paragraph') as HTMLElement;
expect(firstBlockComponent.shadowRoot?.activeElement).toEqual(firstParagraph);
// Now press ArrowDown twice to get to third block
const arrowDownEvent = new KeyboardEvent('keydown', {
key: 'ArrowDown',
code: 'ArrowDown',
bubbles: true,
cancelable: true,
composed: true
});
firstParagraph.dispatchEvent(arrowDownEvent);
await new Promise(resolve => setTimeout(resolve, 200));
// Second block should be focused, dispatch again
const secondActiveElement = secondBlockComponent.shadowRoot?.activeElement;
if (secondActiveElement) {
secondActiveElement.dispatchEvent(arrowDownEvent);
await new Promise(resolve => setTimeout(resolve, 200));
}
// Check if third block is focused
const thirdBlockWrapper = editor.shadowRoot?.querySelector('[data-block-id="nav-3"]');
const thirdBlockComponent = thirdBlockWrapper?.querySelector('dees-wysiwyg-block') as DeesWysiwygBlock;
const thirdParagraph = thirdBlockComponent?.shadowRoot?.querySelector('.block.paragraph') as HTMLElement;
expect(thirdBlockComponent.shadowRoot?.activeElement).toEqual(thirdParagraph);
console.log('ArrowUp/Down navigation test complete');
});
tap.test('Keyboard: Formatting shortcuts', async () => {
const editor: DeesInputWysiwyg = await webhelpers.fixture(
webhelpers.html`<dees-input-wysiwyg></dees-input-wysiwyg>`
);
// Import a paragraph
editor.importBlocks([
{ id: 'format-1', type: 'paragraph', content: 'Test formatting' }
]);
await editor.updateComplete;
await new Promise(resolve => setTimeout(resolve, 100));
// Focus and select text
const blockWrapper = editor.shadowRoot?.querySelector('[data-block-id="format-1"]');
const blockComponent = blockWrapper?.querySelector('dees-wysiwyg-block') as DeesWysiwygBlock;
const blockContainer = blockComponent?.shadowRoot?.querySelector('.wysiwyg-block-container') as HTMLElement;
const paragraph = blockContainer?.querySelector('.block.paragraph') as HTMLElement;
paragraph.focus();
// Select "formatting"
const textNode = paragraph.firstChild;
if (textNode && textNode.nodeType === Node.TEXT_NODE) {
const range = document.createRange();
const selection = window.getSelection();
range.setStart(textNode, 5); // After "Test "
range.setEnd(textNode, 15); // After "formatting"
selection?.removeAllRanges();
selection?.addRange(range);
}
await new Promise(resolve => setTimeout(resolve, 100));
// Press Cmd/Ctrl+B for bold
const boldEvent = new KeyboardEvent('keydown', {
key: 'b',
code: 'KeyB',
metaKey: true, // Use metaKey for Mac, ctrlKey for Windows/Linux
bubbles: true,
cancelable: true,
composed: true
});
paragraph.dispatchEvent(boldEvent);
await new Promise(resolve => setTimeout(resolve, 200));
// Check if bold was applied
const content = paragraph.innerHTML;
expect(content).toContain('<strong>') || expect(content).toContain('<b>');
console.log('Formatting shortcuts test complete');
});
export default tap.start();

View File

@ -0,0 +1,150 @@
import { expect, tap, webhelpers } from '@git.zone/tstest/tapbundle';
import { DeesInputWysiwyg } from '../ts_web/elements/wysiwyg/dees-input-wysiwyg.js';
import { DeesWysiwygBlock } from '../ts_web/elements/wysiwyg/dees-wysiwyg-block.js';
tap.test('Phase 3: Quote block should render and work correctly', async () => {
const editor: DeesInputWysiwyg = await webhelpers.fixture(
webhelpers.html`<dees-input-wysiwyg></dees-input-wysiwyg>`
);
// Import a quote block
editor.importBlocks([
{ id: 'quote-1', type: 'quote', content: 'This is a famous quote' }
]);
await editor.updateComplete;
await new Promise(resolve => setTimeout(resolve, 100));
// Check if quote block was rendered
const quoteBlockWrapper = editor.shadowRoot?.querySelector('[data-block-id="quote-1"]');
const quoteBlockComponent = quoteBlockWrapper?.querySelector('dees-wysiwyg-block') as DeesWysiwygBlock;
expect(quoteBlockComponent).toBeTruthy();
const quoteContainer = quoteBlockComponent?.shadowRoot?.querySelector('.wysiwyg-block-container') as HTMLElement;
const quoteElement = quoteContainer?.querySelector('.block.quote') as HTMLElement;
expect(quoteElement).toBeTruthy();
expect(quoteElement?.textContent).toEqual('This is a famous quote');
// Check if styles are applied (border-left for quote)
const computedStyle = window.getComputedStyle(quoteElement);
expect(computedStyle.borderLeftStyle).toEqual('solid');
expect(computedStyle.fontStyle).toEqual('italic');
});
tap.test('Phase 3: Code block should render and handle tab correctly', async () => {
const editor: DeesInputWysiwyg = await webhelpers.fixture(
webhelpers.html`<dees-input-wysiwyg></dees-input-wysiwyg>`
);
// Import a code block
editor.importBlocks([
{ id: 'code-1', type: 'code', content: 'const x = 42;', metadata: { language: 'javascript' } }
]);
await editor.updateComplete;
await new Promise(resolve => setTimeout(resolve, 100));
// Check if code block was rendered
const codeBlockWrapper = editor.shadowRoot?.querySelector('[data-block-id="code-1"]');
const codeBlockComponent = codeBlockWrapper?.querySelector('dees-wysiwyg-block') as DeesWysiwygBlock;
const codeContainer = codeBlockComponent?.shadowRoot?.querySelector('.wysiwyg-block-container') as HTMLElement;
const codeElement = codeContainer?.querySelector('.block.code') as HTMLElement;
expect(codeElement).toBeTruthy();
expect(codeElement?.textContent).toEqual('const x = 42;');
// Check if language label is shown
const languageLabel = codeContainer?.querySelector('.code-language');
expect(languageLabel?.textContent).toEqual('javascript');
// Check if monospace font is applied
const computedStyle = window.getComputedStyle(codeElement);
expect(computedStyle.fontFamily).toContain('monospace');
});
tap.test('Phase 3: List block should render correctly', async () => {
const editor: DeesInputWysiwyg = await webhelpers.fixture(
webhelpers.html`<dees-input-wysiwyg></dees-input-wysiwyg>`
);
// Import a list block
editor.importBlocks([
{ id: 'list-1', type: 'list', content: 'First item\nSecond item\nThird item' }
]);
await editor.updateComplete;
await new Promise(resolve => setTimeout(resolve, 100));
// Check if list block was rendered
const listBlockWrapper = editor.shadowRoot?.querySelector('[data-block-id="list-1"]');
const listBlockComponent = listBlockWrapper?.querySelector('dees-wysiwyg-block') as DeesWysiwygBlock;
const listContainer = listBlockComponent?.shadowRoot?.querySelector('.wysiwyg-block-container') as HTMLElement;
const listElement = listContainer?.querySelector('.block.list') as HTMLElement;
expect(listElement).toBeTruthy();
// Check if list items were created
const listItems = listElement?.querySelectorAll('li');
expect(listItems?.length).toEqual(3);
expect(listItems?.[0].textContent).toEqual('First item');
expect(listItems?.[1].textContent).toEqual('Second item');
expect(listItems?.[2].textContent).toEqual('Third item');
// Check if it's an unordered list by default
const ulElement = listElement?.querySelector('ul');
expect(ulElement).toBeTruthy();
});
tap.test('Phase 3: Quote block split should work', async () => {
const editor: DeesInputWysiwyg = await webhelpers.fixture(
webhelpers.html`<dees-input-wysiwyg></dees-input-wysiwyg>`
);
// Import a quote block
editor.importBlocks([
{ id: 'quote-split', type: 'quote', content: 'To be or not to be' }
]);
await editor.updateComplete;
await new Promise(resolve => setTimeout(resolve, 100));
// Get the quote block
const quoteBlockWrapper = editor.shadowRoot?.querySelector('[data-block-id="quote-split"]');
const quoteBlockComponent = quoteBlockWrapper?.querySelector('dees-wysiwyg-block') as DeesWysiwygBlock;
const quoteContainer = quoteBlockComponent?.shadowRoot?.querySelector('.wysiwyg-block-container') as HTMLElement;
const quoteElement = quoteContainer?.querySelector('.block.quote') as HTMLElement;
// Focus and set cursor after "To be"
quoteElement.focus();
const textNode = quoteElement.firstChild;
if (textNode && textNode.nodeType === Node.TEXT_NODE) {
const range = document.createRange();
const selection = window.getSelection();
range.setStart(textNode, 5); // After "To be"
range.setEnd(textNode, 5);
selection?.removeAllRanges();
selection?.addRange(range);
await new Promise(resolve => setTimeout(resolve, 100));
// Press Enter to split
const enterEvent = new KeyboardEvent('keydown', {
key: 'Enter',
code: 'Enter',
bubbles: true,
cancelable: true,
composed: true
});
quoteElement.dispatchEvent(enterEvent);
await new Promise(resolve => setTimeout(resolve, 200));
// Check if split happened correctly
expect(editor.blocks.length).toEqual(2);
expect(editor.blocks[0].content).toEqual('To be');
expect(editor.blocks[1].content).toEqual(' or not to be');
expect(editor.blocks[1].type).toEqual('paragraph'); // New block should be paragraph
}
});
export default tap.start();

View File

@ -0,0 +1,105 @@
import { tap, expect, webhelpers } from '@git.zone/tstest/tapbundle';
import { BlockRegistry } from '../ts_web/elements/wysiwyg/blocks/block.registry.js';
import { DividerBlockHandler } from '../ts_web/elements/wysiwyg/blocks/content/divider.block.js';
import { ParagraphBlockHandler } from '../ts_web/elements/wysiwyg/blocks/text/paragraph.block.js';
import { HeadingBlockHandler } from '../ts_web/elements/wysiwyg/blocks/text/heading.block.js';
// Import block registration to ensure handlers are registered
import '../ts_web/elements/wysiwyg/wysiwyg.blockregistration.js';
tap.test('BlockRegistry should register and retrieve handlers', async () => {
// Test divider handler
const dividerHandler = BlockRegistry.getHandler('divider');
expect(dividerHandler).toBeDefined();
expect(dividerHandler).toBeInstanceOf(DividerBlockHandler);
expect(dividerHandler?.type).toEqual('divider');
// Test paragraph handler
const paragraphHandler = BlockRegistry.getHandler('paragraph');
expect(paragraphHandler).toBeDefined();
expect(paragraphHandler).toBeInstanceOf(ParagraphBlockHandler);
expect(paragraphHandler?.type).toEqual('paragraph');
// Test heading handlers
const heading1Handler = BlockRegistry.getHandler('heading-1');
expect(heading1Handler).toBeDefined();
expect(heading1Handler).toBeInstanceOf(HeadingBlockHandler);
expect(heading1Handler?.type).toEqual('heading-1');
const heading2Handler = BlockRegistry.getHandler('heading-2');
expect(heading2Handler).toBeDefined();
expect(heading2Handler).toBeInstanceOf(HeadingBlockHandler);
expect(heading2Handler?.type).toEqual('heading-2');
const heading3Handler = BlockRegistry.getHandler('heading-3');
expect(heading3Handler).toBeDefined();
expect(heading3Handler).toBeInstanceOf(HeadingBlockHandler);
expect(heading3Handler?.type).toEqual('heading-3');
});
tap.test('Block handlers should render content correctly', async () => {
const testBlock = {
id: 'test-1',
type: 'paragraph' as const,
content: 'Test paragraph content'
};
const handler = BlockRegistry.getHandler('paragraph');
expect(handler).toBeDefined();
if (handler) {
const rendered = handler.render(testBlock, false);
expect(rendered).toContain('contenteditable="true"');
expect(rendered).toContain('data-block-type="paragraph"');
expect(rendered).toContain('Test paragraph content');
}
});
tap.test('Divider handler should render correctly', async () => {
const dividerBlock = {
id: 'test-divider',
type: 'divider' as const,
content: ' '
};
const handler = BlockRegistry.getHandler('divider');
expect(handler).toBeDefined();
if (handler) {
const rendered = handler.render(dividerBlock, false);
expect(rendered).toContain('class="block divider"');
expect(rendered).toContain('tabindex="0"');
expect(rendered).toContain('divider-icon');
}
});
tap.test('Heading handlers should render with correct levels', async () => {
const headingBlock = {
id: 'test-h1',
type: 'heading-1' as const,
content: 'Test Heading'
};
const handler = BlockRegistry.getHandler('heading-1');
expect(handler).toBeDefined();
if (handler) {
const rendered = handler.render(headingBlock, false);
expect(rendered).toContain('class="block heading-1"');
expect(rendered).toContain('contenteditable="true"');
expect(rendered).toContain('Test Heading');
}
});
tap.test('getAllTypes should return all registered types', async () => {
const allTypes = BlockRegistry.getAllTypes();
expect(allTypes).toContain('divider');
expect(allTypes).toContain('paragraph');
expect(allTypes).toContain('heading-1');
expect(allTypes).toContain('heading-2');
expect(allTypes).toContain('heading-3');
expect(allTypes.length).toBeGreaterThanOrEqual(5);
});
export default tap.start();

View File

@ -0,0 +1,156 @@
import { expect, tap, webhelpers } from '@git.zone/tstest/tapbundle';
import { DeesInputWysiwyg } from '../ts_web/elements/wysiwyg/dees-input-wysiwyg.js';
import { DeesWysiwygBlock } from '../ts_web/elements/wysiwyg/dees-wysiwyg-block.js';
tap.test('Selection highlighting should work consistently for all block types', async () => {
const editor: DeesInputWysiwyg = await webhelpers.fixture(
webhelpers.html`<dees-input-wysiwyg></dees-input-wysiwyg>`
);
// Import various block types
editor.importBlocks([
{ id: 'para-1', type: 'paragraph', content: 'This is a paragraph' },
{ id: 'heading-1', type: 'heading-1', content: 'This is a heading' },
{ id: 'quote-1', type: 'quote', content: 'This is a quote' },
{ id: 'code-1', type: 'code', content: 'const x = 42;' },
{ id: 'list-1', type: 'list', content: 'Item 1\nItem 2' }
]);
await editor.updateComplete;
await new Promise(resolve => setTimeout(resolve, 100));
// Test paragraph highlighting
console.log('Testing paragraph highlighting...');
const paraWrapper = editor.shadowRoot?.querySelector('[data-block-id="para-1"]');
const paraComponent = paraWrapper?.querySelector('dees-wysiwyg-block') as DeesWysiwygBlock;
const paraContainer = paraComponent?.shadowRoot?.querySelector('.wysiwyg-block-container') as HTMLElement;
const paraElement = paraContainer?.querySelector('.block.paragraph') as HTMLElement;
// Focus paragraph to select it
paraElement.focus();
await new Promise(resolve => setTimeout(resolve, 100));
// Check if paragraph has selected class
const paraHasSelected = paraElement.classList.contains('selected');
console.log('Paragraph has selected class:', paraHasSelected);
// Check computed styles
const paraStyle = window.getComputedStyle(paraElement);
console.log('Paragraph background:', paraStyle.background);
console.log('Paragraph box-shadow:', paraStyle.boxShadow);
// Test heading highlighting
console.log('\nTesting heading highlighting...');
const headingWrapper = editor.shadowRoot?.querySelector('[data-block-id="heading-1"]');
const headingComponent = headingWrapper?.querySelector('dees-wysiwyg-block') as DeesWysiwygBlock;
const headingContainer = headingComponent?.shadowRoot?.querySelector('.wysiwyg-block-container') as HTMLElement;
const headingElement = headingContainer?.querySelector('.block.heading-1') as HTMLElement;
// Focus heading to select it
headingElement.focus();
await new Promise(resolve => setTimeout(resolve, 100));
// Check if heading has selected class
const headingHasSelected = headingElement.classList.contains('selected');
console.log('Heading has selected class:', headingHasSelected);
// Check computed styles
const headingStyle = window.getComputedStyle(headingElement);
console.log('Heading background:', headingStyle.background);
console.log('Heading box-shadow:', headingStyle.boxShadow);
// Test quote highlighting
console.log('\nTesting quote highlighting...');
const quoteWrapper = editor.shadowRoot?.querySelector('[data-block-id="quote-1"]');
const quoteComponent = quoteWrapper?.querySelector('dees-wysiwyg-block') as DeesWysiwygBlock;
const quoteContainer = quoteComponent?.shadowRoot?.querySelector('.wysiwyg-block-container') as HTMLElement;
const quoteElement = quoteContainer?.querySelector('.block.quote') as HTMLElement;
// Focus quote to select it
quoteElement.focus();
await new Promise(resolve => setTimeout(resolve, 100));
// Check if quote has selected class
const quoteHasSelected = quoteElement.classList.contains('selected');
console.log('Quote has selected class:', quoteHasSelected);
// Test code highlighting
console.log('\nTesting code highlighting...');
const codeWrapper = editor.shadowRoot?.querySelector('[data-block-id="code-1"]');
const codeComponent = codeWrapper?.querySelector('dees-wysiwyg-block') as DeesWysiwygBlock;
const codeContainer = codeComponent?.shadowRoot?.querySelector('.wysiwyg-block-container') as HTMLElement;
const codeElement = codeContainer?.querySelector('.block.code') as HTMLElement;
// Focus code to select it
codeElement.focus();
await new Promise(resolve => setTimeout(resolve, 100));
// Check if code has selected class
const codeHasSelected = codeElement.classList.contains('selected');
console.log('Code has selected class:', codeHasSelected);
// Focus back on paragraph and check if others are deselected
console.log('\nFocusing back on paragraph...');
paraElement.focus();
await new Promise(resolve => setTimeout(resolve, 100));
// Check that only paragraph is selected
expect(paraElement.classList.contains('selected')).toBeTrue();
expect(headingElement.classList.contains('selected')).toBeFalse();
expect(quoteElement.classList.contains('selected')).toBeFalse();
expect(codeElement.classList.contains('selected')).toBeFalse();
console.log('Selection highlighting test complete');
});
tap.test('Selected class should toggle correctly when clicking between blocks', async () => {
const editor: DeesInputWysiwyg = await webhelpers.fixture(
webhelpers.html`<dees-input-wysiwyg></dees-input-wysiwyg>`
);
// Import two blocks
editor.importBlocks([
{ id: 'block-1', type: 'paragraph', content: 'First block' },
{ id: 'block-2', type: 'paragraph', content: 'Second block' }
]);
await editor.updateComplete;
await new Promise(resolve => setTimeout(resolve, 100));
// Get both blocks
const block1Wrapper = editor.shadowRoot?.querySelector('[data-block-id="block-1"]');
const block1Component = block1Wrapper?.querySelector('dees-wysiwyg-block') as DeesWysiwygBlock;
const block1Container = block1Component?.shadowRoot?.querySelector('.wysiwyg-block-container') as HTMLElement;
const block1Element = block1Container?.querySelector('.block.paragraph') as HTMLElement;
const block2Wrapper = editor.shadowRoot?.querySelector('[data-block-id="block-2"]');
const block2Component = block2Wrapper?.querySelector('dees-wysiwyg-block') as DeesWysiwygBlock;
const block2Container = block2Component?.shadowRoot?.querySelector('.wysiwyg-block-container') as HTMLElement;
const block2Element = block2Container?.querySelector('.block.paragraph') as HTMLElement;
// Initially neither should be selected
expect(block1Element.classList.contains('selected')).toBeFalse();
expect(block2Element.classList.contains('selected')).toBeFalse();
// Click on first block
block1Element.click();
block1Element.focus();
await new Promise(resolve => setTimeout(resolve, 100));
// First block should be selected
expect(block1Element.classList.contains('selected')).toBeTrue();
expect(block2Element.classList.contains('selected')).toBeFalse();
// Click on second block
block2Element.click();
block2Element.focus();
await new Promise(resolve => setTimeout(resolve, 100));
// Second block should be selected, first should not
expect(block1Element.classList.contains('selected')).toBeFalse();
expect(block2Element.classList.contains('selected')).toBeTrue();
console.log('Toggle test complete');
});
export default tap.start();

View File

@ -0,0 +1,62 @@
import { expect, tap, webhelpers } from '@git.zone/tstest/tapbundle';
import { DeesInputWysiwyg } from '../ts_web/elements/wysiwyg/dees-input-wysiwyg.js';
import { DeesWysiwygBlock } from '../ts_web/elements/wysiwyg/dees-wysiwyg-block.js';
tap.test('Selection highlighting basic test', async () => {
const editor: DeesInputWysiwyg = await webhelpers.fixture(
webhelpers.html`<dees-input-wysiwyg></dees-input-wysiwyg>`
);
// Import two blocks
editor.importBlocks([
{ id: 'para-1', type: 'paragraph', content: 'First paragraph' },
{ id: 'head-1', type: 'heading-1', content: 'First heading' }
]);
await editor.updateComplete;
await new Promise(resolve => setTimeout(resolve, 500));
// Get paragraph element
const paraWrapper = editor.shadowRoot?.querySelector('[data-block-id="para-1"]');
const paraComponent = paraWrapper?.querySelector('dees-wysiwyg-block') as DeesWysiwygBlock;
const paraBlock = paraComponent?.shadowRoot?.querySelector('.block.paragraph') as HTMLElement;
// Get heading element
const headWrapper = editor.shadowRoot?.querySelector('[data-block-id="head-1"]');
const headComponent = headWrapper?.querySelector('dees-wysiwyg-block') as DeesWysiwygBlock;
const headBlock = headComponent?.shadowRoot?.querySelector('.block.heading-1') as HTMLElement;
console.log('Found elements:', {
paraBlock: !!paraBlock,
headBlock: !!headBlock
});
// Focus paragraph
paraBlock.focus();
await new Promise(resolve => setTimeout(resolve, 100));
// Check classes
console.log('Paragraph classes:', paraBlock.className);
console.log('Heading classes:', headBlock.className);
// Check isSelected property
console.log('Paragraph component isSelected:', paraComponent.isSelected);
console.log('Heading component isSelected:', headComponent.isSelected);
// Focus heading
headBlock.focus();
await new Promise(resolve => setTimeout(resolve, 100));
// Check classes again
console.log('\nAfter focusing heading:');
console.log('Paragraph classes:', paraBlock.className);
console.log('Heading classes:', headBlock.className);
console.log('Paragraph component isSelected:', paraComponent.isSelected);
console.log('Heading component isSelected:', headComponent.isSelected);
// Check that heading is selected
expect(headBlock.classList.contains('selected')).toBeTrue();
expect(paraBlock.classList.contains('selected')).toBeFalse();
});
export default tap.start();

View File

@ -0,0 +1,98 @@
import { tap, expect, webhelpers } from '@git.zone/tstest/tapbundle';
import { DeesInputWysiwyg } from '../ts_web/elements/wysiwyg/dees-input-wysiwyg.js';
import { DeesWysiwygBlock } from '../ts_web/elements/wysiwyg/dees-wysiwyg-block.js';
tap.test('should split paragraph content on Enter key', async () => {
// Create the wysiwyg editor
const editor: DeesInputWysiwyg = await webhelpers.fixture(
webhelpers.html`<dees-input-wysiwyg></dees-input-wysiwyg>`
);
// Import a test paragraph
editor.importBlocks([{
id: 'test-para-1',
type: 'paragraph',
content: 'Hello World'
}]);
await editor.updateComplete;
// Wait for blocks to render
await new Promise(resolve => setTimeout(resolve, 100));
// Get the block wrapper and component
const blockWrapper = editor.shadowRoot?.querySelector('[data-block-id="test-para-1"]');
expect(blockWrapper).toBeDefined();
const blockComponent = blockWrapper?.querySelector('dees-wysiwyg-block') as DeesWysiwygBlock;
expect(blockComponent).toBeDefined();
expect(blockComponent.block.type).toEqual('paragraph');
// Wait for block to render
await blockComponent.updateComplete;
// Test getSplitContent
console.log('Testing getSplitContent...');
const splitResult = blockComponent.getSplitContent();
console.log('Split result:', splitResult);
// Since we haven't set cursor position, it might return null or split at start
// This is just to test if the method is callable
expect(typeof blockComponent.getSplitContent).toEqual('function');
});
tap.test('should handle Enter key press in paragraph', async () => {
// Create the wysiwyg editor
const editor: DeesInputWysiwyg = await webhelpers.fixture(
webhelpers.html`<dees-input-wysiwyg></dees-input-wysiwyg>`
);
// Import a test paragraph
editor.importBlocks([{
id: 'test-enter-1',
type: 'paragraph',
content: 'First part|Second part' // | marks where we'll simulate cursor
}]);
await editor.updateComplete;
await new Promise(resolve => setTimeout(resolve, 100));
// Check initial state
expect(editor.blocks.length).toEqual(1);
expect(editor.blocks[0].content).toEqual('First part|Second part');
// Get the block element
const blockWrapper = editor.shadowRoot?.querySelector('[data-block-id="test-enter-1"]');
const blockComponent = blockWrapper?.querySelector('dees-wysiwyg-block') as DeesWysiwygBlock;
const blockElement = blockComponent.shadowRoot?.querySelector('.block.paragraph') as HTMLDivElement;
expect(blockElement).toBeDefined();
// Set content without the | marker
blockElement.textContent = 'First partSecond part';
// Focus the block
blockElement.focus();
// Create and dispatch Enter key event
const enterEvent = new KeyboardEvent('keydown', {
key: 'Enter',
code: 'Enter',
bubbles: true,
cancelable: true,
composed: true
});
// Dispatch the event
blockElement.dispatchEvent(enterEvent);
// Wait for processing
await new Promise(resolve => setTimeout(resolve, 200));
// Check if block was split (this might not work perfectly in test environment)
console.log('Blocks after Enter:', editor.blocks.length);
console.log('Block contents:', editor.blocks.map(b => b.content));
});
export default tap.start();

View File

@ -3,6 +3,6 @@
*/ */
export const commitinfo = { export const commitinfo = {
name: '@design.estate/dees-catalog', name: '@design.estate/dees-catalog',
version: '1.8.1', 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;
background: ${cssManager.bdTheme('#ffffff', '#161616')};
transition: all 0.1s;
border-top-left-radius: 8px;
border-top-right-radius: 8px;
border-top: 1px solid ${cssManager.bdTheme('#e0e0e0', '#444444')};
} }
.vertical .tabIndicator { .tabIndicator.no-transition {
display: none; transition: none;
}
.tabs-wrapper .tabIndicator {
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,15 +1,301 @@
import { html } 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 './dees-form.js';
import './dees-form-submit.js';
import './dees-input-text.js';
import './dees-icon.js';
export const demoFunc = () => html` export const demoFunc = () => html`
<dees-button>This is a slotted Text</dees-button> <dees-demowrapper>
<p> <style>
<dees-button text="Highlighted: This text shows" type="highlighted">Highlighted</dees-button> ${css`
</p> .demo-container {
<p><dees-button type="discreet">This is discreete button</dees-button></p> display: flex;
<p><dees-button disabled>This is a disabled button</dees-button></p> flex-direction: column;
<p><dees-button type="big">This is a slotted Text</dees-button></p> gap: 24px;
<p><dees-button status="normal">Normal Status</dees-button></p> padding: 24px;
<p><dees-button disabled status="pending">Pending Status</dees-button></p> max-width: 1200px;
<p><dees-button disabled status="success">Success Status</dees-button></p> margin: 0 auto;
<p><dees-button disabled status="error">Error Status</dees-button></p> }
dees-panel {
margin-bottom: 24px;
}
dees-panel:last-child {
margin-bottom: 0;
}
.button-group {
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;
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>
<div class="demo-container">
<dees-panel .title=${'1. Button Variants'} .subtitle=${'Different visual styles for various use cases'}>
<div class="button-group">
<dees-button type="default">Default</dees-button>
<dees-button type="secondary">Secondary</dees-button>
<dees-button type="destructive">Destructive</dees-button>
<dees-button type="outline">Outline</dees-button>
<dees-button type="ghost">Ghost</dees-button>
<dees-button type="link">Link Button</dees-button>
</div>
</dees-panel>
<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-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-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-panel .title=${'5. Event Handling'} .subtitle=${'Interactive examples with click event handling'}>
<div class="button-group">
<dees-button
@clicked=${() => {
const output = document.querySelector('#click-output');
if (output) {
output.textContent = `Clicked: Default button at ${new Date().toLocaleTimeString()}`;
}
}}
>
Click Me
</dees-button>
<dees-button
type="secondary"
.eventDetailData=${'custom-data-123'}
@clicked=${(e: CustomEvent) => {
const output = document.querySelector('#click-output');
if (output) {
output.textContent = `Clicked: Secondary button with data: ${e.detail.data}`;
}
}}
>
Click with Data
</dees-button>
<dees-button
type="destructive"
@clicked=${async () => {
const output = document.querySelector('#click-output');
if (output) {
output.textContent = 'Processing...';
await new Promise(resolve => setTimeout(resolve, 2000));
output.textContent = 'Action completed!';
}
}}
>
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-panel .title=${'6. Form Integration'} .subtitle=${'Buttons working within forms with automatic spacing'}>
<dees-form @formData=${(e: CustomEvent) => {
const output = document.querySelector('#form-output');
if (output) {
output.innerHTML = '<strong>Form submitted with data:</strong><br>' +
JSON.stringify(e.detail.data, null, 2);
}
}}>
<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-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-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>
</div>
</dees-demowrapper>
`; `;

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,138 +48,322 @@ 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
}) })
public status: 'normal' | 'pending' | 'success' | 'error' = 'normal'; public status: 'normal' | 'pending' | 'success' | 'error' = 'normal';
@property({
type: Boolean,
reflect: true
})
public insideForm: boolean = false;
constructor() { constructor() {
super(); super();
} }
public async connectedCallback() {
await super.connectedCallback();
// Auto-detect if inside a form
if (!this.insideForm && this.closest('dees-form')) {
this.insideForm = true;
}
}
public static styles = [ public static styles = [
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 */
:host([inside-form]) {
margin-bottom: 16px;
}
:host([inside-form]:last-child) {
margin-bottom: 0;
}
dees-form[horizontal-layout] :host([inside-form]) {
display: inline-block;
margin-right: 16px;
margin-bottom: 0;
}
dees-form[horizontal-layout] :host([inside-form]:last-child) {
margin-right: 0;
}
.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.discreet:hover { .button.size-sm.pending,
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.1)', 'rgba(255, 255, 255, 0.1)')}; .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.pending {
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 // Handle rolling window if enabled
if (this.rollingWindow > 0 && this.realtimeMode) { if (this.rollingWindow > 0 && this.realtimeMode) {
const now = Date.now(); const now = Date.now();
const cutoffTime = now - this.rollingWindow; const cutoffTime = now - this.rollingWindow;
// Filter data to only include points within the rolling window // Filter data to only include points within the rolling window
const filteredSeries = newSeries.map(series => ({ const filteredSeries = newSeries.map(series => ({
name: series.name, name: series.name,
data: (series.data as any[]).filter(point => { data: (series.data as any[]).filter(point => {
if (typeof point === 'object' && point !== null && 'x' in point) { if (typeof point === 'object' && point !== null && 'x' in point) {
return new Date(point.x).getTime() > cutoffTime; 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);
}
} }
return false;
})
}));
// Only update if we have data await this.chart.updateSeries(filteredSeries, false);
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);
}
} }
} else {
this.chart.updateSeries(filteredSeries, false); 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;
} }
// Get computed style of the element try {
const styleChartContainer = window.getComputedStyle(chartContainer); const mainbox: HTMLDivElement = this.shadowRoot.querySelector('.mainbox');
const chartContainer: HTMLDivElement = this.shadowRoot.querySelector('.chartContainer');
// Extract padding values if (!mainbox || !chartContainer) {
const paddingTop = parseInt(styleChartContainer.paddingTop, 10); return;
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 // Force layout recalculation
const actualWidth = mainbox.clientWidth - paddingLeft - paddingRight; void mainbox.offsetHeight;
const actualHeight = mainbox.offsetHeight - paddingTop - paddingBottom;
await this.chart.updateOptions({ // Get computed style of the element
chart: { const styleChartContainer = window.getComputedStyle(chartContainer);
width: actualWidth,
height: actualHeight, // Extract padding values
}, const paddingTop = parseInt(styleChartContainer.paddingTop, 10);
}); 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
const actualWidth = mainbox.clientWidth - paddingLeft - paddingRight;
const actualHeight = mainbox.offsetHeight - paddingTop - paddingBottom;
// 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;
} }
`, `,
]; ];
@ -139,20 +180,26 @@ 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') {
return; return;
@ -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-button @contextmenu=${(eventArg: MouseEvent) => { <dees-panel heading="Advanced Context Menu Example">
DeesContextmenu.openContextMenuWithOptions(eventArg, [ <div class="demo-area" @contextmenu=${(eventArg: MouseEvent) => {
{ DeesContextmenu.openContextMenuWithOptions(eventArg, [
name: 'Button Action 1', {
iconName: 'play', name: 'Format',
action: async () => { iconName: 'type',
console.log('Button action 1'); 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',
name: 'Button Action 2', iconName: 'shuffle',
iconName: 'pause', action: async () => {}, // Parent items with submenus still need an action
action: async () => { submenu: [
console.log('Button action 2'); { 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: 'Disabled Action', name: 'Delete',
iconName: 'ban', iconName: 'trash2',
disabled: true, action: async () => console.log('Delete')
action: async () => { }
console.log('This should not run'); ]);
}, }}>
}, <h3>Advanced Nested Menu Example</h3>
{ divider: true }, <p>This shows deeply nested submenus and various formatting options</p>
{ </div>
name: 'Settings', </dees-panel>
iconName: 'settings',
action: async () => {
console.log('Settings');
},
},
]);
}}>Right-click on this button for a different menu</dees-button>
<div style="margin-top: 20px;"> <dees-panel heading="Static Context Menu (Always Visible)">
<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,9 +127,13 @@ 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();
this.tabIndex = 0; this.tabIndex = 0;
@ -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:active { .menuitem.has-submenu::after {
content: '';
position: absolute;
right: 8px;
font-size: 16px;
opacity: 0.5;
}
.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

@ -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: 'lucideCopy',
action: async () => {
await this.copyToClipboard(detailArg.value, 'Value');
},
},
{
name: 'Copy Key',
iconName: 'lucideKey',
action: async () => {
await this.copyToClipboard(detailArg.name, 'Key');
},
},
{
name: 'Copy Key:Value',
iconName: 'lucideCopyPlus',
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

@ -15,234 +15,173 @@ 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;
}
}
.form-container {
background: #fff;
border: 1px solid #e0e0e0;
border-radius: 8px;
padding: 24px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
@media (prefers-color-scheme: dark) {
.form-container {
background: #222;
border-color: #333;
}
}
.horizontal-form {
display: flex;
align-items: flex-start;
gap: 16px;
flex-wrap: wrap;
} }
`} `}
</style> </style>
<div class="demo-container"> <div class="demo-container">
<div class="demo-section"> <dees-panel .heading="Complete Form Example" .description="A comprehensive form with various input types, validation, and form submission handling">
<h3>Complete Form Example</h3> <dees-form
<p>A comprehensive form with various input types, validation, and form submission handling</p> @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
.required=${true}
key="firstName"
label="First Name"
.description=${'Your given name'}
></dees-input-text>
<div class="form-container"> <dees-input-text
<dees-form .required=${true}
@formData=${async (eventArg) => { key="lastName"
const form: DeesForm = eventArg.currentTarget; label="Last Name"
form.setStatus('pending', 'Processing...'); ></dees-input-text>
await domtools.plugins.smartdelay.delayFor(2000);
form.setStatus('success', 'Form submitted successfully!');
await domtools.plugins.smartdelay.delayFor(2000);
form.reset();
}}
>
<dees-input-text
.required=${true}
key="firstName"
label="First Name"
.description=${'Your given name'}
></dees-input-text>
<dees-input-text <dees-input-text
.required=${true} .required=${true}
key="lastName" key="email"
label="Last Name" label="Email Address"
></dees-input-text> .description=${'We will use this to contact you'}
></dees-input-text>
<dees-input-text <dees-input-dropdown
.required=${true} .required=${true}
key="email" key="country"
label="Email Address" .label=${'Country'}
.description=${'We will use this to contact you'} .options=${[
></dees-input-text> { option: 'United States', key: 'us' },
{ option: 'Canada', key: 'ca' },
{ option: 'Germany', key: 'de' },
{ option: 'France', key: 'fr' },
{ option: 'United Kingdom', key: 'uk' },
]}
></dees-input-dropdown>
<dees-input-dropdown <dees-input-text
.required=${true} .required=${true}
key="country" key="password"
.label=${'Country'} label="Password"
.options=${[ isPasswordBool
{ option: 'United States', key: 'us' }, .description=${'Minimum 8 characters'}
{ option: 'Canada', key: 'ca' }, ></dees-input-text>
{ option: 'Germany', key: 'de' },
{ option: 'France', key: 'fr' },
{ option: 'United Kingdom', key: 'uk' },
]}
></dees-input-dropdown>
<dees-input-text <dees-input-checkbox
.required=${true} .required=${true}
key="password" key="terms"
label="Password" label="I agree to the Terms and Conditions"
isPasswordBool ></dees-input-checkbox>
.description=${'Minimum 8 characters'}
></dees-input-text>
<dees-input-checkbox <dees-input-checkbox
.required=${true} key="newsletter"
key="terms" label="Send me promotional emails"
label="I agree to the Terms and Conditions" .value=${true}
></dees-input-checkbox> ></dees-input-checkbox>
<dees-input-checkbox <dees-form-submit>Create Account</dees-form-submit>
key="newsletter" </dees-form>
label="Send me promotional emails" </dees-panel>
.value=${true}
></dees-input-checkbox>
<dees-form-submit>Create Account</dees-form-submit> <dees-panel .heading="Horizontal Form Layout" .description="Compact form with inputs arranged horizontally - perfect for filters and quick forms">
</dees-form> <dees-form horizontal-layout>
</div> <dees-input-text
</div> key="search"
label="Search"
></dees-input-text>
<div class="demo-section"> <dees-input-dropdown
<h3>Horizontal Form Layout</h3> key="category"
<p>Compact form with inputs arranged horizontally - perfect for filters and quick forms</p> .label=${'Category'}
.enableSearch=${false}
.options=${[
{ option: 'All', key: 'all' },
{ option: 'Products', key: 'products' },
{ option: 'Services', key: 'services' },
{ option: 'Support', key: 'support' },
]}
></dees-input-dropdown>
<div class="form-container"> <dees-input-dropdown
<dees-form horizontal-layout> key="sort"
<dees-input-text .label=${'Sort By'}
key="search" .enableSearch=${false}
label="Search" .options=${[
></dees-input-text> { option: 'Newest', key: 'newest' },
{ option: 'Popular', key: 'popular' },
{ option: 'Price: Low to High', key: 'price_asc' },
{ option: 'Price: High to Low', key: 'price_desc' },
]}
></dees-input-dropdown>
<dees-input-dropdown <dees-input-checkbox
key="category" key="inStock"
.label=${'Category'} label="In Stock Only"
.enableSearch=${false} .value=${true}
.options=${[ ></dees-input-checkbox>
{ option: 'All', key: 'all' }, </dees-form>
{ option: 'Products', key: 'products' }, </dees-panel>
{ option: 'Services', key: 'services' },
{ option: 'Support', key: 'support' },
]}
></dees-input-dropdown>
<dees-input-dropdown <dees-panel .heading="Advanced Form Features" .description="Form with specialized input types and complex validation">
key="sort" <dees-form
.label=${'Sort By'} @formData=${async (eventArg) => {
.enableSearch=${false} const form: DeesForm = eventArg.currentTarget;
.options=${[ const data = eventArg.detail.data;
{ option: 'Newest', key: 'newest' }, console.log('Form data:', data);
{ option: 'Popular', key: 'popular' }, form.setStatus('success', 'Data logged to console!');
{ option: 'Price: Low to High', key: 'price_asc' }, }}
{ option: 'Price: High to Low', key: 'price_desc' }, >
]} <dees-input-iban
></dees-input-dropdown> key="iban"
label="IBAN"
.required=${true}
></dees-input-iban>
<dees-input-checkbox <dees-input-phone
key="inStock" key="phone"
label="In Stock Only" label="Phone Number"
.value=${true} .required=${true}
></dees-input-checkbox> ></dees-input-phone>
</dees-form>
</div>
</div>
<div class="demo-section"> <dees-input-multitoggle
<h3>Advanced Form Features</h3> key="preferences"
<p>Form with specialized input types and complex validation</p> .label=${'Notification Preferences'}
.options=${['Email', 'SMS', 'Push', 'In-App']}
.selectedOption=${'Email'}
></dees-input-multitoggle>
<div class="form-container"> <dees-input-multiselect
<dees-form key="interests"
@formData=${async (eventArg) => { .label=${'Areas of Interest'}
const form: DeesForm = eventArg.currentTarget; .options=${[
const data = eventArg.detail.data; { option: 'Technology', key: 'tech' },
console.log('Form data:', data); { option: 'Design', key: 'design' },
form.setStatus('success', 'Data logged to console!'); { option: 'Business', key: 'business' },
}} { option: 'Marketing', key: 'marketing' },
> { option: 'Sales', key: 'sales' },
<dees-input-iban ]}
key="iban" ></dees-input-multiselect>
label="IBAN"
.required=${true}
></dees-input-iban>
<dees-input-phone <dees-input-fileupload
key="phone" key="documents"
label="Phone Number" .label=${'Upload Documents'}
.required=${true} .description=${'PDF, DOC, or DOCX files up to 10MB'}
></dees-input-phone> ></dees-input-fileupload>
<dees-input-multitoggle <dees-form-submit>Submit Application</dees-form-submit>
key="preferences" </dees-form>
.label=${'Notification Preferences'} </dees-panel>
.options=${['Email', 'SMS', 'Push', 'In-App']}
.selectedOption=${'Email'}
></dees-input-multitoggle>
<dees-input-multiselect
key="interests"
.label=${'Areas of Interest'}
.options=${[
{ option: 'Technology', key: 'tech' },
{ option: 'Design', key: 'design' },
{ option: 'Business', key: 'business' },
{ option: 'Marketing', key: 'marketing' },
{ option: 'Sales', key: 'sales' },
]}
></dees-input-multiselect>
<dees-input-fileupload
key="documents"
.label=${'Upload Documents'}
.description=${'PDF, DOC, or DOCX files up to 10MB'}
></dees-input-fileupload>
<dees-form-submit>Submit Application</dees-form-submit>
</dees-form>
</div>
</div>
</div> </div>
</dees-demowrapper> </dees-demowrapper>
`; `;

View File

@ -11,7 +11,7 @@ import * as domtools from '@design.estate/dees-domtools';
import { DeesInputCheckbox } from './dees-input-checkbox.js'; import { DeesInputCheckbox } from './dees-input-checkbox.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';
@ -31,7 +31,7 @@ const FORM_INPUT_TYPES = [
DeesInputMultitoggle, DeesInputMultitoggle,
DeesInputPhone, DeesInputPhone,
DeesInputQuantitySelector, DeesInputQuantitySelector,
DeesInputRadio, DeesInputRadiogroup,
DeesInputText, DeesInputText,
DeesInputTypelist, DeesInputTypelist,
DeesTable, DeesTable,
@ -45,7 +45,7 @@ export type TFormInputElement =
| DeesInputMultitoggle | DeesInputMultitoggle
| DeesInputPhone | DeesInputPhone
| DeesInputQuantitySelector | DeesInputQuantitySelector
| DeesInputRadio | DeesInputRadiogroup
| DeesInputText | DeesInputText
| DeesInputTypelist | DeesInputTypelist
| DeesTable<any>; | DeesTable<any>;
@ -132,12 +132,16 @@ 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 } } = {};
for (const child of children) { for (const child of children) {
if (!child.key) { if (!child.key) {
console.log(`form element with label "${child.label}" has no key. skipping.`); console.log(`form element with label "${child.label}" has no key. skipping.`);
continue;
} }
valueObject[child.key] = child.value; valueObject[child.key] = child.value;
} }
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'}
.value=${true}
.key=${'terms'}
></dees-input-checkbox>
<dees-input-checkbox <dees-input-checkbox
.label=${'I agree to the Terms and Conditions'} .label=${'Subscribe to newsletter'}
.value=${true} .value=${false}
.key=${'terms'} .key=${'newsletter'}
></dees-input-checkbox> ></dees-input-checkbox>
<dees-input-checkbox <dees-input-checkbox
.label=${'Subscribe to newsletter'} .label=${'Enable notifications'}
.value=${false} .value=${false}
.key=${'newsletter'} .description=${'Receive email updates about your account'}
></dees-input-checkbox> .key=${'notifications'}
></dees-input-checkbox>
</div>
</dees-panel>
<dees-input-checkbox <dees-panel .title=${'Checkbox States'} .subtitle=${'Different checkbox states and configurations'}>
.label=${'Enable notifications'} <div class="checkbox-group">
.required=${true} <dees-input-checkbox
.key=${'notifications'} .label=${'Default state'}
></dees-input-checkbox> .value=${false}
</div> ></dees-input-checkbox>
<div class="demo-section"> <dees-input-checkbox
<h3>Horizontal Layout</h3> .label=${'Checked state'}
<p>Checkboxes arranged horizontally for compact forms</p> .value=${true}
></dees-input-checkbox>
<div class="horizontal-group"> <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">
<h3>Feature Selection Example</h3>
<p>Common use case for feature toggles with batch operations</p>
<dees-panel .title=${'Feature Selection Example'} .subtitle=${'Common use case for feature toggles with batch operations'}>
<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 <dees-input-checkbox
.label=${'Disabled Checked'} .label=${'Personalized recommendations'}
.disabled=${true} .value=${true}
.value=${true} .description=${'Get suggestions based on your activity'}
.key=${'disabled2'} ></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>
<div class="demo-section"> <dees-input-checkbox
<h3>Real-world Examples</h3> .label=${'Third-party integrations'}
<p>Common checkbox patterns in applications</p> .value=${false}
.description=${'Allow approved partners to access your data'}
></dees-input-checkbox>
</div>
</div>
</dees-panel>
<dees-panel .title=${'Interactive Example'} .subtitle=${'Click checkboxes to see value changes'}>
<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

@ -1,5 +1,8 @@
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> <dees-demowrapper>
@ -14,37 +17,12 @@ export const demoFunc = () => html`
margin: 0 auto; margin: 0 auto;
} }
.demo-section { dees-panel {
background: #f8f9fa; margin-bottom: 24px;
border-radius: 8px;
padding: 24px;
position: relative;
} }
@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 { .horizontal-group {
@ -66,10 +44,7 @@ export const demoFunc = () => html`
</style> </style>
<div class="demo-container"> <div class="demo-container">
<div class="demo-section"> <dees-panel .title=${'1. Basic Dropdowns'} .subtitle=${'Standard dropdown with search functionality and various options'}>
<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 +69,9 @@ export const demoFunc = () => html`
{ option: 'Guest', key: 'guest' } { option: 'Guest', key: 'guest' }
]} ]}
></dees-input-dropdown> ></dees-input-dropdown>
</div> </dees-panel>
<div class="demo-section">
<h3>Without Search</h3>
<p>Dropdown with search functionality disabled for simpler selection</p>
<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 +82,9 @@ export const demoFunc = () => html`
]} ]}
.selectedOption=${{ option: 'Medium', key: 'medium' }} .selectedOption=${{ option: 'Medium', key: 'medium' }}
></dees-input-dropdown> ></dees-input-dropdown>
</div> </dees-panel>
<div class="demo-section">
<h3>Horizontal Layout</h3>
<p>Multiple dropdowns in a horizontal layout for compact forms</p>
<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 +119,9 @@ export const demoFunc = () => html`
]} ]}
></dees-input-dropdown> ></dees-input-dropdown>
</div> </div>
</div> </dees-panel>
<div class="demo-section">
<h3>States</h3>
<p>Different states and configurations</p>
<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 +140,13 @@ 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>
<div class="spacer"> <div class="spacer">
(Spacer to test dropdown positioning) (Spacer to test dropdown positioning)
</div> </div>
<div class="demo-section"> <dees-panel .title=${'5. Bottom Positioning'} .subtitle=${'Dropdown that opens upward when near bottom of viewport'}>
<h3>Bottom Positioning</h3>
<p>Dropdown that opens upward when near bottom of viewport</p>
<dees-input-dropdown <dees-input-dropdown
.label=${'Opens Upward'} .label=${'Opens Upward'}
.options=${[ .options=${[
@ -194,7 +157,65 @@ export const demoFunc = () => html`
{ option: 'Fifth Option', key: 'fifth' } { option: 'Fifth Option', key: 'fifth' }
]} ]}
></dees-input-dropdown> ></dees-input-dropdown>
</div> </dees-panel>
<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'] } }
]}
@change=${(e: CustomEvent) => {
const output = document.querySelector('#selection-output');
if (output && e.detail.value) {
output.innerHTML = `
<strong>Selected:</strong> ${e.detail.value.option}<br>
<strong>Key:</strong> ${e.detail.value.key}<br>
<strong>Price:</strong> $${e.detail.value.payload?.price || 'N/A'}<br>
<strong>Features:</strong> ${e.detail.value.payload?.features?.join(', ') || 'N/A'}
`;
}
}}
></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;">
<em>Select a product to see details...</em>
</div>
</dees-panel>
<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>
</div> </div>
</dees-demowrapper> </dees-demowrapper>
` `

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

@ -42,6 +42,21 @@ export class DeesInputFileupload extends DeesInputBase<DeesInputFileupload> {
}) })
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 +67,441 @@ 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 .description { .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 .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;
} }
`, `,
]; ];
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 .iconName=${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 .iconName=${'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 .iconName=${'lucide:cloud-upload'}></dees-icon>
<div>Drag files here or click to browse</div>
</div> </div>
</div> ` `}
)} <div class="uploadButton" @click=${this.openFileSelector}>
<div class="uploadButton" @click=${ <dees-icon .iconName=${'lucide:upload'}></dees-icon>
this.openFileSelector ${this.buttonText}
}> </div>
${this.buttonText}
</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) return;
const inputFile: HTMLInputElement = this.shadowRoot.querySelector('input[type="file"]'); const inputFile: HTMLInputElement = this.shadowRoot.querySelector('input[type="file"]');
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 +513,131 @@ 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); await this.addFiles(newFiles);
}
this.requestUpdate();
console.log(`Got ${this.value.length} files!`);
// 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>
<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-panel .title=${'Boolean Toggle'} .subtitle=${'Simple on/off switches with custom labels'}>
<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>
<div class="section">
<div class="section-title">Settings Grid</div>
<div class="section-description">Configuration options arranged in a responsive grid layout.</div>
<dees-panel .title=${'Settings Panel'} .subtitle=${'Configuration options in a horizontal layout'}>
<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>
<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-panel .title=${'States & Form Integration'} .subtitle=${'Disabled states and form usage'}>
<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,13 +182,25 @@ 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;
}
// Wait one more frame after fonts are loaded
await new Promise(resolve => requestAnimationFrame(resolve));
// Now set the indicator
this.setIndicator();
} }
public async handleSelection(optionArg: string) { public async handleSelection(optionArg: string) {
if (this.disabled) return;
this.selectedOption = optionArg; this.selectedOption = optionArg;
this.requestUpdate();
this.changeSubject.next(this);
await this.updateComplete;
this.setIndicator(); this.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; margin-bottom: 16px;
color: ${cssManager.bdTheme('hsl(0 0% 9%)', 'hsl(0 0% 95%)')};
} }
.product-price { .cart-item {
color: #1976d2; display: flex;
margin-bottom: 16px; 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,277 +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 .runAfterRender=${async (elementArg: HTMLElement) => {
// Implement radio group behavior
const radioGroups = new Map<string, DeesInputRadio[]>();
// Group radios by their container
const radioContainers = elementArg.querySelectorAll('.radio-group');
radioContainers.forEach((container) => {
const radios = Array.from(container.querySelectorAll('dees-input-radio')) as DeesInputRadio[];
const groupName = container.getAttribute('data-group') || 'default';
radioGroups.set(groupName, radios);
// Add click handlers for radio group behavior
radios.forEach((radio) => {
radio.addEventListener('click', () => {
if (!radio.disabled && !radio.value) {
// Uncheck all other radios in the group
radios.forEach((r) => {
if (r !== radio) {
r.value = false;
}
});
radio.value = true;
}
});
});
});
}}>
<style>
${css`
.demo-container {
display: flex;
flex-direction: column;
gap: 24px;
padding: 24px;
max-width: 1200px;
margin: 0 auto;
}
.demo-section {
background: #f8f9fa;
border-radius: 8px;
padding: 24px;
}
@media (prefers-color-scheme: dark) {
.demo-section {
background: #1a1a1a;
}
}
.demo-section h3 {
margin-top: 0;
margin-bottom: 16px;
color: #0069f2;
font-size: 18px;
}
.demo-section p {
margin-top: 0;
margin-bottom: 16px;
color: #666;
font-size: 14px;
}
@media (prefers-color-scheme: dark) {
.demo-section p {
color: #999;
}
}
.horizontal-group {
display: flex;
align-items: center;
gap: 16px;
flex-wrap: wrap;
}
.radio-group {
display: flex;
flex-direction: column;
gap: 8px;
padding: 16px;
background: #f0f0f0;
border-radius: 4px;
margin-bottom: 16px;
}
@media (prefers-color-scheme: dark) {
.radio-group {
background: #0a0a0a;
}
}
.radio-group-title {
font-weight: 500;
margin-bottom: 8px;
color: #333;
}
@media (prefers-color-scheme: dark) {
.radio-group-title {
color: #ccc;
}
}
.grid-layout {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 24px;
}
`}
</style>
<div class="demo-container">
<div class="demo-section">
<h3>Basic Radio Groups</h3>
<p>Radio buttons for single-choice selections</p>
<div class="radio-group" data-group="plan">
<div class="radio-group-title">Select your subscription plan:</div>
<dees-input-radio
.label=${'Basic Plan - $9/month'}
.value=${true}
.key=${'plan-basic'}
></dees-input-radio>
<dees-input-radio
.label=${'Pro Plan - $29/month'}
.key=${'plan-pro'}
></dees-input-radio>
<dees-input-radio
.label=${'Enterprise Plan - $99/month'}
.key=${'plan-enterprise'}
></dees-input-radio>
</div>
<div class="radio-group" data-group="priority">
<div class="radio-group-title">Task Priority:</div>
<dees-input-radio
.label=${'High Priority'}
.key=${'priority-high'}
></dees-input-radio>
<dees-input-radio
.label=${'Medium Priority'}
.value=${true}
.key=${'priority-medium'}
></dees-input-radio>
<dees-input-radio
.label=${'Low Priority'}
.key=${'priority-low'}
></dees-input-radio>
</div>
</div>
<div class="demo-section">
<h3>Horizontal Layout</h3>
<p>Radio buttons arranged horizontally for yes/no questions</p>
<div class="radio-group" data-group="agreement" style="flex-direction: row;">
<div style="margin-right: 16px;">Do you agree?</div>
<dees-input-radio
.label=${'Yes'}
.layoutMode=${'horizontal'}
.value=${true}
.key=${'agree-yes'}
></dees-input-radio>
<dees-input-radio
.label=${'No'}
.layoutMode=${'horizontal'}
.key=${'agree-no'}
></dees-input-radio>
<dees-input-radio
.label=${'Maybe'}
.layoutMode=${'horizontal'}
.key=${'agree-maybe'}
></dees-input-radio>
</div>
<div class="radio-group" data-group="experience" style="flex-direction: row;">
<div style="margin-right: 16px;">Experience Level:</div>
<dees-input-radio
.label=${'Beginner'}
.layoutMode=${'horizontal'}
.key=${'exp-beginner'}
></dees-input-radio>
<dees-input-radio
.label=${'Intermediate'}
.layoutMode=${'horizontal'}
.value=${true}
.key=${'exp-intermediate'}
></dees-input-radio>
<dees-input-radio
.label=${'Expert'}
.layoutMode=${'horizontal'}
.key=${'exp-expert'}
></dees-input-radio>
</div>
</div>
<div class="demo-section">
<h3>Survey Example</h3>
<p>Multiple radio groups in a survey format</p>
<div class="grid-layout">
<div class="radio-group" data-group="satisfaction">
<div class="radio-group-title">How satisfied are you?</div>
<dees-input-radio .label=${'Very Satisfied'} .key=${'sat-very'}></dees-input-radio>
<dees-input-radio .label=${'Satisfied'} .value=${true} .key=${'sat-normal'}></dees-input-radio>
<dees-input-radio .label=${'Neutral'} .key=${'sat-neutral'}></dees-input-radio>
<dees-input-radio .label=${'Dissatisfied'} .key=${'sat-dis'}></dees-input-radio>
<dees-input-radio .label=${'Very Dissatisfied'} .key=${'sat-verydis'}></dees-input-radio>
</div>
<div class="radio-group" data-group="recommend">
<div class="radio-group-title">Would you recommend us?</div>
<dees-input-radio .label=${'Definitely'} .key=${'rec-def'}></dees-input-radio>
<dees-input-radio .label=${'Probably'} .value=${true} .key=${'rec-prob'}></dees-input-radio>
<dees-input-radio .label=${'Not Sure'} .key=${'rec-unsure'}></dees-input-radio>
<dees-input-radio .label=${'Probably Not'} .key=${'rec-probnot'}></dees-input-radio>
<dees-input-radio .label=${'Definitely Not'} .key=${'rec-defnot'}></dees-input-radio>
</div>
</div>
</div>
<div class="demo-section">
<h3>States</h3>
<p>Different radio button states</p>
<div class="radio-group" data-group="states">
<dees-input-radio
.label=${'Normal Radio'}
.key=${'state-normal'}
></dees-input-radio>
<dees-input-radio
.label=${'Selected Radio'}
.value=${true}
.key=${'state-selected'}
></dees-input-radio>
<dees-input-radio
.label=${'Disabled Unchecked'}
.disabled=${true}
.key=${'state-disabled1'}
></dees-input-radio>
<dees-input-radio
.label=${'Disabled Checked'}
.disabled=${true}
.value=${true}
.key=${'state-disabled2'}
></dees-input-radio>
</div>
</div>
<div class="demo-section">
<h3>Settings Example</h3>
<p>Common radio button patterns in settings</p>
<div class="radio-group" data-group="theme">
<div class="radio-group-title">Theme Preference:</div>
<dees-input-radio .label=${'Light Theme'} .key=${'theme-light'}></dees-input-radio>
<dees-input-radio .label=${'Dark Theme'} .value=${true} .key=${'theme-dark'}></dees-input-radio>
<dees-input-radio .label=${'System Default'} .key=${'theme-system'}></dees-input-radio>
</div>
<div class="radio-group" data-group="notifications">
<div class="radio-group-title">Notification Frequency:</div>
<dees-input-radio .label=${'All Notifications'} .key=${'notif-all'}></dees-input-radio>
<dees-input-radio .label=${'Important Only'} .value=${true} .key=${'notif-important'}></dees-input-radio>
<dees-input-radio .label=${'None'} .key=${'notif-none'}></dees-input-radio>
</div>
</div>
</div>
</dees-demowrapper>
`;

View File

@ -1,113 +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;
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 () {
this.value = !this.value;
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,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';
export const demoFunc = () => html` export const demoFunc = () => html`
<dees-demowrapper> <dees-demowrapper>
@ -14,36 +15,12 @@ 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 { .horizontal-group {
@ -64,14 +41,28 @@ export const demoFunc = () => html`
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
} }
.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> </style>
<div class="demo-container"> <div class="demo-container">
<div class="demo-section"> <dees-panel .title=${'Basic Text Inputs'} .subtitle=${'Standard text inputs with labels and descriptions'}>
<h3>Basic Text Inputs</h3>
<p>Standard text inputs with labels and descriptions</p>
<dees-input-text <dees-input-text
.label=${'Username'} .label=${'Username'}
.value=${'johndoe'} .value=${'johndoe'}
@ -91,12 +82,9 @@ export const demoFunc = () => html`
.value=${'secret123'} .value=${'secret123'}
.key=${'password'} .key=${'password'}
></dees-input-text> ></dees-input-text>
</div> </dees-panel>
<div class="demo-section">
<h3>Horizontal Layout</h3>
<p>Multiple inputs arranged horizontally for compact forms</p>
<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 +107,9 @@ export const demoFunc = () => html`
.key=${'age'} .key=${'age'}
></dees-input-text> ></dees-input-text>
</div> </div>
</div> </dees-panel>
<div class="demo-section">
<h3>Label Positions</h3>
<p>Different label positioning options for various layouts</p>
<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 +135,9 @@ export const demoFunc = () => html`
.labelPosition=${'left'} .labelPosition=${'left'}
></dees-input-text> ></dees-input-text>
</div> </div>
</div> </dees-panel>
<div class="demo-section">
<h3>Validation & States</h3>
<p>Different validation states and input configurations</p>
<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 +156,9 @@ 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>
<div class="demo-section">
<h3>Advanced Features</h3>
<p>Password visibility toggle and other advanced features</p>
<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 +172,24 @@ 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>
<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...'}
@changeSubject=${(event) => {
const output = document.querySelector('#text-input-output');
if (output && event.detail) {
output.textContent = `Current value: "${event.detail.getValue()}"`;
}
}}
></dees-input-text>
<div class="interactive-section">
<div id="text-input-output" class="output-text">Current value: ""</div>
</div>
</dees-panel>
</div> </div>
</dees-demowrapper> </dees-demowrapper>
`; `;

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,75 +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 ${cssManager.bdTheme( colors.bright.blueActive, colors.dark.blueActive)}; border-color: ${cssManager.bdTheme('hsl(0 0% 9%)', 'hsl(0 0% 98%)')};
cursor: text; box-shadow: 0 0 0 2px ${cssManager.bdTheme('hsl(0 0% 9% / 0.05)', 'hsl(0 0% 98% / 0.05)')};
} }
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;
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)')};
} }
`, `,
]; ];
@ -142,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 ? css` ${this.validationText
.validationContainer { ? css`
height: 22px; .validationContainer {
opacity: 1; height: auto;
} opacity: 1;
` : css` transform: translateY(0);
.validationContainer { }
height: 4px; `
padding: 2px !important; : css`
opacity: 0; .validationContainer {
} height: 0;
`} padding: 0 !important;
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 .iconName=${this.showPasswordBool ? 'lucideEye' : 'lucideEyeOff'}></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>
`; `;
@ -203,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')};
} }
`, `,
]; ];

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,2 @@
// Re-export the modular component from the wysiwyg directory
export { DeesInputWysiwyg } from './wysiwyg/dees-input-wysiwyg.js';

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 .iconName=${'lucideInfo'}></dees-icon>
<dees-speechbubble .text=${this.description}></dees-speechbubble> <dees-speechbubble .text=${this.description}></dees-speechbubble>
` `
: html``} : html``}

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 {
cssManager, cssManager,
css, css,
@ -83,7 +84,7 @@ export class DeesMobilenavigation extends DeesElement {
min-width: 280px; min-width: 280px;
transform: translateX(200px); transform: translateX(200px);
color: ${cssManager.bdTheme('#333', '#fff')}; color: ${cssManager.bdTheme('#333', '#fff')};
z-index: 250; z-index: ${zIndexLayers.fixed.mobileNav};
opacity: 0; opacity: 0;
padding: 16px 32px; padding: 16px 32px;
right: 0px; right: 0px;

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: #111; max-height: calc(100vh - 40px);
border-radius: 8px; background: ${cssManager.bdTheme('#ffffff', '#09090b')};
border: 1px solid #222; 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: 0px 2px 5px #00000080; 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,75 +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 #222; 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 #222; 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;
cursor: default; font-weight: 500;
cursor: pointer;
user-select: none; user-select: none;
} transition: all 0.15s ease;
background: ${cssManager.bdTheme('#ffffff', '#27272a')};
.modal .bottomButtons .bottomButton:first-child { border: 1px solid ${cssManager.bdTheme('#e5e7eb', '#3f3f46')};
margin-left: 8px; color: ${cssManager.bdTheme('#09090b', '#fafafa')};
} white-space: nowrap;
.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')};
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')};
} }
.modal .bottomButtons .bottomButton:last-child { .modal .bottomButtons .bottomButton:last-child {
border-right: none; border-right: none;
} }
.modal .bottomButtons .bottomButton.primary {
background: ${cssManager.bdTheme('#3b82f6', '#3b82f6')};
border-color: ${cssManager.bdTheme('#3b82f6', '#3b82f6')};
color: #ffffff;
}
.modal .bottomButtons .bottomButton.primary:hover {
background: ${cssManager.bdTheme('#2563eb', '#2563eb')};
border-color: ${cssManager.bdTheme('#2563eb', '#2563eb')};
}
.modal .bottomButtons .bottomButton.primary:active {
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) => html` <div class="header-button" @click=${this.handleHelp} title="Help">
<div class="bottomButton" @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>
`; `;
@ -210,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>
<ul>
<li>Text and paragraphs</li>
<li>Lists and tables</li>
<li>Form inputs</li>
<li>Other components</li>
</ul>
<dees-input-text .label=${'Example Input'} .description=${'Input inside a panel'}></dees-input-text> <dees-panel .title=${'Default Variant'} .variant=${'default'}>
<p>The default variant has a white background, subtle border, and minimal shadow. It's the standard choice for most content.</p>
<div style="margin-top: 16px;"> <p>Use <code>variant="default"</code> or omit the variant property.</p>
<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 .iconName=${iconName}></dees-icon>
`}
${this.selectable ? html`
<div
class="selection-checkbox ${this.selected ? 'checked' : ''}"
@click=${(e: Event) => {
e.stopPropagation();
this.handleSelectionToggle();
}}
>
<dees-icon .iconName=${'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 .iconName=${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

@ -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;
max-width: 1400px;
margin: 0 auto;
}
.demo-section { dees-panel {
margin-bottom: 48px; margin-bottom: 24px;
} }
.demo-title { dees-panel:last-child {
font-size: 24px; margin-bottom: 0;
font-weight: 600; }
margin-bottom: 16px;
color: ${cssManager.bdTheme('#333', '#fff')};
}
.demo-description { .tile-config {
font-size: 14px; display: grid;
color: ${cssManager.bdTheme('#666', '#aaa')}; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
margin-bottom: 24px; gap: 16px;
} margin-top: 16px;
}
.theme-toggle { .config-section {
position: fixed; padding: 16px;
top: 16px; background: ${cssManager.bdTheme('hsl(210 40% 96.1%)', 'hsl(215 20.2% 16.8%)')};
right: 16px; border-radius: 6px;
padding: 8px 16px; }
background: ${cssManager.bdTheme('#fff', '#1a1a1a')};
border: 1px solid ${cssManager.bdTheme('#e0e0e0', '#2a2a2a')}; .config-title {
border-radius: 8px; font-size: 14px;
cursor: pointer; font-weight: 600;
z-index: 100; 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;
} }
.percentage-container { .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 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,10 +383,14 @@ 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
@clicked=${() => this.handleGridAction(action)}
type="outline"
size="sm"
>
${action.iconName ? html`<dees-icon .iconFA=${action.iconName} size="small"></dees-icon>` : ''} ${action.iconName ? html`<dees-icon .iconFA=${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>
` : ''} ` : ''}
@ -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;
let strokeColor = tile.color || cssManager.bdTheme('#0084ff', '#0066cc'); // 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('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;
.noDataSet { font-size: 14px;
text-align: center; border-collapse: separate;
} border-spacing: 0;
tr {
border-bottom: 1px dashed ${cssManager.bdTheme('#999', '#808080')};
text-align: left;
}
tr:last-child {
border-bottom: none;
text-align: left;
}
tr:hover {
}
tr:hover td {
background: ${cssManager.bdTheme('#22222210', '#ffffff10')};
}
tr:first-child:hover {
cursor: auto;
}
tr:first-child:hover .innerCellContainer {
background: none;
}
tr.selected td {
background: ${cssManager.bdTheme('#22222220', '#ffffff20')};
} }
tr.hasAttachment td { .noDataSet {
background: ${cssManager.bdTheme('#0098847c', '#0098847c')}; padding: 48px 24px;
text-align: center;
color: ${cssManager.bdTheme('hsl(215.4 16.3% 56.9%)', 'hsl(215 20.2% 55.1%)')};
}
thead {
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%)')};
}
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;
}
/* 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%)')};
}
:host([show-horizontal-lines]) tbody tr:first-child {
border-top: none;
}
:host([show-horizontal-lines]) tbody tr:last-child {
border-bottom: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
}
tbody tr:hover {
background: ${cssManager.bdTheme('hsl(210 40% 96.1% / 0.5)', 'hsl(0 0% 14.9% / 0.5)')};
}
/* 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%)')};
}
tbody tr.hasAttachment {
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,
td {
position: relative;
vertical-align: top;
padding: 0px; :host([show-vertical-lines]) th {
border-right: 1px dashed ${cssManager.bdTheme('#999', '#808080')}; border-right: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
} }
.innerCellContainer {
min-height: 36px; td {
position: relative; padding: 12px 24px;
height: 100%; vertical-align: middle;
width: 100%; color: ${cssManager.bdTheme('hsl(0 0% 3.9%)', 'hsl(0 0% 98%)')};
padding: 6px 8px;
line-height: 24px;
} }
th:first-child .innerCellContainer,
td:first-child .innerCellContainer { :host([show-vertical-lines]) td {
padding-left: 0px; border-right: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
} }
th:last-child .innerCellContainer,
td:last-child .innerCellContainer { th:first-child,
padding-right: 0px; td:first-child {
padding-left: 24px;
} }
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 {
} }
`, `,
]; ];
@ -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=${() =>
@ -614,7 +750,6 @@ export class DeesTable<T> extends DeesElement {
</div> </div>
` `
)} )}
</div>
</div> </div>
</td> </td>
`; `;
@ -623,6 +758,7 @@ export class DeesTable<T> extends DeesElement {
</tr> </tr>
`; `;
})} })}
</tbody>
</table> </table>
`; `;
})() })()
@ -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 {
@ -33,6 +34,12 @@ export class DeesWindowLayer extends DeesElement {
blur: false blur: false
}; };
@state()
private backdropZIndex: number = 1000;
@state()
private contentZIndex: number = 1001;
// INSTANCE // INSTANCE
@property({ @property({
type: Boolean type: Boolean
@ -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>
`; `;
@ -102,8 +114,20 @@ export class DeesWindowLayer extends DeesElement {
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';
@ -31,9 +32,12 @@ export * from './dees-input-fileupload.js';
export * from './dees-input-iban.js'; export * from './dees-input-iban.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-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';
@ -42,6 +46,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,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

@ -0,0 +1,87 @@
# WYSIWYG Block Migration Status
## Overview
This document tracks the progress of migrating all WYSIWYG blocks to the new block handler architecture.
## Migration Progress
### ✅ Phase 1: Architecture Foundation
- Created block handler base classes and interfaces
- Created block registry system
- Created common block styles and utilities
### ✅ Phase 2: Divider Block
- Simple non-editable block as proof of concept
- See `phase2-summary.md` for details
### ✅ Phase 3: Paragraph Block
- First text block with full editing capabilities
- Established patterns for text selection, cursor tracking, and content splitting
- See commit history for implementation details
### ✅ Phase 4: Heading Blocks
- All three heading levels (h1, h2, h3) using unified handler
- See `phase4-summary.md` for details
### ✅ Phase 5: Other Text Blocks
- [x] Quote block - Completed with custom styling
- [x] Code block - Completed with syntax highlighting, line numbers, and copy button
- [x] List block - Completed with bullet and numbered list support
### 🔄 Phase 6: Media Blocks (In Progress)
- [x] Image block - Completed with click upload, drag-drop, and base64 encoding
- [x] YouTube block - Completed with URL parsing and video embedding
- [ ] Attachment block
### 📋 Phase 7: Content Blocks (Planned)
- [ ] Markdown block
- [ ] HTML block
## Block Handler Status
| Block Type | Handler Created | Registered | Tested | Notes |
|------------|----------------|------------|---------|-------|
| divider | ✅ | ✅ | ✅ | Complete |
| paragraph | ✅ | ✅ | ✅ | Complete |
| heading-1 | ✅ | ✅ | ✅ | Using HeadingBlockHandler |
| heading-2 | ✅ | ✅ | ✅ | Using HeadingBlockHandler |
| heading-3 | ✅ | ✅ | ✅ | Using HeadingBlockHandler |
| quote | ✅ | ✅ | ✅ | Complete with custom styling |
| code | ✅ | ✅ | ✅ | Complete with highlighting, line numbers, copy |
| list | ✅ | ✅ | ✅ | Complete with bullet/numbered support |
| image | ✅ | ✅ | ✅ | Complete with upload, drag-drop support |
| youtube | ✅ | ✅ | ✅ | Complete with URL parsing, video embedding |
| attachment | ❌ | ❌ | ❌ | Phase 6 |
| markdown | ❌ | ❌ | ❌ | Phase 7 |
| html | ❌ | ❌ | ❌ | Phase 7 |
## Files Modified During Migration
### Core Architecture Files
- `blocks/block.base.ts` - Base handler interface and class
- `blocks/block.registry.ts` - Registry for handlers
- `blocks/block.styles.ts` - Common styles
- `blocks/index.ts` - Main exports
- `wysiwyg.blockregistration.ts` - Registration of all handlers
### Handler Files Created
- `blocks/content/divider.block.ts`
- `blocks/text/paragraph.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
- `dees-wysiwyg-block.ts` - Updated to use registry pattern
## Next Steps
1. Begin Phase 6: Media blocks migration
- Start with image block (most common media type)
- 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

@ -0,0 +1,294 @@
# Critical WYSIWYG Knowledge - DO NOT LOSE
This document captures all the hard-won knowledge from our WYSIWYG editor development. These patterns and solutions took significant effort to discover and MUST be preserved during refactoring.
## 1. Static Rendering to Prevent Focus Loss
### Problem
When using Lit's reactive rendering, every state change would cause a re-render, which would:
- Lose cursor position
- Lose focus state
- Interrupt typing
- Break IME (Input Method Editor) support
### Solution
We render blocks **statically** and manage updates imperatively:
```typescript
// In dees-wysiwyg-block.ts
render(): TemplateResult {
if (!this.block) return html``;
// Render empty container - content set in firstUpdated
return html`<div class="wysiwyg-block-container"></div>`;
}
firstUpdated(): void {
const container = this.shadowRoot?.querySelector('.wysiwyg-block-container');
if (container && this.block) {
container.innerHTML = this.renderBlockContent();
}
}
```
### Critical Pattern
- NEVER use reactive properties that would trigger re-renders during typing
- Use `shouldUpdate()` to prevent unnecessary renders
- Manage content updates imperatively through event handlers
## 2. Shadow DOM Selection Handling
### Problem
The Web Selection API doesn't work across Shadow DOM boundaries by default. This broke:
- Text selection
- Cursor position tracking
- Formatting detection
- Content splitting for Enter key
### Solution
Use the `getComposedRanges` API with all relevant shadow roots:
```typescript
// From paragraph.block.ts
const wysiwygBlock = element.closest('dees-wysiwyg-block');
const parentComponent = wysiwygBlock?.closest('dees-input-wysiwyg');
const parentShadowRoot = parentComponent?.shadowRoot;
const blockShadowRoot = (wysiwygBlock as any)?.shadowRoot;
const shadowRoots: ShadowRoot[] = [];
if (parentShadowRoot) shadowRoots.push(parentShadowRoot);
if (blockShadowRoot) shadowRoots.push(blockShadowRoot);
const selectionInfo = WysiwygSelection.getSelectionInfo(...shadowRoots);
```
### Critical Pattern
- ALWAYS collect all shadow roots in the hierarchy
- Use `WysiwygSelection` utility methods that handle shadow DOM
- Never use raw `window.getSelection()` without shadow root context
## 3. Cursor Position Tracking
### Problem
Cursor position would be lost during various operations, making it impossible to:
- Split content at the right position for Enter key
- Restore cursor after operations
- Track position for formatting
### Solution
Track cursor position through multiple events and maintain `lastKnownCursorPosition`:
```typescript
// Track on every relevant event
private lastKnownCursorPosition: number = 0;
// In event handlers:
const pos = this.getCursorPosition(element);
if (pos !== null) {
this.lastKnownCursorPosition = pos;
}
// Fallback when selection not available:
if (!selectionInfo && this.lastKnownCursorPosition !== null) {
// Use last known position
}
```
### Critical Events to Track
- `input` - After text changes
- `keydown` - Before key press
- `keyup` - After key press
- `mouseup` - After mouse selection
- `click` - With setTimeout(0) for browser to set cursor
## 4. Content Splitting for Enter Key
### Problem
Splitting content at cursor position while preserving HTML formatting was complex due to:
- Need to preserve formatting tags
- Shadow DOM complications
- Cursor position accuracy
### Solution
Use Range API to split content while preserving HTML:
```typescript
getSplitContent(): { before: string; after: string } | null {
// Create ranges for before and after cursor
const beforeRange = document.createRange();
const afterRange = document.createRange();
beforeRange.setStart(element, 0);
beforeRange.setEnd(selectionInfo.startContainer, selectionInfo.startOffset);
afterRange.setStart(selectionInfo.startContainer, selectionInfo.startOffset);
afterRange.setEnd(element, element.childNodes.length);
// Extract HTML content
const beforeFragment = beforeRange.cloneContents();
const afterFragment = afterRange.cloneContents();
// Convert to HTML strings
const tempDiv = document.createElement('div');
tempDiv.appendChild(beforeFragment);
const beforeHtml = tempDiv.innerHTML;
tempDiv.innerHTML = '';
tempDiv.appendChild(afterFragment);
const afterHtml = tempDiv.innerHTML;
return { before: beforeHtml, after: afterHtml };
}
```
## 5. Focus Management
### Problem
Focus would be lost or not properly set due to:
- Timing issues with DOM updates
- Shadow DOM complications
- Browser inconsistencies
### Solution
Use defensive focus management with fallbacks:
```typescript
focus(element: HTMLElement): void {
const block = element.querySelector('.block');
if (!block) return;
// Ensure focusable
if (!block.hasAttribute('contenteditable')) {
block.setAttribute('contenteditable', 'true');
}
block.focus();
// Fallback with microtask if focus failed
if (document.activeElement !== block && element.shadowRoot?.activeElement !== block) {
Promise.resolve().then(() => {
block.focus();
});
}
}
```
## 6. Selection Event Handling for Formatting
### Problem
Need to show formatting menu when text is selected, but selection events don't bubble across Shadow DOM.
### Solution
Dispatch custom events with selection information:
```typescript
// Listen for selection changes
document.addEventListener('selectionchange', () => {
const selectionInfo = WysiwygSelection.getSelectionInfo(...shadowRoots);
if (selectedText !== this.lastSelectedText) {
const range = WysiwygSelection.createRangeFromInfo(selectionInfo);
const rect = range.getBoundingClientRect();
// Dispatch custom event
this.dispatchSelectionEvent(element, {
text: selectedText,
blockId: block.id,
range: range,
rect: rect,
hasSelection: true
});
}
});
// Custom event bubbles through Shadow DOM
const event = new CustomEvent('block-text-selected', {
detail,
bubbles: true,
composed: true
});
```
## 7. IME (Input Method Editor) Support
### Problem
Composition events for non-Latin input methods would break without proper handling.
### Solution
Track composition state and handle events:
```typescript
// In dees-input-wysiwyg.ts
public isComposing: boolean = false;
// In block handlers
element.addEventListener('compositionstart', () => {
handlers.onCompositionStart(); // Sets isComposing = true
});
element.addEventListener('compositionend', () => {
handlers.onCompositionEnd(); // Sets isComposing = false
});
// Don't process certain operations during composition
if (this.isComposing) return;
```
## 8. Programmatic Rendering
### Problem
Lit's declarative rendering would cause focus loss and performance issues with many blocks.
### Solution
Render blocks programmatically:
```typescript
public renderBlocksProgrammatically() {
if (!this.editorContentRef) return;
// Clear existing blocks
this.editorContentRef.innerHTML = '';
// Create and append block elements
this.blocks.forEach(block => {
const blockWrapper = this.createBlockElement(block);
this.editorContentRef.appendChild(blockWrapper);
});
}
```
## 9. Block Handler Architecture Requirements
When creating new block handlers, they MUST:
1. **Implement all cursor/selection methods** even if not applicable
2. **Use Shadow DOM-aware selection utilities**
3. **Track cursor position through events**
4. **Handle focus with fallbacks**
5. **Preserve HTML content when getting/setting**
6. **Dispatch selection events for formatting**
7. **Support IME composition events**
8. **Clean up event listeners on disconnect**
## 10. Testing Considerations
### webhelpers.fixture() Issue
The test helper `webhelpers.fixture()` triggers property changes during initialization that can cause null reference errors. Always:
1. Check for null/undefined before accessing nested properties
2. Set required properties in specific order when testing
3. Consider manual element creation for complex test scenarios
## Summary
These patterns represent hours of debugging and problem-solving. When refactoring:
1. **NEVER** remove static rendering approach
2. **ALWAYS** use Shadow DOM-aware selection utilities
3. **MAINTAIN** cursor position tracking through all events
4. **PRESERVE** the complex content splitting logic
5. **KEEP** all focus management fallbacks
6. **ENSURE** selection events bubble through Shadow DOM
7. **SUPPORT** IME composition events
8. **TEST** thoroughly with actual typing, not just unit tests
Any changes that break these patterns will result in a degraded user experience that took significant effort to achieve.

View File

@ -0,0 +1,44 @@
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 {
shadowRoot: ShadowRoot;
component: any; // Reference to the wysiwyg-block component
}
export interface IBlockHandler {
type: string;
render(block: IBlock, isSelected: boolean): string;
setup(element: HTMLElement, block: IBlock, handlers: IBlockEventHandlers): void;
getStyles(): string;
getPlaceholder?(): string;
// Optional methods for editable blocks - now with context
getContent?(element: HTMLElement, context?: IBlockContext): string;
setContent?(element: HTMLElement, content: string, context?: IBlockContext): void;
getCursorPosition?(element: HTMLElement, context?: IBlockContext): number | null;
setCursorToStart?(element: HTMLElement, context?: IBlockContext): void;
setCursorToEnd?(element: HTMLElement, context?: IBlockContext): void;
focus?(element: HTMLElement, context?: IBlockContext): void;
focusWithCursor?(element: HTMLElement, position: 'start' | 'end' | number, context?: IBlockContext): void;
getSplitContent?(element: HTMLElement, context?: IBlockContext): { before: string; after: string } | null;
}
export abstract class BaseBlockHandler implements IBlockHandler {
abstract type: string;
abstract render(block: IBlock, isSelected: boolean): string;
// Default implementation for common setup
setup(element: HTMLElement, block: IBlock, handlers: IBlockEventHandlers): void {
// Common setup logic
}
// Common styles can be defined here
getStyles(): string {
return '';
}
}

View File

@ -0,0 +1,17 @@
import type { IBlockHandler } from './block.base.js';
export class BlockRegistry {
private static handlers = new Map<string, IBlockHandler>();
static register(type: string, handler: IBlockHandler): void {
this.handlers.set(type, handler);
}
static getHandler(type: string): IBlockHandler | undefined {
return this.handlers.get(type);
}
static getAllTypes(): string[] {
return Array.from(this.handlers.keys());
}
}

View File

@ -0,0 +1,64 @@
/**
* Common styles shared across all block types
*/
export const commonBlockStyles = `
/* Common block spacing and layout */
/* TODO: Extract common spacing from existing blocks */
/* Common focus states */
/* TODO: Extract common focus styles */
/* Common selected states */
/* TODO: Extract common selection styles */
/* Common hover states */
/* TODO: Extract common hover styles */
/* Common transition effects */
/* TODO: Extract common transitions */
/* Common placeholder styles */
/* TODO: Extract common placeholder styles */
/* Common error states */
/* TODO: Extract common error styles */
/* Common loading states */
/* TODO: Extract common loading styles */
`;
/**
* Helper function to generate consistent block classes
*/
export const getBlockClasses = (
type: string,
isSelected: boolean,
additionalClasses: string[] = []
): string => {
const classes = ['block', type];
if (isSelected) {
classes.push('selected');
}
classes.push(...additionalClasses);
return classes.join(' ');
};
/**
* Helper function to generate consistent data attributes
*/
export const getBlockDataAttributes = (
blockId: string,
blockType: string,
additionalAttributes: Record<string, string> = {}
): string => {
const attributes = {
'data-block-id': blockId,
'data-block-type': blockType,
...additionalAttributes
};
return Object.entries(attributes)
.map(([key, value]) => `${key}="${value}"`)
.join(' ');
};

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