Compare commits

...

42 Commits

Author SHA1 Message Date
ce33aff843 1.10.10
Some checks failed
Default (tags) / security (push) Failing after 13s
Default (tags) / test (push) Failing after 57s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-06-30 13:07:16 +00:00
09eea844d7 feat(dees-mobilenavigation): update to use zindex registry and shadcn-like design
- Replace old zIndexLayers with new zIndexRegistry system
- Update design to match shadcn aesthetic with clean borders and shadows
- Add support for icons in menu items using Lucide icons
- Improve animations with staggered item appearance
- Better typography using Geist font family
- Add divider support for menu item grouping
- Improve hover and active states
- Add custom scrollbar styling
- Create comprehensive demo showcasing all features
- Ensure proper cleanup in disconnectedCallback
2025-06-30 13:04:19 +00:00
956edf0d63 fix(icons): update icon usage across components
- Replace .iconName property with .icon for dees-icon component
- Fix incorrect lucide icon names to use proper prefix and kebab-case
- Replace deprecated .iconFA property with .icon
- Add loading animation to dees-input-fileupload button
- Maintain compatibility with external interfaces expecting iconName
2025-06-30 12:57:13 +00:00
1db74177b3 update 2025-06-30 12:02:02 +00:00
1c25554c38 update 2025-06-30 11:35:38 +00:00
7d1e06701b update 2025-06-30 11:24:38 +00:00
aae4427281 update 2025-06-30 11:18:30 +00:00
911c51d078 update 2025-06-30 11:08:14 +00:00
2c12c22666 update 2025-06-30 10:58:31 +00:00
60a811fd18 update 2025-06-30 10:53:22 +00:00
9a9aea56da add datepicker 2025-06-30 10:40:23 +00:00
49ad998b2c update 2025-06-29 14:00:55 +00:00
5066681b3a update 2025-06-28 12:34:35 +00:00
ee22879c00 update 2025-06-28 12:27:35 +00:00
9b0ff2d856 1.10.9
Some checks failed
Default (tags) / security (push) Failing after 59s
Default (tags) / test (push) Failing after 19s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-06-28 10:05:09 +00:00
7e14645ed7 update 2025-06-27 23:48:39 +00:00
811737adcd update 2025-06-27 22:55:20 +00:00
7b6c135cd3 update 2025-06-27 22:47:24 +00:00
46065b2424 1.10.8
Some checks failed
Default (tags) / security (push) Failing after 55s
Default (tags) / test (push) Failing after 17s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-06-27 21:19:43 +00:00
e76a6c3632 update 2025-06-27 21:19:14 +00:00
896bc2bbb1 update 2025-06-27 21:16:52 +00:00
296d254ba2 update 2025-06-27 21:07:47 +00:00
ecad05098f update 2025-06-27 21:05:28 +00:00
956964f5b9 update dees-chips 2025-06-27 21:01:12 +00:00
ed73e16bbb update dees-modal 2025-06-27 19:48:32 +00:00
7817b4a440 1.10.7
Some checks failed
Default (tags) / security (push) Failing after 54s
Default (tags) / test (push) Failing after 12s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-06-27 19:33:37 +00:00
03f25b7f10 update wysiwyg 2025-06-27 19:29:26 +00:00
24957f02d4 update wysiwyg 2025-06-27 19:25:34 +00:00
fe3cd0591f update 2025-06-27 18:38:39 +00:00
56f5f5887b update 2025-06-27 18:03:42 +00:00
2e0bf26301 update table 2025-06-27 17:50:54 +00:00
3d7f5253e8 update fonts 2025-06-27 17:32:01 +00:00
669f12e822 1.10.6
Some checks failed
Default (tags) / security (push) Failing after 52s
Default (tags) / test (push) Failing after 19s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-06-27 17:14:49 +00:00
8b870a8e46 update 2025-06-27 17:14:46 +00:00
9148f0595a update 2025-06-27 17:14:26 +00:00
ea7da1c9b9 update 2025-06-27 16:39:17 +00:00
3e81f54e99 update 2025-06-27 16:29:19 +00:00
65aa9f3c06 update 2025-06-27 16:20:06 +00:00
82ebd9c556 update 2025-06-27 15:58:26 +00:00
50aa071e2e update dees-chart 2025-06-27 15:43:26 +00:00
807e1ff733 1.10.5
Some checks failed
Default (tags) / security (push) Failing after 52s
Default (tags) / test (push) Failing after 19s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-06-27 13:44:52 +00:00
4146a348ab update statsgrid 2025-06-27 13:44:36 +00:00
88 changed files with 13027 additions and 2437 deletions

View File

@ -1,5 +1,80 @@
# Changelog # Changelog
## 2025-06-29 - 1.10.10 - improve(dees-dashboardgrid, dees-input-wysiwyg)
Enhanced dashboard grid component with advanced spacing and layout features inspired by gridstack.js
Dashboard Grid improvements:
- Improved margin system supporting uniform or individual margins (top, right, bottom, left)
- Added collision detection to prevent widget overlap during drag operations
- Implemented auto-positioning for new widgets to find first available space
- Added compact() method to eliminate gaps and compress layout vertically or horizontally
- Enhanced resize constraints with minW, maxW, minH, maxH support
- Added optional grid lines visualization for better layout understanding
- Improved resize handles with better visibility and hover states
- Added RTL (right-to-left) layout support
- Implemented cellHeightUnit option supporting 'px', 'em', 'rem', or 'auto' (square cells)
- Added configurable animation with enableAnimation property
- Enhanced demo with interactive controls for testing all features
- Better calculation of widget positions accounting for margins between cells
- Added findAvailablePosition() for intelligent widget placement
- Improved drag and resize calculations for pixel-perfect positioning
WYSIWYG editor drag and drop fixes:
- Fixed drop indicator positioning to properly account for block margins
- Added defensive checks in drag event handlers to prevent potential crashes
- Improved updateBlockPositions with null checks and error handling
- Updated drop indicator calculation to use simplified margin approach
- Fixed drop indicator height to match the exact space occupied by dragged blocks
- Improved drop indicator positioning algorithm to accurately show where blocks will land
- Simplified visual block position calculations accounting for CSS transforms
- Enhanced margin calculation to use correct values based on block type (16px for paragraphs, 24px for headings, 20px for code/quotes)
- Fixed index calculation issue when dragging blocks downward by adjusting target index for excluded dragged block
## 2025-06-28 - 1.10.9 - feat(dees-dashboardgrid)
Add new dashboard grid component with drag-and-drop and resize capabilities
- Created dees-dashboardgrid component for building flexible dashboard layouts
- Features drag-and-drop functionality for rearranging widgets
- Includes resize handles for adjusting widget dimensions
- Supports configurable grid properties (columns, cell height, gap)
- Provides widget locking and editable mode controls
- Styled with shadcn design principles
- No external dependencies - built with native browser APIs
- Emits events for widget movements and resizes
- Includes comprehensive demo with sample dashboard widgets
## 2025-06-27 - 1.10.8 - feat(ui-components)
Update multiple components with shadcn-aligned styling and improved animations
- Updated dees-modal with shadcn colors, borders, and subtle shadows
- Updated dees-chips with shadcn styling and fixed selection logic bug
- Updated dees-dataview-codebox with shadcn syntax highlighting colors and responsive label layout
- Updated dees-input-multitoggle with transparent blue indicator and smooth animations
- Updated dees-appui-tabs with animated sliding indicator for both horizontal and vertical layouts
- Fixed indicator positioning to be perfectly centered on tab content
- Indicator width is content width + 8px for minimal visual padding
- Fixed tab content centering by using consistent padding (12px → 16px on all sides)
- Fixed icon rendering by correcting property name from .iconName to .icon
- Added visual separators between tabs for better distinction
- Added subtle hover backgrounds for improved interactivity
- Refactored tabs component code for better maintainability and elegance
- Updated dees-appui-activitylog with shadcn-aligned styling:
- Updated background and text colors to match shadcn palette
- Enhanced topbar with better spacing and typography
- Improved activity entries with subtle hover states and better spacing
- Added activity type icons with color-coded backgrounds (login, logout, view, create, update)
- Added date separators ("Today", "Yesterday") for better temporal organization
- Enhanced streaming indicators with animated pulse effect
- Redesigned searchbox with modern input styling, search icon, and focus states
- Added custom scrollbar styling for consistency
- Updated timestamps to be more subtle with tabular number formatting
- Refined shadow effects for better visual hierarchy
- Added subtle box shadow to component for depth
- Added fade-in animation for new activity entries
- Improved user name highlighting with better typography
- Updated context menu with more relevant actions
- Improved overall spacing and visual consistency across components
## 2025-06-27 - 1.10.1 - fix(modal) ## 2025-06-27 - 1.10.1 - fix(modal)
Improve modal overscroll behavior by adding 'overscroll-behavior: contain' to content container Improve modal overscroll behavior by adding 'overscroll-behavior: contain' to content container

View File

