Compare commits
41 Commits
Author | SHA1 | Date | |
---|---|---|---|
49ad998b2c | |||
5066681b3a | |||
ee22879c00 | |||
9b0ff2d856 | |||
7e14645ed7 | |||
811737adcd | |||
7b6c135cd3 | |||
46065b2424 | |||
e76a6c3632 | |||
896bc2bbb1 | |||
296d254ba2 | |||
ecad05098f | |||
956964f5b9 | |||
ed73e16bbb | |||
7817b4a440 | |||
03f25b7f10 | |||
24957f02d4 | |||
fe3cd0591f | |||
56f5f5887b | |||
2e0bf26301 | |||
3d7f5253e8 | |||
669f12e822 | |||
8b870a8e46 | |||
9148f0595a | |||
ea7da1c9b9 | |||
3e81f54e99 | |||
65aa9f3c06 | |||
82ebd9c556 | |||
50aa071e2e | |||
807e1ff733 | |||
4146a348ab | |||
bd10b4e64d | |||
243ecddd42 | |||
d7b690621e | |||
60951330d1 | |||
7095197d07 | |||
3ee48e80ad | |||
00ad2b0563 | |||
a57005a49b | |||
d67a66662d | |||
c75c5bcd3b |
174
CLAUDE.md
Normal file
174
CLAUDE.md
Normal 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
|
88
changelog.md
88
changelog.md
@ -1,5 +1,93 @@
|
||||
# Changelog
|
||||
|
||||
## 2025-06-29 - 1.10.10 - improve(dees-dashboardgrid, dees-input-wysiwyg)
|
||||
Enhanced dashboard grid component with advanced spacing and layout features inspired by gridstack.js
|
||||
|
||||
Dashboard Grid improvements:
|
||||
- Improved margin system supporting uniform or individual margins (top, right, bottom, left)
|
||||
- Added collision detection to prevent widget overlap during drag operations
|
||||
- Implemented auto-positioning for new widgets to find first available space
|
||||
- Added compact() method to eliminate gaps and compress layout vertically or horizontally
|
||||
- Enhanced resize constraints with minW, maxW, minH, maxH support
|
||||
- Added optional grid lines visualization for better layout understanding
|
||||
- Improved resize handles with better visibility and hover states
|
||||
- Added RTL (right-to-left) layout support
|
||||
- Implemented cellHeightUnit option supporting 'px', 'em', 'rem', or 'auto' (square cells)
|
||||
- Added configurable animation with enableAnimation property
|
||||
- Enhanced demo with interactive controls for testing all features
|
||||
- Better calculation of widget positions accounting for margins between cells
|
||||
- Added findAvailablePosition() for intelligent widget placement
|
||||
- Improved drag and resize calculations for pixel-perfect positioning
|
||||
|
||||
WYSIWYG editor drag and drop fixes:
|
||||
- Fixed drop indicator positioning to properly account for block margins
|
||||
- Added defensive checks in drag event handlers to prevent potential crashes
|
||||
- Improved updateBlockPositions with null checks and error handling
|
||||
- Updated drop indicator calculation to use simplified margin approach
|
||||
- Fixed drop indicator height to match the exact space occupied by dragged blocks
|
||||
- Improved drop indicator positioning algorithm to accurately show where blocks will land
|
||||
- Simplified visual block position calculations accounting for CSS transforms
|
||||
- Enhanced margin calculation to use correct values based on block type (16px for paragraphs, 24px for headings, 20px for code/quotes)
|
||||
- Fixed index calculation issue when dragging blocks downward by adjusting target index for excluded dragged block
|
||||
|
||||
## 2025-06-28 - 1.10.9 - feat(dees-dashboardgrid)
|
||||
Add new dashboard grid component with drag-and-drop and resize capabilities
|
||||
|
||||
- Created dees-dashboardgrid component for building flexible dashboard layouts
|
||||
- Features drag-and-drop functionality for rearranging widgets
|
||||
- Includes resize handles for adjusting widget dimensions
|
||||
- Supports configurable grid properties (columns, cell height, gap)
|
||||
- Provides widget locking and editable mode controls
|
||||
- Styled with shadcn design principles
|
||||
- No external dependencies - built with native browser APIs
|
||||
- Emits events for widget movements and resizes
|
||||
- Includes comprehensive demo with sample dashboard widgets
|
||||
|
||||
## 2025-06-27 - 1.10.8 - feat(ui-components)
|
||||
Update multiple components with shadcn-aligned styling and improved animations
|
||||
|
||||
- Updated dees-modal with shadcn colors, borders, and subtle shadows
|
||||
- Updated dees-chips with shadcn styling and fixed selection logic bug
|
||||
- Updated dees-dataview-codebox with shadcn syntax highlighting colors and responsive label layout
|
||||
- Updated dees-input-multitoggle with transparent blue indicator and smooth animations
|
||||
- Updated dees-appui-tabs with animated sliding indicator for both horizontal and vertical layouts
|
||||
- Fixed indicator positioning to be perfectly centered on tab content
|
||||
- Indicator width is content width + 8px for minimal visual padding
|
||||
- Fixed tab content centering by using consistent padding (12px → 16px on all sides)
|
||||
- Fixed icon rendering by correcting property name from .iconName to .icon
|
||||
- Added visual separators between tabs for better distinction
|
||||
- Added subtle hover backgrounds for improved interactivity
|
||||
- Refactored tabs component code for better maintainability and elegance
|
||||
- Updated dees-appui-activitylog with shadcn-aligned styling:
|
||||
- Updated background and text colors to match shadcn palette
|
||||
- Enhanced topbar with better spacing and typography
|
||||
- Improved activity entries with subtle hover states and better spacing
|
||||
- Added activity type icons with color-coded backgrounds (login, logout, view, create, update)
|
||||
- Added date separators ("Today", "Yesterday") for better temporal organization
|
||||
- Enhanced streaming indicators with animated pulse effect
|
||||
- Redesigned searchbox with modern input styling, search icon, and focus states
|
||||
- Added custom scrollbar styling for consistency
|
||||
- Updated timestamps to be more subtle with tabular number formatting
|
||||
- Refined shadow effects for better visual hierarchy
|
||||
- Added subtle box shadow to component for depth
|
||||
- Added fade-in animation for new activity entries
|
||||
- Improved user name highlighting with better typography
|
||||
- Updated context menu with more relevant actions
|
||||
- Improved overall spacing and visual consistency across components
|
||||
|
||||
## 2025-06-27 - 1.10.1 - fix(modal)
|
||||
Improve modal overscroll behavior by adding 'overscroll-behavior: contain' to content container
|
||||
|
||||
- Added 'overscroll-behavior: contain' to .modal .content to ensure proper scroll containment
|
||||
- Applied overscroll-behavior in modal container for enhanced responsiveness on mobile and desktop
|
||||
|
||||
## 2025-06-26 - 1.10.0 - feat(dees-modal)
|
||||
Add mobileFullscreen option to modals for full-screen mobile support
|
||||
|
||||
- Introduced a new boolean property 'mobileFullscreen' in ts_web/elements/dees-modal.ts
|
||||
- Updated modal CSS under the media query to apply 'mobile-fullscreen' class, allowing full viewport modals on mobile devices
|
||||
- Extended modal style rules to include adjustments for margin, border-radius, and maximum heights on smaller screens
|
||||
|
||||
## 2025-06-26 - 1.9.9 - fix(dees-input-multitoggle, dees-input-typelist)
|
||||
Replace dynamic import with static import for demo functions in dees-input-multitoggle and dees-input-typelist
|
||||
|
||||
|
@ -1,13 +1,13 @@
|
||||
{
|
||||
"name": "@design.estate/dees-catalog",
|
||||
"version": "1.9.9",
|
||||
"version": "1.10.9",
|
||||
"private": false,
|
||||
"description": "A comprehensive library that provides dynamic web components for building sophisticated and modern web applications using JavaScript and TypeScript.",
|
||||
"main": "dist_ts_web/index.js",
|
||||
"typings": "dist_ts_web/index.d.ts",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"test": "tstest test/ --web --verbose --timeout 30",
|
||||
"test": "tstest test/ --web --verbose --timeout 30 --logfile",
|
||||
"build": "tsbuild tsfolders --allowimplicitany && tsbundle element --production --bundler esbuild",
|
||||
"watch": "tswatch element",
|
||||
"buildDocs": "tsdoc"
|
||||
@ -17,7 +17,7 @@
|
||||
"dependencies": {
|
||||
"@design.estate/dees-domtools": "^2.3.3",
|
||||
"@design.estate/dees-element": "^2.0.45",
|
||||
"@design.estate/dees-wcctools": "^1.0.101",
|
||||
"@design.estate/dees-wcctools": "^1.1.0",
|
||||
"@fortawesome/fontawesome-svg-core": "^6.7.2",
|
||||
"@fortawesome/free-brands-svg-icons": "^6.7.2",
|
||||
"@fortawesome/free-regular-svg-icons": "^6.7.2",
|
||||
@ -36,7 +36,7 @@
|
||||
"apexcharts": "^4.7.0",
|
||||
"highlight.js": "11.11.1",
|
||||
"ibantools": "^4.5.1",
|
||||
"lucide": "^0.523.0",
|
||||
"lucide": "^0.525.0",
|
||||
"monaco-editor": "^0.52.2",
|
||||
"pdfjs-dist": "^4.10.38",
|
||||
"xterm": "^5.3.0",
|
||||
|
22
pnpm-lock.yaml
generated
22
pnpm-lock.yaml
generated
@ -15,8 +15,8 @@ importers:
|
||||
specifier: ^2.0.45
|
||||
version: 2.0.45
|
||||
'@design.estate/dees-wcctools':
|
||||
specifier: ^1.0.101
|
||||
version: 1.0.101
|
||||
specifier: ^1.1.0
|
||||
version: 1.1.0
|
||||
'@fortawesome/fontawesome-svg-core':
|
||||
specifier: ^6.7.2
|
||||
version: 6.7.2
|
||||
@ -72,8 +72,8 @@ importers:
|
||||
specifier: ^4.5.1
|
||||
version: 4.5.1
|
||||
lucide:
|
||||
specifier: ^0.523.0
|
||||
version: 0.523.0
|
||||
specifier: ^0.525.0
|
||||
version: 0.525.0
|
||||
monaco-editor:
|
||||
specifier: ^0.52.2
|
||||
version: 0.52.2
|
||||
@ -323,8 +323,8 @@ packages:
|
||||
'@design.estate/dees-element@2.0.45':
|
||||
resolution: {integrity: sha512-dj8nOOtfwvqEtQceTXQQ5IEy75HIFZ+iuDxPeIynLedYpxtHPsxFrHW8IQ7/ad9MNvVO0kTnlwUOmkjylul+DA==}
|
||||
|
||||
'@design.estate/dees-wcctools@1.0.101':
|
||||
resolution: {integrity: sha512-6j2kGORf7egRkHGwQUNuxSdTe2+6y7eX3+dVomBLeWczH30KhPi1jJKINSt/MqkpB5i7o3kQwvvWA6JYBOjXcg==}
|
||||
'@design.estate/dees-wcctools@1.1.0':
|
||||
resolution: {integrity: sha512-eniG2JsGgcVXQLkSE6M7azJ7av/UeTvvzhE6s3JbcIieHd589SCxQqF+BhlOyKqzJQ1n5jJ7KKdmhvQU5bbdtg==}
|
||||
|
||||
'@emnapi/core@1.4.3':
|
||||
resolution: {integrity: sha512-4m62DuCE07lw01soJwPiBGC0nAww0Q+RY70VZ+n49yDIO13yyinhbWCeNnaob0lakDtWQzSdtNWzJeOJt2ma+g==}
|
||||
@ -3481,8 +3481,8 @@ packages:
|
||||
resolution: {integrity: sha512-MhWWlVnuab1RG5/zMRRcVGXZLCXrZTgfwMikgzCegsPnG62yDQo5JnqKkrK4jO5iKqDAZGItAqN5CtKBCBWRUA==}
|
||||
engines: {node: '>=16.14'}
|
||||
|
||||
lucide@0.523.0:
|
||||
resolution: {integrity: sha512-tiIp5xEP4kpeulfT1J+a/NEaIZGT1k6RyMS3evQWfHRhJvR8uTat/+lN4wnX5qIexOwN02BhmcyMHBNwt+jkLA==}
|
||||
lucide@0.525.0:
|
||||
resolution: {integrity: sha512-sfehWlaE/7NVkcEQ4T9JD3eID8RNMIGJBBUq9wF3UFiJIrcMKRbU3g1KGfDk4svcW7yw8BtDLXaXo02scDtUYQ==}
|
||||
|
||||
make-dir@3.1.0:
|
||||
resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==}
|
||||
@ -5588,7 +5588,7 @@ snapshots:
|
||||
- supports-color
|
||||
- vue
|
||||
|
||||
'@design.estate/dees-wcctools@1.0.101':
|
||||
'@design.estate/dees-wcctools@1.1.0':
|
||||
dependencies:
|
||||
'@design.estate/dees-domtools': 2.3.3
|
||||
'@design.estate/dees-element': 2.0.45
|
||||
@ -5905,10 +5905,8 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- '@nuxt/kit'
|
||||
- '@swc/helpers'
|
||||
- bufferutil
|
||||
- react
|
||||
- supports-color
|
||||
- utf-8-validate
|
||||
- vue
|
||||
|
||||
'@hapi/bourne@3.0.0': {}
|
||||
@ -9564,7 +9562,7 @@ snapshots:
|
||||
|
||||
lru-cache@8.0.5: {}
|
||||
|
||||
lucide@0.523.0: {}
|
||||
lucide@0.525.0: {}
|
||||
|
||||
make-dir@3.1.0:
|
||||
dependencies:
|
||||
|
@ -529,9 +529,9 @@ Base container component for application layout structure with integrated appbar
|
||||
|
||||
// Main menu configuration (left sidebar)
|
||||
.mainmenuTabs=${[
|
||||
{ key: 'dashboard', iconName: 'home', action: () => {} },
|
||||
{ key: 'projects', iconName: 'folder', action: () => {} },
|
||||
{ key: 'settings', iconName: 'cog', action: () => {} }
|
||||
{ key: 'dashboard', iconName: 'lucide:home', action: () => {} },
|
||||
{ key: 'projects', iconName: 'lucide:folder', action: () => {} },
|
||||
{ key: 'settings', iconName: 'lucide:settings', action: () => {} }
|
||||
]}
|
||||
.mainmenuSelectedTab=${selectedTab}
|
||||
|
||||
@ -545,7 +545,7 @@ Base container component for application layout structure with integrated appbar
|
||||
|
||||
// Main content tabs
|
||||
.maincontentTabs=${[
|
||||
{ key: 'tab1', iconName: 'file', action: () => {} }
|
||||
{ key: 'tab1', iconName: 'lucide:file', action: () => {} }
|
||||
]}
|
||||
|
||||
// Event handlers
|
||||
|
72
test-output.log
Normal file
72
test-output.log
Normal 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
|
||||
|
||||
[38;5;231m
|
||||
🔍 Test Discovery[0m
|
||||
[38;5;231m Mode: file[0m
|
||||
[38;5;231m Pattern: test/test.tabs-indicator.browser.ts[0m
|
||||
[38;5;113m Found: 1 test file(s)[0m
|
||||
[38;5;33m
|
||||
▶️ test/test.tabs-indicator.browser.ts (1/1)[0m
|
||||
[38;5;231m Runtime: chromium[0m
|
||||
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
|
||||
[38;5;231m [38;5;116mTest starting: tabs indicator positioning debug[0m[0m
|
||||
[38;5;231m !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!![0m
|
||||
[38;5;231m Using globalThis.tapPromise[0m
|
||||
[38;5;231m !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!![0m
|
||||
connection ended
|
||||
removed connection. 0 sockets remaining.
|
||||
[38;5;33m=> [0m Stopped [38;5;215mtest/test.tabs-indicator.browser.ts[0m chromium instance and server.
|
||||
[38;5;196m
|
||||
⚠️ Error[0m
|
||||
[38;5;196m Only 0 out of 1 completed![0m
|
||||
[38;5;196m
|
||||
⚠️ Error[0m
|
||||
[38;5;196m The amount of received tests and expectedTests is unequal! Therefore the testfile failed[0m
|
||||
[38;5;196m Summary: -1 passed, 1 failed of 0 tests in 2.7s[0m
|
||||
[38;5;231m
|
||||
📊 Test Summary[0m
|
||||
[38;5;231m┌────────────────────────────────┐[0m
|
||||
[38;5;231m│ Total Files: 1 │[0m
|
||||
[38;5;231m│ Total Tests: 0 │[0m
|
||||
[38;5;113m│ Passed: 0 │[0m
|
||||
[38;5;113m│ Failed: 0 │[0m
|
||||
[38;5;231m│ Duration: 4.2s │[0m
|
||||
[38;5;231m└────────────────────────────────┘[0m
|
||||
[38;5;116m
|
||||
⏱️ Performance Metrics:[0m
|
||||
[38;5;231m Average per test: 0ms[0m
|
||||
[38;5;113m
|
||||
ALL TESTS PASSED! 🎉[0m
|
||||
Exited NOT OK!
|
||||
ELIFECYCLE Test failed. See above for more details.
|
35
test/test.contextmenu-demo.browser.ts
Normal file
35
test/test.contextmenu-demo.browser.ts
Normal 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();
|
93
test/test.contextmenu-nested-close.browser.ts
Normal file
93
test/test.contextmenu-nested-close.browser.ts
Normal 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();
|
71
test/test.contextmenu-shadowdom.browser.ts
Normal file
71
test/test.contextmenu-shadowdom.browser.ts
Normal 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();
|
77
test/test.contextmenu.browser.ts
Normal file
77
test/test.contextmenu.browser.ts
Normal 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();
|
146
test/test.tabs-indicator.browser.ts
Normal file
146
test/test.tabs-indicator.browser.ts
Normal 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();
|
85
test/test.wysiwyg-blockmovement.browser.ts
Normal file
85
test/test.wysiwyg-blockmovement.browser.ts
Normal file
@ -0,0 +1,85 @@
|
||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||
import { DeesInputWysiwyg } from '../ts_web/elements/wysiwyg/dees-input-wysiwyg.js';
|
||||
|
||||
// Initialize the element
|
||||
DeesInputWysiwyg;
|
||||
|
||||
tap.test('wysiwyg block movement during drag', async () => {
|
||||
const element = document.createElement('dees-input-wysiwyg');
|
||||
document.body.appendChild(element);
|
||||
|
||||
await element.updateComplete;
|
||||
|
||||
// Set initial content
|
||||
element.blocks = [
|
||||
{ id: 'block1', type: 'paragraph', content: 'Block 1' },
|
||||
{ id: 'block2', type: 'paragraph', content: 'Block 2' },
|
||||
{ id: 'block3', type: 'paragraph', content: 'Block 3' },
|
||||
];
|
||||
element.renderBlocksProgrammatically();
|
||||
|
||||
await element.updateComplete;
|
||||
|
||||
const editorContent = element.shadowRoot!.querySelector('.editor-content') as HTMLDivElement;
|
||||
const block1 = editorContent.querySelector('[data-block-id="block1"]') as HTMLElement;
|
||||
|
||||
// Start dragging block 1
|
||||
const mockDragEvent = {
|
||||
dataTransfer: {
|
||||
effectAllowed: '',
|
||||
setData: () => {},
|
||||
setDragImage: () => {}
|
||||
},
|
||||
clientY: 50,
|
||||
preventDefault: () => {},
|
||||
} as any;
|
||||
|
||||
element.dragDropHandler.handleDragStart(mockDragEvent, element.blocks[0]);
|
||||
|
||||
// Wait for dragging class
|
||||
await new Promise(resolve => setTimeout(resolve, 20));
|
||||
|
||||
// Verify drag state
|
||||
expect(element.dragDropHandler.dragState.draggedBlockId).toEqual('block1');
|
||||
|
||||
// Check that drag height was calculated
|
||||
console.log('Checking drag height...');
|
||||
const dragHandler = element.dragDropHandler as any;
|
||||
console.log('draggedBlockHeight:', dragHandler.draggedBlockHeight);
|
||||
console.log('draggedBlockContentHeight:', dragHandler.draggedBlockContentHeight);
|
||||
|
||||
// Manually call updateBlockPositions to simulate drag movement
|
||||
console.log('Simulating drag movement...');
|
||||
const updateBlockPositions = dragHandler.updateBlockPositions.bind(dragHandler);
|
||||
|
||||
// Simulate dragging down past block 2
|
||||
const block2 = editorContent.querySelector('[data-block-id="block2"]') as HTMLElement;
|
||||
const block2Rect = block2.getBoundingClientRect();
|
||||
const dragToY = block2Rect.bottom + 10;
|
||||
|
||||
console.log('Dragging to Y position:', dragToY);
|
||||
updateBlockPositions(dragToY);
|
||||
|
||||
// Check if blocks have moved
|
||||
await new Promise(resolve => setTimeout(resolve, 50));
|
||||
|
||||
const blocks = Array.from(editorContent.querySelectorAll('.block-wrapper'));
|
||||
console.log('Block states after drag:');
|
||||
blocks.forEach((block, i) => {
|
||||
const classes = block.className;
|
||||
const offset = (block as HTMLElement).style.getPropertyValue('--drag-offset');
|
||||
console.log(`Block ${i}: classes="${classes}", offset="${offset}"`);
|
||||
});
|
||||
|
||||
// Check that at least one block has move class
|
||||
const movedUpBlocks = editorContent.querySelectorAll('.block-wrapper.move-up');
|
||||
const movedDownBlocks = editorContent.querySelectorAll('.block-wrapper.move-down');
|
||||
console.log('Moved up blocks:', movedUpBlocks.length);
|
||||
console.log('Moved down blocks:', movedDownBlocks.length);
|
||||
|
||||
// Clean up
|
||||
element.dragDropHandler.handleDragEnd();
|
||||
document.body.removeChild(element);
|
||||
});
|
||||
|
||||
tap.start();
|
98
test/test.wysiwyg-blocktype-change.browser.ts
Normal file
98
test/test.wysiwyg-blocktype-change.browser.ts
Normal 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();
|
68
test/test.wysiwyg-contextmenu.browser.ts
Normal file
68
test/test.wysiwyg-contextmenu.browser.ts
Normal 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();
|
95
test/test.wysiwyg-dragdrop-simple.browser.ts
Normal file
95
test/test.wysiwyg-dragdrop-simple.browser.ts
Normal file
@ -0,0 +1,95 @@
|
||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||
import { DeesInputWysiwyg } from '../ts_web/elements/wysiwyg/dees-input-wysiwyg.js';
|
||||
|
||||
// Initialize the element
|
||||
DeesInputWysiwyg;
|
||||
|
||||
tap.test('wysiwyg drag handler initialization', async () => {
|
||||
const element = document.createElement('dees-input-wysiwyg');
|
||||
document.body.appendChild(element);
|
||||
|
||||
// Wait for element to be ready
|
||||
await element.updateComplete;
|
||||
|
||||
// Check that drag handler is initialized
|
||||
expect(element.dragDropHandler).toBeTruthy();
|
||||
|
||||
// Set initial content with multiple blocks
|
||||
element.blocks = [
|
||||
{ id: 'block1', type: 'paragraph', content: 'First paragraph' },
|
||||
{ id: 'block2', type: 'paragraph', content: 'Second paragraph' },
|
||||
];
|
||||
element.renderBlocksProgrammatically();
|
||||
|
||||
await element.updateComplete;
|
||||
|
||||
// Check that editor content ref exists
|
||||
console.log('editorContentRef:', element.editorContentRef);
|
||||
expect(element.editorContentRef).toBeTruthy();
|
||||
|
||||
// Check that blocks are rendered
|
||||
const blockWrappers = element.shadowRoot!.querySelectorAll('.block-wrapper');
|
||||
console.log('Number of block wrappers:', blockWrappers.length);
|
||||
expect(blockWrappers.length).toEqual(2);
|
||||
|
||||
// Check drag handles
|
||||
const dragHandles = element.shadowRoot!.querySelectorAll('.drag-handle');
|
||||
console.log('Number of drag handles:', dragHandles.length);
|
||||
expect(dragHandles.length).toEqual(2);
|
||||
|
||||
// Clean up
|
||||
document.body.removeChild(element);
|
||||
});
|
||||
|
||||
tap.test('wysiwyg drag start behavior', async () => {
|
||||
const element = document.createElement('dees-input-wysiwyg');
|
||||
document.body.appendChild(element);
|
||||
|
||||
await element.updateComplete;
|
||||
|
||||
// Set initial content
|
||||
element.blocks = [
|
||||
{ id: 'block1', type: 'paragraph', content: 'Test block' },
|
||||
];
|
||||
element.renderBlocksProgrammatically();
|
||||
|
||||
await element.updateComplete;
|
||||
|
||||
const dragHandle = element.shadowRoot!.querySelector('.drag-handle') as HTMLElement;
|
||||
expect(dragHandle).toBeTruthy();
|
||||
|
||||
// Check that drag handle has draggable attribute
|
||||
console.log('Drag handle draggable:', dragHandle.draggable);
|
||||
expect(dragHandle.draggable).toBeTrue();
|
||||
|
||||
// Test drag handler state before drag
|
||||
console.log('Initial drag state:', element.dragDropHandler.dragState);
|
||||
expect(element.dragDropHandler.dragState.draggedBlockId).toBeNull();
|
||||
|
||||
// Try to manually call handleDragStart
|
||||
const mockDragEvent = {
|
||||
dataTransfer: {
|
||||
effectAllowed: '',
|
||||
setData: (type: string, data: string) => {
|
||||
console.log('setData called with:', type, data);
|
||||
},
|
||||
setDragImage: (img: any, x: number, y: number) => {
|
||||
console.log('setDragImage called');
|
||||
}
|
||||
},
|
||||
clientY: 100,
|
||||
preventDefault: () => {},
|
||||
} as any;
|
||||
|
||||
element.dragDropHandler.handleDragStart(mockDragEvent, element.blocks[0]);
|
||||
|
||||
// Check drag state after drag start
|
||||
console.log('Drag state after start:', element.dragDropHandler.dragState);
|
||||
expect(element.dragDropHandler.dragState.draggedBlockId).toEqual('block1');
|
||||
|
||||
// Clean up
|
||||
element.dragDropHandler.handleDragEnd();
|
||||
document.body.removeChild(element);
|
||||
});
|
||||
|
||||
tap.start();
|
133
test/test.wysiwyg-dragdrop-visual.browser.ts
Normal file
133
test/test.wysiwyg-dragdrop-visual.browser.ts
Normal file
@ -0,0 +1,133 @@
|
||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||
import { DeesInputWysiwyg } from '../ts_web/elements/wysiwyg/dees-input-wysiwyg.js';
|
||||
|
||||
// Initialize the element
|
||||
DeesInputWysiwyg;
|
||||
|
||||
tap.test('wysiwyg drag visual feedback - block movement', async () => {
|
||||
const element = document.createElement('dees-input-wysiwyg');
|
||||
document.body.appendChild(element);
|
||||
|
||||
await element.updateComplete;
|
||||
|
||||
// Set initial content
|
||||
element.blocks = [
|
||||
{ id: 'block1', type: 'paragraph', content: 'Block 1' },
|
||||
{ id: 'block2', type: 'paragraph', content: 'Block 2' },
|
||||
{ id: 'block3', type: 'paragraph', content: 'Block 3' },
|
||||
];
|
||||
element.renderBlocksProgrammatically();
|
||||
|
||||
await element.updateComplete;
|
||||
|
||||
const editorContent = element.shadowRoot!.querySelector('.editor-content') as HTMLDivElement;
|
||||
const block1 = editorContent.querySelector('[data-block-id="block1"]') as HTMLElement;
|
||||
|
||||
// Manually start drag
|
||||
const mockDragEvent = {
|
||||
dataTransfer: {
|
||||
effectAllowed: '',
|
||||
setData: (type: string, data: string) => {},
|
||||
setDragImage: (img: any, x: number, y: number) => {}
|
||||
},
|
||||
clientY: 50,
|
||||
preventDefault: () => {},
|
||||
} as any;
|
||||
|
||||
element.dragDropHandler.handleDragStart(mockDragEvent, element.blocks[0]);
|
||||
|
||||
// Wait for dragging class
|
||||
await new Promise(resolve => setTimeout(resolve, 20));
|
||||
|
||||
// Check dragging state
|
||||
console.log('Block 1 classes:', block1.className);
|
||||
console.log('Editor content classes:', editorContent.className);
|
||||
expect(block1.classList.contains('dragging')).toBeTrue();
|
||||
expect(editorContent.classList.contains('dragging')).toBeTrue();
|
||||
|
||||
// Check drop indicator exists
|
||||
const dropIndicator = editorContent.querySelector('.drop-indicator') as HTMLElement;
|
||||
console.log('Drop indicator:', dropIndicator);
|
||||
expect(dropIndicator).toBeTruthy();
|
||||
|
||||
// Test block movement calculation
|
||||
console.log('Testing updateBlockPositions...');
|
||||
|
||||
// Access private method for testing
|
||||
const updateBlockPositions = element.dragDropHandler['updateBlockPositions'].bind(element.dragDropHandler);
|
||||
|
||||
// Simulate dragging to different position
|
||||
updateBlockPositions(150); // Move down
|
||||
|
||||
// Check if blocks have move classes
|
||||
const blocks = Array.from(editorContent.querySelectorAll('.block-wrapper'));
|
||||
console.log('Block classes after move:');
|
||||
blocks.forEach((block, i) => {
|
||||
console.log(`Block ${i}:`, block.className, 'transform:', (block as HTMLElement).style.getPropertyValue('--drag-offset'));
|
||||
});
|
||||
|
||||
// Clean up
|
||||
element.dragDropHandler.handleDragEnd();
|
||||
document.body.removeChild(element);
|
||||
});
|
||||
|
||||
tap.test('wysiwyg drop indicator positioning', async () => {
|
||||
const element = document.createElement('dees-input-wysiwyg');
|
||||
document.body.appendChild(element);
|
||||
|
||||
await element.updateComplete;
|
||||
|
||||
// Set initial content
|
||||
element.blocks = [
|
||||
{ id: 'block1', type: 'paragraph', content: 'Paragraph 1' },
|
||||
{ id: 'block2', type: 'heading-2', content: 'Heading 2' },
|
||||
];
|
||||
element.renderBlocksProgrammatically();
|
||||
|
||||
await element.updateComplete;
|
||||
|
||||
const editorContent = element.shadowRoot!.querySelector('.editor-content') as HTMLDivElement;
|
||||
|
||||
// Start dragging first block
|
||||
const mockDragEvent = {
|
||||
dataTransfer: {
|
||||
effectAllowed: '',
|
||||
setData: (type: string, data: string) => {},
|
||||
setDragImage: (img: any, x: number, y: number) => {}
|
||||
},
|
||||
clientY: 50,
|
||||
preventDefault: () => {},
|
||||
} as any;
|
||||
|
||||
element.dragDropHandler.handleDragStart(mockDragEvent, element.blocks[0]);
|
||||
|
||||
// Wait for initialization
|
||||
await new Promise(resolve => setTimeout(resolve, 20));
|
||||
|
||||
// Get drop indicator
|
||||
const dropIndicator = editorContent.querySelector('.drop-indicator') as HTMLElement;
|
||||
expect(dropIndicator).toBeTruthy();
|
||||
|
||||
// Check initial display state
|
||||
console.log('Drop indicator initial display:', dropIndicator.style.display);
|
||||
|
||||
// Trigger updateBlockPositions to see drop indicator
|
||||
const updateBlockPositions = element.dragDropHandler['updateBlockPositions'].bind(element.dragDropHandler);
|
||||
updateBlockPositions(100);
|
||||
|
||||
// Check drop indicator position
|
||||
console.log('Drop indicator after update:');
|
||||
console.log('- display:', dropIndicator.style.display);
|
||||
console.log('- top:', dropIndicator.style.top);
|
||||
console.log('- height:', dropIndicator.style.height);
|
||||
|
||||
expect(dropIndicator.style.display).toEqual('block');
|
||||
expect(dropIndicator.style.top).toBeTruthy();
|
||||
expect(dropIndicator.style.height).toBeTruthy();
|
||||
|
||||
// Clean up
|
||||
element.dragDropHandler.handleDragEnd();
|
||||
document.body.removeChild(element);
|
||||
});
|
||||
|
||||
tap.start();
|
172
test/test.wysiwyg-dragdrop.browser.ts
Normal file
172
test/test.wysiwyg-dragdrop.browser.ts
Normal file
@ -0,0 +1,172 @@
|
||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||
import { DeesInputWysiwyg } from '../ts_web/elements/wysiwyg/dees-input-wysiwyg.js';
|
||||
|
||||
// Initialize the element
|
||||
DeesInputWysiwyg;
|
||||
|
||||
tap.test('wysiwyg drag and drop should work correctly', async () => {
|
||||
const element = document.createElement('dees-input-wysiwyg');
|
||||
document.body.appendChild(element);
|
||||
|
||||
// Wait for element to be ready
|
||||
await element.updateComplete;
|
||||
|
||||
// Set initial content with multiple blocks
|
||||
element.blocks = [
|
||||
{ id: 'block1', type: 'paragraph', content: 'First paragraph' },
|
||||
{ id: 'block2', type: 'heading-2', content: 'Test Heading' },
|
||||
{ id: 'block3', type: 'paragraph', content: 'Second paragraph' },
|
||||
];
|
||||
element.renderBlocksProgrammatically();
|
||||
|
||||
await element.updateComplete;
|
||||
|
||||
// Check that blocks are rendered
|
||||
const editorContent = element.shadowRoot!.querySelector('.editor-content') as HTMLDivElement;
|
||||
expect(editorContent).toBeTruthy();
|
||||
|
||||
const blockWrappers = editorContent.querySelectorAll('.block-wrapper');
|
||||
expect(blockWrappers.length).toEqual(3);
|
||||
|
||||
// Test drag handles exist for non-divider blocks
|
||||
const dragHandles = editorContent.querySelectorAll('.drag-handle');
|
||||
expect(dragHandles.length).toEqual(3);
|
||||
|
||||
// Get references to specific blocks
|
||||
const firstBlock = editorContent.querySelector('[data-block-id="block1"]') as HTMLElement;
|
||||
const secondBlock = editorContent.querySelector('[data-block-id="block2"]') as HTMLElement;
|
||||
const firstDragHandle = firstBlock.querySelector('.drag-handle') as HTMLElement;
|
||||
|
||||
expect(firstBlock).toBeTruthy();
|
||||
expect(secondBlock).toBeTruthy();
|
||||
expect(firstDragHandle).toBeTruthy();
|
||||
|
||||
// Test drag initialization
|
||||
console.log('Testing drag initialization...');
|
||||
|
||||
// Create drag event
|
||||
const dragStartEvent = new DragEvent('dragstart', {
|
||||
dataTransfer: new DataTransfer(),
|
||||
clientY: 100,
|
||||
bubbles: true
|
||||
});
|
||||
|
||||
// Simulate drag start
|
||||
firstDragHandle.dispatchEvent(dragStartEvent);
|
||||
|
||||
// Check that drag state is initialized
|
||||
expect(element.dragDropHandler.dragState.draggedBlockId).toEqual('block1');
|
||||
|
||||
// Check that dragging class is applied
|
||||
await new Promise(resolve => setTimeout(resolve, 20)); // Wait for setTimeout in drag start
|
||||
expect(firstBlock.classList.contains('dragging')).toBeTrue();
|
||||
expect(editorContent.classList.contains('dragging')).toBeTrue();
|
||||
|
||||
// Test drop indicator creation
|
||||
const dropIndicator = editorContent.querySelector('.drop-indicator');
|
||||
expect(dropIndicator).toBeTruthy();
|
||||
|
||||
// Simulate drag over
|
||||
const dragOverEvent = new DragEvent('dragover', {
|
||||
dataTransfer: new DataTransfer(),
|
||||
clientY: 200,
|
||||
bubbles: true,
|
||||
cancelable: true
|
||||
});
|
||||
|
||||
document.dispatchEvent(dragOverEvent);
|
||||
|
||||
// Check that blocks move out of the way
|
||||
console.log('Checking block movements...');
|
||||
const blocks = Array.from(editorContent.querySelectorAll('.block-wrapper'));
|
||||
const hasMovedBlocks = blocks.some(block =>
|
||||
block.classList.contains('move-up') || block.classList.contains('move-down')
|
||||
);
|
||||
|
||||
console.log('Blocks with move classes:', blocks.filter(block =>
|
||||
block.classList.contains('move-up') || block.classList.contains('move-down')
|
||||
).length);
|
||||
|
||||
// Test drag end
|
||||
const dragEndEvent = new DragEvent('dragend', {
|
||||
bubbles: true
|
||||
});
|
||||
|
||||
document.dispatchEvent(dragEndEvent);
|
||||
|
||||
// Wait for cleanup
|
||||
await new Promise(resolve => setTimeout(resolve, 150));
|
||||
|
||||
// Check that drag state is cleaned up
|
||||
expect(element.dragDropHandler.dragState.draggedBlockId).toBeNull();
|
||||
expect(firstBlock.classList.contains('dragging')).toBeFalse();
|
||||
expect(editorContent.classList.contains('dragging')).toBeFalse();
|
||||
|
||||
// Check that drop indicator is removed
|
||||
const dropIndicatorAfter = editorContent.querySelector('.drop-indicator');
|
||||
expect(dropIndicatorAfter).toBeFalsy();
|
||||
|
||||
// Clean up
|
||||
document.body.removeChild(element);
|
||||
});
|
||||
|
||||
tap.test('wysiwyg drag and drop visual feedback', async () => {
|
||||
const element = document.createElement('dees-input-wysiwyg');
|
||||
document.body.appendChild(element);
|
||||
|
||||
await element.updateComplete;
|
||||
|
||||
// Set initial content
|
||||
element.blocks = [
|
||||
{ id: 'block1', type: 'paragraph', content: 'Block 1' },
|
||||
{ id: 'block2', type: 'paragraph', content: 'Block 2' },
|
||||
{ id: 'block3', type: 'paragraph', content: 'Block 3' },
|
||||
];
|
||||
element.renderBlocksProgrammatically();
|
||||
|
||||
await element.updateComplete;
|
||||
|
||||
const editorContent = element.shadowRoot!.querySelector('.editor-content') as HTMLDivElement;
|
||||
const block1 = editorContent.querySelector('[data-block-id="block1"]') as HTMLElement;
|
||||
const dragHandle1 = block1.querySelector('.drag-handle') as HTMLElement;
|
||||
|
||||
// Start dragging block 1
|
||||
const dragStartEvent = new DragEvent('dragstart', {
|
||||
dataTransfer: new DataTransfer(),
|
||||
clientY: 50,
|
||||
bubbles: true
|
||||
});
|
||||
|
||||
dragHandle1.dispatchEvent(dragStartEvent);
|
||||
|
||||
// Wait for dragging class
|
||||
await new Promise(resolve => setTimeout(resolve, 20));
|
||||
|
||||
// Simulate dragging down
|
||||
const dragOverEvent = new DragEvent('dragover', {
|
||||
dataTransfer: new DataTransfer(),
|
||||
clientY: 150, // Move down past block 2
|
||||
bubbles: true,
|
||||
cancelable: true
|
||||
});
|
||||
|
||||
// Trigger the global drag over handler
|
||||
element.dragDropHandler['handleGlobalDragOver'](dragOverEvent);
|
||||
|
||||
// Check that transform is applied to dragged block
|
||||
const transform = block1.style.transform;
|
||||
console.log('Dragged block transform:', transform);
|
||||
expect(transform).toContain('translateY');
|
||||
|
||||
// Check drop indicator position
|
||||
const dropIndicator = editorContent.querySelector('.drop-indicator') as HTMLElement;
|
||||
if (dropIndicator) {
|
||||
const indicatorStyle = dropIndicator.style;
|
||||
console.log('Drop indicator position:', indicatorStyle.top, 'display:', indicatorStyle.display);
|
||||
}
|
||||
|
||||
// Clean up
|
||||
document.body.removeChild(element);
|
||||
});
|
||||
|
||||
tap.start();
|
124
test/test.wysiwyg-dragissue.browser.ts
Normal file
124
test/test.wysiwyg-dragissue.browser.ts
Normal file
@ -0,0 +1,124 @@
|
||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||
import { DeesInputWysiwyg } from '../ts_web/elements/wysiwyg/dees-input-wysiwyg.js';
|
||||
|
||||
// Initialize the element
|
||||
DeesInputWysiwyg;
|
||||
|
||||
tap.test('wysiwyg drag full flow without await', async () => {
|
||||
const element = document.createElement('dees-input-wysiwyg');
|
||||
document.body.appendChild(element);
|
||||
|
||||
await element.updateComplete;
|
||||
|
||||
// Set initial content
|
||||
element.blocks = [
|
||||
{ id: 'block1', type: 'paragraph', content: 'Test block' },
|
||||
];
|
||||
element.renderBlocksProgrammatically();
|
||||
|
||||
await element.updateComplete;
|
||||
|
||||
// Mock drag event
|
||||
const mockDragEvent = {
|
||||
dataTransfer: {
|
||||
effectAllowed: '',
|
||||
setData: (type: string, data: string) => {
|
||||
console.log('setData:', type, data);
|
||||
},
|
||||
setDragImage: (img: any, x: number, y: number) => {
|
||||
console.log('setDragImage');
|
||||
}
|
||||
},
|
||||
clientY: 100,
|
||||
preventDefault: () => {},
|
||||
} as any;
|
||||
|
||||
console.log('Starting drag...');
|
||||
element.dragDropHandler.handleDragStart(mockDragEvent, element.blocks[0]);
|
||||
console.log('Drag started');
|
||||
|
||||
// Check immediate state
|
||||
expect(element.dragDropHandler.dragState.draggedBlockId).toEqual('block1');
|
||||
|
||||
// Instead of await with setTimeout, use a done callback
|
||||
return new Promise<void>((resolve) => {
|
||||
console.log('Setting up delayed check...');
|
||||
|
||||
// Use regular setTimeout
|
||||
setTimeout(() => {
|
||||
console.log('In setTimeout callback');
|
||||
|
||||
try {
|
||||
const block1 = element.shadowRoot!.querySelector('[data-block-id="block1"]') as HTMLElement;
|
||||
const editorContent = element.shadowRoot!.querySelector('.editor-content') as HTMLDivElement;
|
||||
|
||||
console.log('Block has dragging class:', block1?.classList.contains('dragging'));
|
||||
console.log('Editor has dragging class:', editorContent?.classList.contains('dragging'));
|
||||
|
||||
// Clean up
|
||||
element.dragDropHandler.handleDragEnd();
|
||||
document.body.removeChild(element);
|
||||
|
||||
resolve();
|
||||
} catch (error) {
|
||||
console.error('Error in setTimeout:', error);
|
||||
throw error;
|
||||
}
|
||||
}, 50);
|
||||
});
|
||||
});
|
||||
|
||||
tap.test('identify the crash point', async () => {
|
||||
console.log('Test started');
|
||||
|
||||
const element = document.createElement('dees-input-wysiwyg');
|
||||
document.body.appendChild(element);
|
||||
|
||||
console.log('Element created');
|
||||
await element.updateComplete;
|
||||
|
||||
console.log('Setting blocks');
|
||||
element.blocks = [{ id: 'block1', type: 'paragraph', content: 'Test' }];
|
||||
element.renderBlocksProgrammatically();
|
||||
|
||||
console.log('Waiting for update');
|
||||
await element.updateComplete;
|
||||
|
||||
console.log('Creating mock event');
|
||||
const mockDragEvent = {
|
||||
dataTransfer: {
|
||||
effectAllowed: '',
|
||||
setData: () => {},
|
||||
setDragImage: () => {}
|
||||
},
|
||||
clientY: 100,
|
||||
preventDefault: () => {},
|
||||
} as any;
|
||||
|
||||
console.log('Calling handleDragStart');
|
||||
element.dragDropHandler.handleDragStart(mockDragEvent, element.blocks[0]);
|
||||
|
||||
console.log('handleDragStart completed');
|
||||
|
||||
// Try different wait methods
|
||||
console.log('About to wait...');
|
||||
|
||||
// Method 1: Direct promise
|
||||
await Promise.resolve();
|
||||
console.log('Promise.resolve completed');
|
||||
|
||||
// Method 2: setTimeout 0
|
||||
await new Promise(resolve => setTimeout(resolve, 0));
|
||||
console.log('setTimeout 0 completed');
|
||||
|
||||
// Method 3: requestAnimationFrame
|
||||
await new Promise(resolve => requestAnimationFrame(() => resolve(undefined)));
|
||||
console.log('requestAnimationFrame completed');
|
||||
|
||||
// Clean up
|
||||
element.dragDropHandler.handleDragEnd();
|
||||
document.body.removeChild(element);
|
||||
console.log('Cleanup completed');
|
||||
});
|
||||
|
||||
tap.start();
|
108
test/test.wysiwyg-dropindicator.browser.ts
Normal file
108
test/test.wysiwyg-dropindicator.browser.ts
Normal file
@ -0,0 +1,108 @@
|
||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||
import { DeesInputWysiwyg } from '../ts_web/elements/wysiwyg/dees-input-wysiwyg.js';
|
||||
|
||||
// Initialize the element
|
||||
DeesInputWysiwyg;
|
||||
|
||||
tap.test('wysiwyg drop indicator creation', async () => {
|
||||
const element = document.createElement('dees-input-wysiwyg');
|
||||
document.body.appendChild(element);
|
||||
|
||||
await element.updateComplete;
|
||||
|
||||
// Set initial content
|
||||
element.blocks = [
|
||||
{ id: 'block1', type: 'paragraph', content: 'Test block' },
|
||||
];
|
||||
element.renderBlocksProgrammatically();
|
||||
|
||||
await element.updateComplete;
|
||||
|
||||
// Check editorContentRef
|
||||
console.log('editorContentRef exists:', !!element.editorContentRef);
|
||||
console.log('editorContentRef tagName:', element.editorContentRef?.tagName);
|
||||
expect(element.editorContentRef).toBeTruthy();
|
||||
|
||||
// Check initial state - no drop indicator
|
||||
let dropIndicator = element.shadowRoot!.querySelector('.drop-indicator');
|
||||
console.log('Drop indicator before drag:', dropIndicator);
|
||||
expect(dropIndicator).toBeFalsy();
|
||||
|
||||
// Manually call createDropIndicator
|
||||
try {
|
||||
console.log('Calling createDropIndicator...');
|
||||
element.dragDropHandler['createDropIndicator']();
|
||||
console.log('createDropIndicator succeeded');
|
||||
} catch (error) {
|
||||
console.error('Error creating drop indicator:', error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Check drop indicator was created
|
||||
dropIndicator = element.shadowRoot!.querySelector('.drop-indicator');
|
||||
console.log('Drop indicator after creation:', dropIndicator);
|
||||
console.log('Drop indicator parent:', dropIndicator?.parentElement?.className);
|
||||
expect(dropIndicator).toBeTruthy();
|
||||
expect(dropIndicator!.style.display).toEqual('none');
|
||||
|
||||
// Clean up
|
||||
document.body.removeChild(element);
|
||||
});
|
||||
|
||||
tap.test('wysiwyg drag initialization with drop indicator', async () => {
|
||||
const element = document.createElement('dees-input-wysiwyg');
|
||||
document.body.appendChild(element);
|
||||
|
||||
await element.updateComplete;
|
||||
|
||||
// Set initial content
|
||||
element.blocks = [
|
||||
{ id: 'block1', type: 'paragraph', content: 'Test block' },
|
||||
];
|
||||
element.renderBlocksProgrammatically();
|
||||
|
||||
await element.updateComplete;
|
||||
|
||||
// Mock drag event
|
||||
const mockDragEvent = {
|
||||
dataTransfer: {
|
||||
effectAllowed: '',
|
||||
setData: (type: string, data: string) => {
|
||||
console.log('setData:', type, data);
|
||||
},
|
||||
setDragImage: (img: any, x: number, y: number) => {
|
||||
console.log('setDragImage');
|
||||
}
|
||||
},
|
||||
clientY: 100,
|
||||
preventDefault: () => {},
|
||||
} as any;
|
||||
|
||||
console.log('Starting drag...');
|
||||
|
||||
try {
|
||||
element.dragDropHandler.handleDragStart(mockDragEvent, element.blocks[0]);
|
||||
console.log('Drag start succeeded');
|
||||
} catch (error) {
|
||||
console.error('Error during drag start:', error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Wait for async operations
|
||||
await new Promise(resolve => setTimeout(resolve, 20));
|
||||
|
||||
// Check drop indicator exists
|
||||
const dropIndicator = element.shadowRoot!.querySelector('.drop-indicator');
|
||||
console.log('Drop indicator after drag start:', dropIndicator);
|
||||
expect(dropIndicator).toBeTruthy();
|
||||
|
||||
// Check drag state
|
||||
console.log('Drag state:', element.dragDropHandler.dragState);
|
||||
expect(element.dragDropHandler.dragState.draggedBlockId).toEqual('block1');
|
||||
|
||||
// Clean up
|
||||
element.dragDropHandler.handleDragEnd();
|
||||
document.body.removeChild(element);
|
||||
});
|
||||
|
||||
tap.start();
|
114
test/test.wysiwyg-eventlisteners.browser.ts
Normal file
114
test/test.wysiwyg-eventlisteners.browser.ts
Normal file
@ -0,0 +1,114 @@
|
||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||
import { DeesInputWysiwyg } from '../ts_web/elements/wysiwyg/dees-input-wysiwyg.js';
|
||||
|
||||
// Initialize the element
|
||||
DeesInputWysiwyg;
|
||||
|
||||
tap.test('wysiwyg global event listeners', async () => {
|
||||
const element = document.createElement('dees-input-wysiwyg');
|
||||
document.body.appendChild(element);
|
||||
|
||||
await element.updateComplete;
|
||||
|
||||
// Set initial content
|
||||
element.blocks = [
|
||||
{ id: 'block1', type: 'paragraph', content: 'Test block' },
|
||||
];
|
||||
element.renderBlocksProgrammatically();
|
||||
|
||||
await element.updateComplete;
|
||||
|
||||
const block1 = element.shadowRoot!.querySelector('[data-block-id="block1"]') as HTMLElement;
|
||||
console.log('Block 1 found:', !!block1);
|
||||
|
||||
// Set up drag state manually without using handleDragStart
|
||||
element.dragDropHandler['draggedBlockId'] = 'block1';
|
||||
element.dragDropHandler['draggedBlockElement'] = block1;
|
||||
element.dragDropHandler['initialMouseY'] = 100;
|
||||
|
||||
// Create drop indicator manually
|
||||
element.dragDropHandler['createDropIndicator']();
|
||||
|
||||
// Test adding global event listeners
|
||||
console.log('Adding event listeners...');
|
||||
const handleGlobalDragOver = element.dragDropHandler['handleGlobalDragOver'];
|
||||
const handleGlobalDragEnd = element.dragDropHandler['handleGlobalDragEnd'];
|
||||
|
||||
try {
|
||||
document.addEventListener('dragover', handleGlobalDragOver);
|
||||
console.log('dragover listener added');
|
||||
|
||||
document.addEventListener('dragend', handleGlobalDragEnd);
|
||||
console.log('dragend listener added');
|
||||
} catch (error) {
|
||||
console.error('Error adding event listeners:', error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Test firing a dragover event
|
||||
console.log('Creating dragover event...');
|
||||
const dragOverEvent = new Event('dragover', {
|
||||
bubbles: true,
|
||||
cancelable: true
|
||||
});
|
||||
Object.defineProperty(dragOverEvent, 'clientY', { value: 150 });
|
||||
|
||||
console.log('Dispatching dragover event...');
|
||||
document.dispatchEvent(dragOverEvent);
|
||||
console.log('dragover event dispatched');
|
||||
|
||||
// Clean up
|
||||
document.removeEventListener('dragover', handleGlobalDragOver);
|
||||
document.removeEventListener('dragend', handleGlobalDragEnd);
|
||||
|
||||
document.body.removeChild(element);
|
||||
});
|
||||
|
||||
tap.test('wysiwyg setTimeout in drag start', async () => {
|
||||
const element = document.createElement('dees-input-wysiwyg');
|
||||
document.body.appendChild(element);
|
||||
|
||||
await element.updateComplete;
|
||||
|
||||
// Set initial content
|
||||
element.blocks = [
|
||||
{ id: 'block1', type: 'paragraph', content: 'Test block' },
|
||||
];
|
||||
element.renderBlocksProgrammatically();
|
||||
|
||||
await element.updateComplete;
|
||||
|
||||
const block1 = element.shadowRoot!.querySelector('[data-block-id="block1"]') as HTMLElement;
|
||||
const editorContent = element.shadowRoot!.querySelector('.editor-content') as HTMLDivElement;
|
||||
|
||||
// Set drag state
|
||||
element.dragDropHandler['draggedBlockId'] = 'block1';
|
||||
element.dragDropHandler['draggedBlockElement'] = block1;
|
||||
|
||||
console.log('Testing setTimeout callback...');
|
||||
|
||||
// Test the setTimeout callback directly
|
||||
try {
|
||||
if (block1) {
|
||||
console.log('Adding dragging class to block...');
|
||||
block1.classList.add('dragging');
|
||||
console.log('Block classes:', block1.className);
|
||||
}
|
||||
if (editorContent) {
|
||||
console.log('Adding dragging class to editor...');
|
||||
editorContent.classList.add('dragging');
|
||||
console.log('Editor classes:', editorContent.className);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error in setTimeout callback:', error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
expect(block1.classList.contains('dragging')).toBeTrue();
|
||||
expect(editorContent.classList.contains('dragging')).toBeTrue();
|
||||
|
||||
// Clean up
|
||||
document.body.removeChild(element);
|
||||
});
|
||||
|
||||
tap.start();
|
@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@design.estate/dees-catalog',
|
||||
version: '1.9.9',
|
||||
version: '1.10.1',
|
||||
description: 'A comprehensive library that provides dynamic web components for building sophisticated and modern web applications using JavaScript and TypeScript.'
|
||||
}
|
||||
|
53
ts_web/elements/00fonts.ts
Normal file
53
ts_web/elements/00fonts.ts
Normal 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;
|
||||
`);
|
@ -11,27 +11,46 @@ import {
|
||||
|
||||
import * as domtools from '@design.estate/dees-domtools';
|
||||
import { DeesContextmenu } from './dees-contextmenu.js';
|
||||
import './dees-icon.js';
|
||||
|
||||
@customElement('dees-appui-activitylog')
|
||||
export class DeesAppuiActivitylog extends DeesElement {
|
||||
// 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
|
||||
public static styles = [
|
||||
cssManager.defaultStyles,
|
||||
css`
|
||||
:host {
|
||||
color: ${cssManager.bdTheme('#333', '#fff')};
|
||||
color: ${cssManager.bdTheme('#09090b', '#fafafa')};
|
||||
position: relative;
|
||||
display: block;
|
||||
width: 100%;
|
||||
max-width: 300px;
|
||||
max-width: 320px;
|
||||
height: 100%;
|
||||
background: ${cssManager.bdTheme('#f8f8f8', '#111c28')};
|
||||
font-family: 'Intel One Mono', sans-serif;
|
||||
border-left: 1px solid ${cssManager.bdTheme('#e0e0e0', '#202020')};
|
||||
background: ${cssManager.bdTheme('#fafafa', '#0a0a0a')};
|
||||
font-family: 'Geist Mono', monospace;
|
||||
border-left: 1px solid ${cssManager.bdTheme('#e5e7eb', '#27272a')};
|
||||
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 {
|
||||
position: absolute;
|
||||
@ -44,108 +63,265 @@ export class DeesAppuiActivitylog extends DeesElement {
|
||||
.topbar {
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
height: 32px;
|
||||
height: 40px;
|
||||
width: 100%;
|
||||
padding: 0px 12px 0px 12px;
|
||||
background: ${cssManager.bdTheme('#ffffff', '#0e151f')};
|
||||
border-bottom: 1px solid ${cssManager.bdTheme('#e0e0e0', '#202020')};
|
||||
padding: 0px 16px;
|
||||
background: ${cssManager.bdTheme('#ffffff', '#09090b')};
|
||||
border-bottom: 1px solid ${cssManager.bdTheme('#e5e7eb', '#27272a')};
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.topbar .heading {
|
||||
text-align: left;
|
||||
line-height: 24px;
|
||||
padding-top: 8px;
|
||||
font-weight: 500;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
font-family: 'Geist Sans', sans-serif;
|
||||
color: ${cssManager.bdTheme('#666', '#ccc')};
|
||||
color: ${cssManager.bdTheme('#09090b', '#fafafa')};
|
||||
}
|
||||
|
||||
.activityContainer {
|
||||
position: absolute;
|
||||
top: 32px;
|
||||
bottom: 40px;
|
||||
top: 40px;
|
||||
bottom: 48px;
|
||||
width: 100%;
|
||||
padding: 8px 0px;
|
||||
overflow-y: scroll;
|
||||
padding: 12px 0px;
|
||||
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 {
|
||||
font-size: 12px;
|
||||
font-size: 11px;
|
||||
text-align: center;
|
||||
padding-top: 16px;
|
||||
color: ${cssManager.bdTheme('#666', '#888')}
|
||||
padding: 16px;
|
||||
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 {
|
||||
padding-top: 0px;
|
||||
padding-top: 8px;
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
|
||||
.activityentry {
|
||||
min-height: 30px;
|
||||
font-size: 12px;
|
||||
padding: 8px 16px;
|
||||
border-bottom: 1px dotted ${cssManager.bdTheme('#00000020', '#ffffff20')};
|
||||
min-height: 36px;
|
||||
font-size: 13px;
|
||||
padding: 10px 16px;
|
||||
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 {
|
||||
border-bottom: 1px solid transparent;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.activityentry:hover {
|
||||
background: ${cssManager.bdTheme('#00000005', '#00000080')};
|
||||
background: ${cssManager.bdTheme('#f4f4f5', '#18181b')};
|
||||
}
|
||||
|
||||
.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 {
|
||||
position: absolute;
|
||||
bottom: 0px;
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
background: ${cssManager.bdTheme('#ffffff', '#0e151f')};
|
||||
border-top: 1px solid ${cssManager.bdTheme('#e0e0e0', '#202020')};
|
||||
height: 48px;
|
||||
background: ${cssManager.bdTheme('#ffffff', '#09090b')};
|
||||
border-top: 1px solid ${cssManager.bdTheme('#e5e7eb', '#27272a')};
|
||||
padding: 8px;
|
||||
}
|
||||
.searchbox input {
|
||||
color: ${cssManager.bdTheme('#333', '#fff')};
|
||||
background: none;
|
||||
|
||||
.search-wrapper {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
line-height: 32px;
|
||||
border: none;
|
||||
padding: 4px 12px;
|
||||
font-family: 'Intel One Mono', sans-serif;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
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 {
|
||||
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 {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 32px;
|
||||
bottom: 40px;
|
||||
height: 24px;
|
||||
bottom: 48px;
|
||||
background: ${cssManager.bdTheme(
|
||||
'linear-gradient(180deg, #f8f8f800 0%, #ffffff 100%)',
|
||||
'linear-gradient(180deg, #111c2800 0%, #0e151f 100%)'
|
||||
'linear-gradient(180deg, transparent 0%, #fafafa 100%)',
|
||||
'linear-gradient(180deg, transparent 0%, #0a0a0a 100%)'
|
||||
)};
|
||||
pointer-events: none;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.topShadow {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 32px;
|
||||
top: 32px;
|
||||
height: 24px;
|
||||
top: 40px;
|
||||
background: ${cssManager.bdTheme(
|
||||
'linear-gradient(0deg, #f8f8f800 0%, #ffffff 100%)',
|
||||
'linear-gradient(0deg, #111c2800 0%, #0e151f 100%)'
|
||||
'linear-gradient(0deg, transparent 0%, #fafafa 100%)',
|
||||
'linear-gradient(0deg, transparent 0%, #0a0a0a 100%)'
|
||||
)};
|
||||
pointer-events: none;
|
||||
opacity: 0.8;
|
||||
}
|
||||
`,
|
||||
];
|
||||
@ -159,86 +335,174 @@ export class DeesAppuiActivitylog extends DeesElement {
|
||||
<div class="heading">Activity Log</div>
|
||||
</div>
|
||||
<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 => {
|
||||
DeesContextmenu.openContextMenuWithOptions(eventArg, [
|
||||
{
|
||||
name: 'app settings',
|
||||
name: 'Copy activity',
|
||||
action: async () => {},
|
||||
},
|
||||
{
|
||||
name: 'account settings',
|
||||
name: 'View details',
|
||||
action: async () => {},
|
||||
},
|
||||
{
|
||||
name: 'logout',
|
||||
name: 'Filter by user',
|
||||
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 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 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 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 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 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 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 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 class="date-separator">Yesterday</div>
|
||||
|
||||
<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 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 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 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 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 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="activityentry">
|
||||
<span class="timestamp">22:15:</span> Max Mustermann changed password
|
||||
<div class="activity-text">
|
||||
<span class="activity-user">Max Mustermann</span> created a new invoice
|
||||
</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 class="streamingIndicator bottom">Loading History</div>
|
||||
</div>
|
||||
<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 class="topShadow"></div>
|
||||
<div class="bottomShadow"></div>
|
||||
|
@ -65,10 +65,10 @@ export const demoFunc = () => {
|
||||
|
||||
// Main menu tabs (left sidebar)
|
||||
const mainMenuTabs: ITab[] = [
|
||||
{ key: 'dashboard', iconName: 'home', action: () => console.log('Dashboard selected') },
|
||||
{ key: 'projects', iconName: 'folder', action: () => console.log('Projects selected') },
|
||||
{ key: 'analytics', iconName: 'lineChart', action: () => console.log('Analytics selected') },
|
||||
{ key: 'settings', iconName: 'settings', action: () => console.log('Settings selected') },
|
||||
{ key: 'dashboard', iconName: 'lucide:home', action: () => console.log('Dashboard selected') },
|
||||
{ key: 'projects', iconName: 'lucide:folder', action: () => console.log('Projects selected') },
|
||||
{ key: 'analytics', iconName: 'lucide:lineChart', action: () => console.log('Analytics selected') },
|
||||
{ key: 'settings', iconName: 'lucide:settings', action: () => console.log('Settings selected') },
|
||||
];
|
||||
|
||||
// Selector options (second sidebar)
|
||||
@ -83,9 +83,9 @@ export const demoFunc = () => {
|
||||
|
||||
// Main content tabs
|
||||
const mainContentTabs: ITab[] = [
|
||||
{ key: 'Details', iconName: 'file', action: () => console.log('Details tab') },
|
||||
{ key: 'Logs', iconName: 'list', action: () => console.log('Logs tab') },
|
||||
{ key: 'Metrics', iconName: 'lineChart', action: () => console.log('Metrics tab') },
|
||||
{ key: 'Details', iconName: 'lucide:file', action: () => console.log('Details tab') },
|
||||
{ key: 'Logs', iconName: 'lucide:list', action: () => console.log('Logs tab') },
|
||||
{ key: 'Metrics', iconName: 'lucide:lineChart', action: () => console.log('Metrics tab') },
|
||||
];
|
||||
|
||||
// Profile menu items
|
||||
|
@ -19,9 +19,9 @@ export class DeesAppuiMaincontent extends DeesElement {
|
||||
public static demo = () => html`
|
||||
<dees-appui-maincontent
|
||||
.tabs=${[
|
||||
{ key: 'Overview', iconName: 'home', action: () => console.log('Overview') },
|
||||
{ key: 'Details', iconName: 'file', action: () => console.log('Details') },
|
||||
{ key: 'Settings', iconName: 'cog', action: () => console.log('Settings') },
|
||||
{ key: 'Overview', iconName: 'lucide:home', action: () => console.log('Overview') },
|
||||
{ key: 'Details', iconName: 'lucide:file', action: () => console.log('Details') },
|
||||
{ key: 'Settings', iconName: 'lucide:settings', action: () => console.log('Settings') },
|
||||
]}
|
||||
>
|
||||
<div slot="content" style="padding: 40px; color: #ccc;">
|
||||
|
@ -22,10 +22,10 @@ export class DeesAppuiMainmenu extends DeesElement {
|
||||
public static demo = () => html`
|
||||
<dees-appui-mainmenu
|
||||
.tabs=${[
|
||||
{ key: 'Dashboard', iconName: 'home', action: () => console.log('Dashboard') },
|
||||
{ key: 'Projects', iconName: 'folder', action: () => console.log('Projects') },
|
||||
{ key: 'Analytics', iconName: 'lineChart', action: () => console.log('Analytics') },
|
||||
{ key: 'Settings', iconName: 'settings', action: () => console.log('Settings') },
|
||||
{ key: 'Dashboard', iconName: 'lucide:home', action: () => console.log('Dashboard') },
|
||||
{ key: 'Projects', iconName: 'lucide:folder', action: () => console.log('Projects') },
|
||||
{ key: 'Analytics', iconName: 'lucide:lineChart', action: () => console.log('Analytics') },
|
||||
{ key: 'Settings', iconName: 'lucide:settings', action: () => console.log('Settings') },
|
||||
]}
|
||||
></dees-appui-mainmenu>
|
||||
`;
|
||||
@ -35,7 +35,7 @@ export class DeesAppuiMainmenu extends DeesElement {
|
||||
// INSTANCE
|
||||
@property({ type: Array })
|
||||
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()
|
||||
@ -112,7 +112,7 @@ export class DeesAppuiMainmenu extends DeesElement {
|
||||
this.updateTab(tabArg);
|
||||
}}"
|
||||
>
|
||||
<dees-icon .icon="${tabArg.iconName ? `lucide:${tabArg.iconName}` : ''}"></dees-icon>
|
||||
<dees-icon .icon="${tabArg.iconName || ''}"></dees-icon>
|
||||
</div>
|
||||
`;
|
||||
})}
|
||||
|
@ -14,16 +14,95 @@ import * as domtools from '@design.estate/dees-domtools';
|
||||
|
||||
@customElement('dees-appui-tabs')
|
||||
export class DeesAppuiTabs extends DeesElement {
|
||||
public static demo = () => html`
|
||||
<dees-appui-tabs
|
||||
.tabs=${[
|
||||
{ key: 'Tab 1', action: () => console.log('Tab 1 clicked') },
|
||||
{ key: 'Tab 2', action: () => console.log('Tab 2 clicked') },
|
||||
{ key: 'Tab 3', action: () => console.log('Tab 3 clicked') },
|
||||
]}
|
||||
></dees-appui-tabs>
|
||||
public static demo = () => {
|
||||
const horizontalTabs: interfaces.ITab[] = [
|
||||
{ key: 'Home', iconName: 'lucide:home', action: () => console.log('Home clicked') },
|
||||
{ key: 'Analytics Dashboard', iconName: 'lucide:lineChart', action: () => console.log('Analytics clicked') },
|
||||
{ key: 'Reports', iconName: 'lucide:fileText', action: () => console.log('Reports clicked') },
|
||||
{ key: 'User Settings', iconName: 'lucide:settings', action: () => console.log('Settings clicked') },
|
||||
{ key: 'Help', iconName: 'lucide:helpCircle', action: () => console.log('Help clicked') },
|
||||
];
|
||||
|
||||
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
|
||||
@property({
|
||||
type: Array,
|
||||
@ -50,148 +129,217 @@ export class DeesAppuiTabs extends DeesElement {
|
||||
|
||||
.tabs-wrapper {
|
||||
position: relative;
|
||||
background: ${cssManager.bdTheme('#f5f5f5', '#000000')};
|
||||
height: 52px;
|
||||
}
|
||||
|
||||
.tabs-wrapper.horizontal-wrapper {
|
||||
border-bottom: 1px solid ${cssManager.bdTheme('#e5e7eb', '#27272a')};
|
||||
}
|
||||
|
||||
.tabsContainer {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.tabsContainer.horizontal {
|
||||
display: grid;
|
||||
padding-top: 20px;
|
||||
padding-bottom: 0px;
|
||||
margin-left: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 14px;
|
||||
overflow-x: auto;
|
||||
scrollbar-width: none;
|
||||
height: 48px;
|
||||
padding: 0 16px;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.tabsContainer.horizontal::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tabsContainer.vertical {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 20px;
|
||||
padding: 8px;
|
||||
font-size: 14px;
|
||||
gap: 2px;
|
||||
position: relative;
|
||||
background: ${cssManager.bdTheme('#f9fafb', '#18181b')};
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.tab {
|
||||
color: ${cssManager.bdTheme('#666', '#a0a0a0')};
|
||||
color: ${cssManager.bdTheme('#71717a', '#71717a')};
|
||||
white-space: nowrap;
|
||||
cursor: default;
|
||||
transition: color 0.1s;
|
||||
cursor: pointer;
|
||||
transition: color 0.15s ease;
|
||||
font-weight: 500;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.horizontal .tab {
|
||||
margin-right: 30px;
|
||||
padding-top: 4px;
|
||||
padding-bottom: 12px;
|
||||
padding: 0 16px;
|
||||
height: 100%;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
position: relative;
|
||||
border-radius: 6px 6px 0 0;
|
||||
transition: background-color 0.15s ease;
|
||||
}
|
||||
|
||||
.vertical .tab {
|
||||
padding: 12px 16px;
|
||||
margin-bottom: 4px;
|
||||
border-radius: 4px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
.horizontal .tab:not(:last-child)::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
right: -2px;
|
||||
top: 50%;
|
||||
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;
|
||||
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 {
|
||||
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 {
|
||||
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 {
|
||||
color: ${cssManager.bdTheme('#333', '#e0e0e0')};
|
||||
.horizontal .tab.selectedTab {
|
||||
color: ${cssManager.bdTheme('#09090b', '#fafafa')};
|
||||
}
|
||||
|
||||
.horizontal .tab.selectedTab::after,
|
||||
.horizontal .tab.selectedTab + .tab::after {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.vertical .tab.selectedTab {
|
||||
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.1)', 'rgba(255, 255, 255, 0.1)')};
|
||||
color: ${cssManager.bdTheme('#000', '#ffffff')};
|
||||
color: ${cssManager.bdTheme('#09090b', '#fafafa')};
|
||||
}
|
||||
|
||||
.tab dees-icon {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.tabs-wrapper .tabIndicator {
|
||||
.tabIndicator {
|
||||
position: absolute;
|
||||
z-index: 0;
|
||||
left: 40px;
|
||||
bottom: 0px;
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
background: ${cssManager.bdTheme('#ffffff', '#161616')};
|
||||
transition: all 0.1s;
|
||||
border-top-left-radius: 8px;
|
||||
border-top-right-radius: 8px;
|
||||
border-top: 1px solid ${cssManager.bdTheme('#e0e0e0', '#444444')};
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.vertical .tabIndicator {
|
||||
display: none;
|
||||
.tabIndicator.no-transition {
|
||||
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 {
|
||||
margin-top: 20px;
|
||||
padding: 32px 24px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
public render(): TemplateResult {
|
||||
return html`
|
||||
${this.tabStyle === 'horizontal' ? html`
|
||||
<style>
|
||||
.tabsContainer.horizontal {
|
||||
grid-template-columns: repeat(${this.tabs.length}, min-content);
|
||||
}
|
||||
</style>
|
||||
<div class="tabs-wrapper">
|
||||
<div class="tabsContainer horizontal">
|
||||
${this.tabs.map((tabArg) => {
|
||||
return html`
|
||||
<div
|
||||
class="tab ${tabArg === this.selectedTab ? 'selectedTab' : ''}"
|
||||
@click="${() => this.selectTab(tabArg)}"
|
||||
>
|
||||
${tabArg.key}
|
||||
</div>
|
||||
`;
|
||||
})}
|
||||
</div>
|
||||
${this.showTabIndicator ? html`
|
||||
<div class="tabIndicator"></div>
|
||||
` : ''}
|
||||
</div>
|
||||
` : html`
|
||||
<div class="tabsContainer vertical">
|
||||
${this.tabs.map((tabArg) => {
|
||||
return html`
|
||||
<div
|
||||
class="tab ${tabArg === this.selectedTab ? 'selectedTab' : ''}"
|
||||
@click="${() => this.selectTab(tabArg)}"
|
||||
>
|
||||
${tabArg.iconName ? html`<dees-icon .iconName=${tabArg.iconName}></dees-icon>` : ''}
|
||||
${tabArg.key}
|
||||
</div>
|
||||
`;
|
||||
})}
|
||||
</div>
|
||||
`}
|
||||
${this.renderTabsWrapper()}
|
||||
<div class="content">
|
||||
<slot></slot>
|
||||
</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) {
|
||||
this.selectedTab = tabArg;
|
||||
this.updateTabIndicator();
|
||||
tabArg.action();
|
||||
|
||||
// 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() {
|
||||
if (this.tabs && this.tabs.length > 0) {
|
||||
this.selectTab(this.tabs[0]);
|
||||
@ -241,7 +364,88 @@ export class DeesAppuiTabs extends DeesElement {
|
||||
}
|
||||
|
||||
if (changedProperties.has('selectedTab') || changedProperties.has('tabs')) {
|
||||
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`;
|
||||
}
|
||||
}
|
@ -35,17 +35,17 @@ export class DeesAppuiView extends DeesElement {
|
||||
id: 'demo-view',
|
||||
name: 'Demo View',
|
||||
description: 'A demonstration view',
|
||||
iconName: 'home',
|
||||
iconName: 'lucide:home',
|
||||
tabs: [
|
||||
{
|
||||
key: 'overview',
|
||||
iconName: 'chart-line',
|
||||
iconName: 'lucide:lineChart',
|
||||
action: () => console.log('Overview tab'),
|
||||
content: html`<div style="padding: 20px;">Overview Content</div>`
|
||||
},
|
||||
{
|
||||
key: 'details',
|
||||
iconName: 'file-alt',
|
||||
iconName: 'lucide:fileText',
|
||||
action: () => console.log('Details tab'),
|
||||
content: html`<div style="padding: 20px;">Details Content</div>`
|
||||
}
|
||||
|
@ -1,77 +1,301 @@
|
||||
import { html, css } 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`
|
||||
<dees-demowrapper>
|
||||
<style>
|
||||
${css`
|
||||
h3 {
|
||||
margin-top: 32px;
|
||||
margin-bottom: 16px;
|
||||
color: #333;
|
||||
.demo-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
padding: 24px;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
h3 {
|
||||
color: #ccc;
|
||||
}
|
||||
dees-panel {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.form-demo {
|
||||
background: #f5f5f5;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.form-demo {
|
||||
background: #1a1a1a;
|
||||
}
|
||||
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;
|
||||
margin: 20px 0;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.demo-output {
|
||||
margin-top: 16px;
|
||||
padding: 12px;
|
||||
background: ${cssManager.bdTheme('hsl(210 40% 96.1%)', 'hsl(215 20.2% 16.8%)')};
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
font-family: monospace;
|
||||
color: ${cssManager.bdTheme('hsl(215.3 25% 8.8%)', 'hsl(210 40% 98%)')};
|
||||
}
|
||||
|
||||
.icon-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin: 8px 0;
|
||||
}
|
||||
|
||||
.code-snippet {
|
||||
background: ${cssManager.bdTheme('hsl(210 40% 96.1%)', 'hsl(215 20.2% 11.8%)')};
|
||||
padding: 8px 12px;
|
||||
border-radius: 4px;
|
||||
font-family: monospace;
|
||||
font-size: 13px;
|
||||
display: inline-block;
|
||||
margin: 4px 0;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
|
||||
<h3>Button Types</h3>
|
||||
<dees-button>This is a slotted Text</dees-button>
|
||||
<p>
|
||||
<dees-button text="Highlighted: This text shows" type="highlighted">Highlighted</dees-button>
|
||||
</p>
|
||||
<p><dees-button type="discreet">This is discreete button</dees-button></p>
|
||||
<p><dees-button disabled>This is a disabled button</dees-button></p>
|
||||
<p><dees-button type="big">This is a slotted Text</dees-button></p>
|
||||
<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>
|
||||
|
||||
<h3>Button States</h3>
|
||||
<p><dees-button status="normal">Normal Status</dees-button></p>
|
||||
<p><dees-button disabled status="pending">Pending Status</dees-button></p>
|
||||
<p><dees-button disabled status="success">Success Status</dees-button></p>
|
||||
<p><dees-button disabled status="error">Error Status</dees-button></p>
|
||||
<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>
|
||||
|
||||
<h3>Buttons in Forms (Auto-spacing)</h3>
|
||||
<div class="form-demo">
|
||||
<dees-form>
|
||||
<dees-input-text label="Name" key="name"></dees-input-text>
|
||||
<dees-input-text label="Email" key="email"></dees-input-text>
|
||||
<dees-button>Save Draft</dees-button>
|
||||
<dees-button type="highlighted">Save and Continue</dees-button>
|
||||
<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>
|
||||
|
||||
<h3>Buttons Outside Forms (No auto-spacing)</h3>
|
||||
<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>Button 1</dees-button>
|
||||
<dees-button>Button 2</dees-button>
|
||||
<dees-button>Button 3</dees-button>
|
||||
<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>
|
||||
|
||||
<h3>Manual Form Spacing</h3>
|
||||
<div>
|
||||
<dees-button inside-form="true">Manually spaced button 1</dees-button>
|
||||
<dees-button inside-form="true">Manually spaced button 2</dees-button>
|
||||
<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">
|
||||
<dees-button type="default" size="sm" @clicked="\${handleClick}"><br>
|
||||
<dees-icon iconFA="faSave"></dees-icon><br>
|
||||
Save Changes<br>
|
||||
</dees-button>
|
||||
</div>
|
||||
</div>
|
||||
</dees-panel>
|
||||
</div>
|
||||
</dees-demowrapper>
|
||||
`;
|
||||
|
@ -1,4 +1,3 @@
|
||||
import { demoFunc } from './dees-button.demo.js';
|
||||
import {
|
||||
customElement,
|
||||
html,
|
||||
@ -12,6 +11,7 @@ import {
|
||||
} from '@design.estate/dees-element';
|
||||
|
||||
import * as domtools from '@design.estate/dees-domtools';
|
||||
import { demoFunc } from './dees-button.demo.js';
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
@ -48,7 +48,12 @@ export class DeesButton extends DeesElement {
|
||||
@property({
|
||||
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({
|
||||
type: String
|
||||
@ -77,25 +82,23 @@ export class DeesButton extends DeesElement {
|
||||
cssManager.defaultStyles,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
display: inline-block;
|
||||
box-sizing: border-box;
|
||||
font-family: 'Geist Sans', 'monospace';
|
||||
font-family: inherit;
|
||||
}
|
||||
:host([hidden]) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Form spacing styles */
|
||||
/* Default vertical form layout */
|
||||
:host([inside-form]) {
|
||||
margin-bottom: 16px; /* Using standard 16px like inputs */
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
:host([inside-form]:last-child) {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* Horizontal form layout - auto-detected via parent */
|
||||
dees-form[horizontal-layout] :host([inside-form]) {
|
||||
display: inline-block;
|
||||
margin-right: 16px;
|
||||
@ -107,114 +110,260 @@ export class DeesButton extends DeesElement {
|
||||
}
|
||||
|
||||
.button {
|
||||
transition: all 0.1s , color 0s;
|
||||
position: relative;
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
background: ${cssManager.bdTheme('#fff', '#333')};
|
||||
box-shadow: ${cssManager.bdTheme('0px 1px 3px rgba(0,0,0,0.3)', 'none')};
|
||||
border: 1px solid ${cssManager.bdTheme('#eee', '#333')};
|
||||
border-top: ${cssManager.bdTheme('1px solid #eee', '1px solid #444')};
|
||||
border-radius: 4px;
|
||||
height: 40px;
|
||||
padding: 0px 8px;
|
||||
min-width: 100px;
|
||||
justify-content: center;
|
||||
white-space: nowrap;
|
||||
border-radius: 6px;
|
||||
font-weight: 500;
|
||||
transition: all 0.15s ease;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
color: ${cssManager.bdTheme('#333', ' #ccc')};
|
||||
max-width: 500px;
|
||||
outline: none;
|
||||
letter-spacing: -0.01em;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.button:hover {
|
||||
background: #0050b9;
|
||||
color: #ffffff;
|
||||
border: 1px solid #0050b9;
|
||||
border-top: 1px solid #0050b9;
|
||||
/* Size variants */
|
||||
.button.size-default {
|
||||
height: 36px;
|
||||
padding: 0 16px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.button:active {
|
||||
background: #0069f2;
|
||||
border-top: 1px solid #0069f2;
|
||||
.button.size-sm {
|
||||
height: 32px;
|
||||
padding: 0 12px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.button.highlighted {
|
||||
background: #e4002b;
|
||||
.button.size-lg {
|
||||
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;
|
||||
color: #fff;
|
||||
text-decoration: underline;
|
||||
text-decoration-color: transparent;
|
||||
height: auto;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.button.highlighted:hover {
|
||||
background: #b50021;
|
||||
border: none;
|
||||
color: #fff;
|
||||
.button.link:hover:not(.disabled) {
|
||||
text-decoration-color: currentColor;
|
||||
}
|
||||
|
||||
.button.discreet {
|
||||
background: none;
|
||||
border: 1px solid #9b9b9e;
|
||||
color: ${cssManager.bdTheme('#000', '#fff')};
|
||||
/* Status states */
|
||||
.button.pending,
|
||||
.button.success,
|
||||
.button.error {
|
||||
pointer-events: none;
|
||||
padding-left: 36px; /* Space for spinner */
|
||||
}
|
||||
|
||||
.button.discreet:hover {
|
||||
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.1)', 'rgba(255, 255, 255, 0.1)')};
|
||||
.button.size-sm.pending,
|
||||
.button.size-sm.success,
|
||||
.button.size-sm.error {
|
||||
padding-left: 32px;
|
||||
}
|
||||
|
||||
.button.size-lg.pending,
|
||||
.button.size-lg.success,
|
||||
.button.size-lg.error {
|
||||
padding-left: 44px;
|
||||
}
|
||||
|
||||
.button.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 {
|
||||
background: ${cssManager.bdTheme('#ffffff00', '#11111100')};
|
||||
border: 1px dashed ${cssManager.bdTheme('#666666', '#666666')};
|
||||
color: #9b9b9e;
|
||||
cursor: default;
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Hidden state */
|
||||
.button.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.button.big {
|
||||
width: 300px;
|
||||
line-height: 48px;
|
||||
font-size: 16px;
|
||||
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;
|
||||
/* Focus state */
|
||||
.button:focus-visible {
|
||||
outline: 2px solid ${cssManager.bdTheme('hsl(222.2 47.4% 51.2%)', 'hsl(213.1 93.9% 67.8%)')};
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* Loading spinner */
|
||||
dees-spinner {
|
||||
position: absolute;
|
||||
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 {
|
||||
// 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`
|
||||
<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'
|
||||
: null}"
|
||||
: ''}"
|
||||
@click="${this.dispatchClick}"
|
||||
>
|
||||
${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>
|
||||
|
@ -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 '@design.estate/dees-wcctools/demotools';
|
||||
|
||||
@ -402,7 +402,7 @@ export const demoFunc = () => {
|
||||
${css`
|
||||
.demoBox {
|
||||
position: relative;
|
||||
background: #000000;
|
||||
background: ${cssManager.bdTheme('hsl(0 0% 95%)', 'hsl(0 0% 9%)')};
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
padding: 40px;
|
||||
@ -425,9 +425,9 @@ export const demoFunc = () => {
|
||||
}
|
||||
|
||||
.info {
|
||||
color: #666;
|
||||
font-size: 11px;
|
||||
font-family: 'Geist Sans', sans-serif;
|
||||
color: ${cssManager.bdTheme('hsl(215.4 16.3% 56.9%)', 'hsl(215 20.2% 55.1%)')};
|
||||
font-size: 12px;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Geist Sans', sans-serif;
|
||||
text-align: center;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
@ -61,6 +61,23 @@ export class DeesChartArea extends DeesElement {
|
||||
private resizeTimeout: number;
|
||||
private internalChartData: ApexAxisChartSeries = [];
|
||||
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() {
|
||||
super();
|
||||
@ -73,46 +90,72 @@ export class DeesChartArea extends DeesElement {
|
||||
}
|
||||
|
||||
this.resizeTimeout = window.setTimeout(() => {
|
||||
for (let entry of entries) {
|
||||
if (entry.target.classList.contains('mainbox') && this.chart) {
|
||||
this.resizeChart();
|
||||
// Simply resize if we have a chart, since we're only observing the mainbox
|
||||
if (this.chart) {
|
||||
// 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
|
||||
});
|
||||
|
||||
this.registerStartupFunction(async () => {
|
||||
this.updateComplete.then(() => {
|
||||
const mainbox = this.shadowRoot.querySelector('.mainbox');
|
||||
if (mainbox) {
|
||||
this.resizeObserver.observe(mainbox);
|
||||
}
|
||||
});
|
||||
});
|
||||
// Note: ResizeObserver is now set up after chart initialization in firstUpdated()
|
||||
// to ensure proper timing and avoid race conditions
|
||||
|
||||
this.registerGarbageFunction(async () => {
|
||||
if (this.resizeTimeout) {
|
||||
clearTimeout(this.resizeTimeout);
|
||||
}
|
||||
if (this.resizeObserver) {
|
||||
this.resizeObserver.disconnect();
|
||||
}
|
||||
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 = [
|
||||
cssManager.defaultStyles,
|
||||
css`
|
||||
:host {
|
||||
font-family: 'Geist Sans', sans-serif;
|
||||
color: #ccc;
|
||||
font-weight: 600;
|
||||
font-size: 12px;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 3.9%)', 'hsl(0 0% 98%)')};
|
||||
font-weight: 400;
|
||||
font-size: 14px;
|
||||
}
|
||||
.mainbox {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
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;
|
||||
overflow: hidden;
|
||||
}
|
||||
@ -122,9 +165,13 @@ export class DeesChartArea extends DeesElement {
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
padding-top: 16px;
|
||||
text-align: left;
|
||||
padding: 16px 24px;
|
||||
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 {
|
||||
position: absolute;
|
||||
@ -132,8 +179,22 @@ export class DeesChartArea extends DeesElement {
|
||||
left: 0px;
|
||||
bottom: 0px;
|
||||
right: 0px;
|
||||
padding: 32px 16px 16px 0px;
|
||||
padding: 44px 16px 16px 0px;
|
||||
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
|
||||
this.internalChartData = chartSeries;
|
||||
|
||||
// Get current theme
|
||||
const isDark = !this.goBright;
|
||||
const theme = isDark ? 'dark' : 'light';
|
||||
|
||||
var options: ApexCharts.ApexOptions = {
|
||||
series: chartSeries,
|
||||
chart: {
|
||||
width: initialWidth || 100, // Use actual width or fallback
|
||||
height: initialHeight || 100, // Use actual height or fallback
|
||||
type: 'area',
|
||||
background: 'transparent', // Transparent background to inherit from container
|
||||
toolbar: {
|
||||
show: false, // This line disables the toolbar
|
||||
},
|
||||
@ -220,12 +286,18 @@ export class DeesChartArea extends DeesElement {
|
||||
speed: 350
|
||||
}
|
||||
},
|
||||
zoom: {
|
||||
enabled: false, // Disable zoom for cleaner interaction
|
||||
},
|
||||
selection: {
|
||||
enabled: false, // Disable selection
|
||||
},
|
||||
},
|
||||
dataLabels: {
|
||||
enabled: false,
|
||||
},
|
||||
stroke: {
|
||||
width: 1,
|
||||
width: 2,
|
||||
curve: 'smooth',
|
||||
},
|
||||
xaxis: {
|
||||
@ -234,8 +306,10 @@ export class DeesChartArea extends DeesElement {
|
||||
format: 'HH:mm:ss', // Time formatting with seconds
|
||||
datetimeUTC: false,
|
||||
style: {
|
||||
colors: '#9e9e9e', // Label color
|
||||
fontSize: '11px',
|
||||
colors: [isDark ? 'hsl(0 0% 63.9%)' : 'hsl(0 0% 20%)'], // Label color
|
||||
fontSize: '12px',
|
||||
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif',
|
||||
fontWeight: '400',
|
||||
},
|
||||
},
|
||||
axisBorder: {
|
||||
@ -251,8 +325,10 @@ export class DeesChartArea extends DeesElement {
|
||||
labels: {
|
||||
formatter: this.yAxisFormatter,
|
||||
style: {
|
||||
colors: '#9e9e9e', // Label color
|
||||
colors: [isDark ? 'hsl(0 0% 63.9%)' : 'hsl(0 0% 20%)'], // Label color
|
||||
fontSize: '12px',
|
||||
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif',
|
||||
fontWeight: '400',
|
||||
},
|
||||
},
|
||||
axisBorder: {
|
||||
@ -269,14 +345,30 @@ export class DeesChartArea extends DeesElement {
|
||||
x: {
|
||||
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
|
||||
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) => {
|
||||
const label = w.globals.seriesNames[index]; // Get series label
|
||||
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>`;
|
||||
@ -286,7 +378,7 @@ export class DeesChartArea extends DeesElement {
|
||||
grid: {
|
||||
xaxis: {
|
||||
lines: {
|
||||
show: true, // This enables the grid lines along the x-axis
|
||||
show: false, // Hide vertical grid lines for cleaner look
|
||||
},
|
||||
},
|
||||
yaxis: {
|
||||
@ -294,38 +386,67 @@ export class DeesChartArea extends DeesElement {
|
||||
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
|
||||
row: {
|
||||
colors: [], // This can be used to alternate the shading of the horizontal rows
|
||||
opacity: 0.1,
|
||||
},
|
||||
column: {
|
||||
colors: [], // For vertical column bands, not needed here but available for customization
|
||||
opacity: 0.1,
|
||||
padding: {
|
||||
top: 10,
|
||||
right: 20,
|
||||
bottom: 10,
|
||||
left: 20,
|
||||
},
|
||||
},
|
||||
fill: {
|
||||
type: 'gradient', // Gradient fill for the area
|
||||
gradient: {
|
||||
shade: 'dark',
|
||||
shade: isDark ? 'dark' : 'light',
|
||||
type: 'vertical',
|
||||
gradientToColors: ['#9c27b0'], // Gradient color ending
|
||||
shadeIntensity: 0.1,
|
||||
opacityFrom: isDark ? 0.2 : 0.3,
|
||||
opacityTo: 0,
|
||||
stops: [0, 100],
|
||||
},
|
||||
},
|
||||
colors: isDark ? this.CHART_COLORS.dark : this.CHART_COLORS.light,
|
||||
theme: {
|
||||
mode: theme,
|
||||
},
|
||||
};
|
||||
|
||||
try {
|
||||
this.chart = new ApexCharts(this.shadowRoot.querySelector('.chartContainer'), options);
|
||||
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>) {
|
||||
super.updated(changedProperties);
|
||||
|
||||
// Update chart theme when goBright changes
|
||||
if (changedProperties.has('goBright') && this.chart) {
|
||||
await this.updateChartTheme();
|
||||
}
|
||||
|
||||
// Update chart if series data changes
|
||||
if (changedProperties.has('series') && this.chart && this.series.length > 0) {
|
||||
await this.updateSeries(this.series);
|
||||
@ -393,6 +514,7 @@ export class DeesChartArea extends DeesElement {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Store the new data first
|
||||
this.internalChartData = newSeries;
|
||||
|
||||
@ -429,14 +551,18 @@ export class DeesChartArea extends DeesElement {
|
||||
}
|
||||
}
|
||||
|
||||
this.chart.updateSeries(filteredSeries, false);
|
||||
await this.chart.updateSeries(filteredSeries, false);
|
||||
}
|
||||
} else {
|
||||
this.chart.updateSeries(newSeries, animate);
|
||||
await this.chart.updateSeries(newSeries, animate);
|
||||
}
|
||||
} catch (error) {
|
||||
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() {
|
||||
if (!this.chart || this.rollingWindow <= 0) {
|
||||
return;
|
||||
@ -453,8 +579,10 @@ export class DeesChartArea extends DeesElement {
|
||||
format: 'HH:mm:ss',
|
||||
datetimeUTC: false,
|
||||
style: {
|
||||
colors: '#9e9e9e',
|
||||
fontSize: '11px',
|
||||
colors: [!this.goBright ? 'hsl(0 0% 63.9%)' : 'hsl(0 0% 20%)'],
|
||||
fontSize: '12px',
|
||||
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif',
|
||||
fontWeight: '400',
|
||||
},
|
||||
},
|
||||
tickAmount: 6,
|
||||
@ -484,6 +612,11 @@ export class DeesChartArea extends DeesElement {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.DEBUG_RESIZE) {
|
||||
console.log('DeesChartArea - resizeChart called');
|
||||
}
|
||||
|
||||
try {
|
||||
const mainbox: HTMLDivElement = this.shadowRoot.querySelector('.mainbox');
|
||||
const chartContainer: HTMLDivElement = this.shadowRoot.querySelector('.chartContainer');
|
||||
|
||||
@ -491,6 +624,9 @@ export class DeesChartArea extends DeesElement {
|
||||
return;
|
||||
}
|
||||
|
||||
// Force layout recalculation
|
||||
void mainbox.offsetHeight;
|
||||
|
||||
// Get computed style of the element
|
||||
const styleChartContainer = window.getComputedStyle(chartContainer);
|
||||
|
||||
@ -504,12 +640,33 @@ export class DeesChartArea extends DeesElement {
|
||||
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() {
|
||||
@ -528,4 +685,43 @@ export class DeesChartArea extends DeesElement {
|
||||
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,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -53,17 +53,17 @@ export class DeesChartLog extends DeesElement {
|
||||
cssManager.defaultStyles,
|
||||
css`
|
||||
:host {
|
||||
font-family: 'Geist Mono', 'Consolas', 'Monaco', monospace;
|
||||
color: #ccc;
|
||||
font-family: 'SF Mono', 'Monaco', 'Consolas', 'Liberation Mono', 'Courier New', monospace;
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 3.9%)', 'hsl(0 0% 98%)')};
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.mainbox {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 400px;
|
||||
background: ${cssManager.bdTheme('#f8f9fa', '#0a0a0a')};
|
||||
border: 1px solid ${cssManager.bdTheme('#dee2e6', '#333')};
|
||||
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;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@ -71,9 +71,9 @@ export class DeesChartLog extends DeesElement {
|
||||
}
|
||||
|
||||
.header {
|
||||
background: ${cssManager.bdTheme('#e9ecef', '#1a1a1a')};
|
||||
padding: 8px 16px;
|
||||
border-bottom: 1px solid ${cssManager.bdTheme('#dee2e6', '#333')};
|
||||
background: ${cssManager.bdTheme('hsl(0 0% 97%)', 'hsl(0 0% 7%)')};
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
@ -81,8 +81,10 @@ export class DeesChartLog extends DeesElement {
|
||||
}
|
||||
|
||||
.title {
|
||||
font-weight: 600;
|
||||
color: ${cssManager.bdTheme('#212529', '#fff')};
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 9%)', 'hsl(0 0% 95%)')};
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
}
|
||||
|
||||
.controls {
|
||||
@ -91,44 +93,48 @@ export class DeesChartLog extends DeesElement {
|
||||
}
|
||||
|
||||
.control-button {
|
||||
background: ${cssManager.bdTheme('#e9ecef', '#2a2a2a')};
|
||||
border: 1px solid ${cssManager.bdTheme('#ced4da', '#444')};
|
||||
border-radius: 4px;
|
||||
padding: 4px 8px;
|
||||
color: ${cssManager.bdTheme('#495057', '#ccc')};
|
||||
background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(0 0% 14.9%)')};
|
||||
border: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
|
||||
border-radius: 6px;
|
||||
padding: 6px 12px;
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 45.1%)', 'hsl(0 0% 63.9%)')};
|
||||
cursor: pointer;
|
||||
font-size: 11px;
|
||||
transition: all 0.2s;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
transition: all 0.15s;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
}
|
||||
|
||||
.control-button:hover {
|
||||
background: ${cssManager.bdTheme('#dee2e6', '#3a3a3a')};
|
||||
border-color: ${cssManager.bdTheme('#adb5bd', '#555')};
|
||||
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%)')};
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 15%)', 'hsl(0 0% 93.9%)')};
|
||||
}
|
||||
|
||||
.control-button.active {
|
||||
background: ${cssManager.bdTheme('#007bff', '#4a4a4a')};
|
||||
color: ${cssManager.bdTheme('#fff', '#fff')};
|
||||
background: ${cssManager.bdTheme('hsl(0 0% 9%)', 'hsl(0 0% 93.9%)')};
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 98%)', 'hsl(0 0% 3.9%)')};
|
||||
}
|
||||
|
||||
.logContainer {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
padding: 8px 16px;
|
||||
padding: 16px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.logEntry {
|
||||
margin-bottom: 2px;
|
||||
margin-bottom: 4px;
|
||||
display: flex;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.timestamp {
|
||||
color: ${cssManager.bdTheme('#6c757d', '#666')};
|
||||
margin-right: 8px;
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 63.9%)', 'hsl(0 0% 45.1%)')};
|
||||
margin-right: 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
@ -143,38 +149,38 @@ export class DeesChartLog extends DeesElement {
|
||||
}
|
||||
|
||||
.level.debug {
|
||||
color: ${cssManager.bdTheme('#6c757d', '#999')};
|
||||
background: ${cssManager.bdTheme('rgba(108, 117, 125, 0.1)', '#333')};
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 45.1%)', 'hsl(0 0% 63.9%)')};
|
||||
background: ${cssManager.bdTheme('hsl(0 0% 45.1% / 0.1)', 'hsl(0 0% 63.9% / 0.1)')};
|
||||
}
|
||||
|
||||
.level.info {
|
||||
color: ${cssManager.bdTheme('#0066cc', '#4a9eff')};
|
||||
background: ${cssManager.bdTheme('rgba(0, 102, 204, 0.1)', 'rgba(74, 158, 255, 0.1)')};
|
||||
color: ${cssManager.bdTheme('hsl(222.2 47.4% 51.2%)', 'hsl(217.2 91.2% 59.8%)')};
|
||||
background: ${cssManager.bdTheme('hsl(222.2 47.4% 51.2% / 0.1)', 'hsl(217.2 91.2% 59.8% / 0.1)')};
|
||||
}
|
||||
|
||||
.level.warn {
|
||||
color: ${cssManager.bdTheme('#ff8800', '#ffb84a')};
|
||||
background: ${cssManager.bdTheme('rgba(255, 136, 0, 0.1)', 'rgba(255, 184, 74, 0.1)')};
|
||||
color: ${cssManager.bdTheme('hsl(25 95% 53%)', 'hsl(25 95% 63%)')};
|
||||
background: ${cssManager.bdTheme('hsl(25 95% 53% / 0.1)', 'hsl(25 95% 63% / 0.1)')};
|
||||
}
|
||||
|
||||
.level.error {
|
||||
color: ${cssManager.bdTheme('#dc3545', '#ff4a4a')};
|
||||
background: ${cssManager.bdTheme('rgba(220, 53, 69, 0.1)', 'rgba(255, 74, 74, 0.1)')};
|
||||
color: ${cssManager.bdTheme('hsl(0 84.2% 60.2%)', 'hsl(0 72.2% 50.6%)')};
|
||||
background: ${cssManager.bdTheme('hsl(0 84.2% 60.2% / 0.1)', 'hsl(0 72.2% 50.6% / 0.1)')};
|
||||
}
|
||||
|
||||
.level.success {
|
||||
color: ${cssManager.bdTheme('#28a745', '#4aff88')};
|
||||
background: ${cssManager.bdTheme('rgba(40, 167, 69, 0.1)', 'rgba(74, 255, 136, 0.1)')};
|
||||
color: ${cssManager.bdTheme('hsl(142.1 76.2% 36.3%)', 'hsl(142.1 70.6% 45.3%)')};
|
||||
background: ${cssManager.bdTheme('hsl(142.1 76.2% 36.3% / 0.1)', 'hsl(142.1 70.6% 45.3% / 0.1)')};
|
||||
}
|
||||
|
||||
.source {
|
||||
color: ${cssManager.bdTheme('#6c757d', '#888')};
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 45.1%)', 'hsl(0 0% 63.9%)')};
|
||||
margin-right: 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.message {
|
||||
color: ${cssManager.bdTheme('#212529', '#ddd')};
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 15%)', 'hsl(0 0% 90%)')};
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
@ -183,7 +189,7 @@ export class DeesChartLog extends DeesElement {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
color: ${cssManager.bdTheme('#6c757d', '#666')};
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 45.1%)', 'hsl(0 0% 63.9%)')};
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
@ -193,16 +199,16 @@ export class DeesChartLog extends DeesElement {
|
||||
}
|
||||
|
||||
.logContainer::-webkit-scrollbar-track {
|
||||
background: ${cssManager.bdTheme('#e9ecef', '#1a1a1a')};
|
||||
background: ${cssManager.bdTheme('hsl(0 0% 95%)', 'hsl(0 0% 10%)')};
|
||||
}
|
||||
|
||||
.logContainer::-webkit-scrollbar-thumb {
|
||||
background: ${cssManager.bdTheme('#adb5bd', '#444')};
|
||||
background: ${cssManager.bdTheme('hsl(0 0% 70%)', 'hsl(0 0% 30%)')};
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.logContainer::-webkit-scrollbar-thumb:hover {
|
||||
background: ${cssManager.bdTheme('#6c757d', '#555')};
|
||||
background: ${cssManager.bdTheme('hsl(0 0% 60%)', 'hsl(0 0% 40%)')};
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
@ -1,41 +1,112 @@
|
||||
import { html } from '@design.estate/dees-element';
|
||||
import { html, cssManager } from '@design.estate/dees-element';
|
||||
|
||||
export const demoFunc = () => html`
|
||||
<style>
|
||||
.demoContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
background: #222;
|
||||
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')};
|
||||
}
|
||||
|
||||
.section-description {
|
||||
font-size: 14px;
|
||||
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
</style>
|
||||
<div class="demoContainer">
|
||||
<div class="section">
|
||||
<div class="section-title">Non-Selectable Chips</div>
|
||||
<div class="section-description">Basic chips without selection capability. Use for display-only tags.</div>
|
||||
<dees-chips
|
||||
selectionMode="none"
|
||||
.selectableChips=${[
|
||||
{ key: 'account1', value: 'Payment Account 1' },
|
||||
{ key: 'account2', value: 'PaymentAccount2' },
|
||||
{ key: 'account3', value: 'Payment Account 3' },
|
||||
{ key: 'status', value: 'Active' },
|
||||
{ key: 'tier', value: 'Premium' },
|
||||
{ key: 'region', value: 'EU-West' },
|
||||
{ key: 'type', value: 'Enterprise' },
|
||||
]}
|
||||
></dees-chips>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<div class="section-title">Single Selection Chips</div>
|
||||
<div class="section-description">Click to select one chip at a time. Useful for filters and options.</div>
|
||||
<dees-chips
|
||||
selectionMode="single"
|
||||
.selectableChips=${[
|
||||
{ key: 'all', value: 'All Projects' },
|
||||
{ key: 'active', value: 'Active' },
|
||||
{ key: 'archived', value: 'Archived' },
|
||||
{ key: 'drafts', value: 'Drafts' },
|
||||
]}
|
||||
></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: 'account1', value: 'Payment Account 1' },
|
||||
{ key: 'account2', value: 'PaymentAccount2' },
|
||||
{ key: 'account3', value: 'Payment Account 3' },
|
||||
]}
|
||||
></dees-chips>
|
||||
<dees-chips
|
||||
selectionMode="multiple"
|
||||
.selectableChips=${[
|
||||
{ key: 'account1', value: 'Payment Account 1' },
|
||||
{ key: 'account2', value: 'PaymentAccount2' },
|
||||
{ key: 'account3', value: 'Payment Account 3' },
|
||||
{ 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>
|
||||
`;
|
||||
|
@ -60,52 +60,93 @@ export class DeesChips extends DeesElement {
|
||||
|
||||
.mainbox {
|
||||
user-select: none;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.chip {
|
||||
border-top: ${cssManager.bdTheme('1px solid #CCC', '1px solid #444')};
|
||||
background: #333333;
|
||||
background: ${cssManager.bdTheme('#f4f4f5', '#27272a')};
|
||||
border: 1px solid ${cssManager.bdTheme('#e5e7eb', '#3f3f46')};
|
||||
display: inline-flex;
|
||||
height: 20px;
|
||||
line-height: 20px;
|
||||
padding: 0px 8px;
|
||||
font-size: 12px;
|
||||
color: #fff;
|
||||
border-radius: 40px;
|
||||
margin-right: 4px;
|
||||
margin-bottom: 4px;
|
||||
align-items: center;
|
||||
height: 32px;
|
||||
padding: 0px 12px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: ${cssManager.bdTheme('#09090b', '#fafafa')};
|
||||
border-radius: 6px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.3);
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.chip:hover {
|
||||
background: #666666;
|
||||
background: ${cssManager.bdTheme('#e5e7eb', '#3f3f46')};
|
||||
border-color: ${cssManager.bdTheme('#d1d5db', '#52525b')};
|
||||
}
|
||||
|
||||
.chip:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
.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 {
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
height: 100%;
|
||||
display: inline-block;
|
||||
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.06)', 'rgba(255, 255, 255, 0.1)')};
|
||||
height: 20px;
|
||||
line-height: 20px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
margin-left: -8px;
|
||||
padding-left: 8px;
|
||||
padding-right: 8px;
|
||||
padding: 0px 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 {
|
||||
padding: 0px 6px 0px 4px;
|
||||
margin-left: 4px;
|
||||
margin-right: -8px;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
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 {
|
||||
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() {
|
||||
if (!this.textContent) {
|
||||
this.textContent = 'Button';
|
||||
this.performUpdate();
|
||||
}
|
||||
// Component initialized
|
||||
}
|
||||
|
||||
private isSelected(chip: Tag): boolean {
|
||||
if (this.selectionMode === 'single') {
|
||||
return this.selectedChip?.key === chip.key;
|
||||
return this.selectedChip ? this.isSameChip(this.selectedChip, chip) : false;
|
||||
} 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) {
|
||||
if (this.selectionMode === 'none') {
|
||||
return;
|
||||
@ -168,7 +215,7 @@ export class DeesChips extends DeesElement {
|
||||
}
|
||||
} else if (this.selectionMode === 'multiple') {
|
||||
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 {
|
||||
this.selectedChips = [...this.selectedChips, chip];
|
||||
}
|
||||
@ -179,13 +226,13 @@ export class DeesChips extends DeesElement {
|
||||
|
||||
public removeChip(chipToRemove: Tag): void {
|
||||
// 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
|
||||
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 (this.selectedChip && this.selectedChip.key === chipToRemove.key) {
|
||||
if (this.selectedChip && this.isSameChip(this.selectedChip, chipToRemove)) {
|
||||
this.selectedChip = null;
|
||||
}
|
||||
|
||||
|
@ -13,139 +13,203 @@ export const demoFunc = () => html`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
padding: 40px;
|
||||
background: #f5f5f5;
|
||||
padding: 20px;
|
||||
min-height: 400px;
|
||||
}
|
||||
.demo-area {
|
||||
background: white;
|
||||
padding: 40px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #e0e0e0;
|
||||
text-align: center;
|
||||
cursor: context-menu;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
.demo-area:hover {
|
||||
background: rgba(0, 0, 0, 0.02);
|
||||
}
|
||||
</style>
|
||||
<div class="demo-container">
|
||||
<dees-panel heading="Basic Context Menu with Nested Submenus">
|
||||
<div class="demo-area" @contextmenu=${(eventArg: MouseEvent) => {
|
||||
DeesContextmenu.openContextMenuWithOptions(eventArg, [
|
||||
{
|
||||
name: 'Cut',
|
||||
iconName: 'scissors',
|
||||
shortcut: 'Cmd+X',
|
||||
action: async () => {
|
||||
console.log('Cut action');
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Copy',
|
||||
iconName: 'copy',
|
||||
shortcut: 'Cmd+C',
|
||||
action: async () => {
|
||||
console.log('Copy action');
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Paste',
|
||||
iconName: 'clipboard',
|
||||
shortcut: 'Cmd+V',
|
||||
action: async () => {
|
||||
console.log('Paste action');
|
||||
},
|
||||
},
|
||||
name: 'File',
|
||||
iconName: 'fileText',
|
||||
action: async () => {}, // Parent items with submenus still need an 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: 'Delete',
|
||||
iconName: 'trash2',
|
||||
action: async () => {
|
||||
console.log('Delete action');
|
||||
},
|
||||
},
|
||||
name: 'Edit',
|
||||
iconName: 'edit3',
|
||||
action: async () => {}, // Parent items with submenus still need an action
|
||||
submenu: [
|
||||
{ name: 'Cut', iconName: 'scissors', shortcut: 'Cmd+X', action: async () => console.log('Cut') },
|
||||
{ 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: 'Select All',
|
||||
shortcut: 'Cmd+A',
|
||||
action: async () => {
|
||||
console.log('Select All action');
|
||||
},
|
||||
},
|
||||
]);
|
||||
}}>
|
||||
<h3>Right-click anywhere in this area</h3>
|
||||
<p>A context menu will appear with various options</p>
|
||||
</div>
|
||||
|
||||
<dees-button @contextmenu=${(eventArg: MouseEvent) => {
|
||||
DeesContextmenu.openContextMenuWithOptions(eventArg, [
|
||||
{
|
||||
name: 'Button Action 1',
|
||||
iconName: 'play',
|
||||
action: async () => {
|
||||
console.log('Button action 1');
|
||||
},
|
||||
{ name: 'Find', iconName: 'search', shortcut: 'Cmd+F', action: async () => console.log('Find') },
|
||||
{ name: 'Replace', iconName: 'repeat', shortcut: 'Cmd+H', action: async () => console.log('Replace') },
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'Button Action 2',
|
||||
iconName: 'pause',
|
||||
action: async () => {
|
||||
console.log('Button action 2');
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Disabled Action',
|
||||
iconName: 'ban',
|
||||
disabled: true,
|
||||
action: async () => {
|
||||
console.log('This should not run');
|
||||
},
|
||||
name: 'View',
|
||||
iconName: 'eye',
|
||||
action: async () => {}, // Parent items with submenus still need an action
|
||||
submenu: [
|
||||
{ name: 'Zoom In', iconName: 'zoomIn', shortcut: 'Cmd++', action: async () => console.log('Zoom in') },
|
||||
{ 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 },
|
||||
{
|
||||
name: 'Settings',
|
||||
iconName: 'settings',
|
||||
action: async () => {
|
||||
console.log('Settings');
|
||||
action: async () => console.log('Settings')
|
||||
},
|
||||
{
|
||||
name: 'Help',
|
||||
iconName: 'helpCircle',
|
||||
action: async () => {}, // Parent items with submenus still need an action
|
||||
submenu: [
|
||||
{ name: 'Documentation', iconName: 'book', action: async () => console.log('Documentation') },
|
||||
{ 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',
|
||||
action: async () => {}, // Parent items with submenus still need an action
|
||||
submenu: [
|
||||
{ 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 for a different menu</dees-button>
|
||||
}}>Right-click on this button</dees-button>
|
||||
</dees-panel>
|
||||
|
||||
<div style="margin-top: 20px;">
|
||||
<h4>Static Context Menu (always visible):</h4>
|
||||
<dees-panel heading="Advanced Context Menu Example">
|
||||
<div class="demo-area" @contextmenu=${(eventArg: MouseEvent) => {
|
||||
DeesContextmenu.openContextMenuWithOptions(eventArg, [
|
||||
{
|
||||
name: 'Format',
|
||||
iconName: 'type',
|
||||
action: async () => {}, // Parent items with submenus still need an action
|
||||
submenu: [
|
||||
{ name: 'Bold', iconName: 'bold', shortcut: 'Cmd+B', action: async () => console.log('Bold') },
|
||||
{ name: 'Italic', iconName: 'italic', shortcut: 'Cmd+I', action: async () => console.log('Italic') },
|
||||
{ name: 'Underline', iconName: 'underline', shortcut: 'Cmd+U', action: async () => console.log('Underline') },
|
||||
{ divider: true },
|
||||
{ name: 'Font Size', iconName: 'type', action: async () => console.log('Font size menu') },
|
||||
{ name: 'Font Color', iconName: 'palette', action: async () => console.log('Font color menu') },
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'Transform',
|
||||
iconName: 'shuffle',
|
||||
action: async () => {}, // Parent items with submenus still need an action
|
||||
submenu: [
|
||||
{ name: 'To Uppercase', iconName: 'arrowUp', action: async () => console.log('Uppercase') },
|
||||
{ name: 'To Lowercase', iconName: 'arrowDown', action: async () => console.log('Lowercase') },
|
||||
{ name: 'Capitalize', iconName: 'type', action: async () => console.log('Capitalize') },
|
||||
]
|
||||
},
|
||||
{ divider: true },
|
||||
{
|
||||
name: 'Delete',
|
||||
iconName: 'trash2',
|
||||
action: async () => console.log('Delete')
|
||||
}
|
||||
]);
|
||||
}}>
|
||||
<h3>Advanced Nested Menu Example</h3>
|
||||
<p>This shows deeply nested submenus and various formatting options</p>
|
||||
</div>
|
||||
</dees-panel>
|
||||
|
||||
<dees-panel heading="Static Context Menu (Always Visible)">
|
||||
<dees-contextmenu
|
||||
class="withMargin"
|
||||
.menuItems=${[
|
||||
{
|
||||
name: 'New File',
|
||||
iconName: 'filePlus',
|
||||
shortcut: 'Cmd+N',
|
||||
action: async () => console.log('New file'),
|
||||
name: 'Project',
|
||||
iconName: 'folder',
|
||||
action: async () => {}, // Parent items with submenus still need an action
|
||||
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',
|
||||
iconName: 'folderOpen',
|
||||
shortcut: 'Cmd+O',
|
||||
action: async () => console.log('Open file'),
|
||||
},
|
||||
{
|
||||
name: 'Save',
|
||||
iconName: 'save',
|
||||
shortcut: 'Cmd+S',
|
||||
action: async () => console.log('Save'),
|
||||
name: 'Tools',
|
||||
iconName: 'tool',
|
||||
action: async () => {}, // Parent items with submenus still need an action
|
||||
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') },
|
||||
{ divider: true },
|
||||
{ name: 'Extensions', iconName: 'package', action: async () => console.log('Extensions') },
|
||||
]
|
||||
},
|
||||
{ divider: true },
|
||||
{
|
||||
name: 'Export',
|
||||
iconName: 'download',
|
||||
action: async () => console.log('Export'),
|
||||
},
|
||||
{
|
||||
name: 'Import',
|
||||
iconName: 'upload',
|
||||
action: async () => console.log('Import'),
|
||||
name: 'Preferences',
|
||||
iconName: 'sliders',
|
||||
action: async () => console.log('Preferences'),
|
||||
},
|
||||
]}
|
||||
></dees-contextmenu>
|
||||
</div>
|
||||
</dees-panel>
|
||||
</div>
|
||||
`;
|
@ -31,7 +31,7 @@ export class DeesContextmenu extends DeesElement {
|
||||
// STATIC
|
||||
// This will store all the accumulated menu items
|
||||
public static contextMenuDeactivated = false;
|
||||
public static accumulatedMenuItems: (plugins.tsclass.website.IMenuItem & { 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
|
||||
public static initializeGlobalListener() {
|
||||
@ -41,16 +41,16 @@ export class DeesContextmenu extends DeesElement {
|
||||
}
|
||||
event.preventDefault();
|
||||
|
||||
// Get the target element of the right-click
|
||||
let target: EventTarget | null = event.target;
|
||||
|
||||
// Clear previously accumulated items
|
||||
DeesContextmenu.accumulatedMenuItems = [];
|
||||
|
||||
// Traverse up the DOM tree to accumulate menu items
|
||||
while (target) {
|
||||
if ((target as any).getContextMenuItems) {
|
||||
const items = (target as any).getContextMenuItems();
|
||||
// Use composedPath to properly traverse shadow DOM boundaries
|
||||
const path = event.composedPath();
|
||||
|
||||
// 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 (DeesContextmenu.accumulatedMenuItems.length > 0) {
|
||||
DeesContextmenu.accumulatedMenuItems.push({ divider: true });
|
||||
@ -58,7 +58,6 @@ export class DeesContextmenu extends DeesElement {
|
||||
DeesContextmenu.accumulatedMenuItems.push(...items);
|
||||
}
|
||||
}
|
||||
target = (target as Node).parentNode;
|
||||
}
|
||||
|
||||
// Open the context menu with the accumulated items
|
||||
@ -67,7 +66,7 @@ export class DeesContextmenu extends DeesElement {
|
||||
}
|
||||
|
||||
// allows opening of a contextmenu with options
|
||||
public static async openContextMenuWithOptions(eventArg: MouseEvent, menuItemsArg: (plugins.tsclass.website.IMenuItem & { 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) {
|
||||
return;
|
||||
}
|
||||
@ -80,8 +79,13 @@ export class DeesContextmenu extends DeesElement {
|
||||
contextMenu.style.transform = 'scale(0.95) translateY(-10px)';
|
||||
contextMenu.menuItems = menuItemsArg;
|
||||
contextMenu.windowLayer = await DeesWindowLayer.createAndShow();
|
||||
contextMenu.windowLayer.addEventListener('click', async () => {
|
||||
contextMenu.windowLayer.addEventListener('click', async (event) => {
|
||||
// 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);
|
||||
|
||||
@ -123,9 +127,13 @@ export class DeesContextmenu extends DeesElement {
|
||||
@property({
|
||||
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;
|
||||
|
||||
private submenu: DeesContextmenu | null = null;
|
||||
private submenuTimeout: any = null;
|
||||
private parentMenu: DeesContextmenu | null = null;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.tabIndex = 0;
|
||||
@ -167,13 +175,22 @@ export class DeesContextmenu extends DeesElement {
|
||||
cursor: default;
|
||||
transition: background 0.1s;
|
||||
line-height: 1;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.menuitem:hover {
|
||||
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)')};
|
||||
}
|
||||
|
||||
@ -215,14 +232,20 @@ export class DeesContextmenu extends DeesElement {
|
||||
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`
|
||||
<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`
|
||||
<dees-icon .icon="${`lucide:${menuItem.iconName}`}"></dees-icon>
|
||||
` : ''}
|
||||
<span class="menuitem-text">${menuItem.name}</span>
|
||||
${menuItem.shortcut ? html`
|
||||
${menuItem.shortcut && !hasSubmenu ? html`
|
||||
<span class="menuitem-shortcut">${menuItem.shortcut}</span>
|
||||
` : ''}
|
||||
</div>
|
||||
@ -282,18 +305,152 @@ export class DeesContextmenu extends DeesElement {
|
||||
|
||||
public async handleClick(menuItem: plugins.tsclass.website.IMenuItem & { shortcut?: string; disabled?: boolean }) {
|
||||
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() {
|
||||
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.style.opacity = '0';
|
||||
this.style.transform = 'scale(0.95) translateY(-10px)';
|
||||
await domtools.plugins.smartdelay.delayFor(100);
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DeesContextmenu.initializeGlobalListener();
|
||||
|
191
ts_web/elements/dees-dashboardgrid.demo.ts
Normal file
191
ts_web/elements/dees-dashboardgrid.demo.ts
Normal file
@ -0,0 +1,191 @@
|
||||
import { html, css, cssManager } from '@design.estate/dees-element';
|
||||
import type { DeesDashboardgrid } from './dees-dashboardgrid.js';
|
||||
import '@design.estate/dees-wcctools/demotools';
|
||||
|
||||
export const demoFunc = () => {
|
||||
return html`
|
||||
<dees-demowrapper .runAfterRender=${async (elementArg: HTMLElement) => {
|
||||
const grid = elementArg.querySelector('#dashboardGrid') as DeesDashboardgrid;
|
||||
|
||||
// Set initial widgets
|
||||
grid.widgets = [
|
||||
{
|
||||
id: 'metrics1',
|
||||
x: 0,
|
||||
y: 0,
|
||||
w: 3,
|
||||
h: 2,
|
||||
title: 'Revenue',
|
||||
icon: 'lucide:dollarSign',
|
||||
content: html`
|
||||
<div style="padding: 20px;">
|
||||
<div style="font-size: 32px; font-weight: 700; color: ${cssManager.bdTheme('#09090b', '#fafafa')};">$124,563</div>
|
||||
<div style="color: #22c55e; font-size: 14px; margin-top: 8px;">↑ 12.5% from last month</div>
|
||||
</div>
|
||||
`
|
||||
},
|
||||
{
|
||||
id: 'metrics2',
|
||||
x: 3,
|
||||
y: 0,
|
||||
w: 3,
|
||||
h: 2,
|
||||
title: 'Users',
|
||||
icon: 'lucide:users',
|
||||
content: html`
|
||||
<div style="padding: 20px;">
|
||||
<div style="font-size: 32px; font-weight: 700; color: ${cssManager.bdTheme('#09090b', '#fafafa')};">8,234</div>
|
||||
<div style="color: #3b82f6; font-size: 14px; margin-top: 8px;">↑ 5.2% from last week</div>
|
||||
</div>
|
||||
`
|
||||
},
|
||||
{
|
||||
id: 'chart1',
|
||||
x: 6,
|
||||
y: 0,
|
||||
w: 6,
|
||||
h: 4,
|
||||
title: 'Analytics',
|
||||
icon: 'lucide:lineChart',
|
||||
content: html`
|
||||
<div style="padding: 20px; height: 100%; display: flex; align-items: center; justify-content: center;">
|
||||
<div style="text-align: center; color: #71717a;">
|
||||
<dees-icon .icon=${'lucide:lineChart'} style="font-size: 48px; margin-bottom: 16px;"></dees-icon>
|
||||
<div>Chart visualization area</div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
];
|
||||
|
||||
// Configure grid
|
||||
grid.cellHeight = 80;
|
||||
grid.margin = { top: 10, right: 10, bottom: 10, left: 10 };
|
||||
grid.enableAnimation = true;
|
||||
grid.showGridLines = false;
|
||||
|
||||
let widgetCounter = 4;
|
||||
|
||||
// Control buttons
|
||||
const buttons = elementArg.querySelectorAll('dees-button');
|
||||
buttons.forEach(button => {
|
||||
const text = button.textContent?.trim();
|
||||
|
||||
if (text === 'Toggle Animation') {
|
||||
button.addEventListener('click', () => {
|
||||
grid.enableAnimation = !grid.enableAnimation;
|
||||
});
|
||||
} else if (text === 'Toggle Grid Lines') {
|
||||
button.addEventListener('click', () => {
|
||||
grid.showGridLines = !grid.showGridLines;
|
||||
});
|
||||
} else if (text === 'Add Widget') {
|
||||
button.addEventListener('click', () => {
|
||||
const newWidget = {
|
||||
id: `widget${widgetCounter++}`,
|
||||
x: 0,
|
||||
y: 0,
|
||||
w: 3,
|
||||
h: 2,
|
||||
autoPosition: true,
|
||||
title: `Widget ${widgetCounter - 1}`,
|
||||
icon: 'lucide:package',
|
||||
content: html`
|
||||
<div style="padding: 20px; text-align: center;">
|
||||
<div style="color: #71717a;">New widget content</div>
|
||||
<div style="margin-top: 8px; font-size: 24px; font-weight: 600; color: ${cssManager.bdTheme('#09090b', '#fafafa')};">${Math.floor(Math.random() * 1000)}</div>
|
||||
</div>
|
||||
`
|
||||
};
|
||||
grid.addWidget(newWidget, true);
|
||||
});
|
||||
} else if (text === 'Compact Grid') {
|
||||
button.addEventListener('click', () => {
|
||||
grid.compact();
|
||||
});
|
||||
} else if (text === 'Toggle Edit Mode') {
|
||||
button.addEventListener('click', () => {
|
||||
grid.editable = !grid.editable;
|
||||
button.textContent = grid.editable ? 'Lock Grid' : 'Unlock Grid';
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Listen to grid events
|
||||
grid.addEventListener('widget-move', (e: CustomEvent) => {
|
||||
console.log('Widget moved:', e.detail.widget);
|
||||
});
|
||||
|
||||
grid.addEventListener('widget-resize', (e: CustomEvent) => {
|
||||
console.log('Widget resized:', e.detail.widget);
|
||||
});
|
||||
}}>
|
||||
<style>
|
||||
${css`
|
||||
.demoBox {
|
||||
position: relative;
|
||||
background: ${cssManager.bdTheme('#f4f4f5', '#09090b')};
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
padding: 40px;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.demo-controls {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.demo-controls dees-button {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.grid-container-wrapper {
|
||||
flex: 1;
|
||||
min-height: 600px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.info {
|
||||
color: ${cssManager.bdTheme('#71717a', '#71717a')};
|
||||
font-size: 12px;
|
||||
font-family: 'Geist Sans', sans-serif;
|
||||
text-align: center;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
<div class="demoBox">
|
||||
<div class="demo-controls">
|
||||
<dees-button-group label="Animation:">
|
||||
<dees-button>Toggle Animation</dees-button>
|
||||
</dees-button-group>
|
||||
|
||||
<dees-button-group label="Display:">
|
||||
<dees-button>Toggle Grid Lines</dees-button>
|
||||
</dees-button-group>
|
||||
|
||||
<dees-button-group label="Actions:">
|
||||
<dees-button>Add Widget</dees-button>
|
||||
<dees-button>Compact Grid</dees-button>
|
||||
</dees-button-group>
|
||||
|
||||
<dees-button-group label="Mode:">
|
||||
<dees-button>Toggle Edit Mode</dees-button>
|
||||
</dees-button-group>
|
||||
</div>
|
||||
|
||||
<div class="grid-container-wrapper">
|
||||
<dees-dashboardgrid id="dashboardGrid"></dees-dashboardgrid>
|
||||
</div>
|
||||
|
||||
<div class="info">
|
||||
Drag widgets to reposition • Resize from edges and corners • Add widgets with auto-positioning
|
||||
</div>
|
||||
</div>
|
||||
</dees-demowrapper>
|
||||
`;
|
||||
};
|
813
ts_web/elements/dees-dashboardgrid.ts
Normal file
813
ts_web/elements/dees-dashboardgrid.ts
Normal file
@ -0,0 +1,813 @@
|
||||
import * as plugins from './00plugins.js';
|
||||
import {
|
||||
DeesElement,
|
||||
type TemplateResult,
|
||||
property,
|
||||
customElement,
|
||||
html,
|
||||
css,
|
||||
cssManager,
|
||||
state,
|
||||
} from '@design.estate/dees-element';
|
||||
|
||||
import * as domtools from '@design.estate/dees-domtools';
|
||||
import './dees-icon.js';
|
||||
import { demoFunc } from './dees-dashboardgrid.demo.js';
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'dees-dashboardgrid': DeesDashboardgrid;
|
||||
}
|
||||
}
|
||||
|
||||
export interface IDashboardWidget {
|
||||
id: string;
|
||||
x: number;
|
||||
y: number;
|
||||
w: number;
|
||||
h: number;
|
||||
minW?: number;
|
||||
minH?: number;
|
||||
maxW?: number;
|
||||
maxH?: number;
|
||||
content: TemplateResult | string;
|
||||
title?: string;
|
||||
icon?: string;
|
||||
noMove?: boolean;
|
||||
noResize?: boolean;
|
||||
locked?: boolean;
|
||||
autoPosition?: boolean; // Auto-position widget in first available space
|
||||
}
|
||||
|
||||
@customElement('dees-dashboardgrid')
|
||||
export class DeesDashboardgrid extends DeesElement {
|
||||
// STATIC
|
||||
public static demo = demoFunc;
|
||||
|
||||
// INSTANCE
|
||||
@property({ type: Array })
|
||||
public widgets: IDashboardWidget[] = [];
|
||||
|
||||
@property({ type: Number })
|
||||
public cellHeight: number = 80;
|
||||
|
||||
@property({ type: Object })
|
||||
public margin: number | { top?: number; right?: number; bottom?: number; left?: number } = 10;
|
||||
|
||||
@property({ type: Number })
|
||||
public columns: number = 12;
|
||||
|
||||
@property({ type: Boolean })
|
||||
public editable: boolean = true;
|
||||
|
||||
@property({ type: Boolean, reflect: true })
|
||||
public enableAnimation: boolean = true;
|
||||
|
||||
@property({ type: String })
|
||||
public cellHeightUnit: 'px' | 'em' | 'rem' | 'auto' = 'px';
|
||||
|
||||
@property({ type: Boolean })
|
||||
public rtl: boolean = false; // Right-to-left support
|
||||
|
||||
@property({ type: Boolean })
|
||||
public showGridLines: boolean = false;
|
||||
|
||||
@state()
|
||||
private draggedWidget: IDashboardWidget | null = null;
|
||||
|
||||
@state()
|
||||
private draggedElement: HTMLElement | null = null;
|
||||
|
||||
@state()
|
||||
private dragOffsetX: number = 0;
|
||||
|
||||
@state()
|
||||
private dragOffsetY: number = 0;
|
||||
|
||||
@state()
|
||||
private dragMouseX: number = 0;
|
||||
|
||||
@state()
|
||||
private dragMouseY: number = 0;
|
||||
|
||||
@state()
|
||||
private placeholderPosition: { x: number; y: number } | null = null;
|
||||
|
||||
@state()
|
||||
private resizingWidget: IDashboardWidget | null = null;
|
||||
|
||||
@state()
|
||||
private resizeStartW: number = 0;
|
||||
|
||||
@state()
|
||||
private resizeStartH: number = 0;
|
||||
|
||||
@state()
|
||||
private resizeStartX: number = 0;
|
||||
|
||||
@state()
|
||||
private resizeStartY: number = 0;
|
||||
|
||||
public static styles = [
|
||||
cssManager.defaultStyles,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.grid-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
min-height: 400px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.grid-widget {
|
||||
position: absolute;
|
||||
will-change: auto;
|
||||
}
|
||||
|
||||
:host([enableanimation]) .grid-widget {
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.grid-widget.dragging {
|
||||
z-index: 1000;
|
||||
transition: none !important;
|
||||
opacity: 0.8;
|
||||
cursor: grabbing;
|
||||
pointer-events: none;
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
.grid-widget.placeholder {
|
||||
pointer-events: none;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.grid-widget.placeholder .widget-content {
|
||||
background: ${cssManager.bdTheme('rgba(59, 130, 246, 0.1)', 'rgba(59, 130, 246, 0.1)')};
|
||||
border: 2px dashed ${cssManager.bdTheme('#3b82f6', '#3b82f6')};
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.grid-widget.resizing {
|
||||
transition: none !important;
|
||||
}
|
||||
|
||||
.widget-content {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
overflow: hidden;
|
||||
background: ${cssManager.bdTheme('#ffffff', '#09090b')};
|
||||
border: 1px solid ${cssManager.bdTheme('#e5e7eb', '#27272a')};
|
||||
border-radius: 8px;
|
||||
box-shadow: ${cssManager.bdTheme(
|
||||
'0 1px 3px rgba(0, 0, 0, 0.1)',
|
||||
'0 1px 3px rgba(0, 0, 0, 0.3)'
|
||||
)};
|
||||
transition: box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.grid-widget:hover .widget-content {
|
||||
box-shadow: ${cssManager.bdTheme(
|
||||
'0 4px 12px rgba(0, 0, 0, 0.15)',
|
||||
'0 4px 12px rgba(0, 0, 0, 0.4)'
|
||||
)};
|
||||
}
|
||||
|
||||
.grid-widget.dragging .widget-content {
|
||||
box-shadow: ${cssManager.bdTheme(
|
||||
'0 16px 48px rgba(0, 0, 0, 0.25)',
|
||||
'0 16px 48px rgba(0, 0, 0, 0.6)'
|
||||
)};
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.widget-header {
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid ${cssManager.bdTheme('#e5e7eb', '#27272a')};
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: ${cssManager.bdTheme('#09090b', '#fafafa')};
|
||||
background: ${cssManager.bdTheme('#f9fafb', '#0a0a0a')};
|
||||
cursor: grab;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.widget-header:hover {
|
||||
background: ${cssManager.bdTheme('#f4f4f5', '#18181b')};
|
||||
}
|
||||
|
||||
.widget-header:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.widget-header.locked {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.widget-header.locked:hover {
|
||||
background: ${cssManager.bdTheme('#f9fafb', '#0a0a0a')};
|
||||
}
|
||||
|
||||
.widget-header dees-icon {
|
||||
font-size: 16px;
|
||||
color: ${cssManager.bdTheme('#71717a', '#71717a')};
|
||||
}
|
||||
|
||||
.widget-body {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
overflow: auto;
|
||||
color: ${cssManager.bdTheme('#09090b', '#fafafa')};
|
||||
}
|
||||
|
||||
.widget-body.has-header {
|
||||
top: 45px;
|
||||
}
|
||||
|
||||
/* Resize handles */
|
||||
.resize-handle {
|
||||
position: absolute;
|
||||
background: transparent;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.resize-handle:hover {
|
||||
background: ${cssManager.bdTheme('#3b82f6', '#3b82f6')};
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.resize-handle-e {
|
||||
cursor: ew-resize;
|
||||
width: 12px;
|
||||
right: -6px;
|
||||
top: 10%;
|
||||
height: 80%;
|
||||
}
|
||||
|
||||
.resize-handle-s {
|
||||
cursor: ns-resize;
|
||||
height: 12px;
|
||||
width: 80%;
|
||||
bottom: -6px;
|
||||
left: 10%;
|
||||
}
|
||||
|
||||
.resize-handle-se {
|
||||
cursor: se-resize;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
right: -2px;
|
||||
bottom: -2px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.resize-handle-se::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
right: 4px;
|
||||
bottom: 4px;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-right: 2px solid ${cssManager.bdTheme('#71717a', '#71717a')};
|
||||
border-bottom: 2px solid ${cssManager.bdTheme('#71717a', '#71717a')};
|
||||
}
|
||||
|
||||
.grid-widget:hover .resize-handle-se {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.resize-handle-se:hover {
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
.resize-handle-se:hover::after {
|
||||
border-color: ${cssManager.bdTheme('#3b82f6', '#3b82f6')};
|
||||
}
|
||||
|
||||
/* Placeholder */
|
||||
.grid-placeholder {
|
||||
position: absolute;
|
||||
background: ${cssManager.bdTheme('#3b82f6', '#3b82f6')};
|
||||
opacity: 0.1;
|
||||
border-radius: 8px;
|
||||
border: 2px dashed ${cssManager.bdTheme('#3b82f6', '#3b82f6')};
|
||||
transition: all 0.2s ease;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Empty state */
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 400px;
|
||||
color: ${cssManager.bdTheme('#71717a', '#71717a')};
|
||||
text-align: center;
|
||||
padding: 32px;
|
||||
}
|
||||
|
||||
.empty-state dees-icon {
|
||||
font-size: 48px;
|
||||
margin-bottom: 16px;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
/* Grid lines */
|
||||
.grid-lines {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
pointer-events: none;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.grid-line-vertical {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 1px;
|
||||
background: ${cssManager.bdTheme('#e5e7eb', '#27272a')};
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.grid-line-horizontal {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 1px;
|
||||
background: ${cssManager.bdTheme('#e5e7eb', '#27272a')};
|
||||
opacity: 0.3;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
public render(): TemplateResult {
|
||||
if (this.widgets.length === 0) {
|
||||
return html`
|
||||
<div class="empty-state">
|
||||
<dees-icon .icon=${'lucide:layoutGrid'}></dees-icon>
|
||||
<div>No widgets configured</div>
|
||||
<div style="font-size: 14px; margin-top: 8px;">Add widgets to populate the dashboard</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
const margins = this.getMargins();
|
||||
const maxY = Math.max(...this.widgets.map(w => w.y + w.h), 4);
|
||||
const cellHeightValue = this.getCellHeight();
|
||||
const gridHeight = maxY * cellHeightValue + (maxY + 1) * margins.vertical;
|
||||
|
||||
return html`
|
||||
<div class="grid-container" style="height: ${gridHeight}px;">
|
||||
${this.showGridLines ? this.renderGridLines(gridHeight) : ''}
|
||||
${this.widgets.map(widget => this.renderWidget(widget))}
|
||||
${this.placeholderPosition && this.draggedWidget ? this.renderPlaceholder() : ''}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderGridLines(gridHeight: number): TemplateResult {
|
||||
const margins = this.getMargins();
|
||||
const cellHeightValue = this.getCellHeight();
|
||||
|
||||
// Convert margin to percentage for consistent calculation
|
||||
const containerWidth = this.getBoundingClientRect().width;
|
||||
const marginHorizontalPercent = (margins.horizontal / containerWidth) * 100;
|
||||
|
||||
const cellWidth = (100 - marginHorizontalPercent * (this.columns + 1)) / this.columns;
|
||||
|
||||
const verticalLines = [];
|
||||
const horizontalLines = [];
|
||||
|
||||
// Vertical lines
|
||||
for (let i = 0; i <= this.columns; i++) {
|
||||
const left = i * cellWidth + i * marginHorizontalPercent;
|
||||
verticalLines.push(html`
|
||||
<div class="grid-line-vertical" style="left: ${left}%;"></div>
|
||||
`);
|
||||
}
|
||||
|
||||
// Horizontal lines
|
||||
const numHorizontalLines = Math.ceil(gridHeight / (cellHeightValue + margins.vertical));
|
||||
for (let i = 0; i <= numHorizontalLines; i++) {
|
||||
const top = i * cellHeightValue + i * margins.vertical;
|
||||
horizontalLines.push(html`
|
||||
<div class="grid-line-horizontal" style="top: ${top}px;"></div>
|
||||
`);
|
||||
}
|
||||
|
||||
return html`
|
||||
<div class="grid-lines">
|
||||
${verticalLines}
|
||||
${horizontalLines}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderWidget(widget: IDashboardWidget): TemplateResult {
|
||||
const isDragging = this.draggedWidget?.id === widget.id;
|
||||
const isResizing = this.resizingWidget?.id === widget.id;
|
||||
const isLocked = widget.locked || !this.editable;
|
||||
|
||||
const margins = this.getMargins();
|
||||
const cellHeightValue = this.getCellHeight();
|
||||
|
||||
// Convert margin to percentage of container width for consistent calculation
|
||||
const containerWidth = this.getBoundingClientRect().width;
|
||||
const marginHorizontalPercent = (margins.horizontal / containerWidth) * 100;
|
||||
|
||||
const cellWidth = (100 - marginHorizontalPercent * (this.columns + 1)) / this.columns;
|
||||
|
||||
const left = widget.x * cellWidth + (widget.x + 1) * marginHorizontalPercent;
|
||||
const top = widget.y * cellHeightValue + (widget.y + 1) * margins.vertical;
|
||||
const width = widget.w * cellWidth + (widget.w - 1) * marginHorizontalPercent;
|
||||
const height = widget.h * cellHeightValue + (widget.h - 1) * margins.vertical;
|
||||
|
||||
// Apply transform when dragging for smooth movement
|
||||
let transform = '';
|
||||
if (isDragging && this.draggedElement) {
|
||||
const containerRect = this.getBoundingClientRect();
|
||||
const translateX = this.dragMouseX - containerRect.left - this.dragOffsetX - (left / 100 * containerRect.width);
|
||||
const translateY = this.dragMouseY - containerRect.top - this.dragOffsetY - top;
|
||||
transform = `transform: translate(${translateX}px, ${translateY}px);`;
|
||||
}
|
||||
|
||||
return html`
|
||||
<div
|
||||
class="grid-widget ${isDragging ? 'dragging' : ''} ${isResizing ? 'resizing' : ''}"
|
||||
style="
|
||||
${this.rtl ? 'right' : 'left'}: ${left}%;
|
||||
top: ${top}px;
|
||||
width: ${width}%;
|
||||
height: ${height}px;
|
||||
${transform}
|
||||
"
|
||||
data-widget-id="${widget.id}"
|
||||
>
|
||||
<div class="widget-content">
|
||||
${widget.title ? html`
|
||||
<div
|
||||
class="widget-header ${isLocked ? 'locked' : ''}"
|
||||
@mousedown=${!isLocked && !widget.noMove ? (e: MouseEvent) => this.startDrag(e, widget) : null}
|
||||
>
|
||||
${widget.icon ? html`<dees-icon .icon=${widget.icon}></dees-icon>` : ''}
|
||||
${widget.title}
|
||||
</div>
|
||||
` : ''}
|
||||
<div class="widget-body ${widget.title ? 'has-header' : ''}">
|
||||
${widget.content}
|
||||
</div>
|
||||
${!isLocked && !widget.noResize ? html`
|
||||
<div class="resize-handle resize-handle-e" @mousedown=${(e: MouseEvent) => this.startResize(e, widget, 'e')}></div>
|
||||
<div class="resize-handle resize-handle-s" @mousedown=${(e: MouseEvent) => this.startResize(e, widget, 's')}></div>
|
||||
<div class="resize-handle resize-handle-se" @mousedown=${(e: MouseEvent) => this.startResize(e, widget, 'se')}></div>
|
||||
` : ''}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderPlaceholder(): TemplateResult {
|
||||
if (!this.placeholderPosition || !this.draggedWidget) return html``;
|
||||
|
||||
const margins = this.getMargins();
|
||||
const cellHeightValue = this.getCellHeight();
|
||||
|
||||
// Convert margin to percentage of container width for consistent calculation
|
||||
const containerWidth = this.getBoundingClientRect().width;
|
||||
const marginHorizontalPercent = (margins.horizontal / containerWidth) * 100;
|
||||
|
||||
const cellWidth = (100 - marginHorizontalPercent * (this.columns + 1)) / this.columns;
|
||||
|
||||
const left = this.placeholderPosition.x * cellWidth + (this.placeholderPosition.x + 1) * marginHorizontalPercent;
|
||||
const top = this.placeholderPosition.y * cellHeightValue + (this.placeholderPosition.y + 1) * margins.vertical;
|
||||
const width = this.draggedWidget.w * cellWidth + (this.draggedWidget.w - 1) * marginHorizontalPercent;
|
||||
const height = this.draggedWidget.h * cellHeightValue + (this.draggedWidget.h - 1) * margins.vertical;
|
||||
|
||||
return html`
|
||||
<div
|
||||
class="grid-widget placeholder"
|
||||
style="
|
||||
${this.rtl ? 'right' : 'left'}: ${left}%;
|
||||
top: ${top}px;
|
||||
width: ${width}%;
|
||||
height: ${height}px;
|
||||
"
|
||||
>
|
||||
<div class="widget-content"></div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private startDrag(e: MouseEvent, widget: IDashboardWidget) {
|
||||
e.preventDefault();
|
||||
this.draggedWidget = widget;
|
||||
this.draggedElement = (e.currentTarget as HTMLElement).closest('.grid-widget') as HTMLElement;
|
||||
|
||||
const rect = this.draggedElement.getBoundingClientRect();
|
||||
|
||||
this.dragOffsetX = e.clientX - rect.left;
|
||||
this.dragOffsetY = e.clientY - rect.top;
|
||||
|
||||
// Initialize mouse position
|
||||
this.dragMouseX = e.clientX;
|
||||
this.dragMouseY = e.clientY;
|
||||
|
||||
// Initialize placeholder at current widget position
|
||||
this.placeholderPosition = { x: widget.x, y: widget.y };
|
||||
|
||||
document.addEventListener('mousemove', this.handleDrag);
|
||||
document.addEventListener('mouseup', this.endDrag);
|
||||
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
private handleDrag = (e: MouseEvent) => {
|
||||
if (!this.draggedWidget || !this.draggedElement) return;
|
||||
|
||||
// Update mouse position for smooth dragging
|
||||
this.dragMouseX = e.clientX;
|
||||
this.dragMouseY = e.clientY;
|
||||
|
||||
const containerRect = this.getBoundingClientRect();
|
||||
const margins = this.getMargins();
|
||||
const cellHeightValue = this.getCellHeight();
|
||||
|
||||
// Get widget position relative to grid container
|
||||
const mouseX = e.clientX - containerRect.left - this.dragOffsetX;
|
||||
const mouseY = e.clientY - containerRect.top - this.dragOffsetY;
|
||||
|
||||
// Use pixel calculations for accuracy
|
||||
const totalWidth = containerRect.width;
|
||||
const totalMarginWidth = margins.horizontal * (this.columns + 1);
|
||||
const availableWidth = totalWidth - totalMarginWidth;
|
||||
const cellWidthPx = availableWidth / this.columns;
|
||||
|
||||
// Calculate grid X position
|
||||
// Account for the initial margin and then repeating pattern of cell+margin
|
||||
let gridX = 0;
|
||||
if (mouseX > margins.horizontal) {
|
||||
const adjustedX = mouseX - margins.horizontal;
|
||||
const cellPlusMargin = cellWidthPx + margins.horizontal;
|
||||
gridX = Math.floor(adjustedX / cellPlusMargin + 0.5); // +0.5 for rounding to nearest
|
||||
}
|
||||
|
||||
// Calculate grid Y position
|
||||
let gridY = 0;
|
||||
if (mouseY > margins.vertical) {
|
||||
const adjustedY = mouseY - margins.vertical;
|
||||
const cellPlusMargin = cellHeightValue + margins.vertical;
|
||||
gridY = Math.floor(adjustedY / cellPlusMargin + 0.5); // +0.5 for rounding to nearest
|
||||
}
|
||||
|
||||
const clampedX = Math.max(0, Math.min(gridX, this.columns - this.draggedWidget.w));
|
||||
const clampedY = Math.max(0, gridY);
|
||||
|
||||
// Update placeholder position instead of widget position during drag
|
||||
if (!this.placeholderPosition ||
|
||||
clampedX !== this.placeholderPosition.x ||
|
||||
clampedY !== this.placeholderPosition.y) {
|
||||
const collision = this.checkCollision(this.draggedWidget, clampedX, clampedY);
|
||||
if (!collision) {
|
||||
this.placeholderPosition = { x: clampedX, y: clampedY };
|
||||
this.requestUpdate();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private endDrag = () => {
|
||||
// Apply final position from placeholder
|
||||
if (this.draggedWidget && this.placeholderPosition) {
|
||||
this.draggedWidget.x = this.placeholderPosition.x;
|
||||
this.draggedWidget.y = this.placeholderPosition.y;
|
||||
|
||||
this.dispatchEvent(new CustomEvent('widget-move', {
|
||||
detail: { widget: this.draggedWidget },
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}));
|
||||
}
|
||||
|
||||
// Clear drag state
|
||||
this.draggedWidget = null;
|
||||
this.draggedElement = null;
|
||||
this.placeholderPosition = null;
|
||||
this.dragMouseX = 0;
|
||||
this.dragMouseY = 0;
|
||||
|
||||
document.removeEventListener('mousemove', this.handleDrag);
|
||||
document.removeEventListener('mouseup', this.endDrag);
|
||||
|
||||
this.requestUpdate();
|
||||
};
|
||||
|
||||
private startResize(e: MouseEvent, widget: IDashboardWidget, handle: string) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this.resizingWidget = widget;
|
||||
this.resizeStartW = widget.w;
|
||||
this.resizeStartH = widget.h;
|
||||
this.resizeStartX = e.clientX;
|
||||
this.resizeStartY = e.clientY;
|
||||
|
||||
const handleResize = (e: MouseEvent) => {
|
||||
if (!this.resizingWidget) return;
|
||||
|
||||
const containerRect = this.getBoundingClientRect();
|
||||
const margins = this.getMargins();
|
||||
const cellHeightValue = this.getCellHeight();
|
||||
const cellWidth = (containerRect.width - margins.horizontal * (this.columns + 1)) / this.columns;
|
||||
|
||||
const deltaX = e.clientX - this.resizeStartX;
|
||||
const deltaY = e.clientY - this.resizeStartY;
|
||||
|
||||
if (handle.includes('e')) {
|
||||
const newW = Math.round(this.resizeStartW + deltaX / (cellWidth + margins.horizontal));
|
||||
const maxW = widget.maxW || (this.columns - this.resizingWidget.x);
|
||||
this.resizingWidget.w = Math.max(widget.minW || 1, Math.min(newW, maxW));
|
||||
}
|
||||
|
||||
if (handle.includes('s')) {
|
||||
const newH = Math.round(this.resizeStartH + deltaY / (cellHeightValue + margins.vertical));
|
||||
const maxH = widget.maxH || Infinity;
|
||||
this.resizingWidget.h = Math.max(widget.minH || 1, Math.min(newH, maxH));
|
||||
}
|
||||
|
||||
this.requestUpdate();
|
||||
|
||||
this.dispatchEvent(new CustomEvent('widget-resize', {
|
||||
detail: { widget: this.resizingWidget },
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}));
|
||||
};
|
||||
|
||||
const endResize = () => {
|
||||
this.resizingWidget = null;
|
||||
document.removeEventListener('mousemove', handleResize);
|
||||
document.removeEventListener('mouseup', endResize);
|
||||
};
|
||||
|
||||
document.addEventListener('mousemove', handleResize);
|
||||
document.addEventListener('mouseup', endResize);
|
||||
}
|
||||
|
||||
|
||||
public removeWidget(widgetId: string) {
|
||||
this.widgets = this.widgets.filter(w => w.id !== widgetId);
|
||||
}
|
||||
|
||||
public updateWidget(widgetId: string, updates: Partial<IDashboardWidget>) {
|
||||
this.widgets = this.widgets.map(w =>
|
||||
w.id === widgetId ? { ...w, ...updates } : w
|
||||
);
|
||||
}
|
||||
|
||||
public getLayout(): Array<{ id: string; x: number; y: number; w: number; h: number }> {
|
||||
return this.widgets.map(({ id, x, y, w, h }) => ({ id, x, y, w, h }));
|
||||
}
|
||||
|
||||
public setLayout(layout: Array<{ id: string; x: number; y: number; w: number; h: number }>) {
|
||||
this.widgets = this.widgets.map(widget => {
|
||||
const layoutItem = layout.find(l => l.id === widget.id);
|
||||
return layoutItem ? { ...widget, ...layoutItem } : widget;
|
||||
});
|
||||
}
|
||||
|
||||
public lockGrid() {
|
||||
this.editable = false;
|
||||
}
|
||||
|
||||
public unlockGrid() {
|
||||
this.editable = true;
|
||||
}
|
||||
|
||||
private getMargins(): { horizontal: number; vertical: number; top: number; right: number; bottom: number; left: number } {
|
||||
if (typeof this.margin === 'number') {
|
||||
return {
|
||||
horizontal: this.margin,
|
||||
vertical: this.margin,
|
||||
top: this.margin,
|
||||
right: this.margin,
|
||||
bottom: this.margin,
|
||||
left: this.margin,
|
||||
};
|
||||
}
|
||||
|
||||
const margins = {
|
||||
top: this.margin.top ?? 10,
|
||||
right: this.margin.right ?? 10,
|
||||
bottom: this.margin.bottom ?? 10,
|
||||
left: this.margin.left ?? 10,
|
||||
};
|
||||
|
||||
return {
|
||||
...margins,
|
||||
horizontal: (margins.left + margins.right) / 2,
|
||||
vertical: (margins.top + margins.bottom) / 2,
|
||||
};
|
||||
}
|
||||
|
||||
private getCellHeight(): number {
|
||||
if (this.cellHeightUnit === 'auto') {
|
||||
// Calculate square cells based on container width
|
||||
const containerWidth = this.getBoundingClientRect().width;
|
||||
const margins = this.getMargins();
|
||||
const cellWidth = (containerWidth - margins.horizontal * (this.columns + 1)) / this.columns;
|
||||
return cellWidth;
|
||||
}
|
||||
|
||||
return this.cellHeight;
|
||||
}
|
||||
|
||||
private checkCollision(widget: IDashboardWidget, newX: number, newY: number): boolean {
|
||||
const widgets = this.widgets.filter(w => w.id !== widget.id);
|
||||
|
||||
for (const other of widgets) {
|
||||
if (newX < other.x + other.w &&
|
||||
newX + widget.w > other.x &&
|
||||
newY < other.y + other.h &&
|
||||
newY + widget.h > other.y) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public addWidget(widget: IDashboardWidget, autoPosition = false) {
|
||||
if (autoPosition || widget.autoPosition) {
|
||||
// Find first available position
|
||||
const position = this.findAvailablePosition(widget.w, widget.h);
|
||||
widget.x = position.x;
|
||||
widget.y = position.y;
|
||||
}
|
||||
|
||||
this.widgets = [...this.widgets, widget];
|
||||
}
|
||||
|
||||
private findAvailablePosition(width: number, height: number): { x: number; y: number } {
|
||||
// Try to find space starting from top-left
|
||||
for (let y = 0; y < 100; y++) { // Reasonable limit
|
||||
for (let x = 0; x <= this.columns - width; x++) {
|
||||
const testWidget = { id: 'test', x, y, w: width, h: height, content: '' } as IDashboardWidget;
|
||||
if (!this.checkCollision(testWidget, x, y)) {
|
||||
return { x, y };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If no space found, place at bottom
|
||||
const maxY = Math.max(...this.widgets.map(w => w.y + w.h), 0);
|
||||
return { x: 0, y: maxY };
|
||||
}
|
||||
|
||||
public compact(direction: 'vertical' | 'horizontal' = 'vertical') {
|
||||
const sortedWidgets = [...this.widgets].sort((a, b) => {
|
||||
if (direction === 'vertical') {
|
||||
if (a.y !== b.y) return a.y - b.y;
|
||||
return a.x - b.x;
|
||||
} else {
|
||||
if (a.x !== b.x) return a.x - b.x;
|
||||
return a.y - b.y;
|
||||
}
|
||||
});
|
||||
|
||||
for (const widget of sortedWidgets) {
|
||||
if (widget.locked || widget.noMove) continue;
|
||||
|
||||
if (direction === 'vertical') {
|
||||
// Move up as far as possible
|
||||
while (widget.y > 0 && !this.checkCollision(widget, widget.x, widget.y - 1)) {
|
||||
widget.y--;
|
||||
}
|
||||
} else {
|
||||
// Move left as far as possible
|
||||
while (widget.x > 0 && !this.checkCollision(widget, widget.x - 1, widget.y)) {
|
||||
widget.x--;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.requestUpdate();
|
||||
}
|
||||
}
|
@ -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`
|
||||
<style>
|
||||
.demoWrapper {
|
||||
box-sizing: border-box;
|
||||
position: absolute;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 20px;
|
||||
background: none;
|
||||
min-height: 100vh;
|
||||
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">
|
||||
import * as text from './hello'; const hiThere = 'nice'; const myFunction = async () => {
|
||||
console.log('nice one'); }
|
||||
interface User {
|
||||
id: number;
|
||||
name: string;
|
||||
email: string;
|
||||
isActive: boolean;
|
||||
}
|
||||
|
||||
class UserService {
|
||||
private users: User[] = [];
|
||||
|
||||
constructor(private apiUrl: string) {
|
||||
console.log('UserService initialized');
|
||||
}
|
||||
|
||||
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>
|
||||
|
||||
<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>
|
||||
`
|
@ -8,6 +8,7 @@ import {
|
||||
state,
|
||||
cssManager,
|
||||
} from '@design.estate/dees-element';
|
||||
import { cssGeistFontFamily, cssMonoFontFamily } from './00fonts.js';
|
||||
|
||||
import hlight from 'highlight.js';
|
||||
|
||||
@ -48,27 +49,27 @@ export class DeesDataviewCodebox extends DeesElement {
|
||||
display: block;
|
||||
text-align: left;
|
||||
font-size: 16px;
|
||||
font-family: 'Geist Sans', sans-serif;
|
||||
font-family: ${cssGeistFontFamily};
|
||||
}
|
||||
.mainbox {
|
||||
position: relative;
|
||||
color: ${this.goBright ? '#333333' : '#ffffff'};
|
||||
border-top: 1px solid ${this.goBright ? '#ffffff' : '#333333'};
|
||||
box-shadow: 0px 0px 5px ${this.goBright ? 'rgba(0,0,0,0.1)' : 'rgba(0,0,0,0.5)'};
|
||||
background: ${this.goBright ? '#ffffff' : '#191919'};
|
||||
border-radius: 16px;
|
||||
color: ${cssManager.bdTheme('#09090b', '#fafafa')};
|
||||
border: 1px solid ${cssManager.bdTheme('#e5e7eb', '#27272a')};
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1), 0 1px 2px rgba(0, 0, 0, 0.06);
|
||||
background: ${cssManager.bdTheme('#ffffff', '#09090b')};
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.appbar {
|
||||
position: relative;
|
||||
color: ${cssManager.bdTheme('#333', '#ccc')};
|
||||
background: ${cssManager.bdTheme('#ffffff', '#161616')};
|
||||
border-bottom: 1px solid ${cssManager.bdTheme('#eeeeeb', '#222222')};
|
||||
height: 24px;
|
||||
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
|
||||
background: ${cssManager.bdTheme('#f9fafb', '#18181b')};
|
||||
border-bottom: 1px solid ${cssManager.bdTheme('#e5e7eb', '#27272a')};
|
||||
height: 32px;
|
||||
display: flex;
|
||||
font-size: 12px;
|
||||
line-height: 24px;
|
||||
font-size: 13px;
|
||||
line-height: 32px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
@ -81,31 +82,38 @@ export class DeesDataviewCodebox extends DeesElement {
|
||||
}
|
||||
|
||||
.bottomBar {
|
||||
color: ${cssManager.bdTheme('#333', '#ccc')};
|
||||
background: ${cssManager.bdTheme('#ffffff', '#161616')};
|
||||
border-top: 1px solid ${cssManager.bdTheme('#eeeeeb', '#222222')};
|
||||
height: 24px;
|
||||
position: relative;
|
||||
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
|
||||
background: ${cssManager.bdTheme('#f9fafb', '#18181b')};
|
||||
border-top: 1px solid ${cssManager.bdTheme('#e5e7eb', '#27272a')};
|
||||
height: 28px;
|
||||
font-size: 12px;
|
||||
line-height: 24px;
|
||||
text-align: right;
|
||||
padding-right: 100px;
|
||||
line-height: 28px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: stretch;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.spacesLabel {
|
||||
padding: 0 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.languageLabel {
|
||||
color: ${cssManager.bdTheme('#333', '#ccc')};
|
||||
color: ${cssManager.bdTheme('#3b82f6', '#3b82f6')};
|
||||
font-size: 12px;
|
||||
line-height: 24px;
|
||||
z-index: 10;
|
||||
background: #6596ff20;
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
bottom: 0px;
|
||||
right: 0px;
|
||||
padding: 0px 16px 0px 8px;
|
||||
line-height: 28px;
|
||||
background: ${cssManager.bdTheme('rgba(59, 130, 246, 0.1)', 'rgba(59, 130, 246, 0.1)')};
|
||||
padding: 0px 16px;
|
||||
font-weight: 500;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.hljs-keyword {
|
||||
color: #ff65ec;
|
||||
color: ${cssManager.bdTheme('#dc2626', '#f87171')};
|
||||
}
|
||||
|
||||
.codegrid {
|
||||
@ -115,10 +123,10 @@ export class DeesDataviewCodebox extends DeesElement {
|
||||
}
|
||||
|
||||
.lineNumbers {
|
||||
color: ${this.goBright ? '#acacac' : '#666666'};
|
||||
padding: 30px 16px 0px 0px;
|
||||
color: ${cssManager.bdTheme('#71717a', '#52525b')};
|
||||
padding: 24px 16px 0px 0px;
|
||||
text-align: right;
|
||||
border-right: 1px solid ${this.goBright ? '#eaeaea' : '#222222'};
|
||||
border-right: 1px solid ${cssManager.bdTheme('#e5e7eb', '#27272a')};
|
||||
}
|
||||
|
||||
.lineCounter:last-child {
|
||||
@ -128,11 +136,11 @@ export class DeesDataviewCodebox extends DeesElement {
|
||||
pre {
|
||||
overflow-x: auto;
|
||||
margin: 0px;
|
||||
padding: 30px 40px;
|
||||
padding: 24px 24px;
|
||||
}
|
||||
|
||||
code {
|
||||
font-weight: ${this.goBright ? '400' : '300'};
|
||||
font-weight: 400;
|
||||
padding: 0px;
|
||||
margin: 0px;
|
||||
}
|
||||
@ -142,27 +150,43 @@ export class DeesDataviewCodebox extends DeesElement {
|
||||
.lineNumbers {
|
||||
line-height: 1.4em;
|
||||
font-weight: 200;
|
||||
font-family: 'Intel One Mono', 'Geist Mono', 'monospace';
|
||||
font-family: ${cssMonoFontFamily};
|
||||
}
|
||||
|
||||
.hljs-string {
|
||||
color: #ffa465;
|
||||
color: ${cssManager.bdTheme('#059669', '#10b981')};
|
||||
}
|
||||
|
||||
.hljs-built_in {
|
||||
color: #65ff6a;
|
||||
color: ${cssManager.bdTheme('#8b5cf6', '#a78bfa')};
|
||||
}
|
||||
|
||||
.hljs-function {
|
||||
color: ${this.goBright ? '#2765DF' : '#6596ff'};
|
||||
color: ${cssManager.bdTheme('#3b82f6', '#60a5fa')};
|
||||
}
|
||||
|
||||
.hljs-params {
|
||||
color: ${this.goBright ? '#3DB420' : '#65d5ff'};
|
||||
color: ${cssManager.bdTheme('#0891b2', '#06b6d4')};
|
||||
}
|
||||
|
||||
.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>
|
||||
<div
|
||||
@ -197,7 +221,7 @@ export class DeesDataviewCodebox extends DeesElement {
|
||||
<pre><code></code></pre>
|
||||
</div>
|
||||
<div class="bottomBar">
|
||||
Spaces: 2
|
||||
<div class="spacesLabel">Spaces: 2</div>
|
||||
<div class="languageLabel">${this.progLang}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -3,47 +3,162 @@ import * as tsclass from '@tsclass/tsclass';
|
||||
|
||||
export const demoFunc = () => html` <style>
|
||||
.demo {
|
||||
background: ${cssManager.bdTheme('#eeeeeb', '#000000')};
|
||||
background: ${cssManager.bdTheme('#f5f5f5', '#0a0a0a')};
|
||||
display: block;
|
||||
content: '';
|
||||
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>
|
||||
<div class="demo">
|
||||
<div class="demo-note">
|
||||
Right-click on any detail row to copy the value, key, or key:value combination
|
||||
</div>
|
||||
<div class="demo-grid">
|
||||
<div class="demo-section">
|
||||
<div class="demo-title">Service Health Status</div>
|
||||
<dees-dataview-statusobject
|
||||
.statusObject=${{
|
||||
id: '1',
|
||||
name: 'Demo Item',
|
||||
combinedStatus: 'partly_ok',
|
||||
combinedStatusText: 'partly_ok',
|
||||
name: 'API Gateway Service',
|
||||
combinedStatus: 'ok',
|
||||
combinedStatusText: 'All systems operational',
|
||||
details: [
|
||||
{
|
||||
name: 'Detail 1',
|
||||
value: 'Value 1',
|
||||
name: 'Response Time',
|
||||
value: '45ms (avg)',
|
||||
status: 'ok',
|
||||
statusText: 'OK',
|
||||
statusText: 'Within normal range',
|
||||
},
|
||||
{
|
||||
name: 'Detail 2',
|
||||
value: 'Value 2',
|
||||
status: 'partly_ok',
|
||||
statusText: 'partly_ok',
|
||||
},
|
||||
{
|
||||
name: 'Detail 3',
|
||||
value: 'Value 3',
|
||||
status: 'not_ok',
|
||||
statusText: 'not_ok',
|
||||
},
|
||||
{
|
||||
name: 'Detail 4',
|
||||
value:
|
||||
'Value 4 jhdkfjhalskdfjhfdjskalsdkfjhfdjskalskdjfhjdkslaksjdhfjdkslaskdfjhfjdkslaskdjfhjdskalskdjhfdjskalskdjfhdjskl',
|
||||
name: 'Uptime',
|
||||
value: '99.99% (30 days)',
|
||||
status: 'ok',
|
||||
statusText: 'OK',
|
||||
statusText: 'Excellent uptime',
|
||||
},
|
||||
{
|
||||
name: 'Active Connections',
|
||||
value: '1,234 / 10,000',
|
||||
status: 'ok',
|
||||
statusText: 'Normal load',
|
||||
},
|
||||
{
|
||||
name: 'SSL Certificate',
|
||||
value: 'Valid until 2024-12-31',
|
||||
status: 'ok',
|
||||
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>`;
|
||||
|
@ -15,6 +15,7 @@ import {
|
||||
} from '@design.estate/dees-element';
|
||||
|
||||
import * as tsclass from '@tsclass/tsclass';
|
||||
import { DeesContextmenu } from './dees-contextmenu.js';
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
@ -31,109 +32,128 @@ export class DeesDataviewStatusobject extends DeesElement {
|
||||
public static styles = [
|
||||
cssManager.defaultStyles,
|
||||
css`
|
||||
:host {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
|
||||
}
|
||||
|
||||
.mainbox {
|
||||
border-radius: 8px;
|
||||
background: ${cssManager.bdTheme('#fff', '#1b1b1b')};
|
||||
box-shadow: 0px 1px 3px #00000030;
|
||||
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%)')};
|
||||
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;
|
||||
color: ${cssManager.bdTheme('#000', '#fff')};
|
||||
border-top: ${cssManager.bdTheme('none', '1px solid #ffffff10')};
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 9%)', 'hsl(0 0% 98%)')};
|
||||
cursor: default;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.heading {
|
||||
display: grid;
|
||||
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 {
|
||||
display: block;
|
||||
margin: 0px;
|
||||
padding: 0px;
|
||||
height: 48px;
|
||||
text-transform: uppercase;
|
||||
font-size: 12px;
|
||||
line-height: 48px;
|
||||
padding: 0px 12px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
letter-spacing: -0.01em;
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 15%)', 'hsl(0 0% 93.9%)')};
|
||||
}
|
||||
|
||||
.statusdot {
|
||||
height: 8px;
|
||||
width: 8px;
|
||||
border-radius: 6px;
|
||||
background: grey;
|
||||
height: 10px;
|
||||
width: 10px;
|
||||
border-radius: 50%;
|
||||
background: ${cssManager.bdTheme('hsl(0 0% 63.9%)', 'hsl(0 0% 45.1%)')};
|
||||
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 {
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
border: 1px solid ${cssManager.bdTheme('#e0e0e0', '#444')};
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(0 0% 14.9%)')};
|
||||
border: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
|
||||
text-align: center;
|
||||
padding: 4px;
|
||||
border-radius: 3px;
|
||||
margin-right: 16px;
|
||||
color: ${cssManager.bdTheme('#333', '#ffffff80')};
|
||||
padding: 6px 12px;
|
||||
border-radius: 6px;
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 45.1%)', 'hsl(0 0% 63.9%)')};
|
||||
user-select: none;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.copyMain:hover {
|
||||
background: ${cssManager.bdTheme(colors.bright.blue, colors.dark.blue)};
|
||||
border: 1px solid ${cssManager.bdTheme(colors.bright.blue, colors.dark.blue)};
|
||||
color: #fff;
|
||||
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%)')};
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 15%)', 'hsl(0 0% 93.9%)')};
|
||||
}
|
||||
|
||||
.copyMain:active {
|
||||
background: ${cssManager.bdTheme(colors.bright.blueActive, colors.dark.blueActive)};
|
||||
border: 1px solid ${cssManager.bdTheme(colors.bright.blueActive, colors.dark.blueActive)};
|
||||
color: #fff;
|
||||
background: ${cssManager.bdTheme('hsl(0 0% 91%)', 'hsl(0 0% 14.9%)')};
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
.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 {
|
||||
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 {
|
||||
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 {
|
||||
min-height: 60px;
|
||||
align-items: center;
|
||||
display: grid;
|
||||
grid-template-columns: 40px auto;
|
||||
border-top: 1px dotted ${cssManager.bdTheme('#e0e0e0', '#282828')};
|
||||
transition: all 0.2s;
|
||||
grid-template-columns: 48px auto;
|
||||
border-top: 1px solid ${cssManager.bdTheme('hsl(0 0% 94%)', 'hsl(0 0% 14.9%)')};
|
||||
transition: background-color 0.15s ease;
|
||||
padding-right: 16px;
|
||||
cursor: context-menu;
|
||||
}
|
||||
|
||||
.detail:hover {
|
||||
background: #ffffff05;
|
||||
background: ${cssManager.bdTheme('hsl(0 0% 97%)', 'hsl(0 0% 7%)')};
|
||||
}
|
||||
|
||||
.detail:active {
|
||||
background: #ffffff10;
|
||||
background: ${cssManager.bdTheme('hsl(0 0% 95%)', 'hsl(0 0% 9%)')};
|
||||
}
|
||||
|
||||
.detail .detailsText {
|
||||
padding-top: 8px;
|
||||
padding-bottom: 8px;
|
||||
padding-right: 8px;
|
||||
padding: 12px;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.detail .detailsText .label {
|
||||
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 {
|
||||
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="heading">
|
||||
<div class="statusdot ${this.statusObject?.combinedStatus}"></div>
|
||||
<h1>${this.statusObject?.name || 'no status object assigned'}</h1>
|
||||
<div class="copyMain">Copy as JSON</div>
|
||||
<h1>${this.statusObject?.name || 'No status object assigned'}</h1>
|
||||
<div class="copyMain" @click=${this.handleCopyAsJson}>Copy JSON</div>
|
||||
</div>
|
||||
${this.statusObject?.details?.map((detailArg) => {
|
||||
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="detailsText">
|
||||
<div class="label">${detailArg.name}</div>
|
||||
@ -162,4 +210,42 @@ export class DeesDataviewStatusobject extends DeesElement {
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -10,6 +10,7 @@ import {
|
||||
} from '@design.estate/dees-element';
|
||||
|
||||
import { demoFunc } from './dees-heading.demo.js';
|
||||
import { cssCalSansFontFamily } from './00fonts.js';
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
@ -39,7 +40,7 @@ export class DeesHeading extends DeesElement {
|
||||
font-weight: 600;
|
||||
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; }
|
||||
h3 { font-size: 24px; }
|
||||
h4 { font-size: 20px; }
|
||||
|
@ -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 './dees-panel.js';
|
||||
import type { DeesInputCheckbox } from './dees-input-checkbox.js';
|
||||
import './dees-button.js';
|
||||
|
||||
@ -41,62 +42,49 @@ export const demoFunc = () => html`
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.demo-section {
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
padding: 24px;
|
||||
dees-panel {
|
||||
margin-bottom: 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;
|
||||
dees-panel:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.checkbox-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.feature-list {
|
||||
background: #f0f0f0;
|
||||
border-radius: 4px;
|
||||
.horizontal-checkboxes {
|
||||
display: flex;
|
||||
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;
|
||||
margin-bottom: 16px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.feature-list {
|
||||
background: #0a0a0a;
|
||||
.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;
|
||||
}
|
||||
|
||||
.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 {
|
||||
@ -104,14 +92,26 @@ export const demoFunc = () => html`
|
||||
gap: 8px;
|
||||
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>
|
||||
|
||||
<div class="demo-container">
|
||||
<div class="demo-section">
|
||||
<h3>Basic Checkboxes</h3>
|
||||
<p>Standard checkbox inputs for boolean selections</p>
|
||||
|
||||
<dees-panel .title=${'Basic Checkboxes'} .subtitle=${'Simple checkbox examples with various labels'}>
|
||||
<div class="checkbox-group">
|
||||
<dees-input-checkbox
|
||||
.label=${'I agree to the Terms and Conditions'}
|
||||
.value=${true}
|
||||
@ -126,48 +126,78 @@ export const demoFunc = () => html`
|
||||
|
||||
<dees-input-checkbox
|
||||
.label=${'Enable notifications'}
|
||||
.required=${true}
|
||||
.value=${false}
|
||||
.description=${'Receive email updates about your account'}
|
||||
.key=${'notifications'}
|
||||
></dees-input-checkbox>
|
||||
</div>
|
||||
</dees-panel>
|
||||
|
||||
<div class="demo-section">
|
||||
<h3>Horizontal Layout</h3>
|
||||
<p>Checkboxes arranged horizontally for compact forms</p>
|
||||
<dees-panel .title=${'Checkbox States'} .subtitle=${'Different checkbox states and configurations'}>
|
||||
<div class="checkbox-group">
|
||||
<dees-input-checkbox
|
||||
.label=${'Default state'}
|
||||
.value=${false}
|
||||
></dees-input-checkbox>
|
||||
|
||||
<div class="horizontal-group">
|
||||
<dees-input-checkbox
|
||||
.label=${'Checked state'}
|
||||
.value=${true}
|
||||
></dees-input-checkbox>
|
||||
|
||||
<dees-input-checkbox
|
||||
.label=${'Disabled unchecked'}
|
||||
.value=${false}
|
||||
.disabled=${true}
|
||||
></dees-input-checkbox>
|
||||
|
||||
<dees-input-checkbox
|
||||
.label=${'Disabled checked'}
|
||||
.value=${true}
|
||||
.disabled=${true}
|
||||
></dees-input-checkbox>
|
||||
|
||||
<dees-input-checkbox
|
||||
.label=${'Required checkbox'}
|
||||
.required=${true}
|
||||
.key=${'required'}
|
||||
></dees-input-checkbox>
|
||||
</div>
|
||||
</dees-panel>
|
||||
|
||||
<dees-panel .title=${'Horizontal Layout'} .subtitle=${'Checkboxes arranged horizontally for compact forms'}>
|
||||
<div class="horizontal-checkboxes">
|
||||
<dees-input-checkbox
|
||||
.label=${'Option A'}
|
||||
.value=${false}
|
||||
.layoutMode=${'horizontal'}
|
||||
.key=${'optionA'}
|
||||
></dees-input-checkbox>
|
||||
|
||||
<dees-input-checkbox
|
||||
.label=${'Option B'}
|
||||
.layoutMode=${'horizontal'}
|
||||
.value=${true}
|
||||
.layoutMode=${'horizontal'}
|
||||
.key=${'optionB'}
|
||||
></dees-input-checkbox>
|
||||
|
||||
<dees-input-checkbox
|
||||
.label=${'Option C'}
|
||||
.value=${false}
|
||||
.layoutMode=${'horizontal'}
|
||||
.key=${'optionC'}
|
||||
></dees-input-checkbox>
|
||||
|
||||
<dees-input-checkbox
|
||||
.label=${'Option D'}
|
||||
.layoutMode=${'horizontal'}
|
||||
.value=${true}
|
||||
.layoutMode=${'horizontal'}
|
||||
.key=${'optionD'}
|
||||
></dees-input-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="demo-section">
|
||||
<h3>Feature Selection Example</h3>
|
||||
<p>Common use case for feature toggles with batch operations</p>
|
||||
</dees-panel>
|
||||
|
||||
<dees-panel .title=${'Feature Selection Example'} .subtitle=${'Common use case for feature toggles with batch operations'}>
|
||||
<div class="button-group">
|
||||
<dees-button id="select-all-btn" type="secondary">Select All</dees-button>
|
||||
<dees-button id="clear-all-btn" type="secondary">Clear All</dees-button>
|
||||
@ -206,62 +236,72 @@ export const demoFunc = () => html`
|
||||
></dees-input-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</dees-panel>
|
||||
|
||||
<div class="demo-section">
|
||||
<h3>States</h3>
|
||||
<p>Different checkbox states and configurations</p>
|
||||
|
||||
<dees-input-checkbox
|
||||
.label=${'Disabled Unchecked'}
|
||||
.disabled=${true}
|
||||
.key=${'disabled1'}
|
||||
></dees-input-checkbox>
|
||||
|
||||
<dees-input-checkbox
|
||||
.label=${'Disabled Checked'}
|
||||
.disabled=${true}
|
||||
.value=${true}
|
||||
.key=${'disabled2'}
|
||||
></dees-input-checkbox>
|
||||
|
||||
<dees-input-checkbox
|
||||
.label=${'Required Checkbox'}
|
||||
.required=${true}
|
||||
.key=${'required'}
|
||||
></dees-input-checkbox>
|
||||
</div>
|
||||
|
||||
<div class="demo-section">
|
||||
<h3>Real-world Examples</h3>
|
||||
<p>Common checkbox patterns in applications</p>
|
||||
<dees-panel .title=${'Privacy Settings Example'} .subtitle=${'Checkboxes in a typical form context'}>
|
||||
<div class="form-section">
|
||||
<h4 class="section-title">Privacy Preferences</h4>
|
||||
|
||||
<div class="checkbox-group">
|
||||
<dees-input-checkbox
|
||||
.label=${'Remember me on this device'}
|
||||
.label=${'Share analytics data'}
|
||||
.value=${true}
|
||||
.key=${'rememberMe'}
|
||||
.description=${'Help us improve by sharing anonymous usage data'}
|
||||
></dees-input-checkbox>
|
||||
|
||||
<dees-input-checkbox
|
||||
.label=${'Make my profile public'}
|
||||
.value=${false}
|
||||
.key=${'publicProfile'}
|
||||
></dees-input-checkbox>
|
||||
|
||||
<dees-input-checkbox
|
||||
.label=${'Allow others to find me by email'}
|
||||
.value=${false}
|
||||
.key=${'findByEmail'}
|
||||
></dees-input-checkbox>
|
||||
|
||||
<dees-input-checkbox
|
||||
.label=${'Send me product updates and announcements'}
|
||||
.label=${'Personalized recommendations'}
|
||||
.value=${true}
|
||||
.key=${'productUpdates'}
|
||||
.description=${'Get suggestions based on your activity'}
|
||||
></dees-input-checkbox>
|
||||
|
||||
<dees-input-checkbox
|
||||
.label=${'Marketing communications'}
|
||||
.value=${false}
|
||||
.description=${'Receive promotional emails and special offers'}
|
||||
></dees-input-checkbox>
|
||||
|
||||
<dees-input-checkbox
|
||||
.label=${'Third-party integrations'}
|
||||
.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">
|
||||
<dees-input-checkbox
|
||||
.label=${'Feature toggle'}
|
||||
.value=${false}
|
||||
@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
|
||||
.label=${'Debug mode'}
|
||||
.value=${false}
|
||||
@changeSubject=${(event: CustomEvent) => {
|
||||
const output = document.querySelector('#debug-output');
|
||||
if (output && event.detail) {
|
||||
const isChecked = event.detail.getValue();
|
||||
output.textContent = `Debug mode: ${isChecked ? 'ON' : 'OFF'}`;
|
||||
}
|
||||
}}
|
||||
></dees-input-checkbox>
|
||||
</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>
|
||||
</dees-demowrapper>
|
||||
`;
|
@ -8,6 +8,7 @@ import {
|
||||
} from '@design.estate/dees-element';
|
||||
import { DeesInputBase } from './dees-input-base.js';
|
||||
import { demoFunc } from './dees-input-checkbox.demo.js';
|
||||
import { cssGeistFontFamily } from './00fonts.js';
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
@ -44,120 +45,106 @@ export class DeesInputCheckbox extends DeesInputBase<DeesInputCheckbox> {
|
||||
:host {
|
||||
position: relative;
|
||||
cursor: default;
|
||||
}
|
||||
:host(:hover) {
|
||||
filter: ${cssManager.bdTheme('brightness(0.95)', 'brightness(1.1)')};
|
||||
font-family: ${cssGeistFontFamily};
|
||||
}
|
||||
|
||||
.maincontainer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 8px 0px;
|
||||
color: ${cssManager.bdTheme('#333', '#ccc')};
|
||||
display: inline-flex;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.maincontainer:hover {
|
||||
color: ${cssManager.bdTheme('#000', '#fff')};
|
||||
}
|
||||
|
||||
.maincontainer:hover .checkbox {
|
||||
border-color: ${cssManager.bdTheme('#999', '#888')};
|
||||
}
|
||||
|
||||
input:focus {
|
||||
outline: none;
|
||||
border-bottom: 1px solid #e4002b;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.checkbox {
|
||||
transition: all 0.1s;
|
||||
box-sizing: border-box;
|
||||
border: 1px solid ${cssManager.bdTheme('#CCC', '#999')};
|
||||
border-radius: 2px;
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
display: inline-block;
|
||||
background: ${cssManager.bdTheme('#fafafa', '#222')};
|
||||
position: relative;
|
||||
height: 18px;
|
||||
width: 18px;
|
||||
flex-shrink: 0;
|
||||
border-radius: 4px;
|
||||
border: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
|
||||
background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(0 0% 3.9%)')};
|
||||
transition: all 0.15s ease;
|
||||
margin-top: 1px;
|
||||
}
|
||||
|
||||
.maincontainer:hover .checkbox {
|
||||
border-color: ${cssManager.bdTheme('hsl(0 0% 79.8%)', 'hsl(0 0% 20.9%)')};
|
||||
}
|
||||
|
||||
.checkbox.selected {
|
||||
background: #0050b9;
|
||||
border: 1px solid #0050b9;
|
||||
background: ${cssManager.bdTheme('hsl(222.2 47.4% 51.2%)', 'hsl(217.2 91.2% 59.8%)')};
|
||||
border-color: ${cssManager.bdTheme('hsl(222.2 47.4% 51.2%)', 'hsl(217.2 91.2% 59.8%)')};
|
||||
}
|
||||
|
||||
.checkbox.disabled {
|
||||
background: none;
|
||||
border: 1px dashed ${cssManager.bdTheme('#666666', '#666666')};
|
||||
.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 {
|
||||
display: inline-block;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
-ms-transform: rotate(45deg); /* IE 9 */
|
||||
-webkit-transform: rotate(45deg); /* Chrome, Safari, Opera */
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
.checkbox .checkmark_stem {
|
||||
position: absolute;
|
||||
width: 3px;
|
||||
height: 9px;
|
||||
background-color: #fff;
|
||||
left: 11px;
|
||||
top: 6px;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
opacity: 0;
|
||||
transition: opacity 0.15s ease;
|
||||
}
|
||||
|
||||
.checkbox .checkmark_kick {
|
||||
position: absolute;
|
||||
width: 3px;
|
||||
height: 3px;
|
||||
background-color: #fff;
|
||||
left: 8px;
|
||||
top: 12px;
|
||||
.checkbox.selected .checkmark {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.checkbox.disabled .checkmark_stem, .checkbox.disabled .checkmark_kick {
|
||||
background-color: ${cssManager.bdTheme('#333', '#fff')};
|
||||
}
|
||||
|
||||
img {
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.checkbox-label {
|
||||
font-size: 14px;
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
.maincontainer:hover .checkbox-label {
|
||||
color: ${cssManager.bdTheme('#1a1a1a', '#ffffff')};
|
||||
.checkbox .checkmark svg {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
stroke: white;
|
||||
stroke-width: 3;
|
||||
}
|
||||
|
||||
/* Disabled state */
|
||||
.maincontainer.disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.maincontainer.disabled:hover {
|
||||
color: ${cssManager.bdTheme('#333', '#ccc')};
|
||||
.checkbox.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%)')};
|
||||
}
|
||||
|
||||
.maincontainer.disabled:hover .checkbox {
|
||||
border-color: ${cssManager.bdTheme('#ccc', '#333')};
|
||||
/* Label */
|
||||
.label-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.checkbox-label {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
line-height: 20px;
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 15%)', 'hsl(0 0% 90%)')};
|
||||
transition: color 0.15s ease;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
.maincontainer:hover .checkbox-label {
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 9%)', 'hsl(0 0% 95%)')};
|
||||
}
|
||||
|
||||
.maincontainer.disabled:hover .checkbox-label {
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 15%)', 'hsl(0 0% 90%)')};
|
||||
}
|
||||
|
||||
/* Description */
|
||||
.description-text {
|
||||
font-size: 12px;
|
||||
color: ${cssManager.bdTheme('#666', '#999')};
|
||||
margin-top: 4px;
|
||||
line-height: 1.4;
|
||||
padding-left: 36px;
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 45.1%)', 'hsl(0 0% 63.9%)')};
|
||||
line-height: 1.5;
|
||||
}
|
||||
`,
|
||||
];
|
||||
@ -166,21 +153,26 @@ export class DeesInputCheckbox extends DeesInputBase<DeesInputCheckbox> {
|
||||
return html`
|
||||
<div class="input-wrapper">
|
||||
<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
|
||||
? html`
|
||||
<span class="checkmark">
|
||||
<div class="checkmark_stem"></div>
|
||||
<div class="checkmark_kick"></div>
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M20 6L9 17L4 12" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</span>
|
||||
`
|
||||
: html``}
|
||||
</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>
|
||||
${this.description ? html`
|
||||
<div class="description-text">${this.description}</div>
|
||||
` : ''}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@ -213,4 +205,11 @@ export class DeesInputCheckbox extends DeesInputBase<DeesInputCheckbox> {
|
||||
(checkboxDiv as any).focus();
|
||||
}
|
||||
}
|
||||
|
||||
private handleKeydown(event: KeyboardEvent) {
|
||||
if (event.key === ' ' || event.key === 'Enter') {
|
||||
event.preventDefault();
|
||||
this.toggleSelected();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -9,8 +9,8 @@ import {
|
||||
} from '@design.estate/dees-element';
|
||||
import * as domtools from '@design.estate/dees-domtools';
|
||||
import { demoFunc } from './dees-input-dropdown.demo.js';
|
||||
import { DeesWindowLayer } from './dees-windowlayer.js';
|
||||
import { DeesInputBase } from './dees-input-base.js';
|
||||
import { cssGeistFontFamily } from './00fonts.js';
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
@ -39,13 +39,11 @@ export class DeesInputDropdown extends DeesInputBase<DeesInputDropdown> {
|
||||
this.selectedOption = val;
|
||||
}
|
||||
|
||||
|
||||
@property({
|
||||
type: Boolean,
|
||||
})
|
||||
public enableSearch: boolean = true;
|
||||
|
||||
|
||||
@state()
|
||||
public opensToTop: boolean = false;
|
||||
|
||||
@ -58,6 +56,9 @@ export class DeesInputDropdown extends DeesInputBase<DeesInputDropdown> {
|
||||
@state()
|
||||
public isOpened = false;
|
||||
|
||||
@state()
|
||||
private searchValue: string = '';
|
||||
|
||||
public static styles = [
|
||||
...DeesInputBase.baseStyles,
|
||||
cssManager.defaultStyles,
|
||||
@ -67,123 +68,201 @@ export class DeesInputDropdown extends DeesInputBase<DeesInputDropdown> {
|
||||
}
|
||||
|
||||
:host {
|
||||
font-family: Roboto;
|
||||
font-family: ${cssGeistFontFamily};
|
||||
position: relative;
|
||||
color: ${cssManager.bdTheme('#222', '#fff')};
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 15%)', 'hsl(0 0% 90%)')};
|
||||
}
|
||||
|
||||
.maincontainer {
|
||||
display: block;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
|
||||
.selectedBox {
|
||||
user-select: none;
|
||||
position: relative;
|
||||
max-width: 420px;
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
line-height: 40px;
|
||||
padding: 0px 8px;
|
||||
background: ${cssManager.bdTheme('#fafafa', '#222222')};
|
||||
box-shadow: ${cssManager.bdTheme('0px 1px 4px rgba(0,0,0,0.3)', 'none')};
|
||||
border-radius: 3px;
|
||||
border-top: ${cssManager.bdTheme('1px solid #CCC', '1px solid #ffffff10')};
|
||||
border-bottom: ${cssManager.bdTheme('1px solid #CCC', '1px solid #222')};
|
||||
transition: all 0.2s ease;
|
||||
font-size: 16px;
|
||||
color: ${cssManager.bdTheme('#222', '#ccc')};
|
||||
line-height: 38px;
|
||||
padding: 0 40px 0 12px;
|
||||
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;
|
||||
font-size: 14px;
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 9%)', 'hsl(0 0% 95%)')};
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.selectedBox:hover {
|
||||
filter: ${cssManager.bdTheme('brightness(0.95)', 'brightness(1.1)')};
|
||||
.selectedBox:hover:not(.disabled) {
|
||||
border-color: ${cssManager.bdTheme('hsl(0 0% 79.8%)', 'hsl(0 0% 20.9%)')};
|
||||
}
|
||||
|
||||
.accentBottom {
|
||||
filter: none !important;
|
||||
.selectedBox:focus-visible {
|
||||
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 {
|
||||
filter: none !important;
|
||||
.selectedBox.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%)')};
|
||||
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 {
|
||||
will-change: transform;
|
||||
will-change: transform, opacity;
|
||||
pointer-events: none;
|
||||
transition: all 0.2s ease;
|
||||
transition: all 0.15s ease;
|
||||
opacity: 0;
|
||||
background: ${cssManager.bdTheme('#ffffff', '#222222')};
|
||||
max-width: 420px;
|
||||
box-shadow: 0px 0px 5px rgba(0, 0, 0, 0.2);
|
||||
transform: translateY(-8px) scale(0.98);
|
||||
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%)')};
|
||||
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;
|
||||
border-radius: 8px;
|
||||
padding: 4px 8px;
|
||||
max-height: 300px;
|
||||
overflow: hidden;
|
||||
border-radius: 6px;
|
||||
position: absolute;
|
||||
user-select: none;
|
||||
margin: 3px 0px 0px 0px;
|
||||
border-top: ${cssManager.bdTheme('1px solid #CCC', '1px solid #ffffff10')};
|
||||
margin-top: 4px;
|
||||
z-index: 50;
|
||||
left: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.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 {
|
||||
transform: translate(0px, -4px);
|
||||
top: 100%;
|
||||
}
|
||||
|
||||
.selectionBox.show {
|
||||
pointer-events: all;
|
||||
transform: scale(1, 1) translate(0px, 0px);
|
||||
transform: translateY(0) scale(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 {
|
||||
transition: all 0.1s;
|
||||
transition: all 0.15s ease;
|
||||
line-height: 32px;
|
||||
padding: 0px 4px;
|
||||
border-radius: 3px;
|
||||
margin: 4px 0px;
|
||||
padding: 0 8px;
|
||||
border-radius: 4px;
|
||||
margin: 2px 0;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 15%)', 'hsl(0 0% 90%)')};
|
||||
}
|
||||
|
||||
.option.highlighted {
|
||||
border-left: 2px solid #0069f2;
|
||||
padding-left: 6px;
|
||||
background: #ffffff20;
|
||||
background: ${cssManager.bdTheme('hsl(0 0% 95.1%)', 'hsl(0 0% 14.9%)')};
|
||||
}
|
||||
|
||||
.option:hover {
|
||||
color: #fff;
|
||||
padding-left: 8px;
|
||||
background: #0069f2;
|
||||
background: ${cssManager.bdTheme('hsl(0 0% 95.1%)', 'hsl(0 0% 14.9%)')};
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 9%)', 'hsl(0 0% 95%)')};
|
||||
}
|
||||
|
||||
.search.top {
|
||||
padding-top: 4px;
|
||||
/* No options message */
|
||||
.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 {
|
||||
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 {
|
||||
display: block;
|
||||
background: none;
|
||||
border: none;
|
||||
height: 24px;
|
||||
color: inherit;
|
||||
text-align: left;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
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 {
|
||||
border-bottom: 1px dotted #333;
|
||||
}
|
||||
.search.bottom input {
|
||||
border-top: 1px dotted #333;
|
||||
.search input::placeholder {
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 63.9%)', 'hsl(0 0% 45.1%)')};
|
||||
}
|
||||
|
||||
.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 {
|
||||
return html`
|
||||
<div class="input-wrapper">
|
||||
<dees-label .label=${this.label}></dees-label>
|
||||
<div class="maincontainer" @keydown="${this.isOpened ? this.handleKeyDown : undefined}">
|
||||
<div class="selectionBox">
|
||||
${this.enableSearch && !this.opensToTop
|
||||
<dees-label .label=${this.label} .description=${this.description} .required=${this.required}></dees-label>
|
||||
<div class="maincontainer">
|
||||
<div
|
||||
class="selectedBox ${this.isOpened ? 'open' : ''} ${this.disabled ? 'disabled' : ''}"
|
||||
@click="${() => !this.disabled && this.toggleSelectionBox()}"
|
||||
tabindex="${this.disabled ? '-1' : '0'}"
|
||||
@keydown="${this.handleSelectedBoxKeydown}"
|
||||
>
|
||||
${this.selectedOption?.option || 'Select an option'}
|
||||
</div>
|
||||
<div class="selectionBox ${this.isOpened ? 'show' : ''} ${this.opensToTop ? 'top' : 'bottom'}">
|
||||
${this.enableSearch
|
||||
? html`
|
||||
<div class="search top">
|
||||
<input type="text" placeholder="Search" @input="${this.handleSearch}" />
|
||||
<div class="search">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search options..."
|
||||
.value="${this.searchValue}"
|
||||
@input="${this.handleSearch}"
|
||||
@click="${(e: Event) => e.stopPropagation()}"
|
||||
@keydown="${this.handleSearchKeydown}"
|
||||
/>
|
||||
</div>
|
||||
`
|
||||
: null}
|
||||
${this.filteredOptions.map((option, index) => {
|
||||
<div class="options-container">
|
||||
${this.filteredOptions.length === 0
|
||||
? html`<div class="no-options">No options found</div>`
|
||||
: this.filteredOptions.map((option, index) => {
|
||||
const isHighlighted = this.highlightedIndex === index;
|
||||
return html`
|
||||
<div
|
||||
class="option ${isHighlighted ? 'highlighted' : ''}"
|
||||
@click=${() => {
|
||||
this.updateSelection(option);
|
||||
}}
|
||||
@click="${() => this.updateSelection(option)}"
|
||||
@mouseenter="${() => this.highlightedIndex = index}"
|
||||
>
|
||||
${option.option}
|
||||
</div>
|
||||
`;
|
||||
})}
|
||||
${this.enableSearch && this.opensToTop
|
||||
? html`
|
||||
<div class="search bottom">
|
||||
<input type="text" placeholder="Search" @input="${this.handleSearch}" />
|
||||
</div>
|
||||
`
|
||||
: null}
|
||||
</div>
|
||||
<div
|
||||
class="selectedBox"
|
||||
@click="${(event) => {
|
||||
if (!this.isElevated) {
|
||||
this.toggleSelectionBox();
|
||||
} else {
|
||||
this.updateSelection(this.selectedOption);
|
||||
})
|
||||
}
|
||||
}}"
|
||||
>
|
||||
${this.selectedOption?.option || 'Select...'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
firstUpdated() {
|
||||
this.selectedOption = this.selectedOption || null;
|
||||
this.filteredOptions = this.options; // Initialize filteredOptions
|
||||
async connectedCallback() {
|
||||
super.connectedCallback();
|
||||
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.isOpened = false;
|
||||
this.searchValue = '';
|
||||
this.filteredOptions = this.options;
|
||||
this.highlightedIndex = 0;
|
||||
|
||||
this.dispatchEvent(
|
||||
new CustomEvent('selectedOption', {
|
||||
@ -253,135 +349,105 @@ export class DeesInputDropdown extends DeesInputBase<DeesInputDropdown> {
|
||||
bubbles: true,
|
||||
})
|
||||
);
|
||||
if (this.isElevated) {
|
||||
this.toggleSelectionBox();
|
||||
}
|
||||
|
||||
this.changeSubject.next(this);
|
||||
}
|
||||
|
||||
private isElevated: boolean = false;
|
||||
private windowOverlay: DeesWindowLayer;
|
||||
private handleClickOutside = (event: MouseEvent) => {
|
||||
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() {
|
||||
this.isOpened = !this.isOpened;
|
||||
const domtoolsInstance = await this.domtoolsPromise;
|
||||
const selectedBox: HTMLElement = this.shadowRoot.querySelector('.selectedBox');
|
||||
const selectionBox: HTMLElement = this.shadowRoot.querySelector('.selectionBox');
|
||||
if (!this.isElevated) {
|
||||
this.windowOverlay = await DeesWindowLayer.createAndShow({
|
||||
blur: false,
|
||||
});
|
||||
const elevatedDropdown = new DeesInputDropdown();
|
||||
elevatedDropdown.isElevated = true;
|
||||
elevatedDropdown.label = this.label;
|
||||
elevatedDropdown.enableSearch = this.enableSearch;
|
||||
elevatedDropdown.required = this.required;
|
||||
elevatedDropdown.disabled = this.disabled;
|
||||
elevatedDropdown.style.position = 'fixed';
|
||||
elevatedDropdown.style.top = this.getBoundingClientRect().top + 'px';
|
||||
elevatedDropdown.style.left = this.getBoundingClientRect().left + 'px';
|
||||
elevatedDropdown.style.width = this.clientWidth + 'px';
|
||||
|
||||
// Get z-index from registry for the elevated dropdown
|
||||
const dropdownZIndex = (await import('./00zindex.js')).zIndexRegistry.getNextZIndex();
|
||||
elevatedDropdown.style.zIndex = dropdownZIndex.toString();
|
||||
(await import('./00zindex.js')).zIndexRegistry.register(elevatedDropdown, dropdownZIndex);
|
||||
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);
|
||||
if (this.isOpened) {
|
||||
// Check available space and set position
|
||||
const selectedBox = this.shadowRoot.querySelector('.selectedBox') as HTMLElement;
|
||||
const rect = selectedBox.getBoundingClientRect();
|
||||
const spaceBelow = window.innerHeight - rect.bottom;
|
||||
const spaceAbove = rect.top;
|
||||
|
||||
// Prevent clicks on the dropdown from closing it
|
||||
elevatedDropdown.addEventListener('click', (e: Event) => {
|
||||
e.stopPropagation();
|
||||
});
|
||||
// Determine if we should open upwards
|
||||
this.opensToTop = spaceBelow < 300 && spaceAbove > spaceBelow;
|
||||
|
||||
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);
|
||||
|
||||
// Unregister elevated dropdown from z-index registry
|
||||
(await import('./00zindex.js')).zIndexRegistry.unregister(elevatedDropdown);
|
||||
|
||||
this.windowOverlay.destroy();
|
||||
};
|
||||
const handleSelection = async () => {
|
||||
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';
|
||||
// Focus search input if present
|
||||
await this.updateComplete;
|
||||
const searchInput = this.shadowRoot.querySelector('.search input') as HTMLInputElement;
|
||||
if (searchInput) {
|
||||
searchInput.focus();
|
||||
}
|
||||
await domtoolsInstance.convenience.smartdelay.delayFor(0);
|
||||
const searchInput = selectionBox.querySelector('input');
|
||||
searchInput?.focus();
|
||||
|
||||
// Get z-index from registry for the selection box
|
||||
const selectionBoxZIndex = (await import('./00zindex.js')).zIndexRegistry.getNextZIndex();
|
||||
selectionBox.style.zIndex = selectionBoxZIndex.toString();
|
||||
(await import('./00zindex.js')).zIndexRegistry.register(selectionBox as HTMLElement, selectionBoxZIndex);
|
||||
|
||||
selectionBox.classList.add('show');
|
||||
// Add click outside listener
|
||||
setTimeout(() => {
|
||||
document.addEventListener('click', this.handleClickOutside);
|
||||
}, 0);
|
||||
} else {
|
||||
selectedBox.style.pointerEvents = 'none';
|
||||
selectionBox.classList.remove('show');
|
||||
|
||||
// Unregister selection box from z-index registry
|
||||
(await import('./00zindex.js')).zIndexRegistry.unregister(selectionBox as HTMLElement);
|
||||
|
||||
// selectedBox.style.opacity = '0';
|
||||
}
|
||||
// Cleanup
|
||||
this.searchValue = '';
|
||||
this.filteredOptions = this.options;
|
||||
document.removeEventListener('click', this.handleClickOutside);
|
||||
}
|
||||
}
|
||||
|
||||
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) =>
|
||||
option.option.toLowerCase().includes(searchTerm)
|
||||
option.option.toLowerCase().includes(searchLower)
|
||||
);
|
||||
this.highlightedIndex = 0; // Reset highlighted index
|
||||
this.highlightedIndex = 0;
|
||||
}
|
||||
|
||||
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 maxIndex = this.filteredOptions.length - 1;
|
||||
|
||||
if (key === 'ArrowDown') {
|
||||
event.preventDefault();
|
||||
this.highlightedIndex = this.highlightedIndex + 1 > maxIndex ? 0 : this.highlightedIndex + 1;
|
||||
event.preventDefault();
|
||||
} else if (key === 'ArrowUp') {
|
||||
event.preventDefault();
|
||||
this.highlightedIndex = this.highlightedIndex - 1 < 0 ? maxIndex : this.highlightedIndex - 1;
|
||||
event.preventDefault();
|
||||
} else if (key === 'Enter') {
|
||||
this.updateSelection(this.filteredOptions[this.highlightedIndex]);
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -392,4 +458,9 @@ export class DeesInputDropdown extends DeesInputBase<DeesInputDropdown> {
|
||||
public setValue(value: { option: string; key: string; payload?: any }): void {
|
||||
this.selectedOption = value;
|
||||
}
|
||||
|
||||
async disconnectedCallback() {
|
||||
await super.disconnectedCallback();
|
||||
document.removeEventListener('click', this.handleClickOutside);
|
||||
}
|
||||
}
|
@ -68,7 +68,7 @@ export class DeesInputFileupload extends DeesInputBase<DeesInputFileupload> {
|
||||
:host {
|
||||
position: relative;
|
||||
display: block;
|
||||
color: ${cssManager.bdTheme('#333', '#ccc')};
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 15%)', 'hsl(0 0% 90%)')};
|
||||
}
|
||||
|
||||
.hidden {
|
||||
@ -83,16 +83,16 @@ export class DeesInputFileupload extends DeesInputBase<DeesInputFileupload> {
|
||||
|
||||
.maincontainer {
|
||||
position: relative;
|
||||
border-radius: 8px;
|
||||
border-radius: 6px;
|
||||
padding: 16px;
|
||||
background: ${cssManager.bdTheme('#f8f9fa', '#1a1a1a')};
|
||||
color: ${cssManager.bdTheme('#333', '#ccc')};
|
||||
border: 1px solid ${cssManager.bdTheme('#e0e0e0', '#333')};
|
||||
transition: all 0.2s ease;
|
||||
background: ${cssManager.bdTheme('hsl(210 40% 98%)', 'hsl(215 20.2% 11.8%)')};
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 9%)', 'hsl(0 0% 95%)')};
|
||||
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('#ccc', '#444')};
|
||||
border-color: ${cssManager.bdTheme('hsl(215 20.2% 55.1%)', 'hsl(215 20.2% 45.1%)')};
|
||||
}
|
||||
|
||||
:host([disabled]) .maincontainer {
|
||||
@ -102,69 +102,69 @@ export class DeesInputFileupload extends DeesInputBase<DeesInputFileupload> {
|
||||
}
|
||||
|
||||
:host([validationState="invalid"]) .maincontainer {
|
||||
border-color: #e74c3c;
|
||||
border-color: ${cssManager.bdTheme('hsl(0 72.2% 50.6%)', 'hsl(0 62.8% 30.6%)')};
|
||||
}
|
||||
|
||||
:host([validationState="valid"]) .maincontainer {
|
||||
border-color: #27ae60;
|
||||
border-color: ${cssManager.bdTheme('hsl(142.1 70.6% 45.3%)', 'hsl(142.1 76.2% 36.3%)')};
|
||||
}
|
||||
|
||||
:host([validationState="warn"]) .maincontainer {
|
||||
border-color: #f39c12;
|
||||
border-color: ${cssManager.bdTheme('hsl(45.4 93.4% 47.5%)', 'hsl(45.4 93.4% 47.5%)')};
|
||||
}
|
||||
|
||||
.maincontainer::after {
|
||||
top: 2px;
|
||||
right: 2px;
|
||||
left: 2px;
|
||||
bottom: 2px;
|
||||
transform: scale3d(0.98, 0.9, 1);
|
||||
top: 1px;
|
||||
right: 1px;
|
||||
left: 1px;
|
||||
bottom: 1px;
|
||||
transform: scale3d(0.98, 0.95, 1);
|
||||
position: absolute;
|
||||
content: '';
|
||||
display: block;
|
||||
border: 2px dashed transparent;
|
||||
border-radius: 6px;
|
||||
transition: all 0.3s ease;
|
||||
border-radius: 5px;
|
||||
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
pointer-events: none;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.maincontainer.dragOver {
|
||||
border-color: ${cssManager.bdTheme('#0084ff', '#0084ff')};
|
||||
background: ${cssManager.bdTheme('#f0f8ff', '#001933')};
|
||||
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 {
|
||||
transform: scale3d(1, 1, 1);
|
||||
border: 2px dashed ${cssManager.bdTheme('#0084ff', '#0084ff')};
|
||||
border: 2px dashed ${cssManager.bdTheme('hsl(217.2 91.2% 59.8%)', 'hsl(213.1 93.9% 67.8%)')};
|
||||
}
|
||||
|
||||
.uploadButton {
|
||||
position: relative;
|
||||
padding: 12px 24px;
|
||||
background: ${cssManager.bdTheme('#0084ff', '#0084ff')};
|
||||
color: white;
|
||||
padding: 10px 20px;
|
||||
background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(0 0% 7.8%)')};
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 9%)', 'hsl(0 0% 95%)')};
|
||||
border: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
|
||||
border-radius: 6px;
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
border: none;
|
||||
transition: all 0.15s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.uploadButton:hover {
|
||||
background: ${cssManager.bdTheme('#0073e6', '#0073e6')};
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 2px 8px rgba(0, 132, 255, 0.3);
|
||||
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%)')};
|
||||
}
|
||||
|
||||
.uploadButton:active {
|
||||
transform: translateY(0);
|
||||
background: ${cssManager.bdTheme('hsl(0 0% 91%)', 'hsl(0 0% 11%)')};
|
||||
}
|
||||
|
||||
.uploadButton dees-icon {
|
||||
@ -181,21 +181,21 @@ export class DeesInputFileupload extends DeesInputBase<DeesInputFileupload> {
|
||||
.uploadCandidate {
|
||||
display: grid;
|
||||
grid-template-columns: 40px 1fr auto;
|
||||
background: ${cssManager.bdTheme('#ffffff', '#2a2a2a')};
|
||||
background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(215 20.2% 16.8%)')};
|
||||
padding: 12px;
|
||||
text-align: left;
|
||||
border-radius: 6px;
|
||||
color: ${cssManager.bdTheme('#333', '#ccc')};
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 9%)', 'hsl(0 0% 95%)')};
|
||||
cursor: default;
|
||||
transition: all 0.2s;
|
||||
border: 1px solid ${cssManager.bdTheme('#e0e0e0', '#333')};
|
||||
transition: all 0.15s ease;
|
||||
border: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.uploadCandidate:hover {
|
||||
background: ${cssManager.bdTheme('#f5f5f5', '#333')};
|
||||
border-color: ${cssManager.bdTheme('#ccc', '#444')};
|
||||
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 {
|
||||
@ -203,19 +203,19 @@ export class DeesInputFileupload extends DeesInputBase<DeesInputFileupload> {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 20px;
|
||||
color: ${cssManager.bdTheme('#666', '#999')};
|
||||
color: ${cssManager.bdTheme('hsl(215.4 16.3% 56.9%)', 'hsl(215 20.2% 55.1%)')};
|
||||
}
|
||||
|
||||
.uploadCandidate.image-file .icon {
|
||||
color: #4CAF50;
|
||||
color: ${cssManager.bdTheme('hsl(142.1 70.6% 45.3%)', 'hsl(142.1 76.2% 36.3%)')};
|
||||
}
|
||||
|
||||
.uploadCandidate.pdf-file .icon {
|
||||
color: #f44336;
|
||||
color: ${cssManager.bdTheme('hsl(0 72.2% 50.6%)', 'hsl(0 62.8% 30.6%)')};
|
||||
}
|
||||
|
||||
.uploadCandidate.doc-file .icon {
|
||||
color: #2196F3;
|
||||
color: ${cssManager.bdTheme('hsl(217.2 91.2% 59.8%)', 'hsl(213.1 93.9% 67.8%)')};
|
||||
}
|
||||
|
||||
.uploadCandidate .info {
|
||||
@ -235,7 +235,7 @@ export class DeesInputFileupload extends DeesInputBase<DeesInputFileupload> {
|
||||
|
||||
.uploadCandidate .filesize {
|
||||
font-size: 12px;
|
||||
color: ${cssManager.bdTheme('#666', '#999')};
|
||||
color: ${cssManager.bdTheme('hsl(215.4 16.3% 56.9%)', 'hsl(215 20.2% 55.1%)')};
|
||||
}
|
||||
|
||||
.uploadCandidate .actions {
|
||||
@ -254,13 +254,13 @@ export class DeesInputFileupload extends DeesInputBase<DeesInputFileupload> {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.2s;
|
||||
color: ${cssManager.bdTheme('#666', '#999')};
|
||||
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('#fee', '#4a1c1c')};
|
||||
color: ${cssManager.bdTheme('#e74c3c', '#ff6b6b')};
|
||||
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 {
|
||||
@ -271,36 +271,37 @@ export class DeesInputFileupload extends DeesInputBase<DeesInputFileupload> {
|
||||
.clear-all-button button {
|
||||
background: none;
|
||||
border: none;
|
||||
color: ${cssManager.bdTheme('#666', '#999')};
|
||||
color: ${cssManager.bdTheme('hsl(215.4 16.3% 56.9%)', 'hsl(215 20.2% 55.1%)')};
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
transition: all 0.2s;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.clear-all-button button:hover {
|
||||
background: ${cssManager.bdTheme('#fee', '#4a1c1c')};
|
||||
color: ${cssManager.bdTheme('#e74c3c', '#ff6b6b')};
|
||||
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: 12px;
|
||||
margin-top: 4px;
|
||||
color: #e74c3c;
|
||||
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('#999', '#666')};
|
||||
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.3;
|
||||
opacity: 0.2;
|
||||
}
|
||||
|
||||
.image-preview {
|
||||
@ -311,10 +312,10 @@ export class DeesInputFileupload extends DeesInputBase<DeesInputFileupload> {
|
||||
}
|
||||
|
||||
.description-text {
|
||||
font-size: 12px;
|
||||
color: ${cssManager.bdTheme('#666', '#999')};
|
||||
margin-top: 4px;
|
||||
line-height: 1.4;
|
||||
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;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
@ -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`
|
||||
<dees-demowrapper>
|
||||
@ -7,10 +7,31 @@ export const demoFunc = () => html`
|
||||
.demo-container {
|
||||
display: flex;
|
||||
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;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.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 {
|
||||
@ -28,7 +49,10 @@ export const demoFunc = () => html`
|
||||
</style>
|
||||
|
||||
<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
|
||||
.label=${'Display Mode'}
|
||||
.description=${'Choose how content is displayed'}
|
||||
@ -36,15 +60,20 @@ export const demoFunc = () => html`
|
||||
.selectedOption=${'Grid View'}
|
||||
></dees-input-multitoggle>
|
||||
|
||||
<br><br>
|
||||
|
||||
<dees-input-multitoggle
|
||||
.label=${'T-Shirt Size'}
|
||||
.description=${'Select your preferred size'}
|
||||
.options=${['XS', 'S', 'M', 'L', 'XL', 'XXL']}
|
||||
.selectedOption=${'M'}
|
||||
></dees-input-multitoggle>
|
||||
</dees-panel>
|
||||
</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
|
||||
.label=${'Notifications'}
|
||||
.description=${'Enable or disable push notifications'}
|
||||
@ -52,6 +81,8 @@ export const demoFunc = () => html`
|
||||
.selectedOption=${'true'}
|
||||
></dees-input-multitoggle>
|
||||
|
||||
<br><br>
|
||||
|
||||
<dees-input-multitoggle
|
||||
.label=${'Theme Mode'}
|
||||
.description=${'Switch between light and dark theme'}
|
||||
@ -60,13 +91,15 @@ export const demoFunc = () => html`
|
||||
.booleanFalseName=${'Light'}
|
||||
.selectedOption=${'Dark'}
|
||||
></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">
|
||||
<dees-input-multitoggle
|
||||
.label=${'Auto-Save'}
|
||||
.layoutMode=${'horizontal'}
|
||||
.type=${'boolean'}
|
||||
.booleanTrueName=${'Enabled'}
|
||||
.booleanFalseName=${'Disabled'}
|
||||
@ -75,30 +108,30 @@ export const demoFunc = () => html`
|
||||
|
||||
<dees-input-multitoggle
|
||||
.label=${'Language'}
|
||||
.layoutMode=${'horizontal'}
|
||||
.options=${['English', 'German', 'French', 'Spanish']}
|
||||
.selectedOption=${'English'}
|
||||
></dees-input-multitoggle>
|
||||
|
||||
<dees-input-multitoggle
|
||||
.label=${'Quality'}
|
||||
.layoutMode=${'horizontal'}
|
||||
.options=${['Low', 'Medium', 'High', 'Ultra']}
|
||||
.selectedOption=${'High'}
|
||||
></dees-input-multitoggle>
|
||||
|
||||
<dees-input-multitoggle
|
||||
.label=${'Privacy'}
|
||||
.layoutMode=${'horizontal'}
|
||||
.type=${'boolean'}
|
||||
.booleanTrueName=${'Private'}
|
||||
.booleanFalseName=${'Public'}
|
||||
.selectedOption=${'Private'}
|
||||
></dees-input-multitoggle>
|
||||
</div>
|
||||
</dees-panel>
|
||||
</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
|
||||
.label=${'Account Type'}
|
||||
.description=${'This setting is locked'}
|
||||
@ -107,6 +140,8 @@ export const demoFunc = () => html`
|
||||
.disabled=${true}
|
||||
></dees-input-multitoggle>
|
||||
|
||||
<br><br>
|
||||
|
||||
<dees-form>
|
||||
<dees-input-text .label=${'Project Name'} .required=${true}></dees-input-text>
|
||||
<dees-input-multitoggle
|
||||
@ -122,7 +157,7 @@ export const demoFunc = () => html`
|
||||
.selectedOption=${'MIT'}
|
||||
></dees-input-multitoggle>
|
||||
</dees-form>
|
||||
</dees-panel>
|
||||
</div>
|
||||
</div>
|
||||
</dees-demowrapper>
|
||||
`;
|
@ -57,9 +57,12 @@ export class DeesInputMultitoggle extends DeesInputBase<DeesInputMultitoggle> {
|
||||
} else {
|
||||
this.selectedOption = val as string;
|
||||
}
|
||||
this.requestUpdate();
|
||||
// Defer indicator update to next frame if component not yet updated
|
||||
if (this.hasUpdated) {
|
||||
requestAnimationFrame(() => {
|
||||
this.setIndicator();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -68,59 +71,71 @@ export class DeesInputMultitoggle extends DeesInputBase<DeesInputMultitoggle> {
|
||||
cssManager.defaultStyles,
|
||||
css`
|
||||
:host {
|
||||
color: ${cssManager.bdTheme('#333', '#ccc')};
|
||||
color: ${cssManager.bdTheme('#09090b', '#fafafa')};
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
|
||||
.selections {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: nowrap;
|
||||
background: ${cssManager.bdTheme('#fff', '#222')};
|
||||
width: min-content;
|
||||
border-radius: 20px;
|
||||
height: 32px;
|
||||
border-top: 1px solid ${cssManager.bdTheme('rgba(0,0,0,0.1)', 'rgba(255,255,255,0.1)')};
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
background: ${cssManager.bdTheme('#ffffff', '#18181b')};
|
||||
border: 1px solid ${cssManager.bdTheme('#e5e7eb', '#27272a')};
|
||||
padding: 4px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.option {
|
||||
color: ${cssManager.bdTheme('#666', '#999')};
|
||||
position: relative;
|
||||
padding: 0px 16px;
|
||||
line-height: 32px;
|
||||
cursor: default;
|
||||
width: min-content; /* Make the width as per the content */
|
||||
white-space: nowrap; /* Prevent text wrapping */
|
||||
transition: all 0.1s;
|
||||
padding: 8px 20px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
transition: color 0.2s ease;
|
||||
font-size: 14px;
|
||||
transform: translateY(-1px);
|
||||
font-weight: 500;
|
||||
color: ${cssManager.bdTheme('#71717a', '#71717a')};
|
||||
line-height: 1;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.option:hover {
|
||||
color: ${cssManager.bdTheme('#333', '#fff')};
|
||||
color: ${cssManager.bdTheme('#18181b', '#e4e4e7')};
|
||||
}
|
||||
|
||||
.option.selected {
|
||||
color: ${cssManager.bdTheme('#fff', '#fff')};
|
||||
color: ${cssManager.bdTheme('#3b82f6', '#60a5fa')};
|
||||
}
|
||||
|
||||
.indicator {
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
height: 24px;
|
||||
left: 4px;
|
||||
top: 3px;
|
||||
border-radius: 16px;
|
||||
background: ${cssManager.bdTheme(colors.bright.blueActive, colors.dark.blueActive)};
|
||||
min-width: 24px;
|
||||
transition: all 0.1s ease-in-out;
|
||||
height: calc(100% - 8px);
|
||||
top: 4px;
|
||||
border-radius: 6px;
|
||||
background: ${cssManager.bdTheme('rgba(59, 130, 246, 0.15)', 'rgba(59, 130, 246, 0.15)')};
|
||||
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.indicator.no-transition {
|
||||
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
|
||||
if (this.type === 'boolean' && this.options.length === 0) {
|
||||
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
|
||||
await this.updateComplete;
|
||||
requestAnimationFrame(() => {
|
||||
|
||||
// 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) {
|
||||
if (this.disabled) return;
|
||||
this.selectedOption = optionArg;
|
||||
this.requestUpdate();
|
||||
this.changeSubject.next(this);
|
||||
await this.updateComplete;
|
||||
this.setIndicator();
|
||||
}
|
||||
|
||||
@ -199,8 +234,8 @@ export class DeesInputMultitoggle extends DeesInputBase<DeesInputMultitoggle> {
|
||||
}, 50);
|
||||
}
|
||||
|
||||
indicator.style.width = `${option.clientWidth - 8}px`;
|
||||
indicator.style.left = `${option.offsetLeft + 4}px`;
|
||||
indicator.style.width = `${option.clientWidth}px`;
|
||||
indicator.style.left = `${option.offsetLeft}px`;
|
||||
indicator.style.opacity = '1';
|
||||
}
|
||||
}
|
||||
@ -218,8 +253,11 @@ export class DeesInputMultitoggle extends DeesInputBase<DeesInputMultitoggle> {
|
||||
} else {
|
||||
this.selectedOption = value as string;
|
||||
}
|
||||
this.requestUpdate();
|
||||
if (this.hasUpdated) {
|
||||
requestAnimationFrame(() => {
|
||||
this.setIndicator();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { html, css, cssManager } from '@design.estate/dees-element';
|
||||
import './dees-shopping-productcard.js';
|
||||
|
||||
export const demoFunc = () => html`
|
||||
<dees-demowrapper>
|
||||
@ -15,25 +16,44 @@ export const demoFunc = () => html`
|
||||
|
||||
.shopping-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
||||
gap: 16px;
|
||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.product-card {
|
||||
padding: 16px;
|
||||
background: ${cssManager.bdTheme('#fff', '#2a2a2a')};
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 4px ${cssManager.bdTheme('rgba(0,0,0,0.1)', 'rgba(0,0,0,0.3)')};
|
||||
.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;
|
||||
}
|
||||
|
||||
.product-name {
|
||||
.cart-summary-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 8px;
|
||||
margin-bottom: 16px;
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 9%)', 'hsl(0 0% 95%)')};
|
||||
}
|
||||
|
||||
.product-price {
|
||||
color: #1976d2;
|
||||
margin-bottom: 16px;
|
||||
.cart-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px 0;
|
||||
font-size: 14px;
|
||||
color: ${cssManager.bdTheme('hsl(215.3 25% 26.7%)', 'hsl(217.9 10.6% 74.9%)')};
|
||||
}
|
||||
|
||||
.cart-total {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding-top: 16px;
|
||||
margin-top: 16px;
|
||||
border-top: 2px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 9%)', 'hsl(0 0% 95%)')};
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
@ -53,36 +73,97 @@ export const demoFunc = () => html`
|
||||
></dees-input-quantityselector>
|
||||
</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="product-card">
|
||||
<div class="product-name">Premium Headphones</div>
|
||||
<div class="product-price">$199.99</div>
|
||||
<dees-input-quantityselector
|
||||
.label=${'Quantity'}
|
||||
.layoutMode=${'horizontal'}
|
||||
.value=${1}
|
||||
></dees-input-quantityselector>
|
||||
<dees-shopping-productcard
|
||||
id="headphones-qty"
|
||||
.productData=${{
|
||||
name: 'Sony WH-1000XM5 Wireless Headphones',
|
||||
category: 'Audio',
|
||||
description: 'Industry-leading noise canceling with Auto NC Optimizer',
|
||||
price: 349.99,
|
||||
originalPrice: 399.99,
|
||||
iconName: 'lucide:headphones'
|
||||
}}
|
||||
.quantity=${1}
|
||||
></dees-shopping-productcard>
|
||||
|
||||
<dees-shopping-productcard
|
||||
id="mouse-qty"
|
||||
.productData=${{
|
||||
name: 'Logitech MX Master 3S',
|
||||
category: 'Accessories',
|
||||
description: 'Performance wireless mouse with ultra-fast scrolling',
|
||||
price: 99.99,
|
||||
iconName: 'lucide:mouse-pointer'
|
||||
}}
|
||||
.quantity=${2}
|
||||
></dees-shopping-productcard>
|
||||
|
||||
<dees-shopping-productcard
|
||||
id="keyboard-qty"
|
||||
.productData=${{
|
||||
name: 'Keychron K2 Wireless Mechanical Keyboard',
|
||||
category: 'Keyboards',
|
||||
description: 'Compact 75% layout with hot-swappable switches',
|
||||
price: 79.99,
|
||||
originalPrice: 94.99,
|
||||
iconName: 'lucide:keyboard'
|
||||
}}
|
||||
.quantity=${1}
|
||||
></dees-shopping-productcard>
|
||||
</div>
|
||||
|
||||
<div class="product-card">
|
||||
<div class="product-name">Wireless Mouse</div>
|
||||
<div class="product-price">$49.99</div>
|
||||
<dees-input-quantityselector
|
||||
.label=${'Quantity'}
|
||||
.layoutMode=${'horizontal'}
|
||||
.value=${2}
|
||||
></dees-input-quantityselector>
|
||||
</div>
|
||||
|
||||
<div class="product-card">
|
||||
<div class="product-name">USB-C Cable</div>
|
||||
<div class="product-price">$19.99</div>
|
||||
<dees-input-quantityselector
|
||||
.label=${'Quantity'}
|
||||
.layoutMode=${'horizontal'}
|
||||
.value=${1}
|
||||
></dees-input-quantityselector>
|
||||
<div class="cart-summary">
|
||||
<h3 class="cart-summary-title">Order Summary</h3>
|
||||
<div id="cart-summary-content">
|
||||
<!-- Content will be dynamically updated -->
|
||||
</div>
|
||||
</div>
|
||||
</dees-panel>
|
||||
|
@ -32,48 +32,91 @@ export class DeesInputQuantitySelector extends DeesInputBase<DeesInputQuantitySe
|
||||
}
|
||||
|
||||
.quantity-container {
|
||||
transition: all 0.1s;
|
||||
transition: all 0.15s ease;
|
||||
font-size: 14px;
|
||||
display: grid;
|
||||
grid-template-columns: 33% 34% 33%;
|
||||
text-align: center;
|
||||
background: ${cssManager.bdTheme('#fafafa', '#222222')};
|
||||
line-height: 40px;
|
||||
padding: 0px;
|
||||
min-width: 110px;
|
||||
color: ${cssManager.bdTheme('#666', '#CCC')};
|
||||
border: 1px solid ${cssManager.bdTheme('#CCC', '#444')};
|
||||
border-radius: 4px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
background: transparent;
|
||||
height: 40px;
|
||||
padding: 0;
|
||||
min-width: 120px;
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 9%)', 'hsl(0 0% 95%)')};
|
||||
border: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.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;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.quantity-container:hover {
|
||||
color: ${cssManager.bdTheme('#333', '#fff')};
|
||||
border-color: ${cssManager.bdTheme('#999', '#666')};
|
||||
.quantity-container:hover:not(.disabled) {
|
||||
border-color: ${cssManager.bdTheme('hsl(0 0% 79.8%)', 'hsl(0 0% 20.9%)')};
|
||||
}
|
||||
|
||||
.minus {
|
||||
padding-left: 5px;
|
||||
}
|
||||
|
||||
.plus {
|
||||
padding-right: 5px;
|
||||
.quantity-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)')};
|
||||
}
|
||||
|
||||
.selector {
|
||||
text-align: center;
|
||||
font-size: 20px;
|
||||
flex: 0 0 40px;
|
||||
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 {
|
||||
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 {
|
||||
flex: 1;
|
||||
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 {
|
||||
return html`
|
||||
<div class="input-wrapper">
|
||||
<dees-label .label=${this.label}></dees-label>
|
||||
<div class="quantity-container ${this.disabled ? 'disabled' : ''}">
|
||||
<div class="selector minus" @click="${() => {this.decrease();}}">-</div>
|
||||
<div class="quantity">${this.value}</div>
|
||||
<div class="selector plus" @click="${() => {this.increase();}}">+</div>
|
||||
${this.label ? html`<dees-label .label=${this.label} .description=${this.description} .required=${this.required}></dees-label>` : ''}
|
||||
<div
|
||||
class="quantity-container ${this.disabled ? 'disabled' : ''}"
|
||||
data-min="${this.value <= 0}"
|
||||
>
|
||||
<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>
|
||||
`;
|
||||
|
@ -69,80 +69,97 @@ export class DeesInputRadiogroup extends DeesInputBase<string | object> {
|
||||
:host {
|
||||
display: block;
|
||||
position: relative;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
|
||||
}
|
||||
|
||||
.maincontainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.maincontainer.horizontal {
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.radio-option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 8px 0;
|
||||
gap: 10px;
|
||||
padding: 6px 0;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
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: 8px 16px 8px 0;
|
||||
padding: 6px 20px 6px 0;
|
||||
}
|
||||
|
||||
.radio-option:hover .radio-circle {
|
||||
border-color: ${cssManager.bdTheme('#0050b9', '#0084ff')};
|
||||
border-color: ${cssManager.bdTheme('hsl(215 20.2% 65.1%)', 'hsl(215 20.2% 35.1%)')};
|
||||
background: ${cssManager.bdTheme('hsl(210 40% 96.1%)', 'hsl(215 20.2% 11.8%)')};
|
||||
}
|
||||
|
||||
.radio-option:hover .radio-label {
|
||||
color: ${cssManager.bdTheme('#1a1a1a', '#ffffff')};
|
||||
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('#999', '#666')};
|
||||
background: ${cssManager.bdTheme('#fff', '#1a1a1a')};
|
||||
transition: all 0.2s ease;
|
||||
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('#0050b9', '#0084ff')};
|
||||
background: ${cssManager.bdTheme('#0050b9', '#0084ff')};
|
||||
border-color: ${cssManager.bdTheme('hsl(217.2 91.2% 59.8%)', 'hsl(213.1 93.9% 67.8%)')};
|
||||
background: ${cssManager.bdTheme('hsl(217.2 91.2% 59.8%)', 'hsl(213.1 93.9% 67.8%)')};
|
||||
}
|
||||
|
||||
.radio-option.selected .radio-circle::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: white;
|
||||
background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(215 30% 6.8%)')};
|
||||
transform: scale(0);
|
||||
transition: transform 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.radio-option.selected .radio-circle::after {
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
.radio-circle:focus-visible {
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 2px ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(215 30% 3.9%)')},
|
||||
0 0 0 4px ${cssManager.bdTheme('hsl(217.2 91.2% 59.8%)', 'hsl(213.1 93.9% 67.8%)')};
|
||||
}
|
||||
|
||||
.radio-label {
|
||||
font-size: 14px;
|
||||
color: ${cssManager.bdTheme('#666', '#999')};
|
||||
transition: color 0.2s ease;
|
||||
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('#1a1a1a', '#ffffff')};
|
||||
font-weight: 500;
|
||||
color: ${cssManager.bdTheme('hsl(215.3 25% 8.8%)', 'hsl(210 40% 98%)')};
|
||||
}
|
||||
|
||||
:host([disabled]) .radio-option {
|
||||
@ -151,40 +168,49 @@ export class DeesInputRadiogroup extends DeesInputBase<string | object> {
|
||||
}
|
||||
|
||||
:host([disabled]) .radio-option:hover .radio-circle {
|
||||
border-color: ${cssManager.bdTheme('#999', '#666')};
|
||||
border-color: ${cssManager.bdTheme('hsl(215 20.2% 65.1%)', 'hsl(215 20.2% 35.1%)')};
|
||||
background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(215 30% 6.8%)')};
|
||||
}
|
||||
|
||||
:host([disabled]) .radio-option:hover .radio-label {
|
||||
color: ${cssManager.bdTheme('#666', '#999')};
|
||||
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('#333', '#ccc')};
|
||||
margin-bottom: 8px;
|
||||
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: 12px;
|
||||
color: ${cssManager.bdTheme('#666', '#999')};
|
||||
margin-top: 8px;
|
||||
line-height: 1.4;
|
||||
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: #e74c3c;
|
||||
border-color: ${cssManager.bdTheme('hsl(0 72.2% 50.6%)', 'hsl(0 62.8% 30.6%)')};
|
||||
}
|
||||
|
||||
:host([validationState="invalid"]) .radio-option.selected .radio-circle {
|
||||
border-color: ${cssManager.bdTheme('hsl(0 72.2% 50.6%)', 'hsl(0 62.8% 30.6%)')};
|
||||
background: ${cssManager.bdTheme('hsl(0 72.2% 50.6%)', 'hsl(0 62.8% 30.6%)')};
|
||||
}
|
||||
|
||||
:host([validationState="valid"]) .radio-option.selected .radio-circle {
|
||||
border-color: #27ae60;
|
||||
background: #27ae60;
|
||||
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: #f39c12;
|
||||
background: #f39c12;
|
||||
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 */
|
||||
@ -212,8 +238,15 @@ export class DeesInputRadiogroup extends DeesInputBase<string | object> {
|
||||
<div
|
||||
class="radio-option ${isSelected ? 'selected' : ''}"
|
||||
@click="${() => this.selectOption(optionKey)}"
|
||||
@keydown="${(e: KeyboardEvent) => this.handleKeydown(e, optionKey)}"
|
||||
>
|
||||
<div class="radio-circle"></div>
|
||||
<div
|
||||
class="radio-circle"
|
||||
tabindex="${this.disabled ? '-1' : '0'}"
|
||||
role="radio"
|
||||
aria-checked="${isSelected}"
|
||||
aria-label="${optionLabel}"
|
||||
></div>
|
||||
<div class="radio-label">${optionLabel}</div>
|
||||
</div>
|
||||
`;
|
||||
@ -292,4 +325,33 @@ export class DeesInputRadiogroup extends DeesInputBase<string | object> {
|
||||
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();
|
||||
}
|
||||
}
|
@ -96,27 +96,27 @@ export class DeesInputRichtext extends DeesInputBase<string> {
|
||||
margin-bottom: 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: ${cssManager.bdTheme('#374151', '#e4e4e7')};
|
||||
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('#e1e5e9', '#2c2c2c')};
|
||||
border-radius: 8px;
|
||||
background: ${cssManager.bdTheme('#ffffff', '#141414')};
|
||||
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 ease;
|
||||
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.editor-container:hover {
|
||||
border-color: ${cssManager.bdTheme('#d1d5db', '#404040')};
|
||||
border-color: ${cssManager.bdTheme('hsl(0 0% 79.8%)', 'hsl(0 0% 20.9%)')};
|
||||
}
|
||||
|
||||
.editor-container.focused {
|
||||
border-color: ${cssManager.bdTheme('#0050b9', '#0069f2')};
|
||||
box-shadow: 0 0 0 3px ${cssManager.bdTheme('rgba(0, 80, 185, 0.1)', 'rgba(0, 105, 242, 0.1)')};
|
||||
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 {
|
||||
@ -124,8 +124,8 @@ export class DeesInputRichtext extends DeesInputBase<string> {
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
padding: 8px 12px;
|
||||
background: ${cssManager.bdTheme('#f8f9fa', '#1a1a1a')};
|
||||
border-bottom: 1px solid ${cssManager.bdTheme('#e1e5e9', '#2c2c2c')};
|
||||
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;
|
||||
}
|
||||
@ -142,8 +142,8 @@ export class DeesInputRichtext extends DeesInputBase<string> {
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: ${cssManager.bdTheme('#374151', '#9ca3af')};
|
||||
transition: all 0.2s;
|
||||
color: ${cssManager.bdTheme('hsl(215.4 16.3% 46.9%)', 'hsl(215 20.2% 65.1%)')};
|
||||
transition: all 0.15s ease;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
@ -153,13 +153,13 @@ export class DeesInputRichtext extends DeesInputBase<string> {
|
||||
}
|
||||
|
||||
.toolbar-button:hover {
|
||||
background: ${cssManager.bdTheme('#e5e7eb', '#2c2c2c')};
|
||||
color: ${cssManager.bdTheme('#1f2937', '#e4e4e7')};
|
||||
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('#0050b9', '#0069f2')};
|
||||
color: white;
|
||||
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 {
|
||||
@ -170,7 +170,7 @@ export class DeesInputRichtext extends DeesInputBase<string> {
|
||||
.toolbar-divider {
|
||||
width: 1px;
|
||||
height: 24px;
|
||||
background: ${cssManager.bdTheme('#d1d5db', '#404040')};
|
||||
background: ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
|
||||
margin: 0 4px;
|
||||
}
|
||||
|
||||
@ -184,7 +184,7 @@ export class DeesInputRichtext extends DeesInputBase<string> {
|
||||
.editor-content .ProseMirror {
|
||||
outline: none;
|
||||
line-height: 1.6;
|
||||
color: ${cssManager.bdTheme('#374151', '#e4e4e7')};
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 3.9%)', 'hsl(0 0% 98%)')};
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
@ -232,25 +232,25 @@ export class DeesInputRichtext extends DeesInputBase<string> {
|
||||
}
|
||||
|
||||
.editor-content .ProseMirror blockquote {
|
||||
border-left: 4px solid ${cssManager.bdTheme('#d1d5db', '#404040')};
|
||||
border-left: 4px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
|
||||
margin: 1em 0;
|
||||
padding-left: 1em;
|
||||
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
|
||||
color: ${cssManager.bdTheme('hsl(215.4 16.3% 46.9%)', 'hsl(215 20.2% 65.1%)')};
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.editor-content .ProseMirror code {
|
||||
background: ${cssManager.bdTheme('#f3f4f6', '#2c2c2c')};
|
||||
border-radius: 4px;
|
||||
background: ${cssManager.bdTheme('hsl(0 0% 95.1%)', 'hsl(0 0% 14.9%)')};
|
||||
border-radius: 3px;
|
||||
padding: 0.2em 0.4em;
|
||||
font-family: 'Fira Code', 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
|
||||
font-family: 'Intel One Mono', 'Fira Code', 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
|
||||
font-size: 0.9em;
|
||||
color: ${cssManager.bdTheme('#e11d48', '#f87171')};
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 15%)', 'hsl(0 0% 93.9%)')};
|
||||
}
|
||||
|
||||
.editor-content .ProseMirror pre {
|
||||
background: ${cssManager.bdTheme('#1f2937', '#0a0a0a')};
|
||||
color: ${cssManager.bdTheme('#f9fafb', '#e4e4e7')};
|
||||
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;
|
||||
@ -265,21 +265,21 @@ export class DeesInputRichtext extends DeesInputBase<string> {
|
||||
}
|
||||
|
||||
.editor-content .ProseMirror a {
|
||||
color: ${cssManager.bdTheme('#0050b9', '#0069f2')};
|
||||
color: ${cssManager.bdTheme('hsl(222.2 47.4% 51.2%)', 'hsl(217.2 91.2% 59.8%)')};
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.editor-content .ProseMirror a:hover {
|
||||
color: ${cssManager.bdTheme('#0069f2', '#0084ff')};
|
||||
color: ${cssManager.bdTheme('hsl(222.2 47.4% 41.2%)', 'hsl(217.2 91.2% 69.8%)')};
|
||||
}
|
||||
|
||||
.editor-footer {
|
||||
padding: 8px 12px;
|
||||
background: ${cssManager.bdTheme('#f8f9fa', '#1a1a1a')};
|
||||
border-top: 1px solid ${cssManager.bdTheme('#e1e5e9', '#2c2c2c')};
|
||||
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('#6b7280', '#9ca3af')};
|
||||
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;
|
||||
@ -295,8 +295,8 @@ export class DeesInputRichtext extends DeesInputBase<string> {
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: ${cssManager.bdTheme('#ffffff', '#1a1a1a')};
|
||||
border: 1px solid ${cssManager.bdTheme('#d1d5db', '#404040')};
|
||||
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;
|
||||
@ -310,17 +310,18 @@ export class DeesInputRichtext extends DeesInputBase<string> {
|
||||
.link-input input {
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid ${cssManager.bdTheme('#d1d5db', '#404040')};
|
||||
border-radius: 4px;
|
||||
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('#ffffff', '#0a0a0a')};
|
||||
color: ${cssManager.bdTheme('#374151', '#e4e4e7')};
|
||||
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('#0050b9', '#0069f2')};
|
||||
box-shadow: 0 0 0 2px ${cssManager.bdTheme('rgba(0, 80, 185, 0.1)', 'rgba(0, 105, 242, 0.1)')};
|
||||
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 {
|
||||
@ -331,33 +332,36 @@ export class DeesInputRichtext extends DeesInputBase<string> {
|
||||
|
||||
.link-input-buttons button {
|
||||
padding: 6px 12px;
|
||||
border: 1px solid ${cssManager.bdTheme('#d1d5db', '#404040')};
|
||||
border: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
|
||||
border-radius: 4px;
|
||||
background: ${cssManager.bdTheme('#ffffff', '#1a1a1a')};
|
||||
background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(0 0% 9%)')};
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
color: ${cssManager.bdTheme('#374151', '#e4e4e7')};
|
||||
transition: all 0.2s;
|
||||
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('#f3f4f6', '#2c2c2c')};
|
||||
background: ${cssManager.bdTheme('hsl(0 0% 95.1%)', 'hsl(0 0% 14.9%)')};
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 9%)', 'hsl(0 0% 95%)')};
|
||||
}
|
||||
|
||||
.link-input-buttons button.primary {
|
||||
background: ${cssManager.bdTheme('#0050b9', '#0069f2')};
|
||||
color: white;
|
||||
border-color: ${cssManager.bdTheme('#0050b9', '#0069f2')};
|
||||
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('#0069f2', '#0084ff')};
|
||||
background: ${cssManager.bdTheme('hsl(0 0% 15%)', 'hsl(0 0% 93.9%)')};
|
||||
border-color: ${cssManager.bdTheme('hsl(0 0% 15%)', 'hsl(0 0% 93.9%)')};
|
||||
}
|
||||
|
||||
.description {
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
|
||||
color: ${cssManager.bdTheme('hsl(215.4 16.3% 46.9%)', 'hsl(215 20.2% 65.1%)')};
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
|
@ -53,7 +53,7 @@ export class DeesInputTags extends DeesInputBase<DeesInputTags> {
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
font-family: 'Geist Sans', sans-serif;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
|
||||
}
|
||||
|
||||
.input-wrapper {
|
||||
@ -64,44 +64,51 @@ export class DeesInputTags extends DeesInputBase<DeesInputTags> {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px;
|
||||
gap: 6px;
|
||||
padding: 6px 10px;
|
||||
min-height: 40px;
|
||||
background: ${cssManager.bdTheme('#fafafa', '#222222')};
|
||||
border: 1px solid ${cssManager.bdTheme('#e0e0e0', '#333333')};
|
||||
border-radius: 8px;
|
||||
transition: all 0.2s ease;
|
||||
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('#0069f2', '#0084ff')};
|
||||
box-shadow: 0 0 0 2px ${cssManager.bdTheme('rgba(0, 105, 242, 0.1)', 'rgba(0, 132, 255, 0.2)')};
|
||||
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('#f5f5f5', '#1a1a1a')};
|
||||
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.6;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 4px 8px;
|
||||
background: ${cssManager.bdTheme('#e3f2fd', '#1e3a5f')};
|
||||
color: ${cssManager.bdTheme('#1976d2', '#90caf9')};
|
||||
border-radius: 16px;
|
||||
font-size: 14px;
|
||||
line-height: 1.2;
|
||||
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.2s ease;
|
||||
animation: tagAppear 0.15s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
@keyframes tagAppear {
|
||||
from {
|
||||
transform: scale(0.8);
|
||||
transform: scale(0.95);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
@ -114,21 +121,23 @@ export class DeesInputTags extends DeesInputBase<DeesInputTags> {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
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('rgba(0, 0, 0, 0.1)', 'rgba(255, 255, 255, 0.1)')};
|
||||
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: 12px;
|
||||
height: 12px;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
}
|
||||
|
||||
.tag-input {
|
||||
@ -139,12 +148,13 @@ export class DeesInputTags extends DeesInputBase<DeesInputTags> {
|
||||
outline: none;
|
||||
font-size: 14px;
|
||||
font-family: inherit;
|
||||
color: ${cssManager.bdTheme('#333', '#fff')};
|
||||
padding: 4px;
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 9%)', 'hsl(0 0% 95%)')};
|
||||
padding: 2px 4px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.tag-input::placeholder {
|
||||
color: ${cssManager.bdTheme('#999', '#666')};
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 63.9%)', 'hsl(0 0% 45.1%)')};
|
||||
}
|
||||
|
||||
.tag-input:disabled {
|
||||
@ -162,44 +172,64 @@ export class DeesInputTags extends DeesInputBase<DeesInputTags> {
|
||||
left: 0;
|
||||
right: 0;
|
||||
margin-top: 4px;
|
||||
background: ${cssManager.bdTheme('#ffffff', '#222222')};
|
||||
border: 1px solid ${cssManager.bdTheme('#e0e0e0', '#333333')};
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 12px ${cssManager.bdTheme('rgba(0, 0, 0, 0.1)', 'rgba(0, 0, 0, 0.3)')};
|
||||
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: 8px 12px;
|
||||
padding: 6px 10px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
color: ${cssManager.bdTheme('#333', '#fff')};
|
||||
transition: all 0.15s ease;
|
||||
font-size: 14px;
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 15%)', 'hsl(0 0% 90%)')};
|
||||
}
|
||||
|
||||
.suggestion:hover,
|
||||
.suggestion.highlighted {
|
||||
background: ${cssManager.bdTheme('#f5f5f5', '#333333')};
|
||||
.suggestion:hover {
|
||||
background: ${cssManager.bdTheme('hsl(0 0% 95.1%)', 'hsl(0 0% 14.9%)')};
|
||||
}
|
||||
|
||||
.suggestion.highlighted {
|
||||
background: ${cssManager.bdTheme('#e3f2fd', '#1e3a5f')};
|
||||
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: #d32f2f;
|
||||
font-size: 12px;
|
||||
margin-top: 4px;
|
||||
min-height: 16px;
|
||||
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('#666', '#999')};
|
||||
font-size: 12px;
|
||||
margin-top: 4px;
|
||||
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%)')};
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
@ -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 './dees-panel.js';
|
||||
|
||||
export const demoFunc = () => html`
|
||||
<dees-demowrapper>
|
||||
@ -14,36 +15,12 @@ export const demoFunc = () => html`
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.demo-section {
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
padding: 24px;
|
||||
dees-panel {
|
||||
margin-bottom: 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;
|
||||
}
|
||||
dees-panel:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.horizontal-group {
|
||||
@ -64,14 +41,28 @@ export const demoFunc = () => html`
|
||||
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>
|
||||
|
||||
<div class="demo-container">
|
||||
<div class="demo-section">
|
||||
<h3>Basic Text Inputs</h3>
|
||||
<p>Standard text inputs with labels and descriptions</p>
|
||||
|
||||
<dees-panel .title=${'Basic Text Inputs'} .subtitle=${'Standard text inputs with labels and descriptions'}>
|
||||
<dees-input-text
|
||||
.label=${'Username'}
|
||||
.value=${'johndoe'}
|
||||
@ -91,12 +82,9 @@ export const demoFunc = () => html`
|
||||
.value=${'secret123'}
|
||||
.key=${'password'}
|
||||
></dees-input-text>
|
||||
</div>
|
||||
|
||||
<div class="demo-section">
|
||||
<h3>Horizontal Layout</h3>
|
||||
<p>Multiple inputs arranged horizontally for compact forms</p>
|
||||
</dees-panel>
|
||||
|
||||
<dees-panel .title=${'Horizontal Layout'} .subtitle=${'Multiple inputs arranged horizontally for compact forms'}>
|
||||
<div class="horizontal-group">
|
||||
<dees-input-text
|
||||
.label=${'First Name'}
|
||||
@ -119,12 +107,9 @@ export const demoFunc = () => html`
|
||||
.key=${'age'}
|
||||
></dees-input-text>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="demo-section">
|
||||
<h3>Label Positions</h3>
|
||||
<p>Different label positioning options for various layouts</p>
|
||||
</dees-panel>
|
||||
|
||||
<dees-panel .title=${'Label Positions'} .subtitle=${'Different label positioning options for various layouts'}>
|
||||
<dees-input-text
|
||||
.label=${'Label on Top (Default)'}
|
||||
.value=${'Standard layout'}
|
||||
@ -150,12 +135,9 @@ export const demoFunc = () => html`
|
||||
.labelPosition=${'left'}
|
||||
></dees-input-text>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="demo-section">
|
||||
<h3>Validation & States</h3>
|
||||
<p>Different validation states and input configurations</p>
|
||||
</dees-panel>
|
||||
|
||||
<dees-panel .title=${'Validation & States'} .subtitle=${'Different validation states and input configurations'}>
|
||||
<dees-input-text
|
||||
.label=${'Required Field'}
|
||||
.required=${true}
|
||||
@ -174,12 +156,9 @@ export const demoFunc = () => html`
|
||||
.validationText=${'Please enter a valid email address'}
|
||||
.validationState=${'invalid'}
|
||||
></dees-input-text>
|
||||
</div>
|
||||
|
||||
<div class="demo-section">
|
||||
<h3>Advanced Features</h3>
|
||||
<p>Password visibility toggle and other advanced features</p>
|
||||
</dees-panel>
|
||||
|
||||
<dees-panel .title=${'Advanced Features'} .subtitle=${'Password visibility toggle and other advanced features'}>
|
||||
<dees-input-text
|
||||
.label=${'Password with Toggle'}
|
||||
.isPasswordBool=${true}
|
||||
@ -193,7 +172,24 @@ export const demoFunc = () => html`
|
||||
.value=${'sk-1234567890abcdef'}
|
||||
.description=${'Keep this key secure and never share it'}
|
||||
></dees-input-text>
|
||||
</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>
|
||||
</dees-demowrapper>
|
||||
`;
|
@ -1,6 +1,7 @@
|
||||
import * as colors from './00colors.js';
|
||||
import { DeesInputBase } from './dees-input-base.js';
|
||||
import { demoFunc } from './dees-input-text.demo.js';
|
||||
import { cssGeistFontFamily, cssMonoFontFamily } from './00fonts.js';
|
||||
|
||||
import {
|
||||
customElement,
|
||||
@ -65,77 +66,129 @@ export class DeesInputText extends DeesInputBase {
|
||||
:host {
|
||||
position: relative;
|
||||
z-index: auto;
|
||||
font-family: ${cssGeistFontFamily};
|
||||
}
|
||||
|
||||
.maincontainer {
|
||||
color: ${cssManager.bdTheme('#333', '#ccc')};
|
||||
position: relative;
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 15%)', 'hsl(0 0% 90%)')};
|
||||
}
|
||||
|
||||
input {
|
||||
margin-top: 0px;
|
||||
background: ${cssManager.bdTheme('#fafafa', '#222')};
|
||||
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;
|
||||
display: flex;
|
||||
height: 40px;
|
||||
width: 100%;
|
||||
line-height: 36px;
|
||||
transition: all 0.2s;
|
||||
padding: 0 12px;
|
||||
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;
|
||||
font-size: 16px;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
cursor: default;
|
||||
cursor: text;
|
||||
font-family: inherit;
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 3.9%)', 'hsl(0 0% 98%)')};
|
||||
}
|
||||
|
||||
input:disabled {
|
||||
background: ${cssManager.bdTheme('#ffffff00', '#11111100')};
|
||||
border: 1px dashed ${cssManager.bdTheme('#666666', '#666666')};
|
||||
color: #9b9b9e;
|
||||
cursor: default;
|
||||
input::placeholder {
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 45.1%)', 'hsl(0 0% 63.9%)')};
|
||||
}
|
||||
|
||||
input:hover:not(:disabled):not(:focus) {
|
||||
border-color: ${cssManager.bdTheme('hsl(0 0% 79.8%)', 'hsl(0 0% 20.9%)')};
|
||||
}
|
||||
|
||||
input:focus {
|
||||
outline: none;
|
||||
border-bottom: 1px solid
|
||||
${cssManager.bdTheme(colors.bright.blueActive, colors.dark.blueActive)};
|
||||
cursor: text;
|
||||
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)')};
|
||||
}
|
||||
|
||||
input:hover {
|
||||
filter: ${cssManager.bdTheme('brightness(0.95)', 'brightness(1.1)')};
|
||||
input: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%)')};
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 45.1%)', 'hsl(0 0% 63.9%)')};
|
||||
cursor: not-allowed;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
/* Password toggle button */
|
||||
.showPassword {
|
||||
position: absolute;
|
||||
bottom: 7px;
|
||||
right: 10px;
|
||||
border: 1px dashed #444;
|
||||
border-radius: 7px;
|
||||
padding: 4px 0px;
|
||||
width: 40px;
|
||||
z-index: 3;
|
||||
text-align: center;
|
||||
right: 1px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
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 {
|
||||
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 {
|
||||
text-align: center;
|
||||
padding: 6px 2px 2px 2px;
|
||||
margin-top: -4px;
|
||||
margin-top: 4px;
|
||||
padding: 4px 8px;
|
||||
font-size: 12px;
|
||||
color: #fff;
|
||||
background: #e4002b;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
border-radius: 3px;
|
||||
transition: all 0.2s;
|
||||
font-weight: 500;
|
||||
border-radius: 4px;
|
||||
transition: all 0.2s ease;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.validationContainer.error {
|
||||
background: ${cssManager.bdTheme('hsl(0 84.2% 60.2% / 0.1)', 'hsl(0 72.2% 50.6% / 0.1)')};
|
||||
color: ${cssManager.bdTheme('hsl(0 84.2% 60.2%)', 'hsl(0 72.2% 50.6%)')};
|
||||
}
|
||||
|
||||
.validationContainer.warn {
|
||||
background: ${cssManager.bdTheme('hsl(25 95% 53% / 0.1)', 'hsl(25 95% 63% / 0.1)')};
|
||||
color: ${cssManager.bdTheme('hsl(25 95% 53%)', 'hsl(25 95% 63%)')};
|
||||
}
|
||||
|
||||
.validationContainer.valid {
|
||||
background: ${cssManager.bdTheme('hsl(142.1 76.2% 36.3% / 0.1)', 'hsl(142.1 70.6% 45.3% / 0.1)')};
|
||||
color: ${cssManager.bdTheme('hsl(142.1 76.2% 36.3%)', 'hsl(142.1 70.6% 45.3%)')};
|
||||
}
|
||||
|
||||
/* Error state for input */
|
||||
:host([validation-state="invalid"]) input {
|
||||
border-color: ${cssManager.bdTheme('hsl(0 84.2% 60.2%)', 'hsl(0 72.2% 50.6%)')};
|
||||
}
|
||||
|
||||
:host([validation-state="invalid"]) input:focus {
|
||||
border-color: ${cssManager.bdTheme('hsl(0 84.2% 60.2%)', 'hsl(0 72.2% 50.6%)')};
|
||||
box-shadow: 0 0 0 2px ${cssManager.bdTheme('hsl(0 84.2% 60.2% / 0.05)', 'hsl(0 72.2% 50.6% / 0.05)')};
|
||||
}
|
||||
|
||||
/* Warning state for input */
|
||||
:host([validation-state="warn"]) input {
|
||||
border-color: ${cssManager.bdTheme('hsl(25 95% 53%)', 'hsl(25 95% 63%)')};
|
||||
}
|
||||
|
||||
:host([validation-state="warn"]) input:focus {
|
||||
border-color: ${cssManager.bdTheme('hsl(25 95% 53%)', 'hsl(25 95% 63%)')};
|
||||
box-shadow: 0 0 0 2px ${cssManager.bdTheme('hsl(25 95% 53% / 0.05)', 'hsl(25 95% 63% / 0.05)')};
|
||||
}
|
||||
|
||||
/* Valid state for input */
|
||||
:host([validation-state="valid"]) input {
|
||||
border-color: ${cssManager.bdTheme('hsl(142.1 76.2% 36.3%)', 'hsl(142.1 70.6% 45.3%)')};
|
||||
}
|
||||
|
||||
:host([validation-state="valid"]) input:focus {
|
||||
border-color: ${cssManager.bdTheme('hsl(142.1 76.2% 36.3%)', 'hsl(142.1 70.6% 45.3%)')};
|
||||
box-shadow: 0 0 0 2px ${cssManager.bdTheme('hsl(142.1 76.2% 36.3% / 0.05)', 'hsl(142.1 70.6% 45.3% / 0.05)')};
|
||||
}
|
||||
`,
|
||||
];
|
||||
@ -144,42 +197,51 @@ export class DeesInputText extends DeesInputBase {
|
||||
return html`
|
||||
<style>
|
||||
input {
|
||||
font-family: ${this.isPasswordBool ? 'monospace' : 'Geist Sans'};
|
||||
letter-spacing: ${this.isPasswordBool ? '1px' : 'normal'};
|
||||
color: ${this.goBright ? '#333' : '#ccc'};
|
||||
font-family: ${this.isPasswordBool ? cssMonoFontFamily : 'inherit'};
|
||||
letter-spacing: ${this.isPasswordBool ? '0.5px' : 'normal'};
|
||||
padding-right: ${this.isPasswordBool ? '48px' : '12px'};
|
||||
}
|
||||
${this.validationText
|
||||
? css`
|
||||
.validationContainer {
|
||||
height: 22px;
|
||||
height: auto;
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
`
|
||||
: css`
|
||||
.validationContainer {
|
||||
height: 4px;
|
||||
padding: 2px !important;
|
||||
height: 0;
|
||||
padding: 0 !important;
|
||||
opacity: 0;
|
||||
transform: translateY(-4px);
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
<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">
|
||||
<input
|
||||
type="${this.isPasswordBool && !this.showPasswordBool ? 'password' : 'text'}"
|
||||
.value=${this.value}
|
||||
@input="${this.updateValue}"
|
||||
.disabled=${this.disabled}
|
||||
placeholder="${this.label ? '' : 'Enter text...'}"
|
||||
/>
|
||||
<div class="validationContainer">${this.validationText}</div>
|
||||
${this.isPasswordBool
|
||||
? html`
|
||||
<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>
|
||||
`
|
||||
: html``}
|
||||
${this.validationText
|
||||
? html`
|
||||
<div class="validationContainer ${this.validationState || 'error'}">
|
||||
${this.validationText}
|
||||
</div>
|
||||
`
|
||||
: html`<div class="validationContainer"></div>`}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@ -205,7 +267,6 @@ export class DeesInputText extends DeesInputBase {
|
||||
|
||||
public async togglePasswordView() {
|
||||
this.showPasswordBool = !this.showPasswordBool;
|
||||
console.log(`this.showPasswordBool is: ${this.showPasswordBool}`);
|
||||
}
|
||||
|
||||
public async focus() {
|
||||
|
@ -35,12 +35,24 @@ export class DeesInputTypelist extends DeesInputBase<DeesInputTypelist> {
|
||||
}
|
||||
.mainbox {
|
||||
border-radius: 3px;
|
||||
background: #222;
|
||||
background: ${cssManager.bdTheme('#fafafa', '#222222')};
|
||||
overflow: hidden;
|
||||
border-top: ${cssManager.bdTheme('1px solid #CCC', '1px solid #444')};
|
||||
border-bottom: ${cssManager.bdTheme('1px solid #CCC', '1px solid #333')};
|
||||
border-right: ${cssManager.bdTheme('1px solid #CCC', 'none')};
|
||||
border-left: ${cssManager.bdTheme('1px solid #CCC', 'none')};
|
||||
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', '1px solid #ffffff10')};
|
||||
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 {
|
||||
@ -50,14 +62,15 @@ export class DeesInputTypelist extends DeesInputBase<DeesInputTypelist> {
|
||||
|
||||
.notags {
|
||||
text-align: center;
|
||||
opacity: 0.5;
|
||||
font-size: 12px;
|
||||
color: ${cssManager.bdTheme('#999', '#666')};
|
||||
font-size: 13px;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
input {
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
background: #181818;
|
||||
background: ${cssManager.bdTheme('#f5f5f5', '#181818')};
|
||||
width: 100%;
|
||||
outline: none;
|
||||
border: none;
|
||||
@ -67,30 +80,68 @@ export class DeesInputTypelist extends DeesInputBase<DeesInputTypelist> {
|
||||
line-height: 32px;
|
||||
height: 0px;
|
||||
transition: height 0.2s;
|
||||
border-top: 1px solid ${cssManager.bdTheme('#e0e0e0', '#333')};
|
||||
}
|
||||
|
||||
input:focus {
|
||||
height: 32px;
|
||||
background: ${cssManager.bdTheme('#fafafa', '#1a1a1a')};
|
||||
}
|
||||
|
||||
input::placeholder {
|
||||
color: ${cssManager.bdTheme('#999', '#666')};
|
||||
}
|
||||
|
||||
.tag {
|
||||
display: inline-block;
|
||||
background: ${cssManager.bdTheme('#e0e0e0', '#444')};
|
||||
color: ${cssManager.bdTheme('#333', '#fff')};
|
||||
padding: 4px 8px;
|
||||
border-radius: 3px;
|
||||
margin: 2px;
|
||||
font-size: 12px;
|
||||
background: ${cssManager.bdTheme('#e8f5e9', '#2d3a2d')};
|
||||
color: ${cssManager.bdTheme('#2e7d32', '#81c784')};
|
||||
padding: 4px 10px;
|
||||
border-radius: 4px;
|
||||
margin: 3px;
|
||||
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 {
|
||||
margin-left: 6px;
|
||||
margin-left: 8px;
|
||||
cursor: pointer;
|
||||
opacity: 0.6;
|
||||
opacity: 0.7;
|
||||
font-weight: 700;
|
||||
font-size: 16px;
|
||||
line-height: 1;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
.tag .remove:hover {
|
||||
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')};
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
@ -32,20 +32,43 @@ export class DeesLabel extends DeesElement {
|
||||
})
|
||||
public description: string;
|
||||
|
||||
@property({
|
||||
type: Boolean,
|
||||
reflect: true,
|
||||
})
|
||||
public required: boolean = false;
|
||||
|
||||
public static styles = [
|
||||
cssManager.defaultStyles,
|
||||
css`
|
||||
:host {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
|
||||
}
|
||||
|
||||
.label {
|
||||
color: ${cssManager.bdTheme('#333', '#ccc')};
|
||||
display: inline-block;
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 15%)', 'hsl(0 0% 90%)')};
|
||||
font-size: 14px;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 500;
|
||||
line-height: 1.5;
|
||||
margin-bottom: 6px;
|
||||
cursor: default;
|
||||
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 {
|
||||
display: inline-block;
|
||||
font-size: 14px;
|
||||
transform: translateY(1.5px);
|
||||
font-size: 12px;
|
||||
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`
|
||||
<div class="label">
|
||||
${this.label}
|
||||
${this.required ? html`<span class="required">*</span>` : ''}
|
||||
${this.description
|
||||
? html`
|
||||
<dees-icon .iconFA=${'circleInfo'}></dees-icon>
|
||||
<dees-icon .iconName=${'lucideInfo'}></dees-icon>
|
||||
<dees-speechbubble .text=${this.description}></dees-speechbubble>
|
||||
`
|
||||
: html``}
|
||||
|
@ -1,6 +1,7 @@
|
||||
import * as colors from './00colors.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 {
|
||||
@ -42,6 +43,7 @@ export class DeesModal extends DeesElement {
|
||||
showCloseButton?: boolean;
|
||||
showHelpButton?: boolean;
|
||||
onHelp?: () => void | Promise<void>;
|
||||
mobileFullscreen?: boolean;
|
||||
}) {
|
||||
const body = document.body;
|
||||
const modal = new DeesModal();
|
||||
@ -54,6 +56,7 @@ export class DeesModal extends DeesElement {
|
||||
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({
|
||||
blur: true,
|
||||
});
|
||||
@ -101,6 +104,9 @@ export class DeesModal extends DeesElement {
|
||||
@property({ attribute: false })
|
||||
public onHelp: () => void | Promise<void>;
|
||||
|
||||
@property({ type: Boolean })
|
||||
public mobileFullscreen: boolean = false;
|
||||
|
||||
@state()
|
||||
private modalZIndex: number = 1000;
|
||||
|
||||
@ -112,7 +118,7 @@ export class DeesModal extends DeesElement {
|
||||
cssManager.defaultStyles,
|
||||
css`
|
||||
:host {
|
||||
font-family: 'Geist Sans', sans-serif;
|
||||
font-family: ${cssGeistFontFamily};
|
||||
color: ${cssManager.bdTheme('#333', '#fff')};
|
||||
will-change: transform;
|
||||
}
|
||||
@ -132,13 +138,17 @@ export class DeesModal extends DeesElement {
|
||||
transform: translateY(0px) scale(0.95);
|
||||
opacity: 0;
|
||||
min-height: 120px;
|
||||
background: ${cssManager.bdTheme('#ffffff', '#111')};
|
||||
border-radius: 8px;
|
||||
border: 1px solid ${cssManager.bdTheme('#e0e0e0', '#333')};
|
||||
transition: all 0.2s;
|
||||
max-height: calc(100vh - 40px);
|
||||
background: ${cssManager.bdTheme('#ffffff', '#09090b')};
|
||||
border-radius: 6px;
|
||||
border: 1px solid ${cssManager.bdTheme('#e5e7eb', '#27272a')};
|
||||
transition: all 0.2s ease;
|
||||
overflow: hidden;
|
||||
box-shadow: ${cssManager.bdTheme('0px 2px 10px rgba(0, 0, 0, 0.1)', '0px 2px 5px rgba(0, 0, 0, 0.5)')};
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1), 0 1px 2px rgba(0, 0, 0, 0.06);
|
||||
margin: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overscroll-behavior: contain;
|
||||
}
|
||||
|
||||
/* Width variations */
|
||||
@ -165,6 +175,26 @@ export class DeesModal extends DeesElement {
|
||||
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 {
|
||||
@ -179,13 +209,15 @@ export class DeesModal extends DeesElement {
|
||||
|
||||
.modal .heading {
|
||||
height: 40px;
|
||||
font-family: 'Geist Sans', sans-serif;
|
||||
min-height: 40px;
|
||||
font-family: ${cssGeistFontFamily};
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 12px;
|
||||
border-bottom: 1px solid ${cssManager.bdTheme('#e0e0e0', '#333')};
|
||||
border-bottom: 1px solid ${cssManager.bdTheme('#e5e7eb', '#27272a')};
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.modal .heading .header-buttons {
|
||||
@ -201,23 +233,23 @@ export class DeesModal extends DeesElement {
|
||||
.modal .heading .header-button {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 6px;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
transition: all 0.15s ease;
|
||||
background: transparent;
|
||||
color: ${cssManager.bdTheme('#666', '#999')};
|
||||
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
|
||||
}
|
||||
|
||||
.modal .heading .header-button:hover {
|
||||
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.08)', 'rgba(255, 255, 255, 0.08)')};
|
||||
color: ${cssManager.bdTheme('#333', '#fff')};
|
||||
background: ${cssManager.bdTheme('#f4f4f5', '#27272a')};
|
||||
color: ${cssManager.bdTheme('#09090b', '#fafafa')};
|
||||
}
|
||||
|
||||
.modal .heading .header-button:active {
|
||||
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.12)', 'rgba(255, 255, 255, 0.12)')};
|
||||
background: ${cssManager.bdTheme('#e5e7eb', '#3f3f46')};
|
||||
}
|
||||
|
||||
.modal .heading .header-button dees-icon {
|
||||
@ -233,55 +265,65 @@ export class DeesModal extends DeesElement {
|
||||
font-size: 14px;
|
||||
line-height: 40px;
|
||||
padding: 0 40px;
|
||||
color: ${cssManager.bdTheme('#09090b', '#fafafa')};
|
||||
}
|
||||
|
||||
.modal .content {
|
||||
padding: 16px;
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
overscroll-behavior: contain;
|
||||
}
|
||||
.modal .bottomButtons {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
border-top: 1px solid ${cssManager.bdTheme('#e0e0e0', '#333')};
|
||||
border-top: 1px solid ${cssManager.bdTheme('#e5e7eb', '#27272a')};
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
padding: 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.modal .bottomButtons .bottomButton {
|
||||
padding: 8px 16px;
|
||||
border-radius: 6px;
|
||||
border-radius: 4px;
|
||||
line-height: 16px;
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
transition: all 0.2s;
|
||||
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.05)', 'rgba(255, 255, 255, 0.05)')};
|
||||
transition: all 0.15s ease;
|
||||
background: ${cssManager.bdTheme('#ffffff', '#27272a')};
|
||||
border: 1px solid ${cssManager.bdTheme('#e5e7eb', '#3f3f46')};
|
||||
color: ${cssManager.bdTheme('#09090b', '#fafafa')};
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.modal .bottomButtons .bottomButton:hover {
|
||||
background: ${cssManager.bdTheme(colors.bright.blue, colors.dark.blue)};
|
||||
color: #ffffff;
|
||||
background: ${cssManager.bdTheme('#f4f4f5', '#3f3f46')};
|
||||
border-color: ${cssManager.bdTheme('#d1d5db', '#52525b')};
|
||||
}
|
||||
.modal .bottomButtons .bottomButton:active {
|
||||
background: ${cssManager.bdTheme(colors.bright.blueActive, colors.dark.blueActive)};
|
||||
color: #ffffff;
|
||||
background: ${cssManager.bdTheme('#e5e7eb', '#52525b')};
|
||||
}
|
||||
.modal .bottomButtons .bottomButton:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.modal .bottomButtons .bottomButton.primary {
|
||||
background: ${cssManager.bdTheme(colors.bright.blue, colors.dark.blue)};
|
||||
background: ${cssManager.bdTheme('#3b82f6', '#3b82f6')};
|
||||
border-color: ${cssManager.bdTheme('#3b82f6', '#3b82f6')};
|
||||
color: #ffffff;
|
||||
}
|
||||
.modal .bottomButtons .bottomButton.primary:hover {
|
||||
background: ${cssManager.bdTheme(colors.bright.blueActive, colors.dark.blueActive)};
|
||||
background: ${cssManager.bdTheme('#2563eb', '#2563eb')};
|
||||
border-color: ${cssManager.bdTheme('#2563eb', '#2563eb')};
|
||||
}
|
||||
.modal .bottomButtons .bottomButton.primary:active {
|
||||
background: ${cssManager.bdTheme(colors.bright.blueMuted, colors.dark.blueMuted)};
|
||||
background: ${cssManager.bdTheme('#1d4ed8', '#1d4ed8')};
|
||||
border-color: ${cssManager.bdTheme('#1d4ed8', '#1d4ed8')};
|
||||
}
|
||||
`,
|
||||
];
|
||||
@ -291,6 +333,7 @@ export class DeesModal extends DeesElement {
|
||||
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`
|
||||
<style>
|
||||
@ -299,7 +342,7 @@ export class DeesModal extends DeesElement {
|
||||
${minWidthStyle ? `.modal { min-width: ${minWidthStyle}; }` : ''}
|
||||
</style>
|
||||
<div class="modalContainer" @click=${this.handleOutsideClick} style="z-index: ${this.modalZIndex}">
|
||||
<div class="modal ${widthClass}">
|
||||
<div class="modal ${widthClass} ${mobileFullscreenClass}">
|
||||
<div class="heading">
|
||||
<div class="heading-text">${this.heading}</div>
|
||||
<div class="header-buttons">
|
||||
|
@ -5,7 +5,7 @@ export const demoFunc = () => html`
|
||||
${css`
|
||||
.demo-background {
|
||||
padding: 24px;
|
||||
background: ${cssManager.bdTheme('#f0f0f0', '#0a0a0a')};
|
||||
background: ${cssManager.bdTheme('hsl(0 0% 95%)', 'hsl(0 0% 5%)')};
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
@ -17,65 +17,156 @@ export const demoFunc = () => html`
|
||||
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 {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
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) {
|
||||
.grid-layout {
|
||||
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>
|
||||
|
||||
<div class="demo-background">
|
||||
<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>It's perfect for creating sections in your application with proper spacing and borders.</p>
|
||||
</dees-panel>
|
||||
|
||||
<dees-panel .title=${'Panel with Subtitle'} .subtitle=${'Additional context information'}>
|
||||
<p>Panels can have both a title and subtitle to provide more context.</p>
|
||||
<p>The subtitle appears in a smaller, muted text below the title.</p>
|
||||
</dees-panel>
|
||||
|
||||
<div class="grid-layout">
|
||||
<dees-panel .title=${'Feature 1'}>
|
||||
<dees-panel .title=${'Feature Overview'} .subtitle=${'Key capabilities'}>
|
||||
<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 .title=${'Feature 2'}>
|
||||
<p>Each panel maintains consistent spacing and styling.</p>
|
||||
<dees-button>Another Action</dees-button>
|
||||
<dees-panel .title=${'Quick Actions'} .subtitle=${'Common tasks'}>
|
||||
<p>Each panel maintains consistent spacing and styling across your application.</p>
|
||||
<dees-button>Get Started</dees-button>
|
||||
</dees-panel>
|
||||
</div>
|
||||
|
||||
<dees-panel .title=${'Complex Content'}>
|
||||
<h4>Nested Elements</h4>
|
||||
<p>Panels can contain any type of content:</p>
|
||||
<ul>
|
||||
<li>Text and paragraphs</li>
|
||||
<li>Lists and tables</li>
|
||||
<li>Form inputs</li>
|
||||
<li>Other components</li>
|
||||
</ul>
|
||||
<h2 class="section-title">Panel Variants</h2>
|
||||
|
||||
<dees-input-text .label=${'Example Input'} .description=${'Input inside a panel'}></dees-input-text>
|
||||
|
||||
<div style="margin-top: 16px;">
|
||||
<dees-button>Submit</dees-button>
|
||||
</div>
|
||||
<dees-panel .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>
|
||||
<p>Use <code>variant="default"</code> or omit the variant property.</p>
|
||||
</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>
|
||||
<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>
|
||||
|
||||
<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>
|
||||
`;
|
@ -8,6 +8,7 @@ import {
|
||||
type TemplateResult,
|
||||
} from '@design.estate/dees-element';
|
||||
import { demoFunc } from './dees-panel.demo.js';
|
||||
import { cssGeistFontFamily } from './00fonts.js';
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
@ -25,33 +26,113 @@ export class DeesPanel extends DeesElement {
|
||||
@property({ type: 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 = [
|
||||
cssManager.defaultStyles,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
background: ${cssManager.bdTheme('#ffffff', '#1a1a1a')};
|
||||
border-radius: 8px;
|
||||
font-family: ${cssGeistFontFamily};
|
||||
background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(0 0% 3.9%)')};
|
||||
border-radius: 6px;
|
||||
padding: 24px;
|
||||
box-shadow: 0 2px 4px ${cssManager.bdTheme('rgba(0,0,0,0.1)', 'rgba(0,0,0,0.3)')};
|
||||
border: 1px solid ${cssManager.bdTheme('rgba(0,0,0,0.1)', 'rgba(255,255,255,0.1)')};
|
||||
border: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
|
||||
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 {
|
||||
margin: 0 0 16px 0;
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
color: ${cssManager.bdTheme('#0069f2', '#0099ff')};
|
||||
font-weight: 600;
|
||||
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 {
|
||||
margin: -12px 0 16px 0;
|
||||
margin: 4px 0 0 0;
|
||||
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 {
|
||||
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 */
|
||||
@ -62,16 +143,57 @@ export class DeesPanel extends DeesElement {
|
||||
.content ::slotted(*:last-child) {
|
||||
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 {
|
||||
return html`
|
||||
<div class="header">
|
||||
${this.title ? html`<h3 class="title">${this.title}</h3>` : ''}
|
||||
${this.subtitle ? html`<p class="subtitle">${this.subtitle}</p>` : ''}
|
||||
</div>
|
||||
<div class="content">
|
||||
<slot></slot>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
public async firstUpdated() {
|
||||
if (this.runAfterRender) {
|
||||
await this.runAfterRender(this);
|
||||
}
|
||||
}
|
||||
}
|
332
ts_web/elements/dees-shopping-productcard.demo.ts
Normal file
332
ts_web/elements/dees-shopping-productcard.demo.ts
Normal 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>
|
||||
`;
|
335
ts_web/elements/dees-shopping-productcard.ts
Normal file
335
ts_web/elements/dees-shopping-productcard.ts
Normal 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
|
||||
}));
|
||||
}
|
||||
}
|
@ -1,34 +1,99 @@
|
||||
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';
|
||||
|
||||
export const demoFunc = () => {
|
||||
// Demo data with different tile types
|
||||
const demoTiles: IStatsTile[] = [
|
||||
return html`
|
||||
<dees-demowrapper>
|
||||
<style>
|
||||
${css`
|
||||
.demo-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
padding: 24px;
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
dees-panel {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
dees-panel:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.tile-config {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 16px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.config-section {
|
||||
padding: 16px;
|
||||
background: ${cssManager.bdTheme('hsl(210 40% 96.1%)', 'hsl(215 20.2% 16.8%)')};
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.config-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 8px;
|
||||
color: ${cssManager.bdTheme('hsl(215.3 25% 8.8%)', 'hsl(210 40% 98%)')};
|
||||
}
|
||||
|
||||
.config-description {
|
||||
font-size: 13px;
|
||||
color: ${cssManager.bdTheme('hsl(215.4 16.3% 56.9%)', 'hsl(215 20.2% 55.1%)')};
|
||||
}
|
||||
|
||||
.code-block {
|
||||
background: ${cssManager.bdTheme('hsl(210 40% 96.1%)', 'hsl(215 20.2% 11.8%)')};
|
||||
border: 1px solid ${cssManager.bdTheme('hsl(214.3 31.8% 91.4%)', 'hsl(215 20.2% 16.8%)')};
|
||||
border-radius: 6px;
|
||||
padding: 16px;
|
||||
font-family: monospace;
|
||||
font-size: 13px;
|
||||
overflow-x: auto;
|
||||
white-space: pre;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
|
||||
<div class="demo-container">
|
||||
<dees-panel .title=${'1. Comprehensive Dashboard'} .subtitle=${'Full-featured stats grid with various tile types, actions, and Lucide icons'}>
|
||||
<dees-statsgrid
|
||||
.tiles=${[
|
||||
{
|
||||
id: 'revenue',
|
||||
title: 'Total Revenue',
|
||||
value: 125420,
|
||||
unit: '$',
|
||||
type: 'number',
|
||||
icon: 'faDollarSign',
|
||||
icon: 'lucide:dollar-sign',
|
||||
description: '+12.5% from last month',
|
||||
color: '#22c55e',
|
||||
actions: [
|
||||
{
|
||||
name: 'View Details',
|
||||
iconName: 'faChartLine',
|
||||
iconName: 'lucide:trending-up',
|
||||
action: async () => {
|
||||
console.log('Viewing revenue details for tile:', 'revenue');
|
||||
console.log('Current value:', 125420);
|
||||
alert(`Revenue Details: $125,420 (+12.5%)`);
|
||||
const output = document.querySelector('#action-output');
|
||||
if (output) {
|
||||
output.textContent = 'Viewing revenue details: $125,420 (+12.5%)';
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Export Data',
|
||||
iconName: 'faFileExport',
|
||||
iconName: 'lucide:download',
|
||||
action: async () => {
|
||||
console.log('Exporting revenue data');
|
||||
alert('Revenue data exported to CSV');
|
||||
const output = document.querySelector('#action-output');
|
||||
if (output) {
|
||||
output.textContent = 'Exporting revenue data to CSV...';
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
@ -38,14 +103,17 @@ export const demoFunc = () => {
|
||||
title: 'Active Users',
|
||||
value: 3847,
|
||||
type: 'number',
|
||||
icon: 'faUsers',
|
||||
icon: 'lucide:users',
|
||||
description: '324 new this week',
|
||||
actions: [
|
||||
{
|
||||
name: 'View User List',
|
||||
iconName: 'faList',
|
||||
iconName: 'lucide:list',
|
||||
action: async () => {
|
||||
console.log('Viewing user list');
|
||||
const output = document.querySelector('#action-output');
|
||||
if (output) {
|
||||
output.textContent = 'Opening user list...';
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
@ -54,15 +122,16 @@ export const demoFunc = () => {
|
||||
id: 'cpu',
|
||||
title: 'CPU Usage',
|
||||
value: 73,
|
||||
unit: '%',
|
||||
type: 'gauge',
|
||||
icon: 'faMicrochip',
|
||||
icon: 'lucide:cpu',
|
||||
gaugeOptions: {
|
||||
min: 0,
|
||||
max: 100,
|
||||
thresholds: [
|
||||
{ value: 0, color: '#22c55e' },
|
||||
{ value: 60, color: '#f59e0b' },
|
||||
{ value: 80, color: '#ef4444' }
|
||||
{ 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%)' }
|
||||
]
|
||||
}
|
||||
},
|
||||
@ -71,43 +140,8 @@ export const demoFunc = () => {
|
||||
title: 'Storage Used',
|
||||
value: 65,
|
||||
type: 'percentage',
|
||||
icon: 'faHardDrive',
|
||||
icon: 'lucide:hard-drive',
|
||||
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',
|
||||
@ -115,275 +149,370 @@ export const demoFunc = () => {
|
||||
value: 142,
|
||||
unit: 'ms',
|
||||
type: 'trend',
|
||||
icon: 'faClock',
|
||||
icon: 'lucide:activity',
|
||||
trendData: [150, 145, 148, 142, 138, 140, 135, 145, 142],
|
||||
description: 'P95 latency'
|
||||
description: 'P95'
|
||||
},
|
||||
{
|
||||
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');
|
||||
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'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
// Grid actions for the demo
|
||||
const gridActions = [
|
||||
]}
|
||||
.gridActions=${[
|
||||
{
|
||||
name: 'Refresh',
|
||||
iconName: 'faSync',
|
||||
iconName: 'lucide:refresh-cw',
|
||||
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);
|
||||
}, 300);
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Export Report',
|
||||
iconName: 'faFileExport',
|
||||
name: 'Export',
|
||||
iconName: 'lucide:share',
|
||||
action: async () => {
|
||||
console.log('Exporting stats report...');
|
||||
const output = document.querySelector('#action-output');
|
||||
if (output) {
|
||||
output.textContent = 'Exporting dashboard report...';
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Settings',
|
||||
iconName: 'faCog',
|
||||
iconName: 'lucide:settings',
|
||||
action: async () => {
|
||||
console.log('Opening settings...');
|
||||
const output = document.querySelector('#action-output');
|
||||
if (output) {
|
||||
output.textContent = 'Opening dashboard settings...';
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
return html`
|
||||
<style>
|
||||
.demo-container {
|
||||
padding: 32px;
|
||||
background: ${cssManager.bdTheme('#f8f9fa', '#0a0a0a')};
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.demo-section {
|
||||
margin-bottom: 48px;
|
||||
}
|
||||
|
||||
.demo-title {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 16px;
|
||||
color: ${cssManager.bdTheme('#333', '#fff')};
|
||||
}
|
||||
|
||||
.demo-description {
|
||||
font-size: 14px;
|
||||
color: ${cssManager.bdTheme('#666', '#aaa')};
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.theme-toggle {
|
||||
position: fixed;
|
||||
top: 16px;
|
||||
right: 16px;
|
||||
padding: 8px 16px;
|
||||
background: ${cssManager.bdTheme('#fff', '#1a1a1a')};
|
||||
border: 1px solid ${cssManager.bdTheme('#e0e0e0', '#2a2a2a')};
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
z-index: 100;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="demo-container">
|
||||
<button class="theme-toggle" @click=${() => {
|
||||
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 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>
|
||||
|
||||
<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-panel .title=${'2. Tile Types'} .subtitle=${'Different visualization types available in the stats grid'}>
|
||||
<dees-statsgrid
|
||||
.tiles=${[
|
||||
{
|
||||
id: 'metric1',
|
||||
title: 'Total Sales',
|
||||
value: 48293,
|
||||
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,
|
||||
id: 'number-example',
|
||||
title: 'Number Tile',
|
||||
value: 42195,
|
||||
unit: '$',
|
||||
type: 'number',
|
||||
icon: 'faReceipt'
|
||||
icon: 'lucide:hash',
|
||||
description: 'Simple numeric display'
|
||||
},
|
||||
{
|
||||
id: 'metric4',
|
||||
title: 'Customer Satisfaction',
|
||||
value: 92,
|
||||
type: 'percentage',
|
||||
icon: 'faSmile',
|
||||
color: '#22c55e'
|
||||
}
|
||||
]}
|
||||
.minTileWidth=${220}
|
||||
.gap=${16}
|
||||
></dees-statsgrid>
|
||||
</div>
|
||||
|
||||
<div class="demo-section">
|
||||
<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',
|
||||
title: 'Database Load',
|
||||
value: 42,
|
||||
id: 'gauge-example',
|
||||
title: 'Gauge Tile',
|
||||
value: 68,
|
||||
unit: '%',
|
||||
type: 'gauge',
|
||||
icon: 'faDatabase',
|
||||
icon: 'lucide:gauge',
|
||||
gaugeOptions: {
|
||||
min: 0,
|
||||
max: 100,
|
||||
thresholds: [
|
||||
{ value: 0, color: '#10b981' },
|
||||
{ value: 50, color: '#f59e0b' },
|
||||
{ value: 75, color: '#ef4444' }
|
||||
{ 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: 'perf2',
|
||||
title: 'Network I/O',
|
||||
value: 856,
|
||||
unit: 'MB/s',
|
||||
type: 'trend',
|
||||
icon: 'faNetworkWired',
|
||||
trendData: [720, 780, 823, 845, 812, 876, 856]
|
||||
},
|
||||
{
|
||||
id: 'perf3',
|
||||
title: 'Cache Hit Rate',
|
||||
value: 94.2,
|
||||
id: 'percentage-example',
|
||||
title: 'Percentage Tile',
|
||||
value: 78,
|
||||
type: 'percentage',
|
||||
icon: 'faBolt',
|
||||
color: '#3b82f6'
|
||||
icon: 'lucide:percent',
|
||||
description: 'Progress bar visualization'
|
||||
},
|
||||
{
|
||||
id: 'perf4',
|
||||
title: 'Active Connections',
|
||||
value: 1428,
|
||||
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}
|
||||
.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: 'faLink',
|
||||
description: 'Peak: 2,100'
|
||||
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}
|
||||
></dees-statsgrid>
|
||||
</dees-panel>
|
||||
|
||||
<dees-panel .title=${'4. Interactive Features'} .subtitle=${'Tiles with actions and real-time updates'}>
|
||||
<dees-statsgrid
|
||||
id="interactive-grid"
|
||||
.tiles=${[
|
||||
{
|
||||
id: 'live-cpu',
|
||||
title: 'Live CPU',
|
||||
value: 45,
|
||||
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%)' }
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
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];
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]}
|
||||
.gridActions=${[
|
||||
{
|
||||
name: 'Auto Refresh',
|
||||
iconName: 'faPlay',
|
||||
action: async () => {
|
||||
console.log('Starting auto refresh...');
|
||||
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=${280}
|
||||
.gap=${20}
|
||||
.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>
|
||||
|
||||
<script>
|
||||
// Simulate real-time updates
|
||||
setInterval(() => {
|
||||
const grids = document.querySelectorAll('dees-statsgrid');
|
||||
grids.forEach(grid => {
|
||||
if (grid.tiles && grid.tiles.length > 0) {
|
||||
// Update some random values
|
||||
const updatedTiles = [...grid.tiles];
|
||||
|
||||
// Update trends with new data point
|
||||
updatedTiles.forEach(tile => {
|
||||
if (tile.type === 'trend' && tile.trendData) {
|
||||
tile.trendData = [...tile.trendData.slice(1),
|
||||
tile.trendData[tile.trendData.length - 1] + Math.random() * 10 - 5
|
||||
];
|
||||
}
|
||||
|
||||
// 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)));
|
||||
// Cleanup live updates on page unload
|
||||
window.addEventListener('beforeunload', () => {
|
||||
if ((window as any).liveUpdateInterval) {
|
||||
clearInterval((window as any).liveUpdateInterval);
|
||||
}
|
||||
});
|
||||
|
||||
grid.tiles = updatedTiles;
|
||||
}
|
||||
});
|
||||
}, 3000);
|
||||
</script>
|
||||
</div>
|
||||
</dees-demowrapper>
|
||||
`;
|
||||
};
|
@ -1,5 +1,6 @@
|
||||
import { demoFunc } from './dees-statsgrid.demo.js';
|
||||
import * as plugins from './00plugins.js';
|
||||
import { cssGeistFontFamily } from './00fonts.js';
|
||||
import {
|
||||
customElement,
|
||||
html,
|
||||
@ -79,30 +80,47 @@ export class DeesStatsGrid extends DeesElement {
|
||||
:host {
|
||||
display: block;
|
||||
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 {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: ${unsafeCSS(16)}px;
|
||||
min-height: 32px;
|
||||
margin-bottom: calc(var(--grid-gap) * 1.5);
|
||||
min-height: 40px;
|
||||
}
|
||||
|
||||
.grid-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: ${cssManager.bdTheme('#333', '#fff')};
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: ${cssManager.bdTheme('#09090b', '#fafafa')};
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
.grid-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.grid-actions dees-button {
|
||||
font-size: 14px;
|
||||
min-width: auto;
|
||||
font-size: var(--label-font-size);
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
@ -112,86 +130,107 @@ export class DeesStatsGrid extends DeesElement {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Tile Base Styles */
|
||||
.stats-tile {
|
||||
background: ${cssManager.bdTheme('#fff', '#1a1a1a')};
|
||||
border: 1px solid ${cssManager.bdTheme('#e0e0e0', '#2a2a2a')};
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
transition: all 0.3s ease;
|
||||
cursor: pointer;
|
||||
background: ${cssManager.bdTheme('#ffffff', '#09090b')};
|
||||
border: 1px solid ${cssManager.bdTheme('hsl(214.3 31.8% 91.4%)', 'hsl(215 20.2% 11.8%)')};
|
||||
border-radius: var(--border-radius);
|
||||
padding: var(--tile-padding);
|
||||
transition: all var(--transition-duration) ease;
|
||||
cursor: default;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.stats-tile:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px ${cssManager.bdTheme('rgba(0,0,0,0.1)', 'rgba(0,0,0,0.3)')};
|
||||
border-color: ${cssManager.bdTheme('#d0d0d0', '#3a3a3a')};
|
||||
background: ${cssManager.bdTheme('hsl(210 40% 98%)', 'hsl(215 20.2% 10.2%)')};
|
||||
border-color: ${cssManager.bdTheme('hsl(214.3 31.8% 85%)', 'hsl(215 20.2% 16.8%)')};
|
||||
}
|
||||
|
||||
.stats-tile.clickable {
|
||||
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 {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
width: 100%;
|
||||
align-items: flex-start;
|
||||
margin-bottom: var(--header-spacing);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.tile-title {
|
||||
font-size: 14px;
|
||||
font-size: var(--title-font-size);
|
||||
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;
|
||||
letter-spacing: -0.01em;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.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 {
|
||||
height: 90px;
|
||||
min-height: var(--content-min-height);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.tile-value {
|
||||
font-size: 32px;
|
||||
font-size: var(--value-font-size);
|
||||
font-weight: 600;
|
||||
color: ${cssManager.bdTheme('#333', '#fff')};
|
||||
line-height: 1.2;
|
||||
color: ${cssManager.bdTheme('hsl(215.3 25% 8.8%)', 'hsl(210 40% 98%)')};
|
||||
line-height: 1.1;
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
width: 100%;
|
||||
gap: 4px;
|
||||
letter-spacing: -0.025em;
|
||||
}
|
||||
|
||||
.tile-unit {
|
||||
font-size: 18px;
|
||||
font-size: var(--unit-font-size);
|
||||
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 {
|
||||
font-size: 12px;
|
||||
color: ${cssManager.bdTheme('#888', '#777')};
|
||||
margin-top: 8px;
|
||||
font-size: var(--label-font-size);
|
||||
color: ${cssManager.bdTheme('hsl(215.4 16.3% 56.9%)', 'hsl(215 20.2% 55.1%)')};
|
||||
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 {
|
||||
width: 100%;
|
||||
width: 140px;
|
||||
height: 80px;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-top: -10px;
|
||||
}
|
||||
|
||||
.gauge-svg {
|
||||
@ -201,96 +240,134 @@ export class DeesStatsGrid extends DeesElement {
|
||||
|
||||
.gauge-background {
|
||||
fill: none;
|
||||
stroke: ${cssManager.bdTheme('#e0e0e0', '#2a2a2a')};
|
||||
stroke-width: 6;
|
||||
stroke: ${cssManager.bdTheme('hsl(214.3 31.8% 91.4%)', 'hsl(215 20.2% 21.8%)')};
|
||||
stroke-width: 8;
|
||||
}
|
||||
|
||||
.gauge-fill {
|
||||
fill: none;
|
||||
stroke-width: 6;
|
||||
stroke-width: 8;
|
||||
stroke-linecap: round;
|
||||
transition: stroke-dashoffset 0.5s ease;
|
||||
transition: stroke-dashoffset 0.6s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.gauge-text {
|
||||
fill: ${cssManager.bdTheme('#333', '#fff')};
|
||||
font-size: 18px;
|
||||
fill: ${cssManager.bdTheme('hsl(215.3 25% 8.8%)', 'hsl(210 40% 98%)')};
|
||||
font-family: ${cssGeistFontFamily};
|
||||
font-size: var(--value-font-size);
|
||||
font-weight: 600;
|
||||
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%;
|
||||
height: 24px;
|
||||
background: ${cssManager.bdTheme('#f0f0f0', '#2a2a2a')};
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
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 {
|
||||
height: 100%;
|
||||
background: ${cssManager.bdTheme('#0084ff', '#0066cc')};
|
||||
transition: width 0.5s ease;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.percentage-text {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: ${cssManager.bdTheme('#333', '#fff')};
|
||||
background: ${cssManager.bdTheme('hsl(215.3 25% 8.8%)', 'hsl(210 40% 98%)')};
|
||||
transition: width 0.6s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* Trend Styles */
|
||||
.trend-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.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 {
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
flex-shrink: 0;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.trend-line {
|
||||
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-linejoin: round;
|
||||
stroke-linecap: round;
|
||||
}
|
||||
|
||||
.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 {
|
||||
font-size: 32px;
|
||||
font-size: var(--value-font-size);
|
||||
font-weight: 600;
|
||||
color: ${cssManager.bdTheme('#333', '#fff')};
|
||||
}
|
||||
|
||||
.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;
|
||||
color: ${cssManager.bdTheme('hsl(215.3 25% 8.8%)', 'hsl(210 40% 98%)')};
|
||||
line-height: 1.1;
|
||||
letter-spacing: -0.025em;
|
||||
}
|
||||
|
||||
/* Context Menu */
|
||||
dees-contextmenu {
|
||||
position: fixed;
|
||||
z-index: 1000;
|
||||
@ -306,10 +383,14 @@ export class DeesStatsGrid extends DeesElement {
|
||||
return html`
|
||||
${this.gridActions.length > 0 ? html`
|
||||
<div class="grid-header">
|
||||
<div class="grid-title">Statistics</div>
|
||||
<div class="grid-title"></div>
|
||||
<div class="grid-actions">
|
||||
${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.name}
|
||||
</dees-button>
|
||||
@ -326,7 +407,7 @@ export class DeesStatsGrid extends DeesElement {
|
||||
<dees-contextmenu
|
||||
.x=${this.contextMenuPosition.x}
|
||||
.y=${this.contextMenuPosition.y}
|
||||
.menuItems=${this.contextMenuActions}
|
||||
.menuItems=${this.contextMenuActions as any}
|
||||
@clicked=${() => this.contextMenuVisible = false}
|
||||
></dees-contextmenu>
|
||||
` : ''}
|
||||
@ -354,7 +435,7 @@ export class DeesStatsGrid extends DeesElement {
|
||||
${this.renderTileContent(tile)}
|
||||
</div>
|
||||
|
||||
${tile.description ? html`
|
||||
${tile.description && tile.type !== 'trend' ? html`
|
||||
<div class="tile-description">${tile.description}</div>
|
||||
` : ''}
|
||||
</div>
|
||||
@ -396,12 +477,31 @@ export class DeesStatsGrid extends DeesElement {
|
||||
const value = typeof tile.value === 'number' ? tile.value : parseFloat(tile.value);
|
||||
const options = tile.gaugeOptions || { min: 0, max: 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) {
|
||||
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) {
|
||||
strokeColor = threshold.color;
|
||||
break;
|
||||
@ -410,30 +510,29 @@ export class DeesStatsGrid extends DeesElement {
|
||||
}
|
||||
|
||||
return html`
|
||||
<div class="gauge-wrapper">
|
||||
<div class="gauge-container">
|
||||
<svg class="gauge-svg" viewBox="0 0 80 80">
|
||||
<circle
|
||||
<svg class="gauge-svg" viewBox="0 0 ${width} ${height}" preserveAspectRatio="xMidYMid meet">
|
||||
<!-- Background arc -->
|
||||
<path
|
||||
class="gauge-background"
|
||||
cx="40"
|
||||
cy="40"
|
||||
r="30"
|
||||
transform="rotate(-90 40 40)"
|
||||
d="${arcPath}"
|
||||
/>
|
||||
<circle
|
||||
<!-- Filled arc -->
|
||||
<path
|
||||
class="gauge-fill"
|
||||
cx="40"
|
||||
cy="40"
|
||||
r="30"
|
||||
transform="rotate(-90 40 40)"
|
||||
d="${arcPath}"
|
||||
stroke="${strokeColor}"
|
||||
stroke-dasharray="${strokeDasharray}"
|
||||
stroke-dasharray="${circumference}"
|
||||
stroke-dashoffset="${strokeDashoffset}"
|
||||
/>
|
||||
<text class="gauge-text" x="40" y="40" dy="0.35em">
|
||||
${value}${tile.unit || ''}
|
||||
<!-- Value text -->
|
||||
<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>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
@ -442,12 +541,14 @@ export class DeesStatsGrid extends DeesElement {
|
||||
const percentage = Math.min(100, Math.max(0, value));
|
||||
|
||||
return html`
|
||||
<div class="percentage-container">
|
||||
<div class="percentage-wrapper">
|
||||
<div class="percentage-value">${percentage}%</div>
|
||||
<div class="percentage-bar">
|
||||
<div
|
||||
class="percentage-fill"
|
||||
style="width: ${percentage}%; ${tile.color ? `background: ${tile.color}` : ''}"
|
||||
></div>
|
||||
<div class="percentage-text">${percentage}%</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@ -461,11 +562,14 @@ export class DeesStatsGrid extends DeesElement {
|
||||
const max = Math.max(...data);
|
||||
const min = Math.min(...data);
|
||||
const range = max - min || 1;
|
||||
const width = 200;
|
||||
const height = 40;
|
||||
const width = 300;
|
||||
const height = 32;
|
||||
|
||||
// Add padding to prevent clipping
|
||||
const padding = 2;
|
||||
const points = data.map((value, index) => {
|
||||
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}`;
|
||||
}).join(' ');
|
||||
|
||||
@ -473,13 +577,16 @@ export class DeesStatsGrid extends DeesElement {
|
||||
|
||||
return html`
|
||||
<div class="trend-container">
|
||||
<div class="trend-header">
|
||||
<span class="trend-value">${tile.value}</span>
|
||||
${tile.unit ? html`<span class="trend-unit">${tile.unit}</span>` : ''}
|
||||
${tile.description ? html`<span class="trend-label">${tile.description}</span>` : ''}
|
||||
</div>
|
||||
<div class="trend-graph">
|
||||
<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 class="trend-value">
|
||||
<span>${tile.value}</span>
|
||||
${tile.unit ? html`<span class="tile-unit">${tile.unit}</span>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { type ITableAction } from './dees-table.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 {
|
||||
date: string;
|
||||
@ -10,16 +10,52 @@ interface ITableDemoData {
|
||||
|
||||
export const demoFunc = () => html`
|
||||
<style>
|
||||
${css`
|
||||
.demoWrapper {
|
||||
box-sizing: border-box;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 20px;
|
||||
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>
|
||||
<div class="demoWrapper">
|
||||
<dees-button class="theme-toggle" @click=${() => {
|
||||
document.body.classList.toggle('bright');
|
||||
document.body.classList.toggle('dark');
|
||||
}}>Toggle Theme</dees-button>
|
||||
|
||||
<div class="demo-container">
|
||||
<div class="demo-section">
|
||||
<h2 class="demo-title">Basic Table with Actions</h2>
|
||||
<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"
|
||||
heading2="Bunq - Payment Account 2 - April 2021"
|
||||
@ -116,14 +152,281 @@ export const demoFunc = () => html`
|
||||
return null;
|
||||
},
|
||||
}
|
||||
] as (ITableAction<ITableDemoData>)[] as any}"
|
||||
.displayFunction=${(itemArg) => {
|
||||
return {
|
||||
...itemArg,
|
||||
onlyDisplayProp: 'onlyDisplay',
|
||||
};
|
||||
}}
|
||||
>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>
|
||||
`;
|
@ -1,6 +1,6 @@
|
||||
import * as colors from './00colors.js';
|
||||
import * as plugins from './00plugins.js';
|
||||
import { demoFunc } from './dees-table.demo.js';
|
||||
import { cssGeistFontFamily } from './00fonts.js';
|
||||
import {
|
||||
customElement,
|
||||
html,
|
||||
@ -9,9 +9,6 @@ import {
|
||||
type TemplateResult,
|
||||
cssManager,
|
||||
css,
|
||||
unsafeCSS,
|
||||
type CSSResult,
|
||||
state,
|
||||
directives,
|
||||
} from '@design.estate/dees-element';
|
||||
|
||||
@ -113,7 +110,7 @@ export class DeesTable<T> extends DeesElement {
|
||||
get value() {
|
||||
return this.data;
|
||||
}
|
||||
set value(valueArg) {}
|
||||
set value(_valueArg) {}
|
||||
public changeSubject = new domtools.plugins.smartrx.rxjs.Subject<DeesTable<T>>();
|
||||
// end dees-form compatibility -----------------------------------------
|
||||
|
||||
@ -157,6 +154,27 @@ export class DeesTable<T> extends DeesElement {
|
||||
})
|
||||
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 fileWeakMap = new WeakMap();
|
||||
|
||||
@ -169,238 +187,358 @@ export class DeesTable<T> extends DeesElement {
|
||||
public static styles = [
|
||||
cssManager.defaultStyles,
|
||||
css`
|
||||
.mainbox {
|
||||
color: ${cssManager.bdTheme('#333', '#fff')};
|
||||
font-family: 'Geist Sans', sans-serif;
|
||||
font-weight: 400;
|
||||
font-size: 14px;
|
||||
padding: 16px;
|
||||
:host {
|
||||
display: block;
|
||||
width: 100%;
|
||||
min-height: 50px;
|
||||
background: ${cssManager.bdTheme('#ffffff', '#222222')};
|
||||
border-radius: 3px;
|
||||
border-top: 1px solid ${cssManager.bdTheme('#fff', '#ffffff10')};
|
||||
box-shadow: 0px 1px 4px rgba(0, 0, 0, 0.3);
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.mainbox {
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 3.9%)', 'hsl(0 0% 98%)')};
|
||||
font-family: ${cssGeistFontFamily};
|
||||
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;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
justify-content: space-between;
|
||||
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 {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.heading {
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.heading1 {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 9%)', 'hsl(0 0% 95%)')};
|
||||
letter-spacing: -0.025em;
|
||||
}
|
||||
|
||||
.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 {
|
||||
margin-top: 7px;
|
||||
border-bottom: 1px solid ${cssManager.bdTheme('#bcbcbc', '#444444')};
|
||||
display: none;
|
||||
}
|
||||
|
||||
.headerActions {
|
||||
user-select: none;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin-left: auto;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.headerAction {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
color: ${cssManager.bdTheme('#333', '#ccc')};
|
||||
margin-left: 16px;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
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 {
|
||||
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 {
|
||||
margin-right: 8px;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
|
||||
.searchGrid {
|
||||
background: ${cssManager.bdTheme('#fff', '#111111')};
|
||||
display: grid;
|
||||
grid-gap: 16px;
|
||||
grid-template-columns: 1fr 200px;
|
||||
margin-top: 16px;
|
||||
padding: 0px 16px;
|
||||
border-top: 1px solid ${cssManager.bdTheme('#fff', '#ffffff20')};
|
||||
border-radius: 8px;
|
||||
padding: 16px 24px;
|
||||
background: ${cssManager.bdTheme('hsl(210 40% 98%)', 'hsl(0 0% 3.9%)')};
|
||||
border-bottom: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.searchGrid.hidden {
|
||||
height: 0px;
|
||||
opacity: 0;
|
||||
overflow: hidden;
|
||||
margin-top: 0px;
|
||||
padding: 0px 24px;
|
||||
border-bottom-width: 0px;
|
||||
}
|
||||
|
||||
table,
|
||||
.noDataSet {
|
||||
margin-top: 16px;
|
||||
color: ${cssManager.bdTheme('#333', '#fff')};
|
||||
border-collapse: collapse;
|
||||
table {
|
||||
width: 100%;
|
||||
}
|
||||
.noDataSet {
|
||||
text-align: center;
|
||||
}
|
||||
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')};
|
||||
caption-side: bottom;
|
||||
font-size: 14px;
|
||||
border-collapse: separate;
|
||||
border-spacing: 0;
|
||||
}
|
||||
|
||||
tr.hasAttachment td {
|
||||
background: ${cssManager.bdTheme('#0098847c', '#0098847c')};
|
||||
.noDataSet {
|
||||
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 {
|
||||
text-transform: none;
|
||||
font-family: 'Geist Sans', sans-serif;
|
||||
height: 48px;
|
||||
padding: 12px 24px;
|
||||
text-align: left;
|
||||
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;
|
||||
border-right: 1px dashed ${cssManager.bdTheme('#999', '#808080')};
|
||||
:host([show-vertical-lines]) th {
|
||||
border-right: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
|
||||
}
|
||||
.innerCellContainer {
|
||||
min-height: 36px;
|
||||
position: relative;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
padding: 6px 8px;
|
||||
line-height: 24px;
|
||||
|
||||
td {
|
||||
padding: 12px 24px;
|
||||
vertical-align: middle;
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 3.9%)', 'hsl(0 0% 98%)')};
|
||||
}
|
||||
th:first-child .innerCellContainer,
|
||||
td:first-child .innerCellContainer {
|
||||
padding-left: 0px;
|
||||
|
||||
:host([show-vertical-lines]) td {
|
||||
border-right: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
|
||||
}
|
||||
th:last-child .innerCellContainer,
|
||||
td:last-child .innerCellContainer {
|
||||
padding-right: 0px;
|
||||
|
||||
th:first-child,
|
||||
td:first-child {
|
||||
padding-left: 24px;
|
||||
}
|
||||
|
||||
th: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;
|
||||
}
|
||||
|
||||
.innerCellContainer {
|
||||
position: relative;
|
||||
min-height: 24px;
|
||||
line-height: 24px;
|
||||
}
|
||||
td input {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
outline: none;
|
||||
border: 2px solid #fa6101;
|
||||
top: 0px;
|
||||
bottom: 0px;
|
||||
right: 0px;
|
||||
left: 0px;
|
||||
position: absolute;
|
||||
background: #fa610140;
|
||||
color: ${cssManager.bdTheme('#333', '#fff')};
|
||||
top: 4px;
|
||||
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-size: 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 {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
height: 24px;
|
||||
transform: translateY(-4px);
|
||||
margin-left: -6px;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.action {
|
||||
position: relative;
|
||||
padding: 8px 10px;
|
||||
line-height: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
size: 16px;
|
||||
border-radius: 8px;
|
||||
border-radius: 6px;
|
||||
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 {
|
||||
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 {
|
||||
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 {
|
||||
filter: ${cssManager.bdTheme('invert(1) brightness(3)', '')};
|
||||
.action dees-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.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;
|
||||
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 {
|
||||
padding: 8px 16px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.footerActions {
|
||||
margin-left: auto;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.footerActions .footerAction {
|
||||
padding: 8px 16px;
|
||||
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;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.footerActions .footerAction:hover {
|
||||
background: ${cssManager.bdTheme(colors.bright.blue, colors.dark.blue)};
|
||||
color: #fff;
|
||||
background: ${cssManager.bdTheme('hsl(0 0% 95.1%)', 'hsl(0 0% 14.9%)')};
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 9%)', 'hsl(0 0% 95%)')};
|
||||
}
|
||||
|
||||
.footerActions .footerAction dees-icon {
|
||||
display: flex;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.footerActions .footerAction:hover dees-icon {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
@ -478,24 +616,23 @@ export class DeesTable<T> extends DeesElement {
|
||||
const headings: string[] = Object.keys(firstTransformedItem);
|
||||
return html`
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
${headings.map(
|
||||
(headingArg) => html`
|
||||
<th>
|
||||
<div class="innerCellContainer">${headingArg}</div>
|
||||
</th>
|
||||
<th>${headingArg}</th>
|
||||
`
|
||||
)}
|
||||
${(() => {
|
||||
if (this.dataActions && this.dataActions.length > 0) {
|
||||
return html`
|
||||
<th>
|
||||
<div class="innerCellContainer">Actions</div>
|
||||
</th>
|
||||
<th>Actions</th>
|
||||
`;
|
||||
}
|
||||
})()}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${this.data.map((itemArg) => {
|
||||
const transformedItem = this.displayFunction(itemArg);
|
||||
const getTr = (elementArg: HTMLElement): HTMLElement => {
|
||||
@ -592,7 +729,6 @@ export class DeesTable<T> extends DeesElement {
|
||||
if (this.dataActions && this.dataActions.length > 0) {
|
||||
return html`
|
||||
<td>
|
||||
<div class="innerCellContainer">
|
||||
<div class="actionsContainer">
|
||||
${this.getActionsForType('inRow').map(
|
||||
(actionArg) => html`
|
||||
@ -615,7 +751,6 @@ export class DeesTable<T> extends DeesElement {
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
`;
|
||||
}
|
||||
@ -623,6 +758,7 @@ export class DeesTable<T> extends DeesElement {
|
||||
</tr>
|
||||
`;
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
`;
|
||||
})()
|
||||
@ -743,7 +879,7 @@ export class DeesTable<T> extends DeesElement {
|
||||
}
|
||||
|
||||
async handleCellEditing(event: Event, itemArg: T, key: string) {
|
||||
const domtools = await this.domtoolsPromise;
|
||||
await this.domtoolsPromise;
|
||||
const target = event.target as HTMLElement;
|
||||
const originalColor = target.style.color;
|
||||
target.style.color = 'transparent';
|
||||
|
@ -3,6 +3,7 @@ import { customElement, DeesElement, type TemplateResult, html, css, property, c
|
||||
import * as domtools from '@design.estate/dees-domtools';
|
||||
import { zIndexLayers } from './00zindex.js';
|
||||
import { demoFunc } from './dees-toast.demo.js';
|
||||
import { cssGeistFontFamily } from './00fonts.js';
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
@ -152,7 +153,7 @@ export class DeesToast extends DeesElement {
|
||||
:host {
|
||||
display: block;
|
||||
pointer-events: auto;
|
||||
font-family: 'Geist Sans', sans-serif;
|
||||
font-family: ${cssGeistFontFamily};
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
|
@ -18,6 +18,7 @@ export * from './dees-chips.js';
|
||||
export * from './dees-contextmenu.js';
|
||||
export * from './dees-dataview-codebox.js';
|
||||
export * from './dees-dataview-statusobject.js';
|
||||
export * from './dees-dashboardgrid.js';
|
||||
export * from './dees-editor.js';
|
||||
export * from './dees-editor-markdown.js';
|
||||
export * from './dees-editor-markdownoutlet.js';
|
||||
@ -46,6 +47,7 @@ export * from './dees-input-multitoggle.js';
|
||||
export * from './dees-panel.js';
|
||||
export * from './dees-pdf.js';
|
||||
export * from './dees-searchbar.js';
|
||||
export * from './dees-shopping-productcard.js';
|
||||
export * from './dees-simple-appdash.js';
|
||||
export * from './dees-simple-login.js';
|
||||
export * from './dees-speechbubble.js';
|
||||
|
@ -3,6 +3,8 @@ import type { IBlock } from '../../wysiwyg.types.js';
|
||||
import { cssManager } from '@design.estate/dees-element';
|
||||
import { WysiwygSelection } from '../../wysiwyg.selection.js';
|
||||
import hlight from 'highlight.js';
|
||||
import { cssGeistFontFamily, cssMonoFontFamily } from '../../../00fonts.js';
|
||||
import { PROGRAMMING_LANGUAGES } from '../../wysiwyg.constants.js';
|
||||
|
||||
/**
|
||||
* CodeBlockHandler with improved architecture
|
||||
@ -20,7 +22,7 @@ export class CodeBlockHandler extends BaseBlockHandler {
|
||||
private highlightTimer: any = null;
|
||||
|
||||
render(block: IBlock, isSelected: boolean): string {
|
||||
const language = block.metadata?.language || 'javascript';
|
||||
const language = block.metadata?.language || 'typescript';
|
||||
const content = block.content || '';
|
||||
const lineCount = content.split('\n').length;
|
||||
|
||||
@ -30,10 +32,18 @@ export class CodeBlockHandler extends BaseBlockHandler {
|
||||
lineNumbersHtml += `<div class="line-number">${i}</div>`;
|
||||
}
|
||||
|
||||
// Generate language options
|
||||
const languageOptions = PROGRAMMING_LANGUAGES.map(lang => {
|
||||
const value = lang.toLowerCase();
|
||||
return `<option value="${value}" ${value === language ? 'selected' : ''}>${lang}</option>`;
|
||||
}).join('');
|
||||
|
||||
return `
|
||||
<div class="code-block-container${isSelected ? ' selected' : ''}" data-language="${language}">
|
||||
<div class="code-header">
|
||||
<span class="language-label">${language}</span>
|
||||
<select class="language-selector" data-block-id="${block.id}">
|
||||
${languageOptions}
|
||||
</select>
|
||||
<button class="copy-button" title="Copy code">
|
||||
<svg class="copy-icon" width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
|
||||
<path d="M0 6.75C0 5.784.784 5 1.75 5h1.5a.75.75 0 010 1.5h-1.5a.25.25 0 00-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 00.25-.25v-1.5a.75.75 0 011.5 0v1.5A1.75 1.75 0 019.25 16h-7.5A1.75 1.75 0 010 14.25v-7.5z"></path>
|
||||
@ -60,9 +70,29 @@ export class CodeBlockHandler extends BaseBlockHandler {
|
||||
const editor = element.querySelector('.code-editor') as HTMLElement;
|
||||
const container = element.querySelector('.code-block-container') as HTMLElement;
|
||||
const copyButton = element.querySelector('.copy-button') as HTMLButtonElement;
|
||||
const languageSelector = element.querySelector('.language-selector') as HTMLSelectElement;
|
||||
|
||||
if (!editor || !container) return;
|
||||
|
||||
// Setup language selector
|
||||
if (languageSelector) {
|
||||
languageSelector.addEventListener('change', (e) => {
|
||||
const newLanguage = (e.target as HTMLSelectElement).value;
|
||||
block.metadata = { ...block.metadata, language: newLanguage };
|
||||
container.setAttribute('data-language', newLanguage);
|
||||
|
||||
// Update the syntax highlighting if content exists and not focused
|
||||
if (block.content && document.activeElement !== editor) {
|
||||
this.applyHighlighting(element, block);
|
||||
}
|
||||
|
||||
// Notify about the change
|
||||
if (handlers.onInput) {
|
||||
handlers.onInput(new InputEvent('input'));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Setup copy button
|
||||
if (copyButton) {
|
||||
copyButton.addEventListener('click', async () => {
|
||||
@ -285,7 +315,7 @@ export class CodeBlockHandler extends BaseBlockHandler {
|
||||
|
||||
// Get plain text content
|
||||
const content = editor.textContent || '';
|
||||
const language = block.metadata?.language || 'javascript';
|
||||
const language = block.metadata?.language || 'typescript';
|
||||
|
||||
// Apply highlighting
|
||||
try {
|
||||
@ -336,7 +366,7 @@ export class CodeBlockHandler extends BaseBlockHandler {
|
||||
type: 'code',
|
||||
content: content,
|
||||
metadata: {
|
||||
language: element.querySelector('.code-block-container')?.getAttribute('data-language') || 'javascript'
|
||||
language: element.querySelector('.code-block-container')?.getAttribute('data-language') || 'typescript'
|
||||
}
|
||||
};
|
||||
this.applyHighlighting(element, block);
|
||||
@ -440,13 +470,30 @@ export class CodeBlockHandler extends BaseBlockHandler {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.language-label {
|
||||
.language-selector {
|
||||
font-size: 12px;
|
||||
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
font-family: ${cssGeistFontFamily};
|
||||
background: transparent;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 4px;
|
||||
padding: 4px 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.language-selector:hover {
|
||||
background: ${cssManager.bdTheme('#f9fafb', '#1f2937')};
|
||||
border-color: ${cssManager.bdTheme('#e5e7eb', '#374151')};
|
||||
color: ${cssManager.bdTheme('#374151', '#e5e7eb')};
|
||||
}
|
||||
|
||||
.language-selector:focus {
|
||||
border-color: ${cssManager.bdTheme('#9ca3af', '#6b7280')};
|
||||
}
|
||||
|
||||
/* Copy Button - Minimal */
|
||||
@ -460,7 +507,7 @@ export class CodeBlockHandler extends BaseBlockHandler {
|
||||
border-radius: 4px;
|
||||
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
|
||||
font-size: 12px;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
font-family: ${cssGeistFontFamily};
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
outline: none;
|
||||
@ -515,7 +562,7 @@ export class CodeBlockHandler extends BaseBlockHandler {
|
||||
.line-number {
|
||||
padding: 0 12px 0 8px;
|
||||
color: ${cssManager.bdTheme('#9ca3af', '#4b5563')};
|
||||
font-family: 'SF Mono', Monaco, Consolas, monospace;
|
||||
font-family: ${cssMonoFontFamily};
|
||||
font-size: 13px;
|
||||
line-height: 20px;
|
||||
height: 20px;
|
||||
@ -538,7 +585,7 @@ export class CodeBlockHandler extends BaseBlockHandler {
|
||||
display: block;
|
||||
padding: 12px 16px;
|
||||
margin: 0;
|
||||
font-family: 'SF Mono', Monaco, Consolas, monospace;
|
||||
font-family: ${cssMonoFontFamily};
|
||||
font-size: 13px;
|
||||
line-height: 20px;
|
||||
color: ${cssManager.bdTheme('#111827', '#f9fafb')};
|
||||
|
@ -46,10 +46,14 @@ export class DeesFormattingMenu extends DeesElement {
|
||||
:host {
|
||||
position: fixed;
|
||||
pointer-events: none;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.formatting-menu {
|
||||
position: absolute;
|
||||
position: fixed;
|
||||
background: ${cssManager.bdTheme('#ffffff', '#262626')};
|
||||
border: 1px solid ${cssManager.bdTheme('#e0e0e0', '#404040')};
|
||||
border-radius: 6px;
|
||||
@ -159,12 +163,12 @@ export class DeesFormattingMenu extends DeesElement {
|
||||
this.position = position;
|
||||
this.callback = callback;
|
||||
|
||||
// Get z-index from registry
|
||||
// Get z-index from registry and apply immediately
|
||||
this.menuZIndex = zIndexRegistry.getNextZIndex();
|
||||
zIndexRegistry.register(this, this.menuZIndex);
|
||||
this.style.zIndex = this.menuZIndex.toString();
|
||||
|
||||
this.visible = true;
|
||||
console.log('FormattingMenu.show - visible set to:', this.visible);
|
||||
}
|
||||
|
||||
public hide(): void {
|
||||
|
@ -235,6 +235,7 @@ export class DeesInputWysiwyg extends DeesInputBase<string> {
|
||||
const blockComponent = document.createElement('dees-wysiwyg-block') as any;
|
||||
blockComponent.block = block;
|
||||
blockComponent.isSelected = this.selectedBlockId === block.id;
|
||||
blockComponent.wysiwygComponent = this; // Pass parent reference
|
||||
blockComponent.handlers = {
|
||||
onInput: (e: InputEvent) => this.inputHandler.handleBlockInput(e, block),
|
||||
onKeyDown: (e: KeyboardEvent) => this.keyboardHandler.handleBlockKeyDown(e, block),
|
||||
@ -247,28 +248,7 @@ export class DeesInputWysiwyg extends DeesInputBase<string> {
|
||||
};
|
||||
wrapper.appendChild(blockComponent);
|
||||
|
||||
// Add settings button for non-divider blocks
|
||||
if (block.type !== 'divider') {
|
||||
const settings = document.createElement('div');
|
||||
settings.className = 'block-settings';
|
||||
settings.innerHTML = `
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
|
||||
<circle cx="12" cy="5" r="2"></circle>
|
||||
<circle cx="12" cy="12" r="2"></circle>
|
||||
<circle cx="12" cy="19" r="2"></circle>
|
||||
</svg>
|
||||
`;
|
||||
settings.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
WysiwygModalManager.showBlockSettingsModal(block, () => {
|
||||
this.updateValue();
|
||||
// Re-render only the updated block
|
||||
this.updateBlockElement(block.id);
|
||||
});
|
||||
});
|
||||
wrapper.appendChild(settings);
|
||||
}
|
||||
// Remove settings button - context menu will handle this
|
||||
|
||||
// Add drag event listeners
|
||||
wrapper.addEventListener('dragover', (e) => this.dragDropHandler.handleDragOver(e, block));
|
||||
@ -507,13 +487,9 @@ export class DeesInputWysiwyg extends DeesInputBase<string> {
|
||||
// Close menu
|
||||
this.closeSlashMenu(false);
|
||||
|
||||
// If it's a code block, ask for language
|
||||
// If it's a code block, default to TypeScript
|
||||
if (type === 'code') {
|
||||
const language = await WysiwygModalManager.showLanguageSelectionModal();
|
||||
if (!language) {
|
||||
return; // User cancelled
|
||||
}
|
||||
currentBlock.metadata = { language };
|
||||
currentBlock.metadata = { language: 'typescript' };
|
||||
}
|
||||
|
||||
// Transform the current block
|
||||
|
@ -1,6 +1,5 @@
|
||||
import {
|
||||
customElement,
|
||||
property,
|
||||
html,
|
||||
DeesElement,
|
||||
type TemplateResult,
|
||||
@ -9,6 +8,7 @@ import {
|
||||
state,
|
||||
} from '@design.estate/dees-element';
|
||||
import { zIndexRegistry } from '../00zindex.js';
|
||||
import '../dees-icon.js';
|
||||
|
||||
import { type ISlashMenuItem } from './wysiwyg.types.js';
|
||||
import { WysiwygShortcuts } from './wysiwyg.shortcuts.js';
|
||||
@ -54,14 +54,18 @@ export class DeesSlashMenu extends DeesElement {
|
||||
:host {
|
||||
position: fixed;
|
||||
pointer-events: none;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.slash-menu {
|
||||
position: absolute;
|
||||
background: ${cssManager.bdTheme('#ffffff', '#262626')};
|
||||
border: 1px solid ${cssManager.bdTheme('#e0e0e0', '#404040')};
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
|
||||
position: fixed;
|
||||
background: ${cssManager.bdTheme('#ffffff', '#09090b')};
|
||||
border: 1px solid ${cssManager.bdTheme('#e5e7eb', '#27272a')};
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1), 0 1px 2px rgba(0, 0, 0, 0.06);
|
||||
padding: 4px;
|
||||
min-width: 220px;
|
||||
max-height: 300px;
|
||||
@ -74,7 +78,7 @@ export class DeesSlashMenu extends DeesElement {
|
||||
@keyframes fadeInScale {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.95) translateY(-10px);
|
||||
transform: scale(0.98) translateY(-2px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
@ -83,37 +87,35 @@ export class DeesSlashMenu extends DeesElement {
|
||||
}
|
||||
|
||||
.slash-menu-item {
|
||||
padding: 10px 12px;
|
||||
padding: 8px 10px;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
border-radius: 4px;
|
||||
color: ${cssManager.bdTheme('#000000', '#e0e0e0')};
|
||||
border-radius: 3px;
|
||||
color: ${cssManager.bdTheme('#09090b', '#fafafa')};
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.slash-menu-item:hover,
|
||||
.slash-menu-item.selected {
|
||||
background: ${cssManager.bdTheme('#f0f0f0', '#333333')};
|
||||
color: ${cssManager.bdTheme('#000000', '#ffffff')};
|
||||
background: ${cssManager.bdTheme('#f4f4f5', '#27272a')};
|
||||
color: ${cssManager.bdTheme('#09090b', '#fafafa')};
|
||||
}
|
||||
|
||||
.slash-menu-item .icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 16px;
|
||||
color: ${cssManager.bdTheme('#666', '#999')};
|
||||
font-weight: 600;
|
||||
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
|
||||
}
|
||||
|
||||
.slash-menu-item:hover .icon,
|
||||
.slash-menu-item.selected .icon {
|
||||
color: ${cssManager.bdTheme('#0066cc', '#4d94ff')};
|
||||
color: ${cssManager.bdTheme('#3b82f6', '#3b82f6')};
|
||||
}
|
||||
`,
|
||||
];
|
||||
@ -121,7 +123,7 @@ export class DeesSlashMenu extends DeesElement {
|
||||
render(): TemplateResult {
|
||||
if (!this.visible) return html``;
|
||||
|
||||
// Apply z-index to host element
|
||||
// Ensure z-index is applied to host element
|
||||
this.style.zIndex = this.menuZIndex.toString();
|
||||
|
||||
const menuItems = this.getFilteredMenuItems();
|
||||
@ -139,7 +141,7 @@ export class DeesSlashMenu extends DeesElement {
|
||||
data-item-type="${item.type}"
|
||||
data-item-index="${index}"
|
||||
>
|
||||
<span class="icon">${item.icon}</span>
|
||||
<dees-icon class="icon" .icon="${item.icon}" iconSize="16"></dees-icon>
|
||||
<span>${item.label}</span>
|
||||
</div>
|
||||
`)}
|
||||
@ -168,9 +170,10 @@ export class DeesSlashMenu extends DeesElement {
|
||||
this.filter = '';
|
||||
this.selectedIndex = 0;
|
||||
|
||||
// Get z-index from registry
|
||||
// Get z-index from registry and apply immediately
|
||||
this.menuZIndex = zIndexRegistry.getNextZIndex();
|
||||
zIndexRegistry.register(this, this.menuZIndex);
|
||||
this.style.zIndex = this.menuZIndex.toString();
|
||||
|
||||
this.visible = true;
|
||||
}
|
||||
|
@ -13,6 +13,8 @@ import { WysiwygBlocks } from './wysiwyg.blocks.js';
|
||||
import { WysiwygSelection } from './wysiwyg.selection.js';
|
||||
import { BlockRegistry, type IBlockEventHandlers } from './blocks/index.js';
|
||||
import './wysiwyg.blockregistration.js';
|
||||
import { WysiwygShortcuts } from './wysiwyg.shortcuts.js';
|
||||
import '../dees-contextmenu.js';
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
@ -38,6 +40,9 @@ export class DeesWysiwygBlock extends DeesElement {
|
||||
@property({ type: Object })
|
||||
public handlers: IBlockEventHandlers;
|
||||
|
||||
@property({ type: Object })
|
||||
public wysiwygComponent: any; // Reference to parent dees-input-wysiwyg
|
||||
|
||||
// Reference to the editable block element
|
||||
private blockElement: HTMLDivElement | null = null;
|
||||
|
||||
@ -685,6 +690,125 @@ export class DeesWysiwygBlock extends DeesElement {
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Get context menu items for this block
|
||||
*/
|
||||
public getContextMenuItems(): any[] {
|
||||
if (!this.block || this.block.type === 'divider') {
|
||||
return [];
|
||||
}
|
||||
|
||||
const blockTypes = WysiwygShortcuts.getSlashMenuItems();
|
||||
const currentType = this.block.type;
|
||||
|
||||
// Use the parent reference passed from dees-input-wysiwyg
|
||||
const wysiwygComponent = this.wysiwygComponent;
|
||||
const blockId = this.block.id;
|
||||
|
||||
|
||||
// Create submenu items for block type change
|
||||
const blockTypeItems = blockTypes
|
||||
.filter(item => item.type !== currentType && item.type !== 'divider')
|
||||
.map(item => ({
|
||||
name: item.label,
|
||||
iconName: item.icon.replace('lucide:', ''),
|
||||
action: async () => {
|
||||
if (wysiwygComponent && wysiwygComponent.blockOperations) {
|
||||
// Transform the block type
|
||||
const blockToTransform = wysiwygComponent.blocks.find((b: IBlock) => b.id === blockId);
|
||||
if (blockToTransform) {
|
||||
blockToTransform.type = item.type;
|
||||
blockToTransform.content = blockToTransform.content || '';
|
||||
|
||||
// Handle special metadata for different block types
|
||||
if (item.type === 'code') {
|
||||
blockToTransform.metadata = { language: 'typescript' };
|
||||
} else if (item.type === 'list') {
|
||||
blockToTransform.metadata = { listType: 'bullet' };
|
||||
} else if (item.type === 'image') {
|
||||
blockToTransform.content = '';
|
||||
blockToTransform.metadata = { url: '', loading: false };
|
||||
} else if (item.type === 'youtube') {
|
||||
blockToTransform.content = '';
|
||||
blockToTransform.metadata = { videoId: '', url: '' };
|
||||
} else if (item.type === 'markdown') {
|
||||
blockToTransform.metadata = { showPreview: false };
|
||||
} else if (item.type === 'html') {
|
||||
blockToTransform.metadata = { showPreview: false };
|
||||
} else if (item.type === 'attachment') {
|
||||
blockToTransform.content = '';
|
||||
blockToTransform.metadata = { files: [] };
|
||||
}
|
||||
|
||||
// Update the block element
|
||||
wysiwygComponent.updateBlockElement(blockId);
|
||||
wysiwygComponent.updateValue();
|
||||
|
||||
// Focus the block after transformation
|
||||
requestAnimationFrame(() => {
|
||||
wysiwygComponent.blockOperations.focusBlock(blockId);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
const menuItems: any[] = [
|
||||
{
|
||||
name: 'Change Type',
|
||||
iconName: 'type',
|
||||
submenu: blockTypeItems
|
||||
}
|
||||
];
|
||||
|
||||
// Add copy/cut/paste for editable blocks
|
||||
if (!['image', 'divider', 'youtube', 'attachment'].includes(this.block.type)) {
|
||||
menuItems.push(
|
||||
{ divider: true },
|
||||
{
|
||||
name: 'Cut',
|
||||
iconName: 'scissors',
|
||||
shortcut: 'Cmd+X',
|
||||
action: async () => {
|
||||
document.execCommand('cut');
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Copy',
|
||||
iconName: 'copy',
|
||||
shortcut: 'Cmd+C',
|
||||
action: async () => {
|
||||
document.execCommand('copy');
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Paste',
|
||||
iconName: 'clipboard',
|
||||
shortcut: 'Cmd+V',
|
||||
action: async () => {
|
||||
document.execCommand('paste');
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Add delete option
|
||||
menuItems.push(
|
||||
{ divider: true },
|
||||
{
|
||||
name: 'Delete Block',
|
||||
iconName: 'trash2',
|
||||
action: async () => {
|
||||
if (wysiwygComponent && wysiwygComponent.blockOperations) {
|
||||
wysiwygComponent.blockOperations.deleteBlock(blockId);
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return menuItems;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets content split at cursor position
|
||||
*/
|
||||
|
@ -11,6 +11,8 @@ export class WysiwygDragDropHandler {
|
||||
private initialBlockY: number = 0;
|
||||
private draggedBlockElement: HTMLElement | null = null;
|
||||
private draggedBlockHeight: number = 0;
|
||||
private draggedBlockContentHeight: number = 0;
|
||||
private draggedBlockMarginTop: number = 0;
|
||||
private lastUpdateTime: number = 0;
|
||||
private updateThrottle: number = 80; // milliseconds
|
||||
|
||||
@ -48,11 +50,33 @@ export class WysiwygDragDropHandler {
|
||||
this.initialMouseY = e.clientY;
|
||||
this.draggedBlockElement = this.component.editorContentRef.querySelector(`[data-block-id="${block.id}"]`);
|
||||
|
||||
|
||||
if (this.draggedBlockElement) {
|
||||
// Get the wrapper rect for measurements
|
||||
const rect = this.draggedBlockElement.getBoundingClientRect();
|
||||
this.draggedBlockHeight = rect.height;
|
||||
this.initialBlockY = rect.top;
|
||||
|
||||
// Get the inner block element for proper measurements
|
||||
const innerBlock = this.draggedBlockElement.querySelector('.block');
|
||||
if (innerBlock) {
|
||||
const innerRect = innerBlock.getBoundingClientRect();
|
||||
const computedStyle = window.getComputedStyle(innerBlock);
|
||||
this.draggedBlockMarginTop = parseInt(computedStyle.marginTop) || 0;
|
||||
this.draggedBlockContentHeight = innerRect.height;
|
||||
}
|
||||
|
||||
// The drop indicator should match the wrapper height exactly
|
||||
// The wrapper already includes all the space the block occupies
|
||||
this.draggedBlockHeight = rect.height;
|
||||
|
||||
console.log('Drag measurements:', {
|
||||
wrapperHeight: rect.height,
|
||||
marginTop: this.draggedBlockMarginTop,
|
||||
dropIndicatorHeight: this.draggedBlockHeight,
|
||||
contentHeight: this.draggedBlockContentHeight,
|
||||
blockId: block.id
|
||||
});
|
||||
|
||||
// Create drop indicator
|
||||
this.createDropIndicator();
|
||||
|
||||
@ -98,6 +122,8 @@ export class WysiwygDragDropHandler {
|
||||
this.dragOverPosition = null;
|
||||
this.draggedBlockElement = null;
|
||||
this.draggedBlockHeight = 0;
|
||||
this.draggedBlockContentHeight = 0;
|
||||
this.draggedBlockMarginTop = 0;
|
||||
this.initialBlockY = 0;
|
||||
|
||||
// Update component state
|
||||
@ -284,34 +310,93 @@ export class WysiwygDragDropHandler {
|
||||
if (!this.dropIndicator || !this.draggedBlockElement) return;
|
||||
|
||||
this.dropIndicator.style.display = 'block';
|
||||
this.dropIndicator.style.height = `${this.draggedBlockHeight}px`;
|
||||
|
||||
const containerRect = this.component.editorContentRef.getBoundingClientRect();
|
||||
// Calculate where the block will actually land
|
||||
let topPosition = 0;
|
||||
|
||||
if (targetIndex === 0) {
|
||||
// Before first block
|
||||
topPosition = 0;
|
||||
} else {
|
||||
// After a specific block
|
||||
const prevIndex = targetIndex - 1;
|
||||
let blockCount = 0;
|
||||
// Build array of visual block positions (excluding dragged block)
|
||||
const visualBlocks: { index: number, top: number, bottom: number }[] = [];
|
||||
|
||||
// Find the visual position of the block that will be before our dropped block
|
||||
for (let i = 0; i < blocks.length; i++) {
|
||||
if (i === draggedIndex) continue; // Skip the dragged block
|
||||
|
||||
if (blockCount === prevIndex) {
|
||||
const rect = blocks[i].getBoundingClientRect();
|
||||
topPosition = rect.bottom - containerRect.top + 16; // 16px gap
|
||||
break;
|
||||
const block = blocks[i];
|
||||
const rect = block.getBoundingClientRect();
|
||||
let top = rect.top - containerRect.top;
|
||||
let bottom = rect.bottom - containerRect.top;
|
||||
|
||||
// Account for any transforms
|
||||
const transform = window.getComputedStyle(block).transform;
|
||||
if (transform && transform !== 'none') {
|
||||
const matrix = new DOMMatrix(transform);
|
||||
const yOffset = matrix.m42;
|
||||
top += yOffset;
|
||||
bottom += yOffset;
|
||||
}
|
||||
|
||||
visualBlocks.push({ index: i, top, bottom });
|
||||
}
|
||||
|
||||
// Sort by visual position
|
||||
visualBlocks.sort((a, b) => a.top - b.top);
|
||||
|
||||
// Adjust targetIndex to account for excluded dragged block
|
||||
let adjustedTargetIndex = targetIndex;
|
||||
if (targetIndex > draggedIndex) {
|
||||
adjustedTargetIndex--; // Reduce by 1 since dragged block is not in visualBlocks
|
||||
}
|
||||
|
||||
// Calculate drop position
|
||||
// Get the margin that will be applied based on the dragged block type
|
||||
let blockMargin = 16; // default margin
|
||||
if (this.draggedBlockElement) {
|
||||
const draggedBlock = this.component.blocks.find(b => b.id === this.draggedBlockId);
|
||||
if (draggedBlock) {
|
||||
const blockType = draggedBlock.type;
|
||||
if (blockType === 'heading-1' || blockType === 'heading-2' || blockType === 'heading-3') {
|
||||
blockMargin = 24;
|
||||
} else if (blockType === 'code' || blockType === 'quote') {
|
||||
blockMargin = 20;
|
||||
}
|
||||
blockCount++;
|
||||
}
|
||||
}
|
||||
|
||||
this.dropIndicator.style.top = `${topPosition}px`;
|
||||
if (adjustedTargetIndex === 0) {
|
||||
// Insert at the very top - no margin needed for first block
|
||||
topPosition = 0;
|
||||
} else if (adjustedTargetIndex >= visualBlocks.length) {
|
||||
// Insert at the end
|
||||
const lastBlock = visualBlocks[visualBlocks.length - 1];
|
||||
if (lastBlock) {
|
||||
topPosition = lastBlock.bottom;
|
||||
// Add margin that will be applied to the dropped block
|
||||
topPosition += blockMargin;
|
||||
}
|
||||
} else {
|
||||
// Insert between blocks
|
||||
const blockBefore = visualBlocks[adjustedTargetIndex - 1];
|
||||
if (blockBefore) {
|
||||
topPosition = blockBefore.bottom;
|
||||
// Add margin that will be applied to the dropped block
|
||||
topPosition += blockMargin;
|
||||
}
|
||||
}
|
||||
|
||||
// Set the indicator height to match the dragged block
|
||||
this.dropIndicator.style.height = `${this.draggedBlockHeight}px`;
|
||||
|
||||
// Set position
|
||||
this.dropIndicator.style.top = `${Math.max(0, topPosition)}px`;
|
||||
|
||||
console.log('Drop indicator update:', {
|
||||
targetIndex,
|
||||
adjustedTargetIndex,
|
||||
draggedIndex,
|
||||
topPosition,
|
||||
height: this.draggedBlockHeight,
|
||||
blockMargin,
|
||||
visualBlocks: visualBlocks.map(b => ({ index: b.index, top: b.top, bottom: b.bottom }))
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -49,19 +49,19 @@ export class WysiwygShortcuts {
|
||||
|
||||
static getSlashMenuItems(): ISlashMenuItem[] {
|
||||
return [
|
||||
{ type: 'paragraph', label: 'Paragraph', icon: '¶' },
|
||||
{ type: 'heading-1', label: 'Heading 1', icon: 'H₁' },
|
||||
{ type: 'heading-2', label: 'Heading 2', icon: 'H₂' },
|
||||
{ type: 'heading-3', label: 'Heading 3', icon: 'H₃' },
|
||||
{ type: 'quote', label: 'Quote', icon: '"' },
|
||||
{ type: 'code', label: 'Code', icon: '<>' },
|
||||
{ type: 'list', label: 'List', icon: '•' },
|
||||
{ type: 'image', label: 'Image', icon: '🖼' },
|
||||
{ type: 'divider', label: 'Divider', icon: '—' },
|
||||
{ type: 'youtube', label: 'YouTube', icon: '▶️' },
|
||||
{ type: 'markdown', label: 'Markdown', icon: 'M↓' },
|
||||
{ type: 'html', label: 'HTML', icon: '</>' },
|
||||
{ type: 'attachment', label: 'File Attachment', icon: '📎' },
|
||||
{ type: 'paragraph', label: 'Paragraph', icon: 'lucide:pilcrow' },
|
||||
{ type: 'heading-1', label: 'Heading 1', icon: 'lucide:heading1' },
|
||||
{ type: 'heading-2', label: 'Heading 2', icon: 'lucide:heading2' },
|
||||
{ type: 'heading-3', label: 'Heading 3', icon: 'lucide:heading3' },
|
||||
{ type: 'quote', label: 'Quote', icon: 'lucide:quote' },
|
||||
{ type: 'code', label: 'Code Block', icon: 'lucide:fileCode' },
|
||||
{ type: 'list', label: 'Bullet List', icon: 'lucide:list' },
|
||||
{ type: 'image', label: 'Image', icon: 'lucide:image' },
|
||||
{ type: 'divider', label: 'Divider', icon: 'lucide:minus' },
|
||||
{ type: 'youtube', label: 'YouTube', icon: 'lucide:youtube' },
|
||||
{ type: 'markdown', label: 'Markdown', icon: 'lucide:fileText' },
|
||||
{ type: 'html', label: 'HTML', icon: 'lucide:code' },
|
||||
{ type: 'attachment', label: 'File Attachment', icon: 'lucide:paperclip' },
|
||||
];
|
||||
}
|
||||
|
||||
|
@ -7,23 +7,25 @@ export const wysiwygStyles = css`
|
||||
}
|
||||
|
||||
.wysiwyg-container {
|
||||
background: ${cssManager.bdTheme('#ffffff', '#1a1a1a')};
|
||||
border: 1px solid ${cssManager.bdTheme('#e0e0e0', '#333')};
|
||||
border-radius: 8px;
|
||||
background: ${cssManager.bdTheme('#ffffff', '#09090b')};
|
||||
border: 1px solid ${cssManager.bdTheme('#e5e7eb', '#27272a')};
|
||||
border-radius: 6px;
|
||||
min-height: 200px;
|
||||
padding: 32px 40px;
|
||||
padding: 24px;
|
||||
position: relative;
|
||||
transition: all 0.2s ease;
|
||||
color: ${cssManager.bdTheme('#000000', '#ffffff')};
|
||||
color: ${cssManager.bdTheme('#09090b', '#fafafa')};
|
||||
}
|
||||
|
||||
.wysiwyg-container:hover {
|
||||
border-color: ${cssManager.bdTheme('#d0d0d0', '#444')};
|
||||
border-color: ${cssManager.bdTheme('#d1d5db', '#3f3f46')};
|
||||
}
|
||||
|
||||
.wysiwyg-container:focus-within {
|
||||
border-color: ${cssManager.bdTheme('#0066cc', '#4d94ff')};
|
||||
box-shadow: 0 0 0 3px ${cssManager.bdTheme('rgba(0, 102, 204, 0.1)', 'rgba(77, 148, 255, 0.1)')};
|
||||
outline: 2px solid transparent;
|
||||
outline-offset: 2px;
|
||||
box-shadow: 0 0 0 2px ${cssManager.bdTheme('#f4f4f5', '#18181b')}, 0 0 0 4px ${cssManager.bdTheme('rgba(59, 130, 246, 0.5)', 'rgba(59, 130, 246, 0.5)')};
|
||||
border-color: ${cssManager.bdTheme('#3b82f6', '#3b82f6')};
|
||||
}
|
||||
|
||||
/* Visual hint for text selection */
|
||||
@ -44,7 +46,7 @@ export const wysiwygStyles = css`
|
||||
position: relative;
|
||||
transition: all 0.15s ease;
|
||||
min-height: 1.6em;
|
||||
color: ${cssManager.bdTheme('#000000', '#e0e0e0')};
|
||||
color: ${cssManager.bdTheme('#09090b', '#fafafa')};
|
||||
}
|
||||
|
||||
/* First and last blocks don't need extra spacing */
|
||||
@ -57,8 +59,9 @@ export const wysiwygStyles = css`
|
||||
}
|
||||
|
||||
.block.selected {
|
||||
background: ${cssManager.bdTheme('rgba(0, 102, 204, 0.05)', 'rgba(77, 148, 255, 0.08)')};
|
||||
box-shadow: inset 0 0 0 2px ${cssManager.bdTheme('rgba(0, 102, 204, 0.2)', 'rgba(77, 148, 255, 0.2)')};
|
||||
background: ${cssManager.bdTheme('rgba(59, 130, 246, 0.05)', 'rgba(59, 130, 246, 0.05)')};
|
||||
outline: 2px solid ${cssManager.bdTheme('rgba(59, 130, 246, 0.2)', 'rgba(59, 130, 246, 0.2)')};
|
||||
outline-offset: -2px;
|
||||
border-radius: 4px;
|
||||
margin-left: -8px;
|
||||
margin-right: -8px;
|
||||
@ -78,7 +81,7 @@ export const wysiwygStyles = css`
|
||||
|
||||
.block.paragraph:empty::before {
|
||||
content: "Type '/' for commands...";
|
||||
color: ${cssManager.bdTheme('#999', '#666')};
|
||||
color: ${cssManager.bdTheme('#71717a', '#71717a')};
|
||||
pointer-events: none;
|
||||
font-size: 16px;
|
||||
line-height: 1.6;
|
||||
@ -89,12 +92,12 @@ export const wysiwygStyles = css`
|
||||
font-size: 32px;
|
||||
font-weight: 700;
|
||||
line-height: 1.2;
|
||||
color: ${cssManager.bdTheme('#000000', '#ffffff')};
|
||||
color: ${cssManager.bdTheme('#09090b', '#fafafa')};
|
||||
}
|
||||
|
||||
.block.heading-1:empty::before {
|
||||
content: "Heading 1";
|
||||
color: ${cssManager.bdTheme('#999', '#666')};
|
||||
color: ${cssManager.bdTheme('#71717a', '#71717a')};
|
||||
pointer-events: none;
|
||||
font-size: 32px;
|
||||
line-height: 1.2;
|
||||
@ -105,12 +108,12 @@ export const wysiwygStyles = css`
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
line-height: 1.3;
|
||||
color: ${cssManager.bdTheme('#000000', '#ffffff')};
|
||||
color: ${cssManager.bdTheme('#09090b', '#fafafa')};
|
||||
}
|
||||
|
||||
.block.heading-2:empty::before {
|
||||
content: "Heading 2";
|
||||
color: ${cssManager.bdTheme('#999', '#666')};
|
||||
color: ${cssManager.bdTheme('#71717a', '#71717a')};
|
||||
pointer-events: none;
|
||||
font-size: 24px;
|
||||
line-height: 1.3;
|
||||
@ -121,12 +124,12 @@ export const wysiwygStyles = css`
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
line-height: 1.4;
|
||||
color: ${cssManager.bdTheme('#000000', '#ffffff')};
|
||||
color: ${cssManager.bdTheme('#09090b', '#fafafa')};
|
||||
}
|
||||
|
||||
.block.heading-3:empty::before {
|
||||
content: "Heading 3";
|
||||
color: ${cssManager.bdTheme('#999', '#666')};
|
||||
color: ${cssManager.bdTheme('#71717a', '#71717a')};
|
||||
pointer-events: none;
|
||||
font-size: 20px;
|
||||
line-height: 1.4;
|
||||
@ -134,10 +137,10 @@ export const wysiwygStyles = css`
|
||||
}
|
||||
|
||||
.block.quote {
|
||||
border-left: 3px solid ${cssManager.bdTheme('#0066cc', '#4d94ff')};
|
||||
border-left: 2px solid ${cssManager.bdTheme('#e5e7eb', '#27272a')};
|
||||
padding-left: 20px;
|
||||
font-style: italic;
|
||||
color: ${cssManager.bdTheme('#555', '#b0b0b0')};
|
||||
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
line-height: 1.6;
|
||||
@ -145,7 +148,7 @@ export const wysiwygStyles = css`
|
||||
|
||||
.block.quote:empty::before {
|
||||
content: "Quote";
|
||||
color: ${cssManager.bdTheme('#999', '#666')};
|
||||
color: ${cssManager.bdTheme('#71717a', '#71717a')};
|
||||
pointer-events: none;
|
||||
font-size: 16px;
|
||||
line-height: 1.6;
|
||||
@ -162,33 +165,33 @@ export const wysiwygStyles = css`
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
background: ${cssManager.bdTheme('#e1e4e8', '#333333')};
|
||||
color: ${cssManager.bdTheme('#586069', '#8b949e')};
|
||||
background: ${cssManager.bdTheme('#f4f4f5', '#27272a')};
|
||||
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
|
||||
padding: 4px 12px;
|
||||
font-size: 12px;
|
||||
border-radius: 0 6px 0 6px;
|
||||
border-radius: 0 4px 0 4px;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
text-transform: lowercase;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.block.code {
|
||||
background: ${cssManager.bdTheme('#f8f8f8', '#0d0d0d')};
|
||||
border: 1px solid ${cssManager.bdTheme('#e0e0e0', '#2a2a2a')};
|
||||
border-radius: 6px;
|
||||
padding: 16px 20px;
|
||||
background: ${cssManager.bdTheme('#f4f4f5', '#18181b')};
|
||||
border: 1px solid ${cssManager.bdTheme('#e5e7eb', '#27272a')};
|
||||
border-radius: 4px;
|
||||
padding: 16px;
|
||||
padding-top: 32px; /* Make room for language indicator */
|
||||
font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Fira Code', monospace;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
white-space: pre-wrap;
|
||||
color: ${cssManager.bdTheme('#24292e', '#e1e4e8')};
|
||||
color: ${cssManager.bdTheme('#09090b', '#fafafa')};
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.block.code:empty::before {
|
||||
content: "// Code block";
|
||||
color: ${cssManager.bdTheme('#999', '#666')};
|
||||
color: ${cssManager.bdTheme('#71717a', '#71717a')};
|
||||
pointer-events: none;
|
||||
font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Fira Code', monospace;
|
||||
font-size: 14px;
|
||||
@ -233,16 +236,16 @@ export const wysiwygStyles = css`
|
||||
|
||||
.block.divider hr {
|
||||
border: none;
|
||||
border-top: 1px solid ${cssManager.bdTheme('#e0e0e0', '#333')};
|
||||
border-top: 1px solid ${cssManager.bdTheme('#e5e7eb', '#27272a')};
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.slash-menu {
|
||||
position: absolute;
|
||||
background: ${cssManager.bdTheme('#ffffff', '#262626')};
|
||||
border: 1px solid ${cssManager.bdTheme('#e0e0e0', '#404040')};
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
|
||||
background: ${cssManager.bdTheme('#ffffff', '#09090b')};
|
||||
border: 1px solid ${cssManager.bdTheme('#e5e7eb', '#27272a')};
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1), 0 1px 2px rgba(0, 0, 0, 0.06);
|
||||
padding: 4px;
|
||||
z-index: 1000;
|
||||
min-width: 220px;
|
||||
@ -253,21 +256,21 @@ export const wysiwygStyles = css`
|
||||
}
|
||||
|
||||
.slash-menu-item {
|
||||
padding: 10px 12px;
|
||||
padding: 8px 10px;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
border-radius: 4px;
|
||||
color: ${cssManager.bdTheme('#000000', '#e0e0e0')};
|
||||
border-radius: 3px;
|
||||
color: ${cssManager.bdTheme('#09090b', '#fafafa')};
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.slash-menu-item:hover,
|
||||
.slash-menu-item.selected {
|
||||
background: ${cssManager.bdTheme('#f0f0f0', '#333333')};
|
||||
color: ${cssManager.bdTheme('#000000', '#ffffff')};
|
||||
background: ${cssManager.bdTheme('#f4f4f5', '#27272a')};
|
||||
color: ${cssManager.bdTheme('#09090b', '#fafafa')};
|
||||
}
|
||||
|
||||
.slash-menu-item .icon {
|
||||
@ -277,23 +280,23 @@ export const wysiwygStyles = css`
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 16px;
|
||||
color: ${cssManager.bdTheme('#666', '#999')};
|
||||
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.slash-menu-item:hover .icon,
|
||||
.slash-menu-item.selected .icon {
|
||||
color: ${cssManager.bdTheme('#0066cc', '#4d94ff')};
|
||||
color: ${cssManager.bdTheme('#3b82f6', '#3b82f6')};
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
position: absolute;
|
||||
top: -40px;
|
||||
left: 0;
|
||||
background: ${cssManager.bdTheme('#ffffff', '#262626')};
|
||||
border: 1px solid ${cssManager.bdTheme('#e0e0e0', '#404040')};
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
|
||||
background: ${cssManager.bdTheme('#ffffff', '#09090b')};
|
||||
border: 1px solid ${cssManager.bdTheme('#e5e7eb', '#27272a')};
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1), 0 1px 2px rgba(0, 0, 0, 0.06);
|
||||
padding: 4px;
|
||||
display: none;
|
||||
gap: 4px;
|
||||
@ -310,17 +313,17 @@ export const wysiwygStyles = css`
|
||||
border: none;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
border-radius: 3px;
|
||||
transition: all 0.15s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: ${cssManager.bdTheme('#000000', '#e0e0e0')};
|
||||
color: ${cssManager.bdTheme('#09090b', '#fafafa')};
|
||||
}
|
||||
|
||||
.toolbar-button:hover {
|
||||
background: ${cssManager.bdTheme('#f0f0f0', '#333333')};
|
||||
color: ${cssManager.bdTheme('#0066cc', '#4d94ff')};
|
||||
background: ${cssManager.bdTheme('#f4f4f5', '#27272a')};
|
||||
color: ${cssManager.bdTheme('#3b82f6', '#3b82f6')};
|
||||
}
|
||||
|
||||
/* Drag and Drop Styles */
|
||||
@ -360,7 +363,7 @@ export const wysiwygStyles = css`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: ${cssManager.bdTheme('#999', '#666')};
|
||||
color: ${cssManager.bdTheme('#71717a', '#71717a')};
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
@ -375,13 +378,13 @@ export const wysiwygStyles = css`
|
||||
}
|
||||
|
||||
.drag-handle:hover {
|
||||
color: ${cssManager.bdTheme('#666', '#999')};
|
||||
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.05)', 'rgba(255, 255, 255, 0.05)')};
|
||||
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
|
||||
background: ${cssManager.bdTheme('#f4f4f5', '#27272a')};
|
||||
}
|
||||
|
||||
.drag-handle:active {
|
||||
cursor: grabbing;
|
||||
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.1)', 'rgba(255, 255, 255, 0.1)')};
|
||||
background: ${cssManager.bdTheme('#e5e7eb', '#3f3f46')};
|
||||
}
|
||||
|
||||
.block-wrapper.dragging {
|
||||
@ -407,9 +410,9 @@ export const wysiwygStyles = css`
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: ${cssManager.bdTheme('rgba(0, 102, 204, 0.08)', 'rgba(77, 148, 255, 0.08)')};
|
||||
border: 2px dashed ${cssManager.bdTheme('#0066cc', '#4d94ff')};
|
||||
border-radius: 8px;
|
||||
background: ${cssManager.bdTheme('rgba(59, 130, 246, 0.05)', 'rgba(59, 130, 246, 0.05)')};
|
||||
border: 2px dashed ${cssManager.bdTheme('#3b82f6', '#3b82f6')};
|
||||
border-radius: 4px;
|
||||
transition: top 0.2s ease, height 0.2s ease;
|
||||
pointer-events: none;
|
||||
z-index: 1999;
|
||||
@ -426,50 +429,21 @@ export const wysiwygStyles = css`
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
/* Block Settings Button */
|
||||
.block-settings {
|
||||
position: absolute;
|
||||
right: -40px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
cursor: pointer;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: ${cssManager.bdTheme('#999', '#666')};
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.block-wrapper:hover .block-settings {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.block-settings:hover {
|
||||
color: ${cssManager.bdTheme('#666', '#999')};
|
||||
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.05)', 'rgba(255, 255, 255, 0.05)')};
|
||||
}
|
||||
|
||||
.block-settings:active {
|
||||
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.1)', 'rgba(255, 255, 255, 0.1)')};
|
||||
}
|
||||
/* Block Settings Button - Removed in favor of context menu */
|
||||
|
||||
/* Text Selection Styles */
|
||||
.block ::selection {
|
||||
background: ${cssManager.bdTheme('rgba(0, 102, 204, 0.3)', 'rgba(77, 148, 255, 0.3)')};
|
||||
background: ${cssManager.bdTheme('rgba(59, 130, 246, 0.2)', 'rgba(59, 130, 246, 0.2)')};
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
/* Formatting Menu */
|
||||
.formatting-menu {
|
||||
position: absolute;
|
||||
background: ${cssManager.bdTheme('#ffffff', '#262626')};
|
||||
border: 1px solid ${cssManager.bdTheme('#e0e0e0', '#404040')};
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 2px 16px rgba(0, 0, 0, 0.15);
|
||||
background: ${cssManager.bdTheme('#ffffff', '#09090b')};
|
||||
border: 1px solid ${cssManager.bdTheme('#e5e7eb', '#27272a')};
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1), 0 1px 2px rgba(0, 0, 0, 0.06);
|
||||
padding: 4px;
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
@ -480,7 +454,7 @@ export const wysiwygStyles = css`
|
||||
@keyframes fadeInScale {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.95) translateY(5px);
|
||||
transform: scale(0.98) translateY(2px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
@ -494,20 +468,20 @@ export const wysiwygStyles = css`
|
||||
border: none;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
border-radius: 3px;
|
||||
transition: all 0.15s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: ${cssManager.bdTheme('#000000', '#e0e0e0')};
|
||||
color: ${cssManager.bdTheme('#09090b', '#fafafa')};
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.format-button:hover {
|
||||
background: ${cssManager.bdTheme('#f0f0f0', '#333333')};
|
||||
color: ${cssManager.bdTheme('#0066cc', '#4d94ff')};
|
||||
background: ${cssManager.bdTheme('#f4f4f5', '#27272a')};
|
||||
color: ${cssManager.bdTheme('#3b82f6', '#3b82f6')};
|
||||
}
|
||||
|
||||
.format-button:active {
|
||||
@ -535,7 +509,7 @@ export const wysiwygStyles = css`
|
||||
.block strong,
|
||||
.block b {
|
||||
font-weight: 600;
|
||||
color: ${cssManager.bdTheme('#000000', '#ffffff')};
|
||||
color: ${cssManager.bdTheme('#09090b', '#fafafa')};
|
||||
}
|
||||
|
||||
.block em,
|
||||
@ -554,22 +528,22 @@ export const wysiwygStyles = css`
|
||||
}
|
||||
|
||||
.block code {
|
||||
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.06)', 'rgba(255, 255, 255, 0.1)')};
|
||||
background: ${cssManager.bdTheme('#f4f4f5', '#27272a')};
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Fira Code', monospace;
|
||||
font-size: 0.9em;
|
||||
color: ${cssManager.bdTheme('#d14', '#ff6b6b')};
|
||||
color: ${cssManager.bdTheme('#09090b', '#fafafa')};
|
||||
}
|
||||
|
||||
.block a {
|
||||
color: ${cssManager.bdTheme('#0066cc', '#4d94ff')};
|
||||
color: ${cssManager.bdTheme('#3b82f6', '#3b82f6')};
|
||||
text-decoration: none;
|
||||
border-bottom: 1px solid transparent;
|
||||
transition: border-color 0.15s ease;
|
||||
}
|
||||
|
||||
.block a:hover {
|
||||
border-bottom-color: ${cssManager.bdTheme('#0066cc', '#4d94ff')};
|
||||
border-bottom-color: ${cssManager.bdTheme('#3b82f6', '#3b82f6')};
|
||||
}
|
||||
`;
|
@ -12,7 +12,7 @@ import '../elements/dees-input-tags.js';
|
||||
import '../elements/dees-input-wysiwyg.js';
|
||||
import '../elements/dees-appui-profiledropdown.js';
|
||||
|
||||
export const showcasePage = () => html`
|
||||
export const zIndexShowcase = () => html`
|
||||
<style>
|
||||
${css`
|
||||
.page-wrapper {
|
||||
@ -245,6 +245,16 @@ export const showcasePage = () => html`
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* Consistent panel spacing */
|
||||
dees-panel {
|
||||
display: block;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
dees-panel:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.test-area {
|
||||
position: relative;
|
||||
height: 200px;
|
||||
@ -665,14 +675,23 @@ export const showcasePage = () => html`
|
||||
.placeholder=${'Type "/" to see slash commands or select text to format...'}
|
||||
.outputFormat=${'html'}
|
||||
.description=${'The slash menu and formatting menu should appear above this modal'}
|
||||
.value=${`<p>Welcome to the WYSIWYG editor demo!</p>
|
||||
<p>This editor demonstrates proper z-index management:</p>
|
||||
<ul>
|
||||
<li>Type <strong>/</strong> to open the slash command menu</li>
|
||||
<li>Select any text to see the formatting toolbar</li>
|
||||
<li>Both menus will appear <em>above</em> this modal</li>
|
||||
</ul>
|
||||
<p>Try it now: Type / here or select this text to format it.</p>`}
|
||||
></dees-input-wysiwyg>
|
||||
</dees-form>
|
||||
<p style="margin-top: 16px; color: ${cssManager.bdTheme('#666', '#999')}">
|
||||
<strong>Tips:</strong><br>
|
||||
• Type "/" to open the slash command menu<br>
|
||||
• Select text to see the formatting toolbar<br>
|
||||
• Both menus should appear above this modal
|
||||
</p>
|
||||
<div style="margin-top: 16px; padding: 16px; background: ${cssManager.bdTheme('#e3f2fd', '#1e3a5f')}; border-radius: 8px;">
|
||||
<strong style="color: ${cssManager.bdTheme('#1976d2', '#90caf9')}">✨ Z-Index Fix Applied!</strong><br>
|
||||
<span style="color: ${cssManager.bdTheme('#1976d2', '#90caf9')}">
|
||||
The WYSIWYG menus now properly use the dynamic z-index registry.<br>
|
||||
They will always appear above the modal, regardless of stacking order.
|
||||
</span>
|
||||
</div>
|
||||
`,
|
||||
menuOptions: [
|
||||
{ name: 'Cancel', action: async (modal) => modal.destroy() },
|
||||
|
Reference in New Issue
Block a user