@ -1,6 +1,6 @@
{ {
"name": "@design.estate/dees-catalog", "name": "@design.estate/dees-catalog",
"version": "1.10.4", "version": "1.10.10",
"private": false, "private": false,
"description": "A comprehensive library that provides dynamic web components for building sophisticated and modern web applications using JavaScript and TypeScript.", "description": "A comprehensive library that provides dynamic web components for building sophisticated and modern web applications using JavaScript and TypeScript.",
"main": "dist_ts_web/index.js", "main": "dist_ts_web/index.js",
@ -17,7 +17,7 @@
"dependencies": { "dependencies": {
"@design.estate/dees-domtools": "^2.3.3", "@design.estate/dees-domtools": "^2.3.3",
"@design.estate/dees-element": "^2.0.45", "@design.estate/dees-element": "^2.0.45",
"@design.estate/dees-wcctools": "^1.0.101", "@design.estate/dees-wcctools": "^1.1.0",
"@fortawesome/fontawesome-svg-core": "^6.7.2", "@fortawesome/fontawesome-svg-core": "^6.7.2",
"@fortawesome/free-brands-svg-icons": "^6.7.2", "@fortawesome/free-brands-svg-icons": "^6.7.2",
"@fortawesome/free-regular-svg-icons": "^6.7.2", "@fortawesome/free-regular-svg-icons": "^6.7.2",
@ -36,7 +36,7 @@
"apexcharts": "^4.7.0", "apexcharts": "^4.7.0",
"highlight.js": "11.11.1", "highlight.js": "11.11.1",
"ibantools": "^4.5.1", "ibantools": "^4.5.1",
"lucide": "^0.523.0", "lucide": "^0.525.0",
"monaco-editor": "^0.52.2", "monaco-editor": "^0.52.2",
"pdfjs-dist": "^4.10.38", "pdfjs-dist": "^4.10.38",
"xterm": "^5.3.0", "xterm": "^5.3.0",

22
pnpm-lock.yaml generated
View File

@ -15,8 +15,8 @@ importers:
specifier: ^2.0.45 specifier: ^2.0.45
version: 2.0.45 version: 2.0.45
'@design.estate/dees-wcctools': '@design.estate/dees-wcctools':
specifier: ^1.0.101 specifier: ^1.1.0
version: 1.0.101 version: 1.1.0
'@fortawesome/fontawesome-svg-core': '@fortawesome/fontawesome-svg-core':
specifier: ^6.7.2 specifier: ^6.7.2
version: 6.7.2 version: 6.7.2
@ -72,8 +72,8 @@ importers:
specifier: ^4.5.1 specifier: ^4.5.1
version: 4.5.1 version: 4.5.1
lucide: lucide:
specifier: ^0.523.0 specifier: ^0.525.0
version: 0.523.0 version: 0.525.0
monaco-editor: monaco-editor:
specifier: ^0.52.2 specifier: ^0.52.2
version: 0.52.2 version: 0.52.2
@ -323,8 +323,8 @@ packages:
'@design.estate/dees-element@2.0.45': '@design.estate/dees-element@2.0.45':
resolution: {integrity: sha512-dj8nOOtfwvqEtQceTXQQ5IEy75HIFZ+iuDxPeIynLedYpxtHPsxFrHW8IQ7/ad9MNvVO0kTnlwUOmkjylul+DA==} resolution: {integrity: sha512-dj8nOOtfwvqEtQceTXQQ5IEy75HIFZ+iuDxPeIynLedYpxtHPsxFrHW8IQ7/ad9MNvVO0kTnlwUOmkjylul+DA==}
'@design.estate/dees-wcctools@1.0.101': '@design.estate/dees-wcctools@1.1.0':
resolution: {integrity: sha512-6j2kGORf7egRkHGwQUNuxSdTe2+6y7eX3+dVomBLeWczH30KhPi1jJKINSt/MqkpB5i7o3kQwvvWA6JYBOjXcg==} resolution: {integrity: sha512-eniG2JsGgcVXQLkSE6M7azJ7av/UeTvvzhE6s3JbcIieHd589SCxQqF+BhlOyKqzJQ1n5jJ7KKdmhvQU5bbdtg==}
'@emnapi/core@1.4.3': '@emnapi/core@1.4.3':
resolution: {integrity: sha512-4m62DuCE07lw01soJwPiBGC0nAww0Q+RY70VZ+n49yDIO13yyinhbWCeNnaob0lakDtWQzSdtNWzJeOJt2ma+g==} resolution: {integrity: sha512-4m62DuCE07lw01soJwPiBGC0nAww0Q+RY70VZ+n49yDIO13yyinhbWCeNnaob0lakDtWQzSdtNWzJeOJt2ma+g==}
@ -3481,8 +3481,8 @@ packages:
resolution: {integrity: sha512-MhWWlVnuab1RG5/zMRRcVGXZLCXrZTgfwMikgzCegsPnG62yDQo5JnqKkrK4jO5iKqDAZGItAqN5CtKBCBWRUA==} resolution: {integrity: sha512-MhWWlVnuab1RG5/zMRRcVGXZLCXrZTgfwMikgzCegsPnG62yDQo5JnqKkrK4jO5iKqDAZGItAqN5CtKBCBWRUA==}
engines: {node: '>=16.14'} engines: {node: '>=16.14'}
lucide@0.523.0: lucide@0.525.0:
resolution: {integrity: sha512-tiIp5xEP4kpeulfT1J+a/NEaIZGT1k6RyMS3evQWfHRhJvR8uTat/+lN4wnX5qIexOwN02BhmcyMHBNwt+jkLA==} resolution: {integrity: sha512-sfehWlaE/7NVkcEQ4T9JD3eID8RNMIGJBBUq9wF3UFiJIrcMKRbU3g1KGfDk4svcW7yw8BtDLXaXo02scDtUYQ==}
make-dir@3.1.0: make-dir@3.1.0:
resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==} resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==}
@ -5588,7 +5588,7 @@ snapshots:
- supports-color - supports-color
- vue - vue
'@design.estate/dees-wcctools@1.0.101': '@design.estate/dees-wcctools@1.1.0':
dependencies: dependencies:
'@design.estate/dees-domtools': 2.3.3 '@design.estate/dees-domtools': 2.3.3
'@design.estate/dees-element': 2.0.45 '@design.estate/dees-element': 2.0.45
@ -5905,10 +5905,8 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- '@nuxt/kit' - '@nuxt/kit'
- '@swc/helpers' - '@swc/helpers'
- bufferutil
- react - react
- supports-color - supports-color
- utf-8-validate
- vue - vue
'@hapi/bourne@3.0.0': {} '@hapi/bourne@3.0.0': {}
@ -9564,7 +9562,7 @@ snapshots:
lru-cache@8.0.5: {} lru-cache@8.0.5: {}
lucide@0.523.0: {} lucide@0.525.0: {}
make-dir@3.1.0: make-dir@3.1.0:
dependencies: dependencies:

288
readme.md
View File

@ -1,5 +1,5 @@
# @design.estate/dees-catalog # @design.estate/dees-catalog
An extensive library for building modern web applications with dynamic components using Web Components, JavaScript, and TypeScript. A comprehensive web components library built with TypeScript and LitElement, providing 75+ UI components for building modern web applications with consistent design and behavior.
## Install ## Install
To install the `@design.estate/dees-catalog` library, you can use npm or any other compatible JavaScript package manager: To install the `@design.estate/dees-catalog` library, you can use npm or any other compatible JavaScript package manager:
@ -12,15 +12,16 @@ npm install @design.estate/dees-catalog
| Category | Components | | Category | Components |
|----------|------------| |----------|------------|
| Core UI | `DeesButton`, `DeesButtonExit`, `DeesButtonGroup`, `DeesBadge`, `DeesChips`, `DeesHeading`, `DeesHint`, `DeesIcon`, `DeesLabel`, `DeesPanel`, `DeesSearchbar`, `DeesSpinner`, `DeesToast`, `DeesWindowcontrols` | | Core UI | [`DeesButton`](#deesbutton), [`DeesButtonExit`](#deesbuttonexit), [`DeesButtonGroup`](#deesbuttongroup), [`DeesBadge`](#deesbadge), [`DeesChips`](#deeschips), [`DeesHeading`](#deesheading), [`DeesHint`](#deeshint), [`DeesIcon`](#deesicon), [`DeesLabel`](#deeslabel), [`DeesPanel`](#deespanel), [`DeesSearchbar`](#deessearchbar), [`DeesSpinner`](#deesspinner), [`DeesToast`](#deestoast), [`DeesWindowcontrols`](#deeswindowcontrols) |
| Forms | `DeesForm`, `DeesInputText`, `DeesInputCheckbox`, `DeesInputDropdown`, `DeesInputRadiogroup`, `DeesInputFileupload`, `DeesInputIban`, `DeesInputPhone`, `DeesInputQuantitySelector`, `DeesInputMultitoggle`, `DeesInputTags`, `DeesInputTypelist`, `DeesInputRichtext`, `DeesInputWysiwyg`, `DeesFormSubmit` | | Forms | [`DeesForm`](#deesform), [`DeesInputText`](#deesinputtext), [`DeesInputCheckbox`](#deesinputcheckbox), [`DeesInputDropdown`](#deesinputdropdown), [`DeesInputRadiogroup`](#deesinputradiogroup), [`DeesInputFileupload`](#deesinputfileupload), [`DeesInputIban`](#deesinputiban), [`DeesInputPhone`](#deesinputphone), [`DeesInputQuantitySelector`](#deesinputquantityselector), [`DeesInputMultitoggle`](#deesinputmultitoggle), [`DeesInputTags`](#deesinputtags), [`DeesInputTypelist`](#deesinputtypelist), [`DeesInputRichtext`](#deesinputrichtext), [`DeesInputWysiwyg`](#deesinputwysiwyg), [`DeesInputDatepicker`](#deesinputdatepicker), [`DeesInputSearchselect`](#deesinputsearchselect), [`DeesFormSubmit`](#deesformsubmit) |
| Layout | `DeesAppuiBase`, `DeesAppuiMainmenu`, `DeesAppuiMainselector`, `DeesAppuiMaincontent`, `DeesAppuiAppbar`, `DeesAppuiActivitylog`, `DeesAppuiProfiledropdown`, `DeesAppuiTabs`, `DeesAppuiView`, `DeesMobileNavigation` | | Layout | [`DeesAppuiBase`](#deesappuibase), [`DeesAppuiMainmenu`](#deesappuimainmenu), [`DeesAppuiMainselector`](#deesappuimainselector), [`DeesAppuiMaincontent`](#deesappuimaincontent), [`DeesAppuiAppbar`](#deesappuiappbar), [`DeesAppuiActivitylog`](#deesappuiactivitylog), [`DeesAppuiProfiledropdown`](#deesappuiprofiledropdown), [`DeesAppuiTabs`](#deesappuitabs), [`DeesAppuiView`](#deesappuiview), [`DeesMobileNavigation`](#deesmobilenavigation), [`DeesDashboardGrid`](#deesdashboardgrid) |
| Data Display | `DeesTable`, `DeesDataviewCodebox`, `DeesDataviewStatusobject`, `DeesPdf`, `DeesStatsGrid`, `DeesPagination` | | Data Display | [`DeesTable`](#deestable), [`DeesDataviewCodebox`](#deesdataviewcodebox), [`DeesDataviewStatusobject`](#deesdataviewstatusobject), [`DeesPdf`](#deespdf), [`DeesStatsGrid`](#deesstatsgrid), [`DeesPagination`](#deespagination) |
| Visualization | `DeesChartArea`, `DeesChartLog` | | Visualization | [`DeesChartArea`](#deeschartarea), [`DeesChartLog`](#deeschartlog) |
| Dialogs & Overlays | `DeesModal`, `DeesContextmenu`, `DeesSpeechbubble`, `DeesWindowlayer` | | Dialogs & Overlays | [`DeesModal`](#deesmodal), [`DeesContextmenu`](#deescontextmenu), [`DeesSpeechbubble`](#deesspeechbubble), [`DeesWindowlayer`](#deeswindowlayer) |
| Navigation | `DeesStepper`, `DeesProgressbar`, `DeesMobileNavigation` | | Navigation | [`DeesStepper`](#deesstepper), [`DeesProgressbar`](#deesprogressbar) |
| Development | `DeesEditor`, `DeesEditorMarkdown`, `DeesEditorMarkdownoutlet`, `DeesTerminal`, `DeesUpdater` | | Development | [`DeesEditor`](#deeseditor), [`DeesEditorMarkdown`](#deeseditormarkdown), [`DeesEditorMarkdownoutlet`](#deeseditormarkdownoutlet), [`DeesTerminal`](#deesterminal), [`DeesUpdater`](#deesupdater) |
| Auth & Utilities | `DeesSimpleAppdash`, `DeesSimpleLogin` | | Auth & Utilities | [`DeesSimpleAppdash`](#deessimpleappdash), [`DeesSimpleLogin`](#deessimplelogin) |
| Shopping | [`DeesShoppingProductcard`](#deesshoppingproductcard) |
## Detailed Component Documentation ## Detailed Component Documentation
@ -70,14 +71,36 @@ Interactive chips/tags with selection capabilities.
``` ```
#### `DeesIcon` #### `DeesIcon`
Display icons from various icon sets including FontAwesome. Display icons from FontAwesome and Lucide icon libraries with library prefixes.
```typescript ```typescript
// FontAwesome icons - use 'fa:' prefix
<dees-icon <dees-icon
icon="home" // FontAwesome icon name icon="fa:check" // FontAwesome icon with fa: prefix
type="solid" // Options: solid, regular, brands iconSize="24" // Size in pixels
size="1.5rem" // Optional: custom size color="#22c55e" // Optional: custom color
></dees-icon>
// Lucide icons - use 'lucide:' prefix
<dees-icon
icon="lucide:menu" // Lucide icon with lucide: prefix
iconSize="24" // Size in pixels
color="#007bff" // Optional: custom color color="#007bff" // Optional: custom color
strokeWidth="2" // Optional: stroke width for Lucide icons
></dees-icon>
// Available FontAwesome icons include:
// fa:check, fa:bell, fa:gear, fa:trash, fa:copy, fa:paste, fa:eye, fa:eyeSlash,
// fa:plus, fa:minus, fa:circleInfo, fa:circleCheck, fa:circleXmark, fa:message,
// fa:arrowRight, fa:facebook, fa:twitter, fa:linkedin, fa:instagram, etc.
// Available Lucide icons include:
// lucide:menu, lucide:settings, lucide:home, lucide:file, lucide:folder,
// lucide:search, lucide:user, lucide:heart, lucide:star, lucide:download, etc.
// Legacy API (deprecated but still supported)
<dees-icon
iconFA="check" // Without prefix - assumes FontAwesome
></dees-icon> ></dees-icon>
``` ```
@ -431,6 +454,78 @@ Dynamic list input for managing arrays of typed values.
></dees-input-typelist> ></dees-input-typelist>
``` ```
#### `DeesInputDatepicker`
Date and time picker component with calendar interface and manual typing support.
```typescript
<dees-input-datepicker
key="eventDate"
label="Event Date"
placeholder="YYYY-MM-DD"
value="2025-01-15T14:30:00Z" // ISO string format
dateFormat="YYYY-MM-DD" // Display format (default: YYYY-MM-DD)
enableTime={true} // Enable time selection
timeFormat="24h" // Options: 24h, 12h
minuteIncrement={15} // Time step in minutes
minDate="2025-01-01" // Minimum selectable date
maxDate="2025-12-31" // Maximum selectable date
.disabledDates=${[ // Array of disabled dates
'2025-01-10',
'2025-01-11'
]}
weekStartsOn={1} // 0 = Sunday, 1 = Monday
required
@change=${handleDateChange}
></dees-input-datepicker>
```
Key Features:
- Interactive calendar popup
- Manual date typing with multiple formats
- Optional time selection
- Configurable date format
- Min/max date constraints
- Disable specific dates
- Keyboard navigation
- Today button
- Clear functionality
- 12/24 hour time formats
- Theme-aware styling
- Live parsing and validation
Manual Input Formats:
```typescript
// Date formats supported
"2023-12-20" // ISO format (YYYY-MM-DD)
"20.12.2023" // European format (DD.MM.YYYY)
"12/20/2023" // US format (MM/DD/YYYY)
// Date with time (add space and time after any date format)
"2023-12-20 14:30"
"20.12.2023 9:45"
"12/20/2023 16:00"
```
The component automatically parses and validates input as you type, updating the internal date value when a valid date is recognized.
#### `DeesInputSearchselect`
Search-enabled dropdown selection component.
```typescript
<dees-input-searchselect
key="category"
label="Select Category"
placeholder="Search categories..."
.options=${[
{ key: 'tech', label: 'Technology' },
{ key: 'health', label: 'Healthcare' },
{ key: 'finance', label: 'Finance' }
]}
required
@change=${handleCategoryChange}
></dees-input-searchselect>
```
#### `DeesInputRichtext` #### `DeesInputRichtext`
Rich text editor with formatting toolbar powered by TipTap. Rich text editor with formatting toolbar powered by TipTap.
@ -529,9 +624,9 @@ Base container component for application layout structure with integrated appbar
// Main menu configuration (left sidebar) // Main menu configuration (left sidebar)
.mainmenuTabs=${[ .mainmenuTabs=${[
{ key: 'dashboard', iconName: 'home', action: () => {} }, { key: 'dashboard', iconName: 'lucide:home', action: () => {} },
{ key: 'projects', iconName: 'folder', action: () => {} }, { key: 'projects', iconName: 'lucide:folder', action: () => {} },
{ key: 'settings', iconName: 'cog', action: () => {} } { key: 'settings', iconName: 'lucide:settings', action: () => {} }
]} ]}
.mainmenuSelectedTab=${selectedTab} .mainmenuSelectedTab=${selectedTab}
@ -545,7 +640,7 @@ Base container component for application layout structure with integrated appbar
// Main content tabs // Main content tabs
.maincontentTabs=${[ .maincontentTabs=${[
{ key: 'tab1', iconName: 'file', action: () => {} } { key: 'tab1', iconName: 'lucide:file', action: () => {} }
]} ]}
// Event handlers // Event handlers
@ -919,6 +1014,100 @@ Responsive navigation component for mobile devices.
></dees-mobile-navigation> ></dees-mobile-navigation>
``` ```
#### `DeesDashboardGrid`
Drag-and-drop grid layout system for creating customizable dashboards.
```typescript
<dees-dashboardgrid
.widgets=${[
{
id: 'widget1',
x: 0, // Grid column position
y: 0, // Grid row position
w: 4, // Width in grid units
h: 3, // Height in grid units
minW: 2, // Minimum width
minH: 2, // Minimum height
maxW: 6, // Maximum width
title: 'Sales Overview',
icon: 'fa:chart-line',
content: html`<div>Widget content here</div>`,
noMove: false, // Allow moving
noResize: false // Allow resizing
},
{
id: 'widget2',
x: 4,
y: 0,
w: 4,
h: 3,
title: 'Recent Activity',
content: html`<dees-table .data=${activityData}></dees-table>`,
autoPosition: true // Auto-find position
}
]}
columns={12} // Number of grid columns
cellHeight={80} // Height of each grid cell in pixels
cellHeightUnit="px" // Options: px, em, rem, auto
margin={10} // Gap between widgets
editable={true} // Enable drag and resize
showGridLines={false} // Show grid guidelines
enableAnimation={true} // Smooth transitions
rtl={false} // Right-to-left support
@widget-move=${handleWidgetMove}
@widget-resize=${handleWidgetResize}
></dees-dashboardgrid>
// Programmatic methods
const grid = document.querySelector('dees-dashboardgrid');
// Add a new widget
grid.addWidget({
id: 'newWidget',
x: 0,
y: 0,
w: 3,
h: 2,
content: html`<div>New widget</div>`
}, true); // true = auto-position
// Remove widget
grid.removeWidget('widget1');
// Update widget
grid.updateWidget('widget2', {
title: 'Updated Title',
w: 6
});
// Get/set layout
const layout = grid.getLayout(); // Returns position data
grid.setLayout(savedLayout); // Restore positions
// Compact widgets
grid.compact('vertical'); // Or 'horizontal'
// Lock/unlock editing
grid.lockGrid();
grid.unlockGrid();
```
Key Features:
- Drag-and-drop widget repositioning
- Resize handles on edges and corners
- Grid-based layout system
- Collision detection
- Auto-positioning for new widgets
- Configurable constraints (min/max dimensions)
- Lock individual widgets or entire grid
- Compact layout algorithm
- Save/restore layout positions
- RTL layout support
- Optional grid lines for alignment
- Smooth animations
- Responsive sizing
- Empty state display
### Data Display Components ### Data Display Components
#### `DeesTable` #### `DeesTable`
@ -1913,6 +2102,69 @@ Key Features:
- Responsive layout - Responsive layout
- Loading states - Loading states
### Shopping Components
#### `DeesShoppingProductcard`
Product card component for e-commerce applications.
```typescript
<dees-shopping-productcard
.productData=${{
name: 'Premium Headphones',
category: 'Electronics',
description: 'High-quality wireless headphones with noise cancellation',
price: 199.99,
originalPrice: 249.99, // Shows strikethrough price
currency: '$',
inStock: true,
stockText: 'In Stock', // Custom stock text
imageUrl: '/images/headphones.jpg',
iconName: 'lucide:headphones' // Fallback icon if no image
}}
quantity={1} // Current quantity
showQuantitySelector={true} // Show quantity selector
selectable={false} // Enable selection mode
selected={false} // Selection state
@quantityChange=${(e) => handleQuantityChange(e.detail)}
@selectionChange=${(e) => handleSelectionChange(e.detail)}
></dees-shopping-productcard>
```
Key Features:
- Product image with fallback icon
- Category label
- Product name and description
- Price display with original price strikethrough
- Stock status indicator
- Built-in quantity selector
- Selection mode for bulk operations
- Hover effects
- Responsive design
- Theme-aware styling
Product Data Interface:
```typescript
interface IProductData {
name: string;
category?: string;
description?: string;
price: number;
originalPrice?: number;
currency?: string;
inStock?: boolean;
stockText?: string;
imageUrl?: string;
iconName?: string;
}
```
Common Use Cases:
- Product listings
- Shopping carts
- Order summaries
- Product comparisons
- Wishlist displays
## License and Legal Information ## License and Legal Information
This repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the license file within this repository. This repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the license file within this repository.

72
test-output.log Normal file
View File

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

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

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

⚠️ Error
 Only 0 out of 1 completed!

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

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

⏱️ Performance Metrics:
 Average per test: 0ms

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,13 +1,13 @@
import { html, css, cssManager } from '@design.estate/dees-element'; import { html, css, cssManager, domtools } from '@design.estate/dees-element';
import '@design.estate/dees-wcctools/demotools'; import '@design.estate/dees-wcctools/demotools';
import './dees-panel.js'; import './dees-panel.js';
import './dees-form.js'; import './dees-form.js';
import './dees-form-submit.js'; import './dees-form-submit.js';
import './dees-input-text.js'; import './dees-input-text.js';
import './dees-icon.js'; import './dees-icon.js';
import type { DeesButton } from './dees-button.js';
export const demoFunc = () => html` export const demoFunc = () => html`
<dees-demowrapper>
<style> <style>
${css` ${css`
.demo-container { .demo-container {
@ -78,6 +78,16 @@ export const demoFunc = () => html`
</style> </style>
<div class="demo-container"> <div class="demo-container">
<dees-demowrapper .runAfterRender=${async (elementArg: HTMLElement) => {
// Log button clicks for demo purposes
const buttons = elementArg.querySelectorAll('dees-button');
buttons.forEach((button) => {
button.addEventListener('clicked', () => {
const type = button.getAttribute('type') || 'default';
console.log(`Button variant clicked: ${type}`);
});
});
}}>
<dees-panel .title=${'1. Button Variants'} .subtitle=${'Different visual styles for various use cases'}> <dees-panel .title=${'1. Button Variants'} .subtitle=${'Different visual styles for various use cases'}>
<div class="button-group"> <div class="button-group">
<dees-button type="default">Default</dees-button> <dees-button type="default">Default</dees-button>
@ -88,7 +98,18 @@ export const demoFunc = () => html`
<dees-button type="link">Link Button</dees-button> <dees-button type="link">Link Button</dees-button>
</div> </div>
</dees-panel> </dees-panel>
</dees-demowrapper>
<dees-demowrapper .runAfterRender=${async (elementArg: HTMLElement) => {
// Demonstrate size differences programmatically
const buttons = elementArg.querySelectorAll('dees-button');
buttons.forEach((button) => {
button.addEventListener('clicked', () => {
const size = button.getAttribute('size') || 'default';
console.log(`Button size: ${size}`);
});
});
}}>
<dees-panel .title=${'2. Button Sizes'} .subtitle=${'Multiple sizes for different contexts and use cases'}> <dees-panel .title=${'2. Button Sizes'} .subtitle=${'Multiple sizes for different contexts and use cases'}>
<div class="button-group"> <div class="button-group">
<dees-button size="sm">Small Button</dees-button> <dees-button size="sm">Small Button</dees-button>
@ -103,7 +124,21 @@ export const demoFunc = () => html`
<dees-button size="lg" type="outline">Large Outline</dees-button> <dees-button size="lg" type="outline">Large Outline</dees-button>
</div> </div>
</dees-panel> </dees-panel>
</dees-demowrapper>
<dees-demowrapper .runAfterRender=${async (elementArg: HTMLElement) => {
// Track icon button clicks
const iconButtons = elementArg.querySelectorAll('dees-button');
iconButtons.forEach((button) => {
button.addEventListener('clicked', () => {
const hasIcon = button.querySelector('dees-icon');
if (hasIcon) {
const iconName = hasIcon.getAttribute('iconFA') || 'unknown';
console.log(`Icon button clicked: ${iconName}`);
}
});
});
}}>
<dees-panel .title=${'3. Buttons with Icons'} .subtitle=${'Combining icons with text for enhanced visual communication'}> <dees-panel .title=${'3. Buttons with Icons'} .subtitle=${'Combining icons with text for enhanced visual communication'}>
<div class="icon-row"> <div class="icon-row">
<dees-button> <dees-button>
@ -153,7 +188,33 @@ export const demoFunc = () => html`
</dees-button> </dees-button>
</div> </div>
</dees-panel> </dees-panel>
</dees-demowrapper>
<dees-demowrapper .runAfterRender=${async (elementArg: HTMLElement) => {
// Demonstrate status changes
const pendingButton = elementArg.querySelector('dees-button[status="pending"]');
const successButton = elementArg.querySelector('dees-button[status="success"]');
const errorButton = elementArg.querySelector('dees-button[status="error"]');
// Simulate status changes
if (pendingButton) {
setTimeout(() => {
console.log('Pending button is showing loading state');
}, 1000);
}
if (successButton) {
successButton.addEventListener('clicked', () => {
console.log('Success state button clicked');
});
}
if (errorButton) {
errorButton.addEventListener('clicked', () => {
console.log('Error state button clicked');
});
}
}}>
<dees-panel .title=${'4. Button States'} .subtitle=${'Different states to indicate button status and loading conditions'}> <dees-panel .title=${'4. Button States'} .subtitle=${'Different states to indicate button status and loading conditions'}>
<div class="button-group"> <div class="button-group">
<dees-button status="normal">Normal</dees-button> <dees-button status="normal">Normal</dees-button>
@ -169,61 +230,81 @@ export const demoFunc = () => html`
<dees-button type="destructive" status="pending" size="lg">Large Loading</dees-button> <dees-button type="destructive" status="pending" size="lg">Large Loading</dees-button>
</div> </div>
</dees-panel> </dees-panel>
</dees-demowrapper>
<dees-demowrapper .runAfterRender=${async (elementArg: HTMLElement) => {
// Set up click handlers with the output element
const output = elementArg.querySelector('#click-output');
const clickMeBtn = elementArg.querySelector('dees-button:first-of-type');
const dataBtn = elementArg.querySelector('dees-button[type="secondary"]');
const asyncBtn = elementArg.querySelector('dees-button[type="destructive"]');
if (clickMeBtn && output) {
clickMeBtn.addEventListener('clicked', () => {
output.textContent = `Clicked: Default button at ${new Date().toLocaleTimeString()}`;
});
}
if (dataBtn && output) {
dataBtn.addEventListener('clicked', (e: CustomEvent) => {
output.textContent = `Clicked: Secondary button with data: ${e.detail.data}`;
});
}
if (asyncBtn && output) {
asyncBtn.addEventListener('clicked', async () => {
output.textContent = 'Processing...';
await domtools.plugins.smartdelay.delayFor(2000);
output.textContent = 'Action completed!';
});
}
}}>
<dees-panel .title=${'5. Event Handling'} .subtitle=${'Interactive examples with click event handling'}> <dees-panel .title=${'5. Event Handling'} .subtitle=${'Interactive examples with click event handling'}>
<div class="button-group"> <div class="button-group">
<dees-button <dees-button>Click Me</dees-button>
@clicked=${() => { <dees-button type="secondary" .eventDetailData=${'custom-data-123'}>
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 Click with Data
</dees-button> </dees-button>
<dees-button type="destructive">Async Action</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>
<div id="click-output" class="demo-output"> <div id="click-output" class="demo-output">
<em>Click a button to see the result...</em> <em>Click a button to see the result...</em>
</div> </div>
</dees-panel> </dees-panel>
</dees-demowrapper>
<dees-panel .title=${'6. Form Integration'} .subtitle=${'Buttons working within forms with automatic spacing'}> <dees-demowrapper .runAfterRender=${async (elementArg: HTMLElement) => {
<dees-form @formData=${(e: CustomEvent) => { // Set up form submission handling
const output = document.querySelector('#form-output'); const form = elementArg.querySelector('dees-form');
if (output) { const output = elementArg.querySelector('#form-output');
if (form && output) {
form.addEventListener('formData', (e: CustomEvent) => {
output.innerHTML = '<strong>Form submitted with data:</strong><br>' + output.innerHTML = '<strong>Form submitted with data:</strong><br>' +
JSON.stringify(e.detail.data, null, 2); JSON.stringify(e.detail.data, null, 2);
});
}
// Track non-submit button clicks
const draftBtn = elementArg.querySelector('dees-button[type="secondary"]');
const cancelBtn = elementArg.querySelector('dees-button[type="ghost"]');
if (draftBtn) {
draftBtn.addEventListener('clicked', () => {
console.log('Save Draft clicked');
});
}
if (cancelBtn) {
cancelBtn.addEventListener('clicked', () => {
console.log('Cancel clicked');
});
} }
}}> }}>
<dees-panel .title=${'6. Form Integration'} .subtitle=${'Buttons working within forms with automatic spacing'}>
<dees-form>
<dees-input-text label="Name" key="name" required></dees-input-text> <dees-input-text label="Name" key="name" required></dees-input-text>
<dees-input-text label="Email" key="email" type="email" 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-input-text label="Message" key="message" isMultiline></dees-input-text>
@ -237,7 +318,18 @@ export const demoFunc = () => html`
<em>Submit the form to see the data...</em> <em>Submit the form to see the data...</em>
</div> </div>
</dees-panel> </dees-panel>
</dees-demowrapper>
<dees-demowrapper .runAfterRender=${async (elementArg: HTMLElement) => {
// Log legacy type mappings
const buttons = elementArg.querySelectorAll('dees-button');
buttons.forEach((button) => {
const type = button.getAttribute('type');
if (type) {
console.log(`Legacy type "${type}" is supported for backward compatibility`);
}
});
}}>
<dees-panel .title=${'7. Backward Compatibility'} .subtitle=${'Old button types are automatically mapped to new variants'}> <dees-panel .title=${'7. Backward Compatibility'} .subtitle=${'Old button types are automatically mapped to new variants'}>
<div class="button-group"> <div class="button-group">
<dees-button type="normal">Normal → Default</dees-button> <dees-button type="normal">Normal → Default</dees-button>
@ -250,7 +342,35 @@ export const demoFunc = () => html`
These legacy type values are maintained for backward compatibility but we recommend using the new variant system. These legacy type values are maintained for backward compatibility but we recommend using the new variant system.
</p> </p>
</dees-panel> </dees-panel>
</dees-demowrapper>
<dees-demowrapper .runAfterRender=${async (elementArg: HTMLElement) => {
// Track action group clicks
const actionGroup = elementArg.querySelectorAll('.vertical-group')[0];
const dangerGroup = elementArg.querySelectorAll('.vertical-group')[1];
if (actionGroup) {
const buttons = actionGroup.querySelectorAll('dees-button');
buttons.forEach((button, index) => {
button.addEventListener('clicked', () => {
const action = ['Save Changes', 'Discard', 'Help'][index];
console.log(`Action group: ${action} clicked`);
});
});
}
if (dangerGroup) {
const buttons = dangerGroup.querySelectorAll('dees-button');
buttons.forEach((button, index) => {
button.addEventListener('clicked', () => {
const action = ['Delete Account', 'Archive Data', 'Not Available'][index];
if (index !== 2) { // Skip disabled button
console.log(`Danger zone: ${action} clicked`);
}
});
});
}
}}>
<dees-panel .title=${'8. Advanced Examples'} .subtitle=${'Complex button configurations and real-world use cases'}> <dees-panel .title=${'8. Advanced Examples'} .subtitle=${'Complex button configurations and real-world use cases'}>
<div class="horizontal-group"> <div class="horizontal-group">
<div class="vertical-group"> <div class="vertical-group">
@ -296,6 +416,6 @@ export const demoFunc = () => html`
</div> </div>
</div> </div>
</dees-panel> </dees-panel>
</div>
</dees-demowrapper> </dees-demowrapper>
</div>
`; `;

View File

@ -1,4 +1,3 @@
import { demoFunc } from './dees-button.demo.js';
import { import {
customElement, customElement,
html, html,
@ -12,6 +11,7 @@ import {
} from '@design.estate/dees-element'; } from '@design.estate/dees-element';
import * as domtools from '@design.estate/dees-domtools'; import * as domtools from '@design.estate/dees-domtools';
import { demoFunc } from './dees-button.demo.js';
declare global { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {

View File

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

View File

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

View File

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

View File

@ -1,41 +1,112 @@
import { html } from '@design.estate/dees-element'; import { html, cssManager } from '@design.estate/dees-element';
export const demoFunc = () => html` export const demoFunc = () => html`
<style> <style>
.demoContainer { .demoContainer {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; gap: 32px;
justify-content: center; padding: 48px;
height: 100%; background: ${cssManager.bdTheme('#f8f9fa', '#0a0a0a')};
background: #222; min-height: 100vh;
}
.section {
background: ${cssManager.bdTheme('#ffffff', '#18181b')};
border: 1px solid ${cssManager.bdTheme('#e5e7eb', '#27272a')};
border-radius: 8px;
padding: 24px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.section-title {
font-size: 18px;
font-weight: 600;
margin-bottom: 16px;
color: ${cssManager.bdTheme('#09090b', '#fafafa')};
}
.section-description {
font-size: 14px;
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
margin-bottom: 16px;
} }
</style> </style>
<div class="demoContainer"> <div class="demoContainer">
<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 <dees-chips
selectionMode="none" selectionMode="none"
.selectableChips=${[ .selectableChips=${[
{ key: 'account1', value: 'Payment Account 1' }, { key: 'status', value: 'Active' },
{ key: 'account2', value: 'PaymentAccount2' }, { key: 'tier', value: 'Premium' },
{ key: 'account3', value: 'Payment Account 3' }, { key: 'region', value: 'EU-West' },
{ key: 'type', value: 'Enterprise' },
]} ]}
></dees-chips> ></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 <dees-chips
selectionMode="single" selectionMode="single"
chipsAreRemovable chipsAreRemovable
.selectableChips=${[ .selectableChips=${[
{ key: 'account1', value: 'Payment Account 1' }, { key: 'env', value: 'Production' },
{ key: 'account2', value: 'PaymentAccount2' }, { key: 'version', value: '2.4.1' },
{ key: 'account3', value: 'Payment Account 3' }, { key: 'branch', value: 'main' },
]} { key: 'author', value: 'John Doe' },
></dees-chips>
<dees-chips
selectionMode="multiple"
.selectableChips=${[
{ key: 'account1', value: 'Payment Account 1' },
{ key: 'account2', value: 'PaymentAccount2' },
{ key: 'account3', value: 'Payment Account 3' },
]} ]}
></dees-chips> ></dees-chips>
</div> </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>
`; `;

View File

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

View File

@ -13,139 +13,203 @@ export const demoFunc = () => html`
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 20px; gap: 20px;
padding: 40px; padding: 20px;
background: #f5f5f5;
min-height: 400px; min-height: 400px;
} }
.demo-area { .demo-area {
background: white;
padding: 40px; padding: 40px;
border-radius: 8px; border-radius: 8px;
border: 1px solid #e0e0e0;
text-align: center; text-align: center;
cursor: context-menu; cursor: context-menu;
transition: background 0.2s;
}
.demo-area:hover {
background: rgba(0, 0, 0, 0.02);
} }
</style> </style>
<div class="demo-container"> <div class="demo-container">
<dees-panel heading="Basic Context Menu with Nested Submenus">
<div class="demo-area" @contextmenu=${(eventArg: MouseEvent) => { <div class="demo-area" @contextmenu=${(eventArg: MouseEvent) => {
DeesContextmenu.openContextMenuWithOptions(eventArg, [ DeesContextmenu.openContextMenuWithOptions(eventArg, [
{ {
name: 'Cut', name: 'File',
iconName: 'scissors', iconName: 'fileText',
shortcut: 'Cmd+X', action: async () => {}, // Parent items with submenus still need an action
action: async () => { submenu: [
console.log('Cut action'); { 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') },
{
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');
},
},
{ divider: true }, { 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', name: 'Edit',
iconName: 'trash2', iconName: 'edit3',
action: async () => { action: async () => {}, // Parent items with submenus still need an action
console.log('Delete 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 }, { divider: true },
{ { name: 'Find', iconName: 'search', shortcut: 'Cmd+F', action: async () => console.log('Find') },
name: 'Select All', { name: 'Replace', iconName: 'repeat', shortcut: 'Cmd+H', action: async () => console.log('Replace') },
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: 'Button Action 2', name: 'View',
iconName: 'pause', iconName: 'eye',
action: async () => { action: async () => {}, // Parent items with submenus still need an action
console.log('Button action 2'); 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') },
name: 'Disabled Action', { divider: true },
iconName: 'ban', { name: 'Full Screen', iconName: 'maximize', shortcut: 'F11', action: async () => console.log('Full screen') },
disabled: true, ]
action: async () => {
console.log('This should not run');
},
}, },
{ divider: true }, { divider: true },
{ {
name: 'Settings', name: 'Settings',
iconName: 'settings', iconName: 'settings',
action: async () => { action: async () => console.log('Settings')
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;"> <dees-panel heading="Advanced Context Menu Example">
<h4>Static Context Menu (always visible):</h4> <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 <dees-contextmenu
class="withMargin" class="withMargin"
.menuItems=${[ .menuItems=${[
{ {
name: 'New File', name: 'Project',
iconName: 'filePlus', iconName: 'folder',
shortcut: 'Cmd+N', action: async () => {}, // Parent items with submenus still need an action
action: async () => console.log('New file'), submenu: [
{ name: 'New Project', iconName: 'folderPlus', shortcut: 'Cmd+Shift+N', action: async () => console.log('New project') },
{ name: 'Open Project', iconName: 'folderOpen', shortcut: 'Cmd+Shift+O', action: async () => console.log('Open project') },
{ divider: true },
{ name: 'Recent Projects', iconName: 'clock', action: async () => {}, submenu: [
{ name: 'Project Alpha', action: async () => console.log('Open Alpha') },
{ name: 'Project Beta', action: async () => console.log('Open Beta') },
{ name: 'Project Gamma', action: async () => console.log('Open Gamma') },
]},
]
}, },
{ {
name: 'Open File', name: 'Tools',
iconName: 'folderOpen', iconName: 'tool',
shortcut: 'Cmd+O', action: async () => {}, // Parent items with submenus still need an action
action: async () => console.log('Open file'), submenu: [
}, { name: 'Terminal', iconName: 'terminal', shortcut: 'Cmd+T', action: async () => console.log('Terminal') },
{ { name: 'Console', iconName: 'monitor', shortcut: 'Cmd+K', action: async () => console.log('Console') },
name: 'Save', { divider: true },
iconName: 'save', { name: 'Extensions', iconName: 'package', action: async () => console.log('Extensions') },
shortcut: 'Cmd+S', ]
action: async () => console.log('Save'),
}, },
{ divider: true }, { divider: true },
{ {
name: 'Export', name: 'Preferences',
iconName: 'download', iconName: 'sliders',
action: async () => console.log('Export'), action: async () => console.log('Preferences'),
},
{
name: 'Import',
iconName: 'upload',
action: async () => console.log('Import'),
}, },
]} ]}
></dees-contextmenu> ></dees-contextmenu>
</div> </dees-panel>
</div> </div>
`; `;

View File

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

View File

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

View File

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

View File

@ -1,18 +1,199 @@
import { html } from '@design.estate/dees-element'; import { html, cssManager } from '@design.estate/dees-element';
export const demoFunc = () => html` <style> export const demoFunc = () => html`
<style>
.demoWrapper { .demoWrapper {
box-sizing: border-box; box-sizing: border-box;
position: absolute; position: relative;
width: 100%; width: 100%;
height: 100%; min-height: 100vh;
padding: 20px; padding: 48px;
background: none; 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> </style>
<div class="demoWrapper"> <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"> <dees-dataview-codebox proglang="typescript">
import * as text from './hello'; const hiThere = 'nice'; const myFunction = async () => { interface User {
console.log('nice one'); } 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> </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>
`

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -5,7 +5,6 @@ import './dees-form.js';
import './dees-form-submit.js'; import './dees-form-submit.js';
export const demoFunc = () => html` export const demoFunc = () => html`
<dees-demowrapper>
<style> <style>
${css` ${css`
.demo-container { .demo-container {
@ -44,6 +43,25 @@ export const demoFunc = () => html`
</style> </style>
<div class="demo-container"> <div class="demo-container">
<dees-demowrapper .runAfterRender=${async (elementArg: HTMLElement) => {
// Demonstrate programmatic interaction with basic dropdowns
const countryDropdown = elementArg.querySelector('dees-input-dropdown[label="Select Country"]');
const roleDropdown = elementArg.querySelector('dees-input-dropdown[label="Select Role"]');
// Log when country changes
if (countryDropdown) {
countryDropdown.addEventListener('selectedOption', (event: CustomEvent) => {
console.log('Country selected:', event.detail);
});
}
// Log when role changes
if (roleDropdown) {
roleDropdown.addEventListener('selectedOption', (event: CustomEvent) => {
console.log('Role selected:', event.detail);
});
}
}}>
<dees-panel .title=${'1. Basic Dropdowns'} .subtitle=${'Standard dropdown with search functionality and various options'}> <dees-panel .title=${'1. Basic Dropdowns'} .subtitle=${'Standard dropdown with search functionality and various options'}>
<dees-input-dropdown <dees-input-dropdown
.label=${'Select Country'} .label=${'Select Country'}
@ -70,7 +88,18 @@ export const demoFunc = () => html`
]} ]}
></dees-input-dropdown> ></dees-input-dropdown>
</dees-panel> </dees-panel>
</dees-demowrapper>
<dees-demowrapper .runAfterRender=${async (elementArg: HTMLElement) => {
// Demonstrate simpler dropdown without search
const priorityDropdown = elementArg.querySelector('dees-input-dropdown');
if (priorityDropdown) {
priorityDropdown.addEventListener('selectedOption', (event: CustomEvent) => {
console.log(`Priority changed to: ${event.detail.option}`);
});
}
}}>
<dees-panel .title=${'2. Without Search'} .subtitle=${'Dropdown with search functionality disabled for simpler selection'}> <dees-panel .title=${'2. Without Search'} .subtitle=${'Dropdown with search functionality disabled for simpler selection'}>
<dees-input-dropdown <dees-input-dropdown
.label=${'Priority Level'} .label=${'Priority Level'}
@ -83,7 +112,20 @@ export const demoFunc = () => html`
.selectedOption=${{ option: 'Medium', key: 'medium' }} .selectedOption=${{ option: 'Medium', key: 'medium' }}
></dees-input-dropdown> ></dees-input-dropdown>
</dees-panel> </dees-panel>
</dees-demowrapper>
<dees-demowrapper .runAfterRender=${async (elementArg: HTMLElement) => {
// Demonstrate horizontal layout with multiple dropdowns
const dropdowns = elementArg.querySelectorAll('dees-input-dropdown');
// Log all changes from horizontal dropdowns
dropdowns.forEach((dropdown) => {
dropdown.addEventListener('selectedOption', (event: CustomEvent) => {
const label = dropdown.getAttribute('label');
console.log(`${label}: ${event.detail.option}`);
});
});
}}>
<dees-panel .title=${'3. Horizontal Layout'} .subtitle=${'Multiple dropdowns in a horizontal layout for compact forms'}> <dees-panel .title=${'3. Horizontal Layout'} .subtitle=${'Multiple dropdowns in a horizontal layout for compact forms'}>
<div class="horizontal-group"> <div class="horizontal-group">
<dees-input-dropdown <dees-input-dropdown
@ -120,7 +162,19 @@ export const demoFunc = () => html`
></dees-input-dropdown> ></dees-input-dropdown>
</div> </div>
</dees-panel> </dees-panel>
</dees-demowrapper>
<dees-demowrapper .runAfterRender=${async (elementArg: HTMLElement) => {
// Demonstrate state handling
const requiredDropdown = elementArg.querySelector('dees-input-dropdown[required]');
if (requiredDropdown) {
// Show validation state changes
requiredDropdown.addEventListener('blur', () => {
console.log('Required dropdown lost focus');
});
}
}}>
<dees-panel .title=${'4. States'} .subtitle=${'Different states and configurations'}> <dees-panel .title=${'4. States'} .subtitle=${'Different states and configurations'}>
<dees-input-dropdown <dees-input-dropdown
.label=${'Required Field'} .label=${'Required Field'}
@ -141,11 +195,25 @@ export const demoFunc = () => html`
.selectedOption=${{ option: 'Cannot Select', key: 'disabled' }} .selectedOption=${{ option: 'Cannot Select', key: 'disabled' }}
></dees-input-dropdown> ></dees-input-dropdown>
</dees-panel> </dees-panel>
</dees-demowrapper>
<div class="spacer"> <div class="spacer">
(Spacer to test dropdown positioning) (Spacer to test dropdown positioning)
</div> </div>
<dees-demowrapper .runAfterRender=${async (elementArg: HTMLElement) => {
// This dropdown demonstrates automatic positioning
const dropdown = elementArg.querySelector('dees-input-dropdown');
if (dropdown) {
dropdown.addEventListener('selectedOption', (event: CustomEvent) => {
console.log('Bottom dropdown selected:', event.detail);
});
// Note: The dropdown automatically detects available space
// and opens upward when near the bottom of the viewport
}
}}>
<dees-panel .title=${'5. Bottom Positioning'} .subtitle=${'Dropdown that opens upward when near bottom of viewport'}> <dees-panel .title=${'5. Bottom Positioning'} .subtitle=${'Dropdown that opens upward when near bottom of viewport'}>
<dees-input-dropdown <dees-input-dropdown
.label=${'Opens Upward'} .label=${'Opens Upward'}
@ -158,7 +226,30 @@ export const demoFunc = () => html`
]} ]}
></dees-input-dropdown> ></dees-input-dropdown>
</dees-panel> </dees-panel>
</dees-demowrapper>
<dees-demowrapper .runAfterRender=${async (elementArg: HTMLElement) => {
// Setup the interactive payload display
const dropdown = elementArg.querySelector('dees-input-dropdown');
const output = elementArg.querySelector('#selection-output');
if (dropdown && output) {
// Initialize output
output.innerHTML = '<em>Select a product to see details...</em>';
// Handle dropdown changes
dropdown.addEventListener('change', (event: CustomEvent) => {
if (event.detail.value) {
output.innerHTML = `
<strong>Selected:</strong> ${event.detail.value.option}<br>
<strong>Key:</strong> ${event.detail.value.key}<br>
<strong>Price:</strong> $${event.detail.value.payload?.price || 'N/A'}<br>
<strong>Features:</strong> ${event.detail.value.payload?.features?.join(', ') || 'N/A'}
`;
}
});
}
}}>
<dees-panel .title=${'6. Event Handling & Payload'} .subtitle=${'Dropdown with payload data and change event handling'}> <dees-panel .title=${'6. Event Handling & Payload'} .subtitle=${'Dropdown with payload data and change event handling'}>
<dees-input-dropdown <dees-input-dropdown
.label=${'Select Product'} .label=${'Select Product'}
@ -167,24 +258,35 @@ export const demoFunc = () => html`
{ option: 'Pro Plan', key: 'pro', payload: { price: 19.99, features: ['Feature A', 'Feature B'] } }, { option: 'Pro Plan', key: 'pro', payload: { price: 19.99, features: ['Feature A', 'Feature B'] } },
{ option: 'Enterprise Plan', key: 'enterprise', payload: { price: 49.99, features: ['Feature A', 'Feature B', 'Feature C'] } } { option: 'Enterprise Plan', key: 'enterprise', payload: { price: 49.99, features: ['Feature A', 'Feature B', 'Feature C'] } }
]} ]}
@change=${(e: CustomEvent) => {
const output = document.querySelector('#selection-output');
if (output && e.detail.value) {
output.innerHTML = `
<strong>Selected:</strong> ${e.detail.value.option}<br>
<strong>Key:</strong> ${e.detail.value.key}<br>
<strong>Price:</strong> $${e.detail.value.payload?.price || 'N/A'}<br>
<strong>Features:</strong> ${e.detail.value.payload?.features?.join(', ') || 'N/A'}
`;
}
}}
></dees-input-dropdown> ></dees-input-dropdown>
<div id="selection-output" style="margin-top: 16px; padding: 12px; background: rgba(0, 105, 242, 0.1); border-radius: 4px; font-size: 14px;"> <div id="selection-output" style="margin-top: 16px; padding: 12px; background: rgba(0, 105, 242, 0.1); border-radius: 4px; font-size: 14px;"></div>
<em>Select a product to see details...</em>
</div>
</dees-panel> </dees-panel>
</dees-demowrapper>
<dees-demowrapper .runAfterRender=${async (elementArg: HTMLElement) => {
// Demonstrate form integration and validation
const form = elementArg.querySelector('dees-form');
const projectTypeDropdown = elementArg.querySelector('dees-input-dropdown[key="projectType"]');
const frameworkDropdown = elementArg.querySelector('dees-input-dropdown[key="framework"]');
if (form) {
form.addEventListener('formData', (event: CustomEvent) => {
console.log('Form submitted with data:', event.detail.data);
});
}
if (projectTypeDropdown && frameworkDropdown) {
// Filter frameworks based on project type
projectTypeDropdown.addEventListener('selectedOption', (event: CustomEvent) => {
const selectedType = event.detail.key;
console.log(`Project type changed to: ${selectedType}`);
// In a real app, you could filter the framework options based on project type
// For demo purposes, we just log the change
});
}
}}>
<dees-panel .title=${'7. Form Integration'} .subtitle=${'Dropdown working within a form with validation'}> <dees-panel .title=${'7. Form Integration'} .subtitle=${'Dropdown working within a form with validation'}>
<dees-form> <dees-form>
<dees-input-dropdown <dees-input-dropdown
@ -216,6 +318,6 @@ export const demoFunc = () => html`
<dees-form-submit .text=${'Create Project'}></dees-form-submit> <dees-form-submit .text=${'Create Project'}></dees-form-submit>
</dees-form> </dees-form>
</dees-panel> </dees-panel>
</div>
</dees-demowrapper> </dees-demowrapper>
</div>
` `

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -53,7 +53,7 @@ export class DeesInputTags extends DeesInputBase<DeesInputTags> {
css` css`
:host { :host {
display: block; display: block;
font-family: 'Geist Sans', sans-serif; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
} }
.input-wrapper { .input-wrapper {
@ -64,44 +64,51 @@ export class DeesInputTags extends DeesInputBase<DeesInputTags> {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
align-items: center; align-items: center;
gap: 8px; gap: 6px;
padding: 8px; padding: 6px 10px;
min-height: 40px; min-height: 40px;
background: ${cssManager.bdTheme('#fafafa', '#222222')}; background: transparent;
border: 1px solid ${cssManager.bdTheme('#e0e0e0', '#333333')}; border: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
border-radius: 8px; border-radius: 6px;
transition: all 0.2s ease; transition: all 0.15s ease;
cursor: text; 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 { .tags-container:focus-within {
border-color: ${cssManager.bdTheme('#0069f2', '#0084ff')}; border-color: ${cssManager.bdTheme('hsl(222.2 47.4% 51.2%)', 'hsl(217.2 91.2% 59.8%)')};
box-shadow: 0 0 0 2px ${cssManager.bdTheme('rgba(0, 105, 242, 0.1)', 'rgba(0, 132, 255, 0.2)')}; 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 { .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; cursor: not-allowed;
opacity: 0.6; opacity: 0.5;
} }
.tag { .tag {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
gap: 4px; gap: 4px;
padding: 4px 8px; padding: 2px 8px;
background: ${cssManager.bdTheme('#e3f2fd', '#1e3a5f')}; background: ${cssManager.bdTheme('hsl(215 20.2% 65.1% / 0.2)', 'hsl(215 20.2% 35.1% / 0.2)')};
color: ${cssManager.bdTheme('#1976d2', '#90caf9')}; color: ${cssManager.bdTheme('hsl(215.3 25% 26.7%)', 'hsl(217.9 10.6% 74.9%)')};
border-radius: 16px; border: 1px solid ${cssManager.bdTheme('hsl(215 20.2% 65.1% / 0.3)', 'hsl(215 20.2% 35.1% / 0.3)')};
font-size: 14px; border-radius: 4px;
line-height: 1.2; font-size: 13px;
font-weight: 500;
line-height: 18px;
user-select: none; user-select: none;
animation: tagAppear 0.2s ease; animation: tagAppear 0.15s cubic-bezier(0.4, 0, 0.2, 1);
} }
@keyframes tagAppear { @keyframes tagAppear {
from { from {
transform: scale(0.8); transform: scale(0.95);
opacity: 0; opacity: 0;
} }
to { to {
@ -114,21 +121,23 @@ export class DeesInputTags extends DeesInputBase<DeesInputTags> {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
width: 16px; width: 14px;
height: 16px; height: 14px;
border-radius: 50%;
cursor: pointer;
transition: all 0.2s ease;
margin-left: 2px; 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 { .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 { .tag-remove dees-icon {
width: 12px; width: 10px;
height: 12px; height: 10px;
} }
.tag-input { .tag-input {
@ -139,12 +148,13 @@ export class DeesInputTags extends DeesInputBase<DeesInputTags> {
outline: none; outline: none;
font-size: 14px; font-size: 14px;
font-family: inherit; font-family: inherit;
color: ${cssManager.bdTheme('#333', '#fff')}; color: ${cssManager.bdTheme('hsl(0 0% 9%)', 'hsl(0 0% 95%)')};
padding: 4px; padding: 2px 4px;
line-height: 20px;
} }
.tag-input::placeholder { .tag-input::placeholder {
color: ${cssManager.bdTheme('#999', '#666')}; color: ${cssManager.bdTheme('hsl(0 0% 63.9%)', 'hsl(0 0% 45.1%)')};
} }
.tag-input:disabled { .tag-input:disabled {
@ -162,44 +172,64 @@ export class DeesInputTags extends DeesInputBase<DeesInputTags> {
left: 0; left: 0;
right: 0; right: 0;
margin-top: 4px; margin-top: 4px;
background: ${cssManager.bdTheme('#ffffff', '#222222')}; background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(0 0% 3.9%)')};
border: 1px solid ${cssManager.bdTheme('#e0e0e0', '#333333')}; border: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
border-radius: 8px; border-radius: 6px;
box-shadow: 0 4px 12px ${cssManager.bdTheme('rgba(0, 0, 0, 0.1)', 'rgba(0, 0, 0, 0.3)')}; 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; max-height: 200px;
overflow-y: auto; overflow-y: auto;
z-index: 1000; z-index: 1000;
} }
.suggestion { .suggestion {
padding: 8px 12px; padding: 6px 10px;
cursor: pointer; cursor: pointer;
transition: all 0.2s ease; transition: all 0.15s ease;
color: ${cssManager.bdTheme('#333', '#fff')}; font-size: 14px;
color: ${cssManager.bdTheme('hsl(0 0% 15%)', 'hsl(0 0% 90%)')};
} }
.suggestion:hover, .suggestion:hover {
.suggestion.highlighted { background: ${cssManager.bdTheme('hsl(0 0% 95.1%)', 'hsl(0 0% 14.9%)')};
background: ${cssManager.bdTheme('#f5f5f5', '#333333')};
} }
.suggestion.highlighted { .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 styles */
.validation-message { .validation-message {
color: #d32f2f; color: ${cssManager.bdTheme('hsl(0 72.2% 50.6%)', 'hsl(0 62.8% 30.6%)')};
font-size: 12px; font-size: 13px;
margin-top: 4px; margin-top: 6px;
min-height: 16px; line-height: 1.5;
} }
/* Description styles */ /* Description styles */
.description { .description {
color: ${cssManager.bdTheme('#666', '#999')}; color: ${cssManager.bdTheme('hsl(215.4 16.3% 56.9%)', 'hsl(215 20.2% 55.1%)')};
font-size: 12px; font-size: 13px;
margin-top: 4px; 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%)')};
} }
`, `,
]; ];

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,7 @@
import * as colors from './00colors.js'; import * as colors from './00colors.js';
import * as plugins from './00plugins.js'; import * as plugins from './00plugins.js';
import { zIndexLayers, zIndexRegistry } from './00zindex.js'; import { zIndexLayers, zIndexRegistry } from './00zindex.js';
import { cssGeistFontFamily } from './00fonts.js';
import { demoFunc } from './dees-modal.demo.js'; import { demoFunc } from './dees-modal.demo.js';
import { import {
@ -117,7 +118,7 @@ export class DeesModal extends DeesElement {
cssManager.defaultStyles, cssManager.defaultStyles,
css` css`
:host { :host {
font-family: 'Geist Sans', sans-serif; font-family: ${cssGeistFontFamily};
color: ${cssManager.bdTheme('#333', '#fff')}; color: ${cssManager.bdTheme('#333', '#fff')};
will-change: transform; will-change: transform;
} }
@ -138,12 +139,12 @@ export class DeesModal extends DeesElement {
opacity: 0; opacity: 0;
min-height: 120px; min-height: 120px;
max-height: calc(100vh - 40px); max-height: calc(100vh - 40px);
background: ${cssManager.bdTheme('#ffffff', '#111')}; background: ${cssManager.bdTheme('#ffffff', '#09090b')};
border-radius: 8px; border-radius: 6px;
border: 1px solid ${cssManager.bdTheme('#e0e0e0', '#333')}; border: 1px solid ${cssManager.bdTheme('#e5e7eb', '#27272a')};
transition: all 0.2s; transition: all 0.2s ease;
overflow: hidden; overflow: hidden;
box-shadow: ${cssManager.bdTheme('0px 2px 10px rgba(0, 0, 0, 0.1)', '0px 2px 5px rgba(0, 0, 0, 0.5)')}; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1), 0 1px 2px rgba(0, 0, 0, 0.06);
margin: 20px; margin: 20px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -192,6 +193,7 @@ export class DeesModal extends DeesElement {
max-height: 100vh !important; max-height: 100vh !important;
margin: 0; margin: 0;
border-radius: 0; border-radius: 0;
border: none;
} }
} }
@ -208,12 +210,12 @@ export class DeesModal extends DeesElement {
.modal .heading { .modal .heading {
height: 40px; height: 40px;
min-height: 40px; min-height: 40px;
font-family: 'Geist Sans', sans-serif; font-family: ${cssGeistFontFamily};
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
padding: 0 12px; padding: 0 12px;
border-bottom: 1px solid ${cssManager.bdTheme('#e0e0e0', '#333')}; border-bottom: 1px solid ${cssManager.bdTheme('#e5e7eb', '#27272a')};
position: relative; position: relative;
flex-shrink: 0; flex-shrink: 0;
} }
@ -231,23 +233,23 @@ export class DeesModal extends DeesElement {
.modal .heading .header-button { .modal .heading .header-button {
width: 28px; width: 28px;
height: 28px; height: 28px;
border-radius: 6px; border-radius: 4px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
cursor: pointer; cursor: pointer;
transition: all 0.2s; transition: all 0.15s ease;
background: transparent; background: transparent;
color: ${cssManager.bdTheme('#666', '#999')}; color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
} }
.modal .heading .header-button:hover { .modal .heading .header-button:hover {
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.08)', 'rgba(255, 255, 255, 0.08)')}; background: ${cssManager.bdTheme('#f4f4f5', '#27272a')};
color: ${cssManager.bdTheme('#333', '#fff')}; color: ${cssManager.bdTheme('#09090b', '#fafafa')};
} }
.modal .heading .header-button:active { .modal .heading .header-button:active {
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.12)', 'rgba(255, 255, 255, 0.12)')}; background: ${cssManager.bdTheme('#e5e7eb', '#3f3f46')};
} }
.modal .heading .header-button dees-icon { .modal .heading .header-button dees-icon {
@ -263,6 +265,7 @@ export class DeesModal extends DeesElement {
font-size: 14px; font-size: 14px;
line-height: 40px; line-height: 40px;
padding: 0 40px; padding: 0 40px;
color: ${cssManager.bdTheme('#09090b', '#fafafa')};
} }
.modal .content { .modal .content {
@ -275,7 +278,7 @@ export class DeesModal extends DeesElement {
.modal .bottomButtons { .modal .bottomButtons {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
border-top: 1px solid ${cssManager.bdTheme('#e0e0e0', '#333')}; border-top: 1px solid ${cssManager.bdTheme('#e5e7eb', '#27272a')};
justify-content: flex-end; justify-content: flex-end;
gap: 8px; gap: 8px;
padding: 8px; padding: 8px;
@ -284,39 +287,43 @@ export class DeesModal extends DeesElement {
.modal .bottomButtons .bottomButton { .modal .bottomButtons .bottomButton {
padding: 8px 16px; padding: 8px 16px;
border-radius: 6px; border-radius: 4px;
line-height: 16px; line-height: 16px;
text-align: center; text-align: center;
font-size: 14px; font-size: 14px;
font-weight: 500; font-weight: 500;
cursor: pointer; cursor: pointer;
user-select: none; user-select: none;
transition: all 0.2s; transition: all 0.15s ease;
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.05)', 'rgba(255, 255, 255, 0.05)')}; background: ${cssManager.bdTheme('#ffffff', '#27272a')};
border: 1px solid ${cssManager.bdTheme('#e5e7eb', '#3f3f46')};
color: ${cssManager.bdTheme('#09090b', '#fafafa')};
white-space: nowrap; white-space: nowrap;
} }
.modal .bottomButtons .bottomButton:hover { .modal .bottomButtons .bottomButton:hover {
background: ${cssManager.bdTheme(colors.bright.blue, colors.dark.blue)}; background: ${cssManager.bdTheme('#f4f4f5', '#3f3f46')};
color: #ffffff; border-color: ${cssManager.bdTheme('#d1d5db', '#52525b')};
} }
.modal .bottomButtons .bottomButton:active { .modal .bottomButtons .bottomButton:active {
background: ${cssManager.bdTheme(colors.bright.blueActive, colors.dark.blueActive)}; background: ${cssManager.bdTheme('#e5e7eb', '#52525b')};
color: #ffffff;
} }
.modal .bottomButtons .bottomButton:last-child { .modal .bottomButtons .bottomButton:last-child {
border-right: none; border-right: none;
} }
.modal .bottomButtons .bottomButton.primary { .modal .bottomButtons .bottomButton.primary {
background: ${cssManager.bdTheme(colors.bright.blue, colors.dark.blue)}; background: ${cssManager.bdTheme('#3b82f6', '#3b82f6')};
border-color: ${cssManager.bdTheme('#3b82f6', '#3b82f6')};
color: #ffffff; color: #ffffff;
} }
.modal .bottomButtons .bottomButton.primary:hover { .modal .bottomButtons .bottomButton.primary:hover {
background: ${cssManager.bdTheme(colors.bright.blueActive, colors.dark.blueActive)}; background: ${cssManager.bdTheme('#2563eb', '#2563eb')};
border-color: ${cssManager.bdTheme('#2563eb', '#2563eb')};
} }
.modal .bottomButtons .bottomButton.primary:active { .modal .bottomButtons .bottomButton.primary:active {
background: ${cssManager.bdTheme(colors.bright.blueMuted, colors.dark.blueMuted)}; background: ${cssManager.bdTheme('#1d4ed8', '#1d4ed8')};
border-color: ${cssManager.bdTheme('#1d4ed8', '#1d4ed8')};
} }
`, `,
]; ];

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -381,8 +381,9 @@ export const demoFunc = () => {
name: 'Increment', name: 'Increment',
iconName: 'lucide:plus', iconName: 'lucide:plus',
action: async () => { action: async () => {
const grid = document.querySelector('#interactive-grid'); const grid = document.querySelector('#interactive-grid') as any;
const tile = grid.tiles.find(t => t.id === 'counter'); if (!grid) return;
const tile = grid.tiles.find((t: any) => t.id === 'counter');
tile.value = typeof tile.value === 'number' ? tile.value + 1 : 1; tile.value = typeof tile.value === 'number' ? tile.value + 1 : 1;
grid.tiles = [...grid.tiles]; grid.tiles = [...grid.tiles];
} }
@ -391,8 +392,9 @@ export const demoFunc = () => {
name: 'Reset', name: 'Reset',
iconName: 'lucide:rotate-ccw', iconName: 'lucide:rotate-ccw',
action: async () => { action: async () => {
const grid = document.querySelector('#interactive-grid'); const grid = document.querySelector('#interactive-grid') as any;
const tile = grid.tiles.find(t => t.id === 'counter'); if (!grid) return;
const tile = grid.tiles.find((t: any) => t.id === 'counter');
tile.value = 0; tile.value = 0;
grid.tiles = [...grid.tiles]; grid.tiles = [...grid.tiles];
} }
@ -406,9 +408,9 @@ export const demoFunc = () => {
iconName: 'lucide:play', iconName: 'lucide:play',
action: async function() { action: async function() {
// Toggle live updates // Toggle live updates
if (!window.liveUpdateInterval) { if (!(window as any).liveUpdateInterval) {
window.liveUpdateInterval = setInterval(() => { (window as any).liveUpdateInterval = setInterval(() => {
const grid = document.querySelector('#interactive-grid'); const grid = document.querySelector('#interactive-grid') as any;
if (grid) { if (grid) {
const tiles = [...grid.tiles]; const tiles = [...grid.tiles];
@ -433,8 +435,8 @@ export const demoFunc = () => {
this.name = 'Stop Live Updates'; this.name = 'Stop Live Updates';
this.iconName = 'lucide:pause'; this.iconName = 'lucide:pause';
} else { } else {
clearInterval(window.liveUpdateInterval); clearInterval((window as any).liveUpdateInterval);
window.liveUpdateInterval = null; (window as any).liveUpdateInterval = null;
this.name = 'Start Live Updates'; this.name = 'Start Live Updates';
this.iconName = 'lucide:play'; this.iconName = 'lucide:play';
} }
@ -506,8 +508,8 @@ html\`
<script> <script>
// Cleanup live updates on page unload // Cleanup live updates on page unload
window.addEventListener('beforeunload', () => { window.addEventListener('beforeunload', () => {
if (window.liveUpdateInterval) { if ((window as any).liveUpdateInterval) {
clearInterval(window.liveUpdateInterval); clearInterval((window as any).liveUpdateInterval);
} }
}); });
</script> </script>

View File

@ -1,5 +1,6 @@
import { demoFunc } from './dees-statsgrid.demo.js'; import { demoFunc } from './dees-statsgrid.demo.js';
import * as plugins from './00plugins.js'; import * as plugins from './00plugins.js';
import { cssGeistFontFamily } from './00fonts.js';
import { import {
customElement, customElement,
html, html,
@ -79,6 +80,7 @@ export class DeesStatsGrid extends DeesElement {
:host { :host {
display: block; display: block;
width: 100%; width: 100%;
font-family: ${cssGeistFontFamily};
} }
/* CSS Variables for consistent spacing and sizing */ /* CSS Variables for consistent spacing and sizing */
@ -194,7 +196,7 @@ export class DeesStatsGrid extends DeesElement {
font-size: var(--value-font-size); font-size: var(--value-font-size);
font-weight: 600; font-weight: 600;
color: ${cssManager.bdTheme('hsl(215.3 25% 8.8%)', 'hsl(210 40% 98%)')}; color: ${cssManager.bdTheme('hsl(215.3 25% 8.8%)', 'hsl(210 40% 98%)')};
line-height: 1; line-height: 1.1;
display: flex; display: flex;
align-items: baseline; align-items: baseline;
gap: 4px; gap: 4px;
@ -226,8 +228,9 @@ export class DeesStatsGrid extends DeesElement {
.gauge-container { .gauge-container {
width: 140px; width: 140px;
height: 70px; height: 80px;
position: relative; position: relative;
margin-top: -10px;
} }
.gauge-svg { .gauge-svg {
@ -250,10 +253,10 @@ export class DeesStatsGrid extends DeesElement {
.gauge-text { .gauge-text {
fill: ${cssManager.bdTheme('hsl(215.3 25% 8.8%)', 'hsl(210 40% 98%)')}; fill: ${cssManager.bdTheme('hsl(215.3 25% 8.8%)', 'hsl(210 40% 98%)')};
font-family: ${cssGeistFontFamily};
font-size: var(--value-font-size); font-size: var(--value-font-size);
font-weight: 600; font-weight: 600;
text-anchor: middle; text-anchor: middle;
dominant-baseline: alphabetic;
letter-spacing: -0.025em; letter-spacing: -0.025em;
} }
@ -261,6 +264,7 @@ export class DeesStatsGrid extends DeesElement {
font-size: var(--unit-font-size); font-size: var(--unit-font-size);
fill: ${cssManager.bdTheme('hsl(215.4 16.3% 46.9%)', 'hsl(215 20.2% 65.1%)')}; fill: ${cssManager.bdTheme('hsl(215.4 16.3% 46.9%)', 'hsl(215 20.2% 65.1%)')};
font-weight: 400; font-weight: 400;
font-family: ${cssGeistFontFamily};
} }
/* Percentage Styles */ /* Percentage Styles */
@ -273,6 +277,7 @@ export class DeesStatsGrid extends DeesElement {
font-size: var(--value-font-size); font-size: var(--value-font-size);
font-weight: 600; font-weight: 600;
color: ${cssManager.bdTheme('hsl(215.3 25% 8.8%)', 'hsl(210 40% 98%)')}; color: ${cssManager.bdTheme('hsl(215.3 25% 8.8%)', 'hsl(210 40% 98%)')};
line-height: 1.1;
letter-spacing: -0.025em; letter-spacing: -0.025em;
margin-bottom: 8px; margin-bottom: 8px;
} }
@ -310,6 +315,7 @@ export class DeesStatsGrid extends DeesElement {
font-size: var(--value-font-size); font-size: var(--value-font-size);
font-weight: 600; font-weight: 600;
color: ${cssManager.bdTheme('hsl(215.3 25% 8.8%)', 'hsl(210 40% 98%)')}; color: ${cssManager.bdTheme('hsl(215.3 25% 8.8%)', 'hsl(210 40% 98%)')};
line-height: 1.1;
letter-spacing: -0.025em; letter-spacing: -0.025em;
} }
@ -357,6 +363,7 @@ export class DeesStatsGrid extends DeesElement {
font-size: var(--value-font-size); font-size: var(--value-font-size);
font-weight: 600; font-weight: 600;
color: ${cssManager.bdTheme('hsl(215.3 25% 8.8%)', 'hsl(210 40% 98%)')}; color: ${cssManager.bdTheme('hsl(215.3 25% 8.8%)', 'hsl(210 40% 98%)')};
line-height: 1.1;
letter-spacing: -0.025em; letter-spacing: -0.025em;
} }
@ -384,7 +391,7 @@ export class DeesStatsGrid extends DeesElement {
type="outline" type="outline"
size="sm" size="sm"
> >
${action.iconName ? html`<dees-icon .iconFA=${action.iconName} size="small"></dees-icon>` : ''} ${action.iconName ? html`<dees-icon .icon=${action.iconName} size="small"></dees-icon>` : ''}
${action.name} ${action.name}
</dees-button> </dees-button>
`)} `)}
@ -400,7 +407,7 @@ export class DeesStatsGrid extends DeesElement {
<dees-contextmenu <dees-contextmenu
.x=${this.contextMenuPosition.x} .x=${this.contextMenuPosition.x}
.y=${this.contextMenuPosition.y} .y=${this.contextMenuPosition.y}
.menuItems=${this.contextMenuActions} .menuItems=${this.contextMenuActions as any}
@clicked=${() => this.contextMenuVisible = false} @clicked=${() => this.contextMenuVisible = false}
></dees-contextmenu> ></dees-contextmenu>
` : ''} ` : ''}
@ -420,7 +427,7 @@ export class DeesStatsGrid extends DeesElement {
<div class="tile-header"> <div class="tile-header">
<h3 class="tile-title">${tile.title}</h3> <h3 class="tile-title">${tile.title}</h3>
${tile.icon ? html` ${tile.icon ? html`
<dees-icon class="tile-icon" .iconFA=${tile.icon} size="small"></dees-icon> <dees-icon class="tile-icon" .icon=${tile.icon} size="small"></dees-icon>
` : ''} ` : ''}
</div> </div>
@ -473,10 +480,10 @@ export class DeesStatsGrid extends DeesElement {
// SVG dimensions and calculations // SVG dimensions and calculations
const width = 140; const width = 140;
const height = 70; const height = 80;
const strokeWidth = 8; const strokeWidth = 8;
const padding = strokeWidth / 2 + 2; const padding = strokeWidth / 2 + 2;
const radius = 40; const radius = 48;
const centerX = width / 2; const centerX = width / 2;
const centerY = height - padding; const centerY = height - padding;
@ -520,8 +527,8 @@ export class DeesStatsGrid extends DeesElement {
stroke-dashoffset="${strokeDashoffset}" stroke-dashoffset="${strokeDashoffset}"
/> />
<!-- Value text --> <!-- Value text -->
<text class="gauge-text" x="${centerX}" y="${centerY}"> <text class="gauge-text" x="${centerX}" y="${centerY - 8}" dominant-baseline="middle">
<tspan>${value}</tspan>${tile.unit ? html`<tspan class="gauge-unit" dx="4">${tile.unit}</tspan>` : ''} <tspan>${value}</tspan>${tile.unit ? html`<tspan class="gauge-unit" dx="2" dy="0">${tile.unit}</tspan>` : ''}
</text> </text>
</svg> </svg>
</div> </div>

View File

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

View File

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

View File

@ -3,6 +3,7 @@ import { customElement, DeesElement, type TemplateResult, html, css, property, c
import * as domtools from '@design.estate/dees-domtools'; import * as domtools from '@design.estate/dees-domtools';
import { zIndexLayers } from './00zindex.js'; import { zIndexLayers } from './00zindex.js';
import { demoFunc } from './dees-toast.demo.js'; import { demoFunc } from './dees-toast.demo.js';
import { cssGeistFontFamily } from './00fonts.js';
declare global { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {
@ -152,7 +153,7 @@ export class DeesToast extends DeesElement {
:host { :host {
display: block; display: block;
pointer-events: auto; pointer-events: auto;
font-family: 'Geist Sans', sans-serif; font-family: ${cssGeistFontFamily};
opacity: 0; opacity: 0;
transform: translateY(-10px); transform: translateY(-10px);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -8,6 +8,7 @@ import {
state, state,
} from '@design.estate/dees-element'; } from '@design.estate/dees-element';
import { zIndexRegistry } from '../00zindex.js'; import { zIndexRegistry } from '../00zindex.js';
import '../dees-icon.js';
import { type ISlashMenuItem } from './wysiwyg.types.js'; import { type ISlashMenuItem } from './wysiwyg.types.js';
import { WysiwygShortcuts } from './wysiwyg.shortcuts.js'; import { WysiwygShortcuts } from './wysiwyg.shortcuts.js';
@ -61,10 +62,10 @@ export class DeesSlashMenu extends DeesElement {
.slash-menu { .slash-menu {
position: fixed; position: fixed;
background: ${cssManager.bdTheme('#ffffff', '#262626')}; background: ${cssManager.bdTheme('#ffffff', '#09090b')};
border: 1px solid ${cssManager.bdTheme('#e0e0e0', '#404040')}; border: 1px solid ${cssManager.bdTheme('#e5e7eb', '#27272a')};
border-radius: 8px; border-radius: 4px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12); box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1), 0 1px 2px rgba(0, 0, 0, 0.06);
padding: 4px; padding: 4px;
min-width: 220px; min-width: 220px;
max-height: 300px; max-height: 300px;
@ -77,7 +78,7 @@ export class DeesSlashMenu extends DeesElement {
@keyframes fadeInScale { @keyframes fadeInScale {
from { from {
opacity: 0; opacity: 0;
transform: scale(0.95) translateY(-10px); transform: scale(0.98) translateY(-2px);
} }
to { to {
opacity: 1; opacity: 1;
@ -86,37 +87,35 @@ export class DeesSlashMenu extends DeesElement {
} }
.slash-menu-item { .slash-menu-item {
padding: 10px 12px; padding: 8px 10px;
cursor: pointer; cursor: pointer;
transition: all 0.15s ease; transition: all 0.15s ease;
display: flex; display: flex;
align-items: center; align-items: center;
gap: 12px; gap: 12px;
border-radius: 4px; border-radius: 3px;
color: ${cssManager.bdTheme('#000000', '#e0e0e0')}; color: ${cssManager.bdTheme('#09090b', '#fafafa')};
font-size: 14px; font-size: 14px;
} }
.slash-menu-item:hover, .slash-menu-item:hover,
.slash-menu-item.selected { .slash-menu-item.selected {
background: ${cssManager.bdTheme('#f0f0f0', '#333333')}; background: ${cssManager.bdTheme('#f4f4f5', '#27272a')};
color: ${cssManager.bdTheme('#000000', '#ffffff')}; color: ${cssManager.bdTheme('#09090b', '#fafafa')};
} }
.slash-menu-item .icon { .slash-menu-item .icon {
width: 24px; width: 20px;
height: 24px; height: 20px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
font-size: 16px; color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
color: ${cssManager.bdTheme('#666', '#999')};
font-weight: 600;
} }
.slash-menu-item:hover .icon, .slash-menu-item:hover .icon,
.slash-menu-item.selected .icon { .slash-menu-item.selected .icon {
color: ${cssManager.bdTheme('#0066cc', '#4d94ff')}; color: ${cssManager.bdTheme('#3b82f6', '#3b82f6')};
} }
`, `,
]; ];
@ -142,7 +141,7 @@ export class DeesSlashMenu extends DeesElement {
data-item-type="${item.type}" data-item-type="${item.type}"
data-item-index="${index}" data-item-index="${index}"
> >
<span class="icon">${item.icon}</span> <dees-icon class="icon" .icon="${item.icon}" iconSize="16"></dees-icon>
<span>${item.label}</span> <span>${item.label}</span>
</div> </div>
`)} `)}

View File

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

View File

@ -11,6 +11,8 @@ export class WysiwygDragDropHandler {
private initialBlockY: number = 0; private initialBlockY: number = 0;
private draggedBlockElement: HTMLElement | null = null; private draggedBlockElement: HTMLElement | null = null;
private draggedBlockHeight: number = 0; private draggedBlockHeight: number = 0;
private draggedBlockContentHeight: number = 0;
private draggedBlockMarginTop: number = 0;
private lastUpdateTime: number = 0; private lastUpdateTime: number = 0;
private updateThrottle: number = 80; // milliseconds private updateThrottle: number = 80; // milliseconds
@ -48,11 +50,33 @@ export class WysiwygDragDropHandler {
this.initialMouseY = e.clientY; this.initialMouseY = e.clientY;
this.draggedBlockElement = this.component.editorContentRef.querySelector(`[data-block-id="${block.id}"]`); this.draggedBlockElement = this.component.editorContentRef.querySelector(`[data-block-id="${block.id}"]`);
if (this.draggedBlockElement) { if (this.draggedBlockElement) {
// Get the wrapper rect for measurements
const rect = this.draggedBlockElement.getBoundingClientRect(); const rect = this.draggedBlockElement.getBoundingClientRect();
this.draggedBlockHeight = rect.height;
this.initialBlockY = rect.top; this.initialBlockY = rect.top;
// Get the inner block element for proper measurements
const innerBlock = this.draggedBlockElement.querySelector('.block');
if (innerBlock) {
const innerRect = innerBlock.getBoundingClientRect();
const computedStyle = window.getComputedStyle(innerBlock);
this.draggedBlockMarginTop = parseInt(computedStyle.marginTop) || 0;
this.draggedBlockContentHeight = innerRect.height;
}
// The drop indicator should match the wrapper height exactly
// The wrapper already includes all the space the block occupies
this.draggedBlockHeight = rect.height;
console.log('Drag measurements:', {
wrapperHeight: rect.height,
marginTop: this.draggedBlockMarginTop,
dropIndicatorHeight: this.draggedBlockHeight,
contentHeight: this.draggedBlockContentHeight,
blockId: block.id
});
// Create drop indicator // Create drop indicator
this.createDropIndicator(); this.createDropIndicator();
@ -98,6 +122,8 @@ export class WysiwygDragDropHandler {
this.dragOverPosition = null; this.dragOverPosition = null;
this.draggedBlockElement = null; this.draggedBlockElement = null;
this.draggedBlockHeight = 0; this.draggedBlockHeight = 0;
this.draggedBlockContentHeight = 0;
this.draggedBlockMarginTop = 0;
this.initialBlockY = 0; this.initialBlockY = 0;
// Update component state // Update component state
@ -284,34 +310,93 @@ export class WysiwygDragDropHandler {
if (!this.dropIndicator || !this.draggedBlockElement) return; if (!this.dropIndicator || !this.draggedBlockElement) return;
this.dropIndicator.style.display = 'block'; this.dropIndicator.style.display = 'block';
this.dropIndicator.style.height = `${this.draggedBlockHeight}px`;
const containerRect = this.component.editorContentRef.getBoundingClientRect(); const containerRect = this.component.editorContentRef.getBoundingClientRect();
// Calculate where the block will actually land
let topPosition = 0; let topPosition = 0;
if (targetIndex === 0) { // Build array of visual block positions (excluding dragged block)
// Before first block const visualBlocks: { index: number, top: number, bottom: number }[] = [];
topPosition = 0;
} else {
// After a specific block
const prevIndex = targetIndex - 1;
let blockCount = 0;
// Find the visual position of the block that will be before our dropped block
for (let i = 0; i < blocks.length; i++) { for (let i = 0; i < blocks.length; i++) {
if (i === draggedIndex) continue; // Skip the dragged block if (i === draggedIndex) continue; // Skip the dragged block
if (blockCount === prevIndex) { const block = blocks[i];
const rect = blocks[i].getBoundingClientRect(); const rect = block.getBoundingClientRect();
topPosition = rect.bottom - containerRect.top + 16; // 16px gap let top = rect.top - containerRect.top;
break; 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 }))
});
} }
/** /**

View File

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

View File

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

View File

@ -443,6 +443,32 @@ export const inputShowcase = () => html`
Specialized input components for specific data types like phone numbers, IBAN, and file uploads. Specialized input components for specific data types like phone numbers, IBAN, and file uploads.
</p> </p>
<dees-panel .title=${'Date & Time Picker'} .subtitle=${'Calendar-based date selection'}>
<div class="demo-grid">
<dees-input-datepicker
.label=${'Event Date'}
.placeholder=${'Select date'}
.description=${'Choose a date from the calendar'}
></dees-input-datepicker>
<dees-input-datepicker
.label=${'Appointment Time'}
.enableTime=${true}
.timeFormat=${'12h'}
.description=${'Date and time with AM/PM'}
></dees-input-datepicker>
<dees-input-datepicker
.label=${'Deadline'}
.enableTime=${true}
.timeFormat=${'24h'}
.minuteIncrement=${15}
.minDate=${new Date().toISOString()}
.description=${'Future dates only, 15 min increments'}
></dees-input-datepicker>
</div>
</dees-panel>
<dees-panel .title=${'Phone & IBAN'}> <dees-panel .title=${'Phone & IBAN'}>
<div class="demo-grid"> <div class="demo-grid">
<dees-input-phone <dees-input-phone
@ -474,6 +500,31 @@ export const inputShowcase = () => html`
.accept=${'image/*'} .accept=${'image/*'}
></dees-input-fileupload> ></dees-input-fileupload>
</dees-panel> </dees-panel>
<dees-panel .title=${'Profile Picture Input'} .subtitle=${'Image upload with cropping'}>
<div class="demo-grid">
<dees-input-profilepicture
.label=${'User Avatar'}
.description=${'Round profile picture'}
.shape=${'round'}
.size=${120}
></dees-input-profilepicture>
<dees-input-profilepicture
.label=${'Company Logo'}
.description=${'Square format'}
.shape=${'square'}
.size=${120}
></dees-input-profilepicture>
<dees-input-profilepicture
.label=${'Team Member'}
.description=${'Larger profile image'}
.shape=${'round'}
.size=${150}
></dees-input-profilepicture>
</div>
</dees-panel>
</section> </section>
<!-- Rich Editors Section --> <!-- Rich Editors Section -->